sequant 1.20.3 → 2.0.1

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 (137) hide show
  1. package/.claude-plugin/marketplace.json +2 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +36 -15
  4. package/dist/bin/cli.js +25 -2
  5. package/dist/src/commands/doctor.js +42 -9
  6. package/dist/src/commands/init.d.ts +1 -0
  7. package/dist/src/commands/init.js +52 -0
  8. package/dist/src/commands/logs.d.ts +1 -0
  9. package/dist/src/commands/logs.js +18 -2
  10. package/dist/src/commands/run.d.ts +7 -0
  11. package/dist/src/commands/run.js +235 -68
  12. package/dist/src/commands/serve.d.ts +13 -0
  13. package/dist/src/commands/serve.js +131 -0
  14. package/dist/src/commands/stats.d.ts +1 -0
  15. package/dist/src/commands/stats.js +185 -26
  16. package/dist/src/commands/status.d.ts +2 -0
  17. package/dist/src/commands/status.js +99 -50
  18. package/dist/src/index.d.ts +2 -2
  19. package/dist/src/index.js +4 -1
  20. package/dist/src/lib/ac-parser.d.ts +2 -0
  21. package/dist/src/lib/ac-parser.js +12 -2
  22. package/dist/src/lib/assess-comment-parser.d.ts +137 -0
  23. package/dist/src/lib/assess-comment-parser.js +344 -0
  24. package/dist/src/lib/ci/config.d.ts +22 -0
  25. package/dist/src/lib/ci/config.js +134 -0
  26. package/dist/src/lib/ci/index.d.ts +12 -0
  27. package/dist/src/lib/ci/index.js +10 -0
  28. package/dist/src/lib/ci/inputs.d.ts +29 -0
  29. package/dist/src/lib/ci/inputs.js +103 -0
  30. package/dist/src/lib/ci/labels.d.ts +34 -0
  31. package/dist/src/lib/ci/labels.js +101 -0
  32. package/dist/src/lib/ci/outputs.d.ts +25 -0
  33. package/dist/src/lib/ci/outputs.js +84 -0
  34. package/dist/src/lib/ci/triggers.d.ts +9 -0
  35. package/dist/src/lib/ci/triggers.js +86 -0
  36. package/dist/src/lib/ci/types.d.ts +131 -0
  37. package/dist/src/lib/ci/types.js +47 -0
  38. package/dist/src/lib/mcp-config.d.ts +54 -0
  39. package/dist/src/lib/mcp-config.js +172 -0
  40. package/dist/src/lib/merge-check/index.js +6 -12
  41. package/dist/src/lib/merge-check/types.d.ts +20 -7
  42. package/dist/src/lib/merge-check/types.js +11 -0
  43. package/dist/src/lib/phase-signal.d.ts +3 -3
  44. package/dist/src/lib/phase-signal.js +5 -3
  45. package/dist/src/lib/settings.d.ts +52 -0
  46. package/dist/src/lib/settings.js +41 -0
  47. package/dist/src/lib/shutdown.d.ts +16 -5
  48. package/dist/src/lib/shutdown.js +32 -12
  49. package/dist/src/lib/solve-comment-parser.d.ts +9 -102
  50. package/dist/src/lib/solve-comment-parser.js +13 -248
  51. package/dist/src/lib/stacks.d.ts +8 -0
  52. package/dist/src/lib/stacks.js +34 -0
  53. package/dist/src/lib/system.js +3 -7
  54. package/dist/src/lib/test-tautology-detector.d.ts +10 -0
  55. package/dist/src/lib/test-tautology-detector.js +43 -4
  56. package/dist/src/lib/upstream/assessment.js +9 -59
  57. package/dist/src/lib/upstream/issues.js +12 -75
  58. package/dist/src/lib/version-check.d.ts +2 -2
  59. package/dist/src/lib/version-check.js +6 -3
  60. package/dist/src/lib/version.d.ts +4 -0
  61. package/dist/src/lib/version.js +25 -0
  62. package/dist/src/lib/workflow/batch-executor.d.ts +26 -86
  63. package/dist/src/lib/workflow/batch-executor.js +269 -55
  64. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
  65. package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
  66. package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
  67. package/dist/src/lib/workflow/drivers/aider.js +160 -0
  68. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
  69. package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
  70. package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
  71. package/dist/src/lib/workflow/drivers/index.js +27 -0
  72. package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
  73. package/dist/src/lib/workflow/error-classifier.js +90 -0
  74. package/dist/src/lib/workflow/log-writer.d.ts +6 -3
  75. package/dist/src/lib/workflow/log-writer.js +57 -27
  76. package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
  77. package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
  78. package/dist/src/lib/workflow/phase-detection.js +45 -29
  79. package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
  80. package/dist/src/lib/workflow/phase-executor.js +375 -229
  81. package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
  82. package/dist/src/lib/workflow/phase-mapper.js +7 -7
  83. package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
  84. package/dist/src/lib/workflow/platforms/github.js +466 -0
  85. package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
  86. package/dist/src/lib/workflow/platforms/index.js +25 -0
  87. package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
  88. package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
  89. package/dist/src/lib/workflow/pr-status.d.ts +2 -4
  90. package/dist/src/lib/workflow/pr-status.js +3 -16
  91. package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
  92. package/dist/src/lib/workflow/qa-cache.js +88 -0
  93. package/dist/src/lib/workflow/reconcile.d.ts +69 -0
  94. package/dist/src/lib/workflow/reconcile.js +290 -0
  95. package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
  96. package/dist/src/lib/workflow/ring-buffer.js +37 -0
  97. package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
  98. package/dist/src/lib/workflow/run-log-schema.js +47 -12
  99. package/dist/src/lib/workflow/run-reflect.js +1 -1
  100. package/dist/src/lib/workflow/state-cleanup.js +21 -0
  101. package/dist/src/lib/workflow/state-manager.d.ts +34 -3
  102. package/dist/src/lib/workflow/state-manager.js +278 -126
  103. package/dist/src/lib/workflow/state-schema.d.ts +34 -30
  104. package/dist/src/lib/workflow/state-schema.js +35 -25
  105. package/dist/src/lib/workflow/state-utils.d.ts +3 -1
  106. package/dist/src/lib/workflow/state-utils.js +1 -0
  107. package/dist/src/lib/workflow/types.d.ts +224 -6
  108. package/dist/src/lib/workflow/types.js +20 -1
  109. package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
  110. package/dist/src/lib/workflow/worktree-discovery.js +6 -14
  111. package/dist/src/lib/workflow/worktree-manager.js +33 -51
  112. package/dist/src/mcp/index.d.ts +4 -0
  113. package/dist/src/mcp/index.js +4 -0
  114. package/dist/src/mcp/resources.d.ts +7 -0
  115. package/dist/src/mcp/resources.js +111 -0
  116. package/dist/src/mcp/run-registry.d.ts +34 -0
  117. package/dist/src/mcp/run-registry.js +42 -0
  118. package/dist/src/mcp/server.d.ts +12 -0
  119. package/dist/src/mcp/server.js +50 -0
  120. package/dist/src/mcp/tools/logs.d.ts +7 -0
  121. package/dist/src/mcp/tools/logs.js +149 -0
  122. package/dist/src/mcp/tools/run.d.ts +121 -0
  123. package/dist/src/mcp/tools/run.js +591 -0
  124. package/dist/src/mcp/tools/status.d.ts +7 -0
  125. package/dist/src/mcp/tools/status.js +127 -0
  126. package/package.json +26 -7
  127. package/templates/hooks/post-tool.sh +19 -8
  128. package/templates/hooks/pre-tool.sh +36 -49
  129. package/templates/mcp.json +6 -0
  130. package/templates/skills/assess/SKILL.md +354 -352
  131. package/templates/skills/exec/SKILL.md +64 -1
  132. package/templates/skills/fullsolve/SKILL.md +35 -4
  133. package/templates/skills/qa/SKILL.md +486 -9
  134. package/templates/skills/qa/scripts/quality-checks.sh +1 -1
  135. package/templates/skills/setup/SKILL.md +386 -0
  136. package/templates/skills/solve/SKILL.md +38 -664
  137. package/templates/skills/spec/SKILL.md +90 -31
@@ -10,11 +10,63 @@
10
10
  import chalk from "chalk";
11
11
  import { spawnSync } from "child_process";
12
12
  import { createPhaseLogFromTiming } from "./log-writer.js";
13
+ import { classifyError } from "./error-classifier.js";
13
14
  import { PhaseSpinner } from "../phase-spinner.js";
14
15
  import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
15
16
  import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
16
17
  import { executePhaseWithRetry } from "./phase-executor.js";
17
- import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, BUG_LABELS, } from "./phase-mapper.js";
18
+ import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, BUG_LABELS, DOCS_LABELS, } from "./phase-mapper.js";
19
+ /**
20
+ * Emit a structured progress line to stderr for MCP progress notifications.
21
+ * Only emits when running under an orchestrator (e.g., MCP server).
22
+ * The MCP handler parses these lines to send `notifications/progress`.
23
+ *
24
+ * @param issue - GitHub issue number
25
+ * @param phase - Phase name (e.g., "spec", "exec", "qa")
26
+ * @param event - Phase lifecycle event: "start", "complete", or "failed"
27
+ * @param extra - Optional fields: durationSeconds (on complete), error (on failed)
28
+ */
29
+ /**
30
+ * Build enriched prompt context for the /loop phase from a failed phase result (#488).
31
+ * Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
32
+ * to reconstruct context from GitHub comments (which fails in subprocess).
33
+ *
34
+ * @internal Exported for testing only
35
+ */
36
+ export function buildLoopContext(failedResult) {
37
+ const parts = [`Previous phase "${failedResult.phase}" failed.`];
38
+ if (failedResult.verdict) {
39
+ parts.push(`QA Verdict: ${failedResult.verdict}`);
40
+ }
41
+ if (failedResult.summary?.gaps?.length) {
42
+ parts.push(`QA Gaps:\n${failedResult.summary.gaps.map((gap) => `- ${gap}`).join("\n")}`);
43
+ }
44
+ if (failedResult.summary?.suggestions?.length) {
45
+ parts.push(`Suggestions:\n${failedResult.summary.suggestions.map((s) => `- ${s}`).join("\n")}`);
46
+ }
47
+ if (failedResult.error) {
48
+ parts.push(`Error: ${failedResult.error}`);
49
+ }
50
+ // Include tail of output for additional context (truncated to avoid prompt bloat)
51
+ if (failedResult.output) {
52
+ const tail = failedResult.output.slice(-2000);
53
+ parts.push(`Last output:\n${tail}`);
54
+ }
55
+ return parts.join("\n\n");
56
+ }
57
+ export function emitProgressLine(issue, phase, event = "start", extra) {
58
+ if (!process.env.SEQUANT_ORCHESTRATOR)
59
+ return;
60
+ const payload = { issue, phase, event };
61
+ if (extra?.durationSeconds !== undefined) {
62
+ payload.durationSeconds = extra.durationSeconds;
63
+ }
64
+ if (extra?.error !== undefined) {
65
+ payload.error = extra.error;
66
+ }
67
+ const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
68
+ process.stderr.write(line);
69
+ }
18
70
  export async function getIssueInfo(issueNumber) {
19
71
  try {
20
72
  const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
@@ -95,8 +147,8 @@ export function sortByDependencies(issueNumbers) {
95
147
  }
96
148
  }
97
149
  // Note: inDegree counts how many issues depend on each issue
98
- // We want to process issues that nothing depends on last
99
- // So we sort by: issues nothing depends on first, then dependent issues
150
+ // We want to process issues that have no dependencies first,
151
+ // so dependent issues come after their prerequisites
100
152
  const sorted = [];
101
153
  const queue = [];
102
154
  // Start with issues that have no dependencies
@@ -163,7 +215,8 @@ export function getEnvConfig() {
163
215
  }
164
216
  return config;
165
217
  }
166
- export async function executeBatch(issueNumbers, config, logWriter, stateManager, options, issueInfoMap, worktreeMap, shutdownManager, packageManager, baseBranch) {
218
+ export async function executeBatch(issueNumbers, batchCtx) {
219
+ const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
167
220
  const results = [];
168
221
  for (const issueNumber of issueNumbers) {
169
222
  // Check if shutdown was triggered
@@ -179,8 +232,21 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
179
232
  if (logWriter) {
180
233
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
181
234
  }
182
- const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, options, worktreeInfo?.path, worktreeInfo?.branch, shutdownManager, false, // Batch mode doesn't support chain
183
- packageManager, undefined, baseBranch);
235
+ const ctx = {
236
+ issueNumber,
237
+ title: issueInfo.title,
238
+ labels: issueInfo.labels,
239
+ config,
240
+ options,
241
+ services: { logWriter, stateManager, shutdownManager },
242
+ worktree: worktreeInfo
243
+ ? { path: worktreeInfo.path, branch: worktreeInfo.branch }
244
+ : undefined,
245
+ packageManager,
246
+ baseBranch,
247
+ onProgress,
248
+ };
249
+ const result = await runIssueWithLogging(ctx);
184
250
  results.push(result);
185
251
  // Record PR info in log before completing issue
186
252
  if (logWriter && result.prNumber && result.prUrl) {
@@ -193,14 +259,23 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
193
259
  }
194
260
  return results;
195
261
  }
196
- export async function runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueTitle, labels, options, worktreePath, branch, shutdownManager, chainMode, packageManager, isLastInChain, baseBranch) {
262
+ export async function runIssueWithLogging(ctx) {
263
+ // Destructure context for use throughout the function
264
+ const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, } = ctx;
265
+ const worktreePath = worktree?.path;
266
+ const branch = worktree?.branch;
267
+ const chainMode = chain?.enabled;
268
+ const isLastInChain = chain?.isLast;
197
269
  const startTime = Date.now();
198
270
  const phaseResults = [];
199
271
  let loopTriggered = false;
200
272
  let sessionId;
201
- console.log(chalk.blue(`\n Issue #${issueNumber}`));
273
+ // In parallel mode, suppress per-issue terminal output to prevent interleaving.
274
+ // The caller (run.ts) handles progress display via updateProgress().
275
+ const log = config.parallel ? () => { } : console.log.bind(console);
276
+ log(chalk.blue(`\n Issue #${issueNumber}`));
202
277
  if (worktreePath) {
203
- console.log(chalk.gray(` Worktree: ${worktreePath}`));
278
+ log(chalk.gray(` Worktree: ${worktreePath}`));
204
279
  }
205
280
  // Initialize state tracking for this issue
206
281
  if (stateManager) {
@@ -224,7 +299,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
224
299
  catch (error) {
225
300
  // State tracking errors shouldn't stop execution
226
301
  if (config.verbose) {
227
- console.log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
302
+ log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
228
303
  }
229
304
  }
230
305
  }
@@ -235,23 +310,39 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
235
310
  if (options.autoDetectPhases) {
236
311
  // Check if labels indicate a simple bug/fix (skip spec entirely)
237
312
  const lowerLabels = labels.map((l) => l.toLowerCase());
238
- const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label.includes(bugLabel)));
313
+ const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label === bugLabel));
314
+ // Check if labels indicate documentation-only work (skip spec)
315
+ const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
239
316
  if (isSimpleBugFix) {
240
317
  // Simple bug fix: skip spec, go straight to exec → qa
241
318
  phases = ["exec", "qa"];
242
- console.log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
319
+ log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
320
+ }
321
+ else if (isDocs) {
322
+ // Documentation issue: skip spec, lighter pipeline
323
+ phases = ["exec", "qa"];
324
+ log(chalk.gray(` Docs issue detected: ${phases.join(" → ")}`));
243
325
  }
244
326
  else {
245
327
  // Run spec first to get recommended workflow
246
- console.log(chalk.gray(` Running spec to determine workflow...`));
247
- // Create spinner for spec phase (1 of estimated 3: spec, exec, qa)
248
- const specSpinner = new PhaseSpinner({
249
- phase: "spec",
250
- phaseIndex: 1,
251
- totalPhases: 3, // Estimate; will be refined after spec
252
- shutdownManager,
253
- });
254
- specSpinner.start();
328
+ log(chalk.gray(` Running spec to determine workflow...`));
329
+ // Create spinner for spec phase (suppressed in parallel mode to prevent interleaving)
330
+ const specSpinner = config.parallel
331
+ ? undefined
332
+ : new PhaseSpinner({
333
+ phase: "spec",
334
+ phaseIndex: 1,
335
+ totalPhases: 3, // Estimate; will be refined after spec
336
+ shutdownManager,
337
+ });
338
+ specSpinner?.start();
339
+ emitProgressLine(issueNumber, "spec", "start");
340
+ try {
341
+ onProgress?.(issueNumber, "spec", "start");
342
+ }
343
+ catch {
344
+ /* progress errors must not halt */
345
+ }
255
346
  // Track spec phase start in state
256
347
  if (stateManager) {
257
348
  try {
@@ -280,14 +371,46 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
280
371
  }
281
372
  phaseResults.push(specResult);
282
373
  specAlreadyRan = true;
374
+ // Emit completion/failure progress event (AC-8)
375
+ const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
376
+ if (specResult.success) {
377
+ const extra = { durationSeconds: specDurationSec };
378
+ emitProgressLine(issueNumber, "spec", "complete", extra);
379
+ try {
380
+ onProgress?.(issueNumber, "spec", "complete", extra);
381
+ }
382
+ catch {
383
+ /* progress errors must not halt */
384
+ }
385
+ }
386
+ else {
387
+ const extra = { error: specResult.error ?? "unknown" };
388
+ emitProgressLine(issueNumber, "spec", "failed", extra);
389
+ try {
390
+ onProgress?.(issueNumber, "spec", "failed", extra);
391
+ }
392
+ catch {
393
+ /* progress errors must not halt */
394
+ }
395
+ }
283
396
  // Log spec phase result
284
397
  // Note: Spec runs in main repo, not worktree, so no git diff stats
285
398
  if (logWriter) {
399
+ // Build errorContext from captured stderr/stdout tails (#447)
400
+ let specErrorContext;
401
+ if (!specResult.success && specResult.stderrTail) {
402
+ specErrorContext = {
403
+ stderrTail: specResult.stderrTail ?? [],
404
+ stdoutTail: specResult.stdoutTail ?? [],
405
+ exitCode: specResult.exitCode,
406
+ category: classifyError(specResult.stderrTail ?? []),
407
+ };
408
+ }
286
409
  const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
287
410
  ? "success"
288
411
  : specResult.error?.includes("Timeout")
289
412
  ? "timeout"
290
- : "failure", { error: specResult.error });
413
+ : "failure", { error: specResult.error, errorContext: specErrorContext });
291
414
  logWriter.logPhase(phaseLog);
292
415
  }
293
416
  // Track spec phase completion in state
@@ -303,7 +426,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
303
426
  }
304
427
  }
305
428
  if (!specResult.success) {
306
- specSpinner.fail(specResult.error);
429
+ specSpinner?.fail(specResult.error);
307
430
  const durationSeconds = (Date.now() - startTime) / 1000;
308
431
  return {
309
432
  issueNumber,
@@ -313,7 +436,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
313
436
  loopTriggered: false,
314
437
  };
315
438
  }
316
- specSpinner.succeed();
439
+ specSpinner?.succeed();
317
440
  // Parse recommended workflow from spec output
318
441
  const parsedWorkflow = specResult.output
319
442
  ? parseRecommendedWorkflow(specResult.output)
@@ -322,15 +445,15 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
322
445
  // Remove spec from phases since we already ran it
323
446
  phases = parsedWorkflow.phases.filter((p) => p !== "spec");
324
447
  detectedQualityLoop = parsedWorkflow.qualityLoop;
325
- console.log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
448
+ log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
326
449
  }
327
450
  else {
328
451
  // Fall back to label-based detection
329
- console.log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
452
+ log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
330
453
  const detected = detectPhasesFromLabels(labels);
331
454
  phases = detected.phases.filter((p) => p !== "spec");
332
455
  detectedQualityLoop = detected.qualityLoop;
333
- console.log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
456
+ log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
334
457
  }
335
458
  }
336
459
  }
@@ -338,21 +461,21 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
338
461
  // Use explicit phases with adjustments
339
462
  phases = determinePhasesForIssue(config.phases, labels, options);
340
463
  if (phases.length !== config.phases.length) {
341
- console.log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
464
+ log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
342
465
  }
343
466
  }
344
467
  // Resume: filter out completed phases if --resume flag is set
345
468
  if (options.resume) {
346
469
  const resumeResult = filterResumedPhases(issueNumber, phases, true);
347
470
  if (resumeResult.skipped.length > 0) {
348
- console.log(chalk.gray(` Resume: skipping completed phases: ${resumeResult.skipped.join(", ")}`));
471
+ log(chalk.gray(` Resume: skipping completed phases: ${resumeResult.skipped.join(", ")}`));
349
472
  phases = resumeResult.phases;
350
473
  }
351
474
  // Also skip spec if it was auto-detected as completed
352
475
  if (specAlreadyRan &&
353
476
  resumeResult.skipped.length === 0 &&
354
477
  resumeResult.phases.length === 0) {
355
- console.log(chalk.gray(` Resume: all phases already completed`));
478
+ log(chalk.gray(` Resume: all phases already completed`));
356
479
  }
357
480
  }
358
481
  // Add testgen phase if requested (and spec was in the phases)
@@ -370,6 +493,12 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
370
493
  }
371
494
  }
372
495
  }
496
+ // Build per-issue config with issue type metadata for skill env propagation
497
+ const lowerLabelsForType = labels.map((l) => l.toLowerCase());
498
+ const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
499
+ const issueConfig = issueIsDocs
500
+ ? { ...config, issueType: "docs" }
501
+ : config;
373
502
  let iteration = 0;
374
503
  const useQualityLoop = config.qualityLoop || detectedQualityLoop;
375
504
  const maxIterations = useQualityLoop ? config.maxIterations : 1;
@@ -377,7 +506,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
377
506
  while (iteration < maxIterations) {
378
507
  iteration++;
379
508
  if (useQualityLoop && iteration > 1) {
380
- console.log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
509
+ log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
381
510
  loopTriggered = true;
382
511
  }
383
512
  let phasesFailed = false;
@@ -388,15 +517,24 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
388
517
  for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
389
518
  const phase = phases[phaseIdx];
390
519
  const phaseNumber = phaseIdx + 1 + phaseIndexOffset;
391
- // Create spinner for this phase
392
- const phaseSpinner = new PhaseSpinner({
393
- phase,
394
- phaseIndex: phaseNumber,
395
- totalPhases,
396
- shutdownManager,
397
- iteration: useQualityLoop ? iteration : undefined,
398
- });
399
- phaseSpinner.start();
520
+ // Create spinner for this phase (suppressed in parallel mode)
521
+ const phaseSpinner = config.parallel
522
+ ? undefined
523
+ : new PhaseSpinner({
524
+ phase,
525
+ phaseIndex: phaseNumber,
526
+ totalPhases,
527
+ shutdownManager,
528
+ iteration: useQualityLoop ? iteration : undefined,
529
+ });
530
+ phaseSpinner?.start();
531
+ emitProgressLine(issueNumber, phase, "start");
532
+ try {
533
+ onProgress?.(issueNumber, phase, "start");
534
+ }
535
+ catch {
536
+ /* progress errors must not halt */
537
+ }
400
538
  // Track phase start in state
401
539
  if (stateManager) {
402
540
  try {
@@ -407,7 +545,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
407
545
  }
408
546
  }
409
547
  const phaseStartTime = new Date();
410
- const result = await executePhaseWithRetry(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, phaseSpinner);
548
+ const result = await executePhaseWithRetry(issueNumber, phase, issueConfig, sessionId, worktreePath, shutdownManager, phaseSpinner);
411
549
  const phaseEndTime = new Date();
412
550
  // Capture session ID for subsequent phases
413
551
  if (result.sessionId) {
@@ -423,6 +561,28 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
423
561
  }
424
562
  }
425
563
  phaseResults.push(result);
564
+ // Emit completion/failure progress event (AC-8)
565
+ const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
566
+ if (result.success) {
567
+ const extra = { durationSeconds: phaseDurationSec };
568
+ emitProgressLine(issueNumber, phase, "complete", extra);
569
+ try {
570
+ onProgress?.(issueNumber, phase, "complete", extra);
571
+ }
572
+ catch {
573
+ /* progress errors must not halt */
574
+ }
575
+ }
576
+ else {
577
+ const extra = { error: result.error ?? "unknown" };
578
+ emitProgressLine(issueNumber, phase, "failed", extra);
579
+ try {
580
+ onProgress?.(issueNumber, phase, "failed", extra);
581
+ }
582
+ catch {
583
+ /* progress errors must not halt */
584
+ }
585
+ }
426
586
  // Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
427
587
  if (logWriter) {
428
588
  // Capture git diff stats for worktree phases (AC-1, AC-3)
@@ -435,6 +595,16 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
435
595
  : undefined;
436
596
  // Read cache metrics for QA phase (AC-7)
437
597
  const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
598
+ // Build errorContext from captured stderr/stdout tails (#447)
599
+ let errorContext;
600
+ if (!result.success && result.stderrTail) {
601
+ errorContext = {
602
+ stderrTail: result.stderrTail ?? [],
603
+ stdoutTail: result.stdoutTail ?? [],
604
+ exitCode: result.exitCode,
605
+ category: classifyError(result.stderrTail ?? []),
606
+ };
607
+ }
438
608
  const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
439
609
  ? "success"
440
610
  : result.error?.includes("Timeout")
@@ -442,11 +612,13 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
442
612
  : "failure", {
443
613
  error: result.error,
444
614
  verdict: result.verdict,
615
+ summary: result.summary,
445
616
  // Observability fields (AC-1, AC-2, AC-3, AC-7)
446
617
  filesModified: diffStats?.filesModified,
447
618
  fileDiffStats: diffStats?.fileDiffStats,
448
619
  commitHash,
449
620
  cacheMetrics,
621
+ errorContext,
450
622
  });
451
623
  logWriter.logPhase(phaseLog);
452
624
  }
@@ -465,34 +637,76 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
465
637
  }
466
638
  }
467
639
  if (result.success) {
468
- phaseSpinner.succeed();
640
+ phaseSpinner?.succeed();
469
641
  }
470
642
  else {
471
- phaseSpinner.fail(result.error);
643
+ phaseSpinner?.fail(result.error);
472
644
  phasesFailed = true;
473
645
  // If quality loop enabled, run loop phase to fix issues
474
646
  if (useQualityLoop && iteration < maxIterations) {
475
- // Create spinner for loop phase
476
- const loopSpinner = new PhaseSpinner({
477
- phase: "loop",
478
- phaseIndex: phaseNumber,
479
- totalPhases,
480
- shutdownManager,
481
- iteration,
482
- });
483
- loopSpinner.start();
484
- const loopResult = await executePhaseWithRetry(issueNumber, "loop", config, sessionId, worktreePath, shutdownManager, loopSpinner);
647
+ // Create spinner for loop phase (suppressed in parallel mode)
648
+ const loopSpinner = config.parallel
649
+ ? undefined
650
+ : new PhaseSpinner({
651
+ phase: "loop",
652
+ phaseIndex: phaseNumber,
653
+ totalPhases,
654
+ shutdownManager,
655
+ iteration,
656
+ });
657
+ loopSpinner?.start();
658
+ emitProgressLine(issueNumber, "loop", "start");
659
+ try {
660
+ onProgress?.(issueNumber, "loop", "start");
661
+ }
662
+ catch {
663
+ /* progress errors must not halt */
664
+ }
665
+ // Build enriched config for loop phase with QA context (#488).
666
+ // Pass verdict, failed ACs, and error directly so the /loop skill
667
+ // doesn't need to reconstruct context from GitHub comments.
668
+ const loopConfig = {
669
+ ...issueConfig,
670
+ lastVerdict: result.verdict ?? undefined,
671
+ failedAcs: result.summary?.gaps?.join("; ") ?? undefined,
672
+ promptContext: buildLoopContext(result),
673
+ };
674
+ const loopStartTime = new Date();
675
+ const loopResult = await executePhaseWithRetry(issueNumber, "loop", loopConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
676
+ const loopEndTime = new Date();
485
677
  phaseResults.push(loopResult);
678
+ // Emit loop completion/failure progress event (AC-8)
679
+ const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
680
+ if (loopResult.success) {
681
+ const extra = { durationSeconds: loopDurationSec };
682
+ emitProgressLine(issueNumber, "loop", "complete", extra);
683
+ try {
684
+ onProgress?.(issueNumber, "loop", "complete", extra);
685
+ }
686
+ catch {
687
+ /* progress errors must not halt */
688
+ }
689
+ }
690
+ else {
691
+ const extra = { error: loopResult.error ?? "unknown" };
692
+ emitProgressLine(issueNumber, "loop", "failed", extra);
693
+ try {
694
+ onProgress?.(issueNumber, "loop", "failed", extra);
695
+ }
696
+ catch {
697
+ /* progress errors must not halt */
698
+ }
699
+ }
486
700
  if (loopResult.sessionId) {
487
701
  sessionId = loopResult.sessionId;
488
702
  }
489
703
  if (loopResult.success) {
490
- loopSpinner.succeed();
704
+ loopSpinner?.succeed();
491
705
  // Continue to next iteration
492
706
  break;
493
707
  }
494
708
  else {
495
- loopSpinner.fail(loopResult.error);
709
+ loopSpinner?.fail(loopResult.error);
496
710
  }
497
711
  }
498
712
  // Stop on first failure (if not in quality loop or loop failed)
@@ -0,0 +1,56 @@
1
+ /**
2
+ * AgentDriver interface — decouples workflow orchestration from agent execution.
3
+ *
4
+ * Claude Code is the default implementation; alternatives (Aider, Codex CLI,
5
+ * Continue.dev, Copilot SDK, Cursor API) can be added by implementing this
6
+ * interface without touching orchestration logic.
7
+ */
8
+ /**
9
+ * Configuration passed to an agent for phase execution.
10
+ */
11
+ export interface AgentExecutionConfig {
12
+ cwd: string;
13
+ env: Record<string, string>;
14
+ abortSignal?: AbortSignal;
15
+ phaseTimeout: number;
16
+ verbose: boolean;
17
+ mcp: boolean;
18
+ /** Resume a previous session (driver-specific; ignored if unsupported) */
19
+ sessionId?: string;
20
+ /** Callback for streaming output */
21
+ onOutput?: (text: string) => void;
22
+ /** Callback for stderr */
23
+ onStderr?: (text: string) => void;
24
+ /** Relevant files for the phase (used by file-oriented drivers like Aider) */
25
+ files?: string[];
26
+ }
27
+ /**
28
+ * Result returned by an agent after executing a phase.
29
+ */
30
+ export interface AgentPhaseResult {
31
+ success: boolean;
32
+ output: string;
33
+ sessionId?: string;
34
+ error?: string;
35
+ /** Last N lines of stderr captured via RingBuffer (#447) */
36
+ stderrTail?: string[];
37
+ /** Last N lines of stdout captured via RingBuffer (#447) */
38
+ stdoutTail?: string[];
39
+ /** Process exit code (undefined for SDK-based drivers) (#447) */
40
+ exitCode?: number;
41
+ }
42
+ /**
43
+ * Interface that all agent backends must implement.
44
+ *
45
+ * The driver is responsible for executing a prompt and returning
46
+ * a structured result. Parsing (QA verdicts, etc.) stays in
47
+ * phase-executor.ts — the driver just captures text.
48
+ */
49
+ export interface AgentDriver {
50
+ /** Human-readable name for logging */
51
+ name: string;
52
+ /** Execute a phase prompt and return structured result */
53
+ executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
54
+ /** Check if this driver is available/configured */
55
+ isAvailable(): Promise<boolean>;
56
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * AgentDriver interface — decouples workflow orchestration from agent execution.
3
+ *
4
+ * Claude Code is the default implementation; alternatives (Aider, Codex CLI,
5
+ * Continue.dev, Copilot SDK, Cursor API) can be added by implementing this
6
+ * interface without touching orchestration logic.
7
+ */
8
+ export {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * AiderDriver — AgentDriver implementation wrapping the Aider CLI.
3
+ *
4
+ * Shells out to `aider --yes --no-auto-commits --message "<prompt>"`
5
+ * for fully non-interactive phase execution. Sequant manages git,
6
+ * not Aider.
7
+ */
8
+ import type { AgentDriver, AgentExecutionConfig, AgentPhaseResult } from "./agent-driver.js";
9
+ import type { AiderSettings } from "../../settings.js";
10
+ export declare class AiderDriver implements AgentDriver {
11
+ name: string;
12
+ private settings?;
13
+ constructor(settings?: AiderSettings);
14
+ executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
15
+ isAvailable(): Promise<boolean>;
16
+ /** Build the CLI argument list for aider. */
17
+ private buildArgs;
18
+ }