edsger 0.29.2 → 0.30.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.
@@ -33,6 +33,24 @@ export declare function claimPendingMessages(channelId: string, workerId: string
33
33
  export declare function markMessageProcessed(messageId: string, verbose?: boolean): Promise<void>;
34
34
  export declare function markChannelRead(channelId: string, lastReadMessageId?: string, verbose?: boolean): Promise<void>;
35
35
  export declare function getUnreadCount(channelId: string, verbose?: boolean): Promise<number>;
36
+ /**
37
+ * Get or create the group chat channel for a product.
38
+ * Every product has one group channel.
39
+ */
40
+ export declare function getProductChannel(productId: string, verbose?: boolean): Promise<ChatChannel>;
41
+ /**
42
+ * Send a system message to a product's group channel.
43
+ * Creates the channel if it doesn't exist.
44
+ */
45
+ export declare function sendProductSystemMessage(productId: string, content: string, metadata?: Record<string, unknown>, verbose?: boolean): Promise<ChatMessage>;
46
+ /**
47
+ * Send an AI message to a product's group channel.
48
+ * Creates the channel if it doesn't exist.
49
+ */
50
+ export declare function sendProductAiMessage(productId: string, content: string, metadata?: Record<string, unknown>, options?: {
51
+ messageType?: ChatMessageType;
52
+ parentMessageId?: string;
53
+ }, verbose?: boolean): Promise<ChatMessage>;
36
54
  /**
37
55
  * Get or create the group chat channel for a feature.
38
56
  * This is the most common operation — every feature has one group channel.
package/dist/api/chat.js CHANGED
@@ -135,6 +135,47 @@ export async function getUnreadCount(channelId, verbose) {
135
135
  return result.unread_count;
136
136
  }
137
137
  // ============================================================
138
+ // Convenience: Product Channel
139
+ // ============================================================
140
+ /**
141
+ * Get or create the group chat channel for a product.
142
+ * Every product has one group channel.
143
+ */
144
+ export async function getProductChannel(productId, verbose) {
145
+ const { channel } = await getOrCreateChannel('product', productId, 'group', undefined, verbose);
146
+ return channel;
147
+ }
148
+ /**
149
+ * Send a system message to a product's group channel.
150
+ * Creates the channel if it doesn't exist.
151
+ */
152
+ export async function sendProductSystemMessage(productId, content, metadata = {}, verbose) {
153
+ try {
154
+ const channel = await getProductChannel(productId, 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 product ${productId}: ${msg}`);
160
+ throw error;
161
+ }
162
+ }
163
+ /**
164
+ * Send an AI message to a product's group channel.
165
+ * Creates the channel if it doesn't exist.
166
+ */
167
+ export async function sendProductAiMessage(productId, content, metadata = {}, options = {}, verbose) {
168
+ try {
169
+ const channel = await getProductChannel(productId, 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 product ${productId}: ${msg}`);
175
+ throw error;
176
+ }
177
+ }
178
+ // ============================================================
138
179
  // Convenience: Feature Channel
139
180
  // ============================================================
140
181
  /**
@@ -10,6 +10,10 @@ interface StatusUpdateOptions {
10
10
  }
11
11
  /**
12
12
  * Check if moving from currentStatus to newStatus is forward progression
13
+ *
14
+ * Special cases for archived status:
15
+ * - Any status → archived: Always allowed (archiving from any state)
16
+ * - Archived → backlog: Always allowed (unarchiving restores to backlog)
13
17
  */
14
18
  export declare function isForwardProgression(currentStatus: FeatureStatus, newStatus: FeatureStatus): boolean;
15
19
  /**
@@ -8,8 +8,18 @@ import { getFeature } from './get-feature.js';
8
8
  import { STATUS_PROGRESSION_ORDER, PHASE_STATUS_MAP, } from '../../config/feature-status.js';
9
9
  /**
10
10
  * Check if moving from currentStatus to newStatus is forward progression
11
+ *
12
+ * Special cases for archived status:
13
+ * - Any status → archived: Always allowed (archiving from any state)
14
+ * - Archived → backlog: Always allowed (unarchiving restores to backlog)
11
15
  */
12
16
  export function isForwardProgression(currentStatus, newStatus) {
17
+ // Any status can transition to archived
18
+ if (newStatus === 'archived')
19
+ return true;
20
+ // Archived can only transition back to backlog (unarchive)
21
+ if (currentStatus === 'archived')
22
+ return newStatus === 'backlog';
13
23
  const currentIndex = STATUS_PROGRESSION_ORDER.indexOf(currentStatus);
14
24
  const newIndex = STATUS_PROGRESSION_ORDER.indexOf(newStatus);
15
25
  // Allow moving forward or staying at same level (for retries, etc.)
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { randomUUID } from 'node:crypto';
17
17
  import { getFeatureChannel, claimPendingMessages, listChannels, sendSystemMessage, } from '../../api/chat.js';
18
- import { processHumanMessages, processPhaseCompletion, } from '../../phases/chat-processor/index.js';
18
+ import { processHumanMessages, processProductHumanMessages, processPhaseCompletion, } from '../../phases/chat-processor/index.js';
19
19
  function sendMessage(msg) {
20
20
  if (process.send) {
21
21
  process.send(msg);
@@ -28,12 +28,17 @@ function log(level, message) {
28
28
  // State
29
29
  // ============================================================
30
30
  let config = null;
31
+ let verbose = false;
31
32
  let isRunning = false;
32
33
  let pollTimer = null;
33
34
  // Unique worker ID for this process instance — used for atomic message claiming
34
35
  const WORKER_ID = `chat-worker-${process.pid}-${randomUUID().slice(0, 8)}`;
35
36
  // Track active feature channels (featureId -> channelId)
36
37
  const activeChannels = new Map();
38
+ // Track active product channels (productId -> channelId)
39
+ const activeProductChannels = new Map();
40
+ // Track feature repo paths (featureId -> repoPath) for setting cwd on AI agent
41
+ const featureRepoPaths = new Map();
37
42
  // Poll interval in ms
38
43
  const POLL_INTERVAL = 5000;
39
44
  // Refresh channel list every N polls (~30s at 5s intervals)
@@ -50,19 +55,33 @@ async function pollForMessages() {
50
55
  if (pollCount % CHANNEL_REFRESH_INTERVAL === 0) {
51
56
  await refreshChannels();
52
57
  }
58
+ // Poll feature channels
53
59
  for (const [featureId, channelId] of activeChannels) {
54
60
  try {
55
- // Atomically claim messages — other workers won't see these
56
61
  const claimed = await claimPendingMessages(channelId, WORKER_ID);
57
62
  if (claimed.length > 0) {
58
63
  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);
64
+ const repoPath = featureRepoPaths.get(featureId);
65
+ await processHumanMessages(claimed, featureId, config, verbose, repoPath);
61
66
  }
62
67
  }
63
68
  catch (error) {
64
69
  const msg = error instanceof Error ? error.message : String(error);
65
- log('error', `Error polling channel ${channelId}: ${msg}`);
70
+ log('error', `Error polling feature channel ${channelId}: ${msg}`);
71
+ }
72
+ }
73
+ // Poll product channels
74
+ for (const [productId, channelId] of activeProductChannels) {
75
+ try {
76
+ const claimed = await claimPendingMessages(channelId, WORKER_ID);
77
+ if (claimed.length > 0) {
78
+ log('info', `Claimed ${claimed.length} message(s) for product ${productId} (worker: ${WORKER_ID})`);
79
+ await processProductHumanMessages(claimed, productId, config, verbose);
80
+ }
81
+ }
82
+ catch (error) {
83
+ const msg = error instanceof Error ? error.message : String(error);
84
+ log('error', `Error polling product channel ${channelId}: ${msg}`);
66
85
  }
67
86
  }
68
87
  }
@@ -95,6 +114,7 @@ function stopPolling() {
95
114
  // ============================================================
96
115
  async function handleInit(msg) {
97
116
  config = msg.config;
117
+ verbose = msg.verbose ?? false;
98
118
  log('info', `Chat worker initialized (id: ${WORKER_ID})`);
99
119
  // Load existing channels before starting the poll loop
100
120
  await refreshChannels();
@@ -109,7 +129,8 @@ async function handlePhaseCompleted(msg) {
109
129
  // Ensure we have the channel registered
110
130
  await ensureFeatureChannel(featureId);
111
131
  // Process with AI for next-step suggestions
112
- await processPhaseCompletion(featureId, phase, summary, phaseOutput, config);
132
+ const repoPath = featureRepoPaths.get(featureId);
133
+ await processPhaseCompletion(featureId, phase, summary, phaseOutput, config, verbose, repoPath);
113
134
  }
114
135
  catch (error) {
115
136
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -155,16 +176,26 @@ async function handleFeatureDone(msg) {
155
176
  */
156
177
  async function refreshChannels() {
157
178
  try {
158
- const channels = await listChannels('feature');
179
+ // Fetch feature and product channels in parallel
180
+ const [featureChannels, productChannels] = await Promise.all([
181
+ listChannels('feature'),
182
+ listChannels('product'),
183
+ ]);
159
184
  let added = 0;
160
- for (const channel of channels) {
185
+ for (const channel of featureChannels) {
161
186
  if (channel.channel_ref_id && !activeChannels.has(channel.channel_ref_id)) {
162
187
  activeChannels.set(channel.channel_ref_id, channel.id);
163
188
  added++;
164
189
  }
165
190
  }
191
+ for (const channel of productChannels) {
192
+ if (channel.channel_ref_id && !activeProductChannels.has(channel.channel_ref_id)) {
193
+ activeProductChannels.set(channel.channel_ref_id, channel.id);
194
+ added++;
195
+ }
196
+ }
166
197
  if (added > 0) {
167
- log('info', `Discovered ${added} new channel(s) (total: ${activeChannels.size})`);
198
+ log('info', `Discovered ${added} new channel(s) (features: ${activeChannels.size}, products: ${activeProductChannels.size})`);
168
199
  }
169
200
  }
170
201
  catch (error) {
@@ -216,6 +247,9 @@ process.on('message', (msg) => {
216
247
  // Register a feature channel for polling when a worker starts
217
248
  case 'event:feature_started': {
218
249
  const startMsg = msg;
250
+ if (startMsg.repoPath) {
251
+ featureRepoPaths.set(startMsg.featureId, startMsg.repoPath);
252
+ }
219
253
  ensureFeatureChannel(startMsg.featureId).catch((error) => {
220
254
  log('error', `Channel registration error: ${error instanceof Error ? error.message : String(error)}`);
221
255
  });
@@ -113,7 +113,7 @@ export class AgentWorkflowProcessor {
113
113
  this.chatWorker = undefined;
114
114
  });
115
115
  // Initialize the chat worker with config
116
- this.chatWorker.send({ type: 'init', config: this.config });
116
+ this.chatWorker.send({ type: 'init', config: this.config, verbose: this.options.verbose });
117
117
  logInfo('Chat worker started');
118
118
  }
119
119
  catch (error) {
@@ -336,7 +336,7 @@ export class AgentWorkflowProcessor {
336
336
  config: this.config,
337
337
  });
338
338
  // Notify chat worker that a feature has started processing
339
- this.notifyChatWorker({ type: 'event:feature_started', featureId });
339
+ this.notifyChatWorker({ type: 'event:feature_started', featureId, repoPath });
340
340
  }
341
341
  catch (error) {
342
342
  this.activeWorkers.delete(featureId);
@@ -6,9 +6,9 @@ import assert from 'node:assert';
6
6
  import { STATUS_PROGRESSION_ORDER, PHASE_STATUS_MAP, } from '../feature-status.js';
7
7
  describe('Feature Status Configuration', () => {
8
8
  describe('STATUS_PROGRESSION_ORDER', () => {
9
- it('should start with backlog and end with shipped', () => {
9
+ it('should start with backlog and end with archived', () => {
10
10
  assert.strictEqual(STATUS_PROGRESSION_ORDER[0], 'backlog', 'First status should be backlog');
11
- assert.strictEqual(STATUS_PROGRESSION_ORDER[STATUS_PROGRESSION_ORDER.length - 1], 'shipped', 'Last status should be shipped');
11
+ assert.strictEqual(STATUS_PROGRESSION_ORDER[STATUS_PROGRESSION_ORDER.length - 1], 'archived', 'Last status should be archived');
12
12
  });
13
13
  it('should be readonly', () => {
14
14
  // This test ensures the configuration is properly typed as readonly
@@ -49,6 +49,7 @@ export const STATUS_PROGRESSION_ORDER = [
49
49
  'testing_failed',
50
50
  'ready_for_review',
51
51
  'shipped',
52
+ 'archived',
52
53
  ];
53
54
  /**
54
55
  * Phase to status mapping
@@ -119,6 +120,7 @@ export const HUMAN_SELECTABLE_STATUSES = [
119
120
  'functional_testing',
120
121
  'ready_for_review',
121
122
  'shipped',
123
+ 'archived',
122
124
  ];
123
125
  /**
124
126
  * Check if a status can be manually selected by a human user
@@ -26,9 +26,15 @@ export declare function clearChannelSession(channelId: string): void;
26
26
  *
27
27
  * Returns the session ID (callers should track this if needed).
28
28
  */
29
- export declare function processHumanMessages(messages: ChatMessage[], featureId: string, config: EdsgerConfig, verbose?: boolean): Promise<string | undefined>;
29
+ export declare function processHumanMessages(messages: ChatMessage[], featureId: string, config: EdsgerConfig, verbose?: boolean, repoPath?: string): Promise<string | undefined>;
30
+ /**
31
+ * Process one or more human messages from a product channel.
32
+ * Same session resumption pattern as feature chat, but uses
33
+ * product context, product prompt, and product tools.
34
+ */
35
+ export declare function processProductHumanMessages(messages: ChatMessage[], productId: string, config: EdsgerConfig, verbose?: boolean): Promise<string | undefined>;
30
36
  /**
31
37
  * Process a phase completion event: analyze output and suggest next steps.
32
38
  * Resumes the same channel session so AI has full conversation context.
33
39
  */
34
- export declare function processPhaseCompletion(featureId: string, phase: string, summary: string, phaseOutput: unknown, config: EdsgerConfig, verbose?: boolean): Promise<void>;
40
+ export declare function processPhaseCompletion(featureId: string, phase: string, summary: string, phaseOutput: unknown, config: EdsgerConfig, verbose?: boolean, repoPath?: string): Promise<void>;
@@ -15,8 +15,11 @@ import { DEFAULT_MODEL } from '../../constants.js';
15
15
  import { logInfo, logError } from '../../utils/logger.js';
16
16
  import { sendAiMessage, markMessageProcessed, sendSystemMessage, getFeatureChannel, listChatMessages, } from '../../api/chat.js';
17
17
  import { buildChatContext, formatContextForAI } from './context.js';
18
+ import { buildProductChatContext, formatProductContextForAI, } from './product-context.js';
18
19
  import { CHAT_RESPONSE_PROMPT, NEXT_STEP_ADVISOR_PROMPT, buildNextStepAdvisorMessage, } from './prompts.js';
20
+ import { PRODUCT_CHAT_RESPONSE_PROMPT } from './product-prompts.js';
19
21
  import { createChatMcpServer } from './tools.js';
22
+ import { createProductChatMcpServer } from './product-tools.js';
20
23
  // ============================================================
21
24
  // Session Management
22
25
  // ============================================================
@@ -44,7 +47,7 @@ export function clearChannelSession(channelId) {
44
47
  *
45
48
  * Returns the session ID (callers should track this if needed).
46
49
  */
47
- export async function processHumanMessages(messages, featureId, config, verbose) {
50
+ export async function processHumanMessages(messages, featureId, config, verbose, repoPath) {
48
51
  if (messages.length === 0)
49
52
  return undefined;
50
53
  const channelId = messages[0].channel_id;
@@ -107,7 +110,7 @@ export async function processHumanMessages(messages, featureId, config, verbose)
107
110
  fullPrompt = userPrompt;
108
111
  }
109
112
  // Run the agent (with resume if we have a session)
110
- const result = await runChatAgent(CHAT_RESPONSE_PROMPT, fullPrompt, config, existingSessionId, verbose);
113
+ const result = await runChatAgent(CHAT_RESPONSE_PROMPT, fullPrompt, config, existingSessionId, verbose, repoPath);
111
114
  // Store the session ID for future resumption
112
115
  if (result.sessionId) {
113
116
  channelSessions.set(channelId, result.sessionId);
@@ -135,7 +138,7 @@ export async function processHumanMessages(messages, featureId, config, verbose)
135
138
  if (existingSessionId) {
136
139
  logInfo(`Clearing session ${existingSessionId} and retrying without resume`);
137
140
  channelSessions.delete(channelId);
138
- return processHumanMessages(messages, featureId, config, verbose);
141
+ return processHumanMessages(messages, featureId, config, verbose, repoPath);
139
142
  }
140
143
  // Send error message to chat
141
144
  try {
@@ -153,11 +156,114 @@ export async function processHumanMessages(messages, featureId, config, verbose)
153
156
  return undefined;
154
157
  }
155
158
  }
159
+ /**
160
+ * Process one or more human messages from a product channel.
161
+ * Same session resumption pattern as feature chat, but uses
162
+ * product context, product prompt, and product tools.
163
+ */
164
+ export async function processProductHumanMessages(messages, productId, config, verbose) {
165
+ if (messages.length === 0)
166
+ return undefined;
167
+ const channelId = messages[0].channel_id;
168
+ const existingSessionId = channelSessions.get(channelId);
169
+ if (verbose) {
170
+ logInfo(`Processing ${messages.length} product message(s) for channel ${channelId}` +
171
+ (existingSessionId ? ` (resuming session ${existingSessionId})` : ' (new session)'));
172
+ }
173
+ try {
174
+ // Build the user prompt
175
+ const messageParts = messages.map((m, i) => {
176
+ if (messages.length === 1) {
177
+ return m.content;
178
+ }
179
+ return `[Message ${i + 1} from ${m.sender_name}]: ${m.content}`;
180
+ });
181
+ const userPrompt = messageParts.join('\n\n');
182
+ let fullPrompt;
183
+ if (!existingSessionId) {
184
+ const context = await buildProductChatContext(productId, channelId, verbose);
185
+ const contextStr = formatProductContextForAI(context);
186
+ // Load recent chat history
187
+ const recentMessages = await listChatMessages(channelId, { limit: 30 }, verbose);
188
+ const currentIds = new Set(messages.map((m) => m.id));
189
+ const historyMessages = recentMessages.filter((m) => !currentIds.has(m.id));
190
+ let historySection = '';
191
+ if (historyMessages.length > 0) {
192
+ const historyLines = historyMessages.map((m) => {
193
+ const role = m.sender_type === 'ai' ? 'AI' : m.sender_type === 'system' ? 'System' : (m.sender_name || 'User');
194
+ return `[${role}]: ${m.content}`;
195
+ });
196
+ historySection = [
197
+ `## Previous Chat History`,
198
+ `The following is the recent conversation history in this channel. Continue the conversation naturally.`,
199
+ '',
200
+ ...historyLines,
201
+ '',
202
+ ].join('\n');
203
+ }
204
+ fullPrompt = [
205
+ `## Current Product Context`,
206
+ contextStr,
207
+ '',
208
+ `## Channel ID: ${channelId}`,
209
+ `## Product ID: ${productId}`,
210
+ '',
211
+ historySection,
212
+ `## Human Message`,
213
+ userPrompt,
214
+ '',
215
+ `Respond to this message. Use tools if you need to take actions. Always respond in the chat using the send_chat_message tool.`,
216
+ ].join('\n');
217
+ }
218
+ else {
219
+ fullPrompt = userPrompt;
220
+ }
221
+ // Run the agent with product tools (no cwd — product chat doesn't operate on a repo)
222
+ const result = await runProductChatAgent(PRODUCT_CHAT_RESPONSE_PROMPT, fullPrompt, config, existingSessionId, verbose);
223
+ if (result.sessionId) {
224
+ channelSessions.set(channelId, result.sessionId);
225
+ }
226
+ const sentViaTool = result.toolsUsed.has('mcp__edsger-product-chat__send_chat_message');
227
+ if (result.text && !sentViaTool) {
228
+ await sendAiMessage(channelId, result.text, {
229
+ in_response_to: messages[messages.length - 1].id,
230
+ });
231
+ }
232
+ for (const message of messages) {
233
+ await markMessageProcessed(message.id, verbose);
234
+ }
235
+ if (verbose) {
236
+ logInfo(`All ${messages.length} product message(s) processed successfully`);
237
+ }
238
+ return result.sessionId;
239
+ }
240
+ catch (error) {
241
+ const msg = error instanceof Error ? error.message : String(error);
242
+ logError(`Failed to process product messages: ${msg}`);
243
+ if (existingSessionId) {
244
+ logInfo(`Clearing session ${existingSessionId} and retrying without resume`);
245
+ channelSessions.delete(channelId);
246
+ return processProductHumanMessages(messages, productId, config, verbose);
247
+ }
248
+ try {
249
+ await sendAiMessage(channelId, `Sorry, I encountered an error processing your message: ${msg}`, {
250
+ error: true,
251
+ });
252
+ }
253
+ catch {
254
+ // Ignore error sending error message
255
+ }
256
+ for (const message of messages) {
257
+ await markMessageProcessed(message.id, verbose);
258
+ }
259
+ return undefined;
260
+ }
261
+ }
156
262
  /**
157
263
  * Process a phase completion event: analyze output and suggest next steps.
158
264
  * Resumes the same channel session so AI has full conversation context.
159
265
  */
160
- export async function processPhaseCompletion(featureId, phase, summary, phaseOutput, config, verbose) {
266
+ export async function processPhaseCompletion(featureId, phase, summary, phaseOutput, config, verbose, repoPath) {
161
267
  if (verbose) {
162
268
  logInfo(`Processing phase completion: ${phase} for feature ${featureId}`);
163
269
  }
@@ -215,7 +321,7 @@ export async function processPhaseCompletion(featureId, phase, summary, phaseOut
215
321
  // but on resume the system prompt from the original session persists.
216
322
  // We still pass the advisor prompt — the SDK will use it for new sessions
217
323
  // and ignore it for resumed ones (system prompt is fixed at session creation).
218
- const result = await runChatAgent(existingSessionId ? CHAT_RESPONSE_PROMPT : NEXT_STEP_ADVISOR_PROMPT, fullPrompt, config, existingSessionId, verbose);
324
+ const result = await runChatAgent(existingSessionId ? CHAT_RESPONSE_PROMPT : NEXT_STEP_ADVISOR_PROMPT, fullPrompt, config, existingSessionId, verbose, repoPath);
219
325
  // Store session ID
220
326
  if (result.sessionId) {
221
327
  channelSessions.set(channelId, result.sessionId);
@@ -253,7 +359,7 @@ async function* prompt(userContent) {
253
359
  * Claude Code's built-in tools (Read, Write, Bash, Grep, Glob) are also available.
254
360
  * Supports session resumption via the `resumeSessionId` parameter.
255
361
  */
256
- async function runChatAgent(systemPrompt, userPrompt, config, resumeSessionId, verbose) {
362
+ async function runChatAgent(systemPrompt, userPrompt, config, resumeSessionId, verbose, cwd) {
257
363
  let lastAssistantResponse = '';
258
364
  const toolUsed = new Set();
259
365
  let sessionId;
@@ -273,6 +379,7 @@ async function runChatAgent(systemPrompt, userPrompt, config, resumeSessionId, v
273
379
  mcpServers: {
274
380
  'edsger-chat': chatMcpServer,
275
381
  },
382
+ ...(cwd ? { cwd } : {}),
276
383
  ...(resumeSessionId ? { resume: resumeSessionId } : {}),
277
384
  },
278
385
  })) {
@@ -308,3 +415,61 @@ async function runChatAgent(systemPrompt, userPrompt, config, resumeSessionId, v
308
415
  toolsUsed: toolUsed,
309
416
  };
310
417
  }
418
+ /**
419
+ * Run the Claude Agent SDK with product chat MCP tools.
420
+ * Same as runChatAgent but uses product-level tools instead of feature-level tools.
421
+ * No cwd parameter since product chat doesn't operate on a specific repo.
422
+ */
423
+ async function runProductChatAgent(systemPrompt, userPrompt, config, resumeSessionId, verbose) {
424
+ let lastAssistantResponse = '';
425
+ const toolUsed = new Set();
426
+ let sessionId;
427
+ const productChatMcpServer = createProductChatMcpServer();
428
+ for await (const message of query({
429
+ prompt: prompt(userPrompt),
430
+ options: {
431
+ systemPrompt: {
432
+ type: 'preset',
433
+ preset: 'claude_code',
434
+ append: systemPrompt,
435
+ },
436
+ model: DEFAULT_MODEL,
437
+ maxTurns: 20,
438
+ permissionMode: 'bypassPermissions',
439
+ mcpServers: {
440
+ 'edsger-product-chat': productChatMcpServer,
441
+ },
442
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
443
+ },
444
+ })) {
445
+ if (message.session_id && !sessionId) {
446
+ sessionId = message.session_id;
447
+ }
448
+ if (message.type === 'assistant' && message.message?.content) {
449
+ for (const content of message.message.content) {
450
+ if (content.type === 'text') {
451
+ lastAssistantResponse += content.text + '\n';
452
+ if (verbose) {
453
+ console.log(`🤖 ${content.text}`);
454
+ }
455
+ }
456
+ else if (content.type === 'tool_use') {
457
+ toolUsed.add(content.name);
458
+ if (verbose) {
459
+ console.log(`🔧 Tool: ${content.name}`);
460
+ }
461
+ }
462
+ }
463
+ }
464
+ if (message.type === 'result') {
465
+ if (verbose) {
466
+ logInfo(`Product chat agent completed: ${message.subtype}`);
467
+ }
468
+ }
469
+ }
470
+ return {
471
+ text: lastAssistantResponse.trim(),
472
+ sessionId,
473
+ toolsUsed: toolUsed,
474
+ };
475
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Build context for the product chat AI processor.
3
+ * Fetches product state, features summary, and recent chat history.
4
+ */
5
+ import type { ChatMessage } from '../../types/index.js';
6
+ export interface ProductChatContext {
7
+ productId: string;
8
+ productName: string;
9
+ productDescription: string;
10
+ featuresSummary: {
11
+ total: number;
12
+ byStatus: Record<string, number>;
13
+ features: Array<{
14
+ id: string;
15
+ name: string;
16
+ status: string;
17
+ description: string;
18
+ }>;
19
+ };
20
+ teamMembers: Array<{
21
+ id: string;
22
+ name: string;
23
+ email: string;
24
+ role: string;
25
+ }>;
26
+ recentChatMessages: ChatMessage[];
27
+ channelId: string;
28
+ }
29
+ /**
30
+ * Build the full context for the product chat AI to process a message.
31
+ */
32
+ export declare function buildProductChatContext(productId: string, channelId: string, verbose?: boolean): Promise<ProductChatContext>;
33
+ /**
34
+ * Format product context into a string for the AI system prompt.
35
+ */
36
+ export declare function formatProductContextForAI(context: ProductChatContext): string;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Build context for the product chat AI processor.
3
+ * Fetches product state, features summary, and recent chat history.
4
+ */
5
+ import { callMcpEndpoint } from '../../api/mcp-client.js';
6
+ import { listChatMessages } from '../../api/chat.js';
7
+ /**
8
+ * Build the full context for the product chat AI to process a message.
9
+ */
10
+ export async function buildProductChatContext(productId, channelId, verbose) {
11
+ // Fetch product, features, and team members in parallel
12
+ const [productResult, featuresResult, membersResult, recentMessages] = await Promise.all([
13
+ callMcpEndpoint('products/list', {
14
+ product_id: productId,
15
+ }),
16
+ callMcpEndpoint('features/list', {
17
+ product_id: productId,
18
+ }),
19
+ callMcpEndpoint('tasks/product_members', {
20
+ product_id: productId,
21
+ }),
22
+ listChatMessages(channelId, { limit: 20 }, verbose),
23
+ ]);
24
+ const products = productResult?.products || productResult?.content?.[0]?.text
25
+ ? (() => {
26
+ try {
27
+ const text = productResult?.content?.[0]?.text;
28
+ return text ? JSON.parse(text) : productResult?.products || [];
29
+ }
30
+ catch {
31
+ return productResult?.products || [];
32
+ }
33
+ })()
34
+ : [];
35
+ const product = Array.isArray(products) ? products.find((p) => p.id === productId) || products[0] || {} : products || {};
36
+ const allFeatures = (featuresResult?.features || []).map((f) => ({
37
+ id: f.id,
38
+ name: f.name,
39
+ status: f.status,
40
+ description: f.description || '',
41
+ }));
42
+ // Count features by status
43
+ const byStatus = {};
44
+ for (const f of allFeatures) {
45
+ byStatus[f.status] = (byStatus[f.status] || 0) + 1;
46
+ }
47
+ // Parse members
48
+ let members = [];
49
+ try {
50
+ const membersData = membersResult?.content?.[0]?.text
51
+ ? JSON.parse(membersResult.content[0].text)
52
+ : membersResult?.members || membersResult || [];
53
+ members = Array.isArray(membersData) ? membersData : [];
54
+ }
55
+ catch {
56
+ members = [];
57
+ }
58
+ return {
59
+ productId,
60
+ productName: product.name || 'Unknown',
61
+ productDescription: product.description || 'No description',
62
+ featuresSummary: {
63
+ total: allFeatures.length,
64
+ byStatus,
65
+ features: allFeatures,
66
+ },
67
+ teamMembers: members,
68
+ recentChatMessages: recentMessages,
69
+ channelId,
70
+ };
71
+ }
72
+ /**
73
+ * Format product context into a string for the AI system prompt.
74
+ */
75
+ export function formatProductContextForAI(context) {
76
+ const parts = [
77
+ `## Product: ${context.productName}`,
78
+ context.productDescription,
79
+ '',
80
+ `## Features Summary (${context.featuresSummary.total} total)`,
81
+ '### By Status:',
82
+ ...Object.entries(context.featuresSummary.byStatus).map(([status, count]) => `- ${status}: ${count}`),
83
+ '',
84
+ '### Features:',
85
+ ];
86
+ if (context.featuresSummary.features.length > 0) {
87
+ for (const f of context.featuresSummary.features) {
88
+ parts.push(`- [${f.status}] ${f.name}: ${f.description.slice(0, 100)}`);
89
+ }
90
+ }
91
+ else {
92
+ parts.push('- (no features yet)');
93
+ }
94
+ parts.push('', `## Team Members (${context.teamMembers.length})`);
95
+ if (context.teamMembers.length > 0) {
96
+ for (const m of context.teamMembers) {
97
+ parts.push(`- ${m.name || m.email} (${m.role})`);
98
+ }
99
+ }
100
+ else {
101
+ parts.push('- (no team members)');
102
+ }
103
+ return parts.join('\n');
104
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * System prompts for the product chat AI processor.
3
+ */
4
+ export declare const PRODUCT_CHAT_RESPONSE_PROMPT = "You are an AI assistant embedded in Edsger, a software development platform. You are helping a team manage a product at the product level.\n\n## Your Capabilities\nYou can see the product's current state, all features (with statuses), and team members. You have tools to:\n- List features with status filtering\n- Create new features for the product\n- Get detailed feature information (drill down into any feature)\n- Create tasks for team members (human) or AI\n- Look up product team members by name\n- Send follow-up messages and present options to the user\n\n## How to Respond\n1. **Understand the intent** \u2014 Is this a question about product state, a request to create something, or coordination work?\n2. **Take action if needed** \u2014 Use the appropriate tools to make changes\n3. **Respond concisely** \u2014 Summarize what you understood and what you did\n4. **Ask for clarification** \u2014 If the message is ambiguous, use provide_options to present choices\n\n## Communication Style\n- Respond in the same language the user writes in\n- Be concise but thorough \u2014 no filler text\n- Reference specific features, statuses, and team members by name\n- When making changes, always explain what you changed and why\n\n## Product-Level Scope\nUnlike feature chat (which focuses on a single feature's lifecycle), you operate at the product level:\n- Answer cross-feature questions (e.g., \"which features are blocked?\", \"what's our progress?\")\n- Help with product planning and prioritization\n- Create new features when the user describes new work\n- Coordinate team work by creating and assigning tasks\n- Provide product-wide insights and summaries\n\n## Task Creation\nWhen the user asks to notify someone, assign a review, or request action from a team member:\n1. Use list_product_members to find the person by name\n2. Use create_task with executor=\"human\", the resolved user ID, and the correct action_url\n3. Confirm in chat what you created and who it's assigned to\n\nWhen the user describes work for AI to do:\n1. Use create_task with executor=\"ai\" \u2014 the task worker will pick it up automatically\n\n### Action URL Patterns\nAlways set action_url to link to the most relevant page:\n- Product page: `/products/{product_id}`\n- Feature details: `/products/{product_id}/features/{feature_id}`\n- Feature tab: `/products/{product_id}/features/{feature_id}?tab={tab}`\n\n## Important Rules\n- Never make destructive changes without confirmation\n- For ambiguous requests, present options rather than guessing\n- If you can't do something, explain why clearly\n- When creating tasks for people, always confirm the person's identity if the name is ambiguous\n";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * System prompts for the product chat AI processor.
3
+ */
4
+ export const PRODUCT_CHAT_RESPONSE_PROMPT = `You are an AI assistant embedded in Edsger, a software development platform. You are helping a team manage a product at the product level.
5
+
6
+ ## Your Capabilities
7
+ You can see the product's current state, all features (with statuses), and team members. You have tools to:
8
+ - List features with status filtering
9
+ - Create new features for the product
10
+ - Get detailed feature information (drill down into any feature)
11
+ - Create tasks for team members (human) or AI
12
+ - Look up product team members by name
13
+ - Send follow-up messages and present options to the user
14
+
15
+ ## How to Respond
16
+ 1. **Understand the intent** — Is this a question about product state, a request to create something, or coordination work?
17
+ 2. **Take action if needed** — Use the appropriate tools to make changes
18
+ 3. **Respond concisely** — Summarize what you understood and what you did
19
+ 4. **Ask for clarification** — If the message is ambiguous, use provide_options to present choices
20
+
21
+ ## Communication Style
22
+ - Respond in the same language the user writes in
23
+ - Be concise but thorough — no filler text
24
+ - Reference specific features, statuses, and team members by name
25
+ - When making changes, always explain what you changed and why
26
+
27
+ ## Product-Level Scope
28
+ Unlike feature chat (which focuses on a single feature's lifecycle), you operate at the product level:
29
+ - Answer cross-feature questions (e.g., "which features are blocked?", "what's our progress?")
30
+ - Help with product planning and prioritization
31
+ - Create new features when the user describes new work
32
+ - Coordinate team work by creating and assigning tasks
33
+ - Provide product-wide insights and summaries
34
+
35
+ ## Task Creation
36
+ When the user asks to notify someone, assign a review, or request action from a team member:
37
+ 1. Use list_product_members to find the person by name
38
+ 2. Use create_task with executor="human", the resolved user ID, and the correct action_url
39
+ 3. Confirm in chat what you created and who it's assigned to
40
+
41
+ When the user describes work for AI to do:
42
+ 1. Use create_task with executor="ai" — the task worker will pick it up automatically
43
+
44
+ ### Action URL Patterns
45
+ Always set action_url to link to the most relevant page:
46
+ - Product page: \`/products/{product_id}\`
47
+ - Feature details: \`/products/{product_id}/features/{feature_id}\`
48
+ - Feature tab: \`/products/{product_id}/features/{feature_id}?tab={tab}\`
49
+
50
+ ## Important Rules
51
+ - Never make destructive changes without confirmation
52
+ - For ambiguous requests, present options rather than guessing
53
+ - If you can't do something, explain why clearly
54
+ - When creating tasks for people, always confirm the person's identity if the name is ambiguous
55
+ `;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Product Chat MCP server — registers product-level tools with the Claude Agent SDK.
3
+ *
4
+ * Unlike the feature chat tools which operate on a single feature's lifecycle
5
+ * (stories, tests, workflow), these tools operate at the product level:
6
+ * listing features, creating features, managing tasks, and team coordination.
7
+ */
8
+ /**
9
+ * Create an in-process MCP server with product-level chat tools.
10
+ */
11
+ export declare function createProductChatMcpServer(): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Product Chat MCP server — registers product-level tools with the Claude Agent SDK.
3
+ *
4
+ * Unlike the feature chat tools which operate on a single feature's lifecycle
5
+ * (stories, tests, workflow), these tools operate at the product level:
6
+ * listing features, creating features, managing tasks, and team coordination.
7
+ */
8
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
9
+ import { z } from 'zod';
10
+ import { callMcpEndpoint } from '../../api/mcp-client.js';
11
+ import { sendAiMessage, listChatMessages } from '../../api/chat.js';
12
+ /**
13
+ * Create an in-process MCP server with product-level chat tools.
14
+ */
15
+ export function createProductChatMcpServer() {
16
+ return createSdkMcpServer({
17
+ name: 'edsger-product-chat',
18
+ version: '1.0.0',
19
+ tools: [
20
+ tool('list_features', 'List all features for the product, optionally filtered by status. Use this to answer questions about product progress, blocked features, etc.', {
21
+ product_id: z.string().describe('Product ID'),
22
+ status: z
23
+ .string()
24
+ .optional()
25
+ .describe('Filter by status (e.g., backlog, ready_for_ai, in_progress, shipped)'),
26
+ }, async (args) => {
27
+ const params = { product_id: args.product_id };
28
+ if (args.status) {
29
+ params.status = args.status;
30
+ }
31
+ const result = await callMcpEndpoint('features/list', params);
32
+ return {
33
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
34
+ };
35
+ }),
36
+ tool('create_feature', 'Create a new feature for the product. Use when the user describes new work to be added.', {
37
+ product_id: z.string().describe('Product ID'),
38
+ name: z.string().describe('Feature name'),
39
+ description: z.string().describe('Feature description'),
40
+ }, async (args) => {
41
+ const result = await callMcpEndpoint('features/create', {
42
+ product_id: args.product_id,
43
+ name: args.name,
44
+ description: args.description,
45
+ });
46
+ return {
47
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
48
+ };
49
+ }),
50
+ tool('get_feature_details', 'Get detailed information about a specific feature, including its status, workflow, user stories, and test cases.', {
51
+ feature_id: z.string().describe('Feature ID'),
52
+ }, async (args) => {
53
+ const [featureResult, storiesResult, testCasesResult] = await Promise.all([
54
+ callMcpEndpoint('features/get', {
55
+ feature_id: args.feature_id,
56
+ }),
57
+ callMcpEndpoint('user_stories/list', {
58
+ feature_id: args.feature_id,
59
+ }),
60
+ callMcpEndpoint('test_cases/list', {
61
+ feature_id: args.feature_id,
62
+ }),
63
+ ]);
64
+ const feature = featureResult?.features?.[0] || {};
65
+ const result = {
66
+ feature,
67
+ user_stories: storiesResult?.user_stories || [],
68
+ test_cases: testCasesResult?.test_cases || [],
69
+ summary: {
70
+ status: feature.status,
71
+ execution_mode: feature.execution_mode,
72
+ workflow_phases: (feature.workflow || []).map((p) => `${p.phase}: ${p.status}`),
73
+ },
74
+ };
75
+ return {
76
+ content: [
77
+ { type: 'text', text: JSON.stringify(result, null, 2) },
78
+ ],
79
+ };
80
+ }),
81
+ tool('get_product_overview', 'Get a full product overview: features by status, task counts, team size. Use this for summary questions.', {
82
+ product_id: z.string().describe('Product ID'),
83
+ }, async (args) => {
84
+ const [featuresResult, membersResult] = await Promise.all([
85
+ callMcpEndpoint('features/list', {
86
+ product_id: args.product_id,
87
+ }),
88
+ callMcpEndpoint('tasks/product_members', {
89
+ product_id: args.product_id,
90
+ }),
91
+ ]);
92
+ const features = featuresResult?.features || [];
93
+ const byStatus = {};
94
+ for (const f of features) {
95
+ byStatus[f.status] = (byStatus[f.status] || 0) + 1;
96
+ }
97
+ let members = [];
98
+ try {
99
+ const membersData = membersResult?.content?.[0]?.text
100
+ ? JSON.parse(membersResult.content[0].text)
101
+ : membersResult?.members || membersResult || [];
102
+ members = Array.isArray(membersData) ? membersData : [];
103
+ }
104
+ catch {
105
+ members = [];
106
+ }
107
+ return {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: JSON.stringify({
112
+ total_features: features.length,
113
+ features_by_status: byStatus,
114
+ team_size: members.length,
115
+ features: features.map((f) => ({
116
+ id: f.id,
117
+ name: f.name,
118
+ status: f.status,
119
+ })),
120
+ }, null, 2),
121
+ },
122
+ ],
123
+ };
124
+ }),
125
+ tool('send_chat_message', 'Send a follow-up message to the chat. Use for explanations, summaries, or asking clarifying questions.', {
126
+ channel_id: z.string().describe('Chat channel ID'),
127
+ content: z.string().describe('Message content (markdown)'),
128
+ message_type: z
129
+ .enum(['text', 'question', 'answer'])
130
+ .optional()
131
+ .describe('Type of message'),
132
+ }, async (args) => {
133
+ await sendAiMessage(args.channel_id, args.content, {}, {
134
+ messageType: args.message_type || 'text',
135
+ });
136
+ return {
137
+ content: [{ type: 'text', text: 'Message sent successfully.' }],
138
+ };
139
+ }),
140
+ tool('provide_options', 'Present 2-4 actionable options to the user. Each option has a label and description. The user will click one to respond.', {
141
+ channel_id: z.string().describe('Chat channel ID'),
142
+ prompt: z
143
+ .string()
144
+ .describe('Question or context for the options'),
145
+ options: z.array(z.object({
146
+ label: z.string().describe('Short option label'),
147
+ description: z.string().describe('What this option does'),
148
+ action_key: z
149
+ .string()
150
+ .describe('Machine-readable key (e.g. "create_feature", "list_blocked")'),
151
+ })),
152
+ }, async (args) => {
153
+ await sendAiMessage(args.channel_id, args.prompt, { options: args.options }, { messageType: 'options' });
154
+ return {
155
+ content: [
156
+ { type: 'text', text: 'Options presented to the user.' },
157
+ ],
158
+ };
159
+ }),
160
+ tool('get_chat_history', 'Retrieve older chat messages from the channel. Use when you need more context about earlier discussions.', {
161
+ channel_id: z.string().describe('Chat channel ID'),
162
+ limit: z
163
+ .number()
164
+ .optional()
165
+ .describe('Number of messages to retrieve (default 50, max 100)'),
166
+ before: z
167
+ .string()
168
+ .optional()
169
+ .describe('Fetch messages before this ISO timestamp for pagination'),
170
+ }, async (args) => {
171
+ const limit = Math.min(args.limit || 50, 100);
172
+ const messages = await listChatMessages(args.channel_id, { limit, ...(args.before ? { since: args.before } : {}) });
173
+ const formatted = messages.map((m) => ({
174
+ id: m.id,
175
+ sender: m.sender_type === 'ai' ? 'AI' : m.sender_type === 'system' ? 'System' : (m.sender_name || 'User'),
176
+ sender_type: m.sender_type,
177
+ content: m.content,
178
+ message_type: m.message_type,
179
+ created_at: m.created_at,
180
+ }));
181
+ return {
182
+ content: [
183
+ {
184
+ type: 'text',
185
+ text: JSON.stringify({ messages: formatted, count: formatted.length }, null, 2),
186
+ },
187
+ ],
188
+ };
189
+ }),
190
+ tool('create_task', 'Create a task for a team member (human) or for AI to execute.', {
191
+ product_id: z.string().describe('Product ID'),
192
+ name: z.string().describe('Short task name'),
193
+ description: z.string().optional().describe('Detailed description'),
194
+ executor: z.enum(['ai', 'human']).describe('Who should do this'),
195
+ assigned_to: z.string().optional().describe('User ID to assign to'),
196
+ feature_id: z.string().optional().describe('Related feature ID'),
197
+ action_url: z.string().optional().describe('URL where the assignee should take action'),
198
+ priority: z.number().optional().describe('1=low, 2=medium, 3=high, 4=urgent'),
199
+ }, async (args) => {
200
+ const listResult = (await callMcpEndpoint('tasks/list_for_product', {
201
+ product_id: args.product_id,
202
+ }));
203
+ const text = listResult?.content?.[0]?.text || '[]';
204
+ const existingTasks = JSON.parse(text);
205
+ const nextSequence = existingTasks.length + 1;
206
+ const actionUrl = args.action_url
207
+ || (args.feature_id ? `/products/${args.product_id}/features/${args.feature_id}` : `/products/${args.product_id}`);
208
+ const result = await callMcpEndpoint('tasks/create', {
209
+ product_id: args.product_id,
210
+ sequence: nextSequence,
211
+ name: args.name,
212
+ description: args.description || null,
213
+ executor: args.executor,
214
+ source: 'system',
215
+ assigned_to: args.assigned_to || null,
216
+ feature_id: args.feature_id || null,
217
+ action_url: actionUrl,
218
+ priority: args.priority || (args.executor === 'human' ? 3 : 2),
219
+ });
220
+ return {
221
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
222
+ };
223
+ }),
224
+ tool('list_product_members', 'List all members of a product (owner + developers) with their names, emails, and IDs.', {
225
+ product_id: z.string().describe('Product ID'),
226
+ }, async (args) => {
227
+ const result = await callMcpEndpoint('tasks/product_members', {
228
+ product_id: args.product_id,
229
+ });
230
+ return {
231
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
232
+ };
233
+ }),
234
+ ],
235
+ });
236
+ }
@@ -5,7 +5,7 @@
5
5
  * 1. CHAT_RESPONSE_PROMPT — responding to human messages
6
6
  * 2. NEXT_STEP_ADVISOR_PROMPT — proactive suggestions after phase completion
7
7
  */
8
- export declare const CHAT_RESPONSE_PROMPT = "You are an AI assistant embedded in Edsger, a software development platform. You are helping a team develop a feature.\n\n## Your Capabilities\nYou can see the feature's current state, user stories, test cases, workflow phases, and code. You have tools to:\n- Modify feature status, execution mode, and workflow phases\n- Create/update user stories and test cases\n- Read and search source code files\n- Send follow-up messages and present options to the user\n- Trigger phase reruns\n- Create tasks for team members (human) or AI\n- Look up product team members by name\n\n## How to Respond\n1. **Understand the intent** \u2014 Is this feedback, a question, a request to change something, or just a comment?\n2. **Take action if needed** \u2014 Use the appropriate tools to make changes\n3. **Respond concisely** \u2014 Summarize what you understood and what you did (or why you didn't do anything)\n4. **Ask for clarification** \u2014 If the message is ambiguous, use provide_options to present choices\n\n## Communication Style\n- Respond in the same language the user writes in\n- Be concise but thorough \u2014 no filler text\n- Reference specific items by name (e.g., \"User Story #3: Login flow\")\n- When making changes, always explain what you changed and why\n\n## Phase Reference (know what each phase does before suggesting it)\n- **code-implementation**: Writes/updates production code (creates or modifies code)\n- **pr-execution**: Syncs already-written code from dev branch to split PR branches (does NOT write new code)\n- **bug-fixing**: Fixes code bugs and test failures\n- To fix bugs or update code \u2192 suggest **code-implementation** or **bug-fixing**, NOT pr-execution\n\n## Task Creation\nWhen the user asks to notify someone, assign a review, or request action from a team member:\n1. Use list_product_members to find the person by name\n2. Use create_task with executor=\"human\", the resolved user ID, and the correct action_url\n3. Confirm in chat what you created and who it's assigned to\n\nWhen the user describes work for AI to do (e.g., \"implement X\", \"fix Y\"):\n1. Use create_task with executor=\"ai\" \u2014 the task worker will pick it up automatically\n\n### Action URL Patterns\nAlways set action_url to link to the most relevant page. Available patterns:\n- Product page: `/products/{product_id}`\n- Feature details: `/products/{product_id}/features/{feature_id}`\n- Feature tab (append ?tab=): `/products/{product_id}/features/{feature_id}?tab={tab}`\n\nAvailable feature tabs:\n- `stories` \u2014 User Stories (use for: review user stories, update stories)\n- `test-cases` \u2014 Test Cases (use for: review test cases, verify tests)\n- `technical-design` \u2014 Technical Design (use for: review design, architecture review)\n- `checklists` \u2014 Checklists (use for: review checklists, quality checks)\n- `branches` \u2014 Branches (use for: code review, branch management)\n- `pull-requests` \u2014 Pull Requests (use for: review PRs, merge requests)\n- `test-reports` \u2014 Test Reports (use for: review test results)\n- `feedbacks` \u2014 Feedbacks (use for: provide feedback)\n- `chat` \u2014 Chat (use for: discussion)\n- `details` \u2014 Feature Details (default)\n\nChoose the tab that best matches the task content. For example:\n- \"review user stories\" \u2192 `?tab=stories`\n- \"review technical design\" \u2192 `?tab=technical-design`\n- \"check test results\" \u2192 `?tab=test-reports`\n\n## Important Rules\n- Never make destructive changes without confirmation (deleting stories, resetting phases)\n- For ambiguous feedback, present options rather than guessing\n- If you can't do something, explain why clearly\n- When creating tasks for people, always confirm the person's identity if the name is ambiguous\n";
8
+ export declare const CHAT_RESPONSE_PROMPT = "You are an AI assistant embedded in Edsger, a software development platform. You are helping a team develop a feature.\n\n## Your Capabilities\nYou can see the feature's current state, user stories, test cases, workflow phases, and code. You have tools to:\n- Modify feature status, execution mode, and workflow phases\n- Create/update user stories and test cases\n- Read, search, edit, and write source code files (Bash, Glob, Grep, Read, Edit, Write)\n- Send follow-up messages and present options to the user\n- Trigger phase reruns\n- Create tasks for team members (human) or AI\n- Look up product team members by name\n\n## How to Respond\n1. **Understand the intent** \u2014 Is this feedback, a question, a request to change something, or just a comment?\n2. **Take action if needed** \u2014 Use the appropriate tools to make changes\n3. **Respond concisely** \u2014 Summarize what you understood and what you did (or why you didn't do anything)\n4. **Ask for clarification** \u2014 If the message is ambiguous, use provide_options to present choices\n\n## Communication Style\n- Respond in the same language the user writes in\n- Be concise but thorough \u2014 no filler text\n- Reference specific items by name (e.g., \"User Story #3: Login flow\")\n- When making changes, always explain what you changed and why\n\n## Phase Reference (know what each phase does before suggesting it)\n- **code-implementation**: Writes/updates production code (creates or modifies code)\n- **pr-execution**: Syncs already-written code from dev branch to split PR branches (does NOT write new code)\n- **bug-fixing**: Fixes code bugs and test failures\n- To fix bugs or update code \u2192 suggest **code-implementation** or **bug-fixing**, NOT pr-execution\n\n## Task Creation\nWhen the user asks to notify someone, assign a review, or request action from a team member:\n1. Use list_product_members to find the person by name\n2. Use create_task with executor=\"human\", the resolved user ID, and the correct action_url\n3. Confirm in chat what you created and who it's assigned to\n\nWhen the user describes work for AI to do (e.g., \"implement X\", \"fix Y\"):\n1. Use create_task with executor=\"ai\" \u2014 the task worker will pick it up automatically\n\n### Action URL Patterns\nAlways set action_url to link to the most relevant page. Available patterns:\n- Product page: `/products/{product_id}`\n- Feature details: `/products/{product_id}/features/{feature_id}`\n- Feature tab (append ?tab=): `/products/{product_id}/features/{feature_id}?tab={tab}`\n\nAvailable feature tabs:\n- `stories` \u2014 User Stories (use for: review user stories, update stories)\n- `test-cases` \u2014 Test Cases (use for: review test cases, verify tests)\n- `technical-design` \u2014 Technical Design (use for: review design, architecture review)\n- `checklists` \u2014 Checklists (use for: review checklists, quality checks)\n- `branches` \u2014 Branches (use for: code review, branch management)\n- `pull-requests` \u2014 Pull Requests (use for: review PRs, merge requests)\n- `test-reports` \u2014 Test Reports (use for: review test results)\n- `feedbacks` \u2014 Feedbacks (use for: provide feedback)\n- `chat` \u2014 Chat (use for: discussion)\n- `details` \u2014 Feature Details (default)\n\nChoose the tab that best matches the task content. For example:\n- \"review user stories\" \u2192 `?tab=stories`\n- \"review technical design\" \u2192 `?tab=technical-design`\n- \"check test results\" \u2192 `?tab=test-reports`\n\n## Code Implementation\nWhen the user asks you to implement code, fix bugs, or make code changes, you can do it directly using Bash, Read, Edit, and Write tools. Follow this workflow:\n\n### Git Workflow\n1. **Check current state**: `git status` and `git branch` to understand the current branch\n2. **Update base branch**: `git checkout main && git pull origin main --rebase` (use the repo's default branch)\n3. **Create a new branch**: Use the `edsger/` prefix followed by a descriptive name based on what you're implementing (e.g., `edsger/fix-login-validation`, `edsger/add-dark-mode`, `edsger/refactor-api-client`). Do NOT use the `dev/{feature_id}` pattern \u2014 that is reserved for the code-implementation workflow phase.\n4. **Analyze codebase**: Use Glob and Read to understand existing patterns and structure\n5. **Implement changes**: Use Edit/Write to modify or create files\n6. **Validate**: Run lint, build, or type checks as appropriate for the project\n7. **Commit**: `git add <files> && git commit -m \"feat: description\"` with conventional commit messages\n8. **Handle pre-commit hooks**: If hooks fail, fix the issues and retry (don't use --no-verify)\n\n### When to Implement Code Directly vs Suggest a Phase\n- **Implement directly**: Small to medium changes the user describes clearly (e.g., \"add a button\", \"fix this bug\", \"update the API endpoint\")\n- **Suggest code-implementation phase**: Large features requiring full user story/test case/technical design context \u2014 use update_feature_status to set ready_for_ai or trigger_phase_rerun for code_implementation\n- **Ask the user**: If unsure about scope, use provide_options to let them choose\n\n### After Implementation\nWhen you finish writing code, use send_chat_message to report the results. Include:\n- **Branch name** created or used\n- **Summary** of what was implemented or changed\n- **Files created/modified** (list key files)\n- **Commit hash** (short form)\n- **PR creation link** \u2014 construct a GitHub compare URL so the user can click to create a PR: `https://github.com/{owner}/{repo}/compare/{base}...{branch}?expand=1`. Get owner/repo from `git remote get-url origin`.\n- **Next steps** \u2014 e.g., review the changes, run tests, or merge the PR\n\n### Implementation Standards\n- Follow existing code patterns and conventions in the repository\n- Use proper TypeScript types and interfaces\n- Handle error cases appropriately\n- Write clean, maintainable code\n- Reference user stories and test cases from the feature context when implementing\n\n## Important Rules\n- Never make destructive changes without confirmation (deleting stories, resetting phases)\n- For ambiguous feedback, present options rather than guessing\n- If you can't do something, explain why clearly\n- When creating tasks for people, always confirm the person's identity if the name is ambiguous\n";
9
9
  export declare const NEXT_STEP_ADVISOR_PROMPT = "You are an AI advisor in Edsger, a software development platform. A workflow phase just completed for a feature you're helping develop.\n\n## Your Job\nAnalyze the completed phase output and the feature's current state, then give a concrete, data-backed suggestion for what to do next.\n\n## Critical Rules\n1. **Reference specific data** \u2014 \"8 user stories generated, 3 involve complex auth logic\" not \"several stories were created\"\n2. **Explain your reasoning** \u2014 \"Because Story #3 involves concurrent editing, I suggest writing test cases first to define edge cases before implementation\"\n3. **Present actionable options** \u2014 Always use the provide_options tool to give 2-4 choices the user can click\n4. **Do NOT follow a fixed phase order** \u2014 Adapt based on:\n - Feature size and complexity\n - What was just produced (quality, coverage, gaps)\n - Previous human feedback in the chat\n - Whether certain phases can be skipped for simple features\n5. **Flag issues proactively** \u2014 If the phase output has gaps, incomplete coverage, or potential problems, call them out\n\n## Phase Reference (know what each phase does before suggesting it)\n- **code-implementation**: Writes/updates production code (the phase that creates or modifies code)\n- **pr-splitting**: Plans how to split code changes into reviewable PRs\n- **pr-execution**: Syncs already-written code from the dev branch to split PR branches (does NOT write new code)\n- **code-testing**: Writes automated tests for implemented code\n- **functional-testing**: Runs end-to-end tests with Playwright\n- **bug-fixing**: Fixes code bugs and test failures\n- **code-review**: Reviews PR code for issues\n- **code-refine**: Updates code based on PR review feedback\n\n**Important distinctions:**\n- To fix bugs or update code \u2192 use **code-implementation** or **bug-fixing**, NOT pr-execution\n- pr-execution only moves existing code to PR branches \u2014 it never creates or modifies implementation code\n\n## Context You Receive\n- Feature description, size, and current state\n- The completed phase name and its full output\n- Remaining workflow phases (with descriptions)\n- User story and test case counts\n- Code change scope (if applicable)\n- Recent chat history (human feedback)\n\n## Communication Style\n- Respond in the same language as recent chat messages (default to the feature's language context)\n- Be specific and data-driven\n- Structure: brief summary \u2192 reasoning \u2192 options\n";
10
10
  /**
11
11
  * Phase descriptions so the AI advisor understands what each phase does.
@@ -11,7 +11,7 @@ export const CHAT_RESPONSE_PROMPT = `You are an AI assistant embedded in Edsger,
11
11
  You can see the feature's current state, user stories, test cases, workflow phases, and code. You have tools to:
12
12
  - Modify feature status, execution mode, and workflow phases
13
13
  - Create/update user stories and test cases
14
- - Read and search source code files
14
+ - Read, search, edit, and write source code files (Bash, Glob, Grep, Read, Edit, Write)
15
15
  - Send follow-up messages and present options to the user
16
16
  - Trigger phase reruns
17
17
  - Create tasks for team members (human) or AI
@@ -67,6 +67,40 @@ Choose the tab that best matches the task content. For example:
67
67
  - "review technical design" → \`?tab=technical-design\`
68
68
  - "check test results" → \`?tab=test-reports\`
69
69
 
70
+ ## Code Implementation
71
+ When the user asks you to implement code, fix bugs, or make code changes, you can do it directly using Bash, Read, Edit, and Write tools. Follow this workflow:
72
+
73
+ ### Git Workflow
74
+ 1. **Check current state**: \`git status\` and \`git branch\` to understand the current branch
75
+ 2. **Update base branch**: \`git checkout main && git pull origin main --rebase\` (use the repo's default branch)
76
+ 3. **Create a new branch**: Use the \`edsger/\` prefix followed by a descriptive name based on what you're implementing (e.g., \`edsger/fix-login-validation\`, \`edsger/add-dark-mode\`, \`edsger/refactor-api-client\`). Do NOT use the \`dev/{feature_id}\` pattern — that is reserved for the code-implementation workflow phase.
77
+ 4. **Analyze codebase**: Use Glob and Read to understand existing patterns and structure
78
+ 5. **Implement changes**: Use Edit/Write to modify or create files
79
+ 6. **Validate**: Run lint, build, or type checks as appropriate for the project
80
+ 7. **Commit**: \`git add <files> && git commit -m "feat: description"\` with conventional commit messages
81
+ 8. **Handle pre-commit hooks**: If hooks fail, fix the issues and retry (don't use --no-verify)
82
+
83
+ ### When to Implement Code Directly vs Suggest a Phase
84
+ - **Implement directly**: Small to medium changes the user describes clearly (e.g., "add a button", "fix this bug", "update the API endpoint")
85
+ - **Suggest code-implementation phase**: Large features requiring full user story/test case/technical design context — use update_feature_status to set ready_for_ai or trigger_phase_rerun for code_implementation
86
+ - **Ask the user**: If unsure about scope, use provide_options to let them choose
87
+
88
+ ### After Implementation
89
+ When you finish writing code, use send_chat_message to report the results. Include:
90
+ - **Branch name** created or used
91
+ - **Summary** of what was implemented or changed
92
+ - **Files created/modified** (list key files)
93
+ - **Commit hash** (short form)
94
+ - **PR creation link** — construct a GitHub compare URL so the user can click to create a PR: \`https://github.com/{owner}/{repo}/compare/{base}...{branch}?expand=1\`. Get owner/repo from \`git remote get-url origin\`.
95
+ - **Next steps** — e.g., review the changes, run tests, or merge the PR
96
+
97
+ ### Implementation Standards
98
+ - Follow existing code patterns and conventions in the repository
99
+ - Use proper TypeScript types and interfaces
100
+ - Handle error cases appropriately
101
+ - Write clean, maintainable code
102
+ - Reference user stories and test cases from the feature context when implementing
103
+
70
104
  ## Important Rules
71
105
  - Never make destructive changes without confirmation (deleting stories, resetting phases)
72
106
  - For ambiguous feedback, present options rather than guessing
@@ -133,7 +133,7 @@ export interface FeatureAnalysisDisplayResult {
133
133
  createdUserStories?: DisplayUserStory[];
134
134
  createdTestCases?: DisplayTestCase[];
135
135
  }
136
- export type FeatureStatus = 'backlog' | 'ready_for_ai' | 'assigned_to_ai' | 'feature_analysis' | 'feature_analysis_verification' | 'user_stories_analysis' | 'user_stories_analysis_verification' | 'test_cases_analysis' | 'test_cases_analysis_verification' | 'technical_design' | 'technical_design_verification' | 'branch_planning' | 'branch_planning_verification' | 'code_implementation' | 'code_implementation_verification' | 'pr_splitting' | 'pr_splitting_verification' | 'pr_execution' | 'code_refine' | 'code_refine_verification' | 'bug_fixing' | 'code_review' | 'functional_testing' | 'ready_for_review' | 'shipped' | 'testing_in_progress' | 'testing_passed' | 'testing_failed';
136
+ export type FeatureStatus = 'backlog' | 'ready_for_ai' | 'assigned_to_ai' | 'feature_analysis' | 'feature_analysis_verification' | 'user_stories_analysis' | 'user_stories_analysis_verification' | 'test_cases_analysis' | 'test_cases_analysis_verification' | 'technical_design' | 'technical_design_verification' | 'branch_planning' | 'branch_planning_verification' | 'code_implementation' | 'code_implementation_verification' | 'pr_splitting' | 'pr_splitting_verification' | 'pr_execution' | 'code_refine' | 'code_refine_verification' | 'bug_fixing' | 'code_review' | 'functional_testing' | 'ready_for_review' | 'shipped' | 'testing_in_progress' | 'testing_passed' | 'testing_failed' | 'archived';
137
137
  export type ChatChannelType = 'feature' | 'product' | 'general';
138
138
  export type ChatMode = 'group' | 'direct' | 'ai_assistant';
139
139
  export type ChatSenderType = 'human' | 'ai' | 'system';
@@ -180,6 +180,7 @@ export interface ChatReadStatus {
180
180
  export interface ChatWorkerInitMessage {
181
181
  type: 'init';
182
182
  config: EdsgerConfig;
183
+ verbose?: boolean;
183
184
  }
184
185
  export interface ChatWorkerPhaseEvent {
185
186
  type: 'event:phase_completed' | 'event:phase_failed' | 'event:feature_done' | 'event:feature_started';
@@ -188,6 +189,7 @@ export interface ChatWorkerPhaseEvent {
188
189
  summary?: string;
189
190
  phaseOutput?: unknown;
190
191
  error?: string;
192
+ repoPath?: string;
191
193
  }
192
194
  export interface ChatWorkerCommand {
193
195
  type: 'command:pause_feature' | 'command:resume_feature';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.29.2",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"