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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +81 -5
- package/dist/bin/cli.js +140 -13
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- 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 +46 -0
- package/dist/src/commands/prompt.js +273 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +42 -0
- package/dist/src/commands/run-progress.js +93 -0
- package/dist/src/commands/run.js +90 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +18 -0
- package/dist/src/commands/watch.js +211 -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 +220 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -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/merge-check/types.js +1 -1
- 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 +112 -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 +70 -0
- package/dist/src/lib/relay/types.js +85 -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/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 +274 -185
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -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/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
- package/dist/src/lib/workflow/phase-executor.js +244 -130
- package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
- package/dist/src/lib/workflow/phase-mapper.js +70 -51
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- 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-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
- package/dist/src/lib/workflow/run-orchestrator.js +464 -25
- 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 +31 -2
- package/dist/src/lib/workflow/state-manager.js +64 -1
- package/dist/src/lib/workflow/state-schema.d.ts +82 -35
- package/dist/src/lib/workflow/state-schema.js +63 -4
- package/dist/src/lib/workflow/types.d.ts +139 -16
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- 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 +14 -6
- 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 +92 -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 +122 -68
- 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 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +12 -6
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/testgen/SKILL.md +24 -17
|
@@ -146,6 +146,9 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
146
146
|
: (settings.run.retry ?? true);
|
|
147
147
|
const isSequential = mergedOptions.sequential ?? false;
|
|
148
148
|
const isParallel = !isSequential && issueCount > 1;
|
|
149
|
+
// `--no-relay` arrives from Commander as `mergedOptions.relay === false`;
|
|
150
|
+
// explicit `false` overrides settings, otherwise settings/default win.
|
|
151
|
+
const relayEnabled = mergedOptions.relay === false ? false : (settings.run.relay ?? true);
|
|
149
152
|
return {
|
|
150
153
|
...DEFAULT_CONFIG,
|
|
151
154
|
phases: explicitPhases ?? DEFAULT_PHASES,
|
|
@@ -163,5 +166,6 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
163
166
|
agent: mergedOptions.agent ?? settings.run.agent,
|
|
164
167
|
aiderSettings: settings.run.aider,
|
|
165
168
|
isolateParallel: mergedOptions.isolateParallel,
|
|
169
|
+
relayEnabled,
|
|
166
170
|
};
|
|
167
171
|
}
|
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
* Continue.dev, Copilot SDK, Cursor API) can be added by implementing this
|
|
6
6
|
* interface without touching orchestration logic.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Resume handle for a previous agent session.
|
|
10
|
+
*
|
|
11
|
+
* Replaces the opaque `sessionId` string with a driver-tagged value that
|
|
12
|
+
* records the cwd the session was created in. Drivers use this to enforce
|
|
13
|
+
* cwd-safe resume (Claude Code: session storage is cwd-namespaced; Codex:
|
|
14
|
+
* cwd-independent SDK requires driver-side gating). See #674.
|
|
15
|
+
*/
|
|
16
|
+
export interface ResumeHandle {
|
|
17
|
+
/** Driver name that created this handle (e.g. "claude-code", "codex"). */
|
|
18
|
+
driver: string;
|
|
19
|
+
/** Driver-specific resume token (session id, thread id, etc.). */
|
|
20
|
+
token: string;
|
|
21
|
+
/** Absolute cwd the session was created in. */
|
|
22
|
+
originCwd: string;
|
|
23
|
+
}
|
|
8
24
|
/**
|
|
9
25
|
* Configuration passed to an agent for phase execution.
|
|
10
26
|
*/
|
|
@@ -15,8 +31,17 @@ export interface AgentExecutionConfig {
|
|
|
15
31
|
phaseTimeout: number;
|
|
16
32
|
verbose: boolean;
|
|
17
33
|
mcp: boolean;
|
|
18
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Resume a previous session (driver-specific; ignored if unsupported).
|
|
36
|
+
*
|
|
37
|
+
* @deprecated Use {@link resumeHandle}. The opaque `sessionId` field is
|
|
38
|
+
* retained for one release to keep in-flight `.sequant/state.json` records
|
|
39
|
+
* resumable across upgrade. Drivers MUST prefer `resumeHandle` when both
|
|
40
|
+
* are set. See #674.
|
|
41
|
+
*/
|
|
19
42
|
sessionId?: string;
|
|
43
|
+
/** Driver-tagged resume handle with originCwd for cwd-safe resume (#674). */
|
|
44
|
+
resumeHandle?: ResumeHandle;
|
|
20
45
|
/** Callback for streaming output */
|
|
21
46
|
onOutput?: (text: string) => void;
|
|
22
47
|
/** Callback for stderr */
|
|
@@ -30,7 +55,14 @@ export interface AgentExecutionConfig {
|
|
|
30
55
|
export interface AgentPhaseResult {
|
|
31
56
|
success: boolean;
|
|
32
57
|
output: string;
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Use {@link resumeHandle}. Retained as a mirror of
|
|
60
|
+
* `resumeHandle.token` for one release to ease state-file migration. See
|
|
61
|
+
* #674.
|
|
62
|
+
*/
|
|
33
63
|
sessionId?: string;
|
|
64
|
+
/** Driver-tagged resume handle for cwd-safe cross-phase resume (#674). */
|
|
65
|
+
resumeHandle?: ResumeHandle;
|
|
34
66
|
error?: string;
|
|
35
67
|
/** Last N lines of stderr captured via RingBuffer (#447) */
|
|
36
68
|
stderrTail?: string[];
|
|
@@ -53,4 +85,19 @@ export interface AgentDriver {
|
|
|
53
85
|
executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
|
|
54
86
|
/** Check if this driver is available/configured */
|
|
55
87
|
isAvailable(): Promise<boolean>;
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether a resume handle can be safely used for a target cwd.
|
|
90
|
+
*
|
|
91
|
+
* Implementations enforce the asymmetric resume contract (#674):
|
|
92
|
+
* - Claude Code: session storage is cwd-namespaced; resume only if cwds
|
|
93
|
+
* match byte-equal.
|
|
94
|
+
* - Codex (when added in #497): runtime is cwd-independent; the driver
|
|
95
|
+
* enforces cwd match (and AGENTS.md parity) to prevent silent
|
|
96
|
+
* misexecution.
|
|
97
|
+
* - Drivers without a session-resume concept return `false`.
|
|
98
|
+
*
|
|
99
|
+
* Drivers MUST also verify `handle.driver === this.name` and reject
|
|
100
|
+
* cross-driver handles.
|
|
101
|
+
*/
|
|
102
|
+
canResume(handle: ResumeHandle, targetCwd: string): boolean;
|
|
56
103
|
}
|
|
@@ -5,12 +5,18 @@
|
|
|
5
5
|
* for fully non-interactive phase execution. Sequant manages git,
|
|
6
6
|
* not Aider.
|
|
7
7
|
*/
|
|
8
|
-
import type { AgentDriver, AgentExecutionConfig, AgentPhaseResult } from "./agent-driver.js";
|
|
8
|
+
import type { AgentDriver, AgentExecutionConfig, AgentPhaseResult, ResumeHandle } from "./agent-driver.js";
|
|
9
9
|
import type { AiderSettings } from "../../settings.js";
|
|
10
10
|
export declare class AiderDriver implements AgentDriver {
|
|
11
11
|
name: string;
|
|
12
12
|
private settings?;
|
|
13
13
|
constructor(settings?: AiderSettings);
|
|
14
|
+
/**
|
|
15
|
+
* Aider has no session-resume concept: each invocation is a one-shot
|
|
16
|
+
* `aider --message <prompt>` against a fresh chat. There is no token to
|
|
17
|
+
* reattach to, so resume is always declined (#674).
|
|
18
|
+
*/
|
|
19
|
+
canResume(handle: ResumeHandle, targetCwd: string): boolean;
|
|
14
20
|
executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
|
|
15
21
|
isAvailable(): Promise<boolean>;
|
|
16
22
|
/** Build the CLI argument list for aider. */
|
|
@@ -14,6 +14,15 @@ export class AiderDriver {
|
|
|
14
14
|
constructor(settings) {
|
|
15
15
|
this.settings = settings;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Aider has no session-resume concept: each invocation is a one-shot
|
|
19
|
+
* `aider --message <prompt>` against a fresh chat. There is no token to
|
|
20
|
+
* reattach to, so resume is always declined (#674).
|
|
21
|
+
*/
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
|
+
canResume(handle, targetCwd) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
17
26
|
async executePhase(prompt, config) {
|
|
18
27
|
const args = this.buildArgs(prompt, config.files);
|
|
19
28
|
return new Promise((resolve) => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns the `@anthropic-ai/claude-agent-sdk` import. No other file in the
|
|
5
5
|
* orchestration layer should import the SDK directly.
|
|
6
6
|
*/
|
|
7
|
-
import type { AgentDriver, AgentExecutionConfig, AgentPhaseResult } from "./agent-driver.js";
|
|
7
|
+
import type { AgentDriver, AgentExecutionConfig, AgentPhaseResult, ResumeHandle } from "./agent-driver.js";
|
|
8
8
|
export declare class ClaudeCodeDriver implements AgentDriver {
|
|
9
9
|
name: string;
|
|
10
10
|
/**
|
|
@@ -12,6 +12,22 @@ export declare class ClaudeCodeDriver implements AgentDriver {
|
|
|
12
12
|
* Set after each executePhase() call.
|
|
13
13
|
*/
|
|
14
14
|
private lastSessionId?;
|
|
15
|
+
/**
|
|
16
|
+
* Decide whether a resume handle can be used for a target cwd.
|
|
17
|
+
*
|
|
18
|
+
* Claude Code namespaces session storage by encoded cwd
|
|
19
|
+
* (`~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`), so a session
|
|
20
|
+
* created in cwd A cannot be located when resuming from cwd B — the SDK
|
|
21
|
+
* returns `error_during_execution` ("No conversation found") rather than
|
|
22
|
+
* crashing (verified against `@anthropic-ai/claude-agent-sdk@0.3.142`,
|
|
23
|
+
* see #674).
|
|
24
|
+
*
|
|
25
|
+
* We use byte-equal comparison, not a normalized path: the SDK's storage
|
|
26
|
+
* key is derived from the literal cwd string, so normalizing here would
|
|
27
|
+
* risk false-positive resumes whose token would then miss on disk.
|
|
28
|
+
*/
|
|
29
|
+
canResume(handle: ResumeHandle, targetCwd: string): boolean;
|
|
15
30
|
executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
|
|
31
|
+
private buildResumeHandle;
|
|
16
32
|
isAvailable(): Promise<boolean>;
|
|
17
33
|
}
|
|
@@ -14,6 +14,25 @@ export class ClaudeCodeDriver {
|
|
|
14
14
|
* Set after each executePhase() call.
|
|
15
15
|
*/
|
|
16
16
|
lastSessionId;
|
|
17
|
+
/**
|
|
18
|
+
* Decide whether a resume handle can be used for a target cwd.
|
|
19
|
+
*
|
|
20
|
+
* Claude Code namespaces session storage by encoded cwd
|
|
21
|
+
* (`~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`), so a session
|
|
22
|
+
* created in cwd A cannot be located when resuming from cwd B — the SDK
|
|
23
|
+
* returns `error_during_execution` ("No conversation found") rather than
|
|
24
|
+
* crashing (verified against `@anthropic-ai/claude-agent-sdk@0.3.142`,
|
|
25
|
+
* see #674).
|
|
26
|
+
*
|
|
27
|
+
* We use byte-equal comparison, not a normalized path: the SDK's storage
|
|
28
|
+
* key is derived from the literal cwd string, so normalizing here would
|
|
29
|
+
* risk false-positive resumes whose token would then miss on disk.
|
|
30
|
+
*/
|
|
31
|
+
canResume(handle, targetCwd) {
|
|
32
|
+
if (handle.driver !== this.name)
|
|
33
|
+
return false;
|
|
34
|
+
return handle.originCwd === targetCwd;
|
|
35
|
+
}
|
|
17
36
|
async executePhase(prompt, config) {
|
|
18
37
|
const abortController = new AbortController();
|
|
19
38
|
// Wire external abort signal
|
|
@@ -30,6 +49,23 @@ export class ClaudeCodeDriver {
|
|
|
30
49
|
let capturedStderr = "";
|
|
31
50
|
const stderrBuffer = new RingBuffer(50);
|
|
32
51
|
const stdoutBuffer = new RingBuffer(50);
|
|
52
|
+
// Resolve resume token with cwd-safety check.
|
|
53
|
+
//
|
|
54
|
+
// Prefer the driver-tagged `resumeHandle` over the legacy `sessionId`
|
|
55
|
+
// string (#674). On cwd mismatch we silently drop the resume — Claude
|
|
56
|
+
// Code's cwd-mismatched resume is recoverable (`error_during_execution`,
|
|
57
|
+
// "No conversation found"), but starting fresh is the cleaner outcome
|
|
58
|
+
// than surfacing a per-phase error the caller can't act on.
|
|
59
|
+
let resumeToken;
|
|
60
|
+
if (config.resumeHandle &&
|
|
61
|
+
this.canResume(config.resumeHandle, config.cwd)) {
|
|
62
|
+
resumeToken = config.resumeHandle.token;
|
|
63
|
+
}
|
|
64
|
+
else if (!config.resumeHandle && config.sessionId) {
|
|
65
|
+
// Back-compat: legacy state (sessionId without originCwd) cannot prove
|
|
66
|
+
// cwd parity. Per #674's fail-safe rule, do NOT resume — start fresh.
|
|
67
|
+
resumeToken = undefined;
|
|
68
|
+
}
|
|
33
69
|
try {
|
|
34
70
|
// Get MCP servers config if enabled
|
|
35
71
|
const mcpServers = config.mcp ? getMcpServersConfig() : undefined;
|
|
@@ -43,8 +79,8 @@ export class ClaudeCodeDriver {
|
|
|
43
79
|
tools: { type: "preset", preset: "claude_code" },
|
|
44
80
|
permissionMode: "bypassPermissions",
|
|
45
81
|
allowDangerouslySkipPermissions: true,
|
|
46
|
-
// Resume from previous session
|
|
47
|
-
...(
|
|
82
|
+
// Resume from previous session only when cwd matches origin (#674).
|
|
83
|
+
...(resumeToken ? { resume: resumeToken } : {}),
|
|
48
84
|
env: config.env,
|
|
49
85
|
...(mcpServers ? { mcpServers } : {}),
|
|
50
86
|
stderr: (data) => {
|
|
@@ -84,12 +120,17 @@ export class ClaudeCodeDriver {
|
|
|
84
120
|
}
|
|
85
121
|
clearTimeout(timeoutId);
|
|
86
122
|
this.lastSessionId = resultSessionId;
|
|
123
|
+
// Build the cwd-bound resume handle from the session created in
|
|
124
|
+
// `config.cwd`. `sessionId` is mirrored for one release (#674) so
|
|
125
|
+
// upgraded callers can still drive resume off the deprecated field.
|
|
126
|
+
const resumeHandle = this.buildResumeHandle(resultSessionId, config.cwd);
|
|
87
127
|
if (resultMessage) {
|
|
88
128
|
if (resultMessage.subtype === "success") {
|
|
89
129
|
return {
|
|
90
130
|
success: true,
|
|
91
131
|
output: capturedOutput,
|
|
92
132
|
sessionId: resultSessionId,
|
|
133
|
+
resumeHandle,
|
|
93
134
|
stderrTail: stderrBuffer.getLines(),
|
|
94
135
|
stdoutTail: stdoutBuffer.getLines(),
|
|
95
136
|
};
|
|
@@ -113,6 +154,7 @@ export class ClaudeCodeDriver {
|
|
|
113
154
|
success: false,
|
|
114
155
|
output: capturedOutput,
|
|
115
156
|
sessionId: resultSessionId,
|
|
157
|
+
resumeHandle,
|
|
116
158
|
error,
|
|
117
159
|
stderrTail: stderrBuffer.getLines(),
|
|
118
160
|
stdoutTail: stdoutBuffer.getLines(),
|
|
@@ -122,6 +164,7 @@ export class ClaudeCodeDriver {
|
|
|
122
164
|
success: false,
|
|
123
165
|
output: capturedOutput,
|
|
124
166
|
sessionId: resultSessionId,
|
|
167
|
+
resumeHandle,
|
|
125
168
|
error: "No result received from Claude",
|
|
126
169
|
stderrTail: stderrBuffer.getLines(),
|
|
127
170
|
stdoutTail: stdoutBuffer.getLines(),
|
|
@@ -146,12 +189,18 @@ export class ClaudeCodeDriver {
|
|
|
146
189
|
success: false,
|
|
147
190
|
output: capturedOutput,
|
|
148
191
|
sessionId: resultSessionId,
|
|
192
|
+
resumeHandle: this.buildResumeHandle(resultSessionId, config.cwd),
|
|
149
193
|
error: error + stderrSuffix,
|
|
150
194
|
stderrTail: stderrBuffer.getLines(),
|
|
151
195
|
stdoutTail: stdoutBuffer.getLines(),
|
|
152
196
|
};
|
|
153
197
|
}
|
|
154
198
|
}
|
|
199
|
+
buildResumeHandle(token, originCwd) {
|
|
200
|
+
if (!token)
|
|
201
|
+
return undefined;
|
|
202
|
+
return { driver: this.name, token, originCwd };
|
|
203
|
+
}
|
|
155
204
|
async isAvailable() {
|
|
156
205
|
try {
|
|
157
206
|
// If we can import the SDK, it's available
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentDriver } from "./agent-driver.js";
|
|
8
8
|
import type { AiderSettings } from "../../settings.js";
|
|
9
|
-
export type { AgentDriver, AgentExecutionConfig, AgentPhaseResult, } from "./agent-driver.js";
|
|
9
|
+
export type { AgentDriver, AgentExecutionConfig, AgentPhaseResult, ResumeHandle, } from "./agent-driver.js";
|
|
10
10
|
export interface DriverOptions {
|
|
11
11
|
aiderSettings?: AiderSettings;
|
|
12
12
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed workflow event emitter (#504).
|
|
3
|
+
*
|
|
4
|
+
* Provides a strongly-typed `EventEmitter` for `RunOrchestrator` lifecycle
|
|
5
|
+
* events. Multiple consumers (TUI, MCP server, future webhooks) can subscribe
|
|
6
|
+
* without the orchestrator knowing they exist.
|
|
7
|
+
*
|
|
8
|
+
* Design notes:
|
|
9
|
+
* - Wraps Node's built-in `node:events.EventEmitter` (no third-party deps per
|
|
10
|
+
* AC-2). The typing is compile-time only — at runtime this is a vanilla
|
|
11
|
+
* `EventEmitter`.
|
|
12
|
+
* - `emit()` is overridden to invoke listeners through `Promise.allSettled`
|
|
13
|
+
* so a slow or throwing listener can never crash the pipeline (AC-5). The
|
|
14
|
+
* override returns `Promise<void>` so callers in critical paths can `await`
|
|
15
|
+
* the wrapper; fire-and-forget callers (e.g. `progress` ticks) just drop
|
|
16
|
+
* the returned promise.
|
|
17
|
+
* - The orchestrator coexists with the existing `onProgress` callback rather
|
|
18
|
+
* than replacing it. `onProgress` remains the synchronous TUI render hook;
|
|
19
|
+
* events are the async multi-subscriber surface. Consolidation is an
|
|
20
|
+
* intentional non-goal (see issue #504, descope comment 2026-04-09).
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { QaVerdict } from "./run-log-schema.js";
|
|
25
|
+
/** Issue lifecycle status surfaced through `issue_status_changed`. */
|
|
26
|
+
export type IssueEventStatus = "queued" | "running" | "passed" | "failed";
|
|
27
|
+
/**
|
|
28
|
+
* Base fields present on every workflow event payload.
|
|
29
|
+
*
|
|
30
|
+
* Payloads are JSON-serializable: only primitives and plain records, no
|
|
31
|
+
* class instances or circular references. This keeps MCP/webhook consumers
|
|
32
|
+
* cheap (`JSON.stringify(payload)` is always safe).
|
|
33
|
+
*/
|
|
34
|
+
export interface BaseEventPayload {
|
|
35
|
+
/** GitHub issue number this event is about. */
|
|
36
|
+
issueNumber: number;
|
|
37
|
+
/** ISO 8601 timestamp captured when the event was emitted. */
|
|
38
|
+
timestamp: string;
|
|
39
|
+
}
|
|
40
|
+
/** Payload for `run_started` / `run_completed`. */
|
|
41
|
+
export interface RunEventPayload extends BaseEventPayload {
|
|
42
|
+
/** Wall-clock duration in seconds. Present on `run_completed` only. */
|
|
43
|
+
duration?: number;
|
|
44
|
+
/** Aggregate success across all phases. Present on `run_completed` only. */
|
|
45
|
+
success?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Payload for `phase_started`. */
|
|
48
|
+
export interface PhaseStartedPayload extends BaseEventPayload {
|
|
49
|
+
/** Phase name (e.g. `spec`, `exec`, `qa`). */
|
|
50
|
+
phase: string;
|
|
51
|
+
/** Outer-loop iteration (1-based). Present when running under quality-loop. */
|
|
52
|
+
iteration?: number;
|
|
53
|
+
}
|
|
54
|
+
/** Payload for `phase_completed`. */
|
|
55
|
+
export interface PhaseCompletedPayload extends BaseEventPayload {
|
|
56
|
+
phase: string;
|
|
57
|
+
/** Phase wall-clock duration in seconds (matches `PhaseLog.durationSeconds`). */
|
|
58
|
+
duration: number;
|
|
59
|
+
iteration?: number;
|
|
60
|
+
}
|
|
61
|
+
/** Payload for `phase_failed`. */
|
|
62
|
+
export interface PhaseFailedPayload extends BaseEventPayload {
|
|
63
|
+
phase: string;
|
|
64
|
+
duration?: number;
|
|
65
|
+
/** Stringified error message. Class instances are flattened to strings. */
|
|
66
|
+
error: string;
|
|
67
|
+
iteration?: number;
|
|
68
|
+
}
|
|
69
|
+
/** Payload for `issue_status_changed`. */
|
|
70
|
+
export interface IssueStatusChangedPayload extends BaseEventPayload {
|
|
71
|
+
/** Previous lifecycle status. */
|
|
72
|
+
from: IssueEventStatus;
|
|
73
|
+
/** New lifecycle status. */
|
|
74
|
+
to: IssueEventStatus;
|
|
75
|
+
}
|
|
76
|
+
/** Payload for `qa_verdict`. Emitted once per QA phase that produces a verdict. */
|
|
77
|
+
export interface QaVerdictPayload extends BaseEventPayload {
|
|
78
|
+
phase: "qa";
|
|
79
|
+
verdict: QaVerdict;
|
|
80
|
+
}
|
|
81
|
+
/** Payload for `progress` (sub-phase activity ping, ~10 Hz). */
|
|
82
|
+
export interface ProgressPayload extends BaseEventPayload {
|
|
83
|
+
phase: string;
|
|
84
|
+
/** Short one-line snippet for the dashboard activity row. */
|
|
85
|
+
text?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Discriminated map of event name → payload type. Add new events by
|
|
89
|
+
* extending this map; the `emit`/`on`/`off` overloads below pick them up
|
|
90
|
+
* automatically. Reject `string` index signatures so consumers cannot
|
|
91
|
+
* subscribe to typo'd event names without a TypeScript error (AC-1, AC-2).
|
|
92
|
+
*
|
|
93
|
+
* Event names use `snake_case` intentionally — they match the issue body and
|
|
94
|
+
* common JSON-payload conventions, and they are part of the public contract
|
|
95
|
+
* for MCP / webhook consumers. Payload field names stay `camelCase`. Do not
|
|
96
|
+
* "normalize" the event-name casing.
|
|
97
|
+
*/
|
|
98
|
+
export interface WorkflowEvents {
|
|
99
|
+
run_started: RunEventPayload;
|
|
100
|
+
run_completed: RunEventPayload;
|
|
101
|
+
phase_started: PhaseStartedPayload;
|
|
102
|
+
phase_completed: PhaseCompletedPayload;
|
|
103
|
+
phase_failed: PhaseFailedPayload;
|
|
104
|
+
issue_status_changed: IssueStatusChangedPayload;
|
|
105
|
+
qa_verdict: QaVerdictPayload;
|
|
106
|
+
progress: ProgressPayload;
|
|
107
|
+
}
|
|
108
|
+
/** Listener signature for a given event name. */
|
|
109
|
+
export type WorkflowEventListener<E extends keyof WorkflowEvents> = (payload: WorkflowEvents[E]) => void | Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Build an ISO 8601 timestamp for an event payload. Centralized so tests can
|
|
112
|
+
* inject a fixed clock via the constructor.
|
|
113
|
+
*
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export type Clock = () => Date;
|
|
117
|
+
/**
|
|
118
|
+
* Typed wrapper around `node:events.EventEmitter`.
|
|
119
|
+
*
|
|
120
|
+
* Type parameter `E` is the event-name → payload map. `on`, `off`, and
|
|
121
|
+
* `emit` are all narrowed to that map; passing an unknown event name fails
|
|
122
|
+
* compilation (AC-2).
|
|
123
|
+
*/
|
|
124
|
+
export declare class WorkflowEventEmitter {
|
|
125
|
+
private readonly inner;
|
|
126
|
+
private readonly clock;
|
|
127
|
+
private readonly onListenerError;
|
|
128
|
+
constructor(opts?: {
|
|
129
|
+
clock?: Clock;
|
|
130
|
+
/**
|
|
131
|
+
* Invoked whenever a listener throws or rejects. Defaults to a no-op
|
|
132
|
+
* because rejection logging is the orchestrator's call (it knows whether
|
|
133
|
+
* `verbose` is enabled). Tests use this to assert isolation.
|
|
134
|
+
*/
|
|
135
|
+
onListenerError?: (eventName: string, error: unknown) => void;
|
|
136
|
+
});
|
|
137
|
+
/** Register a listener for the given event. */
|
|
138
|
+
on<E extends keyof WorkflowEvents>(event: E, listener: WorkflowEventListener<E>): this;
|
|
139
|
+
/** Remove a previously registered listener. */
|
|
140
|
+
off<E extends keyof WorkflowEvents>(event: E, listener: WorkflowEventListener<E>): this;
|
|
141
|
+
/** Number of listeners attached to `event`. Useful in tests. */
|
|
142
|
+
listenerCount<E extends keyof WorkflowEvents>(event: E): number;
|
|
143
|
+
/** Drop all listeners. Call from `RunOrchestrator` teardown to avoid leaks. */
|
|
144
|
+
removeAllListeners(): this;
|
|
145
|
+
/**
|
|
146
|
+
* Emit an event. Listeners are invoked through `Promise.allSettled` so a
|
|
147
|
+
* single misbehaving subscriber cannot crash the pipeline (AC-5).
|
|
148
|
+
*
|
|
149
|
+
* Returns `Promise<void>` so critical-path callers may `await`. Fire-and-
|
|
150
|
+
* forget callers (`progress`, etc.) ignore the returned promise — the
|
|
151
|
+
* `Promise.allSettled` swallowing handles unhandled rejection warnings.
|
|
152
|
+
*
|
|
153
|
+
* The `timestamp` field on the payload is populated automatically when
|
|
154
|
+
* absent so call sites stay terse.
|
|
155
|
+
*/
|
|
156
|
+
emit<E extends keyof WorkflowEvents>(event: E, payload: Omit<WorkflowEvents[E], "timestamp"> & Partial<Pick<WorkflowEvents[E], "timestamp">>): Promise<void>;
|
|
157
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed workflow event emitter (#504).
|
|
3
|
+
*
|
|
4
|
+
* Provides a strongly-typed `EventEmitter` for `RunOrchestrator` lifecycle
|
|
5
|
+
* events. Multiple consumers (TUI, MCP server, future webhooks) can subscribe
|
|
6
|
+
* without the orchestrator knowing they exist.
|
|
7
|
+
*
|
|
8
|
+
* Design notes:
|
|
9
|
+
* - Wraps Node's built-in `node:events.EventEmitter` (no third-party deps per
|
|
10
|
+
* AC-2). The typing is compile-time only — at runtime this is a vanilla
|
|
11
|
+
* `EventEmitter`.
|
|
12
|
+
* - `emit()` is overridden to invoke listeners through `Promise.allSettled`
|
|
13
|
+
* so a slow or throwing listener can never crash the pipeline (AC-5). The
|
|
14
|
+
* override returns `Promise<void>` so callers in critical paths can `await`
|
|
15
|
+
* the wrapper; fire-and-forget callers (e.g. `progress` ticks) just drop
|
|
16
|
+
* the returned promise.
|
|
17
|
+
* - The orchestrator coexists with the existing `onProgress` callback rather
|
|
18
|
+
* than replacing it. `onProgress` remains the synchronous TUI render hook;
|
|
19
|
+
* events are the async multi-subscriber surface. Consolidation is an
|
|
20
|
+
* intentional non-goal (see issue #504, descope comment 2026-04-09).
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import { EventEmitter } from "node:events";
|
|
25
|
+
const defaultClock = () => new Date();
|
|
26
|
+
/**
|
|
27
|
+
* Typed wrapper around `node:events.EventEmitter`.
|
|
28
|
+
*
|
|
29
|
+
* Type parameter `E` is the event-name → payload map. `on`, `off`, and
|
|
30
|
+
* `emit` are all narrowed to that map; passing an unknown event name fails
|
|
31
|
+
* compilation (AC-2).
|
|
32
|
+
*/
|
|
33
|
+
export class WorkflowEventEmitter {
|
|
34
|
+
inner = new EventEmitter();
|
|
35
|
+
clock;
|
|
36
|
+
onListenerError;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.clock = opts?.clock ?? defaultClock;
|
|
39
|
+
this.onListenerError = opts?.onListenerError ?? (() => { });
|
|
40
|
+
// Effectively no listener cap — multiple consumers (TUI, MCP, log writer
|
|
41
|
+
// in the future) can subscribe to popular events like `progress`.
|
|
42
|
+
this.inner.setMaxListeners(0);
|
|
43
|
+
}
|
|
44
|
+
/** Register a listener for the given event. */
|
|
45
|
+
on(event, listener) {
|
|
46
|
+
this.inner.on(event, listener);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
/** Remove a previously registered listener. */
|
|
50
|
+
off(event, listener) {
|
|
51
|
+
this.inner.off(event, listener);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
/** Number of listeners attached to `event`. Useful in tests. */
|
|
55
|
+
listenerCount(event) {
|
|
56
|
+
return this.inner.listenerCount(event);
|
|
57
|
+
}
|
|
58
|
+
/** Drop all listeners. Call from `RunOrchestrator` teardown to avoid leaks. */
|
|
59
|
+
removeAllListeners() {
|
|
60
|
+
this.inner.removeAllListeners();
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Emit an event. Listeners are invoked through `Promise.allSettled` so a
|
|
65
|
+
* single misbehaving subscriber cannot crash the pipeline (AC-5).
|
|
66
|
+
*
|
|
67
|
+
* Returns `Promise<void>` so critical-path callers may `await`. Fire-and-
|
|
68
|
+
* forget callers (`progress`, etc.) ignore the returned promise — the
|
|
69
|
+
* `Promise.allSettled` swallowing handles unhandled rejection warnings.
|
|
70
|
+
*
|
|
71
|
+
* The `timestamp` field on the payload is populated automatically when
|
|
72
|
+
* absent so call sites stay terse.
|
|
73
|
+
*/
|
|
74
|
+
emit(event, payload) {
|
|
75
|
+
const finalPayload = {
|
|
76
|
+
...payload,
|
|
77
|
+
timestamp: payload.timestamp ?? this.clock().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
const listeners = this.inner.listeners(event);
|
|
80
|
+
if (listeners.length === 0) {
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
}
|
|
83
|
+
// Wrap each listener in `Promise.resolve().then(() => listener(payload))`
|
|
84
|
+
// so synchronous throws are reified into rejections before
|
|
85
|
+
// `Promise.allSettled` runs. Without the resolve-then trampoline, a
|
|
86
|
+
// synchronous throw inside a non-async listener would propagate
|
|
87
|
+
// immediately and bypass `allSettled`.
|
|
88
|
+
const settled = Promise.allSettled(listeners.map((listener) => Promise.resolve().then(() => listener(finalPayload))));
|
|
89
|
+
return settled.then((results) => {
|
|
90
|
+
for (const r of results) {
|
|
91
|
+
if (r.status === "rejected") {
|
|
92
|
+
try {
|
|
93
|
+
this.onListenerError(event, r.reason);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* swallow — error handler must not propagate */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -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 {};
|