@hyperdrive.bot/bmad-workflow 1.0.25 → 1.0.27

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 +24 -15
  33. package/dist/services/agents/agent-runner-factory.js +95 -15
  34. package/dist/services/agents/channel-agent-runner.d.ts +76 -0
  35. package/dist/services/agents/channel-agent-runner.js +256 -0
  36. package/dist/services/agents/channel-session-manager.d.ts +126 -0
  37. package/dist/services/agents/channel-session-manager.js +260 -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
@@ -0,0 +1,260 @@
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 { randomUUID } from 'node:crypto';
11
+ import { spawn as nodeSpawn } from 'node:child_process';
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ let _discoveryModule = null;
14
+ function loadDiscovery() {
15
+ if (!_discoveryModule) {
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
17
+ _discoveryModule = require('@hyperdrive.bot/claude-channels/dist/services/discovery.js');
18
+ }
19
+ return _discoveryModule;
20
+ }
21
+ const defaultListAgents = () => loadDiscovery().listAgents();
22
+ const defaultDeregister = (sessionId) => loadDiscovery().deregisterAgent(sessionId);
23
+ /**
24
+ * Error thrown when agent session start times out waiting for discovery registration
25
+ */
26
+ export class SessionStartTimeoutError extends Error {
27
+ agentType;
28
+ timeoutMs;
29
+ constructor(agentType, timeoutMs) {
30
+ super(`Agent session '${agentType}' failed to register within ${timeoutMs}ms`);
31
+ this.name = 'SessionStartTimeoutError';
32
+ this.agentType = agentType;
33
+ this.timeoutMs = timeoutMs;
34
+ }
35
+ }
36
+ const DEFAULT_START_TIMEOUT = 30_000;
37
+ const DEFAULT_HEALTH_TIMEOUT = 2_000;
38
+ const POLL_INTERVAL = 500;
39
+ /**
40
+ * Manages Channel-connected Claude Code agent session lifecycles.
41
+ *
42
+ * Handles spawning, discovery polling, health checks, graceful shutdown,
43
+ * and SIGINT cleanup for multiple concurrent agent sessions.
44
+ */
45
+ export class ChannelSessionManager {
46
+ static sigintRegistered = false;
47
+ sessions = new Map();
48
+ logger;
49
+ listAgentsFn;
50
+ deregisterAgentFn;
51
+ spawnFn;
52
+ constructor(logger, deps = {}) {
53
+ this.logger = logger;
54
+ this.listAgentsFn = deps.listAgents ?? defaultListAgents;
55
+ this.deregisterAgentFn = deps.deregisterAgent ?? defaultDeregister;
56
+ this.spawnFn = deps.spawn ?? nodeSpawn;
57
+ this.registerSigintHandler();
58
+ }
59
+ /**
60
+ * Start a new agent session and wait for it to register via discovery.
61
+ *
62
+ * Spawns a Claude Code subprocess with --channel flag, then polls listAgents()
63
+ * at 500ms intervals until a new AgentRegistration appears. Returns a SessionHandle
64
+ * on success, throws SessionStartTimeoutError if discovery times out.
65
+ *
66
+ * @param agentType - Agent type identifier (used to match discovery registration)
67
+ * @param config - Session configuration
68
+ * @returns SessionHandle with session details
69
+ * @throws SessionStartTimeoutError if agent doesn't register within startTimeout
70
+ */
71
+ async startAgentSession(agentType, config = {}) {
72
+ const startTimeout = config.startTimeout ?? DEFAULT_START_TIMEOUT;
73
+ this.logger.info({ agentType, startTimeout }, 'Starting agent session');
74
+ // Snapshot existing agent session IDs before spawning
75
+ const existingIds = new Set(this.listAgentsFn().map((a) => a.sessionId));
76
+ // Spawn Claude Code with --channel flag
77
+ const command = config.command ?? 'claude';
78
+ const args = ['--channel'];
79
+ if (config.model) {
80
+ args.push('--model', config.model);
81
+ }
82
+ const env = { ...process.env };
83
+ delete env.CLAUDECODE;
84
+ const child = this.spawnFn(command, args, {
85
+ cwd: config.cwd,
86
+ env,
87
+ stdio: 'pipe',
88
+ });
89
+ const pid = child.pid;
90
+ this.logger.info({ agentType, pid }, 'Spawned Claude Code process');
91
+ // Register exit listener for unexpected process death
92
+ child.on('exit', (code, signal) => {
93
+ for (const [sessionId, handle] of this.sessions.entries()) {
94
+ if (handle.pid === pid) {
95
+ this.logger.warn({ agentType, code, pid, sessionId, signal }, 'Agent process exited unexpectedly');
96
+ this.sessions.delete(sessionId);
97
+ break;
98
+ }
99
+ }
100
+ });
101
+ // Poll for new discovery registration
102
+ const startTime = Date.now();
103
+ return new Promise((resolve, reject) => {
104
+ const poll = () => {
105
+ if (Date.now() - startTime > startTimeout) {
106
+ try {
107
+ child.kill('SIGTERM');
108
+ }
109
+ catch {
110
+ // Process may already be dead
111
+ }
112
+ reject(new SessionStartTimeoutError(agentType, startTimeout));
113
+ return;
114
+ }
115
+ try {
116
+ const agents = this.listAgentsFn();
117
+ const newAgent = agents.find((a) => !existingIds.has(a.sessionId));
118
+ if (newAgent) {
119
+ const handle = {
120
+ agentType,
121
+ pid,
122
+ port: newAgent.port ?? 0,
123
+ process: child,
124
+ sessionId: newAgent.sessionId,
125
+ };
126
+ this.sessions.set(newAgent.sessionId, handle);
127
+ this.logger.info({ agentType, pid, port: newAgent.port, sessionId: newAgent.sessionId }, 'Agent session registered via discovery');
128
+ resolve(handle);
129
+ return;
130
+ }
131
+ }
132
+ catch (error) {
133
+ this.logger.debug({ error: error.message }, 'Discovery poll error (retrying)');
134
+ }
135
+ setTimeout(poll, POLL_INTERVAL);
136
+ };
137
+ poll();
138
+ });
139
+ }
140
+ /**
141
+ * Send a ping envelope to the agent and check for pong response.
142
+ *
143
+ * @param handle - Session handle to health-check
144
+ * @param config - Optional config for health timeout override
145
+ * @returns true if agent responds with pong, false otherwise
146
+ */
147
+ async checkHealth(handle, config = {}) {
148
+ const healthTimeout = config.healthTimeout ?? DEFAULT_HEALTH_TIMEOUT;
149
+ const envelope = {
150
+ from: 'bmad-session-manager',
151
+ id: randomUUID(),
152
+ payload: {},
153
+ timestamp: new Date().toISOString(),
154
+ to: handle.sessionId,
155
+ type: 'ping',
156
+ };
157
+ try {
158
+ const controller = new AbortController();
159
+ const timeoutId = setTimeout(() => controller.abort(), healthTimeout);
160
+ const response = await fetch(`http://localhost:${handle.port}/webhook`, {
161
+ body: JSON.stringify(envelope),
162
+ headers: { 'Content-Type': 'application/json' },
163
+ method: 'POST',
164
+ signal: controller.signal,
165
+ });
166
+ clearTimeout(timeoutId);
167
+ if (response.status !== 200) {
168
+ this.logger.debug({ sessionId: handle.sessionId, status: response.status }, 'Health check: non-200 status');
169
+ return false;
170
+ }
171
+ const body = (await response.json());
172
+ return body.type === 'pong';
173
+ }
174
+ catch {
175
+ this.logger.debug({ sessionId: handle.sessionId }, 'Health check failed (timeout or connection error)');
176
+ return false;
177
+ }
178
+ }
179
+ /**
180
+ * Stop a single agent session: SIGTERM + deregister + remove from map.
181
+ *
182
+ * @param handle - Session handle to stop
183
+ */
184
+ async stopSession(handle) {
185
+ try {
186
+ this.logger.info({ pid: handle.pid, sessionId: handle.sessionId }, 'Stopping agent session');
187
+ try {
188
+ process.kill(handle.pid, 'SIGTERM');
189
+ }
190
+ catch (error) {
191
+ this.logger.warn({ error: error.message, pid: handle.pid }, 'Failed to send SIGTERM (process may already be dead)');
192
+ }
193
+ try {
194
+ this.deregisterAgentFn(handle.sessionId);
195
+ }
196
+ catch (error) {
197
+ this.logger.warn({ error: error.message, sessionId: handle.sessionId }, 'Failed to deregister agent (file may already be removed)');
198
+ }
199
+ this.sessions.delete(handle.sessionId);
200
+ }
201
+ catch (error) {
202
+ this.logger.error({ error: error.message, sessionId: handle.sessionId }, 'Error stopping session');
203
+ }
204
+ }
205
+ /**
206
+ * Stop all managed sessions. Best-effort: errors per session are logged, never thrown.
207
+ */
208
+ async stopAll() {
209
+ const handles = Array.from(this.sessions.values());
210
+ this.logger.info({ sessionCount: handles.length }, 'Stopping all agent sessions');
211
+ const results = await Promise.allSettled(handles.map((h) => this.stopSession(h)));
212
+ for (const result of results) {
213
+ if (result.status === 'rejected') {
214
+ this.logger.error({ error: result.reason.message }, 'Error during stopAll session cleanup');
215
+ }
216
+ }
217
+ this.sessions.clear();
218
+ }
219
+ /**
220
+ * Shutdown method for orchestrator integration.
221
+ * Call this in the orchestrator's finally block.
222
+ */
223
+ async shutdown() {
224
+ await this.stopAll();
225
+ }
226
+ /**
227
+ * Get the number of currently managed sessions.
228
+ */
229
+ get sessionCount() {
230
+ return this.sessions.size;
231
+ }
232
+ /**
233
+ * Get a session handle by sessionId.
234
+ */
235
+ getSession(sessionId) {
236
+ return this.sessions.get(sessionId);
237
+ }
238
+ /**
239
+ * Register SIGINT handler for graceful cleanup.
240
+ * Uses a static flag to prevent duplicate registration.
241
+ */
242
+ registerSigintHandler() {
243
+ if (ChannelSessionManager.sigintRegistered)
244
+ return;
245
+ ChannelSessionManager.sigintRegistered = true;
246
+ process.on('SIGINT', async () => {
247
+ this.logger.info('Received SIGINT — cleaning up channel sessions');
248
+ await this.stopAll();
249
+ process.exit(130);
250
+ });
251
+ }
252
+ /**
253
+ * Reset the static SIGINT registration flag.
254
+ * Only for testing — do not use in production.
255
+ * @internal
256
+ */
257
+ static resetSigintFlag() {
258
+ ChannelSessionManager.sigintRegistered = false;
259
+ }
260
+ }
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Encapsulates Claude CLI process spawning with comprehensive error handling,
5
5
  * logging, and timeout management for reliable AI agent execution.
6
+ *
7
+ * Uses spawn() with stream-json output format for real-time progress reporting.
6
8
  */
7
9
  import type pino from 'pino';
8
10
  import type { AgentOptions, AgentResult } from '../../models/index.js';
@@ -13,69 +15,26 @@ import type { AIProviderRunner } from './agent-runner.js';
13
15
  *
14
16
  * Spawns Claude CLI processes to execute AI agents with specified prompts.
15
17
  * Handles timeout, error collection, result formatting, and process cleanup.
16
- *
17
- * @example
18
- * ```typescript
19
- * const logger = createLogger({ namespace: 'agent-runner' })
20
- * const runner = new ClaudeAgentRunner(logger)
21
- * const result = await runner.runAgent({
22
- * prompt: '@.bmad-core/agents/architect.md Create epic for user auth',
23
- * agentType: 'architect',
24
- * timeout: 300000
25
- * })
26
- * ```
18
+ * Uses stream-json output format for real-time progress reporting.
27
19
  */
28
20
  export declare class ClaudeAgentRunner implements AIProviderRunner {
29
21
  readonly provider: AIProvider;
30
22
  private readonly config;
31
23
  private readonly logger;
32
- /**
33
- * Create a new ClaudeAgentRunner instance
34
- *
35
- * @param logger - Logger instance for structured logging
36
- */
37
24
  constructor(logger: pino.Logger);
38
- /**
39
- * Get the count of active processes
40
- * (Useful for testing and monitoring purposes)
41
- *
42
- * @returns Number of currently active child processes
43
- */
44
25
  getActiveProcessCount(): number;
45
- /**
46
- * Execute a Claude AI agent with the specified prompt and options
47
- *
48
- * This method spawns a Claude CLI process, captures output, handles errors,
49
- * and enforces timeouts. It never throws exceptions - all errors are returned
50
- * as typed AgentResult objects.
51
- *
52
- * @param prompt - The prompt to execute with the agent
53
- * @param options - Agent execution options (without prompt)
54
- * @returns AgentResult with success status, output, errors, and metadata
55
- *
56
- * @example
57
- * ```typescript
58
- * const result = await runner.runAgent('@.bmad-core/agents/architect.md Create epic for user auth', {
59
- * agentType: 'architect',
60
- * timeout: 300000
61
- * })
62
- *
63
- * if (result.success) {
64
- * console.log('Output:', result.output)
65
- * } else {
66
- * console.error('Error:', result.errors)
67
- * }
68
- * ```
69
- */
70
26
  runAgent(prompt: string, options: Omit<AgentOptions, 'prompt'>): Promise<AgentResult>;
71
27
  /**
72
- * Build detailed error context for debugging process failures
73
- * @private
28
+ * Spawn Claude process and stream output in real-time
74
29
  */
75
- private buildErrorContext;
30
+ private spawnAndStream;
76
31
  /**
77
32
  * Invoke onResponse callback and return result
78
33
  * @private
79
34
  */
80
35
  private returnWithCallback;
36
+ /**
37
+ * Shell-escape a single argument
38
+ */
39
+ private shellEscape;
81
40
  }