edsger 0.29.3 → 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 +32 -7
- package/dist/config/__tests__/feature-status.test.js +2 -2
- package/dist/config/feature-status.js +2 -0
- package/dist/index.js +0 -0
- package/dist/phases/chat-processor/index.d.ts +6 -0
- package/dist/phases/chat-processor/index.js +164 -0
- 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/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
- package/dist/services/lifecycle-agent/index.d.ts +24 -0
- package/dist/services/lifecycle-agent/index.js +25 -0
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
- package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
- package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
- package/dist/services/lifecycle-agent/transition-rules.js +184 -0
- package/dist/services/lifecycle-agent/types.d.ts +190 -0
- package/dist/services/lifecycle-agent/types.js +12 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -28
- package/.env.local +0 -12
- package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
- package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
- package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
- package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
- package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
- package/dist/commands/workflow/pipeline-runner.js +0 -393
- package/dist/commands/workflow/runner.d.ts +0 -26
- package/dist/commands/workflow/runner.js +0 -119
- package/dist/commands/workflow/workflow-runner.d.ts +0 -26
- package/dist/commands/workflow/workflow-runner.js +0 -119
- package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
- package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
- package/dist/phases/code-implementation/analyzer.d.ts +0 -32
- package/dist/phases/code-implementation/analyzer.js +0 -629
- package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
- package/dist/phases/code-implementation/context-fetcher.js +0 -86
- package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
- package/dist/phases/code-implementation/mcp-server.js +0 -93
- package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
- package/dist/phases/code-implementation/prompts-improvement.js +0 -108
- package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
- package/dist/phases/code-implementation-verification/verifier.js +0 -196
- package/dist/phases/code-refine/analyzer.d.ts +0 -41
- package/dist/phases/code-refine/analyzer.js +0 -561
- package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
- package/dist/phases/code-refine/context-fetcher.js +0 -423
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
- package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
- package/dist/phases/code-refine-verification/verifier.js +0 -597
- package/dist/phases/code-review/analyzer.d.ts +0 -29
- package/dist/phases/code-review/analyzer.js +0 -363
- package/dist/phases/code-review/context-fetcher.d.ts +0 -92
- package/dist/phases/code-review/context-fetcher.js +0 -296
- package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
- package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
- package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
- package/dist/phases/feature-analysis/analyzer.js +0 -208
- package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
- package/dist/phases/feature-analysis/context-fetcher.js +0 -134
- package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
- package/dist/phases/feature-analysis/http-fallback.js +0 -95
- package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
- package/dist/phases/feature-analysis/mcp-server.js +0 -144
- package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
- package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
- package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
- package/dist/phases/feature-analysis-verification/verifier.js +0 -147
- package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
- package/dist/phases/technical-design/analyzer-helpers.js +0 -39
- package/dist/phases/technical-design/analyzer.d.ts +0 -21
- package/dist/phases/technical-design/analyzer.js +0 -461
- package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
- package/dist/phases/technical-design/context-fetcher.js +0 -39
- package/dist/phases/technical-design/http-fallback.d.ts +0 -17
- package/dist/phases/technical-design/http-fallback.js +0 -151
- package/dist/phases/technical-design/mcp-server.d.ts +0 -1
- package/dist/phases/technical-design/mcp-server.js +0 -157
- package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
- package/dist/phases/technical-design/prompts-improvement.js +0 -93
- package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
- package/dist/phases/technical-design-verification/verifier.js +0 -170
- package/dist/services/feature-branches.d.ts +0 -77
- package/dist/services/feature-branches.js +0 -205
- package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
- package/dist/workflow-runner/config/phase-configs.js +0 -120
- package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
- package/dist/workflow-runner/core/feature-filter.js +0 -46
- package/dist/workflow-runner/core/index.d.ts +0 -8
- package/dist/workflow-runner/core/index.js +0 -12
- package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
- package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
- package/dist/workflow-runner/core/state-manager.d.ts +0 -24
- package/dist/workflow-runner/core/state-manager.js +0 -42
- package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
- package/dist/workflow-runner/core/workflow-logger.js +0 -65
- package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
- package/dist/workflow-runner/executors/phase-executor.js +0 -248
- package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
- package/dist/workflow-runner/feature-workflow-runner.js +0 -119
- package/dist/workflow-runner/index.d.ts +0 -2
- package/dist/workflow-runner/index.js +0 -2
- package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
- package/dist/workflow-runner/pipeline-runner.js +0 -393
- package/dist/workflow-runner/workflow-processor.d.ts +0 -54
- package/dist/workflow-runner/workflow-processor.js +0 -170
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);
|
|
@@ -35,6 +35,8 @@ let pollTimer = null;
|
|
|
35
35
|
const WORKER_ID = `chat-worker-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
36
36
|
// Track active feature channels (featureId -> channelId)
|
|
37
37
|
const activeChannels = new Map();
|
|
38
|
+
// Track active product channels (productId -> channelId)
|
|
39
|
+
const activeProductChannels = new Map();
|
|
38
40
|
// Track feature repo paths (featureId -> repoPath) for setting cwd on AI agent
|
|
39
41
|
const featureRepoPaths = new Map();
|
|
40
42
|
// Poll interval in ms
|
|
@@ -53,20 +55,33 @@ async function pollForMessages() {
|
|
|
53
55
|
if (pollCount % CHANNEL_REFRESH_INTERVAL === 0) {
|
|
54
56
|
await refreshChannels();
|
|
55
57
|
}
|
|
58
|
+
// Poll feature channels
|
|
56
59
|
for (const [featureId, channelId] of activeChannels) {
|
|
57
60
|
try {
|
|
58
|
-
// Atomically claim messages — other workers won't see these
|
|
59
61
|
const claimed = await claimPendingMessages(channelId, WORKER_ID);
|
|
60
62
|
if (claimed.length > 0) {
|
|
61
63
|
log('info', `Claimed ${claimed.length} message(s) for feature ${featureId} (worker: ${WORKER_ID})`);
|
|
62
|
-
// Batch all claimed messages into a single AI session
|
|
63
64
|
const repoPath = featureRepoPaths.get(featureId);
|
|
64
65
|
await processHumanMessages(claimed, featureId, config, verbose, repoPath);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
catch (error) {
|
|
68
69
|
const msg = error instanceof Error ? error.message : String(error);
|
|
69
|
-
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}`);
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
}
|
|
@@ -161,16 +176,26 @@ async function handleFeatureDone(msg) {
|
|
|
161
176
|
*/
|
|
162
177
|
async function refreshChannels() {
|
|
163
178
|
try {
|
|
164
|
-
|
|
179
|
+
// Fetch feature and product channels in parallel
|
|
180
|
+
const [featureChannels, productChannels] = await Promise.all([
|
|
181
|
+
listChannels('feature'),
|
|
182
|
+
listChannels('product'),
|
|
183
|
+
]);
|
|
165
184
|
let added = 0;
|
|
166
|
-
for (const channel of
|
|
185
|
+
for (const channel of featureChannels) {
|
|
167
186
|
if (channel.channel_ref_id && !activeChannels.has(channel.channel_ref_id)) {
|
|
168
187
|
activeChannels.set(channel.channel_ref_id, channel.id);
|
|
169
188
|
added++;
|
|
170
189
|
}
|
|
171
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
|
+
}
|
|
172
197
|
if (added > 0) {
|
|
173
|
-
log('info', `Discovered ${added} new channel(s) (
|
|
198
|
+
log('info', `Discovered ${added} new channel(s) (features: ${activeChannels.size}, products: ${activeProductChannels.size})`);
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
201
|
catch (error) {
|
|
@@ -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
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -27,6 +27,12 @@ export declare function clearChannelSession(channelId: string): void;
|
|
|
27
27
|
* Returns the session ID (callers should track this if needed).
|
|
28
28
|
*/
|
|
29
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.
|
|
@@ -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
|
// ============================================================
|
|
@@ -153,6 +156,109 @@ 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.
|
|
@@ -309,3 +415,61 @@ async function runChatAgent(systemPrompt, userPrompt, config, resumeSessionId, v
|
|
|
309
415
|
toolsUsed: toolUsed,
|
|
310
416
|
};
|
|
311
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;
|