sequant 2.1.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +73 -0
- package/dist/bin/cli.js +95 -9
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +118 -0
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +39 -0
- package/dist/src/commands/prompt.js +179 -0
- package/dist/src/commands/run-display.d.ts +26 -0
- package/dist/src/commands/run-display.js +150 -0
- package/dist/src/commands/run-progress.d.ts +32 -0
- package/dist/src/commands/run-progress.js +76 -0
- package/dist/src/commands/run.js +83 -73
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +27 -1
- package/dist/src/commands/watch.d.ts +16 -0
- package/dist/src/commands/watch.js +147 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +106 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +68 -0
- package/dist/src/lib/relay/types.js +76 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/skill-version.d.ts +19 -0
- package/dist/src/lib/skill-version.js +68 -0
- package/dist/src/lib/templates.d.ts +1 -0
- package/dist/src/lib/templates.js +1 -1
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +249 -176
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
- package/dist/src/lib/workflow/phase-executor.js +276 -52
- package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
- package/dist/src/lib/workflow/phase-mapper.js +17 -20
- package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
- package/dist/src/lib/workflow/run-orchestrator.js +382 -29
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +12 -1
- package/dist/src/lib/workflow/state-manager.js +37 -0
- package/dist/src/lib/workflow/state-schema.d.ts +62 -0
- package/dist/src/lib/workflow/state-schema.js +35 -1
- package/dist/src/lib/workflow/types.d.ts +74 -1
- package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
- package/dist/src/lib/workflow/worktree-manager.js +76 -17
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +10 -3
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +11 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +261 -94
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -49
- package/templates/skills/fullsolve/SKILL.md +80 -32
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +746 -8
- package/templates/skills/qa/scripts/quality-checks.sh +47 -1
- package/templates/skills/setup/SKILL.md +6 -0
- package/templates/skills/spec/SKILL.md +217 -964
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/quality-checklist.md +75 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/test/SKILL.md +0 -27
- package/templates/skills/testgen/SKILL.md +24 -44
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ShutdownManager } from "../shutdown.js";
|
|
2
|
+
export interface LivenessHeartbeatOptions {
|
|
3
|
+
/** Polling cadence for heartbeat ticks. Default: 30_000ms */
|
|
4
|
+
pollIntervalMs?: number;
|
|
5
|
+
/** Stall threshold (mtime gap) before warning fires. Default: 5min */
|
|
6
|
+
stallThresholdMs?: number;
|
|
7
|
+
/** Liveness file whose mtime is the activity proxy. Default: .sequant/state.json */
|
|
8
|
+
livenessFile?: string;
|
|
9
|
+
/** When false, no timer is started (heartbeat fully suppressed). Default: true */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Per-phase timeout in seconds, used for the "phase timeout in N" suffix
|
|
13
|
+
* on stall warnings. When omitted, suffix is dropped.
|
|
14
|
+
*/
|
|
15
|
+
phaseTimeoutSeconds?: number;
|
|
16
|
+
/** Optional ShutdownManager for graceful cleanup */
|
|
17
|
+
shutdownManager?: ShutdownManager;
|
|
18
|
+
/** Override TTY detection (testing). Default: process.stdout.isTTY */
|
|
19
|
+
isTTY?: boolean;
|
|
20
|
+
/** Override clock (testing). Default: Date.now */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
/** Override stdout writer (testing). Default: process.stdout.write */
|
|
23
|
+
stdoutWrite?: (s: string) => void;
|
|
24
|
+
/** Override stderr writer (testing). Default: process.stderr.write */
|
|
25
|
+
stderrWrite?: (s: string) => void;
|
|
26
|
+
}
|
|
27
|
+
interface PhaseKey {
|
|
28
|
+
issueNumber: number;
|
|
29
|
+
phase: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class LivenessHeartbeat {
|
|
32
|
+
private readonly pollIntervalMs;
|
|
33
|
+
private readonly stallThresholdMs;
|
|
34
|
+
private readonly livenessFile;
|
|
35
|
+
private readonly enabled;
|
|
36
|
+
private readonly phaseTimeoutSeconds?;
|
|
37
|
+
private readonly shutdownManager?;
|
|
38
|
+
private readonly tty;
|
|
39
|
+
private readonly now;
|
|
40
|
+
private readonly stdoutWrite;
|
|
41
|
+
private readonly stderrWrite;
|
|
42
|
+
private readonly phases;
|
|
43
|
+
private timer;
|
|
44
|
+
private stopped;
|
|
45
|
+
private cleanupRegistered;
|
|
46
|
+
constructor(options?: LivenessHeartbeatOptions);
|
|
47
|
+
/**
|
|
48
|
+
* Begin tracking a phase. Starts the shared poll timer on first call.
|
|
49
|
+
* No-op if `enabled === false`.
|
|
50
|
+
*/
|
|
51
|
+
start(entry: PhaseKey & {
|
|
52
|
+
startedAt: number;
|
|
53
|
+
}): void;
|
|
54
|
+
/**
|
|
55
|
+
* Stop tracking a specific phase. When the last phase is removed, the timer
|
|
56
|
+
* is cleared so no orphaned polls remain.
|
|
57
|
+
*/
|
|
58
|
+
stop(key?: PhaseKey): void;
|
|
59
|
+
/**
|
|
60
|
+
* Dispose all tracked phases and clear the timer. Idempotent.
|
|
61
|
+
*/
|
|
62
|
+
dispose(): void;
|
|
63
|
+
/** Test hook: drive a poll synchronously without waiting on real timers. */
|
|
64
|
+
tickNow(): void;
|
|
65
|
+
private tick;
|
|
66
|
+
private writeHeartbeat;
|
|
67
|
+
private writeStallWarning;
|
|
68
|
+
}
|
|
69
|
+
/** Convenience factory mirroring `phaseSpinner()`. */
|
|
70
|
+
export declare function livenessHeartbeat(options?: LivenessHeartbeatOptions): LivenessHeartbeat;
|
|
71
|
+
export {};
|
|
@@ -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)[K`;
|
|
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[K" : "";
|
|
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
|
+
}
|
|
@@ -8,9 +8,38 @@
|
|
|
8
8
|
* is agent-agnostic.
|
|
9
9
|
*/
|
|
10
10
|
import { ShutdownManager } from "../shutdown.js";
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Lifecycle hook for pausing the run renderer's live zone while verbose
|
|
13
|
+
* Claude streaming writes through stdout, then resuming after the agent
|
|
14
|
+
* call completes. Replaces the legacy `PhaseSpinner` argument (#618).
|
|
15
|
+
*/
|
|
16
|
+
export interface PhasePauseHandle {
|
|
17
|
+
pause(): void;
|
|
18
|
+
resume(): void;
|
|
19
|
+
}
|
|
12
20
|
import { Phase, ExecutionConfig, PhaseResult, QaVerdict } from "./types.js";
|
|
13
21
|
import type { QaSummary } from "./run-log-schema.js";
|
|
22
|
+
import type { AgentPhaseResult } from "./drivers/index.js";
|
|
23
|
+
/**
|
|
24
|
+
* Leading + trailing throttle. Fires the wrapped callback immediately on the
|
|
25
|
+
* first call, drops subsequent calls that arrive inside `intervalMs` but
|
|
26
|
+
* remembers the latest payload, and fires one final "trailing" call with that
|
|
27
|
+
* latest payload after the window closes. Used to bridge the agent driver's
|
|
28
|
+
* fine-grained `onOutput` stream (#543) to the TUI's `nowLine` without
|
|
29
|
+
* either burning the 10 Hz snapshot budget on every chunk or losing the last
|
|
30
|
+
* useful chunk before the agent goes idle.
|
|
31
|
+
*
|
|
32
|
+
* `cancel()` clears the pending timer + payload — call after the consuming
|
|
33
|
+
* phase finishes so a residual trailing fire doesn't outlive its phase
|
|
34
|
+
* context. (The orchestrator's stale-phase guard catches it anyway, but
|
|
35
|
+
* cleanup avoids holding even a no-op timer.)
|
|
36
|
+
*
|
|
37
|
+
* @internal Exported for testing only.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createThrottledReporter(fn: (text: string) => void, intervalMs: number): {
|
|
40
|
+
report(text: string): void;
|
|
41
|
+
cancel(): void;
|
|
42
|
+
};
|
|
14
43
|
/**
|
|
15
44
|
* Spec-specific retry configuration.
|
|
16
45
|
* Spec failures have a higher failure rate (~8.6%) than other phases due to
|
|
@@ -40,6 +69,62 @@ export declare function parseQaSummary(output: string): QaSummary | null;
|
|
|
40
69
|
* Format duration in human-readable format
|
|
41
70
|
*/
|
|
42
71
|
export declare function formatDuration(seconds: number): string;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the base ref the zero-diff guard should compare against for
|
|
74
|
+
* this worktree.
|
|
75
|
+
*
|
|
76
|
+
* Reads `branch.<current>.sequantBase` — written by `scripts/new-feature.sh`
|
|
77
|
+
* when a worktree is created with `--base <branch>`. Returns `origin/<base>`
|
|
78
|
+
* (prepending `origin/` only when the recorded value does not already
|
|
79
|
+
* reference a remote). Falls back to `"origin/main"` on missing config,
|
|
80
|
+
* missing branch, or any git error — preserves the pre-#537 behavior
|
|
81
|
+
* for worktrees that predate this change or are managed outside
|
|
82
|
+
* `new-feature.sh`.
|
|
83
|
+
*
|
|
84
|
+
* Uses `execFileSync` (not `execSync`) so argv is passed directly to
|
|
85
|
+
* `execve` without shell interpretation — the recorded value originates
|
|
86
|
+
* from the user-supplied `--base` CLI flag, and shell-interpolating it
|
|
87
|
+
* would open a shell-injection vector. With `execFileSync`, a malicious
|
|
88
|
+
* value is at worst treated as an invalid revspec by git (triggering
|
|
89
|
+
* the fail-open path), never executed as shell.
|
|
90
|
+
*
|
|
91
|
+
* @internal Exported for testing only.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveBaseRef(cwd: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Check whether the exec phase produced any changes in the worktree.
|
|
96
|
+
* Returns true if HEAD has commits unique to it relative to the resolved
|
|
97
|
+
* base ref (see {@link resolveBaseRef}) OR uncommitted work is present.
|
|
98
|
+
*
|
|
99
|
+
* Uses `git rev-list --count <base>..HEAD` (commits reachable from HEAD
|
|
100
|
+
* but not the base) instead of `git diff <base>..HEAD`, because the
|
|
101
|
+
* two-dot diff also fires in reverse when the base has advanced past HEAD
|
|
102
|
+
* — on stale branches that would falsely report "has commits" even when the
|
|
103
|
+
* exec phase produced nothing, reintroducing the bug #534 is fixing.
|
|
104
|
+
*
|
|
105
|
+
* The base ref defaults to `origin/main` but is overridden to the worktree's
|
|
106
|
+
* recorded base (see #537) so zero-diff execs are still detected on
|
|
107
|
+
* custom-base worktrees (e.g. those created with `--base feature/epic`).
|
|
108
|
+
*
|
|
109
|
+
* Fails open (returns true) on git errors — a missing origin ref is better
|
|
110
|
+
* diagnosed as a real zero-diff run than as a false phase failure.
|
|
111
|
+
*
|
|
112
|
+
* @internal Exported for testing only.
|
|
113
|
+
*/
|
|
114
|
+
export declare function hasExecChanges(cwd: string): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Map a successful AgentPhaseResult to a PhaseResult, applying phase-specific
|
|
117
|
+
* guards that catch agent sessions which returned success without producing
|
|
118
|
+
* usable work (#534):
|
|
119
|
+
*
|
|
120
|
+
* - `qa`: fails when no parseable verdict is found (empty or malformed output).
|
|
121
|
+
* - `exec`: fails when no commits and no uncommitted changes exist.
|
|
122
|
+
*
|
|
123
|
+
* @internal Exported for testing only.
|
|
124
|
+
*/
|
|
125
|
+
export declare function mapAgentSuccessToPhaseResult(phase: Phase, agentResult: AgentPhaseResult, durationSeconds: number, cwd: string): PhaseResult & {
|
|
126
|
+
sessionId?: string;
|
|
127
|
+
};
|
|
43
128
|
/**
|
|
44
129
|
* Get the prompt for a phase with the issue number substituted.
|
|
45
130
|
* Selects self-contained prompts for non-Claude agents.
|
|
@@ -52,7 +137,7 @@ export declare function getPhasePrompt(phase: Phase, issueNumber: number, agent?
|
|
|
52
137
|
/**
|
|
53
138
|
* Execute a single phase for an issue using the configured AgentDriver.
|
|
54
139
|
*/
|
|
55
|
-
declare function executePhase(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?:
|
|
140
|
+
declare function executePhase(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhasePauseHandle): Promise<PhaseResult & {
|
|
56
141
|
sessionId?: string;
|
|
57
142
|
}>;
|
|
58
143
|
/**
|
|
@@ -69,7 +154,7 @@ declare function executePhase(issueNumber: number, phase: Phase, config: Executi
|
|
|
69
154
|
/**
|
|
70
155
|
* @internal Exported for testing only
|
|
71
156
|
*/
|
|
72
|
-
export declare function executePhaseWithRetry(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?:
|
|
157
|
+
export declare function executePhaseWithRetry(issueNumber: number, phase: Phase, config: ExecutionConfig, sessionId?: string, worktreePath?: string, shutdownManager?: ShutdownManager, spinner?: PhasePauseHandle,
|
|
73
158
|
/** @internal Injected for testing — defaults to module-level executePhase */
|
|
74
159
|
executePhaseFn?: typeof executePhase,
|
|
75
160
|
/** @internal Injected for testing — defaults to setTimeout-based delay */
|