sequant 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +81 -5
  4. package/dist/bin/cli.js +140 -13
  5. package/dist/src/commands/abort.d.ts +36 -0
  6. package/dist/src/commands/abort.js +138 -0
  7. package/dist/src/commands/doctor.d.ts +25 -0
  8. package/dist/src/commands/doctor.js +36 -1
  9. package/dist/src/commands/locks.d.ts +67 -0
  10. package/dist/src/commands/locks.js +290 -0
  11. package/dist/src/commands/merge.js +11 -0
  12. package/dist/src/commands/prompt.d.ts +46 -0
  13. package/dist/src/commands/prompt.js +273 -0
  14. package/dist/src/commands/run-display.d.ts +11 -2
  15. package/dist/src/commands/run-display.js +62 -28
  16. package/dist/src/commands/run-progress.d.ts +42 -0
  17. package/dist/src/commands/run-progress.js +93 -0
  18. package/dist/src/commands/run.js +90 -18
  19. package/dist/src/commands/stats.d.ts +2 -0
  20. package/dist/src/commands/stats.js +94 -8
  21. package/dist/src/commands/status.js +12 -0
  22. package/dist/src/commands/watch.d.ts +18 -0
  23. package/dist/src/commands/watch.js +211 -0
  24. package/dist/src/lib/ac-linter.d.ts +1 -1
  25. package/dist/src/lib/ac-linter.js +81 -0
  26. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  27. package/dist/src/lib/assess-collision-detect.js +217 -0
  28. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  29. package/dist/src/lib/assess-comment-parser.js +124 -2
  30. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  31. package/dist/src/lib/cli-ui/format.js +34 -0
  32. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +220 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
  36. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  37. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  38. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  39. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  40. package/dist/src/lib/locks/index.d.ts +7 -0
  41. package/dist/src/lib/locks/index.js +5 -0
  42. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  43. package/dist/src/lib/locks/lock-manager.js +433 -0
  44. package/dist/src/lib/locks/types.d.ts +59 -0
  45. package/dist/src/lib/locks/types.js +31 -0
  46. package/dist/src/lib/merge-check/types.js +1 -1
  47. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  48. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  49. package/dist/src/lib/relay/activation.d.ts +60 -0
  50. package/dist/src/lib/relay/activation.js +122 -0
  51. package/dist/src/lib/relay/archive.d.ts +34 -0
  52. package/dist/src/lib/relay/archive.js +112 -0
  53. package/dist/src/lib/relay/frame.d.ts +20 -0
  54. package/dist/src/lib/relay/frame.js +76 -0
  55. package/dist/src/lib/relay/index.d.ts +13 -0
  56. package/dist/src/lib/relay/index.js +13 -0
  57. package/dist/src/lib/relay/paths.d.ts +43 -0
  58. package/dist/src/lib/relay/paths.js +59 -0
  59. package/dist/src/lib/relay/pid.d.ts +34 -0
  60. package/dist/src/lib/relay/pid.js +72 -0
  61. package/dist/src/lib/relay/reader.d.ts +35 -0
  62. package/dist/src/lib/relay/reader.js +115 -0
  63. package/dist/src/lib/relay/types.d.ts +70 -0
  64. package/dist/src/lib/relay/types.js +85 -0
  65. package/dist/src/lib/relay/writer.d.ts +48 -0
  66. package/dist/src/lib/relay/writer.js +113 -0
  67. package/dist/src/lib/settings.d.ts +31 -1
  68. package/dist/src/lib/settings.js +18 -3
  69. package/dist/src/lib/version-check.d.ts +60 -5
  70. package/dist/src/lib/version-check.js +97 -9
  71. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  72. package/dist/src/lib/workflow/batch-executor.js +274 -185
  73. package/dist/src/lib/workflow/config-resolver.js +4 -0
  74. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  75. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  76. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  77. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  78. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  79. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  80. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  81. package/dist/src/lib/workflow/event-emitter.js +102 -0
  82. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  83. package/dist/src/lib/workflow/heartbeat.js +194 -0
  84. package/dist/src/lib/workflow/notice.d.ts +32 -0
  85. package/dist/src/lib/workflow/notice.js +38 -0
  86. package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
  87. package/dist/src/lib/workflow/phase-executor.js +244 -130
  88. package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
  89. package/dist/src/lib/workflow/phase-mapper.js +70 -51
  90. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  91. package/dist/src/lib/workflow/phase-registry.js +233 -0
  92. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  93. package/dist/src/lib/workflow/platforms/github.js +20 -3
  94. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  95. package/dist/src/lib/workflow/pr-status.js +41 -9
  96. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  97. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  98. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  99. package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
  100. package/dist/src/lib/workflow/run-orchestrator.js +464 -25
  101. package/dist/src/lib/workflow/run-reflect.js +1 -1
  102. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  103. package/dist/src/lib/workflow/run-state.js +14 -0
  104. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  105. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  106. package/dist/src/lib/workflow/state-manager.d.ts +31 -2
  107. package/dist/src/lib/workflow/state-manager.js +64 -1
  108. package/dist/src/lib/workflow/state-schema.d.ts +82 -35
  109. package/dist/src/lib/workflow/state-schema.js +63 -4
  110. package/dist/src/lib/workflow/types.d.ts +139 -16
  111. package/dist/src/lib/workflow/types.js +18 -13
  112. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  113. package/dist/src/lib/workflow/worktree-manager.js +15 -6
  114. package/dist/src/mcp/tools/run.d.ts +44 -0
  115. package/dist/src/mcp/tools/run.js +104 -13
  116. package/dist/src/ui/tui/App.d.ts +14 -0
  117. package/dist/src/ui/tui/App.js +41 -0
  118. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  119. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  120. package/dist/src/ui/tui/Header.d.ts +6 -0
  121. package/dist/src/ui/tui/Header.js +15 -0
  122. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  123. package/dist/src/ui/tui/IssueBox.js +68 -0
  124. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  125. package/dist/src/ui/tui/Spinner.js +18 -0
  126. package/dist/src/ui/tui/index.d.ts +15 -0
  127. package/dist/src/ui/tui/index.js +29 -0
  128. package/dist/src/ui/tui/theme.d.ts +29 -0
  129. package/dist/src/ui/tui/theme.js +52 -0
  130. package/dist/src/ui/tui/truncate.d.ts +11 -0
  131. package/dist/src/ui/tui/truncate.js +31 -0
  132. package/package.json +14 -6
  133. package/templates/agents/sequant-explorer.md +1 -0
  134. package/templates/agents/sequant-qa-checker.md +2 -1
  135. package/templates/agents/sequant-testgen.md +1 -0
  136. package/templates/hooks/post-tool.sh +92 -0
  137. package/templates/hooks/pre-tool.sh +18 -9
  138. package/templates/hooks/relay-check.sh +107 -0
  139. package/templates/relay/frame.txt +11 -0
  140. package/templates/scripts/cleanup-worktree.sh +25 -3
  141. package/templates/scripts/new-feature.sh +6 -0
  142. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  143. package/templates/skills/_shared/references/subagent-types.md +21 -8
  144. package/templates/skills/assess/SKILL.md +122 -68
  145. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  146. package/templates/skills/docs/SKILL.md +141 -22
  147. package/templates/skills/exec/SKILL.md +10 -8
  148. package/templates/skills/fullsolve/SKILL.md +79 -5
  149. package/templates/skills/loop/SKILL.md +28 -0
  150. package/templates/skills/merger/SKILL.md +621 -0
  151. package/templates/skills/qa/SKILL.md +727 -8
  152. package/templates/skills/setup/SKILL.md +12 -6
  153. package/templates/skills/spec/SKILL.md +52 -0
  154. package/templates/skills/spec/references/parallel-groups.md +7 -0
  155. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  156. package/templates/skills/testgen/SKILL.md +24 -17
@@ -0,0 +1,194 @@
1
+ /**
2
+ * LivenessHeartbeat — `-q` mode liveness signal + stall warning.
3
+ *
4
+ * Surfaces a per-phase liveness line (TTY only) and a one-shot stall warning
5
+ * (TTY and non-TTY) so users can distinguish "agent working" from "process hung"
6
+ * without inspecting `state.json` or `ps`/`lsof`.
7
+ *
8
+ * Liveness source: mtime of `.sequant/state.json` — written 3-10x per phase by
9
+ * `StateManager.saveState()`. Zero new infrastructure.
10
+ *
11
+ * @see Issue #574
12
+ */
13
+ import * as fs from "fs";
14
+ import { formatElapsedTime } from "../cli-ui/format.js";
15
+ const DEFAULT_POLL_INTERVAL_MS = 30_000;
16
+ const DEFAULT_STALL_THRESHOLD_MS = 5 * 60_000;
17
+ const DEFAULT_LIVENESS_FILE = ".sequant/state.json";
18
+ const CLEANUP_NAME = "liveness-heartbeat";
19
+ function keyFor(k) {
20
+ return `${k.issueNumber}:${k.phase}`;
21
+ }
22
+ /**
23
+ * Format the stall window's elapsed seconds. Floors to whole seconds.
24
+ */
25
+ function formatStall(seconds) {
26
+ return formatElapsedTime(Math.floor(seconds));
27
+ }
28
+ export class LivenessHeartbeat {
29
+ pollIntervalMs;
30
+ stallThresholdMs;
31
+ livenessFile;
32
+ enabled;
33
+ phaseTimeoutSeconds;
34
+ shutdownManager;
35
+ tty;
36
+ now;
37
+ stdoutWrite;
38
+ stderrWrite;
39
+ phases = new Map();
40
+ timer = null;
41
+ stopped = false;
42
+ cleanupRegistered = false;
43
+ constructor(options = {}) {
44
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
45
+ this.stallThresholdMs =
46
+ options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
47
+ this.livenessFile = options.livenessFile ?? DEFAULT_LIVENESS_FILE;
48
+ this.enabled = options.enabled ?? true;
49
+ this.phaseTimeoutSeconds = options.phaseTimeoutSeconds;
50
+ this.shutdownManager = options.shutdownManager;
51
+ this.tty = options.isTTY ?? Boolean(process.stdout.isTTY);
52
+ this.now = options.now ?? Date.now;
53
+ this.stdoutWrite =
54
+ options.stdoutWrite ?? ((s) => void process.stdout.write(s));
55
+ this.stderrWrite =
56
+ options.stderrWrite ?? ((s) => void process.stderr.write(s));
57
+ }
58
+ /**
59
+ * Begin tracking a phase. Starts the shared poll timer on first call.
60
+ * No-op if `enabled === false`.
61
+ */
62
+ start(entry) {
63
+ if (!this.enabled || this.stopped)
64
+ return;
65
+ const key = keyFor(entry);
66
+ this.phases.set(key, {
67
+ issueNumber: entry.issueNumber,
68
+ phase: entry.phase,
69
+ startedAt: entry.startedAt,
70
+ warningFired: false,
71
+ });
72
+ if (this.timer === null) {
73
+ this.timer = setInterval(() => this.tick(), this.pollIntervalMs);
74
+ // Don't keep the event loop alive solely for the heartbeat.
75
+ if (typeof this.timer.unref === "function")
76
+ this.timer.unref();
77
+ }
78
+ if (this.shutdownManager && !this.cleanupRegistered) {
79
+ this.shutdownManager.registerCleanup(CLEANUP_NAME, async () => {
80
+ this.dispose();
81
+ });
82
+ this.cleanupRegistered = true;
83
+ }
84
+ }
85
+ /**
86
+ * Stop tracking a specific phase. When the last phase is removed, the timer
87
+ * is cleared so no orphaned polls remain.
88
+ */
89
+ stop(key) {
90
+ if (key) {
91
+ this.phases.delete(keyFor(key));
92
+ }
93
+ else {
94
+ this.phases.clear();
95
+ }
96
+ // Clear the timer when no phases remain so we don't poll uselessly between
97
+ // phases. We do NOT call dispose() here — that would set `stopped = true`
98
+ // and silently no-op the next start() in a sequential single-issue run
99
+ // (spec → stop → exec). Terminal teardown is dispose() called from the
100
+ // run.ts `finally` block.
101
+ if (this.phases.size === 0 && this.timer !== null) {
102
+ clearInterval(this.timer);
103
+ this.timer = null;
104
+ }
105
+ }
106
+ /**
107
+ * Dispose all tracked phases and clear the timer. Idempotent.
108
+ */
109
+ dispose() {
110
+ this.stopped = true;
111
+ this.phases.clear();
112
+ if (this.timer !== null) {
113
+ clearInterval(this.timer);
114
+ this.timer = null;
115
+ }
116
+ if (this.shutdownManager && this.cleanupRegistered) {
117
+ this.shutdownManager.unregisterCleanup(CLEANUP_NAME);
118
+ this.cleanupRegistered = false;
119
+ }
120
+ }
121
+ /** Test hook: drive a poll synchronously without waiting on real timers. */
122
+ tickNow() {
123
+ this.tick();
124
+ }
125
+ tick() {
126
+ if (this.stopped || this.phases.size === 0)
127
+ return;
128
+ const now = this.now();
129
+ for (const entry of this.phases.values()) {
130
+ let mtimeMs;
131
+ try {
132
+ const stat = fs.statSync(this.livenessFile);
133
+ mtimeMs = stat.mtimeMs;
134
+ }
135
+ catch {
136
+ // ENOENT (state.json not yet written) or EACCES — treat as no signal.
137
+ // Skip both heartbeat and stall logic this tick.
138
+ mtimeMs = null;
139
+ }
140
+ if (mtimeMs === null)
141
+ continue;
142
+ const sinceActivityMs = Math.max(0, now - mtimeMs);
143
+ const elapsedSinceStartMs = Math.max(0, now - entry.startedAt);
144
+ // AC-1: TTY heartbeat line — rewrite via \r.
145
+ if (this.tty) {
146
+ this.writeHeartbeat(entry, elapsedSinceStartMs, sinceActivityMs);
147
+ }
148
+ // AC-2: One-shot stall warning (TTY and non-TTY).
149
+ if (sinceActivityMs >= this.stallThresholdMs) {
150
+ if (!entry.warningFired) {
151
+ this.writeStallWarning(entry, sinceActivityMs, elapsedSinceStartMs);
152
+ entry.warningFired = true;
153
+ }
154
+ }
155
+ else if (entry.warningFired) {
156
+ // Activity resumed — reset for the next stall window.
157
+ entry.warningFired = false;
158
+ }
159
+ }
160
+ }
161
+ writeHeartbeat(entry, elapsedSinceStartMs, sinceActivityMs) {
162
+ if (this.stopped)
163
+ return;
164
+ const elapsed = formatElapsedTime(Math.floor(elapsedSinceStartMs / 1000));
165
+ const sinceActivity = formatElapsedTime(Math.floor(sinceActivityMs / 1000));
166
+ // \r rewrites current line; \x1b[K clears the rest in case the new line is
167
+ // shorter than the old one.
168
+ const line = `\r ▸ #${entry.issueNumber} ${entry.phase} (${elapsed} elapsed, last log update ${sinceActivity} ago)`;
169
+ this.stdoutWrite(line);
170
+ }
171
+ writeStallWarning(entry, sinceActivityMs, elapsedSinceStartMs) {
172
+ if (this.stopped)
173
+ return;
174
+ const stallStr = formatStall(sinceActivityMs / 1000);
175
+ let suffix = "";
176
+ if (this.phaseTimeoutSeconds !== undefined) {
177
+ const remaining = this.phaseTimeoutSeconds - elapsedSinceStartMs / 1000;
178
+ if (remaining > 0) {
179
+ suffix = ` (phase timeout in ${formatElapsedTime(Math.floor(remaining))})`;
180
+ }
181
+ }
182
+ // Prefix \r\x1b[K to clear any in-flight TTY heartbeat on this line, then
183
+ // emit the warning on its own line. Non-TTY mode prints leading control
184
+ // chars too — they are inert when not interpreted, and matter for TTY co-
185
+ // existence with the heartbeat rewrite.
186
+ const prefix = this.tty ? "\r" : "";
187
+ const line = `${prefix} ⚠ #${entry.issueNumber} ${entry.phase} no log activity for ${stallStr}${suffix}\n`;
188
+ this.stderrWrite(line);
189
+ }
190
+ }
191
+ /** Convenience factory mirroring `phaseSpinner()`. */
192
+ export function livenessHeartbeat(options) {
193
+ return new LivenessHeartbeat(options);
194
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Renderer-aware notice routing (#647 AC-3).
3
+ *
4
+ * Lives in its own module so both `phase-executor` and `run-orchestrator`
5
+ * can import without creating a backwards dependency (orchestrator → executor).
6
+ *
7
+ * @module
8
+ */
9
+ import type { PhasePauseHandle } from "./types.js";
10
+ /**
11
+ * Print a line to stdout while the renderer is active without breaking
12
+ * log-update's cursor model.
13
+ *
14
+ * `log-update` tracks `previousLineCount` from its own writes only; any
15
+ * out-of-band write to the same pty advances the cursor without its
16
+ * knowledge, so the next `eraseLines(previousLineCount)` undershoots and
17
+ * strands the prior frame's top rows in scrollback as duplicate headers.
18
+ *
19
+ * Routing:
20
+ * - With a `PhasePauseHandle` (TTY run): route through `appendNotice`,
21
+ * which clears the live zone, writes through the renderer's own
22
+ * stdout channel, then redraws. log-update's bookkeeping stays
23
+ * correct because the clear+redraw goes through the same path as
24
+ * a normal event line.
25
+ * - Without a handle (quiet mode / non-TTY / orchestrator): fall back
26
+ * to `console.log` — there's no live zone to corrupt.
27
+ *
28
+ * Callers in `phase-executor.ts` / `run-orchestrator.ts` must use this
29
+ * helper instead of raw `console.log` — enforced by ESLint
30
+ * `no-restricted-syntax` rule in `eslint.config.js`.
31
+ */
32
+ export declare function bracketedConsoleLog(spinner: PhasePauseHandle | undefined, message: string): void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Renderer-aware notice routing (#647 AC-3).
3
+ *
4
+ * Lives in its own module so both `phase-executor` and `run-orchestrator`
5
+ * can import without creating a backwards dependency (orchestrator → executor).
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * Print a line to stdout while the renderer is active without breaking
11
+ * log-update's cursor model.
12
+ *
13
+ * `log-update` tracks `previousLineCount` from its own writes only; any
14
+ * out-of-band write to the same pty advances the cursor without its
15
+ * knowledge, so the next `eraseLines(previousLineCount)` undershoots and
16
+ * strands the prior frame's top rows in scrollback as duplicate headers.
17
+ *
18
+ * Routing:
19
+ * - With a `PhasePauseHandle` (TTY run): route through `appendNotice`,
20
+ * which clears the live zone, writes through the renderer's own
21
+ * stdout channel, then redraws. log-update's bookkeeping stays
22
+ * correct because the clear+redraw goes through the same path as
23
+ * a normal event line.
24
+ * - Without a handle (quiet mode / non-TTY / orchestrator): fall back
25
+ * to `console.log` — there's no live zone to corrupt.
26
+ *
27
+ * Callers in `phase-executor.ts` / `run-orchestrator.ts` must use this
28
+ * helper instead of raw `console.log` — enforced by ESLint
29
+ * `no-restricted-syntax` rule in `eslint.config.js`.
30
+ */
31
+ export function bracketedConsoleLog(spinner, message) {
32
+ if (spinner) {
33
+ spinner.appendNotice(message);
34
+ }
35
+ else {
36
+ console.log(message);
37
+ }
38
+ }
@@ -8,20 +8,33 @@
8
8
  * is agent-agnostic.
9
9
  */
10
10
  import { ShutdownManager } from "../shutdown.js";
11
- import { PhaseSpinner } from "../phase-spinner.js";
12
- import { Phase, ExecutionConfig, PhaseResult, QaVerdict } from "./types.js";
11
+ import { Phase, ExecutionConfig, PhaseResult, QaVerdict, PhasePauseHandle } from "./types.js";
13
12
  import type { QaSummary } from "./run-log-schema.js";
14
- import type { AgentPhaseResult } from "./drivers/index.js";
13
+ import type { AgentPhaseResult, ResumeHandle } from "./drivers/index.js";
15
14
  /**
16
- * Spec-specific retry configuration.
17
- * Spec failures have a higher failure rate (~8.6%) than other phases due to
18
- * transient GitHub API issues and rate limits. One extra retry with backoff
19
- * recovers most of these without user intervention.
15
+ * Leading + trailing throttle. Fires the wrapped callback immediately on the
16
+ * first call, drops subsequent calls that arrive inside `intervalMs` but
17
+ * remembers the latest payload, and fires one final "trailing" call with that
18
+ * latest payload after the window closes. Used to bridge the agent driver's
19
+ * fine-grained `onOutput` stream (#543) to the TUI's `nowLine` without
20
+ * either burning the 10 Hz snapshot budget on every chunk or losing the last
21
+ * useful chunk before the agent goes idle.
22
+ *
23
+ * `cancel()` clears the pending timer + payload — call after the consuming
24
+ * phase finishes so a residual trailing fire doesn't outlive its phase
25
+ * context. (The orchestrator's stale-phase guard catches it anyway, but
26
+ * cleanup avoids holding even a no-op timer.)
27
+ *
28
+ * @internal Exported for testing only.
20
29
  */
30
+ export declare function createThrottledReporter(fn: (text: string) => void, intervalMs: number): {
31
+ report(text: string): void;
32
+ cancel(): void;
33
+ };
21
34
  /** @internal Exported for testing only */
22
- export declare const SPEC_RETRY_BACKOFF_MS = 5000;
35
+ export declare const SPEC_RETRY_BACKOFF_MS: number;
23
36
  /** @internal Exported for testing only */
24
- export declare const SPEC_EXTRA_RETRIES = 1;
37
+ export declare const SPEC_EXTRA_RETRIES: number;
25
38
  export declare function parseQaVerdict(output: string): QaVerdict | null;
26
39
  /**
27
40
  * Parse condensed QA summary from QA phase output (#434).
@@ -41,17 +54,43 @@ export declare function parseQaSummary(output: string): QaSummary | null;
41
54
  * Format duration in human-readable format
42
55
  */
43
56
  export declare function formatDuration(seconds: number): string;
57
+ /**
58
+ * Resolve the base ref the zero-diff guard should compare against for
59
+ * this worktree.
60
+ *
61
+ * Reads `branch.<current>.sequantBase` — written by `scripts/new-feature.sh`
62
+ * when a worktree is created with `--base <branch>`. Returns `origin/<base>`
63
+ * (prepending `origin/` only when the recorded value does not already
64
+ * reference a remote). Falls back to `"origin/main"` on missing config,
65
+ * missing branch, or any git error — preserves the pre-#537 behavior
66
+ * for worktrees that predate this change or are managed outside
67
+ * `new-feature.sh`.
68
+ *
69
+ * Uses `execFileSync` (not `execSync`) so argv is passed directly to
70
+ * `execve` without shell interpretation — the recorded value originates
71
+ * from the user-supplied `--base` CLI flag, and shell-interpolating it
72
+ * would open a shell-injection vector. With `execFileSync`, a malicious
73
+ * value is at worst treated as an invalid revspec by git (triggering
74
+ * the fail-open path), never executed as shell.
75
+ *
76
+ * @internal Exported for testing only.
77
+ */
78
+ export declare function resolveBaseRef(cwd: string): string;
44
79
  /**
45
80
  * Check whether the exec phase produced any changes in the worktree.
46
- * Returns true if HEAD has commits unique to it relative to origin/main
47
- * OR uncommitted work is present.
81
+ * Returns true if HEAD has commits unique to it relative to the resolved
82
+ * base ref (see {@link resolveBaseRef}) OR uncommitted work is present.
48
83
  *
49
- * Uses `git rev-list --count origin/main..HEAD` (commits reachable from HEAD
50
- * but not origin/main) instead of `git diff origin/main..HEAD`, because the
51
- * two-dot diff also fires in reverse when origin/main has advanced past HEAD
84
+ * Uses `git rev-list --count <base>..HEAD` (commits reachable from HEAD
85
+ * but not the base) instead of `git diff <base>..HEAD`, because the
86
+ * two-dot diff also fires in reverse when the base has advanced past HEAD
52
87
  * — on stale branches that would falsely report "has commits" even when the
53
88
  * exec phase produced nothing, reintroducing the bug #534 is fixing.
54
89
  *
90
+ * The base ref defaults to `origin/main` but is overridden to the worktree's
91
+ * recorded base (see #537) so zero-diff execs are still detected on
92
+ * custom-base worktrees (e.g. those created with `--base feature/epic`).
93
+ *
55
94
  * Fails open (returns true) on git errors — a missing origin ref is better
56
95
  * diagnosed as a real zero-diff run than as a false phase failure.
57
96
  *
@@ -70,6 +109,7 @@ export declare function hasExecChanges(cwd: string): boolean;
70
109
  */
71
110
  export declare function mapAgentSuccessToPhaseResult(phase: Phase, agentResult: AgentPhaseResult, durationSeconds: number, cwd: string): PhaseResult & {
72
111
  sessionId?: string;
112
+ resumeHandle?: ResumeHandle;
73
113
  };
74
114
  /**
75
115
  * Get the prompt for a phase with the issue number substituted.
@@ -83,8 +123,9 @@ export declare function getPhasePrompt(phase: Phase, issueNumber: number, agent?
83
123
  /**
84
124
  * Execute a single phase for an issue using the configured AgentDriver.
85
125
  */
86
- declare function executePhase(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhaseSpinner): Promise<PhaseResult & {
126
+ declare function executePhase(issueNumber: number, phase: Phase, config: ExecutionConfig, resumeHandle?: ResumeHandle, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhasePauseHandle): Promise<PhaseResult & {
87
127
  sessionId?: string;
128
+ resumeHandle?: ResumeHandle;
88
129
  }>;
89
130
  /**
90
131
  * Execute a phase with automatic retry for cold-start failures and MCP fallback.
@@ -100,11 +141,12 @@ declare function executePhase(issueNumber: number, phase: Phase, config: Executi
100
141
  /**
101
142
  * @internal Exported for testing only
102
143
  */
103
- export declare function executePhaseWithRetry(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhaseSpinner,
144
+ export declare function executePhaseWithRetry(issueNumber: number, phase: Phase, config: ExecutionConfig, resumeHandle?: ResumeHandle, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhasePauseHandle,
104
145
  /** @internal Injected for testing — defaults to module-level executePhase */
105
146
  executePhaseFn?: typeof executePhase,
106
147
  /** @internal Injected for testing — defaults to setTimeout-based delay */
107
148
  delayFn?: (ms: number) => Promise<void>): Promise<PhaseResult & {
108
149
  sessionId?: string;
150
+ resumeHandle?: ResumeHandle;
109
151
  }>;
110
152
  export {};