sequant 2.0.0 → 2.1.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 (61) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +7 -6
  4. package/dist/bin/cli.js +2 -1
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +6 -0
  7. package/dist/marketplace/external_plugins/sequant/README.md +58 -8
  8. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +19 -8
  9. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +36 -49
  10. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +158 -48
  11. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +354 -352
  12. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +1155 -33
  13. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +35 -4
  14. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +2157 -104
  15. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +1 -1
  16. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +386 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +38 -664
  18. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +505 -120
  19. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +246 -1
  20. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +138 -1
  21. package/dist/src/commands/dashboard.js +1 -1
  22. package/dist/src/commands/doctor.js +1 -1
  23. package/dist/src/commands/init.js +10 -10
  24. package/dist/src/commands/logs.js +1 -1
  25. package/dist/src/commands/run.js +49 -39
  26. package/dist/src/commands/state.js +3 -3
  27. package/dist/src/commands/status.js +5 -5
  28. package/dist/src/commands/sync.js +8 -8
  29. package/dist/src/commands/update.js +16 -16
  30. package/dist/src/lib/cli-ui.js +20 -19
  31. package/dist/src/lib/merge-check/index.js +2 -2
  32. package/dist/src/lib/settings.d.ts +8 -0
  33. package/dist/src/lib/settings.js +1 -0
  34. package/dist/src/lib/shutdown.js +1 -1
  35. package/dist/src/lib/templates.js +2 -0
  36. package/dist/src/lib/wizard.js +6 -4
  37. package/dist/src/lib/workflow/batch-executor.d.ts +9 -1
  38. package/dist/src/lib/workflow/batch-executor.js +39 -2
  39. package/dist/src/lib/workflow/log-writer.js +6 -6
  40. package/dist/src/lib/workflow/metrics-writer.js +5 -3
  41. package/dist/src/lib/workflow/phase-executor.d.ts +1 -1
  42. package/dist/src/lib/workflow/phase-executor.js +52 -22
  43. package/dist/src/lib/workflow/platforms/github.js +5 -1
  44. package/dist/src/lib/workflow/state-cleanup.js +1 -1
  45. package/dist/src/lib/workflow/state-manager.js +15 -13
  46. package/dist/src/lib/workflow/state-rebuild.js +2 -2
  47. package/dist/src/lib/workflow/types.d.ts +27 -0
  48. package/dist/src/lib/workflow/worktree-manager.js +40 -41
  49. package/dist/src/lib/worktree-isolation.d.ts +130 -0
  50. package/dist/src/lib/worktree-isolation.js +310 -0
  51. package/package.json +24 -14
  52. package/templates/agents/sequant-explorer.md +23 -0
  53. package/templates/agents/sequant-implementer.md +18 -0
  54. package/templates/agents/sequant-qa-checker.md +24 -0
  55. package/templates/agents/sequant-testgen.md +25 -0
  56. package/templates/scripts/cleanup-worktree.sh +18 -0
  57. package/templates/skills/_shared/references/subagent-types.md +158 -48
  58. package/templates/skills/exec/SKILL.md +72 -6
  59. package/templates/skills/qa/SKILL.md +8 -217
  60. package/templates/skills/spec/SKILL.md +446 -120
  61. package/templates/skills/testgen/SKILL.md +138 -1
@@ -30,6 +30,7 @@ export const DEFAULT_ROTATION_SETTINGS = {
30
30
  export const DEFAULT_AGENT_SETTINGS = {
31
31
  parallel: false,
32
32
  model: "haiku",
33
+ isolateParallel: false,
33
34
  };
34
35
  /**
35
36
  * Default trivial thresholds for scope assessment
@@ -138,7 +138,7 @@ export class ShutdownManager {
138
138
  return;
139
139
  }
140
140
  this._isShuttingDown = true;
141
- this.output(chalk.yellow(`\n⚠️ Received ${signal}, shutting down gracefully...`));
141
+ this.output(chalk.yellow(`\n! Received ${signal}, shutting down gracefully...`));
142
142
  // Abort all in-flight phases immediately
143
143
  if (this.abortControllers.size > 0) {
144
144
  const count = this.abortControllers.size;
@@ -205,6 +205,8 @@ export async function copyTemplates(stack, tokens, options = {}) {
205
205
  }
206
206
  // Copy skills
207
207
  await copyDir(join(templatesDir, "skills"), ".claude/skills");
208
+ // Copy agent definitions
209
+ await copyDir(join(templatesDir, "agents"), ".claude/agents");
208
210
  // Copy hooks
209
211
  await copyDir(join(templatesDir, "hooks"), ".claude/hooks");
210
212
  // Copy memory (constitution, etc.)
@@ -66,11 +66,13 @@ export function displayDependencyStatus(result) {
66
66
  console.log(chalk.yellow(` ${chalk.yellow("!")} ${dep.displayName} - installed but not authenticated`));
67
67
  }
68
68
  else {
69
- console.log(chalk.green(` ${chalk.green("")} ${dep.displayName} - installed`));
69
+ console.log(chalk.green(` ${chalk.green("\u2714")} ${dep.displayName} - installed`));
70
70
  }
71
71
  }
72
72
  else {
73
- const marker = dep.required ? chalk.red("✗") : chalk.yellow("○");
73
+ const marker = dep.required
74
+ ? chalk.red("\u2716")
75
+ : chalk.yellow("\u00B7");
74
76
  const status = dep.required
75
77
  ? "not installed (required)"
76
78
  : "not installed (optional)";
@@ -135,7 +137,7 @@ export async function runSetupWizard(result, options = {}) {
135
137
  };
136
138
  }
137
139
  // Ask if user wants to set up missing dependencies
138
- let setupDeps = false;
140
+ let setupDeps;
139
141
  try {
140
142
  const response = await inquirer.prompt([
141
143
  {
@@ -181,7 +183,7 @@ export async function runSetupWizard(result, options = {}) {
181
183
  console.log(` ${instruction}`);
182
184
  }
183
185
  console.log();
184
- let action = "skip";
186
+ let action;
185
187
  try {
186
188
  const response = await inquirer.prompt([
187
189
  {
@@ -7,7 +7,7 @@
7
7
  * (including quality-loop retries, checkpoint commits, rebasing, and PR
8
8
  * creation).
9
9
  */
10
- import { IssueResult, type RunOptions, type IssueExecutionContext, type BatchExecutionContext } from "./types.js";
10
+ import { PhaseResult, IssueResult, type RunOptions, type IssueExecutionContext, type BatchExecutionContext } from "./types.js";
11
11
  export type { RunOptions, ProgressCallback, IssueExecutionContext, BatchExecutionContext, } from "./types.js";
12
12
  /**
13
13
  * Emit a structured progress line to stderr for MCP progress notifications.
@@ -19,6 +19,14 @@ export type { RunOptions, ProgressCallback, IssueExecutionContext, BatchExecutio
19
19
  * @param event - Phase lifecycle event: "start", "complete", or "failed"
20
20
  * @param extra - Optional fields: durationSeconds (on complete), error (on failed)
21
21
  */
22
+ /**
23
+ * Build enriched prompt context for the /loop phase from a failed phase result (#488).
24
+ * Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
25
+ * to reconstruct context from GitHub comments (which fails in subprocess).
26
+ *
27
+ * @internal Exported for testing only
28
+ */
29
+ export declare function buildLoopContext(failedResult: PhaseResult): string;
22
30
  export declare function emitProgressLine(issue: number, phase: string, event?: "start" | "complete" | "failed", extra?: {
23
31
  durationSeconds?: number;
24
32
  error?: string;
@@ -26,6 +26,34 @@ import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIss
26
26
  * @param event - Phase lifecycle event: "start", "complete", or "failed"
27
27
  * @param extra - Optional fields: durationSeconds (on complete), error (on failed)
28
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
+ }
29
57
  export function emitProgressLine(issue, phase, event = "start", extra) {
30
58
  if (!process.env.SEQUANT_ORCHESTRATOR)
31
59
  return;
@@ -271,7 +299,7 @@ export async function runIssueWithLogging(ctx) {
271
299
  catch (error) {
272
300
  // State tracking errors shouldn't stop execution
273
301
  if (config.verbose) {
274
- log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
302
+ log(chalk.yellow(` ! State tracking error: ${error}`));
275
303
  }
276
304
  }
277
305
  }
@@ -634,8 +662,17 @@ export async function runIssueWithLogging(ctx) {
634
662
  catch {
635
663
  /* progress errors must not halt */
636
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
+ };
637
674
  const loopStartTime = new Date();
638
- const loopResult = await executePhaseWithRetry(issueNumber, "loop", issueConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
675
+ const loopResult = await executePhaseWithRetry(issueNumber, "loop", loopConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
639
676
  const loopEndTime = new Date();
640
677
  phaseResults.push(loopResult);
641
678
  // Emit loop completion/failure progress event (AC-8)
@@ -53,7 +53,7 @@ export class LogWriter {
53
53
  await this.ensureLogDirectory(userPath);
54
54
  }
55
55
  if (this.verbose && this.runLog) {
56
- console.log(`📝 Log initialized: ${this.runLog.runId}`);
56
+ console.log(`Log initialized: ${this.runLog.runId}`);
57
57
  }
58
58
  }
59
59
  /**
@@ -79,7 +79,7 @@ export class LogWriter {
79
79
  // Keep currentIssue in sync for callers that don't pass issueNumber
80
80
  this.currentIssue = issueData;
81
81
  if (this.verbose) {
82
- console.log(`📝 Started logging issue #${issueNumber}`);
82
+ console.log(`Log started: issue #${issueNumber}`);
83
83
  }
84
84
  }
85
85
  /**
@@ -102,7 +102,7 @@ export class LogWriter {
102
102
  issue.status = "partial";
103
103
  }
104
104
  if (this.verbose) {
105
- console.log(`📝 Logged phase: ${phaseLog.phase} (${phaseLog.status}) - ${phaseLog.durationSeconds.toFixed(1)}s`);
105
+ console.log(`Log phase: ${phaseLog.phase} (${phaseLog.status}) - ${phaseLog.durationSeconds.toFixed(1)}s`);
106
106
  }
107
107
  }
108
108
  /**
@@ -160,7 +160,7 @@ export class LogWriter {
160
160
  this.currentIssue = null;
161
161
  }
162
162
  if (this.verbose) {
163
- console.log(`📝 Completed issue #${issueLog.issueNumber} (${issueLog.status})`);
163
+ console.log(`Log complete: issue #${issueLog.issueNumber} (${issueLog.status})`);
164
164
  }
165
165
  }
166
166
  /**
@@ -197,13 +197,13 @@ export class LogWriter {
197
197
  await this.writeLogFile(userPath, finalLog);
198
198
  }
199
199
  if (this.verbose) {
200
- console.log(`📝 Log written: ${projectPath}`);
200
+ console.log(`Log written: ${projectPath}`);
201
201
  }
202
202
  // Auto-rotate if needed
203
203
  if (this.rotation.enabled) {
204
204
  const result = rotateIfNeeded(this.logPath, this.rotation);
205
205
  if (result.rotated && this.verbose) {
206
- console.log(`📝 Rotated ${result.deletedCount} old log(s), reclaimed ${(result.bytesReclaimed / 1024).toFixed(1)} KB`);
206
+ console.log(`Log rotated: ${result.deletedCount} old log(s), reclaimed ${(result.bytesReclaimed / 1024).toFixed(1)} KB`);
207
207
  }
208
208
  }
209
209
  return projectPath;
@@ -66,7 +66,9 @@ export class MetricsWriter {
66
66
  }
67
67
  catch (error) {
68
68
  if (error instanceof SyntaxError) {
69
- throw new Error(`Invalid JSON in metrics file: ${error.message}`);
69
+ throw new Error(`Invalid JSON in metrics file: ${error.message}`, {
70
+ cause: error,
71
+ });
70
72
  }
71
73
  throw error;
72
74
  }
@@ -119,7 +121,7 @@ export class MetricsWriter {
119
121
  metrics.runs.push(run);
120
122
  await this.saveMetrics(metrics);
121
123
  if (this.verbose) {
122
- console.log(`📊 Recorded run: ${run.id.slice(0, 8)}... (${run.outcome})`);
124
+ console.log(`Metrics recorded: ${run.id.slice(0, 8)}... (${run.outcome})`);
123
125
  }
124
126
  return run;
125
127
  }
@@ -162,7 +164,7 @@ export class MetricsWriter {
162
164
  fs.unlinkSync(this.metricsPath);
163
165
  this.cachedMetrics = null;
164
166
  if (this.verbose) {
165
- console.log(`📊 Metrics deleted: ${this.metricsPath}`);
167
+ console.log(`Metrics deleted: ${this.metricsPath}`);
166
168
  }
167
169
  }
168
170
  }
@@ -48,7 +48,7 @@ export declare function formatDuration(seconds: number): string;
48
48
  *
49
49
  * @internal Exported for testing only
50
50
  */
51
- export declare function getPhasePrompt(phase: Phase, issueNumber: number, agent?: string): Promise<string>;
51
+ export declare function getPhasePrompt(phase: Phase, issueNumber: number, agent?: string, promptContext?: string): Promise<string>;
52
52
  /**
53
53
  * Execute a single phase for an issue using the configured AgentDriver.
54
54
  */
@@ -224,9 +224,13 @@ export function formatDuration(seconds) {
224
224
  *
225
225
  * @internal Exported for testing only
226
226
  */
227
- export async function getPhasePrompt(phase, issueNumber, agent) {
227
+ export async function getPhasePrompt(phase, issueNumber, agent, promptContext) {
228
228
  const prompts = agent && agent !== "claude-code" ? AIDER_PHASE_PROMPTS : PHASE_PROMPTS;
229
- const basePrompt = prompts[phase].replace(/\{issue\}/g, String(issueNumber));
229
+ let basePrompt = prompts[phase].replace(/\{issue\}/g, String(issueNumber));
230
+ // Append phase-specific context (e.g., QA findings for loop phase)
231
+ if (promptContext) {
232
+ basePrompt += `\n\n---\n\n${promptContext}`;
233
+ }
230
234
  // Include AGENTS.md content in the prompt context for non-Claude agent compatibility.
231
235
  // Claude reads CLAUDE.md natively, but other agents (Aider, Codex, Gemini CLI)
232
236
  // rely on AGENTS.md for project context.
@@ -241,7 +245,7 @@ export async function getPhasePrompt(phase, issueNumber, agent) {
241
245
  */
242
246
  async function executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner) {
243
247
  const startTime = Date.now();
244
- const prompt = await getPhasePrompt(phase, issueNumber, config.agent);
248
+ const prompt = await getPhasePrompt(phase, issueNumber, config.agent, config.promptContext);
245
249
  if (config.dryRun) {
246
250
  // Dry run - show the prompt that would be sent, then return
247
251
  if (config.verbose) {
@@ -318,6 +322,17 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
318
322
  if (config.issueType) {
319
323
  env.SEQUANT_ISSUE_TYPE = config.issueType;
320
324
  }
325
+ // Pass QA context to loop phase so it doesn't need to reconstruct from GitHub (#488)
326
+ if (config.lastVerdict) {
327
+ env.SEQUANT_LAST_VERDICT = config.lastVerdict;
328
+ }
329
+ if (config.failedAcs) {
330
+ env.SEQUANT_FAILED_ACS = config.failedAcs;
331
+ }
332
+ // Propagate parallel isolation mode to exec skill (#485)
333
+ if (config.isolateParallel) {
334
+ env.SEQUANT_ISOLATE_PARALLEL = "true";
335
+ }
321
336
  // Track whether we're actively streaming verbose output
322
337
  // Pausing spinner once per streaming session prevents truncation from rapid pause/resume cycles
323
338
  // (Issue #283: ora's stop() clears the current line, which can truncate output when
@@ -452,37 +467,52 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
452
467
  if (config.retry === false) {
453
468
  return executePhaseFn(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner);
454
469
  }
470
+ // Skip cold-start retries for `loop` phase (#488).
471
+ // Loop is always a re-run after a failed QA — never a first boot.
472
+ // Failures at 47-51s are genuine skill failures, not cold-start issues.
473
+ // Without this guard, 2 cold-start retries + 1 MCP fallback = 3 wasted spawns per loop.
474
+ const skipColdStartRetry = phase === "loop";
455
475
  let lastResult;
456
- // Phase 1: Cold-start retry attempts (with MCP enabled if configured)
457
- for (let attempt = 0; attempt <= COLD_START_MAX_RETRIES; attempt++) {
476
+ if (skipColdStartRetry) {
477
+ // Single attempt no cold-start retry loop
458
478
  lastResult = await executePhaseFn(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner);
459
- const duration = lastResult.durationSeconds ?? 0;
460
- // Success → return immediately
461
479
  if (lastResult.success) {
462
480
  return lastResult;
463
481
  }
464
- // Genuine failure (took long enough to be real work) → skip cold-start retries.
465
- // For spec phase, break to allow Phase 3 (spec-specific retry) to run.
466
- // For other phases, return immediately no further retries.
467
- if (duration >= COLD_START_THRESHOLD_SECONDS) {
468
- if (phase === "spec") {
469
- break;
482
+ }
483
+ else {
484
+ // Phase 1: Cold-start retry attempts (with MCP enabled if configured)
485
+ for (let attempt = 0; attempt <= COLD_START_MAX_RETRIES; attempt++) {
486
+ lastResult = await executePhaseFn(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner);
487
+ const duration = lastResult.durationSeconds ?? 0;
488
+ // Success → return immediately
489
+ if (lastResult.success) {
490
+ return lastResult;
470
491
  }
471
- return lastResult;
472
- }
473
- // Cold-start failure detectedretry
474
- if (attempt < COLD_START_MAX_RETRIES) {
475
- if (config.verbose) {
476
- console.log(chalk.yellow(`\n ⟳ Cold-start failure detected (${duration.toFixed(1)}s), retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
492
+ // Genuine failure (took long enough to be real work) → skip cold-start retries.
493
+ // For spec phase, break to allow Phase 3 (spec-specific retry) to run.
494
+ // For other phases, return immediately no further retries.
495
+ if (duration >= COLD_START_THRESHOLD_SECONDS) {
496
+ if (phase === "spec") {
497
+ break;
498
+ }
499
+ return lastResult;
500
+ }
501
+ // Cold-start failure detected — retry
502
+ if (attempt < COLD_START_MAX_RETRIES) {
503
+ if (config.verbose) {
504
+ console.log(chalk.yellow(`\n ⟳ Cold-start failure detected (${duration.toFixed(1)}s), retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
505
+ }
477
506
  }
478
507
  }
479
508
  }
480
509
  // Capture the original error for better diagnostics
481
510
  const originalError = lastResult.error;
482
511
  // Phase 2: MCP fallback - if MCP is enabled and we're still failing, try without MCP
483
- // This handles npx-based MCP servers that fail on first run due to cold-cache issues
484
- if (config.mcp && !lastResult.success) {
485
- console.log(chalk.yellow(`\n ⚠️ Phase failed with MCP enabled, retrying without MCP...`));
512
+ // This handles npx-based MCP servers that fail on first run due to cold-cache issues.
513
+ // Skip for `loop` phase — MCP is never the cause of loop failures (#488).
514
+ if (config.mcp && !lastResult.success && !skipColdStartRetry) {
515
+ console.log(chalk.yellow(`\n ! Phase failed with MCP enabled, retrying without MCP...`));
486
516
  // Create config copy with MCP disabled
487
517
  const configWithoutMcp = {
488
518
  ...config,
@@ -344,8 +344,12 @@ export class GitHubProvider {
344
344
  stdio: ["pipe", "pipe", "pipe"],
345
345
  timeout: 30000,
346
346
  });
347
- if (result.status !== 0 || !result.stdout)
347
+ if (result.status !== 0 || !result.stdout) {
348
+ if (result.stderr) {
349
+ console.error(`gh issue create failed: ${result.stderr.trim()}`);
350
+ }
348
351
  return null;
352
+ }
349
353
  const url = result.stdout.trim();
350
354
  const numberMatch = url.match(/\/issues\/(\d+)$/);
351
355
  const number = numberMatch ? parseInt(numberMatch[1], 10) : 0;
@@ -73,7 +73,7 @@ export async function cleanupStaleEntries(options = {}) {
73
73
  if (issueState.worktree &&
74
74
  !activeWorktrees.includes(issueState.worktree)) {
75
75
  if (options.verbose) {
76
- console.log(`🔍 Orphaned: #${issueNum} (worktree not found: ${issueState.worktree})`);
76
+ console.log(`Orphaned: #${issueNum} (worktree not found: ${issueState.worktree})`);
77
77
  }
78
78
  // Check if this issue has a PR and if it's merged
79
79
  let prMerged = false;
@@ -100,7 +100,7 @@ export class StateManager {
100
100
  continue;
101
101
  }
102
102
  if (Date.now() - start > this.lockTimeout) {
103
- throw new Error(`Timeout acquiring state lock after ${this.lockTimeout}ms`);
103
+ throw new Error(`Timeout acquiring state lock after ${this.lockTimeout}ms`, { cause: err });
104
104
  }
105
105
  // Wait and retry
106
106
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
@@ -150,7 +150,9 @@ export class StateManager {
150
150
  }
151
151
  catch (error) {
152
152
  if (error instanceof SyntaxError) {
153
- throw new Error(`Invalid JSON in state file: ${error.message}`);
153
+ throw new Error(`Invalid JSON in state file: ${error.message}`, {
154
+ cause: error,
155
+ });
154
156
  }
155
157
  throw error;
156
158
  }
@@ -182,7 +184,7 @@ export class StateManager {
182
184
  }
183
185
  }
184
186
  if (pruned.length > 0 && this.verbose) {
185
- console.log(`📊 Pruned ${pruned.length} expired issue(s): ${pruned.map((k) => `#${k}`).join(", ")}`);
187
+ console.log(`State: Pruned ${pruned.length} expired issue(s): ${pruned.map((k) => `#${k}`).join(", ")}`);
186
188
  }
187
189
  }
188
190
  catch {
@@ -272,7 +274,7 @@ export class StateManager {
272
274
  await this.saveState(state);
273
275
  });
274
276
  if (this.verbose) {
275
- console.log(`📊 Initialized issue #${issueNumber}: ${title}`);
277
+ console.log(`State: Initialized issue #${issueNumber}: ${title}`);
276
278
  }
277
279
  }
278
280
  /**
@@ -313,7 +315,7 @@ export class StateManager {
313
315
  await this.saveState(state);
314
316
  });
315
317
  if (this.verbose) {
316
- console.log(`📊 Phase ${phase} → ${status} for issue #${issueNumber}`);
318
+ console.log(`State: Phase ${phase} → ${status} for issue #${issueNumber}`);
317
319
  }
318
320
  }
319
321
  /**
@@ -335,7 +337,7 @@ export class StateManager {
335
337
  await this.saveState(state);
336
338
  });
337
339
  if (this.verbose) {
338
- console.log(`📊 Issue #${issueNumber} status → ${status}`);
340
+ console.log(`State: Issue #${issueNumber} status → ${status}`);
339
341
  }
340
342
  }
341
343
  /**
@@ -353,7 +355,7 @@ export class StateManager {
353
355
  await this.saveState(state);
354
356
  });
355
357
  if (this.verbose) {
356
- console.log(`📊 PR #${pr.number} linked to issue #${issueNumber}`);
358
+ console.log(`State: PR #${pr.number} linked to issue #${issueNumber}`);
357
359
  }
358
360
  }
359
361
  /**
@@ -372,7 +374,7 @@ export class StateManager {
372
374
  await this.saveState(state);
373
375
  });
374
376
  if (this.verbose) {
375
- console.log(`📊 Worktree updated for issue #${issueNumber}: ${worktree}`);
377
+ console.log(`State: Worktree updated for issue #${issueNumber}: ${worktree}`);
376
378
  }
377
379
  }
378
380
  /**
@@ -407,7 +409,7 @@ export class StateManager {
407
409
  await this.saveState(state);
408
410
  });
409
411
  if (this.verbose) {
410
- console.log(`📊 Loop iteration ${iteration} for issue #${issueNumber}`);
412
+ console.log(`State: Loop iteration ${iteration} for issue #${issueNumber}`);
411
413
  }
412
414
  }
413
415
  /**
@@ -423,7 +425,7 @@ export class StateManager {
423
425
  await this.saveState(state);
424
426
  });
425
427
  if (this.verbose) {
426
- console.log(`📊 Removed issue #${issueNumber} from state`);
428
+ console.log(`State: Removed issue #${issueNumber} from state`);
427
429
  }
428
430
  }
429
431
  // === Acceptance Criteria Operations ===
@@ -444,7 +446,7 @@ export class StateManager {
444
446
  await this.saveState(state);
445
447
  });
446
448
  if (this.verbose) {
447
- console.log(`📊 AC updated for issue #${issueNumber}: ${acceptanceCriteria.items.length} items`);
449
+ console.log(`State: AC updated for issue #${issueNumber}: ${acceptanceCriteria.items.length} items`);
448
450
  }
449
451
  }
450
452
  /**
@@ -487,7 +489,7 @@ export class StateManager {
487
489
  await this.saveState(state);
488
490
  });
489
491
  if (this.verbose) {
490
- console.log(`📊 AC ${acId} → ${status} for issue #${issueNumber}`);
492
+ console.log(`State: AC ${acId} → ${status} for issue #${issueNumber}`);
491
493
  }
492
494
  }
493
495
  // === Scope Assessment Operations ===
@@ -508,7 +510,7 @@ export class StateManager {
508
510
  await this.saveState(state);
509
511
  });
510
512
  if (this.verbose) {
511
- console.log(`📊 Scope assessment updated for issue #${issueNumber}: ${scopeAssessment.verdict}`);
513
+ console.log(`State: Scope assessment updated for issue #${issueNumber}: ${scopeAssessment.verdict}`);
512
514
  }
513
515
  }
514
516
  /**
@@ -55,7 +55,7 @@ export async function rebuildStateFromLogs(options = {}) {
55
55
  const log = RunLogSchema.safeParse(logData);
56
56
  if (!log.success) {
57
57
  if (options.verbose) {
58
- console.log(`⚠️ Invalid log format: ${file}`);
58
+ console.log(`! Invalid log format: ${file}`);
59
59
  }
60
60
  continue;
61
61
  }
@@ -109,7 +109,7 @@ export async function rebuildStateFromLogs(options = {}) {
109
109
  }
110
110
  catch (err) {
111
111
  if (options.verbose) {
112
- console.log(`⚠️ Error reading ${file}: ${err}`);
112
+ console.log(`! Error reading ${file}: ${err}`);
113
113
  }
114
114
  }
115
115
  }
@@ -71,6 +71,11 @@ export interface ExecutionConfig {
71
71
  * Default: "claude-code"
72
72
  */
73
73
  agent?: string;
74
+ /**
75
+ * Isolate parallel agent groups in separate worktrees.
76
+ * Propagated as SEQUANT_ISOLATE_PARALLEL env var to exec skill.
77
+ */
78
+ isolateParallel?: boolean;
74
79
  /**
75
80
  * Aider-specific configuration. Passed to AiderDriver when agent is "aider".
76
81
  */
@@ -80,6 +85,22 @@ export interface ExecutionConfig {
80
85
  * Propagated as SEQUANT_ISSUE_TYPE env var to skills.
81
86
  */
82
87
  issueType?: string;
88
+ /**
89
+ * Additional context appended to the phase prompt.
90
+ * Used by the quality loop to pass QA findings directly to the /loop skill
91
+ * so it doesn't need to reconstruct context from GitHub comments.
92
+ */
93
+ promptContext?: string;
94
+ /**
95
+ * Last QA verdict from a preceding phase.
96
+ * Propagated as SEQUANT_LAST_VERDICT env var to skills.
97
+ */
98
+ lastVerdict?: string;
99
+ /**
100
+ * Failed AC descriptions from a preceding QA phase.
101
+ * Propagated as SEQUANT_FAILED_ACS env var to skills.
102
+ */
103
+ failedAcs?: string;
83
104
  }
84
105
  /**
85
106
  * Default execution configuration
@@ -212,6 +233,12 @@ export interface RunOptions {
212
233
  * Default: "claude-code"
213
234
  */
214
235
  agent?: string;
236
+ /**
237
+ * Isolate parallel agent groups in separate worktrees.
238
+ * When true, each agent in a parallel group gets its own sub-worktree.
239
+ * Resolution priority: CLI flag → settings.agents.isolateParallel → false
240
+ */
241
+ isolateParallel?: boolean;
215
242
  }
216
243
  /**
217
244
  * CLI arguments for run command