sequant 2.8.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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +23 -4
  4. package/dist/bin/cli.js +99 -18
  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 +58 -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 +38 -10
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +405 -67
  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 +250 -344
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +10 -7
  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 +4 -2
  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/reflect/SKILL.md +27 -13
  28. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  29. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  30. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +11 -3
  31. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  32. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  33. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  34. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  35. package/dist/src/commands/assess-render.d.ts +23 -0
  36. package/dist/src/commands/assess-render.js +60 -0
  37. package/dist/src/commands/doctor.js +8 -13
  38. package/dist/src/commands/init.js +13 -1
  39. package/dist/src/commands/locks.js +2 -0
  40. package/dist/src/commands/logs.js +20 -6
  41. package/dist/src/commands/merge.js +154 -2
  42. package/dist/src/commands/ready-tui-adapter.js +6 -1
  43. package/dist/src/commands/ready.d.ts +25 -3
  44. package/dist/src/commands/ready.js +39 -13
  45. package/dist/src/commands/resume.d.ts +113 -0
  46. package/dist/src/commands/resume.js +193 -0
  47. package/dist/src/commands/run-display.d.ts +20 -0
  48. package/dist/src/commands/run-display.js +117 -5
  49. package/dist/src/commands/run-flags.d.ts +42 -1
  50. package/dist/src/commands/run-flags.js +53 -1
  51. package/dist/src/commands/run-progress.js +26 -1
  52. package/dist/src/commands/run.js +11 -10
  53. package/dist/src/commands/state.js +7 -0
  54. package/dist/src/commands/stats.js +47 -0
  55. package/dist/src/commands/status.d.ts +9 -0
  56. package/dist/src/commands/status.js +24 -1
  57. package/dist/src/commands/sync.js +90 -19
  58. package/dist/src/commands/update.js +28 -5
  59. package/dist/src/lib/ac-parser.d.ts +14 -0
  60. package/dist/src/lib/ac-parser.js +99 -5
  61. package/dist/src/lib/assess/index.d.ts +10 -0
  62. package/dist/src/lib/assess/index.js +9 -0
  63. package/dist/src/lib/assess/renderer.d.ts +64 -0
  64. package/dist/src/lib/assess/renderer.js +481 -0
  65. package/dist/src/lib/assess/types.d.ts +224 -0
  66. package/dist/src/lib/assess/types.js +241 -0
  67. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  68. package/dist/src/lib/assess-collision-detect.js +68 -4
  69. package/dist/src/lib/ci/outputs.d.ts +15 -2
  70. package/dist/src/lib/ci/outputs.js +17 -5
  71. package/dist/src/lib/cli-flags.d.ts +33 -0
  72. package/dist/src/lib/cli-flags.js +46 -0
  73. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  74. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  75. package/dist/src/lib/cli-ui/run-renderer.js +79 -9
  76. package/dist/src/lib/errors.d.ts +84 -6
  77. package/dist/src/lib/errors.js +142 -17
  78. package/dist/src/lib/locks/index.d.ts +3 -3
  79. package/dist/src/lib/locks/index.js +2 -2
  80. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  81. package/dist/src/lib/locks/lock-manager.js +47 -3
  82. package/dist/src/lib/locks/types.d.ts +34 -3
  83. package/dist/src/lib/locks/types.js +16 -0
  84. package/dist/src/lib/manifest.js +1 -17
  85. package/dist/src/lib/mcp-config.d.ts +45 -0
  86. package/dist/src/lib/mcp-config.js +77 -1
  87. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  88. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  89. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  90. package/dist/src/lib/merge-check/command-result.js +56 -0
  91. package/dist/src/lib/merge-check/index.d.ts +4 -0
  92. package/dist/src/lib/merge-check/index.js +1 -1
  93. package/dist/src/lib/merge-check/report.js +5 -1
  94. package/dist/src/lib/merge-check/types.d.ts +9 -0
  95. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  96. package/dist/src/lib/merge-check/watch.js +215 -0
  97. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  98. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  99. package/dist/src/lib/settings.d.ts +14 -0
  100. package/dist/src/lib/settings.js +61 -8
  101. package/dist/src/lib/shutdown.d.ts +42 -2
  102. package/dist/src/lib/shutdown.js +75 -5
  103. package/dist/src/lib/skills-check.d.ts +26 -0
  104. package/dist/src/lib/skills-check.js +31 -0
  105. package/dist/src/lib/stacks.d.ts +110 -1
  106. package/dist/src/lib/stacks.js +244 -5
  107. package/dist/src/lib/templates.d.ts +74 -0
  108. package/dist/src/lib/templates.js +171 -16
  109. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  110. package/dist/src/lib/test-tautology-detector.js +153 -12
  111. package/dist/src/lib/version-check.d.ts +31 -0
  112. package/dist/src/lib/version-check.js +46 -7
  113. package/dist/src/lib/workflow/batch-executor.d.ts +138 -6
  114. package/dist/src/lib/workflow/batch-executor.js +550 -54
  115. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  116. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  117. package/dist/src/lib/workflow/chain-resume.d.ts +118 -0
  118. package/dist/src/lib/workflow/chain-resume.js +170 -0
  119. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  120. package/dist/src/lib/workflow/completed-status.js +57 -0
  121. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  122. package/dist/src/lib/workflow/config-resolver.js +54 -2
  123. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  124. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  125. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  126. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  127. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  128. package/dist/src/lib/workflow/drivers/claude-code.d.ts +21 -0
  129. package/dist/src/lib/workflow/drivers/claude-code.js +50 -8
  130. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  131. package/dist/src/lib/workflow/error-classifier.js +14 -1
  132. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  133. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  134. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  135. package/dist/src/lib/workflow/heartbeat.js +90 -0
  136. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  137. package/dist/src/lib/workflow/log-writer.js +81 -14
  138. package/dist/src/lib/workflow/metrics-schema.d.ts +42 -3
  139. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  140. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  141. package/dist/src/lib/workflow/phase-executor.d.ts +234 -13
  142. package/dist/src/lib/workflow/phase-executor.js +533 -56
  143. package/dist/src/lib/workflow/phase-registry.js +2 -1
  144. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  145. package/dist/src/lib/workflow/platforms/github.js +152 -0
  146. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  147. package/dist/src/lib/workflow/qa-cache.js +22 -9
  148. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  149. package/dist/src/lib/workflow/ready-gate.js +25 -9
  150. package/dist/src/lib/workflow/run-log-schema.d.ts +37 -0
  151. package/dist/src/lib/workflow/run-log-schema.js +92 -2
  152. package/dist/src/lib/workflow/run-orchestrator.d.ts +33 -0
  153. package/dist/src/lib/workflow/run-orchestrator.js +429 -47
  154. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  155. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  156. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  157. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  158. package/dist/src/lib/workflow/state-manager.js +83 -0
  159. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  160. package/dist/src/lib/workflow/state-schema.js +47 -0
  161. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  162. package/dist/src/lib/workflow/status-derivation.js +27 -0
  163. package/dist/src/lib/workflow/types.d.ts +149 -4
  164. package/dist/src/lib/workflow/types.js +1 -0
  165. package/dist/src/lib/workflow/worktree-manager.d.ts +70 -2
  166. package/dist/src/lib/workflow/worktree-manager.js +201 -52
  167. package/dist/src/mcp/tools/run.d.ts +11 -1
  168. package/dist/src/mcp/tools/run.js +22 -1
  169. package/dist/src/mcp/tools/status.js +6 -0
  170. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  171. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  172. package/dist/src/ui/tui/IssueBox.js +10 -2
  173. package/dist/src/ui/tui/row-cap.js +2 -1
  174. package/dist/src/ui/tui/theme.d.ts +7 -0
  175. package/dist/src/ui/tui/theme.js +9 -0
  176. package/package.json +9 -10
  177. package/templates/hooks/parallel-marker.sh +50 -0
  178. package/templates/hooks/post-tool.sh +38 -10
  179. package/templates/hooks/pre-tool.sh +405 -67
  180. package/templates/scripts/cleanup-worktree.sh +282 -30
  181. package/templates/scripts/new-feature.sh +264 -16
  182. package/templates/skills/_shared/references/trust-model.md +18 -0
  183. package/templates/skills/assess/SKILL.md +250 -344
  184. package/templates/skills/assess/references/predicted-collision-detection.md +10 -7
  185. package/templates/skills/docs/SKILL.md +4 -4
  186. package/templates/skills/exec/SKILL.md +26 -6
  187. package/templates/skills/fullsolve/SKILL.md +4 -2
  188. package/templates/skills/loop/SKILL.md +38 -2
  189. package/templates/skills/merger/SKILL.md +2 -2
  190. package/templates/skills/qa/SKILL.md +337 -57
  191. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  192. package/templates/skills/qa/references/call-site-review.md +2 -2
  193. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  194. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  195. package/templates/skills/qa/references/quality-gates.md +21 -7
  196. package/templates/skills/qa/references/testing-requirements.md +1 -1
  197. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  198. package/templates/skills/reflect/SKILL.md +27 -13
  199. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  200. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  201. package/templates/skills/release/SKILL.md +11 -3
  202. package/templates/skills/setup/SKILL.md +5 -5
  203. package/templates/skills/spec/SKILL.md +2 -0
  204. package/templates/skills/spec/references/verification-criteria.md +1 -1
  205. package/templates/skills/test/SKILL.md +1 -1
  206. package/dist/src/lib/phase-spinner.d.ts +0 -146
  207. package/dist/src/lib/phase-spinner.js +0 -255
  208. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  209. package/dist/src/lib/workflow/pr-operations.js +0 -326
  210. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  211. package/dist/src/lib/workflow/run-summary.js +0 -142
@@ -8,7 +8,8 @@ import chalk from "chalk";
8
8
  import { spawnSync } from "child_process";
9
9
  import { existsSync, readFileSync } from "fs";
10
10
  import path from "path";
11
- import { PM_CONFIG } from "../stacks.js";
11
+ import { resolvePackageManager, resolvePackageManagerConfig, } from "../stacks.js";
12
+ import { resolveDiffBase } from "./git-diff-utils.js";
12
13
  import { getResumablePhasesForIssue } from "./phase-detection.js";
13
14
  import { GitHubProvider } from "./platforms/github.js";
14
15
  /**
@@ -155,7 +156,11 @@ export function checkWorktreeFreshness(worktreePath, verbose, baseBranch = "main
155
156
  const countResult = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${mergeBase}..${baseHead}`], { stdio: "pipe" });
156
157
  if (countResult.status === 0) {
157
158
  result.commitsBehind = parseInt(countResult.stdout.toString().trim(), 10);
158
- // Consider stale if more than 5 commits behind (configurable threshold)
159
+ // Consider stale if more than 5 commits behind. NOT configurable — the
160
+ // threshold is the literal below, and a stale-but-clean worktree is
161
+ // force-removed and rebuilt on the strength of it (see the recreate
162
+ // branch in `ensureWorktree`). Surfaced in #810; left as a literal here
163
+ // because making it configurable is a behavior change, not a comment fix.
159
164
  result.isStale = result.commitsBehind > 5;
160
165
  }
161
166
  }
@@ -231,7 +236,8 @@ export function listWorktrees() {
231
236
  * Get changed files in a worktree compared to main
232
237
  */
233
238
  export function getWorktreeChangedFiles(worktreePath) {
234
- const result = spawnSync("git", ["-C", worktreePath, "diff", "--name-only", "main...HEAD"], { stdio: "pipe" });
239
+ const diffBase = resolveDiffBase(worktreePath, "main");
240
+ const result = spawnSync("git", ["-C", worktreePath, "diff", "--name-only", `${diffBase}...HEAD`], { stdio: "pipe" });
235
241
  if (result.status !== 0)
236
242
  return [];
237
243
  return result.stdout
@@ -245,7 +251,8 @@ export function getWorktreeChangedFiles(worktreePath) {
245
251
  * Returns aggregate metrics only - no file paths to preserve privacy
246
252
  */
247
253
  export function getWorktreeDiffStats(worktreePath) {
248
- const result = spawnSync("git", ["-C", worktreePath, "diff", "--stat", "main...HEAD"], { stdio: "pipe" });
254
+ const diffBase = resolveDiffBase(worktreePath, "main");
255
+ const result = spawnSync("git", ["-C", worktreePath, "diff", "--stat", `${diffBase}...HEAD`], { stdio: "pipe" });
249
256
  if (result.status !== 0) {
250
257
  return { filesChanged: 0, linesAdded: 0 };
251
258
  }
@@ -314,6 +321,63 @@ export function filterResumedPhases(issueNumber, phases, resume) {
314
321
  const skipped = phases.filter((p) => !resumable.includes(p));
315
322
  return { phases: resumable, skipped };
316
323
  }
324
+ /**
325
+ * Run the frozen dependency install for a freshly provisioned worktree and
326
+ * surface (rather than swallow) a failed install.
327
+ *
328
+ * Mirrors the status check in `reinstallIfLockfileChanged` (#846): a plain
329
+ * `npm install` self-heals a stale/absent lockfile, but the frozen `npm ci`
330
+ * hard-fails (exit 1, no node_modules). Without this check a failing install
331
+ * left a silently dependency-less worktree whose breakage only surfaced later
332
+ * as a confusing phase error. The warning names the resolved command so the
333
+ * user can rerun it by hand.
334
+ *
335
+ * @returns true if the install succeeded, false if it failed (warn-and-continue)
336
+ * @internal Exported for testing
337
+ */
338
+ export function installWorktreeDeps(worktreePath, packageManager, verbose) {
339
+ if (verbose) {
340
+ console.log(chalk.gray(` Installing dependencies...`));
341
+ }
342
+ // Two independent questions, resolved in order against the worktree itself.
343
+ //
344
+ // WHICH manager: the manifest's packageManager is a snapshot and may be
345
+ // absent, so fall back to the worktree's own lockfile — that keeps this in
346
+ // agreement with what new-feature.sh would run for the same project (#870).
347
+ //
348
+ // WHICH commands for it: yarn's frozen install differs between classic
349
+ // (`--frozen-lockfile`) and berry (`--immutable`), and both majors use
350
+ // `yarn.lock`, so the manager's identity alone cannot say which (#871).
351
+ //
352
+ // The worktree is checked out by now, so its package.json / .yarnrc.yml /
353
+ // yarn.lock are all readable here.
354
+ const pmConfig = resolvePackageManagerConfig(resolvePackageManager(packageManager, worktreePath), worktreePath);
355
+ // ciInstall, not installSilent: a plain `npm install` normalizes and
356
+ // rewrites package-lock.json (observed: npm 10 strips the `libc` fields a
357
+ // newer npm committed), so every provisioned worktree started dirty. That
358
+ // one unstaged file cascaded: rebaseBeforePR refused to run, stale
359
+ // worktrees read as having "uncommitted changes" and were never recreated,
360
+ // and chain checkpoints skipped on an out-of-scope dirty file. A frozen
361
+ // install never touches the lockfile. Same substitution #803 made for
362
+ // merge-check's combined-branch test. The `!existsSync(node_modules)`
363
+ // guard at the call site means `npm ci`'s wipe-and-reinstall has nothing
364
+ // to wipe.
365
+ const [cmd, ...args] = pmConfig.ciInstall.split(" ");
366
+ const command = [cmd, ...args].join(" ");
367
+ const installResult = spawnSync(cmd, args, {
368
+ cwd: worktreePath,
369
+ stdio: "pipe",
370
+ });
371
+ if (installResult.status !== 0) {
372
+ // stderr may be empty on some failures — the resolved command alone is
373
+ // enough to act on, so always name it (AC-2).
374
+ const error = installResult.stderr?.toString().trim() ?? "";
375
+ const detail = error ? `: ${error}` : "";
376
+ console.log(chalk.yellow(` ! Dependency install failed (${command})${detail}`));
377
+ return false;
378
+ }
379
+ return true;
380
+ }
317
381
  /**
318
382
  * Create or reuse a worktree for an issue
319
383
  * @param baseBranch - Optional branch to use as base instead of origin/main (for chain mode)
@@ -365,21 +429,14 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
365
429
  if (verbose) {
366
430
  console.log(chalk.gray(` Rebasing existing worktree onto chain base (${baseBranch})...`));
367
431
  }
368
- const rebaseResult = spawnSync("git", ["-C", existingPath, "rebase", baseBranch], { stdio: "pipe" });
369
- if (rebaseResult.status !== 0) {
370
- const rebaseError = rebaseResult.stderr.toString();
371
- // Check if it's a conflict
372
- if (rebaseError.includes("CONFLICT") ||
373
- rebaseError.includes("could not apply")) {
432
+ const rebase = rebaseOntoLocalBranch(existingPath, baseBranch, verbose);
433
+ if (!rebase.success) {
434
+ if (rebase.conflict) {
374
435
  console.log(chalk.yellow(` ! Rebase conflict detected. Aborting rebase and keeping original branch state.`));
375
436
  console.log(chalk.yellow(` ℹ️ Branch ${branch} is not properly chained. Manual rebase may be required.`));
376
- // Abort the rebase to restore branch state
377
- spawnSync("git", ["-C", existingPath, "rebase", "--abort"], {
378
- stdio: "pipe",
379
- });
380
437
  }
381
438
  else {
382
- console.log(chalk.yellow(` ! Rebase failed: ${rebaseError.trim()}`));
439
+ console.log(chalk.yellow(` ! Rebase failed: ${rebase.error ?? ""}`));
383
440
  console.log(chalk.yellow(` ℹ️ Continuing with branch in its original state.`));
384
441
  }
385
442
  return {
@@ -390,9 +447,6 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
390
447
  rebased: false,
391
448
  };
392
449
  }
393
- if (verbose) {
394
- console.log(chalk.green(` ✔ Existing worktree rebased onto ${baseBranch}`));
395
- }
396
450
  return {
397
451
  issue: issueNumber,
398
452
  path: existingPath,
@@ -523,17 +577,7 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
523
577
  // Install dependencies if needed
524
578
  const nodeModulesPath = path.join(worktreePath, "node_modules");
525
579
  if (!existsSync(nodeModulesPath)) {
526
- if (verbose) {
527
- console.log(chalk.gray(` Installing dependencies...`));
528
- }
529
- // Use detected package manager or default to npm
530
- const pm = packageManager || "npm";
531
- const pmConfig = PM_CONFIG[pm];
532
- const [cmd, ...args] = pmConfig.installSilent.split(" ");
533
- spawnSync(cmd, args, {
534
- cwd: worktreePath,
535
- stdio: "pipe",
536
- });
580
+ installWorktreeDeps(worktreePath, packageManager, verbose);
537
581
  }
538
582
  if (verbose) {
539
583
  console.log(chalk.green(` ✔ Worktree ready: ${worktreePath}`));
@@ -700,9 +744,10 @@ passed QA in chain mode. It serves as a recovery point if later issues fail.`;
700
744
  const commitResult = spawnSync("git", ["-C", worktreePath, "commit", "-m", commitMessage], { stdio: "pipe" });
701
745
  if (commitResult.status !== 0) {
702
746
  const error = commitResult.stderr.toString();
703
- if (verbose) {
704
- console.log(chalk.yellow(` ! Could not create checkpoint commit: ${error}`));
705
- }
747
+ // #760: chain resume rebases the next link onto this checkpoint, so a
748
+ // commit failure is surfaced regardless of --verbose (the call site adds
749
+ // the resume-impact warning; here we show the underlying git error).
750
+ console.log(chalk.yellow(` ! Could not create checkpoint commit for #${issueNumber}: ${error.trim()}`));
706
751
  return false;
707
752
  }
708
753
  console.log(chalk.green(` 📌 Checkpoint commit created for #${issueNumber}`));
@@ -751,9 +796,14 @@ export function reinstallIfLockfileChanged(worktreePath, packageManager, verbose
751
796
  }
752
797
  // Re-run install to sync node_modules with updated lockfile
753
798
  console.log(chalk.blue(` Reinstalling dependencies (lockfile changed)...`));
754
- const pm = packageManager || "npm";
755
- const pmConfig = PM_CONFIG[pm];
756
- const [cmd, ...args] = pmConfig.installSilent.split(" ");
799
+ // Same two-step resolution as installWorktreeDeps: manifest-or-lockfile for
800
+ // the manager (#870), then classic-vs-berry for its commands (#871).
801
+ const pmConfig = resolvePackageManagerConfig(resolvePackageManager(packageManager, worktreePath), worktreePath);
802
+ // ciInstall for the same reason as provisioning (see ensureWorktree): this
803
+ // reinstall exists because a rebase pulled in a NEW lockfile, so installing
804
+ // exactly what that lockfile says — never rewriting it — is the semantic
805
+ // the function's own name promises.
806
+ const [cmd, ...args] = pmConfig.ciInstall.split(" ");
757
807
  const installResult = spawnSync(cmd, args, {
758
808
  cwd: worktreePath,
759
809
  stdio: "pipe",
@@ -766,6 +816,39 @@ export function reinstallIfLockfileChanged(worktreePath, packageManager, verbose
766
816
  console.log(chalk.green(` ✔ Dependencies reinstalled`));
767
817
  return true;
768
818
  }
819
+ /**
820
+ * Rebase a worktree's branch onto a *local* branch ref (e.g. a chain
821
+ * predecessor's feature branch), NOT origin/main. Used to chain a successor
822
+ * onto its predecessor's committed work at execution time (#748), and to
823
+ * re-chain a pre-existing worktree onto its chain base.
824
+ *
825
+ * On conflict, aborts the rebase to restore the original branch state and
826
+ * returns `{ success: false, conflict: true }` so callers can warn rather than
827
+ * silently treat a broken link as healthy.
828
+ */
829
+ export function rebaseOntoLocalBranch(worktreePath, ontoBranch, verbose = false) {
830
+ const rebaseResult = spawnSync("git", ["-C", worktreePath, "rebase", ontoBranch], { stdio: "pipe" });
831
+ if (rebaseResult.status === 0) {
832
+ if (verbose) {
833
+ console.log(chalk.green(` ✔ Rebased worktree onto ${ontoBranch}`));
834
+ }
835
+ return { performed: true, success: true, conflict: false };
836
+ }
837
+ const rebaseError = rebaseResult.stderr.toString();
838
+ const isConflict = rebaseError.includes("CONFLICT") || rebaseError.includes("could not apply");
839
+ if (isConflict) {
840
+ // Abort to restore the original branch state.
841
+ spawnSync("git", ["-C", worktreePath, "rebase", "--abort"], {
842
+ stdio: "pipe",
843
+ });
844
+ }
845
+ return {
846
+ performed: true,
847
+ success: false,
848
+ conflict: isConflict,
849
+ error: rebaseError.trim(),
850
+ };
851
+ }
769
852
  /**
770
853
  * Rebase the worktree branch onto the base branch before PR creation.
771
854
  * This ensures the branch is up-to-date and prevents lockfile drift.
@@ -832,11 +915,84 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
832
915
  reinstalled,
833
916
  };
834
917
  }
918
+ /**
919
+ * Render a non-A+ QA verdict as a PR-body note, or `null` for an A+ / absent
920
+ * verdict.
921
+ *
922
+ * Only stopping states that still break to a PR reach this path
923
+ * (`AC_MET_BUT_NOT_A_PLUS`, `NEEDS_VERIFICATION`); `READY_FOR_MERGE` and an
924
+ * absent verdict produce no note. (#749)
925
+ */
926
+ function qaVerdictNote(verdict) {
927
+ if (!verdict || verdict === "READY_FOR_MERGE")
928
+ return null;
929
+ switch (verdict) {
930
+ case "AC_MET_BUT_NOT_A_PLUS":
931
+ return `> **QA verdict: AC_MET_BUT_NOT_A_PLUS** — acceptance criteria met, but not A+. The run broke to this PR for review rather than entering the quality loop; see the QA review comment for improvement notes.`;
932
+ case "NEEDS_VERIFICATION":
933
+ return `> **QA verdict: NEEDS_VERIFICATION** — acceptance criteria met, pending external verification (e.g. CI or a manual test). See the QA review comment.`;
934
+ default:
935
+ return `> **QA verdict: ${verdict}** — see the QA review comment.`;
936
+ }
937
+ }
938
+ /**
939
+ * Build the automated PR body for `sequant run`.
940
+ *
941
+ * Pure and I/O-free, kept separate from {@link createPR} (which shells out to
942
+ * `gh`) so the body — including the non-A+ QA note (#749) — is unit-testable
943
+ * directly.
944
+ *
945
+ * @param issueNumber Issue the PR closes
946
+ * @param opts.stackManifest Appended before the trailer under --stacked (#605)
947
+ * @param opts.qaVerdict QA verdict for the run; a non-A+ stopping state
948
+ * (anything other than `READY_FOR_MERGE`) surfaces a note so a human reviewer
949
+ * sees why the run broke to PR without reaching A+ (#749).
950
+ * @param opts.readyGateReport Rendered `sequant ready` gap report (#817). Set
951
+ * only when the run used `--ready-gate`; surfaces the gate outcome (threshold
952
+ * reached vs guard halt) in the PR body the same way `sequant ready` does.
953
+ * @internal Exported for testing
954
+ */
955
+ export function buildAutomatedPRBody(issueNumber, opts) {
956
+ const bodyLines = [
957
+ `## Summary`,
958
+ ``,
959
+ `Automated PR for issue #${issueNumber}.`,
960
+ ``,
961
+ `Fixes #${issueNumber}`,
962
+ ``,
963
+ ];
964
+ // #749: surface a non-A+ QA verdict in the PR body (not just the run log) so
965
+ // a reviewer sees why the run broke to PR rather than reaching A+.
966
+ //
967
+ // #817: when the ready gate ran, its report already carries the terminal
968
+ // verdict and stop reason (a superset of the #749 note), so prefer it and
969
+ // suppress the standalone note to avoid a contradictory double-headline.
970
+ if (opts?.readyGateReport) {
971
+ bodyLines.push(opts.readyGateReport, ``);
972
+ }
973
+ else {
974
+ const note = qaVerdictNote(opts?.qaVerdict);
975
+ if (note) {
976
+ bodyLines.push(note, ``);
977
+ }
978
+ }
979
+ // #605 AC-4: emit stack manifest before the trailer so reviewers see the
980
+ // chain at the top of the body. Manifest is only set under --stacked.
981
+ if (opts?.stackManifest) {
982
+ bodyLines.push(opts.stackManifest, ``);
983
+ }
984
+ bodyLines.push(`---`, `🤖 Generated by \`sequant run\``);
985
+ return bodyLines.join("\n");
986
+ }
835
987
  /**
836
988
  * Push branch and create a PR after successful QA.
837
989
  *
838
990
  * Handles both fresh PR creation and detection of existing PRs.
839
- * Failures are warnings — they don't fail the run.
991
+ *
992
+ * Returns `{ success: false, error }` on failure. As of #879 the caller
993
+ * (`runIssueWithLogging`) treats an attempted-but-failed PR as a run failure:
994
+ * a passing run that produced no PR is not a success. This function itself
995
+ * still only reports — it never throws — so relay teardown runs regardless.
840
996
  *
841
997
  * @param worktreePath Path to the worktree
842
998
  * @param issueNumber Issue number
@@ -847,10 +1003,13 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
847
1003
  * @param stackOptions When set under --stacked, `prBase` overrides the default
848
1004
  * PR target (otherwise gh defaults to the repo's default branch) and
849
1005
  * `stackManifest` is appended to the PR body. (#605)
1006
+ * @param qaVerdict QA verdict surfaced in the PR body when non-A+ (#749).
1007
+ * @param readyGateReport Rendered `sequant ready` gap report, set only when the
1008
+ * run used `--ready-gate` (#817).
850
1009
  * @returns PRCreationResult with PR info or error
851
1010
  * @internal Exported for testing
852
1011
  */
853
- export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions) {
1012
+ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions, qaVerdict, readyGateReport) {
854
1013
  const github = new GitHubProvider();
855
1014
  // Step 1: Check for existing PR on this branch
856
1015
  const existingPRInfo = github.viewPRByBranchSync(branch, worktreePath);
@@ -886,21 +1045,11 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
886
1045
  const isBug = labels?.some((l) => /^bug/i.test(l));
887
1046
  const prefix = isBug ? "fix" : "feat";
888
1047
  const prTitle = `${prefix}(#${issueNumber}): ${issueTitle}`;
889
- const bodyLines = [
890
- `## Summary`,
891
- ``,
892
- `Automated PR for issue #${issueNumber}.`,
893
- ``,
894
- `Fixes #${issueNumber}`,
895
- ``,
896
- ];
897
- // #605 AC-4: emit stack manifest before the trailer so reviewers see the
898
- // chain at the top of the body. Manifest is only set under --stacked.
899
- if (stackOptions?.stackManifest) {
900
- bodyLines.push(stackOptions.stackManifest, ``);
901
- }
902
- bodyLines.push(`---`, `🤖 Generated by \`sequant run\``);
903
- const prBody = bodyLines.join("\n");
1048
+ const prBody = buildAutomatedPRBody(issueNumber, {
1049
+ stackManifest: stackOptions?.stackManifest,
1050
+ qaVerdict,
1051
+ readyGateReport,
1052
+ });
904
1053
  const prResult = github.createPRCliSync(prTitle, prBody, branch, worktreePath, stackOptions?.prBase);
905
1054
  if (prResult.exitCode !== 0) {
906
1055
  const prError = prResult.stderr.trim() || "Unknown error";
@@ -32,6 +32,8 @@ interface RunToolResponse {
32
32
  total: number;
33
33
  passed: number;
34
34
  failed: number;
35
+ /** Issues that ended `partial` — timed out, no failure, no recovery (#766). */
36
+ partial: number;
35
37
  durationSeconds: number;
36
38
  };
37
39
  phases: string;
@@ -92,13 +94,21 @@ export declare function parseRunIdLine(line: string): string | null;
92
94
  export interface ProgressEvent {
93
95
  issue: number;
94
96
  phase: string;
95
- event: "start" | "complete" | "failed";
97
+ event: "start" | "complete" | "failed" | "waiting";
96
98
  durationSeconds?: number;
97
99
  error?: string;
100
+ /** #860: epoch ms of the auto-wait wake; absent on the terminal notice. */
101
+ wakeAtMs?: number;
102
+ /** #860: ms remaining in the auto-wait (0 on the terminal notice). */
103
+ remainingMs?: number;
98
104
  }
99
105
  /**
100
106
  * Parse a SEQUANT_PROGRESS line emitted by the batch executor.
101
107
  * Returns the parsed event or null if the line isn't a progress line.
108
+ *
109
+ * `waiting` events (#860) are the auto-wait liveness signal: a run paused on
110
+ * a rate-limit window emits one per minute so an MCP client sees a deliberate
111
+ * pause with a wake time rather than silence indistinguishable from a hang.
102
112
  */
103
113
  export declare function parseProgressLine(line: string): ProgressEvent | null;
104
114
  /**
@@ -11,6 +11,7 @@ import { resolve, dirname, join } from "path";
11
11
  import { existsSync } from "fs";
12
12
  import { readdir, readFile } from "fs/promises";
13
13
  import { homedir } from "os";
14
+ import { formatResetTime } from "../../lib/errors.js";
14
15
  import { LOG_PATHS, RunLogSchema } from "../../lib/workflow/run-log-schema.js";
15
16
  import { registerRun, unregisterRun } from "../run-registry.js";
16
17
  /** Maximum total response size in bytes (64 KB) */
@@ -182,6 +183,7 @@ export function buildStructuredResponse(runLog, rawOutput, overallStatus, exitCo
182
183
  total: runLog.summary.totalIssues,
183
184
  passed: runLog.summary.passed,
184
185
  failed: runLog.summary.failed,
186
+ partial: runLog.summary.partial ?? 0,
185
187
  durationSeconds: runLog.summary.totalDurationSeconds,
186
188
  },
187
189
  phases: phasesRan || runLog.config.phases.join(","),
@@ -232,6 +234,7 @@ function buildFallbackResponse(stdout, issueNumbers, overallStatus, phases, exit
232
234
  total: issueNumbers.length,
233
235
  passed: overallStatus === "success" ? issueNumbers.length : 0,
234
236
  failed: overallStatus === "failure" ? issueNumbers.length : 0,
237
+ partial: 0,
235
238
  durationSeconds: 0,
236
239
  },
237
240
  phases,
@@ -259,6 +262,10 @@ export function parseRunIdLine(line) {
259
262
  /**
260
263
  * Parse a SEQUANT_PROGRESS line emitted by the batch executor.
261
264
  * Returns the parsed event or null if the line isn't a progress line.
265
+ *
266
+ * `waiting` events (#860) are the auto-wait liveness signal: a run paused on
267
+ * a rate-limit window emits one per minute so an MCP client sees a deliberate
268
+ * pause with a wake time rather than silence indistinguishable from a hang.
262
269
  */
263
270
  export function parseProgressLine(line) {
264
271
  if (!line.startsWith(PROGRESS_LINE_PREFIX))
@@ -270,7 +277,8 @@ export function parseProgressLine(line) {
270
277
  typeof json.event === "string" &&
271
278
  (json.event === "start" ||
272
279
  json.event === "complete" ||
273
- json.event === "failed")) {
280
+ json.event === "failed" ||
281
+ json.event === "waiting")) {
274
282
  const result = {
275
283
  issue: json.issue,
276
284
  phase: json.phase,
@@ -282,6 +290,12 @@ export function parseProgressLine(line) {
282
290
  if (typeof json.error === "string") {
283
291
  result.error = json.error;
284
292
  }
293
+ if (typeof json.wakeAtMs === "number") {
294
+ result.wakeAtMs = json.wakeAtMs;
295
+ }
296
+ if (typeof json.remainingMs === "number") {
297
+ result.remainingMs = json.remainingMs;
298
+ }
285
299
  return result;
286
300
  }
287
301
  return null;
@@ -339,6 +353,13 @@ export function formatProgressMessage(event) {
339
353
  const reason = event.error ? ` \u2014 ${event.error}` : "";
340
354
  return `${prefix}: ${event.phase} \u2717${reason}`;
341
355
  }
356
+ case "waiting": {
357
+ // #860: name the wake time so a paused run reads as deliberate.
358
+ if (event.wakeAtMs === undefined) {
359
+ return `${prefix}: ${event.phase} auto-wait complete \u2014 resuming`;
360
+ }
361
+ return `${prefix}: ${event.phase} \u23f8 rate-limit window \u2014 resuming at ${formatResetTime(event.wakeAtMs)}`;
362
+ }
342
363
  }
343
364
  }
344
365
  /**
@@ -97,6 +97,12 @@ export function registerStatusTool(server) {
97
97
  phases: issueState.phases,
98
98
  worktree: issueState.worktree,
99
99
  pr: issueState.pr,
100
+ // #860: surfaced so MCP pollers see a deliberate rate-limit
101
+ // pause (with its wake time) instead of a stale in-progress.
102
+ autoWait: issueState.autoWait,
103
+ // #892: a durable window halt — resumable via `sequant
104
+ // resume` after `resumeAt` — is a pause, not a failure.
105
+ windowHalt: issueState.windowHalt,
100
106
  lastActivity: issueState.lastActivity,
101
107
  nextAction: getNextActionHint(issueState),
102
108
  lastSynced: reconcileResult.lastSynced,
@@ -1,10 +1,19 @@
1
1
  import { type JSX } from "react";
2
2
  /**
3
- * Per-issue elapsed timer. Owns its own interval so tick-driven re-renders
4
- * are scoped to this leaf component and do not propagate to `IssueBox`.
3
+ * Per-issue elapsed timer. Reads the shared 1 Hz `now` that `App` already owns
4
+ * and threads through `IssueBox`, rather than running its own interval: `App`'s
5
+ * snapshot poller re-renders the whole tree every tick regardless, so a
6
+ * per-issue clock bought no re-render scoping.
7
+ *
8
+ * When `completedAt` is set, the duration freezes at `completedAt - startedAt`.
9
+ * This is computed in render (not by tearing down an interval on mount), so an
10
+ * issue that transitions running → passed on the same mounted component freezes
11
+ * at the transition without a remount.
5
12
  */
6
- export declare function ElapsedTimer({ startedAt }: {
13
+ export declare function ElapsedTimer({ startedAt, completedAt, now, }: {
7
14
  startedAt?: Date;
15
+ completedAt?: Date;
16
+ now: number;
8
17
  }): JSX.Element;
9
18
  /** Format an absolute timestamp as the "last activity Xs ago" stamp. */
10
19
  export declare function formatSinceActivity(now: number, activityAt: Date): string;
@@ -1,19 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
2
  import { Text } from "ink";
4
3
  /**
5
- * Per-issue elapsed timer. Owns its own interval so tick-driven re-renders
6
- * are scoped to this leaf component and do not propagate to `IssueBox`.
4
+ * Per-issue elapsed timer. Reads the shared 1 Hz `now` that `App` already owns
5
+ * and threads through `IssueBox`, rather than running its own interval: `App`'s
6
+ * snapshot poller re-renders the whole tree every tick regardless, so a
7
+ * per-issue clock bought no re-render scoping.
8
+ *
9
+ * When `completedAt` is set, the duration freezes at `completedAt - startedAt`.
10
+ * This is computed in render (not by tearing down an interval on mount), so an
11
+ * issue that transitions running → passed on the same mounted component freezes
12
+ * at the transition without a remount.
7
13
  */
8
- export function ElapsedTimer({ startedAt }) {
9
- const [now, setNow] = useState(() => Date.now());
10
- useEffect(() => {
11
- const id = setInterval(() => setNow(Date.now()), 1000);
12
- return () => clearInterval(id);
13
- }, []);
14
+ export function ElapsedTimer({ startedAt, completedAt, now, }) {
14
15
  if (!startedAt)
15
16
  return _jsx(Text, { children: "--:--" });
16
- const secs = Math.max(0, Math.floor((now - startedAt.getTime()) / 1000));
17
+ const end = completedAt?.getTime() ?? now;
18
+ const secs = Math.max(0, Math.floor((end - startedAt.getTime()) / 1000));
17
19
  const mm = Math.floor(secs / 60)
18
20
  .toString()
19
21
  .padStart(2, "0");
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { ACTIVE_PHASE_COLOR, DIVIDER_COLOR, PHASE_GLYPHS, borderColorForIssue, phaseStatusColor, } from "./theme.js";
3
+ import { ACTIVE_PHASE_COLOR, DIVIDER_COLOR, PHASE_GLYPHS, borderColorForIssue, isTerminalStatus, phaseStatusColor, } from "./theme.js";
4
4
  import { Spinner } from "./Spinner.js";
5
5
  import { ElapsedTimer, formatSinceActivity } from "./ElapsedTimer.js";
6
6
  import { truncateToWidth } from "./truncate.js";
@@ -20,7 +20,15 @@ export function IssueBox({ state, slot, width, now, }) {
20
20
  const displayPhaseN = activePhaseIndex >= 0 ? activePhaseIndex + 1 : doneCount;
21
21
  const total = state.phases.length;
22
22
  const headerTitle = truncateToWidth(`#${state.number} ${state.title}`, Math.max(10, innerWidth - 20));
23
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt })] })] }), _jsx(Divider, { width: innerWidth }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "branch " }), _jsx(Text, { children: truncateToWidth(state.branch, innerWidth - 8) })] }), _jsx(PhaseProgression, { phases: state.phases, activeColor: ACTIVE_PHASE_COLOR }), state.currentPhase?.logPath ? (_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "log " }), _jsx(Text, { children: truncateToWidth(state.currentPhase.logPath, innerWidth - 8) })] })) : null] }), _jsx(Divider, { width: innerWidth }), _jsx(Box, { flexDirection: "column", children: state.currentPhase ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "now " }), _jsx(Spinner, { color: ACTIVE_PHASE_COLOR }), _jsxs(Text, { children: [" ", truncateToWidth(state.currentPhase.nowLine, innerWidth - 12)] })] }), _jsx(Box, { children: _jsxs(Text, { color: DIVIDER_COLOR, children: [" └ last activity ", formatSinceActivity(now, state.currentPhase.lastActivityAt)] }) })] })) : (_jsx(Text, { color: DIVIDER_COLOR, children: statusLine(state) })) })] }));
23
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt,
24
+ // #866: only a terminal status may freeze the clock. Mirrors the
25
+ // plain renderer, which branches on status *before* reading
26
+ // `completedAt` (`run-renderer.ts` `statusHeader`) and so always
27
+ // measures a running issue against the live clock. Without this
28
+ // gate a stale `completedAt` on a running issue — what the #766
29
+ // quality-loop recovery used to publish — freezes the header while
30
+ // the issue is visibly working.
31
+ completedAt: isTerminalStatus(state.status) ? state.completedAt : undefined, now: now })] })] }), _jsx(Divider, { width: innerWidth }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "branch " }), _jsx(Text, { children: truncateToWidth(state.branch, innerWidth - 8) })] }), _jsx(PhaseProgression, { phases: state.phases, activeColor: ACTIVE_PHASE_COLOR }), state.currentPhase?.logPath ? (_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "log " }), _jsx(Text, { children: truncateToWidth(state.currentPhase.logPath, innerWidth - 8) })] })) : null] }), _jsx(Divider, { width: innerWidth }), _jsx(Box, { flexDirection: "column", children: state.currentPhase ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "now " }), _jsx(Spinner, { color: ACTIVE_PHASE_COLOR }), _jsxs(Text, { children: [" ", truncateToWidth(state.currentPhase.nowLine, innerWidth - 12)] })] }), _jsx(Box, { children: _jsxs(Text, { color: DIVIDER_COLOR, children: [" └ last activity ", formatSinceActivity(now, state.currentPhase.lastActivityAt)] }) })] })) : (_jsx(Text, { color: DIVIDER_COLOR, children: statusLine(state) })) })] }));
24
32
  }
25
33
  function statusLine(state) {
26
34
  switch (state.status) {
@@ -12,6 +12,7 @@
12
12
  * bottom margin). `LINES_PER_BOX` is sized accordingly so the dynamic cap
13
13
  * reflects how many boxes actually fit.
14
14
  */
15
+ import { isTerminalStatus } from "./theme.js";
15
16
  /** Static row cap (matches the plain renderer's default). */
16
17
  export const DEFAULT_TUI_ROW_CAP = 10;
17
18
  /**
@@ -27,7 +28,7 @@ export const TUI_LINES_PER_BOX = 11;
27
28
  const FIXED_OVERHEAD = 4;
28
29
  /** A queued or running issue is "active"; passed/failed are terminal. */
29
30
  function isActive(issue) {
30
- return issue.status === "queued" || issue.status === "running";
31
+ return !isTerminalStatus(issue.status);
31
32
  }
32
33
  /**
33
34
  * Effective cap: the smaller of the static cap and a dynamic terminal-height
@@ -46,6 +46,13 @@ export declare const ACTIVE_PHASE_COLOR: "#FF8012";
46
46
  /** Brand green for the rolled-up `✔ N done` summary line (#699, parity with the
47
47
  * plain renderer's #624 rollup). */
48
48
  export declare const ROLLUP_COLOR: "#10b981";
49
+ /**
50
+ * `passed`/`failed` are terminal; `queued`/`running` mean the issue is still
51
+ * live. The single definition of that split for the TUI — `row-cap` uses it to
52
+ * decide which issues can roll up, and `IssueBox` uses it to decide whether the
53
+ * elapsed clock may freeze (#866).
54
+ */
55
+ export declare function isTerminalStatus(status: IssueStatus): boolean;
49
56
  /**
50
57
  * Pick the border color for an issue.
51
58
  * Failed / passed states win over rotation; otherwise rotate by slot.
@@ -44,6 +44,15 @@ export const ACTIVE_PHASE_COLOR = BRAND_ORANGE;
44
44
  /** Brand green for the rolled-up `✔ N done` summary line (#699, parity with the
45
45
  * plain renderer's #624 rollup). */
46
46
  export const ROLLUP_COLOR = BRAND_GREEN;
47
+ /**
48
+ * `passed`/`failed` are terminal; `queued`/`running` mean the issue is still
49
+ * live. The single definition of that split for the TUI — `row-cap` uses it to
50
+ * decide which issues can roll up, and `IssueBox` uses it to decide whether the
51
+ * elapsed clock may freeze (#866).
52
+ */
53
+ export function isTerminalStatus(status) {
54
+ return status === "passed" || status === "failed";
55
+ }
47
56
  /**
48
57
  * Pick the border color for an issue.
49
58
  * Failed / passed states win over rotation; otherwise rotate by slot.