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
@@ -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
- /** Resume a previous session (driver-specific; ignored if unsupported) */
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 if provided
47
- ...(config.sessionId ? { resume: config.sessionId } : {}),
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 {};