sequant 2.9.0 → 2.11.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +20 -5
  4. package/dist/bin/cli.js +143 -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 +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.d.ts +20 -1
  36. package/dist/src/commands/locks.js +208 -4
  37. package/dist/src/commands/logs.js +14 -5
  38. package/dist/src/commands/merge.js +154 -2
  39. package/dist/src/commands/ready-tui-adapter.js +6 -1
  40. package/dist/src/commands/ready.d.ts +31 -3
  41. package/dist/src/commands/ready.js +53 -13
  42. package/dist/src/commands/resume.d.ts +113 -0
  43. package/dist/src/commands/resume.js +193 -0
  44. package/dist/src/commands/run-display.js +38 -4
  45. package/dist/src/commands/run-flags.d.ts +42 -1
  46. package/dist/src/commands/run-flags.js +53 -1
  47. package/dist/src/commands/run-progress.js +26 -1
  48. package/dist/src/commands/run.js +11 -10
  49. package/dist/src/commands/state.js +7 -0
  50. package/dist/src/commands/status.d.ts +9 -0
  51. package/dist/src/commands/status.js +24 -1
  52. package/dist/src/commands/sync.js +90 -19
  53. package/dist/src/commands/update.js +28 -5
  54. package/dist/src/commands/worktree.d.ts +31 -0
  55. package/dist/src/commands/worktree.js +95 -0
  56. package/dist/src/lib/ac-parser.d.ts +14 -0
  57. package/dist/src/lib/ac-parser.js +99 -5
  58. package/dist/src/lib/assess/index.d.ts +10 -0
  59. package/dist/src/lib/assess/index.js +9 -0
  60. package/dist/src/lib/assess/renderer.d.ts +64 -0
  61. package/dist/src/lib/assess/renderer.js +481 -0
  62. package/dist/src/lib/assess/types.d.ts +224 -0
  63. package/dist/src/lib/assess/types.js +241 -0
  64. package/dist/src/lib/ci/outputs.d.ts +15 -2
  65. package/dist/src/lib/ci/outputs.js +17 -5
  66. package/dist/src/lib/cli-flags.d.ts +56 -0
  67. package/dist/src/lib/cli-flags.js +89 -0
  68. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
  69. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  70. package/dist/src/lib/cli-ui/run-renderer.js +69 -1
  71. package/dist/src/lib/errors.d.ts +78 -6
  72. package/dist/src/lib/errors.js +133 -15
  73. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  74. package/dist/src/lib/locks/checkout-lock.js +389 -0
  75. package/dist/src/lib/locks/index.d.ts +7 -4
  76. package/dist/src/lib/locks/index.js +5 -3
  77. package/dist/src/lib/locks/lock-manager.d.ts +101 -3
  78. package/dist/src/lib/locks/lock-manager.js +277 -8
  79. package/dist/src/lib/locks/types.d.ts +106 -3
  80. package/dist/src/lib/locks/types.js +44 -0
  81. package/dist/src/lib/mcp-config.d.ts +45 -0
  82. package/dist/src/lib/mcp-config.js +77 -1
  83. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  84. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  85. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  86. package/dist/src/lib/merge-check/command-result.js +56 -0
  87. package/dist/src/lib/merge-check/index.d.ts +4 -0
  88. package/dist/src/lib/merge-check/index.js +1 -1
  89. package/dist/src/lib/merge-check/report.js +5 -1
  90. package/dist/src/lib/merge-check/types.d.ts +9 -0
  91. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  92. package/dist/src/lib/merge-check/watch.js +215 -0
  93. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  94. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  95. package/dist/src/lib/settings.d.ts +87 -0
  96. package/dist/src/lib/settings.js +106 -8
  97. package/dist/src/lib/shutdown.d.ts +42 -2
  98. package/dist/src/lib/shutdown.js +75 -5
  99. package/dist/src/lib/skills-check.d.ts +26 -0
  100. package/dist/src/lib/skills-check.js +31 -0
  101. package/dist/src/lib/stacks.d.ts +110 -1
  102. package/dist/src/lib/stacks.js +244 -5
  103. package/dist/src/lib/templates.d.ts +74 -0
  104. package/dist/src/lib/templates.js +171 -16
  105. package/dist/src/lib/test-tautology-detector.d.ts +10 -2
  106. package/dist/src/lib/test-tautology-detector.js +213 -12
  107. package/dist/src/lib/version-check.d.ts +31 -0
  108. package/dist/src/lib/version-check.js +45 -2
  109. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  110. package/dist/src/lib/workflow/batch-executor.js +552 -60
  111. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  112. package/dist/src/lib/workflow/chain-resume.js +24 -20
  113. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  114. package/dist/src/lib/workflow/completed-status.js +57 -0
  115. package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
  116. package/dist/src/lib/workflow/config-resolver.js +143 -2
  117. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  118. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  119. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  120. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  121. package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
  122. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  123. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  124. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  125. package/dist/src/lib/workflow/error-classifier.js +4 -0
  126. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  127. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  128. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  129. package/dist/src/lib/workflow/heartbeat.js +90 -0
  130. package/dist/src/lib/workflow/log-writer.d.ts +54 -2
  131. package/dist/src/lib/workflow/log-writer.js +95 -6
  132. package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
  133. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  134. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  135. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  136. package/dist/src/lib/workflow/phase-detection.js +5 -1
  137. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  138. package/dist/src/lib/workflow/phase-executor.js +467 -52
  139. package/dist/src/lib/workflow/phase-registry.js +2 -1
  140. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  141. package/dist/src/lib/workflow/platforms/github.js +152 -0
  142. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  143. package/dist/src/lib/workflow/qa-cache.js +22 -9
  144. package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
  145. package/dist/src/lib/workflow/ready-gate.js +49 -12
  146. package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
  147. package/dist/src/lib/workflow/run-log-schema.js +78 -2
  148. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  149. package/dist/src/lib/workflow/run-orchestrator.js +168 -20
  150. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  151. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  152. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  153. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  154. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  155. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  156. package/dist/src/lib/workflow/state-manager.js +83 -0
  157. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  158. package/dist/src/lib/workflow/state-schema.js +47 -0
  159. package/dist/src/lib/workflow/types.d.ts +190 -4
  160. package/dist/src/lib/workflow/types.js +1 -0
  161. package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
  162. package/dist/src/lib/workflow/worktree-manager.js +111 -24
  163. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  164. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  165. package/dist/src/mcp/tools/run.d.ts +9 -1
  166. package/dist/src/mcp/tools/run.js +20 -1
  167. package/dist/src/mcp/tools/status.js +6 -0
  168. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  169. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  170. package/dist/src/ui/tui/IssueBox.js +10 -2
  171. package/dist/src/ui/tui/row-cap.js +2 -1
  172. package/dist/src/ui/tui/theme.d.ts +7 -0
  173. package/dist/src/ui/tui/theme.js +9 -0
  174. package/package.json +10 -8
  175. package/templates/hooks/parallel-marker.sh +50 -0
  176. package/templates/hooks/post-tool.sh +20 -7
  177. package/templates/hooks/pre-tool.sh +303 -10
  178. package/templates/scripts/cleanup-worktree.sh +217 -33
  179. package/templates/scripts/new-feature.sh +289 -35
  180. package/templates/skills/_shared/references/subagent-types.md +7 -18
  181. package/templates/skills/_shared/references/trust-model.md +18 -0
  182. package/templates/skills/assess/SKILL.md +220 -391
  183. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  184. package/templates/skills/docs/SKILL.md +4 -4
  185. package/templates/skills/exec/SKILL.md +87 -13
  186. package/templates/skills/fullsolve/SKILL.md +130 -22
  187. package/templates/skills/loop/SKILL.md +94 -13
  188. package/templates/skills/merger/SKILL.md +100 -12
  189. package/templates/skills/qa/SKILL.md +396 -63
  190. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  191. package/templates/skills/qa/references/call-site-review.md +2 -2
  192. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  193. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  194. package/templates/skills/qa/references/quality-gates.md +21 -7
  195. package/templates/skills/qa/references/testing-requirements.md +1 -1
  196. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  197. package/templates/skills/release/SKILL.md +80 -1
  198. package/templates/skills/setup/SKILL.md +5 -5
  199. package/templates/skills/spec/SKILL.md +33 -15
  200. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  201. package/templates/skills/test/SKILL.md +1 -1
  202. package/templates/skills/testgen/SKILL.md +23 -6
  203. package/dist/src/lib/phase-spinner.d.ts +0 -146
  204. package/dist/src/lib/phase-spinner.js +0 -255
  205. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  206. package/dist/src/lib/workflow/pr-operations.js +0 -326
  207. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  208. package/dist/src/lib/workflow/run-summary.js +0 -142
  209. package/templates/agents/sequant-explorer.md +0 -24
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Evidence-based effort escalation on quality-loop retries (#915).
3
+ *
4
+ * Sequant already detects several "this attempt is a retry" moments: the
5
+ * outer quality-loop re-entering a phase (`batch-executor.ts`) and the
6
+ * `sequant ready` QA-pass loop re-running `qa`/`loop` (`ready-gate.ts`).
7
+ * Escalation raises the phase's reasoning effort one tier for exactly that
8
+ * retried execution when the workflow observed a prior attempt fail — never
9
+ * speculatively, and never more than one tier per retry (see AC-6).
10
+ *
11
+ * Deliberately its own module rather than living beside `resolvePhasePolicies`
12
+ * in `config-resolver.ts`: `config-resolver.ts` imports `getEnvConfig` from
13
+ * `batch-executor.ts`, and `batch-executor.ts` is one of this module's
14
+ * dispatch-time callers, so co-locating here avoids introducing that cycle.
15
+ */
16
+ import { EFFORT_LEVELS } from "../settings.js";
17
+ import type { ExecutionConfig, Phase } from "./types.js";
18
+ /**
19
+ * Base effort assumed for a phase with no configured `effort` override, when
20
+ * escalation needs a starting point to step up from (AC-5). `phase-executor.ts`
21
+ * omits the `effort` key entirely in that case (#914) so the Agent SDK's own
22
+ * default applies — #914 deliberately never encoded what that default is,
23
+ * since "omitted" is not the same claim as "equals X".
24
+ *
25
+ * Verified against `@anthropic-ai/claude-agent-sdk`'s own `query()` Options
26
+ * type (`sdk.d.ts`): `effort?: EffortLevel` is documented inline as
27
+ * `'high' — Deep reasoning (default)`. That is the SDK's default for the
28
+ * exact call sequant makes (raw `query()`, not the Claude Code CLI product —
29
+ * whose own `xhigh` default is a caller choice on top of this SDK, not the
30
+ * SDK's own default), so this constant is not a guess.
31
+ */
32
+ export declare const DEFAULT_ESCALATION_BASE: (typeof EFFORT_LEVELS)[number];
33
+ /**
34
+ * Pure ladder step: one tier above `base` on `EFFORT_LEVELS`, capped at the
35
+ * top (`max`). Returns `base` unchanged whenever `enabled` is false or this
36
+ * isn't a retry — the disabled/first-attempt path must be indistinguishable
37
+ * from #914 with escalation never having existed (AC-2).
38
+ *
39
+ * Always escalates from the phase's CONFIGURED base, never from a previously
40
+ * escalated value — callers must not accumulate escalation across iterations
41
+ * (AC-6): base `high` on the 3rd loop iteration is `xhigh`, not `max`.
42
+ */
43
+ export declare function resolveEscalatedEffort(base: string | undefined, isRetry: boolean, enabled: boolean): string | undefined;
44
+ /** One escalated execution, for observability (run metrics + verbose output). */
45
+ export interface EscalationRecord {
46
+ phase: Phase;
47
+ base: string;
48
+ escalated: string;
49
+ }
50
+ export interface EscalationOutcome {
51
+ /**
52
+ * The config to dispatch with. Identical by reference to the input `config`
53
+ * whenever nothing escalated — so a shared `ExecutionConfig` object is never
54
+ * mutated and an escalation never leaks into a phase execution it wasn't
55
+ * computed for (AC-7).
56
+ */
57
+ config: ExecutionConfig;
58
+ /** Present only when this dispatch actually escalated. */
59
+ record?: EscalationRecord;
60
+ }
61
+ /**
62
+ * Apply escalation to ONE phase's execution, for THIS dispatch only.
63
+ *
64
+ * This is deliberately a per-execution decision made at the dispatch site,
65
+ * not a value baked into `ExecutionConfig` at build time: `buildExecutionConfig`
66
+ * / `buildPhaseConfig` run once per run/gate, not once per phase execution, so
67
+ * a static escalated value would leak across every phase in the chain and
68
+ * violate AC-7. The three retry-dispatch sites (batch-executor.ts's quality
69
+ * loop, ready-gate.ts's QA-pass loop `qa`/`loop` dispatch) call this function
70
+ * — and only this function — so they cannot drift on the cap/one-tier rules
71
+ * in AC-6 (see resolveEscalatedEffort's doc comment).
72
+ */
73
+ export declare function withEscalatedEffort(config: ExecutionConfig, phase: Phase, isRetry: boolean): EscalationOutcome;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Evidence-based effort escalation on quality-loop retries (#915).
3
+ *
4
+ * Sequant already detects several "this attempt is a retry" moments: the
5
+ * outer quality-loop re-entering a phase (`batch-executor.ts`) and the
6
+ * `sequant ready` QA-pass loop re-running `qa`/`loop` (`ready-gate.ts`).
7
+ * Escalation raises the phase's reasoning effort one tier for exactly that
8
+ * retried execution when the workflow observed a prior attempt fail — never
9
+ * speculatively, and never more than one tier per retry (see AC-6).
10
+ *
11
+ * Deliberately its own module rather than living beside `resolvePhasePolicies`
12
+ * in `config-resolver.ts`: `config-resolver.ts` imports `getEnvConfig` from
13
+ * `batch-executor.ts`, and `batch-executor.ts` is one of this module's
14
+ * dispatch-time callers, so co-locating here avoids introducing that cycle.
15
+ */
16
+ import { EFFORT_LEVELS } from "../settings.js";
17
+ /**
18
+ * Base effort assumed for a phase with no configured `effort` override, when
19
+ * escalation needs a starting point to step up from (AC-5). `phase-executor.ts`
20
+ * omits the `effort` key entirely in that case (#914) so the Agent SDK's own
21
+ * default applies — #914 deliberately never encoded what that default is,
22
+ * since "omitted" is not the same claim as "equals X".
23
+ *
24
+ * Verified against `@anthropic-ai/claude-agent-sdk`'s own `query()` Options
25
+ * type (`sdk.d.ts`): `effort?: EffortLevel` is documented inline as
26
+ * `'high' — Deep reasoning (default)`. That is the SDK's default for the
27
+ * exact call sequant makes (raw `query()`, not the Claude Code CLI product —
28
+ * whose own `xhigh` default is a caller choice on top of this SDK, not the
29
+ * SDK's own default), so this constant is not a guess.
30
+ */
31
+ export const DEFAULT_ESCALATION_BASE = "high";
32
+ /**
33
+ * Pure ladder step: one tier above `base` on `EFFORT_LEVELS`, capped at the
34
+ * top (`max`). Returns `base` unchanged whenever `enabled` is false or this
35
+ * isn't a retry — the disabled/first-attempt path must be indistinguishable
36
+ * from #914 with escalation never having existed (AC-2).
37
+ *
38
+ * Always escalates from the phase's CONFIGURED base, never from a previously
39
+ * escalated value — callers must not accumulate escalation across iterations
40
+ * (AC-6): base `high` on the 3rd loop iteration is `xhigh`, not `max`.
41
+ */
42
+ export function resolveEscalatedEffort(base, isRetry, enabled) {
43
+ if (!enabled || !isRetry)
44
+ return base;
45
+ const effectiveBase = (base ??
46
+ DEFAULT_ESCALATION_BASE);
47
+ const baseIdx = EFFORT_LEVELS.indexOf(effectiveBase);
48
+ const resolvedIdx = baseIdx === -1 ? EFFORT_LEVELS.indexOf(DEFAULT_ESCALATION_BASE) : baseIdx;
49
+ const nextIdx = Math.min(resolvedIdx + 1, EFFORT_LEVELS.length - 1);
50
+ return EFFORT_LEVELS[nextIdx];
51
+ }
52
+ /**
53
+ * Apply escalation to ONE phase's execution, for THIS dispatch only.
54
+ *
55
+ * This is deliberately a per-execution decision made at the dispatch site,
56
+ * not a value baked into `ExecutionConfig` at build time: `buildExecutionConfig`
57
+ * / `buildPhaseConfig` run once per run/gate, not once per phase execution, so
58
+ * a static escalated value would leak across every phase in the chain and
59
+ * violate AC-7. The three retry-dispatch sites (batch-executor.ts's quality
60
+ * loop, ready-gate.ts's QA-pass loop `qa`/`loop` dispatch) call this function
61
+ * — and only this function — so they cannot drift on the cap/one-tier rules
62
+ * in AC-6 (see resolveEscalatedEffort's doc comment).
63
+ */
64
+ export function withEscalatedEffort(config, phase, isRetry) {
65
+ if (!config.effortEscalation || !isRetry)
66
+ return { config };
67
+ const currentPolicy = config.phasePolicies?.[phase];
68
+ const base = currentPolicy?.effort;
69
+ const escalated = resolveEscalatedEffort(base, isRetry, true);
70
+ if (!escalated || escalated === base)
71
+ return { config };
72
+ return {
73
+ config: {
74
+ ...config,
75
+ phasePolicies: {
76
+ ...config.phasePolicies,
77
+ [phase]: { ...currentPolicy, effort: escalated },
78
+ },
79
+ },
80
+ record: { phase, base: base ?? DEFAULT_ESCALATION_BASE, escalated },
81
+ };
82
+ }
@@ -11,9 +11,12 @@ import { SequantError } from "../errors.js";
11
11
  * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
12
  * errors — `classifyError` never produces them, since stderr text cannot
13
13
  * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
15
+ * site, not by `classifyError` — a failed `createPR` has no failing phase to
16
+ * classify, so it would otherwise leave `failureCategory` `undefined`.
14
17
  * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
18
  */
16
- export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "unknown"];
19
+ export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "pr_creation", "unknown"];
17
20
  export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
18
21
  /**
19
22
  * Map from error type name to legacy category string.
@@ -11,6 +11,9 @@ import { ContextOverflowError, ApiError, HookFailureError, BuildError, TimeoutEr
11
11
  * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
12
  * errors — `classifyError` never produces them, since stderr text cannot
13
13
  * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
15
+ * site, not by `classifyError` — a failed `createPR` has no failing phase to
16
+ * classify, so it would otherwise leave `failureCategory` `undefined`.
14
17
  * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
18
  */
16
19
  export const ERROR_CATEGORIES = [
@@ -21,6 +24,7 @@ export const ERROR_CATEGORIES = [
21
24
  "timeout",
22
25
  "rate_limit",
23
26
  "billing",
27
+ "pr_creation",
24
28
  "unknown",
25
29
  ];
26
30
  /**
@@ -18,18 +18,50 @@ export interface GitDiffStatsResult {
18
18
  /** Total lines deleted across all files */
19
19
  totalDeletions: number;
20
20
  }
21
+ /**
22
+ * Resolve the ref diff stats should compare against (#878).
23
+ *
24
+ * Worktrees are created from `origin/<base>` (worktree-manager), but callers
25
+ * historically passed the bare branch name and the diff ran against the
26
+ * *local* ref. When local `<base>` lags the remote — routine, since nothing
27
+ * in the run path updates it — `<base>...HEAD` attributes commits the run
28
+ * never made (phantom filesModified).
29
+ *
30
+ * Candidates are the origin-qualified and bare forms of `baseBranch`
31
+ * (already-remote-qualified input keeps a single candidate). Among the
32
+ * candidates that resolve to a commit, pick the one nearest to HEAD
33
+ * (smallest `rev-list --count <cand>..HEAD`), preferring the
34
+ * origin-qualified form on a tie. Nearest-wins matches the worktree's
35
+ * actual creation point in both staleness directions: a stale local
36
+ * default branch (this issue) and a chain-mode worktree branched from a
37
+ * local base that is ahead of its pushed counterpart. Falls back to
38
+ * `baseBranch` verbatim when no candidate resolves (e.g. remote-less repo
39
+ * with a missing branch) — the diff then fails gracefully to empty, the
40
+ * pre-#878 behavior.
41
+ */
42
+ export declare function resolveDiffBase(worktreePath: string, baseBranch: string): string;
21
43
  /**
22
44
  * Get git commit SHA for a worktree (AC-2)
23
45
  *
46
+ * When `baseRef` is provided, returns undefined if HEAD has no commits
47
+ * unique to it (#878) — a branch that never moved off its base would
48
+ * otherwise log the base tip as if it were the phase's commit. Callers
49
+ * recording plain "where is HEAD" markers (run start/end) omit `baseRef`.
50
+ *
24
51
  * @param worktreePath - Path to the git worktree
25
- * @returns The current HEAD commit SHA, or undefined on error
52
+ * @param baseRef - Optional resolved base ref (see resolveDiffBase); when
53
+ * given, a HEAD with zero commits past it yields undefined
54
+ * @returns The current HEAD commit SHA, or undefined on error / no unique
55
+ * commits
26
56
  */
27
- export declare function getCommitHash(worktreePath: string): string | undefined;
57
+ export declare function getCommitHash(worktreePath: string, baseRef?: string): string | undefined;
28
58
  /**
29
59
  * Get git diff statistics for a worktree (AC-1, AC-3, AC-4)
30
60
  *
31
61
  * Efficiently captures both filesModified and fileDiffStats using
32
- * minimal git commands. Uses main...HEAD comparison by default.
62
+ * minimal git commands. The base is resolved via resolveDiffBase (#878) so
63
+ * the comparison targets the ref the worktree was actually created from
64
+ * (origin/<base> in the common case) rather than a possibly-stale local ref.
33
65
  *
34
66
  * @param worktreePath - Path to the git worktree
35
67
  * @param baseBranch - Branch to compare against (default: "main")
@@ -73,13 +73,73 @@ function parseNameStatus(output) {
73
73
  }
74
74
  return result;
75
75
  }
76
+ /**
77
+ * Resolve the ref diff stats should compare against (#878).
78
+ *
79
+ * Worktrees are created from `origin/<base>` (worktree-manager), but callers
80
+ * historically passed the bare branch name and the diff ran against the
81
+ * *local* ref. When local `<base>` lags the remote — routine, since nothing
82
+ * in the run path updates it — `<base>...HEAD` attributes commits the run
83
+ * never made (phantom filesModified).
84
+ *
85
+ * Candidates are the origin-qualified and bare forms of `baseBranch`
86
+ * (already-remote-qualified input keeps a single candidate). Among the
87
+ * candidates that resolve to a commit, pick the one nearest to HEAD
88
+ * (smallest `rev-list --count <cand>..HEAD`), preferring the
89
+ * origin-qualified form on a tie. Nearest-wins matches the worktree's
90
+ * actual creation point in both staleness directions: a stale local
91
+ * default branch (this issue) and a chain-mode worktree branched from a
92
+ * local base that is ahead of its pushed counterpart. Falls back to
93
+ * `baseBranch` verbatim when no candidate resolves (e.g. remote-less repo
94
+ * with a missing branch) — the diff then fails gracefully to empty, the
95
+ * pre-#878 behavior.
96
+ */
97
+ export function resolveDiffBase(worktreePath, baseBranch) {
98
+ const candidates = baseBranch.startsWith("origin/")
99
+ ? [baseBranch]
100
+ : [`origin/${baseBranch}`, baseBranch];
101
+ let best;
102
+ let bestCount = Number.POSITIVE_INFINITY;
103
+ for (const candidate of candidates) {
104
+ const verify = spawnSync("git", [
105
+ "-C",
106
+ worktreePath,
107
+ "rev-parse",
108
+ "--verify",
109
+ "--quiet",
110
+ `${candidate}^{commit}`,
111
+ ], { stdio: "pipe", encoding: "utf-8" });
112
+ if (verify.status !== 0)
113
+ continue;
114
+ const count = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${candidate}..HEAD`], { stdio: "pipe", encoding: "utf-8" });
115
+ if (count.status !== 0)
116
+ continue;
117
+ const n = Number.parseInt(count.stdout.trim(), 10);
118
+ if (Number.isNaN(n))
119
+ continue;
120
+ // Strict < keeps the earlier (origin-qualified) candidate on a tie.
121
+ if (n < bestCount) {
122
+ best = candidate;
123
+ bestCount = n;
124
+ }
125
+ }
126
+ return best ?? baseBranch;
127
+ }
76
128
  /**
77
129
  * Get git commit SHA for a worktree (AC-2)
78
130
  *
131
+ * When `baseRef` is provided, returns undefined if HEAD has no commits
132
+ * unique to it (#878) — a branch that never moved off its base would
133
+ * otherwise log the base tip as if it were the phase's commit. Callers
134
+ * recording plain "where is HEAD" markers (run start/end) omit `baseRef`.
135
+ *
79
136
  * @param worktreePath - Path to the git worktree
80
- * @returns The current HEAD commit SHA, or undefined on error
137
+ * @param baseRef - Optional resolved base ref (see resolveDiffBase); when
138
+ * given, a HEAD with zero commits past it yields undefined
139
+ * @returns The current HEAD commit SHA, or undefined on error / no unique
140
+ * commits
81
141
  */
82
- export function getCommitHash(worktreePath) {
142
+ export function getCommitHash(worktreePath, baseRef) {
83
143
  const result = spawnSync("git", ["-C", worktreePath, "rev-parse", "HEAD"], {
84
144
  stdio: "pipe",
85
145
  encoding: "utf-8",
@@ -87,20 +147,33 @@ export function getCommitHash(worktreePath) {
87
147
  if (result.status !== 0) {
88
148
  return undefined;
89
149
  }
150
+ if (baseRef !== undefined) {
151
+ const count = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${baseRef}..HEAD`], { stdio: "pipe", encoding: "utf-8" });
152
+ // Fail open on git errors: a transient failure should not erase a real
153
+ // commit hash from the log — only a confirmed zero suppresses it.
154
+ if (count.status === 0) {
155
+ const n = Number.parseInt(count.stdout.trim(), 10);
156
+ if (n === 0) {
157
+ return undefined;
158
+ }
159
+ }
160
+ }
90
161
  return result.stdout.trim();
91
162
  }
92
163
  /**
93
164
  * Get git diff statistics for a worktree (AC-1, AC-3, AC-4)
94
165
  *
95
166
  * Efficiently captures both filesModified and fileDiffStats using
96
- * minimal git commands. Uses main...HEAD comparison by default.
167
+ * minimal git commands. The base is resolved via resolveDiffBase (#878) so
168
+ * the comparison targets the ref the worktree was actually created from
169
+ * (origin/<base> in the common case) rather than a possibly-stale local ref.
97
170
  *
98
171
  * @param worktreePath - Path to the git worktree
99
172
  * @param baseBranch - Branch to compare against (default: "main")
100
173
  * @returns GitDiffStatsResult with files, stats, and totals
101
174
  */
102
175
  export function getGitDiffStats(worktreePath, baseBranch = "main") {
103
- const diffRef = `${baseBranch}...HEAD`;
176
+ const diffRef = `${resolveDiffBase(worktreePath, baseBranch)}...HEAD`;
104
177
  // Get numstat for additions/deletions
105
178
  const numstatResult = spawnSync("git", ["-C", worktreePath, "diff", "--numstat", diffRef], { stdio: "pipe", encoding: "utf-8" });
106
179
  // Get name-status for file status (added/modified/deleted/renamed)
@@ -1,4 +1,13 @@
1
1
  import type { ShutdownManager } from "../shutdown.js";
2
+ /**
3
+ * Cadence of the non-TTY auto-wait notice (#860 AC-6). Non-TTY output is
4
+ * append-only (no `\r` rewrite), so the TTY heartbeat's every-tick refresh
5
+ * would log ~600 lines over a five-hour wait. Five minutes keeps an unattended
6
+ * log legibly alive (~60 lines per five-hour window) without drowning it.
7
+ *
8
+ * @internal Exported for testing only
9
+ */
10
+ export declare const NON_TTY_WAIT_NOTICE_INTERVAL_MS: number;
2
11
  export interface LivenessHeartbeatOptions {
3
12
  /** Polling cadence for heartbeat ticks. Default: 30_000ms */
4
13
  pollIntervalMs?: number;
@@ -60,10 +69,39 @@ export declare class LivenessHeartbeat {
60
69
  * Dispose all tracked phases and clear the timer. Idempotent.
61
70
  */
62
71
  dispose(): void;
72
+ /**
73
+ * Mark a phase as auto-waiting for a rate-limit window to reopen (#804 AC-7).
74
+ *
75
+ * This is not cosmetic. The stall detector's activity proxy is the mtime of
76
+ * `.sequant/state.json`, and an auto-wait produces no writes by definition —
77
+ * so a multi-hour wait would otherwise trip the "no log activity" warning
78
+ * within minutes and report an entirely expected pause as a stall. Marking
79
+ * the phase both relabels the heartbeat line with the wake time and
80
+ * suppresses that false warning.
81
+ *
82
+ * No-op for an untracked phase, so an out-of-order or late notice cannot
83
+ * resurrect a stopped entry.
84
+ */
85
+ pauseForWait(key: PhaseKey, wakeAtMs: number): void;
86
+ /** Clear the auto-wait marker set by {@link pauseForWait} (#804). */
87
+ resumeFromWait(key: PhaseKey): void;
63
88
  /** Test hook: drive a poll synchronously without waiting on real timers. */
64
89
  tickNow(): void;
65
90
  private tick;
66
91
  private writeHeartbeat;
92
+ /**
93
+ * Heartbeat line for a phase paused on an auto-wait (#804). Names the wake
94
+ * time and the remaining wait so a multi-hour pause is legibly deliberate
95
+ * rather than indistinguishable from a hang.
96
+ */
97
+ private writeWaitHeartbeat;
98
+ /**
99
+ * Append-only auto-wait notice for non-TTY runs (#860 AC-6). Same facts as
100
+ * {@link writeWaitHeartbeat} — wake time and remaining wait — but a plain
101
+ * `\n`-terminated line with no cursor control, throttled by the caller to
102
+ * {@link NON_TTY_WAIT_NOTICE_INTERVAL_MS} so an unattended log stays legible.
103
+ */
104
+ private writeNonTtyWaitNotice;
67
105
  private writeStallWarning;
68
106
  }
69
107
  /** Convenience factory mirroring `phaseSpinner()`. */
@@ -12,10 +12,20 @@
12
12
  */
13
13
  import * as fs from "fs";
14
14
  import { formatElapsedTime } from "../cli-ui/format.js";
15
+ import { formatResetTime } from "../errors.js";
15
16
  const DEFAULT_POLL_INTERVAL_MS = 30_000;
16
17
  const DEFAULT_STALL_THRESHOLD_MS = 5 * 60_000;
17
18
  const DEFAULT_LIVENESS_FILE = ".sequant/state.json";
18
19
  const CLEANUP_NAME = "liveness-heartbeat";
20
+ /**
21
+ * Cadence of the non-TTY auto-wait notice (#860 AC-6). Non-TTY output is
22
+ * append-only (no `\r` rewrite), so the TTY heartbeat's every-tick refresh
23
+ * would log ~600 lines over a five-hour wait. Five minutes keeps an unattended
24
+ * log legibly alive (~60 lines per five-hour window) without drowning it.
25
+ *
26
+ * @internal Exported for testing only
27
+ */
28
+ export const NON_TTY_WAIT_NOTICE_INTERVAL_MS = 5 * 60_000;
19
29
  function keyFor(k) {
20
30
  return `${k.issueNumber}:${k.phase}`;
21
31
  }
@@ -118,6 +128,39 @@ export class LivenessHeartbeat {
118
128
  this.cleanupRegistered = false;
119
129
  }
120
130
  }
131
+ /**
132
+ * Mark a phase as auto-waiting for a rate-limit window to reopen (#804 AC-7).
133
+ *
134
+ * This is not cosmetic. The stall detector's activity proxy is the mtime of
135
+ * `.sequant/state.json`, and an auto-wait produces no writes by definition —
136
+ * so a multi-hour wait would otherwise trip the "no log activity" warning
137
+ * within minutes and report an entirely expected pause as a stall. Marking
138
+ * the phase both relabels the heartbeat line with the wake time and
139
+ * suppresses that false warning.
140
+ *
141
+ * No-op for an untracked phase, so an out-of-order or late notice cannot
142
+ * resurrect a stopped entry.
143
+ */
144
+ pauseForWait(key, wakeAtMs) {
145
+ const entry = this.phases.get(keyFor(key));
146
+ if (!entry)
147
+ return;
148
+ entry.waitingUntil = wakeAtMs;
149
+ // Clear any warning already fired so a genuine stall after the wait can
150
+ // still warn once.
151
+ entry.warningFired = false;
152
+ // #860 AC-6: a fresh wait announces itself on the next non-TTY tick.
153
+ entry.lastWaitNoticeAt = undefined;
154
+ }
155
+ /** Clear the auto-wait marker set by {@link pauseForWait} (#804). */
156
+ resumeFromWait(key) {
157
+ const entry = this.phases.get(keyFor(key));
158
+ if (!entry)
159
+ return;
160
+ entry.waitingUntil = undefined;
161
+ entry.warningFired = false;
162
+ entry.lastWaitNoticeAt = undefined;
163
+ }
121
164
  /** Test hook: drive a poll synchronously without waiting on real timers. */
122
165
  tickNow() {
123
166
  this.tick();
@@ -127,6 +170,23 @@ export class LivenessHeartbeat {
127
170
  return;
128
171
  const now = this.now();
129
172
  for (const entry of this.phases.values()) {
173
+ // #804: an auto-waiting phase has its own liveness story. Handled before
174
+ // the mtime read because the wait produces no file activity at all — the
175
+ // stall detector's proxy is meaningless here, and the `mtimeMs === null`
176
+ // early-continue below would otherwise leave the wait completely silent.
177
+ if (entry.waitingUntil !== undefined) {
178
+ if (this.tty) {
179
+ this.writeWaitHeartbeat(entry, Math.max(0, now - entry.startedAt));
180
+ }
181
+ else if (entry.lastWaitNoticeAt === undefined ||
182
+ now - entry.lastWaitNoticeAt >= NON_TTY_WAIT_NOTICE_INTERVAL_MS) {
183
+ // #860 AC-6: without this, a non-TTY wait emits nothing for hours —
184
+ // indistinguishable from the #856 hang. Append-only line, throttled.
185
+ this.writeNonTtyWaitNotice(entry);
186
+ entry.lastWaitNoticeAt = now;
187
+ }
188
+ continue;
189
+ }
130
190
  let mtimeMs;
131
191
  try {
132
192
  const stat = fs.statSync(this.livenessFile);
@@ -168,6 +228,36 @@ export class LivenessHeartbeat {
168
228
  const line = `\r ▸ #${entry.issueNumber} ${entry.phase} (${elapsed} elapsed, last log update ${sinceActivity} ago)`;
169
229
  this.stdoutWrite(line);
170
230
  }
231
+ /**
232
+ * Heartbeat line for a phase paused on an auto-wait (#804). Names the wake
233
+ * time and the remaining wait so a multi-hour pause is legibly deliberate
234
+ * rather than indistinguishable from a hang.
235
+ */
236
+ writeWaitHeartbeat(entry, elapsedSinceStartMs) {
237
+ if (this.stopped || entry.waitingUntil === undefined)
238
+ return;
239
+ const elapsed = formatElapsedTime(Math.floor(elapsedSinceStartMs / 1000));
240
+ const remainingMs = Math.max(0, entry.waitingUntil - this.now());
241
+ const remaining = formatElapsedTime(Math.floor(remainingMs / 1000));
242
+ const wake = formatResetTime(entry.waitingUntil);
243
+ const line = `\r ⏸ #${entry.issueNumber} ${entry.phase} (${elapsed} elapsed, rate-limit window — resuming at ${wake}, ${remaining} left)\x1b[K`;
244
+ this.stdoutWrite(line);
245
+ }
246
+ /**
247
+ * Append-only auto-wait notice for non-TTY runs (#860 AC-6). Same facts as
248
+ * {@link writeWaitHeartbeat} — wake time and remaining wait — but a plain
249
+ * `\n`-terminated line with no cursor control, throttled by the caller to
250
+ * {@link NON_TTY_WAIT_NOTICE_INTERVAL_MS} so an unattended log stays legible.
251
+ */
252
+ writeNonTtyWaitNotice(entry) {
253
+ if (this.stopped || entry.waitingUntil === undefined)
254
+ return;
255
+ const remainingMs = Math.max(0, entry.waitingUntil - this.now());
256
+ const remaining = formatElapsedTime(Math.floor(remainingMs / 1000));
257
+ const wake = formatResetTime(entry.waitingUntil);
258
+ const line = ` ⏸ #${entry.issueNumber} ${entry.phase} rate-limit window — resuming at ${wake} (${remaining} left)\n`;
259
+ this.stdoutWrite(line);
260
+ }
171
261
  writeStallWarning(entry, sinceActivityMs, elapsedSinceStartMs) {
172
262
  if (this.stopped)
173
263
  return;
@@ -13,7 +13,7 @@
13
13
  * await writer.finalize();
14
14
  * ```
15
15
  */
16
- import { type RunLog, type RunConfig, type PhaseLog, type Phase } from "./run-log-schema.js";
16
+ import { type RunLog, type RunConfig, type PhaseLog, type Phase, type SpecRecommendation } from "./run-log-schema.js";
17
17
  import { type RotationSettings } from "./log-rotation.js";
18
18
  export interface LogWriterOptions {
19
19
  /** Path to log directory (default: .sequant/logs in current directory) */
@@ -26,6 +26,13 @@ export interface LogWriterOptions {
26
26
  rotation?: RotationSettings;
27
27
  /** Git commit SHA at run start (AC-2) */
28
28
  startCommit?: string;
29
+ /**
30
+ * Run start timestamp (#867). When provided, the log's `startTime` uses this
31
+ * origin instead of self-stamping `new Date()` at initialize(), so the log's
32
+ * stored wall clock and the orchestrator's summary derive from one shared
33
+ * start. Defaults to now when omitted.
34
+ */
35
+ startTime?: Date;
29
36
  }
30
37
  /**
31
38
  * Manages writing structured run logs to disk
@@ -36,11 +43,20 @@ export declare class LogWriter {
36
43
  private activeIssues;
37
44
  /** @deprecated Single-issue slot for backwards compatibility — use activeIssues */
38
45
  private currentIssue;
46
+ /**
47
+ * Issue slots forced to `failure` by {@link markIssueFailed} (#879). Keyed on
48
+ * the slot object so it covers both the numbered and legacy `currentIssue`
49
+ * paths. `completeIssue` re-derives status from phases (#856), so a seeded
50
+ * `issue.status = "failure"` alone would be overwritten back to `success`;
51
+ * this marker survives that re-derivation.
52
+ */
53
+ private forcedFailures;
39
54
  private logPath;
40
55
  private writeToUserLogs;
41
56
  private verbose;
42
57
  private rotation;
43
58
  private startCommit?;
59
+ private startTime?;
44
60
  constructor(options?: LogWriterOptions);
45
61
  /**
46
62
  * Initialize a new run log
@@ -66,10 +82,37 @@ export declare class LogWriter {
66
82
  * Set PR info on the current issue (call before completeIssue)
67
83
  */
68
84
  setPRInfo(prNumber: number, prUrl: string, issueNumber?: number): void;
85
+ /**
86
+ * Record how the spec→run phase recommendation was resolved (#921 AC-4).
87
+ *
88
+ * Called right after `resolveSpecRecommendation` runs, before the next
89
+ * phase starts — mirrors {@link setPRInfo}'s post-hoc-setter shape because
90
+ * the spec `PhaseLog` is already written by the time resolution (which does
91
+ * its own comment-fetch I/O) completes.
92
+ */
93
+ setSpecRecommendation(recommendation: SpecRecommendation, issueNumber?: number): void;
94
+ /**
95
+ * Force the in-flight issue's status to `failure` (#879).
96
+ *
97
+ * `deriveIssueLogStatus` runs at phase-log time, so an issue whose phases all
98
+ * passed but whose PR creation then failed would otherwise be recorded as
99
+ * `success`. Call this after the last phase is logged and before
100
+ * {@link completeIssue} to count it under `failed`. No-op if the issue is not
101
+ * active. Reuses the existing `failure` enum — no schema change.
102
+ */
103
+ markIssueFailed(issueNumber?: number): void;
69
104
  /**
70
105
  * Complete the current issue and add it to the run log
106
+ *
107
+ * @param issueNumber - Issue to complete (defaults to the legacy single slot)
108
+ * @param abort - Set when the run is being torn down by an external signal
109
+ * (#856). Marks the issue aborted with its cause instead of persisting
110
+ * whatever verdict the incomplete phase list happens to imply.
71
111
  */
72
- completeIssue(issueNumber?: number): void;
112
+ completeIssue(issueNumber?: number, abort?: {
113
+ signal: string;
114
+ reason: string;
115
+ }): void;
73
116
  /**
74
117
  * Finalize the run log and write to disk
75
118
  *
@@ -77,10 +120,19 @@ export declare class LogWriter {
77
120
  *
78
121
  * @param options - Optional finalization options
79
122
  * @param options.endCommit - Git commit SHA at run end (AC-2)
123
+ * @param options.aborted - Set when finalizing because the run was
124
+ * terminated by an external signal (#856). Every still-in-flight issue is
125
+ * recorded as an abort naming its cause, and the run log carries
126
+ * `abortedBy`. Without it, a killed run's log is indistinguishable from a
127
+ * clean one that happened to do nothing.
80
128
  * @returns Path to the written log file
81
129
  */
82
130
  finalize(options?: {
83
131
  endCommit?: string;
132
+ aborted?: {
133
+ signal: string;
134
+ reason: string;
135
+ };
84
136
  }): Promise<string>;
85
137
  /**
86
138
  * Get the current run log (for inspection)