edsger 0.27.11 → 0.28.1

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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * MCP client wrappers for chat operations.
3
+ * Used by the chat-worker subprocess to interact with chat channels and messages.
4
+ */
5
+ import type { ChatChannel, ChatChannelType, ChatMode, ChatMessage, ChatMessageType } from '../types/index.js';
6
+ export declare function getOrCreateChannel(channelType: ChatChannelType, channelRefId: string | null, chatMode?: ChatMode, name?: string, verbose?: boolean): Promise<{
7
+ channel: ChatChannel;
8
+ created: boolean;
9
+ }>;
10
+ export declare function listChannels(channelType?: ChatChannelType, channelRefId?: string, verbose?: boolean): Promise<ChatChannel[]>;
11
+ export declare function addChannelMember(channelId: string, userId: string, role?: 'owner' | 'admin' | 'member', verbose?: boolean): Promise<void>;
12
+ export declare function removeChannelMember(channelId: string, userId: string, verbose?: boolean): Promise<void>;
13
+ export declare function listChatMessages(channelId: string, options?: {
14
+ since?: string;
15
+ limit?: number;
16
+ offset?: number;
17
+ }, verbose?: boolean): Promise<ChatMessage[]>;
18
+ export declare function sendAiMessage(channelId: string, content: string, metadata?: Record<string, unknown>, options?: {
19
+ messageType?: ChatMessageType;
20
+ parentMessageId?: string;
21
+ }, verbose?: boolean): Promise<ChatMessage>;
22
+ export declare function sendSystemMessage(channelId: string, content: string, metadata?: Record<string, unknown>, verbose?: boolean): Promise<ChatMessage>;
23
+ export declare function getPendingMessages(channelId: string, verbose?: boolean): Promise<ChatMessage[]>;
24
+ /**
25
+ * Atomically claim pending messages for a channel.
26
+ * Uses FOR UPDATE SKIP LOCKED at the database level so multiple CLI workers
27
+ * never process the same message. Stale claims are automatically released.
28
+ */
29
+ export declare function claimPendingMessages(channelId: string, workerId: string, options?: {
30
+ limit?: number;
31
+ staleTimeoutSeconds?: number;
32
+ }, verbose?: boolean): Promise<ChatMessage[]>;
33
+ export declare function markMessageProcessed(messageId: string, verbose?: boolean): Promise<void>;
34
+ export declare function markChannelRead(channelId: string, lastReadMessageId?: string, verbose?: boolean): Promise<void>;
35
+ export declare function getUnreadCount(channelId: string, verbose?: boolean): Promise<number>;
36
+ /**
37
+ * Get or create the group chat channel for a feature.
38
+ * This is the most common operation — every feature has one group channel.
39
+ */
40
+ export declare function getFeatureChannel(featureId: string, verbose?: boolean): Promise<ChatChannel>;
41
+ /**
42
+ * Send a system message to a feature's group channel.
43
+ * Creates the channel if it doesn't exist.
44
+ */
45
+ export declare function sendFeatureSystemMessage(featureId: string, content: string, metadata?: Record<string, unknown>, verbose?: boolean): Promise<ChatMessage>;
46
+ /**
47
+ * Send an AI message to a feature's group channel.
48
+ * Creates the channel if it doesn't exist.
49
+ */
50
+ export declare function sendFeatureAiMessage(featureId: string, content: string, metadata?: Record<string, unknown>, options?: {
51
+ messageType?: ChatMessageType;
52
+ parentMessageId?: string;
53
+ }, verbose?: boolean): Promise<ChatMessage>;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * MCP client wrappers for chat operations.
3
+ * Used by the chat-worker subprocess to interact with chat channels and messages.
4
+ */
5
+ import { callMcpEndpoint } from './mcp-client.js';
6
+ import { logInfo, logError } from '../utils/logger.js';
7
+ // ============================================================
8
+ // Channels
9
+ // ============================================================
10
+ export async function getOrCreateChannel(channelType, channelRefId, chatMode = 'group', name, verbose) {
11
+ if (verbose) {
12
+ logInfo(`Getting/creating channel: ${channelType}/${channelRefId}/${chatMode}`);
13
+ }
14
+ const result = (await callMcpEndpoint('chat/channels/get_or_create', {
15
+ channel_type: channelType,
16
+ channel_ref_id: channelRefId,
17
+ chat_mode: chatMode,
18
+ name,
19
+ }));
20
+ return result;
21
+ }
22
+ export async function listChannels(channelType, channelRefId, verbose) {
23
+ if (verbose) {
24
+ logInfo(`Listing channels: type=${channelType || 'all'}`);
25
+ }
26
+ const result = (await callMcpEndpoint('chat/channels/list', {
27
+ channel_type: channelType,
28
+ channel_ref_id: channelRefId,
29
+ }));
30
+ return result.channels || [];
31
+ }
32
+ // ============================================================
33
+ // Members
34
+ // ============================================================
35
+ export async function addChannelMember(channelId, userId, role = 'member', verbose) {
36
+ if (verbose) {
37
+ logInfo(`Adding member ${userId} to channel ${channelId}`);
38
+ }
39
+ await callMcpEndpoint('chat/members/add', {
40
+ channel_id: channelId,
41
+ user_id: userId,
42
+ role,
43
+ });
44
+ }
45
+ export async function removeChannelMember(channelId, userId, verbose) {
46
+ if (verbose) {
47
+ logInfo(`Removing member ${userId} from channel ${channelId}`);
48
+ }
49
+ await callMcpEndpoint('chat/members/remove', {
50
+ channel_id: channelId,
51
+ user_id: userId,
52
+ });
53
+ }
54
+ // ============================================================
55
+ // Messages
56
+ // ============================================================
57
+ export async function listChatMessages(channelId, options = {}, verbose) {
58
+ if (verbose) {
59
+ logInfo(`Listing messages for channel ${channelId}`);
60
+ }
61
+ const result = (await callMcpEndpoint('chat/messages/list', {
62
+ channel_id: channelId,
63
+ ...options,
64
+ }));
65
+ return result.messages || [];
66
+ }
67
+ export async function sendAiMessage(channelId, content, metadata = {}, options = {}, verbose) {
68
+ if (verbose) {
69
+ logInfo(`Sending AI message to channel ${channelId}`);
70
+ }
71
+ const result = (await callMcpEndpoint('chat/messages/send_ai', {
72
+ channel_id: channelId,
73
+ content,
74
+ message_type: options.messageType || 'text',
75
+ metadata,
76
+ parent_message_id: options.parentMessageId,
77
+ }));
78
+ return result.message;
79
+ }
80
+ export async function sendSystemMessage(channelId, content, metadata = {}, verbose) {
81
+ if (verbose) {
82
+ logInfo(`Sending system message to channel ${channelId}`);
83
+ }
84
+ const result = (await callMcpEndpoint('chat/messages/send_system', {
85
+ channel_id: channelId,
86
+ content,
87
+ metadata,
88
+ }));
89
+ return result.message;
90
+ }
91
+ export async function getPendingMessages(channelId, verbose) {
92
+ const result = (await callMcpEndpoint('chat/messages/pending', {
93
+ channel_id: channelId,
94
+ }));
95
+ return result.messages || [];
96
+ }
97
+ /**
98
+ * Atomically claim pending messages for a channel.
99
+ * Uses FOR UPDATE SKIP LOCKED at the database level so multiple CLI workers
100
+ * never process the same message. Stale claims are automatically released.
101
+ */
102
+ export async function claimPendingMessages(channelId, workerId, options = {}, verbose) {
103
+ if (verbose) {
104
+ logInfo(`Claiming pending messages for channel ${channelId} (worker: ${workerId})`);
105
+ }
106
+ const result = (await callMcpEndpoint('chat/messages/claim', {
107
+ channel_id: channelId,
108
+ worker_id: workerId,
109
+ limit: options.limit || 10,
110
+ stale_timeout_seconds: options.staleTimeoutSeconds || 300,
111
+ }));
112
+ return result.messages || [];
113
+ }
114
+ export async function markMessageProcessed(messageId, verbose) {
115
+ if (verbose) {
116
+ logInfo(`Marking message ${messageId} as processed`);
117
+ }
118
+ await callMcpEndpoint('chat/messages/mark_processed', {
119
+ message_id: messageId,
120
+ });
121
+ }
122
+ // ============================================================
123
+ // Read Status
124
+ // ============================================================
125
+ export async function markChannelRead(channelId, lastReadMessageId, verbose) {
126
+ await callMcpEndpoint('chat/read_status/mark_read', {
127
+ channel_id: channelId,
128
+ last_read_message_id: lastReadMessageId,
129
+ });
130
+ }
131
+ export async function getUnreadCount(channelId, verbose) {
132
+ const result = (await callMcpEndpoint('chat/read_status/unread_count', {
133
+ channel_id: channelId,
134
+ }));
135
+ return result.unread_count;
136
+ }
137
+ // ============================================================
138
+ // Convenience: Feature Channel
139
+ // ============================================================
140
+ /**
141
+ * Get or create the group chat channel for a feature.
142
+ * This is the most common operation — every feature has one group channel.
143
+ */
144
+ export async function getFeatureChannel(featureId, verbose) {
145
+ const { channel } = await getOrCreateChannel('feature', featureId, 'group', undefined, verbose);
146
+ return channel;
147
+ }
148
+ /**
149
+ * Send a system message to a feature's group channel.
150
+ * Creates the channel if it doesn't exist.
151
+ */
152
+ export async function sendFeatureSystemMessage(featureId, content, metadata = {}, verbose) {
153
+ try {
154
+ const channel = await getFeatureChannel(featureId, verbose);
155
+ return await sendSystemMessage(channel.id, content, metadata, verbose);
156
+ }
157
+ catch (error) {
158
+ const msg = error instanceof Error ? error.message : String(error);
159
+ logError(`Failed to send system message for feature ${featureId}: ${msg}`);
160
+ throw error;
161
+ }
162
+ }
163
+ /**
164
+ * Send an AI message to a feature's group channel.
165
+ * Creates the channel if it doesn't exist.
166
+ */
167
+ export async function sendFeatureAiMessage(featureId, content, metadata = {}, options = {}, verbose) {
168
+ try {
169
+ const channel = await getFeatureChannel(featureId, verbose);
170
+ return await sendAiMessage(channel.id, content, metadata, options, verbose);
171
+ }
172
+ catch (error) {
173
+ const msg = error instanceof Error ? error.message : String(error);
174
+ logError(`Failed to send AI message for feature ${featureId}: ${msg}`);
175
+ throw error;
176
+ }
177
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Chat Worker — Child process entry point for processing chat messages.
3
+ *
4
+ * Spawned by AgentWorkflowProcessor as a parallel subprocess alongside feature workers.
5
+ * Runs continuously, polling for unprocessed human messages across all active feature channels.
6
+ *
7
+ * Communication with parent via IPC:
8
+ * - Parent sends: { type: 'init', config }
9
+ * - Parent sends: { type: 'event:phase_completed', featureId, phase, summary, phaseOutput }
10
+ * - Parent sends: { type: 'event:phase_failed', featureId, phase, error }
11
+ * - Parent sends: { type: 'event:feature_done', featureId }
12
+ * - Worker sends: { type: 'log', level, message }
13
+ * - Worker sends: { type: 'command:pause_feature', featureId }
14
+ * - Worker sends: { type: 'command:resume_feature', featureId }
15
+ */
16
+ export {};
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Chat Worker — Child process entry point for processing chat messages.
3
+ *
4
+ * Spawned by AgentWorkflowProcessor as a parallel subprocess alongside feature workers.
5
+ * Runs continuously, polling for unprocessed human messages across all active feature channels.
6
+ *
7
+ * Communication with parent via IPC:
8
+ * - Parent sends: { type: 'init', config }
9
+ * - Parent sends: { type: 'event:phase_completed', featureId, phase, summary, phaseOutput }
10
+ * - Parent sends: { type: 'event:phase_failed', featureId, phase, error }
11
+ * - Parent sends: { type: 'event:feature_done', featureId }
12
+ * - Worker sends: { type: 'log', level, message }
13
+ * - Worker sends: { type: 'command:pause_feature', featureId }
14
+ * - Worker sends: { type: 'command:resume_feature', featureId }
15
+ */
16
+ import { randomUUID } from 'node:crypto';
17
+ import { getFeatureChannel, claimPendingMessages, listChannels, sendSystemMessage, } from '../../api/chat.js';
18
+ import { processHumanMessages, processPhaseCompletion, } from '../../phases/chat-processor/index.js';
19
+ function sendMessage(msg) {
20
+ if (process.send) {
21
+ process.send(msg);
22
+ }
23
+ }
24
+ function log(level, message) {
25
+ sendMessage({ type: 'log', level, message });
26
+ }
27
+ // ============================================================
28
+ // State
29
+ // ============================================================
30
+ let config = null;
31
+ let isRunning = false;
32
+ let pollTimer = null;
33
+ // Unique worker ID for this process instance — used for atomic message claiming
34
+ const WORKER_ID = `chat-worker-${process.pid}-${randomUUID().slice(0, 8)}`;
35
+ // Track active feature channels (featureId -> channelId)
36
+ const activeChannels = new Map();
37
+ // Poll interval in ms
38
+ const POLL_INTERVAL = 5000;
39
+ // Refresh channel list every N polls (~30s at 5s intervals)
40
+ const CHANNEL_REFRESH_INTERVAL = 6;
41
+ let pollCount = 0;
42
+ // ============================================================
43
+ // Message Polling Loop
44
+ // ============================================================
45
+ async function pollForMessages() {
46
+ if (!config || !isRunning)
47
+ return;
48
+ // Periodically refresh the channel list to pick up newly created channels
49
+ pollCount++;
50
+ if (pollCount % CHANNEL_REFRESH_INTERVAL === 0) {
51
+ await refreshChannels();
52
+ }
53
+ for (const [featureId, channelId] of activeChannels) {
54
+ try {
55
+ // Atomically claim messages — other workers won't see these
56
+ const claimed = await claimPendingMessages(channelId, WORKER_ID);
57
+ if (claimed.length > 0) {
58
+ log('info', `Claimed ${claimed.length} message(s) for feature ${featureId} (worker: ${WORKER_ID})`);
59
+ // Batch all claimed messages into a single AI session
60
+ await processHumanMessages(claimed, featureId, config);
61
+ }
62
+ }
63
+ catch (error) {
64
+ const msg = error instanceof Error ? error.message : String(error);
65
+ log('error', `Error polling channel ${channelId}: ${msg}`);
66
+ }
67
+ }
68
+ }
69
+ function startPolling() {
70
+ if (pollTimer)
71
+ return;
72
+ isRunning = true;
73
+ log('info', 'Chat worker started polling');
74
+ // Initial poll
75
+ pollForMessages().catch((error) => {
76
+ log('error', `Initial poll error: ${error instanceof Error ? error.message : String(error)}`);
77
+ });
78
+ // Set up interval
79
+ pollTimer = setInterval(() => {
80
+ pollForMessages().catch((error) => {
81
+ log('error', `Poll error: ${error instanceof Error ? error.message : String(error)}`);
82
+ });
83
+ }, POLL_INTERVAL);
84
+ }
85
+ function stopPolling() {
86
+ isRunning = false;
87
+ if (pollTimer) {
88
+ clearInterval(pollTimer);
89
+ pollTimer = null;
90
+ }
91
+ log('info', 'Chat worker stopped polling');
92
+ }
93
+ // ============================================================
94
+ // Event Handlers
95
+ // ============================================================
96
+ async function handleInit(msg) {
97
+ config = msg.config;
98
+ log('info', `Chat worker initialized (id: ${WORKER_ID})`);
99
+ // Load existing channels before starting the poll loop
100
+ await refreshChannels();
101
+ startPolling();
102
+ }
103
+ async function handlePhaseCompleted(msg) {
104
+ if (!config)
105
+ return;
106
+ const { featureId, phase, summary, phaseOutput } = msg;
107
+ log('info', `Phase completed: ${phase} for feature ${featureId}`);
108
+ try {
109
+ // Ensure we have the channel registered
110
+ await ensureFeatureChannel(featureId);
111
+ // Process with AI for next-step suggestions
112
+ await processPhaseCompletion(featureId, phase, summary, phaseOutput, config);
113
+ }
114
+ catch (error) {
115
+ const errorMsg = error instanceof Error ? error.message : String(error);
116
+ log('error', `Failed to process phase completion: ${errorMsg}`);
117
+ }
118
+ }
119
+ async function handlePhaseFailed(msg) {
120
+ const { featureId, phase, error: errorStr } = msg;
121
+ log('warning', `Phase failed: ${phase} for feature ${featureId}`);
122
+ try {
123
+ await ensureFeatureChannel(featureId);
124
+ const channelId = activeChannels.get(featureId);
125
+ if (channelId) {
126
+ await sendSystemMessage(channelId, `Phase "${phase}" failed: ${errorStr}`, { phase, status: 'failed', error: errorStr });
127
+ }
128
+ }
129
+ catch (error) {
130
+ const errorMsg = error instanceof Error ? error.message : String(error);
131
+ log('error', `Failed to send phase failure message: ${errorMsg}`);
132
+ }
133
+ }
134
+ async function handleFeatureDone(msg) {
135
+ const { featureId } = msg;
136
+ log('info', `Feature done: ${featureId}`);
137
+ try {
138
+ const channelId = activeChannels.get(featureId);
139
+ if (channelId) {
140
+ await sendSystemMessage(channelId, 'All workflow phases completed.', { status: 'feature_done' });
141
+ }
142
+ }
143
+ catch (error) {
144
+ // Non-critical
145
+ }
146
+ // Keep the channel in activeChannels so we can still process messages
147
+ }
148
+ // ============================================================
149
+ // Channel Management
150
+ // ============================================================
151
+ /**
152
+ * Refresh the active channels list from the server.
153
+ * Called on init and periodically during polling to discover
154
+ * newly created channels (e.g., when a user opens a feature chat on the web).
155
+ */
156
+ async function refreshChannels() {
157
+ try {
158
+ const channels = await listChannels('feature');
159
+ let added = 0;
160
+ for (const channel of channels) {
161
+ if (channel.channel_ref_id && !activeChannels.has(channel.channel_ref_id)) {
162
+ activeChannels.set(channel.channel_ref_id, channel.id);
163
+ added++;
164
+ }
165
+ }
166
+ if (added > 0) {
167
+ log('info', `Discovered ${added} new channel(s) (total: ${activeChannels.size})`);
168
+ }
169
+ }
170
+ catch (error) {
171
+ const msg = error instanceof Error ? error.message : String(error);
172
+ log('error', `Failed to refresh channels: ${msg}`);
173
+ }
174
+ }
175
+ async function ensureFeatureChannel(featureId) {
176
+ if (activeChannels.has(featureId)) {
177
+ return activeChannels.get(featureId);
178
+ }
179
+ try {
180
+ const channel = await getFeatureChannel(featureId);
181
+ activeChannels.set(featureId, channel.id);
182
+ log('info', `Registered channel ${channel.id} for feature ${featureId}`);
183
+ return channel.id;
184
+ }
185
+ catch (error) {
186
+ const msg = error instanceof Error ? error.message : String(error);
187
+ log('error', `Failed to get channel for feature ${featureId}: ${msg}`);
188
+ throw error;
189
+ }
190
+ }
191
+ // ============================================================
192
+ // Main IPC Listener
193
+ // ============================================================
194
+ process.on('message', (msg) => {
195
+ switch (msg.type) {
196
+ case 'init':
197
+ handleInit(msg).catch((error) => {
198
+ log('error', `Init error: ${error instanceof Error ? error.message : String(error)}`);
199
+ });
200
+ break;
201
+ case 'event:phase_completed':
202
+ handlePhaseCompleted(msg).catch((error) => {
203
+ log('error', `Phase event error: ${error instanceof Error ? error.message : String(error)}`);
204
+ });
205
+ break;
206
+ case 'event:phase_failed':
207
+ handlePhaseFailed(msg).catch((error) => {
208
+ log('error', `Phase fail event error: ${error instanceof Error ? error.message : String(error)}`);
209
+ });
210
+ break;
211
+ case 'event:feature_done':
212
+ handleFeatureDone(msg).catch((error) => {
213
+ log('error', `Feature done event error: ${error instanceof Error ? error.message : String(error)}`);
214
+ });
215
+ break;
216
+ // Register a feature channel for polling when a worker starts
217
+ case 'event:feature_started': {
218
+ const startMsg = msg;
219
+ ensureFeatureChannel(startMsg.featureId).catch((error) => {
220
+ log('error', `Channel registration error: ${error instanceof Error ? error.message : String(error)}`);
221
+ });
222
+ break;
223
+ }
224
+ default:
225
+ log('warning', `Unknown message type: ${msg.type}`);
226
+ }
227
+ });
228
+ // Handle graceful shutdown
229
+ process.on('SIGTERM', () => {
230
+ stopPolling();
231
+ process.exit(0);
232
+ });
233
+ process.on('SIGINT', () => {
234
+ stopPolling();
235
+ process.exit(0);
236
+ });
237
+ // Handle uncaught errors
238
+ process.on('uncaughtException', (error) => {
239
+ log('error', `Uncaught exception: ${error.message}`);
240
+ stopPolling();
241
+ process.exit(1);
242
+ });
@@ -33,8 +33,13 @@ export declare class AgentWorkflowProcessor {
33
33
  private pollTimer?;
34
34
  /** Currently active worker processes, keyed by featureId */
35
35
  private activeWorkers;
36
+ /** Chat worker subprocess — runs in parallel, handles chat messages and phase events */
37
+ private chatWorker?;
36
38
  constructor(options: AgentWorkflowOptions, config: EdsgerConfig);
37
39
  start(): Promise<void>;
40
+ private startChatWorker;
41
+ /** Send a message to the chat worker via IPC */
42
+ private notifyChatWorker;
38
43
  stop(): void;
39
44
  private processNextFeatures;
40
45
  /**
@@ -25,10 +25,11 @@ import { createInitialState, updateFeatureState, createProcessingState, createCo
25
25
  * - Memory: each fork() creates ~50-80MB Node.js process
26
26
  */
27
27
  export const MAX_CONCURRENCY = 10;
28
- /** Path to the compiled feature-worker.js entry point */
28
+ /** Path to the compiled worker entry points */
29
29
  const __filename = fileURLToPath(import.meta.url);
30
30
  const __dirname = dirname(__filename);
31
31
  const WORKER_SCRIPT = join(__dirname, 'feature-worker.js');
32
+ const CHAT_WORKER_SCRIPT = join(__dirname, 'chat-worker.js');
32
33
  export class AgentWorkflowProcessor {
33
34
  options;
34
35
  config;
@@ -37,6 +38,8 @@ export class AgentWorkflowProcessor {
37
38
  pollTimer;
38
39
  /** Currently active worker processes, keyed by featureId */
39
40
  activeWorkers = new Map();
41
+ /** Chat worker subprocess — runs in parallel, handles chat messages and phase events */
42
+ chatWorker;
40
43
  constructor(options, config) {
41
44
  this.options = {
42
45
  pollInterval: 30_000,
@@ -54,6 +57,8 @@ export class AgentWorkflowProcessor {
54
57
  }
55
58
  this.isRunning = true;
56
59
  logInfo(`Concurrent processing: up to ${this.options.maxConcurrent} feature(s)`);
60
+ // Start chat worker subprocess
61
+ this.startChatWorker();
57
62
  // Initial feature check
58
63
  await this.processNextFeatures();
59
64
  // Set up polling
@@ -63,6 +68,69 @@ export class AgentWorkflowProcessor {
63
68
  });
64
69
  }, this.options.pollInterval);
65
70
  }
71
+ startChatWorker() {
72
+ try {
73
+ this.chatWorker = fork(CHAT_WORKER_SCRIPT, [], {
74
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
75
+ env: { ...process.env },
76
+ });
77
+ // Forward chat worker logs to parent
78
+ this.chatWorker.stdout?.on('data', (data) => {
79
+ const lines = data.toString().trim().split('\n');
80
+ for (const line of lines) {
81
+ if (line)
82
+ logInfo(` [chat] ${line}`);
83
+ }
84
+ });
85
+ this.chatWorker.stderr?.on('data', (data) => {
86
+ const lines = data.toString().trim().split('\n');
87
+ for (const line of lines) {
88
+ if (line)
89
+ logError(` [chat] ${line}`);
90
+ }
91
+ });
92
+ this.chatWorker.on('message', (msg) => {
93
+ if (msg.type === 'log') {
94
+ const prefix = '[chat]';
95
+ switch (msg.level) {
96
+ case 'info':
97
+ logInfo(` ${prefix} ${msg.message}`);
98
+ break;
99
+ case 'warning':
100
+ logWarning(` ${prefix} ${msg.message}`);
101
+ break;
102
+ case 'error':
103
+ logError(` ${prefix} ${msg.message}`);
104
+ break;
105
+ case 'success':
106
+ logSuccess(` ${prefix} ${msg.message}`);
107
+ break;
108
+ }
109
+ }
110
+ });
111
+ this.chatWorker.on('exit', (code) => {
112
+ logWarning(`Chat worker exited with code ${code}`);
113
+ this.chatWorker = undefined;
114
+ });
115
+ // Initialize the chat worker with config
116
+ this.chatWorker.send({ type: 'init', config: this.config });
117
+ logInfo('Chat worker started');
118
+ }
119
+ catch (error) {
120
+ logError(`Failed to start chat worker: ${error instanceof Error ? error.message : String(error)}`);
121
+ }
122
+ }
123
+ /** Send a message to the chat worker via IPC */
124
+ notifyChatWorker(msg) {
125
+ if (this.chatWorker && this.chatWorker.connected) {
126
+ try {
127
+ this.chatWorker.send(msg);
128
+ }
129
+ catch {
130
+ // Chat worker may have died — non-critical
131
+ }
132
+ }
133
+ }
66
134
  stop() {
67
135
  if (!this.isRunning)
68
136
  return;
@@ -71,7 +139,17 @@ export class AgentWorkflowProcessor {
71
139
  clearInterval(this.pollTimer);
72
140
  this.pollTimer = undefined;
73
141
  }
74
- // Kill all active workers gracefully
142
+ // Kill chat worker
143
+ if (this.chatWorker) {
144
+ try {
145
+ this.chatWorker.kill('SIGTERM');
146
+ }
147
+ catch {
148
+ // Ignore kill errors
149
+ }
150
+ this.chatWorker = undefined;
151
+ }
152
+ // Kill all active feature workers gracefully
75
153
  for (const [featureId, worker] of this.activeWorkers) {
76
154
  logInfo(`Stopping worker for feature: ${featureId}`);
77
155
  try {
@@ -232,6 +310,8 @@ export class AgentWorkflowProcessor {
232
310
  verbose: this.options.verbose,
233
311
  config: this.config,
234
312
  });
313
+ // Notify chat worker that a feature has started processing
314
+ this.notifyChatWorker({ type: 'event:feature_started', featureId });
235
315
  }
236
316
  catch (error) {
237
317
  this.activeWorkers.delete(featureId);
@@ -243,10 +323,19 @@ export class AgentWorkflowProcessor {
243
323
  if (success) {
244
324
  this.processedFeatures = updateFeatureState(this.processedFeatures, featureId, (currentState) => createCompletedState(featureId, currentState));
245
325
  logSuccess(`Feature completed: ${featureName}`);
326
+ // Notify chat worker that feature workflow is done
327
+ this.notifyChatWorker({ type: 'event:feature_done', featureId });
246
328
  }
247
329
  else {
248
330
  this.processedFeatures = updateFeatureState(this.processedFeatures, featureId, (currentState) => createFailedState(featureId, currentState));
249
331
  logError(`Feature failed: ${featureName}${error ? ` - ${error}` : ''}`);
332
+ // Notify chat worker about the failure
333
+ this.notifyChatWorker({
334
+ type: 'event:phase_failed',
335
+ featureId,
336
+ phase: 'workflow',
337
+ error: error || 'Unknown error',
338
+ });
250
339
  }
251
340
  // Clear heartbeat feature info
252
341
  sendHeartbeat().catch(() => { });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Build context for the chat AI processor.
3
+ * Fetches feature state, user stories, test cases, and recent chat history.
4
+ */
5
+ import type { ChatMessage } from '../../types/index.js';
6
+ export interface ChatProcessorContext {
7
+ featureId: string;
8
+ featureDescription: string;
9
+ featureStatus: string;
10
+ executionMode: string;
11
+ workflow: Array<{
12
+ phase: string;
13
+ status: string;
14
+ }>;
15
+ userStories: Array<{
16
+ id: string;
17
+ title: string;
18
+ description: string;
19
+ status: string;
20
+ }>;
21
+ testCases: Array<{
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ is_critical: boolean;
26
+ }>;
27
+ recentChatMessages: ChatMessage[];
28
+ channelId: string;
29
+ }
30
+ /**
31
+ * Build the full context for the chat AI to process a message.
32
+ */
33
+ export declare function buildChatContext(featureId: string, channelId: string, verbose?: boolean): Promise<ChatProcessorContext>;
34
+ /**
35
+ * Format context into a string for the AI system prompt.
36
+ */
37
+ export declare function formatContextForAI(context: ChatProcessorContext): string;