sequant 1.20.3 → 2.0.0

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 +29 -9
  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 +18 -86
  63. package/dist/src/lib/workflow/batch-executor.js +232 -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 +340 -220
  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 +208 -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 +10 -1
  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,35 @@
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
+ export function emitProgressLine(issue, phase, event = "start", extra) {
30
+ if (!process.env.SEQUANT_ORCHESTRATOR)
31
+ return;
32
+ const payload = { issue, phase, event };
33
+ if (extra?.durationSeconds !== undefined) {
34
+ payload.durationSeconds = extra.durationSeconds;
35
+ }
36
+ if (extra?.error !== undefined) {
37
+ payload.error = extra.error;
38
+ }
39
+ const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
40
+ process.stderr.write(line);
41
+ }
18
42
  export async function getIssueInfo(issueNumber) {
19
43
  try {
20
44
  const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
@@ -95,8 +119,8 @@ export function sortByDependencies(issueNumbers) {
95
119
  }
96
120
  }
97
121
  // 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
122
+ // We want to process issues that have no dependencies first,
123
+ // so dependent issues come after their prerequisites
100
124
  const sorted = [];
101
125
  const queue = [];
102
126
  // Start with issues that have no dependencies
@@ -163,7 +187,8 @@ export function getEnvConfig() {
163
187
  }
164
188
  return config;
165
189
  }
166
- export async function executeBatch(issueNumbers, config, logWriter, stateManager, options, issueInfoMap, worktreeMap, shutdownManager, packageManager, baseBranch) {
190
+ export async function executeBatch(issueNumbers, batchCtx) {
191
+ const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
167
192
  const results = [];
168
193
  for (const issueNumber of issueNumbers) {
169
194
  // Check if shutdown was triggered
@@ -179,8 +204,21 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
179
204
  if (logWriter) {
180
205
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
181
206
  }
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);
207
+ const ctx = {
208
+ issueNumber,
209
+ title: issueInfo.title,
210
+ labels: issueInfo.labels,
211
+ config,
212
+ options,
213
+ services: { logWriter, stateManager, shutdownManager },
214
+ worktree: worktreeInfo
215
+ ? { path: worktreeInfo.path, branch: worktreeInfo.branch }
216
+ : undefined,
217
+ packageManager,
218
+ baseBranch,
219
+ onProgress,
220
+ };
221
+ const result = await runIssueWithLogging(ctx);
184
222
  results.push(result);
185
223
  // Record PR info in log before completing issue
186
224
  if (logWriter && result.prNumber && result.prUrl) {
@@ -193,14 +231,23 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
193
231
  }
194
232
  return results;
195
233
  }
196
- export async function runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueTitle, labels, options, worktreePath, branch, shutdownManager, chainMode, packageManager, isLastInChain, baseBranch) {
234
+ export async function runIssueWithLogging(ctx) {
235
+ // Destructure context for use throughout the function
236
+ const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, } = ctx;
237
+ const worktreePath = worktree?.path;
238
+ const branch = worktree?.branch;
239
+ const chainMode = chain?.enabled;
240
+ const isLastInChain = chain?.isLast;
197
241
  const startTime = Date.now();
198
242
  const phaseResults = [];
199
243
  let loopTriggered = false;
200
244
  let sessionId;
201
- console.log(chalk.blue(`\n Issue #${issueNumber}`));
245
+ // In parallel mode, suppress per-issue terminal output to prevent interleaving.
246
+ // The caller (run.ts) handles progress display via updateProgress().
247
+ const log = config.parallel ? () => { } : console.log.bind(console);
248
+ log(chalk.blue(`\n Issue #${issueNumber}`));
202
249
  if (worktreePath) {
203
- console.log(chalk.gray(` Worktree: ${worktreePath}`));
250
+ log(chalk.gray(` Worktree: ${worktreePath}`));
204
251
  }
205
252
  // Initialize state tracking for this issue
206
253
  if (stateManager) {
@@ -224,7 +271,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
224
271
  catch (error) {
225
272
  // State tracking errors shouldn't stop execution
226
273
  if (config.verbose) {
227
- console.log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
274
+ log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
228
275
  }
229
276
  }
230
277
  }
@@ -235,23 +282,39 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
235
282
  if (options.autoDetectPhases) {
236
283
  // Check if labels indicate a simple bug/fix (skip spec entirely)
237
284
  const lowerLabels = labels.map((l) => l.toLowerCase());
238
- const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label.includes(bugLabel)));
285
+ const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label === bugLabel));
286
+ // Check if labels indicate documentation-only work (skip spec)
287
+ const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
239
288
  if (isSimpleBugFix) {
240
289
  // Simple bug fix: skip spec, go straight to exec → qa
241
290
  phases = ["exec", "qa"];
242
- console.log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
291
+ log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
292
+ }
293
+ else if (isDocs) {
294
+ // Documentation issue: skip spec, lighter pipeline
295
+ phases = ["exec", "qa"];
296
+ log(chalk.gray(` Docs issue detected: ${phases.join(" → ")}`));
243
297
  }
244
298
  else {
245
299
  // 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();
300
+ log(chalk.gray(` Running spec to determine workflow...`));
301
+ // Create spinner for spec phase (suppressed in parallel mode to prevent interleaving)
302
+ const specSpinner = config.parallel
303
+ ? undefined
304
+ : new PhaseSpinner({
305
+ phase: "spec",
306
+ phaseIndex: 1,
307
+ totalPhases: 3, // Estimate; will be refined after spec
308
+ shutdownManager,
309
+ });
310
+ specSpinner?.start();
311
+ emitProgressLine(issueNumber, "spec", "start");
312
+ try {
313
+ onProgress?.(issueNumber, "spec", "start");
314
+ }
315
+ catch {
316
+ /* progress errors must not halt */
317
+ }
255
318
  // Track spec phase start in state
256
319
  if (stateManager) {
257
320
  try {
@@ -280,14 +343,46 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
280
343
  }
281
344
  phaseResults.push(specResult);
282
345
  specAlreadyRan = true;
346
+ // Emit completion/failure progress event (AC-8)
347
+ const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
348
+ if (specResult.success) {
349
+ const extra = { durationSeconds: specDurationSec };
350
+ emitProgressLine(issueNumber, "spec", "complete", extra);
351
+ try {
352
+ onProgress?.(issueNumber, "spec", "complete", extra);
353
+ }
354
+ catch {
355
+ /* progress errors must not halt */
356
+ }
357
+ }
358
+ else {
359
+ const extra = { error: specResult.error ?? "unknown" };
360
+ emitProgressLine(issueNumber, "spec", "failed", extra);
361
+ try {
362
+ onProgress?.(issueNumber, "spec", "failed", extra);
363
+ }
364
+ catch {
365
+ /* progress errors must not halt */
366
+ }
367
+ }
283
368
  // Log spec phase result
284
369
  // Note: Spec runs in main repo, not worktree, so no git diff stats
285
370
  if (logWriter) {
371
+ // Build errorContext from captured stderr/stdout tails (#447)
372
+ let specErrorContext;
373
+ if (!specResult.success && specResult.stderrTail) {
374
+ specErrorContext = {
375
+ stderrTail: specResult.stderrTail ?? [],
376
+ stdoutTail: specResult.stdoutTail ?? [],
377
+ exitCode: specResult.exitCode,
378
+ category: classifyError(specResult.stderrTail ?? []),
379
+ };
380
+ }
286
381
  const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
287
382
  ? "success"
288
383
  : specResult.error?.includes("Timeout")
289
384
  ? "timeout"
290
- : "failure", { error: specResult.error });
385
+ : "failure", { error: specResult.error, errorContext: specErrorContext });
291
386
  logWriter.logPhase(phaseLog);
292
387
  }
293
388
  // Track spec phase completion in state
@@ -303,7 +398,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
303
398
  }
304
399
  }
305
400
  if (!specResult.success) {
306
- specSpinner.fail(specResult.error);
401
+ specSpinner?.fail(specResult.error);
307
402
  const durationSeconds = (Date.now() - startTime) / 1000;
308
403
  return {
309
404
  issueNumber,
@@ -313,7 +408,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
313
408
  loopTriggered: false,
314
409
  };
315
410
  }
316
- specSpinner.succeed();
411
+ specSpinner?.succeed();
317
412
  // Parse recommended workflow from spec output
318
413
  const parsedWorkflow = specResult.output
319
414
  ? parseRecommendedWorkflow(specResult.output)
@@ -322,15 +417,15 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
322
417
  // Remove spec from phases since we already ran it
323
418
  phases = parsedWorkflow.phases.filter((p) => p !== "spec");
324
419
  detectedQualityLoop = parsedWorkflow.qualityLoop;
325
- console.log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
420
+ log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
326
421
  }
327
422
  else {
328
423
  // Fall back to label-based detection
329
- console.log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
424
+ log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
330
425
  const detected = detectPhasesFromLabels(labels);
331
426
  phases = detected.phases.filter((p) => p !== "spec");
332
427
  detectedQualityLoop = detected.qualityLoop;
333
- console.log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
428
+ log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
334
429
  }
335
430
  }
336
431
  }
@@ -338,21 +433,21 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
338
433
  // Use explicit phases with adjustments
339
434
  phases = determinePhasesForIssue(config.phases, labels, options);
340
435
  if (phases.length !== config.phases.length) {
341
- console.log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
436
+ log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
342
437
  }
343
438
  }
344
439
  // Resume: filter out completed phases if --resume flag is set
345
440
  if (options.resume) {
346
441
  const resumeResult = filterResumedPhases(issueNumber, phases, true);
347
442
  if (resumeResult.skipped.length > 0) {
348
- console.log(chalk.gray(` Resume: skipping completed phases: ${resumeResult.skipped.join(", ")}`));
443
+ log(chalk.gray(` Resume: skipping completed phases: ${resumeResult.skipped.join(", ")}`));
349
444
  phases = resumeResult.phases;
350
445
  }
351
446
  // Also skip spec if it was auto-detected as completed
352
447
  if (specAlreadyRan &&
353
448
  resumeResult.skipped.length === 0 &&
354
449
  resumeResult.phases.length === 0) {
355
- console.log(chalk.gray(` Resume: all phases already completed`));
450
+ log(chalk.gray(` Resume: all phases already completed`));
356
451
  }
357
452
  }
358
453
  // Add testgen phase if requested (and spec was in the phases)
@@ -370,6 +465,12 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
370
465
  }
371
466
  }
372
467
  }
468
+ // Build per-issue config with issue type metadata for skill env propagation
469
+ const lowerLabelsForType = labels.map((l) => l.toLowerCase());
470
+ const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
471
+ const issueConfig = issueIsDocs
472
+ ? { ...config, issueType: "docs" }
473
+ : config;
373
474
  let iteration = 0;
374
475
  const useQualityLoop = config.qualityLoop || detectedQualityLoop;
375
476
  const maxIterations = useQualityLoop ? config.maxIterations : 1;
@@ -377,7 +478,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
377
478
  while (iteration < maxIterations) {
378
479
  iteration++;
379
480
  if (useQualityLoop && iteration > 1) {
380
- console.log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
481
+ log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
381
482
  loopTriggered = true;
382
483
  }
383
484
  let phasesFailed = false;
@@ -388,15 +489,24 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
388
489
  for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
389
490
  const phase = phases[phaseIdx];
390
491
  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();
492
+ // Create spinner for this phase (suppressed in parallel mode)
493
+ const phaseSpinner = config.parallel
494
+ ? undefined
495
+ : new PhaseSpinner({
496
+ phase,
497
+ phaseIndex: phaseNumber,
498
+ totalPhases,
499
+ shutdownManager,
500
+ iteration: useQualityLoop ? iteration : undefined,
501
+ });
502
+ phaseSpinner?.start();
503
+ emitProgressLine(issueNumber, phase, "start");
504
+ try {
505
+ onProgress?.(issueNumber, phase, "start");
506
+ }
507
+ catch {
508
+ /* progress errors must not halt */
509
+ }
400
510
  // Track phase start in state
401
511
  if (stateManager) {
402
512
  try {
@@ -407,7 +517,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
407
517
  }
408
518
  }
409
519
  const phaseStartTime = new Date();
410
- const result = await executePhaseWithRetry(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, phaseSpinner);
520
+ const result = await executePhaseWithRetry(issueNumber, phase, issueConfig, sessionId, worktreePath, shutdownManager, phaseSpinner);
411
521
  const phaseEndTime = new Date();
412
522
  // Capture session ID for subsequent phases
413
523
  if (result.sessionId) {
@@ -423,6 +533,28 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
423
533
  }
424
534
  }
425
535
  phaseResults.push(result);
536
+ // Emit completion/failure progress event (AC-8)
537
+ const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
538
+ if (result.success) {
539
+ const extra = { durationSeconds: phaseDurationSec };
540
+ emitProgressLine(issueNumber, phase, "complete", extra);
541
+ try {
542
+ onProgress?.(issueNumber, phase, "complete", extra);
543
+ }
544
+ catch {
545
+ /* progress errors must not halt */
546
+ }
547
+ }
548
+ else {
549
+ const extra = { error: result.error ?? "unknown" };
550
+ emitProgressLine(issueNumber, phase, "failed", extra);
551
+ try {
552
+ onProgress?.(issueNumber, phase, "failed", extra);
553
+ }
554
+ catch {
555
+ /* progress errors must not halt */
556
+ }
557
+ }
426
558
  // Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
427
559
  if (logWriter) {
428
560
  // Capture git diff stats for worktree phases (AC-1, AC-3)
@@ -435,6 +567,16 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
435
567
  : undefined;
436
568
  // Read cache metrics for QA phase (AC-7)
437
569
  const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
570
+ // Build errorContext from captured stderr/stdout tails (#447)
571
+ let errorContext;
572
+ if (!result.success && result.stderrTail) {
573
+ errorContext = {
574
+ stderrTail: result.stderrTail ?? [],
575
+ stdoutTail: result.stdoutTail ?? [],
576
+ exitCode: result.exitCode,
577
+ category: classifyError(result.stderrTail ?? []),
578
+ };
579
+ }
438
580
  const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
439
581
  ? "success"
440
582
  : result.error?.includes("Timeout")
@@ -442,11 +584,13 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
442
584
  : "failure", {
443
585
  error: result.error,
444
586
  verdict: result.verdict,
587
+ summary: result.summary,
445
588
  // Observability fields (AC-1, AC-2, AC-3, AC-7)
446
589
  filesModified: diffStats?.filesModified,
447
590
  fileDiffStats: diffStats?.fileDiffStats,
448
591
  commitHash,
449
592
  cacheMetrics,
593
+ errorContext,
450
594
  });
451
595
  logWriter.logPhase(phaseLog);
452
596
  }
@@ -465,34 +609,67 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
465
609
  }
466
610
  }
467
611
  if (result.success) {
468
- phaseSpinner.succeed();
612
+ phaseSpinner?.succeed();
469
613
  }
470
614
  else {
471
- phaseSpinner.fail(result.error);
615
+ phaseSpinner?.fail(result.error);
472
616
  phasesFailed = true;
473
617
  // If quality loop enabled, run loop phase to fix issues
474
618
  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);
619
+ // Create spinner for loop phase (suppressed in parallel mode)
620
+ const loopSpinner = config.parallel
621
+ ? undefined
622
+ : new PhaseSpinner({
623
+ phase: "loop",
624
+ phaseIndex: phaseNumber,
625
+ totalPhases,
626
+ shutdownManager,
627
+ iteration,
628
+ });
629
+ loopSpinner?.start();
630
+ emitProgressLine(issueNumber, "loop", "start");
631
+ try {
632
+ onProgress?.(issueNumber, "loop", "start");
633
+ }
634
+ catch {
635
+ /* progress errors must not halt */
636
+ }
637
+ const loopStartTime = new Date();
638
+ const loopResult = await executePhaseWithRetry(issueNumber, "loop", issueConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
639
+ const loopEndTime = new Date();
485
640
  phaseResults.push(loopResult);
641
+ // Emit loop completion/failure progress event (AC-8)
642
+ const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
643
+ if (loopResult.success) {
644
+ const extra = { durationSeconds: loopDurationSec };
645
+ emitProgressLine(issueNumber, "loop", "complete", extra);
646
+ try {
647
+ onProgress?.(issueNumber, "loop", "complete", extra);
648
+ }
649
+ catch {
650
+ /* progress errors must not halt */
651
+ }
652
+ }
653
+ else {
654
+ const extra = { error: loopResult.error ?? "unknown" };
655
+ emitProgressLine(issueNumber, "loop", "failed", extra);
656
+ try {
657
+ onProgress?.(issueNumber, "loop", "failed", extra);
658
+ }
659
+ catch {
660
+ /* progress errors must not halt */
661
+ }
662
+ }
486
663
  if (loopResult.sessionId) {
487
664
  sessionId = loopResult.sessionId;
488
665
  }
489
666
  if (loopResult.success) {
490
- loopSpinner.succeed();
667
+ loopSpinner?.succeed();
491
668
  // Continue to next iteration
492
669
  break;
493
670
  }
494
671
  else {
495
- loopSpinner.fail(loopResult.error);
672
+ loopSpinner?.fail(loopResult.error);
496
673
  }
497
674
  }
498
675
  // 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
+ }