@hyperdrive.bot/bmad-workflow 1.0.26 → 1.0.28

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 (72) hide show
  1. package/dist/commands/epics/create.d.ts +1 -0
  2. package/dist/commands/lock/acquire.d.ts +54 -0
  3. package/dist/commands/lock/acquire.js +193 -0
  4. package/dist/commands/lock/cleanup.d.ts +38 -0
  5. package/dist/commands/lock/cleanup.js +148 -0
  6. package/dist/commands/lock/list.d.ts +31 -0
  7. package/dist/commands/lock/list.js +123 -0
  8. package/dist/commands/lock/release.d.ts +42 -0
  9. package/dist/commands/lock/release.js +134 -0
  10. package/dist/commands/lock/status.d.ts +34 -0
  11. package/dist/commands/lock/status.js +109 -0
  12. package/dist/commands/stories/create.d.ts +1 -0
  13. package/dist/commands/stories/develop.d.ts +4 -0
  14. package/dist/commands/stories/develop.js +55 -5
  15. package/dist/commands/stories/qa.d.ts +1 -0
  16. package/dist/commands/stories/qa.js +31 -0
  17. package/dist/commands/stories/review.d.ts +1 -0
  18. package/dist/commands/workflow.d.ts +11 -0
  19. package/dist/commands/workflow.js +120 -4
  20. package/dist/models/agent-options.d.ts +33 -0
  21. package/dist/models/agent-result.d.ts +10 -1
  22. package/dist/models/dispatch.d.ts +16 -0
  23. package/dist/models/dispatch.js +8 -0
  24. package/dist/models/index.d.ts +3 -0
  25. package/dist/models/index.js +2 -0
  26. package/dist/models/lock.d.ts +80 -0
  27. package/dist/models/lock.js +69 -0
  28. package/dist/models/phase-result.d.ts +8 -0
  29. package/dist/models/provider.js +1 -1
  30. package/dist/models/workflow-callbacks.d.ts +37 -0
  31. package/dist/models/workflow-config.d.ts +50 -0
  32. package/dist/services/agents/agent-runner-factory.d.ts +14 -15
  33. package/dist/services/agents/agent-runner-factory.js +56 -15
  34. package/dist/services/agents/channel-agent-runner.d.ts +76 -0
  35. package/dist/services/agents/channel-agent-runner.js +246 -0
  36. package/dist/services/agents/channel-session-manager.d.ts +119 -0
  37. package/dist/services/agents/channel-session-manager.js +250 -0
  38. package/dist/services/agents/claude-agent-runner.d.ts +9 -50
  39. package/dist/services/agents/claude-agent-runner.js +221 -199
  40. package/dist/services/agents/gemini-agent-runner.js +3 -0
  41. package/dist/services/agents/index.d.ts +1 -0
  42. package/dist/services/agents/index.js +1 -0
  43. package/dist/services/agents/opencode-agent-runner.js +3 -0
  44. package/dist/services/file-system/file-manager.d.ts +11 -0
  45. package/dist/services/file-system/file-manager.js +26 -0
  46. package/dist/services/git/git-ops.d.ts +58 -0
  47. package/dist/services/git/git-ops.js +73 -0
  48. package/dist/services/git/index.d.ts +3 -0
  49. package/dist/services/git/index.js +2 -0
  50. package/dist/services/git/push-conflict-handler.d.ts +32 -0
  51. package/dist/services/git/push-conflict-handler.js +84 -0
  52. package/dist/services/lock/git-backed-lock-service.d.ts +76 -0
  53. package/dist/services/lock/git-backed-lock-service.js +173 -0
  54. package/dist/services/lock/lock-cleanup.d.ts +49 -0
  55. package/dist/services/lock/lock-cleanup.js +85 -0
  56. package/dist/services/lock/lock-service.d.ts +143 -0
  57. package/dist/services/lock/lock-service.js +290 -0
  58. package/dist/services/orchestration/locked-story-dispatcher.d.ts +40 -0
  59. package/dist/services/orchestration/locked-story-dispatcher.js +84 -0
  60. package/dist/services/orchestration/workflow-orchestrator.d.ts +31 -0
  61. package/dist/services/orchestration/workflow-orchestrator.js +181 -31
  62. package/dist/services/review/ai-review-scanner.js +1 -0
  63. package/dist/services/review/review-phase-executor.js +3 -0
  64. package/dist/services/review/self-heal-loop.js +1 -0
  65. package/dist/services/review/types.d.ts +2 -0
  66. package/dist/utils/errors.d.ts +17 -1
  67. package/dist/utils/errors.js +18 -0
  68. package/dist/utils/session-naming.d.ts +23 -0
  69. package/dist/utils/session-naming.js +30 -0
  70. package/dist/utils/shared-flags.d.ts +1 -0
  71. package/dist/utils/shared-flags.js +5 -0
  72. package/package.json +3 -2
@@ -2,30 +2,29 @@
2
2
  * Agent Runner Factory
3
3
  *
4
4
  * Creates the appropriate AIProviderRunner based on provider configuration.
5
+ * Includes both the legacy `createAgentRunner()` function and the new
6
+ * `AgentRunnerFactory` class with Channel transport discovery.
5
7
  */
6
8
  import type pino from 'pino';
9
+ import type { WorkflowCallbacks } from '../../models/workflow-callbacks.js';
10
+ import type { WorkflowConfig } from '../../models/workflow-config.js';
7
11
  import type { AIProvider } from '../../models/provider.js';
8
12
  import type { AIProviderRunner } from './agent-runner.js';
9
13
  /**
10
14
  * Create an AI provider runner for the specified provider
11
- *
12
- * @param provider - The AI provider to create a runner for
13
- * @param logger - Logger instance for structured logging
14
- * @returns An AIProviderRunner instance for the specified provider
15
- * @throws Error if the provider is not supported
16
- *
17
- * @example
18
- * ```typescript
19
- * const logger = createLogger({ namespace: 'agent-runner' })
20
- * const runner = createAgentRunner('claude', logger)
21
- * const result = await runner.runAgent('Hello', { agentType: 'dev' })
22
- * ```
23
15
  */
24
16
  export declare function createAgentRunner(provider: AIProvider, logger: pino.Logger): AIProviderRunner;
25
17
  /**
26
18
  * Check if a provider is supported
27
- *
28
- * @param provider - The provider to check
29
- * @returns true if the provider is supported
30
19
  */
31
20
  export declare function isProviderSupported(provider: string): provider is AIProvider;
21
+ /**
22
+ * AgentRunnerFactory — discovery-based runner selection
23
+ */
24
+ export declare class AgentRunnerFactory {
25
+ private readonly callbacks?;
26
+ private readonly logger;
27
+ constructor(logger: pino.Logger, callbacks?: WorkflowCallbacks);
28
+ create(agentType: string, config: WorkflowConfig, phaseName?: string): AIProviderRunner;
29
+ private fireChannelFallback;
30
+ }
@@ -2,24 +2,16 @@
2
2
  * Agent Runner Factory
3
3
  *
4
4
  * Creates the appropriate AIProviderRunner based on provider configuration.
5
+ * Includes both the legacy `createAgentRunner()` function and the new
6
+ * `AgentRunnerFactory` class with Channel transport discovery.
5
7
  */
8
+ import { listAgents } from '@hyperdrive.bot/claude-channels/dist/services/discovery.js';
9
+ import { ChannelAgentRunner } from './channel-agent-runner.js';
6
10
  import { ClaudeAgentRunner } from './claude-agent-runner.js';
7
11
  import { GeminiAgentRunner } from './gemini-agent-runner.js';
8
12
  import { OpenCodeAgentRunner } from './opencode-agent-runner.js';
9
13
  /**
10
14
  * Create an AI provider runner for the specified provider
11
- *
12
- * @param provider - The AI provider to create a runner for
13
- * @param logger - Logger instance for structured logging
14
- * @returns An AIProviderRunner instance for the specified provider
15
- * @throws Error if the provider is not supported
16
- *
17
- * @example
18
- * ```typescript
19
- * const logger = createLogger({ namespace: 'agent-runner' })
20
- * const runner = createAgentRunner('claude', logger)
21
- * const result = await runner.runAgent('Hello', { agentType: 'dev' })
22
- * ```
23
15
  */
24
16
  export function createAgentRunner(provider, logger) {
25
17
  switch (provider) {
@@ -39,10 +31,59 @@ export function createAgentRunner(provider, logger) {
39
31
  }
40
32
  /**
41
33
  * Check if a provider is supported
42
- *
43
- * @param provider - The provider to check
44
- * @returns true if the provider is supported
45
34
  */
46
35
  export function isProviderSupported(provider) {
47
36
  return provider === 'claude' || provider === 'gemini' || provider === 'opencode';
48
37
  }
38
+ /**
39
+ * AgentRunnerFactory — discovery-based runner selection
40
+ */
41
+ export class AgentRunnerFactory {
42
+ callbacks;
43
+ logger;
44
+ constructor(logger, callbacks) {
45
+ this.logger = logger;
46
+ this.callbacks = callbacks;
47
+ }
48
+ create(agentType, config, phaseName) {
49
+ if (!config.useChannels) {
50
+ return createAgentRunner(config.provider ?? 'claude', this.logger);
51
+ }
52
+ let agents;
53
+ try {
54
+ agents = listAgents();
55
+ }
56
+ catch (error) {
57
+ this.logger.warn(`[channel] Discovery failed: ${error.message} — falling back to subprocess`);
58
+ this.fireChannelFallback(agentType, 'not_discovered', phaseName);
59
+ return createAgentRunner(config.provider ?? 'claude', this.logger);
60
+ }
61
+ const matches = agents.filter((reg) => reg.name.toLowerCase() === agentType.toLowerCase());
62
+ if (matches.length === 0) {
63
+ this.logger.info(`[subprocess] Falling back to claude -p for ${agentType} — no Channel agent discovered`);
64
+ this.fireChannelFallback(agentType, 'not_discovered', phaseName);
65
+ return createAgentRunner(config.provider ?? 'claude', this.logger);
66
+ }
67
+ const matched = matches.length === 1
68
+ ? matches[0]
69
+ : matches.sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
70
+ this.logger.info({ agentType, sessionId: matched.sessionId, webhookUrl: matched.webhookUrl }, `[channel] Discovered Channel agent for ${agentType}`);
71
+ return new ChannelAgentRunner(this.logger, matched.webhookUrl, matched.sessionId, this.callbacks);
72
+ }
73
+ fireChannelFallback(agentType, reason, phaseName) {
74
+ const callback = this.callbacks?.onChannelFallback;
75
+ if (!callback)
76
+ return;
77
+ const context = {
78
+ agentType,
79
+ phaseName: phaseName ?? 'unknown',
80
+ reason,
81
+ };
82
+ try {
83
+ callback(context);
84
+ }
85
+ catch (error) {
86
+ this.logger.warn({ callbackName: 'onChannelFallback', error: error.message }, 'Callback error (ignored to prevent workflow interruption)');
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ChannelAgentRunner Service
3
+ *
4
+ * Sends task envelopes to discovered Channel agent endpoints via HTTP POST
5
+ * and returns results as AgentResult. Used by the factory to delegate work
6
+ * to persistent, context-preserving Claude Code sessions.
7
+ */
8
+ import type pino from 'pino';
9
+ import type { AgentOptions, AgentResult, WorkflowCallbacks } from '../../models/index.js';
10
+ import type { AIProvider } from '../../models/provider.js';
11
+ import type { AIProviderRunner } from './agent-runner.js';
12
+ /**
13
+ * Error thrown when the channel agent endpoint is unreachable (ECONNREFUSED).
14
+ * The factory uses this signal to trigger subprocess fallback.
15
+ */
16
+ export declare class ChannelUnavailableError extends Error {
17
+ readonly name = "ChannelUnavailableError";
18
+ readonly webhookUrl: string;
19
+ constructor(webhookUrl: string, cause?: Error);
20
+ }
21
+ /**
22
+ * ChannelAgentRunner — sends task envelopes to channel agent endpoints via HTTP POST.
23
+ *
24
+ * Never throws on agent errors (returns AgentResult with success: false).
25
+ * The ONLY exception is ECONNREFUSED → ChannelUnavailableError, because
26
+ * the factory needs this signal to trigger subprocess fallback.
27
+ */
28
+ export declare class ChannelAgentRunner implements AIProviderRunner {
29
+ readonly provider: AIProvider;
30
+ private activeRequests;
31
+ private connected;
32
+ private readonly callbacks?;
33
+ private readonly logger;
34
+ private readonly port;
35
+ private readonly sessionId;
36
+ private readonly webhookUrl;
37
+ constructor(logger: pino.Logger, webhookUrl: string, sessionId: string, callbacks?: WorkflowCallbacks);
38
+ getActiveProcessCount(): number;
39
+ private fireChannelConnect;
40
+ /**
41
+ * Send a task to the Channel agent and return the result.
42
+ *
43
+ * **Task envelope structure:**
44
+ * The prompt is wrapped in a Channel envelope via `EnvelopeFactory.createTask()`:
45
+ * - `from`: `"bmad-orchestrator"` — identifies the caller
46
+ * - `to`: the target session ID discovered during factory creation
47
+ * - `type`: `"task"` — tells the Channel agent this is a work request
48
+ * - `payload`: `{ prompt, context? }` — the actual prompt plus optional metadata
49
+ * (systemPrompt, model) passed via the context field
50
+ * - `id` / `timestamp`: auto-generated for tracing and deduplication
51
+ *
52
+ * **Response parsing:**
53
+ * - `type: "result"` with `payload.output` (string) and `payload.exitCode` (number)
54
+ * → success path. `exitCode === 0` means the agent completed successfully.
55
+ * - `type: "error"` → the Channel agent encountered an internal error. Returned as
56
+ * `AgentResult` with `success: false` (never throws).
57
+ * - Any other `type` → treated as unexpected and returned as failure.
58
+ *
59
+ * **Timeout handling:**
60
+ * An `AbortController` is created per request with the configured timeout (default
61
+ * 300s). If the Channel agent doesn't respond within the timeout window, the fetch
62
+ * is aborted and an `AgentResult` with `success: false` is returned. The timer is
63
+ * always cleared in `finally` to prevent leaks.
64
+ *
65
+ * **Connection errors:**
66
+ * ECONNREFUSED is the only error that throws (`ChannelUnavailableError`) instead of
67
+ * returning a failed `AgentResult`. This allows the factory to catch it and trigger
68
+ * subprocess fallback.
69
+ *
70
+ * @param prompt - The task prompt to send to the Channel agent
71
+ * @param options - Agent options (agentType, timeout, model, systemPrompt, phaseName)
72
+ * @returns AgentResult with output, exitCode, duration, and transport: "channel"
73
+ * @throws ChannelUnavailableError when the endpoint is unreachable (ECONNREFUSED)
74
+ */
75
+ runAgent(prompt: string, options: Omit<AgentOptions, 'prompt'>): Promise<AgentResult>;
76
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * ChannelAgentRunner Service
3
+ *
4
+ * Sends task envelopes to discovered Channel agent endpoints via HTTP POST
5
+ * and returns results as AgentResult. Used by the factory to delegate work
6
+ * to persistent, context-preserving Claude Code sessions.
7
+ */
8
+ import { EnvelopeFactory } from '@hyperdrive.bot/claude-channels/dist/envelope/factory.js';
9
+ /**
10
+ * Error thrown when the channel agent endpoint is unreachable (ECONNREFUSED).
11
+ * The factory uses this signal to trigger subprocess fallback.
12
+ */
13
+ export class ChannelUnavailableError extends Error {
14
+ name = 'ChannelUnavailableError';
15
+ webhookUrl;
16
+ constructor(webhookUrl, cause) {
17
+ super(`Channel unavailable at ${webhookUrl}`);
18
+ this.webhookUrl = webhookUrl;
19
+ if (cause) {
20
+ this.cause = cause;
21
+ }
22
+ }
23
+ }
24
+ /**
25
+ * ChannelAgentRunner — sends task envelopes to channel agent endpoints via HTTP POST.
26
+ *
27
+ * Never throws on agent errors (returns AgentResult with success: false).
28
+ * The ONLY exception is ECONNREFUSED → ChannelUnavailableError, because
29
+ * the factory needs this signal to trigger subprocess fallback.
30
+ */
31
+ export class ChannelAgentRunner {
32
+ provider = 'claude';
33
+ activeRequests = 0;
34
+ connected = false;
35
+ callbacks;
36
+ logger;
37
+ port;
38
+ sessionId;
39
+ webhookUrl;
40
+ constructor(logger, webhookUrl, sessionId, callbacks) {
41
+ this.logger = logger;
42
+ this.webhookUrl = webhookUrl;
43
+ this.sessionId = sessionId;
44
+ this.callbacks = callbacks;
45
+ // Extract port from webhook URL
46
+ try {
47
+ this.port = new URL(webhookUrl).port ? Number.parseInt(new URL(webhookUrl).port, 10) : 0;
48
+ }
49
+ catch {
50
+ this.port = 0;
51
+ }
52
+ }
53
+ getActiveProcessCount() {
54
+ return this.activeRequests;
55
+ }
56
+ fireChannelConnect(agentType, phaseName) {
57
+ if (this.connected)
58
+ return;
59
+ this.connected = true;
60
+ const callback = this.callbacks?.onChannelConnect;
61
+ if (!callback)
62
+ return;
63
+ const context = {
64
+ agentType,
65
+ phaseName,
66
+ port: this.port,
67
+ sessionId: this.sessionId,
68
+ };
69
+ try {
70
+ callback(context);
71
+ }
72
+ catch (error) {
73
+ this.logger.warn({ callbackName: 'onChannelConnect', error: error.message }, 'Callback error (ignored to prevent workflow interruption)');
74
+ }
75
+ }
76
+ /**
77
+ * Send a task to the Channel agent and return the result.
78
+ *
79
+ * **Task envelope structure:**
80
+ * The prompt is wrapped in a Channel envelope via `EnvelopeFactory.createTask()`:
81
+ * - `from`: `"bmad-orchestrator"` — identifies the caller
82
+ * - `to`: the target session ID discovered during factory creation
83
+ * - `type`: `"task"` — tells the Channel agent this is a work request
84
+ * - `payload`: `{ prompt, context? }` — the actual prompt plus optional metadata
85
+ * (systemPrompt, model) passed via the context field
86
+ * - `id` / `timestamp`: auto-generated for tracing and deduplication
87
+ *
88
+ * **Response parsing:**
89
+ * - `type: "result"` with `payload.output` (string) and `payload.exitCode` (number)
90
+ * → success path. `exitCode === 0` means the agent completed successfully.
91
+ * - `type: "error"` → the Channel agent encountered an internal error. Returned as
92
+ * `AgentResult` with `success: false` (never throws).
93
+ * - Any other `type` → treated as unexpected and returned as failure.
94
+ *
95
+ * **Timeout handling:**
96
+ * An `AbortController` is created per request with the configured timeout (default
97
+ * 300s). If the Channel agent doesn't respond within the timeout window, the fetch
98
+ * is aborted and an `AgentResult` with `success: false` is returned. The timer is
99
+ * always cleared in `finally` to prevent leaks.
100
+ *
101
+ * **Connection errors:**
102
+ * ECONNREFUSED is the only error that throws (`ChannelUnavailableError`) instead of
103
+ * returning a failed `AgentResult`. This allows the factory to catch it and trigger
104
+ * subprocess fallback.
105
+ *
106
+ * @param prompt - The task prompt to send to the Channel agent
107
+ * @param options - Agent options (agentType, timeout, model, systemPrompt, phaseName)
108
+ * @returns AgentResult with output, exitCode, duration, and transport: "channel"
109
+ * @throws ChannelUnavailableError when the endpoint is unreachable (ECONNREFUSED)
110
+ */
111
+ async runAgent(prompt, options) {
112
+ const startTime = Date.now();
113
+ const timeout = options.timeout ?? 300_000;
114
+ // Build task envelope
115
+ const context = {};
116
+ if (options.systemPrompt)
117
+ context.systemPrompt = options.systemPrompt;
118
+ if (options.model)
119
+ context.model = options.model;
120
+ const envelope = EnvelopeFactory.createTask('bmad-orchestrator', this.sessionId, prompt, Object.keys(context).length > 0 ? context : undefined);
121
+ this.logger.debug({ agentType: options.agentType, envelopeId: envelope.id, sessionId: this.sessionId }, 'Sending task envelope to channel agent');
122
+ const controller = new AbortController();
123
+ const timer = setTimeout(() => controller.abort(), timeout);
124
+ this.activeRequests++;
125
+ try {
126
+ const response = await fetch(this.webhookUrl, {
127
+ body: JSON.stringify(envelope),
128
+ headers: { 'Content-Type': 'application/json' },
129
+ method: 'POST',
130
+ signal: controller.signal,
131
+ });
132
+ // Parse response JSON
133
+ let body;
134
+ try {
135
+ body = await response.json();
136
+ }
137
+ catch {
138
+ this.logger.warn({ envelopeId: envelope.id, status: response.status }, 'Malformed channel response: invalid JSON');
139
+ return {
140
+ agentType: options.agentType,
141
+ duration: Date.now() - startTime,
142
+ errors: 'Malformed channel response: invalid JSON',
143
+ exitCode: 1,
144
+ output: '',
145
+ success: false,
146
+ transport: 'channel',
147
+ };
148
+ }
149
+ // Validate envelope structure
150
+ const parsed = body;
151
+ if (!parsed.type || !parsed.payload) {
152
+ this.logger.warn({ envelopeId: envelope.id, responseType: parsed.type }, 'Malformed channel response: missing type or payload');
153
+ return {
154
+ agentType: options.agentType,
155
+ duration: Date.now() - startTime,
156
+ errors: `Malformed channel response: missing ${!parsed.type ? 'type' : 'payload'} field`,
157
+ exitCode: 1,
158
+ output: '',
159
+ success: false,
160
+ transport: 'channel',
161
+ };
162
+ }
163
+ // Handle result envelope
164
+ if (parsed.type === 'result') {
165
+ const payload = parsed.payload;
166
+ if (typeof payload.output === 'string' && typeof payload.exitCode === 'number') {
167
+ const duration = Date.now() - startTime;
168
+ const exitCode = payload.exitCode;
169
+ // Fire onChannelConnect on first successful exchange
170
+ this.fireChannelConnect(options.agentType, options.phaseName ?? 'unknown');
171
+ this.logger.debug({ agentType: options.agentType, duration, envelopeId: envelope.id, exitCode }, 'Channel agent completed');
172
+ return {
173
+ agentType: options.agentType,
174
+ duration,
175
+ errors: '',
176
+ exitCode,
177
+ output: payload.output,
178
+ success: exitCode === 0,
179
+ transport: 'channel',
180
+ };
181
+ }
182
+ // Result type but malformed payload
183
+ this.logger.warn({ envelopeId: envelope.id }, 'Malformed channel response: result payload missing output or exitCode');
184
+ return {
185
+ agentType: options.agentType,
186
+ duration: Date.now() - startTime,
187
+ errors: 'Malformed channel response: result payload missing output or exitCode',
188
+ exitCode: 1,
189
+ output: '',
190
+ success: false,
191
+ transport: 'channel',
192
+ };
193
+ }
194
+ // Non-result envelope type (error, status, etc.)
195
+ const errorDescription = parsed.type === 'error'
196
+ ? `Channel returned error: ${JSON.stringify(parsed.payload)}`
197
+ : `Unexpected envelope type: ${parsed.type}`;
198
+ this.logger.warn({ envelopeId: envelope.id, responseType: parsed.type }, errorDescription);
199
+ return {
200
+ agentType: options.agentType,
201
+ duration: Date.now() - startTime,
202
+ errors: errorDescription,
203
+ exitCode: 1,
204
+ output: '',
205
+ success: false,
206
+ transport: 'channel',
207
+ };
208
+ }
209
+ catch (error) {
210
+ const err = error;
211
+ // Connection refused — throw ChannelUnavailableError for factory fallback
212
+ if (err.cause?.code === 'ECONNREFUSED' || (err instanceof TypeError && err.cause?.code === 'ECONNREFUSED')) {
213
+ this.logger.error({ webhookUrl: this.webhookUrl }, 'Channel agent unreachable (ECONNREFUSED)');
214
+ throw new ChannelUnavailableError(this.webhookUrl, err);
215
+ }
216
+ // Timeout via AbortController
217
+ if (err.name === 'AbortError') {
218
+ this.logger.warn({ envelopeId: envelope.id, timeout }, `Channel timeout after ${timeout}ms`);
219
+ return {
220
+ agentType: options.agentType,
221
+ duration: Date.now() - startTime,
222
+ errors: `Channel timeout after ${timeout}ms`,
223
+ exitCode: 1,
224
+ output: '',
225
+ success: false,
226
+ transport: 'channel',
227
+ };
228
+ }
229
+ // Unexpected error
230
+ this.logger.error({ envelopeId: envelope.id, error: err.message }, 'Unexpected channel error');
231
+ return {
232
+ agentType: options.agentType,
233
+ duration: Date.now() - startTime,
234
+ errors: `Unexpected channel error: ${err.message}`,
235
+ exitCode: 1,
236
+ output: '',
237
+ success: false,
238
+ transport: 'channel',
239
+ };
240
+ }
241
+ finally {
242
+ this.activeRequests--;
243
+ clearTimeout(timer);
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * ChannelSessionManager — Agent Lifecycle Manager
3
+ *
4
+ * Manages the lifecycle of Channel-connected Claude Code agent sessions:
5
+ * start (with discovery polling), health check (ping/pong), stop, and cleanup.
6
+ *
7
+ * Used by the orchestrator to pre-provision agent sessions before workflow runs
8
+ * and guarantee clean shutdown on errors or SIGINT.
9
+ */
10
+ import { type ChildProcess, spawn as nodeSpawn } from 'node:child_process';
11
+ import type pino from 'pino';
12
+ import type { AgentRegistration } from '@hyperdrive.bot/claude-channels/dist/types/discovery.js';
13
+ /**
14
+ * Handle to a managed agent session
15
+ */
16
+ export interface SessionHandle {
17
+ sessionId: string;
18
+ port: number;
19
+ agentType: string;
20
+ pid: number;
21
+ process: ChildProcess;
22
+ }
23
+ /**
24
+ * Configuration for starting an agent session
25
+ */
26
+ export interface SessionConfig {
27
+ command?: string;
28
+ model?: string;
29
+ systemPrompt?: string;
30
+ cwd?: string;
31
+ startTimeout?: number;
32
+ healthTimeout?: number;
33
+ }
34
+ /**
35
+ * Injectable dependencies for testing
36
+ */
37
+ export interface SessionManagerDeps {
38
+ listAgents?: () => AgentRegistration[];
39
+ deregisterAgent?: (sessionId: string) => void;
40
+ spawn?: typeof nodeSpawn;
41
+ }
42
+ /**
43
+ * Error thrown when agent session start times out waiting for discovery registration
44
+ */
45
+ export declare class SessionStartTimeoutError extends Error {
46
+ readonly agentType: string;
47
+ readonly timeoutMs: number;
48
+ constructor(agentType: string, timeoutMs: number);
49
+ }
50
+ /**
51
+ * Manages Channel-connected Claude Code agent session lifecycles.
52
+ *
53
+ * Handles spawning, discovery polling, health checks, graceful shutdown,
54
+ * and SIGINT cleanup for multiple concurrent agent sessions.
55
+ */
56
+ export declare class ChannelSessionManager {
57
+ private static sigintRegistered;
58
+ private readonly sessions;
59
+ private readonly logger;
60
+ private readonly listAgentsFn;
61
+ private readonly deregisterAgentFn;
62
+ private readonly spawnFn;
63
+ constructor(logger: pino.Logger, deps?: SessionManagerDeps);
64
+ /**
65
+ * Start a new agent session and wait for it to register via discovery.
66
+ *
67
+ * Spawns a Claude Code subprocess with --channel flag, then polls listAgents()
68
+ * at 500ms intervals until a new AgentRegistration appears. Returns a SessionHandle
69
+ * on success, throws SessionStartTimeoutError if discovery times out.
70
+ *
71
+ * @param agentType - Agent type identifier (used to match discovery registration)
72
+ * @param config - Session configuration
73
+ * @returns SessionHandle with session details
74
+ * @throws SessionStartTimeoutError if agent doesn't register within startTimeout
75
+ */
76
+ startAgentSession(agentType: string, config?: SessionConfig): Promise<SessionHandle>;
77
+ /**
78
+ * Send a ping envelope to the agent and check for pong response.
79
+ *
80
+ * @param handle - Session handle to health-check
81
+ * @param config - Optional config for health timeout override
82
+ * @returns true if agent responds with pong, false otherwise
83
+ */
84
+ checkHealth(handle: SessionHandle, config?: SessionConfig): Promise<boolean>;
85
+ /**
86
+ * Stop a single agent session: SIGTERM + deregister + remove from map.
87
+ *
88
+ * @param handle - Session handle to stop
89
+ */
90
+ stopSession(handle: SessionHandle): Promise<void>;
91
+ /**
92
+ * Stop all managed sessions. Best-effort: errors per session are logged, never thrown.
93
+ */
94
+ stopAll(): Promise<void>;
95
+ /**
96
+ * Shutdown method for orchestrator integration.
97
+ * Call this in the orchestrator's finally block.
98
+ */
99
+ shutdown(): Promise<void>;
100
+ /**
101
+ * Get the number of currently managed sessions.
102
+ */
103
+ get sessionCount(): number;
104
+ /**
105
+ * Get a session handle by sessionId.
106
+ */
107
+ getSession(sessionId: string): SessionHandle | undefined;
108
+ /**
109
+ * Register SIGINT handler for graceful cleanup.
110
+ * Uses a static flag to prevent duplicate registration.
111
+ */
112
+ private registerSigintHandler;
113
+ /**
114
+ * Reset the static SIGINT registration flag.
115
+ * Only for testing — do not use in production.
116
+ * @internal
117
+ */
118
+ static resetSigintFlag(): void;
119
+ }