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.
Files changed (64) hide show
  1. package/dist/bin/cli.js +21 -3
  2. package/dist/bin/cli.js.map +1 -1
  3. package/dist/src/commands/doctor.d.ts.map +1 -1
  4. package/dist/src/commands/doctor.js +1 -25
  5. package/dist/src/commands/doctor.js.map +1 -1
  6. package/dist/src/commands/doctor.test.js +28 -97
  7. package/dist/src/commands/doctor.test.js.map +1 -1
  8. package/dist/src/commands/init.d.ts.map +1 -1
  9. package/dist/src/commands/init.js +3 -27
  10. package/dist/src/commands/init.js.map +1 -1
  11. package/dist/src/commands/init.test.js +32 -75
  12. package/dist/src/commands/init.test.js.map +1 -1
  13. package/dist/src/commands/logs.d.ts +18 -0
  14. package/dist/src/commands/logs.d.ts.map +1 -0
  15. package/dist/src/commands/logs.js +188 -0
  16. package/dist/src/commands/logs.js.map +1 -0
  17. package/dist/src/commands/run.d.ts +10 -1
  18. package/dist/src/commands/run.d.ts.map +1 -1
  19. package/dist/src/commands/run.js +429 -98
  20. package/dist/src/commands/run.js.map +1 -1
  21. package/dist/src/commands/update.d.ts.map +1 -1
  22. package/dist/src/commands/update.js +16 -0
  23. package/dist/src/commands/update.js.map +1 -1
  24. package/dist/src/lib/system.d.ts +16 -0
  25. package/dist/src/lib/system.d.ts.map +1 -0
  26. package/dist/src/lib/system.js +52 -0
  27. package/dist/src/lib/system.js.map +1 -0
  28. package/dist/src/lib/system.test.d.ts +2 -0
  29. package/dist/src/lib/system.test.d.ts.map +1 -0
  30. package/dist/src/lib/system.test.js +80 -0
  31. package/dist/src/lib/system.test.js.map +1 -0
  32. package/dist/src/lib/workflow/log-writer.d.ts +83 -0
  33. package/dist/src/lib/workflow/log-writer.d.ts.map +1 -0
  34. package/dist/src/lib/workflow/log-writer.js +193 -0
  35. package/dist/src/lib/workflow/log-writer.js.map +1 -0
  36. package/dist/src/lib/workflow/log-writer.test.d.ts +7 -0
  37. package/dist/src/lib/workflow/log-writer.test.d.ts.map +1 -0
  38. package/dist/src/lib/workflow/log-writer.test.js +451 -0
  39. package/dist/src/lib/workflow/log-writer.test.js.map +1 -0
  40. package/dist/src/lib/workflow/run-log-schema.d.ts +261 -0
  41. package/dist/src/lib/workflow/run-log-schema.d.ts.map +1 -0
  42. package/dist/src/lib/workflow/run-log-schema.js +234 -0
  43. package/dist/src/lib/workflow/run-log-schema.js.map +1 -0
  44. package/dist/src/lib/workflow/run-log-schema.test.d.ts +2 -0
  45. package/dist/src/lib/workflow/run-log-schema.test.d.ts.map +1 -0
  46. package/dist/src/lib/workflow/run-log-schema.test.js +455 -0
  47. package/dist/src/lib/workflow/run-log-schema.test.js.map +1 -0
  48. package/package.json +4 -2
  49. package/templates/hooks/pre-tool.sh +14 -2
  50. package/templates/scripts/cleanup-worktree.sh +23 -1
  51. package/templates/skills/assess/SKILL.md +15 -0
  52. package/templates/skills/clean/SKILL.md +15 -0
  53. package/templates/skills/docs/SKILL.md +16 -0
  54. package/templates/skills/exec/SKILL.md +32 -0
  55. package/templates/skills/fullsolve/SKILL.md +42 -0
  56. package/templates/skills/loop/SKILL.md +14 -0
  57. package/templates/skills/qa/SKILL.md +67 -0
  58. package/templates/skills/reflect/SKILL.md +14 -0
  59. package/templates/skills/security-review/SKILL.md +15 -0
  60. package/templates/skills/solve/SKILL.md +44 -0
  61. package/templates/skills/spec/SKILL.md +59 -0
  62. package/templates/skills/test/SKILL.md +14 -0
  63. package/templates/skills/testgen/SKILL.md +15 -0
  64. package/templates/skills/verify/SKILL.md +15 -0
@@ -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 { 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 { LogWriter, createPhaseLogFromTiming, } from "../lib/workflow/log-writer.js";
12
+ import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
9
13
  /**
10
- * Check if claude CLI is available
14
+ * Natural language prompts for each phase
15
+ * These prompts will invoke the corresponding skills via natural language
11
16
  */
12
- function checkClaudeCli() {
13
- const result = spawnSync("claude", ["--version"], {
14
- stdio: "pipe",
15
- shell: true,
16
- });
17
- return result.status === 0;
18
- }
19
- 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"];
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
- * Execute a single phase for an issue using claude CLI
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
- // Execute claude CLI with the skill
48
- return new Promise((resolve) => {
49
- const command = `/${phase} ${issueNumber}`;
50
- const timeout = config.phaseTimeout * 1000;
51
- if (config.verbose) {
52
- console.log(chalk.gray(` Executing: ${command}`));
53
- }
54
- const proc = spawn("claude", ["--print", "--dangerously-skip-permissions", "-p", command], {
55
- stdio: config.verbose ? "inherit" : "pipe",
56
- shell: true,
57
- 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
+ },
58
97
  });
59
- let killed = false;
60
- const timer = setTimeout(() => {
61
- killed = true;
62
- proc.kill("SIGTERM");
63
- }, timeout);
64
- proc.on("close", (code) => {
65
- clearTimeout(timer);
66
- const durationSeconds = (Date.now() - startTime) / 1000;
67
- if (killed) {
68
- resolve({
69
- phase,
70
- success: false,
71
- durationSeconds,
72
- error: `Timeout after ${config.phaseTimeout}s`,
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
- else if (code === 0) {
76
- resolve({
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
- 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 {
84
150
  phase,
85
151
  success: false,
86
152
  durationSeconds,
87
- error: `Exit code ${code}`,
88
- });
153
+ error: lastError,
154
+ sessionId: resultSessionId,
155
+ };
89
156
  }
90
- });
91
- proc.on("error", (err) => {
92
- clearTimeout(timer);
93
- const durationSeconds = (Date.now() - startTime) / 1000;
94
- 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 {
95
173
  phase,
96
174
  success: false,
97
175
  durationSeconds,
98
- error: err.message,
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
- * Execute all phases for a single issue
188
+ * Fetch issue info from GitHub
105
189
  */
106
- async function runIssue(issueNumber, config) {
107
- const startTime = Date.now();
108
- const phaseResults = [];
109
- console.log(chalk.blue(`\n Issue #${issueNumber}`));
110
- for (const phase of config.phases) {
111
- console.log(chalk.gray(` ⏳ ${phase}...`));
112
- const result = await executePhase(issueNumber, phase, config);
113
- phaseResults.push(result);
114
- if (result.success) {
115
- const duration = result.durationSeconds
116
- ? ` (${formatDuration(result.durationSeconds)})`
117
- : "";
118
- console.log(chalk.green(` ✓ ${phase}${duration}`));
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
- console.log(chalk.red(` ✗ ${phase}: ${result.error}`));
122
- // Stop on first failure
123
- break;
241
+ phases.push("test");
124
242
  }
125
243
  }
126
- const durationSeconds = (Date.now() - startTime) / 1000;
127
- const success = phaseResults.every((r) => r.success);
128
- return {
129
- issueNumber,
130
- success,
131
- phaseResults,
132
- durationSeconds,
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
- // Check if claude CLI is available (skip for dry-run)
147
- if (!options.dryRun && !checkClaudeCli()) {
148
- console.log(chalk.red("❌ Claude CLI not found. Install it from https://claude.ai/code"));
149
- console.log(chalk.gray(" Or use --dry-run to preview without execution."));
150
- 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));
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: options.phases
166
- ? options.phases.split(",").map((p) => p.trim())
312
+ phases: mergedOptions.phases
313
+ ? mergedOptions.phases.split(",").map((p) => p.trim())
167
314
  : DEFAULT_PHASES,
168
- sequential: options.sequential ?? false,
169
- dryRun: options.dryRun ?? false,
170
- verbose: options.verbose ?? false,
171
- 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,
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 (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) {
184
376
  // Sequential execution
185
377
  for (const issueNumber of issueNumbers) {
186
- const result = await runIssue(issueNumber, config);
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 result = await runIssue(issueNumber, config);
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
- console.log(` ${status} #${result.issueNumber}: ${phases}${duration}`);
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