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.
- package/dist/api/chat.d.ts +18 -0
- package/dist/api/chat.js +41 -0
- package/dist/api/features/status-updater.d.ts +4 -0
- package/dist/api/features/status-updater.js +10 -0
- package/dist/commands/agent-workflow/chat-worker.js +43 -9
- package/dist/commands/agent-workflow/processor.js +2 -2
- package/dist/config/__tests__/feature-status.test.js +2 -2
- package/dist/config/feature-status.js +2 -0
- package/dist/phases/chat-processor/index.d.ts +8 -2
- package/dist/phases/chat-processor/index.js +171 -6
- package/dist/phases/chat-processor/product-context.d.ts +36 -0
- package/dist/phases/chat-processor/product-context.js +104 -0
- package/dist/phases/chat-processor/product-prompts.d.ts +4 -0
- package/dist/phases/chat-processor/product-prompts.js +55 -0
- package/dist/phases/chat-processor/product-tools.d.ts +11 -0
- package/dist/phases/chat-processor/product-tools.js +236 -0
- package/dist/phases/chat-processor/prompts.d.ts +1 -1
- package/dist/phases/chat-processor/prompts.js +35 -1
- package/dist/types/index.d.ts +3 -1
- package/package.json +1 -1
package/dist/api/chat.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) (
|
|
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
|
|
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], '
|
|
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
|
|
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
|
|
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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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';
|