sequant 2.2.0 → 2.4.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 (156) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +81 -5
  4. package/dist/bin/cli.js +140 -13
  5. package/dist/src/commands/abort.d.ts +36 -0
  6. package/dist/src/commands/abort.js +138 -0
  7. package/dist/src/commands/doctor.d.ts +25 -0
  8. package/dist/src/commands/doctor.js +36 -1
  9. package/dist/src/commands/locks.d.ts +67 -0
  10. package/dist/src/commands/locks.js +290 -0
  11. package/dist/src/commands/merge.js +11 -0
  12. package/dist/src/commands/prompt.d.ts +46 -0
  13. package/dist/src/commands/prompt.js +273 -0
  14. package/dist/src/commands/run-display.d.ts +11 -2
  15. package/dist/src/commands/run-display.js +62 -28
  16. package/dist/src/commands/run-progress.d.ts +42 -0
  17. package/dist/src/commands/run-progress.js +93 -0
  18. package/dist/src/commands/run.js +90 -18
  19. package/dist/src/commands/stats.d.ts +2 -0
  20. package/dist/src/commands/stats.js +94 -8
  21. package/dist/src/commands/status.js +12 -0
  22. package/dist/src/commands/watch.d.ts +18 -0
  23. package/dist/src/commands/watch.js +211 -0
  24. package/dist/src/lib/ac-linter.d.ts +1 -1
  25. package/dist/src/lib/ac-linter.js +81 -0
  26. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  27. package/dist/src/lib/assess-collision-detect.js +217 -0
  28. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  29. package/dist/src/lib/assess-comment-parser.js +124 -2
  30. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  31. package/dist/src/lib/cli-ui/format.js +34 -0
  32. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +220 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
  36. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  37. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  38. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  39. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  40. package/dist/src/lib/locks/index.d.ts +7 -0
  41. package/dist/src/lib/locks/index.js +5 -0
  42. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  43. package/dist/src/lib/locks/lock-manager.js +433 -0
  44. package/dist/src/lib/locks/types.d.ts +59 -0
  45. package/dist/src/lib/locks/types.js +31 -0
  46. package/dist/src/lib/merge-check/types.js +1 -1
  47. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  48. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  49. package/dist/src/lib/relay/activation.d.ts +60 -0
  50. package/dist/src/lib/relay/activation.js +122 -0
  51. package/dist/src/lib/relay/archive.d.ts +34 -0
  52. package/dist/src/lib/relay/archive.js +112 -0
  53. package/dist/src/lib/relay/frame.d.ts +20 -0
  54. package/dist/src/lib/relay/frame.js +76 -0
  55. package/dist/src/lib/relay/index.d.ts +13 -0
  56. package/dist/src/lib/relay/index.js +13 -0
  57. package/dist/src/lib/relay/paths.d.ts +43 -0
  58. package/dist/src/lib/relay/paths.js +59 -0
  59. package/dist/src/lib/relay/pid.d.ts +34 -0
  60. package/dist/src/lib/relay/pid.js +72 -0
  61. package/dist/src/lib/relay/reader.d.ts +35 -0
  62. package/dist/src/lib/relay/reader.js +115 -0
  63. package/dist/src/lib/relay/types.d.ts +70 -0
  64. package/dist/src/lib/relay/types.js +85 -0
  65. package/dist/src/lib/relay/writer.d.ts +48 -0
  66. package/dist/src/lib/relay/writer.js +113 -0
  67. package/dist/src/lib/settings.d.ts +31 -1
  68. package/dist/src/lib/settings.js +18 -3
  69. package/dist/src/lib/version-check.d.ts +60 -5
  70. package/dist/src/lib/version-check.js +97 -9
  71. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  72. package/dist/src/lib/workflow/batch-executor.js +274 -185
  73. package/dist/src/lib/workflow/config-resolver.js +4 -0
  74. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  75. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  76. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  77. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  78. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  79. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  80. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  81. package/dist/src/lib/workflow/event-emitter.js +102 -0
  82. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  83. package/dist/src/lib/workflow/heartbeat.js +194 -0
  84. package/dist/src/lib/workflow/notice.d.ts +32 -0
  85. package/dist/src/lib/workflow/notice.js +38 -0
  86. package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
  87. package/dist/src/lib/workflow/phase-executor.js +244 -130
  88. package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
  89. package/dist/src/lib/workflow/phase-mapper.js +70 -51
  90. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  91. package/dist/src/lib/workflow/phase-registry.js +233 -0
  92. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  93. package/dist/src/lib/workflow/platforms/github.js +20 -3
  94. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  95. package/dist/src/lib/workflow/pr-status.js +41 -9
  96. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  97. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  98. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  99. package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
  100. package/dist/src/lib/workflow/run-orchestrator.js +464 -25
  101. package/dist/src/lib/workflow/run-reflect.js +1 -1
  102. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  103. package/dist/src/lib/workflow/run-state.js +14 -0
  104. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  105. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  106. package/dist/src/lib/workflow/state-manager.d.ts +31 -2
  107. package/dist/src/lib/workflow/state-manager.js +64 -1
  108. package/dist/src/lib/workflow/state-schema.d.ts +82 -35
  109. package/dist/src/lib/workflow/state-schema.js +63 -4
  110. package/dist/src/lib/workflow/types.d.ts +139 -16
  111. package/dist/src/lib/workflow/types.js +18 -13
  112. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  113. package/dist/src/lib/workflow/worktree-manager.js +15 -6
  114. package/dist/src/mcp/tools/run.d.ts +44 -0
  115. package/dist/src/mcp/tools/run.js +104 -13
  116. package/dist/src/ui/tui/App.d.ts +14 -0
  117. package/dist/src/ui/tui/App.js +41 -0
  118. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  119. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  120. package/dist/src/ui/tui/Header.d.ts +6 -0
  121. package/dist/src/ui/tui/Header.js +15 -0
  122. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  123. package/dist/src/ui/tui/IssueBox.js +68 -0
  124. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  125. package/dist/src/ui/tui/Spinner.js +18 -0
  126. package/dist/src/ui/tui/index.d.ts +15 -0
  127. package/dist/src/ui/tui/index.js +29 -0
  128. package/dist/src/ui/tui/theme.d.ts +29 -0
  129. package/dist/src/ui/tui/theme.js +52 -0
  130. package/dist/src/ui/tui/truncate.d.ts +11 -0
  131. package/dist/src/ui/tui/truncate.js +31 -0
  132. package/package.json +14 -6
  133. package/templates/agents/sequant-explorer.md +1 -0
  134. package/templates/agents/sequant-qa-checker.md +2 -1
  135. package/templates/agents/sequant-testgen.md +1 -0
  136. package/templates/hooks/post-tool.sh +92 -0
  137. package/templates/hooks/pre-tool.sh +18 -9
  138. package/templates/hooks/relay-check.sh +107 -0
  139. package/templates/relay/frame.txt +11 -0
  140. package/templates/scripts/cleanup-worktree.sh +25 -3
  141. package/templates/scripts/new-feature.sh +6 -0
  142. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  143. package/templates/skills/_shared/references/subagent-types.md +21 -8
  144. package/templates/skills/assess/SKILL.md +122 -68
  145. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  146. package/templates/skills/docs/SKILL.md +141 -22
  147. package/templates/skills/exec/SKILL.md +10 -8
  148. package/templates/skills/fullsolve/SKILL.md +79 -5
  149. package/templates/skills/loop/SKILL.md +28 -0
  150. package/templates/skills/merger/SKILL.md +621 -0
  151. package/templates/skills/qa/SKILL.md +727 -8
  152. package/templates/skills/setup/SKILL.md +12 -6
  153. package/templates/skills/spec/SKILL.md +52 -0
  154. package/templates/skills/spec/references/parallel-groups.md +7 -0
  155. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  156. package/templates/skills/testgen/SKILL.md +24 -17
@@ -11,11 +11,11 @@ import chalk from "chalk";
11
11
  import { spawnSync } from "child_process";
12
12
  import { createPhaseLogFromTiming } from "./log-writer.js";
13
13
  import { classifyError, errorTypeToCategory } from "./error-classifier.js";
14
- import { PhaseSpinner } from "../phase-spinner.js";
15
14
  import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
16
15
  import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
17
16
  import { executePhaseWithRetry } from "./phase-executor.js";
18
- import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, BUG_LABELS, DOCS_LABELS, } from "./phase-mapper.js";
17
+ import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, DOCS_LABELS, } from "./phase-mapper.js";
18
+ import { activateRelay, deactivateRelay, } from "../relay/activation.js";
19
19
  /**
20
20
  * Emit a structured progress line to stderr for MCP progress notifications.
21
21
  * Only emits when running under an orchestrator (e.g., MCP server).
@@ -26,6 +26,30 @@ 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
+ * Wrap an `ExecutionConfig` with an `onActivity` hook that re-emits each
31
+ * agent-output ping as a `"activity"` progress event for the dashboard (#543).
32
+ *
33
+ * Returns the input config unchanged when no `onProgress` callback is set,
34
+ * so non-TUI runs pay no overhead.
35
+ *
36
+ * @internal Exported for testing only
37
+ */
38
+ export function withActivityHook(base, issueNumber, phase, onProgress) {
39
+ if (!onProgress)
40
+ return base;
41
+ return {
42
+ ...base,
43
+ onActivity: (text) => {
44
+ try {
45
+ onProgress(issueNumber, phase, "activity", { text });
46
+ }
47
+ catch {
48
+ // Activity events must never disrupt the run.
49
+ }
50
+ },
51
+ };
52
+ }
29
53
  /**
30
54
  * Build enriched prompt context for the /loop phase from a failed phase result (#488).
31
55
  * Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
@@ -64,9 +88,26 @@ export function emitProgressLine(issue, phase, event = "start", extra) {
64
88
  if (extra?.error !== undefined) {
65
89
  payload.error = extra.error;
66
90
  }
91
+ // #624 Item 3: surface the outer-loop iteration so MCP consumers (and the
92
+ // renderer) can label retried events as `(attempt N/M)` / `loop N/M`.
93
+ if (extra?.iteration !== undefined) {
94
+ payload.iteration = extra.iteration;
95
+ }
67
96
  const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
68
97
  process.stderr.write(line);
69
98
  }
99
+ /**
100
+ * Emit the current run's UUID on stderr so MCP callers can look up the exact
101
+ * log file produced by this subprocess instead of relying on a fuzzy time
102
+ * filter (#631). Gated on `SEQUANT_ORCHESTRATOR` so CLI users see nothing.
103
+ *
104
+ * Must be called before `emitProgressLine` to satisfy AC-1.
105
+ */
106
+ export function emitRunIdLine(runId) {
107
+ if (!process.env.SEQUANT_ORCHESTRATOR)
108
+ return;
109
+ process.stderr.write(`SEQUANT_RUN_ID:${runId}\n`);
110
+ }
70
111
  export async function getIssueInfo(issueNumber) {
71
112
  try {
72
113
  const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
@@ -213,10 +254,13 @@ export function getEnvConfig() {
213
254
  if (process.env.SEQUANT_TESTGEN === "true") {
214
255
  config.testgen = true;
215
256
  }
257
+ if (process.env.SEQUANT_SECURITY_REVIEW === "true") {
258
+ config.securityReview = true;
259
+ }
216
260
  return config;
217
261
  }
218
262
  export async function executeBatch(issueNumbers, batchCtx) {
219
- const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
263
+ const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = batchCtx;
220
264
  const results = [];
221
265
  for (const issueNumber of issueNumbers) {
222
266
  // Check if shutdown was triggered
@@ -245,6 +289,8 @@ export async function executeBatch(issueNumbers, batchCtx) {
245
289
  packageManager,
246
290
  baseBranch,
247
291
  onProgress,
292
+ onPhasePlan,
293
+ phasePauseHandle,
248
294
  };
249
295
  const result = await runIssueWithLogging(ctx);
250
296
  results.push(result);
@@ -261,7 +307,7 @@ export async function executeBatch(issueNumbers, batchCtx) {
261
307
  }
262
308
  export async function runIssueWithLogging(ctx) {
263
309
  // 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;
310
+ const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
265
311
  const worktreePath = worktree?.path;
266
312
  const branch = worktree?.branch;
267
313
  const chainMode = chain?.enabled;
@@ -269,7 +315,8 @@ export async function runIssueWithLogging(ctx) {
269
315
  const startTime = Date.now();
270
316
  const phaseResults = [];
271
317
  let loopTriggered = false;
272
- let sessionId;
318
+ // Cross-phase resume token, driver-tagged and cwd-bound (#674).
319
+ let resumeHandle;
273
320
  // In parallel mode, suppress per-issue terminal output to prevent interleaving.
274
321
  // The caller (run.ts) handles progress display via updateProgress().
275
322
  const log = config.parallel ? () => { } : console.log.bind(console);
@@ -303,162 +350,174 @@ export async function runIssueWithLogging(ctx) {
303
350
  }
304
351
  }
305
352
  }
353
+ // Activate relay (#383) if enabled. Tolerates errors — relay must never
354
+ // block the underlying run.
355
+ let relayActivation = null;
356
+ if (config.relayEnabled && !config.dryRun) {
357
+ try {
358
+ relayActivation = await activateRelay(issueNumber, {
359
+ worktreePath,
360
+ stateManager: stateManager ?? null,
361
+ });
362
+ if (relayActivation.warning && config.verbose) {
363
+ log(chalk.yellow(` ! Relay: ${relayActivation.warning}`));
364
+ }
365
+ else if (relayActivation.activated && config.verbose) {
366
+ log(chalk.gray(` Relay active — use \`sequant prompt ${issueNumber} "<msg>"\` to nudge`));
367
+ }
368
+ }
369
+ catch (err) {
370
+ if (config.verbose) {
371
+ log(chalk.yellow(` ! Relay activation failed: ${err}`));
372
+ }
373
+ }
374
+ }
306
375
  // Determine phases for this specific issue
307
376
  let phases;
308
377
  let detectedQualityLoop = false;
309
378
  let specAlreadyRan = false;
310
379
  if (options.autoDetectPhases) {
311
- // Check if labels indicate a simple bug/fix (skip spec entirely)
312
- const lowerLabels = labels.map((l) => l.toLowerCase());
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));
316
- if (isSimpleBugFix) {
317
- // Simple bug fix: skip spec, go straight to exec → qa
318
- phases = ["exec", "qa"];
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(" → ")}`));
380
+ // #533: Always run spec to get recommended workflow.
381
+ // The prior bug/docs shortcut (skip spec → exec → qa) was removed because
382
+ // bug and docs issues often contain design decisions (scope boundaries,
383
+ // edge cases, test-strategy shifts) that benefit from a spec pass.
384
+ log(chalk.gray(` Running spec to determine workflow...`));
385
+ // RunRenderer (#618) owns spec progress via emitProgressLine + onProgress.
386
+ // The legacy PhaseSpinner produced duplicate lines for single-issue runs.
387
+ emitProgressLine(issueNumber, "spec", "start");
388
+ try {
389
+ onProgress?.(issueNumber, "spec", "start");
325
390
  }
326
- else {
327
- // Run spec first to get recommended workflow
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");
391
+ catch {
392
+ /* progress errors must not halt */
393
+ }
394
+ // Track spec phase start in state
395
+ if (stateManager) {
340
396
  try {
341
- onProgress?.(issueNumber, "spec", "start");
397
+ await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
342
398
  }
343
399
  catch {
344
- /* progress errors must not halt */
400
+ // State tracking errors shouldn't stop execution
345
401
  }
346
- // Track spec phase start in state
402
+ }
403
+ const specStartTime = new Date();
404
+ // Note: spec runs in main repo (not worktree) for planning
405
+ const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), resumeHandle, worktreePath, // Will be ignored for spec (non-isolated phase)
406
+ shutdownManager, phasePauseHandle);
407
+ const specEndTime = new Date();
408
+ if (specResult.resumeHandle) {
409
+ resumeHandle = specResult.resumeHandle;
410
+ // Persist resume token + originCwd for cross-process resume (#674).
347
411
  if (stateManager) {
348
412
  try {
349
- await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
413
+ await stateManager.updateResumeHandle(issueNumber, specResult.resumeHandle);
350
414
  }
351
415
  catch {
352
416
  // State tracking errors shouldn't stop execution
353
417
  }
354
418
  }
355
- const specStartTime = new Date();
356
- // Note: spec runs in main repo (not worktree) for planning
357
- const specResult = await executePhaseWithRetry(issueNumber, "spec", config, sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
358
- shutdownManager, specSpinner);
359
- const specEndTime = new Date();
360
- if (specResult.sessionId) {
361
- sessionId = specResult.sessionId;
362
- // Update session ID in state for resume capability
363
- if (stateManager) {
364
- try {
365
- await stateManager.updateSessionId(issueNumber, specResult.sessionId);
366
- }
367
- catch {
368
- // State tracking errors shouldn't stop execution
369
- }
370
- }
419
+ }
420
+ phaseResults.push(specResult);
421
+ specAlreadyRan = true;
422
+ // Emit completion/failure progress event (AC-8)
423
+ const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
424
+ if (specResult.success) {
425
+ const extra = { durationSeconds: specDurationSec };
426
+ emitProgressLine(issueNumber, "spec", "complete", extra);
427
+ try {
428
+ onProgress?.(issueNumber, "spec", "complete", extra);
371
429
  }
372
- phaseResults.push(specResult);
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
- }
430
+ catch {
431
+ /* progress errors must not halt */
385
432
  }
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
- }
433
+ }
434
+ else {
435
+ const extra = { error: specResult.error ?? "unknown" };
436
+ emitProgressLine(issueNumber, "spec", "failed", extra);
437
+ try {
438
+ onProgress?.(issueNumber, "spec", "failed", extra);
395
439
  }
396
- // Log spec phase result
397
- // Note: Spec runs in main repo, not worktree, so no git diff stats
398
- if (logWriter) {
399
- // Build errorContext from captured stderr/stdout tails (#447)
400
- let specErrorContext;
401
- if (!specResult.success && specResult.stderrTail) {
402
- const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
403
- specErrorContext = {
404
- stderrTail: specResult.stderrTail ?? [],
405
- stdoutTail: specResult.stdoutTail ?? [],
406
- exitCode: specResult.exitCode,
407
- category: errorTypeToCategory(specError),
408
- errorType: specError.name,
409
- errorMetadata: specError.metadata,
410
- isRetryable: specError.isRetryable,
411
- };
412
- }
413
- const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
414
- ? "success"
415
- : specResult.error?.includes("Timeout")
416
- ? "timeout"
417
- : "failure", { error: specResult.error, errorContext: specErrorContext });
418
- logWriter.logPhase(phaseLog);
440
+ catch {
441
+ /* progress errors must not halt */
419
442
  }
420
- // Track spec phase completion in state
421
- if (stateManager) {
443
+ }
444
+ // Log spec phase result
445
+ // Note: Spec runs in main repo, not worktree, so no git diff stats
446
+ if (logWriter) {
447
+ // Build errorContext from captured stderr/stdout tails (#447)
448
+ let specErrorContext;
449
+ if (!specResult.success && specResult.stderrTail) {
450
+ const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
451
+ specErrorContext = {
452
+ stderrTail: specResult.stderrTail ?? [],
453
+ stdoutTail: specResult.stdoutTail ?? [],
454
+ exitCode: specResult.exitCode,
455
+ category: errorTypeToCategory(specError),
456
+ errorType: specError.name,
457
+ errorMetadata: specError.metadata,
458
+ isRetryable: specError.isRetryable,
459
+ };
460
+ }
461
+ const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
462
+ ? "success"
463
+ : specResult.error?.includes("Timeout")
464
+ ? "timeout"
465
+ : "failure", { error: specResult.error, errorContext: specErrorContext });
466
+ logWriter.logPhase(phaseLog);
467
+ }
468
+ // Track spec phase completion in state
469
+ if (stateManager) {
470
+ try {
471
+ const phaseStatus = specResult.success ? "completed" : "failed";
472
+ await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
473
+ error: specResult.error,
474
+ });
475
+ }
476
+ catch {
477
+ // State tracking errors shouldn't stop execution
478
+ }
479
+ }
480
+ if (!specResult.success) {
481
+ const durationSeconds = (Date.now() - startTime) / 1000;
482
+ // Archive relay state on early exit (spec failure).
483
+ if (relayActivation) {
422
484
  try {
423
- const phaseStatus = specResult.success ? "completed" : "failed";
424
- await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
425
- error: specResult.error,
485
+ await deactivateRelay(issueNumber, {
486
+ phase: "spec",
487
+ startedAt: relayActivation.startedAt,
488
+ worktreePath,
489
+ stateManager: stateManager ?? null,
426
490
  });
427
491
  }
428
492
  catch {
429
- // State tracking errors shouldn't stop execution
493
+ /* swallow */
430
494
  }
431
495
  }
432
- if (!specResult.success) {
433
- specSpinner?.fail(specResult.error);
434
- const durationSeconds = (Date.now() - startTime) / 1000;
435
- return {
436
- issueNumber,
437
- success: false,
438
- phaseResults,
439
- durationSeconds,
440
- loopTriggered: false,
441
- };
442
- }
443
- specSpinner?.succeed();
444
- // Parse recommended workflow from spec output
445
- const parsedWorkflow = specResult.output
446
- ? parseRecommendedWorkflow(specResult.output)
447
- : null;
448
- if (parsedWorkflow) {
449
- // Remove spec from phases since we already ran it
450
- phases = parsedWorkflow.phases.filter((p) => p !== "spec");
451
- detectedQualityLoop = parsedWorkflow.qualityLoop;
452
- log(chalk.gray(` Spec recommends: ${phases.join(" ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
453
- }
454
- else {
455
- // Fall back to label-based detection
456
- log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
457
- const detected = detectPhasesFromLabels(labels);
458
- phases = detected.phases.filter((p) => p !== "spec");
459
- detectedQualityLoop = detected.qualityLoop;
460
- log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
461
- }
496
+ return {
497
+ issueNumber,
498
+ success: false,
499
+ phaseResults,
500
+ durationSeconds,
501
+ loopTriggered: false,
502
+ };
503
+ }
504
+ // Parse recommended workflow from spec output
505
+ const parsedWorkflow = specResult.output
506
+ ? parseRecommendedWorkflow(specResult.output)
507
+ : null;
508
+ if (parsedWorkflow) {
509
+ // Remove spec from phases since we already ran it
510
+ phases = parsedWorkflow.phases.filter((p) => p !== "spec");
511
+ detectedQualityLoop = parsedWorkflow.qualityLoop;
512
+ log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
513
+ }
514
+ else {
515
+ // Fall back to label-based detection
516
+ log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
517
+ const detected = detectPhasesFromLabels(labels);
518
+ phases = detected.phases.filter((p) => p !== "spec");
519
+ detectedQualityLoop = detected.qualityLoop;
520
+ log(chalk.gray(` Fallback: ${phases.join(" ")}`));
462
521
  }
463
522
  }
464
523
  else {
@@ -497,6 +556,35 @@ export async function runIssueWithLogging(ctx) {
497
556
  }
498
557
  }
499
558
  }
559
+ // Add security-review phase if requested (and spec was in the phases).
560
+ // Idempotent vs label-based auto-detection — only inserts if not present.
561
+ if (options.securityReview &&
562
+ (phases.includes("spec") || specAlreadyRan) &&
563
+ !phases.includes("security-review")) {
564
+ if (specAlreadyRan) {
565
+ phases.unshift("security-review");
566
+ }
567
+ else {
568
+ const specIndex = phases.indexOf("spec");
569
+ if (specIndex !== -1) {
570
+ phases.splice(specIndex + 1, 0, "security-review");
571
+ }
572
+ }
573
+ }
574
+ // #672 AC-2: surface the resolved phase pipeline to the renderer so it can
575
+ // seed pending cells for every phase before any one of them fires. This
576
+ // runs once per issue after all phase-list mutations (auto-detect, resume
577
+ // filter, testgen/security-review insertion). The full pipeline for the row
578
+ // is `spec` (if it already ran) plus the remaining `phases` array.
579
+ if (onPhasePlan) {
580
+ const fullPlan = specAlreadyRan ? ["spec", ...phases] : [...phases];
581
+ try {
582
+ onPhasePlan(issueNumber, fullPlan);
583
+ }
584
+ catch {
585
+ /* renderer wiring errors must not halt execution */
586
+ }
587
+ }
500
588
  // Build per-issue config with issue type metadata for skill env propagation
501
589
  const lowerLabelsForType = labels.map((l) => l.toLowerCase());
502
590
  const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
@@ -514,27 +602,17 @@ export async function runIssueWithLogging(ctx) {
514
602
  loopTriggered = true;
515
603
  }
516
604
  let phasesFailed = false;
517
- // Calculate total phases for progress indicator
518
- // If spec already ran in auto-detect mode, it's counted separately
519
- const totalPhases = specAlreadyRan ? phases.length + 1 : phases.length;
520
- const phaseIndexOffset = specAlreadyRan ? 1 : 0;
521
605
  for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
522
606
  const phase = phases[phaseIdx];
523
- const phaseNumber = phaseIdx + 1 + phaseIndexOffset;
524
- // Create spinner for this phase (suppressed in parallel mode)
525
- const phaseSpinner = config.parallel
526
- ? undefined
527
- : new PhaseSpinner({
528
- phase,
529
- phaseIndex: phaseNumber,
530
- totalPhases,
531
- shutdownManager,
532
- iteration: useQualityLoop ? iteration : undefined,
533
- });
534
- phaseSpinner?.start();
535
- emitProgressLine(issueNumber, phase, "start");
607
+ // RunRenderer (#618) owns phase progress via emitProgressLine + onProgress.
608
+ // #624 Item 3: surface the outer-loop iteration on every retried phase
609
+ // event so the renderer can label them `(attempt N/M)`. First-attempt
610
+ // events still get `iteration: 1` so the data flow is uniform; the
611
+ // renderer's `formatRetrySuffix` suppresses the suffix when iteration ≤ 1.
612
+ const phaseExtra = { iteration };
613
+ emitProgressLine(issueNumber, phase, "start", phaseExtra);
536
614
  try {
537
- onProgress?.(issueNumber, phase, "start");
615
+ onProgress?.(issueNumber, phase, "start", phaseExtra);
538
616
  }
539
617
  catch {
540
618
  /* progress errors must not halt */
@@ -549,15 +627,14 @@ export async function runIssueWithLogging(ctx) {
549
627
  }
550
628
  }
551
629
  const phaseStartTime = new Date();
552
- const result = await executePhaseWithRetry(issueNumber, phase, issueConfig, sessionId, worktreePath, shutdownManager, phaseSpinner);
630
+ const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
553
631
  const phaseEndTime = new Date();
554
- // Capture session ID for subsequent phases
555
- if (result.sessionId) {
556
- sessionId = result.sessionId;
557
- // Update session ID in state for resume capability
632
+ // Capture resume handle for subsequent phases (#674).
633
+ if (result.resumeHandle) {
634
+ resumeHandle = result.resumeHandle;
558
635
  if (stateManager) {
559
636
  try {
560
- await stateManager.updateSessionId(issueNumber, result.sessionId);
637
+ await stateManager.updateResumeHandle(issueNumber, result.resumeHandle);
561
638
  }
562
639
  catch {
563
640
  // State tracking errors shouldn't stop execution
@@ -568,7 +645,7 @@ export async function runIssueWithLogging(ctx) {
568
645
  // Emit completion/failure progress event (AC-8)
569
646
  const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
570
647
  if (result.success) {
571
- const extra = { durationSeconds: phaseDurationSec };
648
+ const extra = { durationSeconds: phaseDurationSec, iteration };
572
649
  emitProgressLine(issueNumber, phase, "complete", extra);
573
650
  try {
574
651
  onProgress?.(issueNumber, phase, "complete", extra);
@@ -578,7 +655,7 @@ export async function runIssueWithLogging(ctx) {
578
655
  }
579
656
  }
580
657
  else {
581
- const extra = { error: result.error ?? "unknown" };
658
+ const extra = { error: result.error ?? "unknown", iteration };
582
659
  emitProgressLine(issueNumber, phase, "failed", extra);
583
660
  try {
584
661
  onProgress?.(issueNumber, phase, "failed", extra);
@@ -645,27 +722,18 @@ export async function runIssueWithLogging(ctx) {
645
722
  }
646
723
  }
647
724
  if (result.success) {
648
- phaseSpinner?.succeed();
725
+ // Phase succeeded — RunRenderer (#618) updates state via onProgress.
649
726
  }
650
727
  else {
651
- phaseSpinner?.fail(result.error);
652
728
  phasesFailed = true;
653
729
  // If quality loop enabled, run loop phase to fix issues
654
730
  if (useQualityLoop && iteration < maxIterations) {
655
- // Create spinner for loop phase (suppressed in parallel mode)
656
- const loopSpinner = config.parallel
657
- ? undefined
658
- : new PhaseSpinner({
659
- phase: "loop",
660
- phaseIndex: phaseNumber,
661
- totalPhases,
662
- shutdownManager,
663
- iteration,
664
- });
665
- loopSpinner?.start();
666
- emitProgressLine(issueNumber, "loop", "start");
731
+ // #624 Item 3 (AC-3.3): the loop phase carries the current outer
732
+ // iteration so the live-zone status cell can show `loop N/M`.
733
+ const loopStartExtra = { iteration };
734
+ emitProgressLine(issueNumber, "loop", "start", loopStartExtra);
667
735
  try {
668
- onProgress?.(issueNumber, "loop", "start");
736
+ onProgress?.(issueNumber, "loop", "start", loopStartExtra);
669
737
  }
670
738
  catch {
671
739
  /* progress errors must not halt */
@@ -680,13 +748,13 @@ export async function runIssueWithLogging(ctx) {
680
748
  promptContext: buildLoopContext(result),
681
749
  };
682
750
  const loopStartTime = new Date();
683
- const loopResult = await executePhaseWithRetry(issueNumber, "loop", loopConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
751
+ const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
684
752
  const loopEndTime = new Date();
685
753
  phaseResults.push(loopResult);
686
754
  // Emit loop completion/failure progress event (AC-8)
687
755
  const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
688
756
  if (loopResult.success) {
689
- const extra = { durationSeconds: loopDurationSec };
757
+ const extra = { durationSeconds: loopDurationSec, iteration };
690
758
  emitProgressLine(issueNumber, "loop", "complete", extra);
691
759
  try {
692
760
  onProgress?.(issueNumber, "loop", "complete", extra);
@@ -696,7 +764,7 @@ export async function runIssueWithLogging(ctx) {
696
764
  }
697
765
  }
698
766
  else {
699
- const extra = { error: loopResult.error ?? "unknown" };
767
+ const extra = { error: loopResult.error ?? "unknown", iteration };
700
768
  emitProgressLine(issueNumber, "loop", "failed", extra);
701
769
  try {
702
770
  onProgress?.(issueNumber, "loop", "failed", extra);
@@ -705,17 +773,13 @@ export async function runIssueWithLogging(ctx) {
705
773
  /* progress errors must not halt */
706
774
  }
707
775
  }
708
- if (loopResult.sessionId) {
709
- sessionId = loopResult.sessionId;
776
+ if (loopResult.resumeHandle) {
777
+ resumeHandle = loopResult.resumeHandle;
710
778
  }
711
779
  if (loopResult.success) {
712
- loopSpinner?.succeed();
713
780
  // Continue to next iteration
714
781
  break;
715
782
  }
716
- else {
717
- loopSpinner?.fail(loopResult.error);
718
- }
719
783
  }
720
784
  // Stop on first failure (if not in quality loop or loop failed)
721
785
  break;
@@ -766,7 +830,15 @@ export async function runIssueWithLogging(ctx) {
766
830
  let prUrl;
767
831
  const shouldCreatePR = success && worktreePath && branch && !options.noPr;
768
832
  if (shouldCreatePR) {
769
- const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels);
833
+ // #605: under --stacked, target predecessor branch (only for non-first,
834
+ // non-last issues). Last PR keeps `main` so partial progress can land.
835
+ const stackOptions = chain?.predecessorBranch || chain?.stackManifest
836
+ ? {
837
+ prBase: chain.predecessorBranch,
838
+ stackManifest: chain.stackManifest,
839
+ }
840
+ : undefined;
841
+ const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions);
770
842
  if (prResult.success && prResult.prNumber && prResult.prUrl) {
771
843
  prNumber = prResult.prNumber;
772
844
  prUrl = prResult.prUrl;
@@ -784,6 +856,23 @@ export async function runIssueWithLogging(ctx) {
784
856
  }
785
857
  }
786
858
  }
859
+ // Deactivate relay (#383) — archive inbox/outbox transcripts to
860
+ // .sequant/logs/relay/ before worktree teardown (AC-D2). Never throws.
861
+ if (relayActivation) {
862
+ try {
863
+ await deactivateRelay(issueNumber, {
864
+ phase: phaseResults[phaseResults.length - 1]?.phase ?? "exec",
865
+ startedAt: relayActivation.startedAt,
866
+ worktreePath,
867
+ stateManager: stateManager ?? null,
868
+ });
869
+ }
870
+ catch (err) {
871
+ if (config.verbose) {
872
+ log(chalk.yellow(` ! Relay deactivation failed: ${err}`));
873
+ }
874
+ }
875
+ }
787
876
  return {
788
877
  issueNumber,
789
878
  success,