sequant 2.1.2 → 2.3.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +73 -0
  4. package/dist/bin/cli.js +95 -9
  5. package/dist/src/commands/doctor.d.ts +25 -0
  6. package/dist/src/commands/doctor.js +36 -1
  7. package/dist/src/commands/init.d.ts +1 -0
  8. package/dist/src/commands/init.js +118 -0
  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 +39 -0
  13. package/dist/src/commands/prompt.js +179 -0
  14. package/dist/src/commands/run-display.d.ts +26 -0
  15. package/dist/src/commands/run-display.js +150 -0
  16. package/dist/src/commands/run-progress.d.ts +32 -0
  17. package/dist/src/commands/run-progress.js +76 -0
  18. package/dist/src/commands/run.js +83 -73
  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 +27 -1
  22. package/dist/src/commands/watch.d.ts +16 -0
  23. package/dist/src/commands/watch.js +147 -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 +181 -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 +239 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
  36. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  37. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  38. package/dist/src/lib/locks/index.d.ts +7 -0
  39. package/dist/src/lib/locks/index.js +5 -0
  40. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  41. package/dist/src/lib/locks/lock-manager.js +433 -0
  42. package/dist/src/lib/locks/types.d.ts +59 -0
  43. package/dist/src/lib/locks/types.js +31 -0
  44. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  45. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  46. package/dist/src/lib/relay/activation.d.ts +60 -0
  47. package/dist/src/lib/relay/activation.js +122 -0
  48. package/dist/src/lib/relay/archive.d.ts +34 -0
  49. package/dist/src/lib/relay/archive.js +106 -0
  50. package/dist/src/lib/relay/frame.d.ts +20 -0
  51. package/dist/src/lib/relay/frame.js +76 -0
  52. package/dist/src/lib/relay/index.d.ts +13 -0
  53. package/dist/src/lib/relay/index.js +13 -0
  54. package/dist/src/lib/relay/paths.d.ts +43 -0
  55. package/dist/src/lib/relay/paths.js +59 -0
  56. package/dist/src/lib/relay/pid.d.ts +34 -0
  57. package/dist/src/lib/relay/pid.js +72 -0
  58. package/dist/src/lib/relay/reader.d.ts +35 -0
  59. package/dist/src/lib/relay/reader.js +115 -0
  60. package/dist/src/lib/relay/types.d.ts +68 -0
  61. package/dist/src/lib/relay/types.js +76 -0
  62. package/dist/src/lib/relay/writer.d.ts +48 -0
  63. package/dist/src/lib/relay/writer.js +113 -0
  64. package/dist/src/lib/settings.d.ts +31 -1
  65. package/dist/src/lib/settings.js +18 -3
  66. package/dist/src/lib/skill-version.d.ts +19 -0
  67. package/dist/src/lib/skill-version.js +68 -0
  68. package/dist/src/lib/templates.d.ts +1 -0
  69. package/dist/src/lib/templates.js +1 -1
  70. package/dist/src/lib/version-check.d.ts +60 -5
  71. package/dist/src/lib/version-check.js +97 -9
  72. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  73. package/dist/src/lib/workflow/batch-executor.js +249 -176
  74. package/dist/src/lib/workflow/config-resolver.js +4 -0
  75. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  76. package/dist/src/lib/workflow/heartbeat.js +194 -0
  77. package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
  78. package/dist/src/lib/workflow/phase-executor.js +276 -52
  79. package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
  80. package/dist/src/lib/workflow/phase-mapper.js +17 -20
  81. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  82. package/dist/src/lib/workflow/platforms/github.js +20 -3
  83. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  84. package/dist/src/lib/workflow/pr-status.js +41 -9
  85. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  86. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  87. package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
  88. package/dist/src/lib/workflow/run-orchestrator.js +382 -29
  89. package/dist/src/lib/workflow/run-reflect.js +1 -1
  90. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  91. package/dist/src/lib/workflow/run-state.js +14 -0
  92. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  93. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  94. package/dist/src/lib/workflow/state-manager.d.ts +12 -1
  95. package/dist/src/lib/workflow/state-manager.js +37 -0
  96. package/dist/src/lib/workflow/state-schema.d.ts +62 -0
  97. package/dist/src/lib/workflow/state-schema.js +35 -1
  98. package/dist/src/lib/workflow/types.d.ts +74 -1
  99. package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
  100. package/dist/src/lib/workflow/worktree-manager.js +76 -17
  101. package/dist/src/mcp/tools/run.d.ts +44 -0
  102. package/dist/src/mcp/tools/run.js +104 -13
  103. package/dist/src/ui/tui/App.d.ts +14 -0
  104. package/dist/src/ui/tui/App.js +41 -0
  105. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  106. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  107. package/dist/src/ui/tui/Header.d.ts +6 -0
  108. package/dist/src/ui/tui/Header.js +15 -0
  109. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  110. package/dist/src/ui/tui/IssueBox.js +68 -0
  111. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  112. package/dist/src/ui/tui/Spinner.js +18 -0
  113. package/dist/src/ui/tui/index.d.ts +15 -0
  114. package/dist/src/ui/tui/index.js +29 -0
  115. package/dist/src/ui/tui/theme.d.ts +29 -0
  116. package/dist/src/ui/tui/theme.js +52 -0
  117. package/dist/src/ui/tui/truncate.d.ts +11 -0
  118. package/dist/src/ui/tui/truncate.js +31 -0
  119. package/package.json +10 -3
  120. package/templates/agents/sequant-explorer.md +1 -0
  121. package/templates/agents/sequant-qa-checker.md +2 -1
  122. package/templates/agents/sequant-testgen.md +1 -0
  123. package/templates/hooks/post-tool.sh +11 -0
  124. package/templates/hooks/pre-tool.sh +18 -9
  125. package/templates/hooks/relay-check.sh +107 -0
  126. package/templates/relay/frame.txt +11 -0
  127. package/templates/scripts/cleanup-worktree.sh +25 -3
  128. package/templates/scripts/new-feature.sh +6 -0
  129. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  130. package/templates/skills/_shared/references/subagent-types.md +21 -8
  131. package/templates/skills/assess/SKILL.md +261 -94
  132. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  133. package/templates/skills/docs/SKILL.md +141 -22
  134. package/templates/skills/exec/SKILL.md +10 -49
  135. package/templates/skills/fullsolve/SKILL.md +80 -32
  136. package/templates/skills/loop/SKILL.md +28 -0
  137. package/templates/skills/merger/SKILL.md +621 -0
  138. package/templates/skills/qa/SKILL.md +746 -8
  139. package/templates/skills/qa/scripts/quality-checks.sh +47 -1
  140. package/templates/skills/setup/SKILL.md +6 -0
  141. package/templates/skills/spec/SKILL.md +217 -964
  142. package/templates/skills/spec/references/parallel-groups.md +7 -0
  143. package/templates/skills/spec/references/quality-checklist.md +75 -0
  144. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  145. package/templates/skills/test/SKILL.md +0 -27
  146. package/templates/skills/testgen/SKILL.md +24 -44
@@ -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,6 +254,9 @@ 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) {
@@ -303,162 +347,174 @@ export async function runIssueWithLogging(ctx) {
303
347
  }
304
348
  }
305
349
  }
350
+ // Activate relay (#383) if enabled. Tolerates errors — relay must never
351
+ // block the underlying run.
352
+ let relayActivation = null;
353
+ if (config.relayEnabled && !config.dryRun) {
354
+ try {
355
+ relayActivation = await activateRelay(issueNumber, {
356
+ worktreePath,
357
+ stateManager: stateManager ?? null,
358
+ });
359
+ if (relayActivation.warning && config.verbose) {
360
+ log(chalk.yellow(` ! Relay: ${relayActivation.warning}`));
361
+ }
362
+ else if (relayActivation.activated && config.verbose) {
363
+ log(chalk.gray(` Relay active — use \`sequant prompt ${issueNumber} "<msg>"\` to nudge`));
364
+ }
365
+ }
366
+ catch (err) {
367
+ if (config.verbose) {
368
+ log(chalk.yellow(` ! Relay activation failed: ${err}`));
369
+ }
370
+ }
371
+ }
306
372
  // Determine phases for this specific issue
307
373
  let phases;
308
374
  let detectedQualityLoop = false;
309
375
  let specAlreadyRan = false;
310
376
  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(" → ")}`));
377
+ // #533: Always run spec to get recommended workflow.
378
+ // The prior bug/docs shortcut (skip spec → exec → qa) was removed because
379
+ // bug and docs issues often contain design decisions (scope boundaries,
380
+ // edge cases, test-strategy shifts) that benefit from a spec pass.
381
+ log(chalk.gray(` Running spec to determine workflow...`));
382
+ // RunRenderer (#618) owns spec progress via emitProgressLine + onProgress.
383
+ // The legacy PhaseSpinner produced duplicate lines for single-issue runs.
384
+ emitProgressLine(issueNumber, "spec", "start");
385
+ try {
386
+ onProgress?.(issueNumber, "spec", "start");
325
387
  }
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");
388
+ catch {
389
+ /* progress errors must not halt */
390
+ }
391
+ // Track spec phase start in state
392
+ if (stateManager) {
340
393
  try {
341
- onProgress?.(issueNumber, "spec", "start");
394
+ await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
342
395
  }
343
396
  catch {
344
- /* progress errors must not halt */
397
+ // State tracking errors shouldn't stop execution
345
398
  }
346
- // Track spec phase start in state
399
+ }
400
+ const specStartTime = new Date();
401
+ // Note: spec runs in main repo (not worktree) for planning
402
+ const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
403
+ shutdownManager);
404
+ const specEndTime = new Date();
405
+ if (specResult.sessionId) {
406
+ sessionId = specResult.sessionId;
407
+ // Update session ID in state for resume capability
347
408
  if (stateManager) {
348
409
  try {
349
- await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
410
+ await stateManager.updateSessionId(issueNumber, specResult.sessionId);
350
411
  }
351
412
  catch {
352
413
  // State tracking errors shouldn't stop execution
353
414
  }
354
415
  }
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
- }
416
+ }
417
+ phaseResults.push(specResult);
418
+ specAlreadyRan = true;
419
+ // Emit completion/failure progress event (AC-8)
420
+ const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
421
+ if (specResult.success) {
422
+ const extra = { durationSeconds: specDurationSec };
423
+ emitProgressLine(issueNumber, "spec", "complete", extra);
424
+ try {
425
+ onProgress?.(issueNumber, "spec", "complete", extra);
371
426
  }
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
- }
427
+ catch {
428
+ /* progress errors must not halt */
385
429
  }
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
- }
430
+ }
431
+ else {
432
+ const extra = { error: specResult.error ?? "unknown" };
433
+ emitProgressLine(issueNumber, "spec", "failed", extra);
434
+ try {
435
+ onProgress?.(issueNumber, "spec", "failed", extra);
395
436
  }
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);
437
+ catch {
438
+ /* progress errors must not halt */
419
439
  }
420
- // Track spec phase completion in state
421
- if (stateManager) {
440
+ }
441
+ // Log spec phase result
442
+ // Note: Spec runs in main repo, not worktree, so no git diff stats
443
+ if (logWriter) {
444
+ // Build errorContext from captured stderr/stdout tails (#447)
445
+ let specErrorContext;
446
+ if (!specResult.success && specResult.stderrTail) {
447
+ const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
448
+ specErrorContext = {
449
+ stderrTail: specResult.stderrTail ?? [],
450
+ stdoutTail: specResult.stdoutTail ?? [],
451
+ exitCode: specResult.exitCode,
452
+ category: errorTypeToCategory(specError),
453
+ errorType: specError.name,
454
+ errorMetadata: specError.metadata,
455
+ isRetryable: specError.isRetryable,
456
+ };
457
+ }
458
+ const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
459
+ ? "success"
460
+ : specResult.error?.includes("Timeout")
461
+ ? "timeout"
462
+ : "failure", { error: specResult.error, errorContext: specErrorContext });
463
+ logWriter.logPhase(phaseLog);
464
+ }
465
+ // Track spec phase completion in state
466
+ if (stateManager) {
467
+ try {
468
+ const phaseStatus = specResult.success ? "completed" : "failed";
469
+ await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
470
+ error: specResult.error,
471
+ });
472
+ }
473
+ catch {
474
+ // State tracking errors shouldn't stop execution
475
+ }
476
+ }
477
+ if (!specResult.success) {
478
+ const durationSeconds = (Date.now() - startTime) / 1000;
479
+ // Archive relay state on early exit (spec failure).
480
+ if (relayActivation) {
422
481
  try {
423
- const phaseStatus = specResult.success ? "completed" : "failed";
424
- await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
425
- error: specResult.error,
482
+ await deactivateRelay(issueNumber, {
483
+ phase: "spec",
484
+ startedAt: relayActivation.startedAt,
485
+ worktreePath,
486
+ stateManager: stateManager ?? null,
426
487
  });
427
488
  }
428
489
  catch {
429
- // State tracking errors shouldn't stop execution
490
+ /* swallow */
430
491
  }
431
492
  }
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
- }
493
+ return {
494
+ issueNumber,
495
+ success: false,
496
+ phaseResults,
497
+ durationSeconds,
498
+ loopTriggered: false,
499
+ };
500
+ }
501
+ // Parse recommended workflow from spec output
502
+ const parsedWorkflow = specResult.output
503
+ ? parseRecommendedWorkflow(specResult.output)
504
+ : null;
505
+ if (parsedWorkflow) {
506
+ // Remove spec from phases since we already ran it
507
+ phases = parsedWorkflow.phases.filter((p) => p !== "spec");
508
+ detectedQualityLoop = parsedWorkflow.qualityLoop;
509
+ log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
510
+ }
511
+ else {
512
+ // Fall back to label-based detection
513
+ log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
514
+ const detected = detectPhasesFromLabels(labels);
515
+ phases = detected.phases.filter((p) => p !== "spec");
516
+ detectedQualityLoop = detected.qualityLoop;
517
+ log(chalk.gray(` Fallback: ${phases.join(" ")}`));
462
518
  }
463
519
  }
464
520
  else {
@@ -497,6 +553,21 @@ export async function runIssueWithLogging(ctx) {
497
553
  }
498
554
  }
499
555
  }
556
+ // Add security-review phase if requested (and spec was in the phases).
557
+ // Idempotent vs label-based auto-detection — only inserts if not present.
558
+ if (options.securityReview &&
559
+ (phases.includes("spec") || specAlreadyRan) &&
560
+ !phases.includes("security-review")) {
561
+ if (specAlreadyRan) {
562
+ phases.unshift("security-review");
563
+ }
564
+ else {
565
+ const specIndex = phases.indexOf("spec");
566
+ if (specIndex !== -1) {
567
+ phases.splice(specIndex + 1, 0, "security-review");
568
+ }
569
+ }
570
+ }
500
571
  // Build per-issue config with issue type metadata for skill env propagation
501
572
  const lowerLabelsForType = labels.map((l) => l.toLowerCase());
502
573
  const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
@@ -514,27 +585,17 @@ export async function runIssueWithLogging(ctx) {
514
585
  loopTriggered = true;
515
586
  }
516
587
  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
588
  for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
522
589
  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");
590
+ // RunRenderer (#618) owns phase progress via emitProgressLine + onProgress.
591
+ // #624 Item 3: surface the outer-loop iteration on every retried phase
592
+ // event so the renderer can label them `(attempt N/M)`. First-attempt
593
+ // events still get `iteration: 1` so the data flow is uniform; the
594
+ // renderer's `formatRetrySuffix` suppresses the suffix when iteration ≤ 1.
595
+ const phaseExtra = { iteration };
596
+ emitProgressLine(issueNumber, phase, "start", phaseExtra);
536
597
  try {
537
- onProgress?.(issueNumber, phase, "start");
598
+ onProgress?.(issueNumber, phase, "start", phaseExtra);
538
599
  }
539
600
  catch {
540
601
  /* progress errors must not halt */
@@ -549,7 +610,7 @@ export async function runIssueWithLogging(ctx) {
549
610
  }
550
611
  }
551
612
  const phaseStartTime = new Date();
552
- const result = await executePhaseWithRetry(issueNumber, phase, issueConfig, sessionId, worktreePath, shutdownManager, phaseSpinner);
613
+ const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress), sessionId, worktreePath, shutdownManager);
553
614
  const phaseEndTime = new Date();
554
615
  // Capture session ID for subsequent phases
555
616
  if (result.sessionId) {
@@ -568,7 +629,7 @@ export async function runIssueWithLogging(ctx) {
568
629
  // Emit completion/failure progress event (AC-8)
569
630
  const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
570
631
  if (result.success) {
571
- const extra = { durationSeconds: phaseDurationSec };
632
+ const extra = { durationSeconds: phaseDurationSec, iteration };
572
633
  emitProgressLine(issueNumber, phase, "complete", extra);
573
634
  try {
574
635
  onProgress?.(issueNumber, phase, "complete", extra);
@@ -578,7 +639,7 @@ export async function runIssueWithLogging(ctx) {
578
639
  }
579
640
  }
580
641
  else {
581
- const extra = { error: result.error ?? "unknown" };
642
+ const extra = { error: result.error ?? "unknown", iteration };
582
643
  emitProgressLine(issueNumber, phase, "failed", extra);
583
644
  try {
584
645
  onProgress?.(issueNumber, phase, "failed", extra);
@@ -645,27 +706,18 @@ export async function runIssueWithLogging(ctx) {
645
706
  }
646
707
  }
647
708
  if (result.success) {
648
- phaseSpinner?.succeed();
709
+ // Phase succeeded — RunRenderer (#618) updates state via onProgress.
649
710
  }
650
711
  else {
651
- phaseSpinner?.fail(result.error);
652
712
  phasesFailed = true;
653
713
  // If quality loop enabled, run loop phase to fix issues
654
714
  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");
715
+ // #624 Item 3 (AC-3.3): the loop phase carries the current outer
716
+ // iteration so the live-zone status cell can show `loop N/M`.
717
+ const loopStartExtra = { iteration };
718
+ emitProgressLine(issueNumber, "loop", "start", loopStartExtra);
667
719
  try {
668
- onProgress?.(issueNumber, "loop", "start");
720
+ onProgress?.(issueNumber, "loop", "start", loopStartExtra);
669
721
  }
670
722
  catch {
671
723
  /* progress errors must not halt */
@@ -680,13 +732,13 @@ export async function runIssueWithLogging(ctx) {
680
732
  promptContext: buildLoopContext(result),
681
733
  };
682
734
  const loopStartTime = new Date();
683
- const loopResult = await executePhaseWithRetry(issueNumber, "loop", loopConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
735
+ const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), sessionId, worktreePath, shutdownManager);
684
736
  const loopEndTime = new Date();
685
737
  phaseResults.push(loopResult);
686
738
  // Emit loop completion/failure progress event (AC-8)
687
739
  const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
688
740
  if (loopResult.success) {
689
- const extra = { durationSeconds: loopDurationSec };
741
+ const extra = { durationSeconds: loopDurationSec, iteration };
690
742
  emitProgressLine(issueNumber, "loop", "complete", extra);
691
743
  try {
692
744
  onProgress?.(issueNumber, "loop", "complete", extra);
@@ -696,7 +748,7 @@ export async function runIssueWithLogging(ctx) {
696
748
  }
697
749
  }
698
750
  else {
699
- const extra = { error: loopResult.error ?? "unknown" };
751
+ const extra = { error: loopResult.error ?? "unknown", iteration };
700
752
  emitProgressLine(issueNumber, "loop", "failed", extra);
701
753
  try {
702
754
  onProgress?.(issueNumber, "loop", "failed", extra);
@@ -709,13 +761,9 @@ export async function runIssueWithLogging(ctx) {
709
761
  sessionId = loopResult.sessionId;
710
762
  }
711
763
  if (loopResult.success) {
712
- loopSpinner?.succeed();
713
764
  // Continue to next iteration
714
765
  break;
715
766
  }
716
- else {
717
- loopSpinner?.fail(loopResult.error);
718
- }
719
767
  }
720
768
  // Stop on first failure (if not in quality loop or loop failed)
721
769
  break;
@@ -747,7 +795,7 @@ export async function runIssueWithLogging(ctx) {
747
795
  }
748
796
  // Create checkpoint commit in chain mode after QA passes
749
797
  if (success && chainMode && worktreePath) {
750
- createCheckpointCommit(worktreePath, issueNumber, config.verbose);
798
+ createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
751
799
  }
752
800
  // Rebase onto the base branch before PR creation (unless --no-rebase)
753
801
  // This ensures the branch is up-to-date and prevents lockfile drift
@@ -766,7 +814,15 @@ export async function runIssueWithLogging(ctx) {
766
814
  let prUrl;
767
815
  const shouldCreatePR = success && worktreePath && branch && !options.noPr;
768
816
  if (shouldCreatePR) {
769
- const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels);
817
+ // #605: under --stacked, target predecessor branch (only for non-first,
818
+ // non-last issues). Last PR keeps `main` so partial progress can land.
819
+ const stackOptions = chain?.predecessorBranch || chain?.stackManifest
820
+ ? {
821
+ prBase: chain.predecessorBranch,
822
+ stackManifest: chain.stackManifest,
823
+ }
824
+ : undefined;
825
+ const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions);
770
826
  if (prResult.success && prResult.prNumber && prResult.prUrl) {
771
827
  prNumber = prResult.prNumber;
772
828
  prUrl = prResult.prUrl;
@@ -784,6 +840,23 @@ export async function runIssueWithLogging(ctx) {
784
840
  }
785
841
  }
786
842
  }
843
+ // Deactivate relay (#383) — archive inbox/outbox transcripts to
844
+ // .sequant/logs/relay/ before worktree teardown (AC-D2). Never throws.
845
+ if (relayActivation) {
846
+ try {
847
+ await deactivateRelay(issueNumber, {
848
+ phase: phaseResults[phaseResults.length - 1]?.phase ?? "exec",
849
+ startedAt: relayActivation.startedAt,
850
+ worktreePath,
851
+ stateManager: stateManager ?? null,
852
+ });
853
+ }
854
+ catch (err) {
855
+ if (config.verbose) {
856
+ log(chalk.yellow(` ! Relay deactivation failed: ${err}`));
857
+ }
858
+ }
859
+ }
787
860
  return {
788
861
  issueNumber,
789
862
  success,
@@ -146,6 +146,9 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
146
146
  : (settings.run.retry ?? true);
147
147
  const isSequential = mergedOptions.sequential ?? false;
148
148
  const isParallel = !isSequential && issueCount > 1;
149
+ // `--no-relay` arrives from Commander as `mergedOptions.relay === false`;
150
+ // explicit `false` overrides settings, otherwise settings/default win.
151
+ const relayEnabled = mergedOptions.relay === false ? false : (settings.run.relay ?? true);
149
152
  return {
150
153
  ...DEFAULT_CONFIG,
151
154
  phases: explicitPhases ?? DEFAULT_PHASES,
@@ -163,5 +166,6 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
163
166
  agent: mergedOptions.agent ?? settings.run.agent,
164
167
  aiderSettings: settings.run.aider,
165
168
  isolateParallel: mergedOptions.isolateParallel,
169
+ relayEnabled,
166
170
  };
167
171
  }