sequant 2.9.0 → 2.10.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 (188) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +14 -3
  4. package/dist/bin/cli.js +97 -17
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +56 -2
  8. package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
  14. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
  25. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
  26. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
  27. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
  28. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  31. package/dist/src/commands/assess-render.d.ts +23 -0
  32. package/dist/src/commands/assess-render.js +60 -0
  33. package/dist/src/commands/doctor.js +8 -13
  34. package/dist/src/commands/init.js +13 -1
  35. package/dist/src/commands/locks.js +2 -0
  36. package/dist/src/commands/logs.js +14 -5
  37. package/dist/src/commands/merge.js +154 -2
  38. package/dist/src/commands/ready-tui-adapter.js +6 -1
  39. package/dist/src/commands/ready.d.ts +25 -3
  40. package/dist/src/commands/ready.js +39 -13
  41. package/dist/src/commands/resume.d.ts +113 -0
  42. package/dist/src/commands/resume.js +193 -0
  43. package/dist/src/commands/run-display.js +37 -4
  44. package/dist/src/commands/run-flags.d.ts +42 -1
  45. package/dist/src/commands/run-flags.js +53 -1
  46. package/dist/src/commands/run-progress.js +26 -1
  47. package/dist/src/commands/run.js +11 -10
  48. package/dist/src/commands/state.js +7 -0
  49. package/dist/src/commands/status.d.ts +9 -0
  50. package/dist/src/commands/status.js +24 -1
  51. package/dist/src/commands/sync.js +90 -19
  52. package/dist/src/commands/update.js +28 -5
  53. package/dist/src/lib/ac-parser.d.ts +14 -0
  54. package/dist/src/lib/ac-parser.js +99 -5
  55. package/dist/src/lib/assess/index.d.ts +10 -0
  56. package/dist/src/lib/assess/index.js +9 -0
  57. package/dist/src/lib/assess/renderer.d.ts +64 -0
  58. package/dist/src/lib/assess/renderer.js +481 -0
  59. package/dist/src/lib/assess/types.d.ts +224 -0
  60. package/dist/src/lib/assess/types.js +241 -0
  61. package/dist/src/lib/ci/outputs.d.ts +15 -2
  62. package/dist/src/lib/ci/outputs.js +17 -5
  63. package/dist/src/lib/cli-flags.d.ts +33 -0
  64. package/dist/src/lib/cli-flags.js +46 -0
  65. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  66. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  67. package/dist/src/lib/cli-ui/run-renderer.js +62 -0
  68. package/dist/src/lib/errors.d.ts +78 -6
  69. package/dist/src/lib/errors.js +133 -15
  70. package/dist/src/lib/locks/index.d.ts +3 -3
  71. package/dist/src/lib/locks/index.js +2 -2
  72. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  73. package/dist/src/lib/locks/lock-manager.js +47 -3
  74. package/dist/src/lib/locks/types.d.ts +34 -3
  75. package/dist/src/lib/locks/types.js +16 -0
  76. package/dist/src/lib/mcp-config.d.ts +45 -0
  77. package/dist/src/lib/mcp-config.js +77 -1
  78. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  79. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  80. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  81. package/dist/src/lib/merge-check/command-result.js +56 -0
  82. package/dist/src/lib/merge-check/index.d.ts +4 -0
  83. package/dist/src/lib/merge-check/index.js +1 -1
  84. package/dist/src/lib/merge-check/report.js +5 -1
  85. package/dist/src/lib/merge-check/types.d.ts +9 -0
  86. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  87. package/dist/src/lib/merge-check/watch.js +215 -0
  88. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  89. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  90. package/dist/src/lib/settings.d.ts +14 -0
  91. package/dist/src/lib/settings.js +61 -8
  92. package/dist/src/lib/shutdown.d.ts +42 -2
  93. package/dist/src/lib/shutdown.js +75 -5
  94. package/dist/src/lib/skills-check.d.ts +26 -0
  95. package/dist/src/lib/skills-check.js +31 -0
  96. package/dist/src/lib/stacks.d.ts +110 -1
  97. package/dist/src/lib/stacks.js +244 -5
  98. package/dist/src/lib/templates.d.ts +74 -0
  99. package/dist/src/lib/templates.js +171 -16
  100. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  101. package/dist/src/lib/test-tautology-detector.js +153 -12
  102. package/dist/src/lib/version-check.d.ts +31 -0
  103. package/dist/src/lib/version-check.js +45 -2
  104. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  105. package/dist/src/lib/workflow/batch-executor.js +477 -44
  106. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  107. package/dist/src/lib/workflow/chain-resume.js +24 -20
  108. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  109. package/dist/src/lib/workflow/completed-status.js +57 -0
  110. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  111. package/dist/src/lib/workflow/config-resolver.js +54 -2
  112. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  113. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  114. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  115. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  116. package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
  117. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  118. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  119. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  120. package/dist/src/lib/workflow/heartbeat.js +90 -0
  121. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  122. package/dist/src/lib/workflow/log-writer.js +75 -6
  123. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  124. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  125. package/dist/src/lib/workflow/phase-executor.js +457 -52
  126. package/dist/src/lib/workflow/phase-registry.js +2 -1
  127. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  128. package/dist/src/lib/workflow/platforms/github.js +152 -0
  129. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  130. package/dist/src/lib/workflow/qa-cache.js +22 -9
  131. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  132. package/dist/src/lib/workflow/ready-gate.js +25 -9
  133. package/dist/src/lib/workflow/run-log-schema.d.ts +14 -0
  134. package/dist/src/lib/workflow/run-log-schema.js +47 -1
  135. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  136. package/dist/src/lib/workflow/run-orchestrator.js +141 -20
  137. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  138. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  139. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  140. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  141. package/dist/src/lib/workflow/state-manager.js +83 -0
  142. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  143. package/dist/src/lib/workflow/state-schema.js +47 -0
  144. package/dist/src/lib/workflow/types.d.ts +126 -4
  145. package/dist/src/lib/workflow/types.js +1 -0
  146. package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
  147. package/dist/src/lib/workflow/worktree-manager.js +102 -23
  148. package/dist/src/mcp/tools/run.d.ts +9 -1
  149. package/dist/src/mcp/tools/run.js +20 -1
  150. package/dist/src/mcp/tools/status.js +6 -0
  151. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  152. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  153. package/dist/src/ui/tui/IssueBox.js +10 -2
  154. package/dist/src/ui/tui/row-cap.js +2 -1
  155. package/dist/src/ui/tui/theme.d.ts +7 -0
  156. package/dist/src/ui/tui/theme.js +9 -0
  157. package/package.json +8 -7
  158. package/templates/hooks/parallel-marker.sh +50 -0
  159. package/templates/hooks/post-tool.sh +20 -7
  160. package/templates/hooks/pre-tool.sh +75 -10
  161. package/templates/scripts/cleanup-worktree.sh +182 -19
  162. package/templates/scripts/new-feature.sh +264 -16
  163. package/templates/skills/_shared/references/trust-model.md +18 -0
  164. package/templates/skills/assess/SKILL.md +215 -390
  165. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  166. package/templates/skills/docs/SKILL.md +4 -4
  167. package/templates/skills/exec/SKILL.md +26 -6
  168. package/templates/skills/fullsolve/SKILL.md +3 -1
  169. package/templates/skills/loop/SKILL.md +38 -2
  170. package/templates/skills/merger/SKILL.md +2 -2
  171. package/templates/skills/qa/SKILL.md +337 -57
  172. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  173. package/templates/skills/qa/references/call-site-review.md +2 -2
  174. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  175. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  176. package/templates/skills/qa/references/quality-gates.md +21 -7
  177. package/templates/skills/qa/references/testing-requirements.md +1 -1
  178. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  179. package/templates/skills/release/SKILL.md +1 -1
  180. package/templates/skills/setup/SKILL.md +5 -5
  181. package/templates/skills/spec/SKILL.md +2 -0
  182. package/templates/skills/test/SKILL.md +1 -1
  183. package/dist/src/lib/phase-spinner.d.ts +0 -146
  184. package/dist/src/lib/phase-spinner.js +0 -255
  185. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  186. package/dist/src/lib/workflow/pr-operations.js +0 -326
  187. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  188. package/dist/src/lib/workflow/run-summary.js +0 -142
@@ -164,6 +164,13 @@ export interface RunResult {
164
164
  mergedOptions: RunOptions;
165
165
  /** Log writer (for reflection access) */
166
166
  logWriter: LogWriter | null;
167
+ /**
168
+ * Run wall-clock duration in seconds, computed once by the orchestrator from
169
+ * the run's start and end (#867). The post-run summary reads THIS value rather
170
+ * than re-summing per-issue durations — under `--parallel` that sum
171
+ * double-counts overlapping issues and over-reports by ~the concurrency factor.
172
+ */
173
+ wallClockDurationSeconds: number;
167
174
  }
168
175
  /**
169
176
  * CLI-free workflow execution engine.
@@ -188,6 +195,18 @@ export declare class RunOrchestrator {
188
195
  * listening (#504, AC-3).
189
196
  */
190
197
  getEmitter(): WorkflowEventEmitter;
198
+ /**
199
+ * The wrapped progress sink handed to the batch executor — every phase
200
+ * lifecycle event enters the orchestrator through here.
201
+ *
202
+ * @internal Exported for testing only. `applyProgressEvent` is private and
203
+ * depends on `issueStates`/`phaseStartTimes`/`emitter`, so there is no way
204
+ * to exercise its branches short of a full `run()`. Without this seam the
205
+ * #804 `waiting` branch would be untestable — and an untested `waiting`
206
+ * branch is not cosmetic: falling through to the complete/failed handler
207
+ * marks a merely-paused phase `failed` and emits `phase_failed`.
208
+ */
209
+ getProgressCallback(): ProgressCallback;
191
210
  /**
192
211
  * Point-in-time view of the entire run.
193
212
  *
@@ -27,6 +27,8 @@ function formatSignalLine(issue, pid, result) {
27
27
  return ` Refused to signal PID ${pid} for #${issue} (matches this process or its parent)`;
28
28
  case "pid-dead":
29
29
  return ` Could not signal PID ${pid} for #${issue} (already exited)`;
30
+ case "stale-pid-untrusted":
31
+ return ` Refused to signal PID ${pid} for #${issue} (lock is past the age ceiling; that PID has likely been recycled)`;
30
32
  case "kill-failed":
31
33
  return ` Could not signal PID ${pid} for #${issue} (kill syscall failed)`;
32
34
  case "orchestrator":
@@ -69,11 +71,13 @@ function isWorktreeDirty(worktreePath) {
69
71
  }
70
72
  return result.stdout.trim().length > 0;
71
73
  }
72
- import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, emitRunIdLine, } from "./batch-executor.js";
74
+ import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, recordIssueCompletion, emitRunIdLine, } from "./batch-executor.js";
73
75
  import { reconcileStateAtStartup } from "./state-utils.js";
74
76
  import { runChainPreflight } from "./chain-preflight.js";
77
+ import { runSkillsPreflight } from "./skills-preflight.js";
75
78
  import { getCommitHash } from "./git-diff-utils.js";
76
79
  import { planChainResumeFromState, } from "./chain-resume.js";
80
+ import { isCompletedIssueStatus } from "./completed-status.js";
77
81
  import { MetricsWriter } from "./metrics-writer.js";
78
82
  import { WorkflowEventEmitter } from "./event-emitter.js";
79
83
  import { determineOutcome } from "./metrics-schema.js";
@@ -131,6 +135,20 @@ export class RunOrchestrator {
131
135
  getEmitter() {
132
136
  return this.emitter;
133
137
  }
138
+ /**
139
+ * The wrapped progress sink handed to the batch executor — every phase
140
+ * lifecycle event enters the orchestrator through here.
141
+ *
142
+ * @internal Exported for testing only. `applyProgressEvent` is private and
143
+ * depends on `issueStates`/`phaseStartTimes`/`emitter`, so there is no way
144
+ * to exercise its branches short of a full `run()`. Without this seam the
145
+ * #804 `waiting` branch would be untestable — and an untested `waiting`
146
+ * branch is not cosmetic: falling through to the complete/failed handler
147
+ * marks a merely-paused phase `failed` and emits `phase_failed`.
148
+ */
149
+ getProgressCallback() {
150
+ return this.cfg.onProgress;
151
+ }
134
152
  /**
135
153
  * Point-in-time view of the entire run.
136
154
  *
@@ -197,6 +215,16 @@ export class RunOrchestrator {
197
215
  const wasStatus = state.status;
198
216
  if (!state.startedAt)
199
217
  state.startedAt = new Date();
218
+ // #866: a phase starting means the issue is live again, so any
219
+ // `completedAt` from an earlier terminal verdict is stale and must not
220
+ // outlive it. The #766 quality-loop recovery path reaches here holding
221
+ // one: a non-loop failure pins `failed` and stamps `completedAt` below,
222
+ // then iteration 2 re-runs the phase through this branch. Leaving it set
223
+ // published a completion time for a running issue — which froze the TUI's
224
+ // header clock for the whole recovery window. `startedAt` is deliberately
225
+ // NOT reset: the header measures the issue's total wall clock, spanning
226
+ // every iteration.
227
+ state.completedAt = undefined;
200
228
  state.status = "running";
201
229
  const now = new Date();
202
230
  this.phaseStartTimes.set(`${issue}:${phase}`, now.getTime());
@@ -224,6 +252,25 @@ export class RunOrchestrator {
224
252
  }
225
253
  return;
226
254
  }
255
+ // #804 AC-7: an auto-wait is a *paused running* phase, not a terminal one.
256
+ // This branch must precede the complete/failed fall-through below, which
257
+ // would otherwise close out the phase the moment a wait began. Surfaces the
258
+ // wait through the same `nowLine` the TUI already renders.
259
+ if (event === "waiting") {
260
+ if (!state.currentPhase || state.currentPhase.name !== phase)
261
+ return;
262
+ const line = extra?.text;
263
+ if (!line)
264
+ return;
265
+ state.currentPhase.nowLine = line;
266
+ state.currentPhase.lastActivityAt = new Date();
267
+ void this.emitter.emit("progress", {
268
+ issueNumber: issue,
269
+ phase,
270
+ text: line,
271
+ });
272
+ return;
273
+ }
227
274
  if (event === "activity") {
228
275
  // Ignore activity for stale phases (race between completion and a
229
276
  // final flushed output chunk).
@@ -369,6 +416,15 @@ export class RunOrchestrator {
369
416
  */
370
417
  static async run(init, issueArgs, batches) {
371
418
  const { manifest, onProgress, phasePauseHandle, settings } = init;
419
+ // #867: capture the run's start before any service/log setup, so wall clock
420
+ // is independent of whether the log writer exists (AC-3: correct under
421
+ // --no-log and when log init fails). This single origin is the source of
422
+ // truth for both the printed summary and the run log (AC-2).
423
+ const runStartedAt = Date.now();
424
+ // Every `return` below — the success path and all six early exits — closes
425
+ // the bracket the same way, so the subtraction lives in one place rather
426
+ // than being restated per exit and left to drift.
427
+ const wallClock = () => (Date.now() - runStartedAt) / 1000;
372
428
  // ── Config resolution ──────────────────────────────────────────────
373
429
  const resolved = RunOrchestrator.resolveConfig(init, issueArgs, batches);
374
430
  const { mergedOptions, config, baseBranch } = resolved;
@@ -384,6 +440,7 @@ export class RunOrchestrator {
384
440
  config,
385
441
  mergedOptions,
386
442
  logWriter: null,
443
+ wallClockDurationSeconds: wallClock(),
387
444
  };
388
445
  }
389
446
  // ── Services setup ─────────────────────────────────────────────────
@@ -405,6 +462,10 @@ export class RunOrchestrator {
405
462
  logPath: mergedOptions.logPath ?? settings.run.logPath,
406
463
  verbose: config.verbose,
407
464
  startCommit: getCommitHash(process.cwd()),
465
+ // #867: share the orchestrator's run origin so the log's stored
466
+ // wall clock (finalizeRunLog: (end-start)/1000) is derived from the
467
+ // same start as the summary — AC-2/AC-7 (one producer, two consumers).
468
+ startTime: new Date(runStartedAt),
408
469
  });
409
470
  await logWriter.initialize(runConfig);
410
471
  const runId = logWriter.getRunId();
@@ -424,8 +485,12 @@ export class RunOrchestrator {
424
485
  const shutdown = new ShutdownManager();
425
486
  if (logWriter) {
426
487
  const writer = logWriter;
427
- shutdown.registerCleanup("Finalize run logs", async () => {
428
- await writer.finalize();
488
+ // #856: forward the abort cause into the log. This cleanup runs on the
489
+ // SIGINT/SIGTERM path, where no phase result will ever arrive for the
490
+ // in-flight issue — without the context, `finalize()` writes that issue
491
+ // out as if the run had simply ended.
492
+ shutdown.registerCleanup("Finalize run logs", async (abort) => {
493
+ await writer.finalize(abort ? { aborted: abort } : undefined);
429
494
  });
430
495
  }
431
496
  // ── Pre-flight state guard ─────────────────────────────────────────
@@ -478,6 +543,7 @@ export class RunOrchestrator {
478
543
  config,
479
544
  mergedOptions,
480
545
  logWriter: null,
546
+ wallClockDurationSeconds: wallClock(),
481
547
  };
482
548
  }
483
549
  if (plan.allComplete) {
@@ -492,6 +558,7 @@ export class RunOrchestrator {
492
558
  config,
493
559
  mergedOptions,
494
560
  logWriter: null,
561
+ wallClockDurationSeconds: wallClock(),
495
562
  };
496
563
  }
497
564
  if (plan.skipped.length > 0) {
@@ -513,9 +580,7 @@ export class RunOrchestrator {
513
580
  for (const issueNumber of issueNumbers) {
514
581
  try {
515
582
  const issueState = await stateManager.getIssueState(issueNumber);
516
- if (issueState &&
517
- (issueState.status === "ready_for_merge" ||
518
- issueState.status === "merged")) {
583
+ if (issueState && isCompletedIssueStatus(issueState.status)) {
519
584
  console.log(chalk.yellow(` ! #${issueNumber}: already ${issueState.status} — skipping (use --force to re-run)`));
520
585
  }
521
586
  else {
@@ -541,6 +606,7 @@ export class RunOrchestrator {
541
606
  config,
542
607
  mergedOptions,
543
608
  logWriter: null,
609
+ wallClockDurationSeconds: wallClock(),
544
610
  };
545
611
  }
546
612
  }
@@ -600,6 +666,7 @@ export class RunOrchestrator {
600
666
  config,
601
667
  mergedOptions,
602
668
  logWriter: null,
669
+ wallClockDurationSeconds: wallClock(),
603
670
  };
604
671
  }
605
672
  }
@@ -630,6 +697,7 @@ export class RunOrchestrator {
630
697
  config,
631
698
  mergedOptions,
632
699
  logWriter: null,
700
+ wallClockDurationSeconds: wallClock(),
633
701
  };
634
702
  }
635
703
  }
@@ -639,6 +707,49 @@ export class RunOrchestrator {
639
707
  for (const issueNumber of issueNumbers) {
640
708
  issueInfoMap.set(issueNumber, await getIssueInfo(issueNumber));
641
709
  }
710
+ // ── Skills pre-flight (#813) ───────────────────────────────────────
711
+ // Fail fast when the driver resolves phases via `.claude/skills/` and a
712
+ // required skill is missing — BEFORE any worktree is provisioned or
713
+ // per-issue state entry written. Without this, the phase agent hunts for
714
+ // a slash command that can never resolve and the run surfaces as a bogus
715
+ // "spec retry" failure. Skipped for non-skill drivers (aider) and for
716
+ // --dry-run (no agent executes).
717
+ if (!config.dryRun) {
718
+ const preflight = await runSkillsPreflight({
719
+ agent: config.agent,
720
+ aiderSettings: config.aiderSettings,
721
+ phases: config.phases,
722
+ autoDetectPhases: resolved.autoDetectPhases,
723
+ qualityLoop: config.qualityLoop,
724
+ testgen: mergedOptions.testgen,
725
+ securityReview: mergedOptions.securityReview,
726
+ issueNumbers,
727
+ issueInfoMap,
728
+ });
729
+ if (!preflight.ok) {
730
+ bracketedConsoleLog(phasePauseHandle, chalk.red(`\n ✖ Skills pre-flight failed: ${preflight.cause}`));
731
+ bracketedConsoleLog(phasePauseHandle, chalk.red(` ${preflight.remedy}`));
732
+ shutdown.dispose();
733
+ return {
734
+ results: issueNumbers.map((issueNumber) => ({
735
+ issueNumber,
736
+ success: false,
737
+ phaseResults: [],
738
+ durationSeconds: 0,
739
+ loopTriggered: false,
740
+ abortReason: `skills pre-flight failed: ${preflight.cause}`,
741
+ })),
742
+ logPath: null,
743
+ exitCode: 1,
744
+ worktreeMap: new Map(),
745
+ issueInfoMap,
746
+ config,
747
+ mergedOptions,
748
+ logWriter: null,
749
+ wallClockDurationSeconds: wallClock(),
750
+ };
751
+ }
752
+ }
642
753
  const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
643
754
  let worktreeMap = new Map();
644
755
  if (useWorktreeIsolation && !config.dryRun) {
@@ -710,10 +821,15 @@ export class RunOrchestrator {
710
821
  endCommit: getCommitHash(process.cwd()),
711
822
  });
712
823
  }
824
+ // #867: the run's wall clock, computed once from the single origin. Both
825
+ // the metrics record and the RunResult below derive from this value — no
826
+ // consumer re-sums per-issue durations (which double-counts under
827
+ // --parallel). Same notion the log stores via finalizeRunLog above.
828
+ const wallClockDurationSeconds = wallClock();
713
829
  // ── Record metrics ─────────────────────────────────────────────
714
830
  if (!config.dryRun && results.length > 0) {
715
831
  try {
716
- await RunOrchestrator.recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers);
832
+ await RunOrchestrator.recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers, wallClockDurationSeconds);
717
833
  }
718
834
  catch (metricsError) {
719
835
  logNonFatalWarning(" ! Metrics recording failed, continuing...", metricsError, config.verbose);
@@ -729,6 +845,7 @@ export class RunOrchestrator {
729
845
  config,
730
846
  mergedOptions,
731
847
  logWriter,
848
+ wallClockDurationSeconds,
732
849
  };
733
850
  }
734
851
  finally {
@@ -925,14 +1042,13 @@ export class RunOrchestrator {
925
1042
  : undefined,
926
1043
  });
927
1044
  results.push(result);
928
- if (!result.success) {
929
- if (options.qaGate && options.chain) {
930
- const qaFailed = result.phaseResults.some((p) => p.phase === "qa" && !p.success);
931
- if (qaFailed)
932
- break;
933
- }
1045
+ // A chain halts on ANY failed link, QA or otherwise: every successor
1046
+ // rebases onto its predecessor's committed work, so continuing past a
1047
+ // failure would build on a broken or absent base. #795 removed a
1048
+ // `--qa-gate` branch here that re-tested for a QA failure before this
1049
+ // same unconditional break — it could never change the outcome.
1050
+ if (!result.success)
934
1051
  break;
935
- }
936
1052
  }
937
1053
  return results;
938
1054
  }
@@ -1017,11 +1133,12 @@ export class RunOrchestrator {
1017
1133
  });
1018
1134
  }
1019
1135
  }
1020
- if (logWriter && result.prNumber && result.prUrl) {
1021
- logWriter.setPRInfo(result.prNumber, result.prUrl, parallelIssueNumber);
1022
- }
1136
+ // Record PR info, flip status on a PR-creation failure (#879), and
1137
+ // finalize — via the shared helper so this live path stays in lockstep
1138
+ // with executeBatch. Before #879 this block omitted the failure flip, so
1139
+ // a real `sequant run` left the run-log status at `success` on PR failure.
1023
1140
  if (logWriter) {
1024
- logWriter.completeIssue(parallelIssueNumber);
1141
+ recordIssueCompletion(logWriter, result, parallelIssueNumber);
1025
1142
  }
1026
1143
  return result;
1027
1144
  }
@@ -1034,9 +1151,13 @@ export class RunOrchestrator {
1034
1151
  });
1035
1152
  }
1036
1153
  }
1037
- static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers) {
1154
+ static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers, wallClockDurationSeconds) {
1038
1155
  const metricsWriter = new MetricsWriter({ verbose: config.verbose });
1039
- const totalDuration = results.reduce((sum, r) => sum + (r.durationSeconds ?? 0), 0);
1156
+ // #867: run wall clock, not the sum of per-issue durations under
1157
+ // --parallel that sum double-counts overlapping issues (same defect the
1158
+ // SUMMARY header had). The orchestrator owns the run's start/end and passes
1159
+ // the single authoritative value in.
1160
+ const totalDuration = wallClockDurationSeconds;
1040
1161
  const allPhases = new Set();
1041
1162
  for (const result of results) {
1042
1163
  for (const pr of result.phaseResults) {
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Skills pre-flight for `sequant run` (#813).
3
+ *
4
+ * The claude-code driver executes phases as slash-command skills loaded from
5
+ * project scope only (`settingSources: ["project"]`, #19/#711). Without
6
+ * `.claude/skills/` the phase agent hunts for a command that can never
7
+ * resolve, does no work, and the run surfaces as a bogus "spec retry"
8
+ * failure. This module computes the skills a run actually needs — from the
9
+ * phases resolved for that run, not a hardcoded triple — and checks them via
10
+ * the same `checkSkillsInstalled` helper `doctor` uses, so the two cannot
11
+ * drift.
12
+ *
13
+ * Drivers whose phase prompts do the work inline (aider's `driverOverrides`
14
+ * templates in phase-registry.ts) never resolve skills, so the pre-flight is
15
+ * skipped for them via `AgentDriver.resolvesSkills`.
16
+ */
17
+ import type { AiderSettings } from "../settings.js";
18
+ import type { Phase } from "./types.js";
19
+ export interface SkillsPreflightInput {
20
+ /** Agent driver name (default claude-code). */
21
+ agent?: string;
22
+ /** Aider settings, forwarded to the driver factory. */
23
+ aiderSettings?: AiderSettings;
24
+ /** Base pipeline for explicit-phase runs (`config.phases`). */
25
+ phases: Phase[];
26
+ /** True when phases are auto-detected from labels (no explicit --phases). */
27
+ autoDetectPhases: boolean;
28
+ /** True when the quality loop may invoke the loop skill. */
29
+ qualityLoop: boolean;
30
+ /** Additive phase flags (`--testgen` / `--security-review`). */
31
+ testgen?: boolean;
32
+ securityReview?: boolean;
33
+ /** Issues in the run, with their labels (drives per-issue phase rules). */
34
+ issueNumbers: number[];
35
+ issueInfoMap: Map<number, {
36
+ title: string;
37
+ labels: string[];
38
+ }>;
39
+ /** Project root to check under (default: `process.cwd()`). */
40
+ cwd?: string;
41
+ }
42
+ export type SkillsPreflightResult = {
43
+ ok: true;
44
+ } | {
45
+ ok: false;
46
+ /** Human-readable cause, e.g. `missing skills: spec, exec, qa`. */
47
+ cause: string;
48
+ /** Missing skill names, in required order. */
49
+ missingSkills: string[];
50
+ /** Driver whose skill resolution triggered the check. */
51
+ driverName: string;
52
+ /** Remedy line for display. */
53
+ remedy: string;
54
+ };
55
+ /**
56
+ * Compute the union of skills required by the phases resolved for this run.
57
+ *
58
+ * Explicit-phase runs start from `phases` as given; auto-detect runs start
59
+ * from each issue's label-detected pipeline. Both then apply the additive
60
+ * `--testgen` / `--security-review` / UI-label rules via
61
+ * `determinePhasesForIssue`.
62
+ *
63
+ * Two deliberate over-approximations keep late-added phases covered:
64
+ *
65
+ * - An explicit `--testgen` / `--security-review` flag requires its skill
66
+ * unconditionally, even when `determinePhasesForIssue` would not insert
67
+ * the phase because `spec` is absent from the pipeline. On a resume where
68
+ * spec already completed, batch-executor inserts the phase anyway
69
+ * (`phases.includes("spec") || specAlreadyRan`), and the pre-flight cannot
70
+ * cheaply know `specAlreadyRan` — requiring the skill the user asked for
71
+ * is the safe superset.
72
+ * - The loop skill is required when the quality loop is enabled up front OR
73
+ * when any issue's labels would auto-enable it (`complex`/`refactor`/...,
74
+ * via `detectPhasesFromLabels().qualityLoop`), since the loop skill is
75
+ * invoked the same way as any phase skill.
76
+ *
77
+ * Phases recommended later by spec output (`parseRecommendedWorkflow`)
78
+ * remain unknowable at pre-flight time — the accepted gap documented on
79
+ * #813.
80
+ *
81
+ * Exported for direct unit testing (AC-2).
82
+ */
83
+ export declare function resolveRequiredSkills(input: Pick<SkillsPreflightInput, "phases" | "autoDetectPhases" | "qualityLoop" | "testgen" | "securityReview" | "issueNumbers" | "issueInfoMap">): string[];
84
+ /**
85
+ * Run the skills pre-flight. Returns `{ok: true}` when the run may proceed:
86
+ * either every required skill is installed, or the selected driver does not
87
+ * resolve skills at all (AC-3).
88
+ */
89
+ export declare function runSkillsPreflight(input: SkillsPreflightInput): Promise<SkillsPreflightResult>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Skills pre-flight for `sequant run` (#813).
3
+ *
4
+ * The claude-code driver executes phases as slash-command skills loaded from
5
+ * project scope only (`settingSources: ["project"]`, #19/#711). Without
6
+ * `.claude/skills/` the phase agent hunts for a command that can never
7
+ * resolve, does no work, and the run surfaces as a bogus "spec retry"
8
+ * failure. This module computes the skills a run actually needs — from the
9
+ * phases resolved for that run, not a hardcoded triple — and checks them via
10
+ * the same `checkSkillsInstalled` helper `doctor` uses, so the two cannot
11
+ * drift.
12
+ *
13
+ * Drivers whose phase prompts do the work inline (aider's `driverOverrides`
14
+ * templates in phase-registry.ts) never resolve skills, so the pre-flight is
15
+ * skipped for them via `AgentDriver.resolvesSkills`.
16
+ */
17
+ import { getDriver } from "./drivers/index.js";
18
+ import { detectPhasesFromLabels, determinePhasesForIssue, } from "./phase-mapper.js";
19
+ import { phaseRegistry } from "./phase-registry.js";
20
+ import { checkSkillsInstalled, SKILLS_DIR } from "../skills-check.js";
21
+ /**
22
+ * Compute the union of skills required by the phases resolved for this run.
23
+ *
24
+ * Explicit-phase runs start from `phases` as given; auto-detect runs start
25
+ * from each issue's label-detected pipeline. Both then apply the additive
26
+ * `--testgen` / `--security-review` / UI-label rules via
27
+ * `determinePhasesForIssue`.
28
+ *
29
+ * Two deliberate over-approximations keep late-added phases covered:
30
+ *
31
+ * - An explicit `--testgen` / `--security-review` flag requires its skill
32
+ * unconditionally, even when `determinePhasesForIssue` would not insert
33
+ * the phase because `spec` is absent from the pipeline. On a resume where
34
+ * spec already completed, batch-executor inserts the phase anyway
35
+ * (`phases.includes("spec") || specAlreadyRan`), and the pre-flight cannot
36
+ * cheaply know `specAlreadyRan` — requiring the skill the user asked for
37
+ * is the safe superset.
38
+ * - The loop skill is required when the quality loop is enabled up front OR
39
+ * when any issue's labels would auto-enable it (`complex`/`refactor`/...,
40
+ * via `detectPhasesFromLabels().qualityLoop`), since the loop skill is
41
+ * invoked the same way as any phase skill.
42
+ *
43
+ * Phases recommended later by spec output (`parseRecommendedWorkflow`)
44
+ * remain unknowable at pre-flight time — the accepted gap documented on
45
+ * #813.
46
+ *
47
+ * Exported for direct unit testing (AC-2).
48
+ */
49
+ export function resolveRequiredSkills(input) {
50
+ const additiveFlags = {
51
+ testgen: input.testgen,
52
+ securityReview: input.securityReview,
53
+ };
54
+ const requiredPhases = new Set();
55
+ let qualityLoop = input.qualityLoop;
56
+ for (const issueNumber of input.issueNumbers) {
57
+ const labels = input.issueInfoMap.get(issueNumber)?.labels ?? [];
58
+ const detected = input.autoDetectPhases
59
+ ? detectPhasesFromLabels(labels)
60
+ : null;
61
+ if (detected?.qualityLoop)
62
+ qualityLoop = true;
63
+ const basePhases = detected ? detected.phases : input.phases;
64
+ for (const phase of determinePhasesForIssue(basePhases, labels, additiveFlags)) {
65
+ requiredPhases.add(phase);
66
+ }
67
+ }
68
+ if (input.testgen)
69
+ requiredPhases.add("testgen");
70
+ if (input.securityReview)
71
+ requiredPhases.add("security-review");
72
+ if (qualityLoop)
73
+ requiredPhases.add("loop");
74
+ return [...requiredPhases].map((phase) => phaseRegistry.get(phase).skill);
75
+ }
76
+ /**
77
+ * Run the skills pre-flight. Returns `{ok: true}` when the run may proceed:
78
+ * either every required skill is installed, or the selected driver does not
79
+ * resolve skills at all (AC-3).
80
+ */
81
+ export async function runSkillsPreflight(input) {
82
+ let driver;
83
+ try {
84
+ driver = getDriver(input.agent, {
85
+ aiderSettings: input.aiderSettings,
86
+ });
87
+ }
88
+ catch {
89
+ // Unknown driver name (bad `settings.run.agent`). Don't let the
90
+ // pre-flight be the thing that crashes the run with a raw throw —
91
+ // skip it and let phase-executor surface the unknown-driver error
92
+ // through its normal per-issue failure path.
93
+ return { ok: true };
94
+ }
95
+ if (!driver.resolvesSkills)
96
+ return { ok: true };
97
+ const requiredSkills = resolveRequiredSkills(input);
98
+ const { skillsDirExists, missingSkills } = await checkSkillsInstalled(requiredSkills, input.cwd);
99
+ if (missingSkills.length === 0)
100
+ return { ok: true };
101
+ const cause = skillsDirExists
102
+ ? `missing skills: ${missingSkills.join(", ")}`
103
+ : `missing ${SKILLS_DIR}/ directory (needs: ${missingSkills.join(", ")})`;
104
+ return {
105
+ ok: false,
106
+ cause,
107
+ missingSkills,
108
+ driverName: driver.name,
109
+ remedy: `The ${driver.name} driver resolves phases from ${SKILLS_DIR}/ — ` +
110
+ `run \`sequant sync\` to install them, then re-run.`,
111
+ };
112
+ }
@@ -221,13 +221,20 @@ export async function reconcileStateAtStartup(options = {}) {
221
221
  const state = await manager.getState();
222
222
  const advanced = [];
223
223
  const stillPending = [];
224
- // Find issues in ready_for_merge, in_progress, or waiting_for_qa_gate state.
224
+ // Find issues whose PR may have merged since their status was written.
225
225
  // in_progress covers PRs merged outside this session (#592).
226
226
  // waiting_for_qa_gate covers PRs merged before the next QA-gate run (#606).
227
+ // waiting_for_human_merge covers #817's `--ready-gate` terminal: a gated
228
+ // issue never reaches ready_for_merge, so without it a gated issue whose PR
229
+ // a human then merged stayed here forever and never advanced to merged
230
+ // (#837). Note this list is deliberately WIDER than `isCompletedIssueStatus`
231
+ // — it asks "might this have a merged PR?", not "is this done?", which is
232
+ // why in_progress belongs here but not there.
227
233
  for (const [issueNumStr, issueState] of Object.entries(state.issues)) {
228
234
  if (issueState.status !== "ready_for_merge" &&
229
235
  issueState.status !== "in_progress" &&
230
- issueState.status !== "waiting_for_qa_gate") {
236
+ issueState.status !== "waiting_for_qa_gate" &&
237
+ issueState.status !== "waiting_for_human_merge") {
231
238
  continue;
232
239
  }
233
240
  const issueNum = parseInt(issueNumStr, 10);
@@ -126,6 +126,39 @@ export declare class StateManager {
126
126
  * Update PR information for an issue
127
127
  */
128
128
  updatePRInfo(issueNumber: number, pr: PRInfo): Promise<void>;
129
+ /**
130
+ * Record or clear an in-progress auto-wait (#860). `wakeAtMs` sets the
131
+ * pause marker (and which phase is paused); `null` clears it on wake.
132
+ *
133
+ * Unlike the sibling updaters this NEVER throws on an untracked issue —
134
+ * it is called from a liveness hook during a live wait, and a bookkeeping
135
+ * miss must not interrupt the pause it is describing.
136
+ */
137
+ updateAutoWait(issueNumber: number, phase: string, wakeAtMs: number | null): Promise<void>;
138
+ /**
139
+ * Record a durable waitable-window halt (#892 AC-1). Preserves an existing
140
+ * re-entry counter so repeated halts on a still-closed window keep counting
141
+ * toward the re-entry bound instead of resetting it.
142
+ *
143
+ * Like `updateAutoWait`, NEVER throws on an untracked issue — it runs on a
144
+ * halt path where bookkeeping must not mask the real failure.
145
+ */
146
+ updateWindowHalt(issueNumber: number, phase: string, resumeAtMs: number): Promise<void>;
147
+ /**
148
+ * Clear a stale window-halt record (#892) — on any phase success (the
149
+ * window demonstrably reopened; progress also resets the re-entry counter)
150
+ * or a non-window failure (the halt cause is no longer waitable, so an
151
+ * unattended re-entry must not fire on it). No-ops (no state write) when
152
+ * nothing is recorded; never throws on an untracked issue.
153
+ */
154
+ clearWindowHalt(issueNumber: number): Promise<void>;
155
+ /**
156
+ * Consume one re-entry attempt for a halted issue (#892 AC-3). Returns the
157
+ * new count, or `null` when the issue has no window-halt record (nothing to
158
+ * resume). Called by `sequant resume` immediately before re-entry so a
159
+ * re-entry that halts again (window still closed) is already counted.
160
+ */
161
+ incrementWindowHaltReentries(issueNumber: number): Promise<number | null>;
129
162
  /**
130
163
  * Update worktree information for an issue
131
164
  */