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.
Files changed (118) hide show
  1. package/dist/api/chat.d.ts +18 -0
  2. package/dist/api/chat.js +41 -0
  3. package/dist/api/features/status-updater.d.ts +4 -0
  4. package/dist/api/features/status-updater.js +10 -0
  5. package/dist/commands/agent-workflow/chat-worker.js +32 -7
  6. package/dist/config/__tests__/feature-status.test.js +2 -2
  7. package/dist/config/feature-status.js +2 -0
  8. package/dist/index.js +0 -0
  9. package/dist/phases/chat-processor/index.d.ts +6 -0
  10. package/dist/phases/chat-processor/index.js +164 -0
  11. package/dist/phases/chat-processor/product-context.d.ts +36 -0
  12. package/dist/phases/chat-processor/product-context.js +104 -0
  13. package/dist/phases/chat-processor/product-prompts.d.ts +4 -0
  14. package/dist/phases/chat-processor/product-prompts.js +55 -0
  15. package/dist/phases/chat-processor/product-tools.d.ts +11 -0
  16. package/dist/phases/chat-processor/product-tools.js +236 -0
  17. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
  18. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
  19. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
  20. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
  21. package/dist/services/lifecycle-agent/index.d.ts +24 -0
  22. package/dist/services/lifecycle-agent/index.js +25 -0
  23. package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
  24. package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
  25. package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
  26. package/dist/services/lifecycle-agent/transition-rules.js +184 -0
  27. package/dist/services/lifecycle-agent/types.d.ts +190 -0
  28. package/dist/services/lifecycle-agent/types.js +12 -0
  29. package/dist/types/index.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/.claude/settings.local.json +0 -28
  32. package/.env.local +0 -12
  33. package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
  34. package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
  35. package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
  36. package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
  37. package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
  38. package/dist/commands/workflow/pipeline-runner.js +0 -393
  39. package/dist/commands/workflow/runner.d.ts +0 -26
  40. package/dist/commands/workflow/runner.js +0 -119
  41. package/dist/commands/workflow/workflow-runner.d.ts +0 -26
  42. package/dist/commands/workflow/workflow-runner.js +0 -119
  43. package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
  44. package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
  45. package/dist/phases/code-implementation/analyzer.d.ts +0 -32
  46. package/dist/phases/code-implementation/analyzer.js +0 -629
  47. package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
  48. package/dist/phases/code-implementation/context-fetcher.js +0 -86
  49. package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
  50. package/dist/phases/code-implementation/mcp-server.js +0 -93
  51. package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
  52. package/dist/phases/code-implementation/prompts-improvement.js +0 -108
  53. package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
  54. package/dist/phases/code-implementation-verification/verifier.js +0 -196
  55. package/dist/phases/code-refine/analyzer.d.ts +0 -41
  56. package/dist/phases/code-refine/analyzer.js +0 -561
  57. package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
  58. package/dist/phases/code-refine/context-fetcher.js +0 -423
  59. package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
  60. package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
  61. package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
  62. package/dist/phases/code-refine-verification/verifier.js +0 -597
  63. package/dist/phases/code-review/analyzer.d.ts +0 -29
  64. package/dist/phases/code-review/analyzer.js +0 -363
  65. package/dist/phases/code-review/context-fetcher.d.ts +0 -92
  66. package/dist/phases/code-review/context-fetcher.js +0 -296
  67. package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
  68. package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
  69. package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
  70. package/dist/phases/feature-analysis/analyzer.js +0 -208
  71. package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
  72. package/dist/phases/feature-analysis/context-fetcher.js +0 -134
  73. package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
  74. package/dist/phases/feature-analysis/http-fallback.js +0 -95
  75. package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
  76. package/dist/phases/feature-analysis/mcp-server.js +0 -144
  77. package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
  78. package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
  79. package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
  80. package/dist/phases/feature-analysis-verification/verifier.js +0 -147
  81. package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
  82. package/dist/phases/technical-design/analyzer-helpers.js +0 -39
  83. package/dist/phases/technical-design/analyzer.d.ts +0 -21
  84. package/dist/phases/technical-design/analyzer.js +0 -461
  85. package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
  86. package/dist/phases/technical-design/context-fetcher.js +0 -39
  87. package/dist/phases/technical-design/http-fallback.d.ts +0 -17
  88. package/dist/phases/technical-design/http-fallback.js +0 -151
  89. package/dist/phases/technical-design/mcp-server.d.ts +0 -1
  90. package/dist/phases/technical-design/mcp-server.js +0 -157
  91. package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
  92. package/dist/phases/technical-design/prompts-improvement.js +0 -93
  93. package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
  94. package/dist/phases/technical-design-verification/verifier.js +0 -170
  95. package/dist/services/feature-branches.d.ts +0 -77
  96. package/dist/services/feature-branches.js +0 -205
  97. package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
  98. package/dist/workflow-runner/config/phase-configs.js +0 -120
  99. package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
  100. package/dist/workflow-runner/core/feature-filter.js +0 -46
  101. package/dist/workflow-runner/core/index.d.ts +0 -8
  102. package/dist/workflow-runner/core/index.js +0 -12
  103. package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
  104. package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
  105. package/dist/workflow-runner/core/state-manager.d.ts +0 -24
  106. package/dist/workflow-runner/core/state-manager.js +0 -42
  107. package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
  108. package/dist/workflow-runner/core/workflow-logger.js +0 -65
  109. package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
  110. package/dist/workflow-runner/executors/phase-executor.js +0 -248
  111. package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
  112. package/dist/workflow-runner/feature-workflow-runner.js +0 -119
  113. package/dist/workflow-runner/index.d.ts +0 -2
  114. package/dist/workflow-runner/index.js +0 -2
  115. package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
  116. package/dist/workflow-runner/pipeline-runner.js +0 -393
  117. package/dist/workflow-runner/workflow-processor.d.ts +0 -54
  118. package/dist/workflow-runner/workflow-processor.js +0 -170
@@ -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
- const channels = await listChannels('feature');
179
+ // Fetch feature and product channels in parallel
180
+ const [featureChannels, productChannels] = await Promise.all([
181
+ listChannels('feature'),
182
+ listChannels('product'),
183
+ ]);
165
184
  let added = 0;
166
- for (const channel of channels) {
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) (total: ${activeChannels.size})`);
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 shipped', () => {
9
+ it('should start with backlog and end with archived', () => {
10
10
  assert.strictEqual(STATUS_PROGRESSION_ORDER[0], 'backlog', 'First status should be backlog');
11
- assert.strictEqual(STATUS_PROGRESSION_ORDER[STATUS_PROGRESSION_ORDER.length - 1], 'shipped', 'Last status should be shipped');
11
+ assert.strictEqual(STATUS_PROGRESSION_ORDER[STATUS_PROGRESSION_ORDER.length - 1], 'archived', 'Last status should be archived');
12
12
  });
13
13
  it('should be readonly', () => {
14
14
  // This test ensures the configuration is properly typed as readonly
@@ -49,6 +49,7 @@ export const STATUS_PROGRESSION_ORDER = [
49
49
  'testing_failed',
50
50
  'ready_for_review',
51
51
  'shipped',
52
+ 'archived',
52
53
  ];
53
54
  /**
54
55
  * Phase to status mapping
@@ -119,6 +120,7 @@ export const HUMAN_SELECTABLE_STATUSES = [
119
120
  'functional_testing',
120
121
  'ready_for_review',
121
122
  'shipped',
123
+ 'archived',
122
124
  ];
123
125
  /**
124
126
  * Check if a status can be manually selected by a human user
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;