@auto-engineer/server-implementer 0.11.9 → 0.11.11

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/package.json CHANGED
@@ -17,8 +17,8 @@
17
17
  "debug": "^4.3.4",
18
18
  "fast-glob": "^3.3.3",
19
19
  "vite": "^5.4.1",
20
- "@auto-engineer/ai-gateway": "0.11.9",
21
- "@auto-engineer/message-bus": "0.11.9"
20
+ "@auto-engineer/ai-gateway": "0.11.11",
21
+ "@auto-engineer/message-bus": "0.11.11"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/fs-extra": "^11.0.4",
@@ -28,9 +28,9 @@
28
28
  "glob": "^11.0.3",
29
29
  "tsx": "^4.20.3",
30
30
  "typescript": "^5.8.3",
31
- "@auto-engineer/cli": "0.11.9"
31
+ "@auto-engineer/cli": "0.11.11"
32
32
  },
33
- "version": "0.11.9",
33
+ "version": "0.11.11",
34
34
  "scripts": {
35
35
  "build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
36
36
  "test": "vitest run --reporter=dot",
@@ -88,7 +88,6 @@ function extractCodeBlock(text: string): string {
88
88
  .trim();
89
89
  }
90
90
 
91
- // Load all TypeScript files from the slice directory
92
91
  async function loadContextFiles(sliceDir: string): Promise<Record<string, string>> {
93
92
  const files = await fg(['*.ts'], { cwd: sliceDir });
94
93
  const context: Record<string, string> = {};
@@ -96,36 +95,58 @@ async function loadContextFiles(sliceDir: string): Promise<Record<string, string
96
95
  const absPath = path.join(sliceDir, file);
97
96
  context[file] = await readFile(absPath, 'utf-8');
98
97
  }
98
+
99
+ const sharedTypesPath = path.resolve(sliceDir, '../../../shared/types.ts');
100
+ if (existsSync(sharedTypesPath)) {
101
+ const sharedTypesContent = await readFile(sharedTypesPath, 'utf-8');
102
+ context['domain-shared-types.ts'] = sharedTypesContent;
103
+ }
104
+
99
105
  return context;
100
106
  }
101
107
 
102
- // Find files that need implementation
103
- function findFilesToImplement(contextFiles: Record<string, string>, hasErrors: boolean): Array<[string, string]> {
104
- if (hasErrors) {
105
- return Object.entries(contextFiles);
108
+ const IMPLEMENTATION_MARKER = '// @auto-implement';
109
+
110
+ function hasImplementationMarker(content: string): boolean {
111
+ return content.includes(IMPLEMENTATION_MARKER);
112
+ }
113
+
114
+ function addImplementationMarker(content: string): string {
115
+ if (hasImplementationMarker(content)) {
116
+ return content;
106
117
  }
107
- return Object.entries(contextFiles).filter(
108
- ([, content]) => content.includes('TODO:') || content.includes('IMPLEMENTATION INSTRUCTIONS'),
118
+ return `${IMPLEMENTATION_MARKER}\n${content}`;
119
+ }
120
+
121
+ function needsImplementation(content: string): boolean {
122
+ return (
123
+ hasImplementationMarker(content) || content.includes('TODO:') || content.includes('IMPLEMENTATION INSTRUCTIONS')
109
124
  );
110
125
  }
111
126
 
112
- // System prompt for AI implementation
127
+ function findFilesToImplement(contextFiles: Record<string, string>): Array<[string, string]> {
128
+ return Object.entries(contextFiles).filter(([, content]) => hasImplementationMarker(content));
129
+ }
130
+
113
131
  const SYSTEM_PROMPT = `
114
132
  You are a software engineer implementing missing logic in a sliced event-driven TypeScript server. Each slice contains partially scaffolded code, and your task is to complete the logic following implementation instructions embedded in each file.
115
133
 
116
134
  Project Characteristics:
117
135
  - Architecture: sliced event-sourced CQRS (Command, Query, Reaction slices)
118
- - Language: TypeScript with type-graphql and Emmett
136
+ - Language: TypeScript with type-graphql and @event-driven-io/emmett
119
137
  - Each slice has scaffolded files with implementation instructions clearly marked with comments (e.g., '## IMPLEMENTATION INSTRUCTIONS ##') or TODOs.
120
138
  - Tests (e.g., *.specs.ts) must pass.
121
139
  - Type errors are not allowed.
140
+ - The domain uses shared enums defined in domain/shared/types.ts for type-safe values. When a field type is an enum (e.g., Status), you MUST use enum constants (e.g., Status.IN_PROGRESS) instead of string literals (e.g., 'in_progress').
122
141
 
123
142
  Your Goal:
124
143
  - Read the implementation instructions from the provided file.
125
144
  - Generate only the code needed to fulfill the instructions, nothing extra and provide back the whole file without the instructions.
126
145
  - Maintain immutability and adhere to functional best practices.
127
146
  - Use only the types and domain constructs already present in the slice.
147
+ - CRITICAL: When a field has an enum type (e.g., status: Status), you MUST use the enum constant (e.g., Status.IN_PROGRESS) NOT a string literal (e.g., 'in_progress'). Check domain-shared-types.ts for the exact enum constant names.
128
148
  - Do not remove existing imports or types that are still referenced or required in the file.
149
+ - Preserve index signatures like [key: string]: unknown as they are required for TypeScript compatibility.
129
150
  - Return the entire updated file, not just the modified parts and remove any TODO comments or instructions after implementing the logic
130
151
 
131
152
  Key rules:
@@ -138,24 +159,61 @@ Key rules:
138
159
  - All code must be TypeScript compliant and follow functional patterns.
139
160
  - If a test exists, make it pass.
140
161
  - Keep implementations minimal and idiomatic.
162
+ - CRITICAL: When assigning values to enum-typed fields, use the enum constant name from domain-shared-types.ts. For example, if Status enum has IN_PROGRESS = 'in_progress', use Status.IN_PROGRESS not 'in_progress'.
141
163
 
142
164
  Avoid:
143
165
  - Adding new dependencies.
144
166
  - Refactoring unrelated code.
145
167
  - Changing the structure of already scaffolded files unless instructed.
168
+ - Using string literals for enum-typed fields. ALWAYS use the enum constant from domain-shared-types.ts (e.g., if status field type is Status and Status enum defines IN_PROGRESS = 'in_progress', use Status.IN_PROGRESS not the string 'in_progress').
146
169
 
147
170
  You will receive:
148
171
  - The path of the file to implement.
149
172
  - The current contents of the file, with instruction comments.
150
173
  - Other relevant files from the same slice (e.g., types, test, state, etc.).
174
+ - Shared domain types including enum definitions (domain-shared-types.ts).
151
175
 
152
176
  You must:
153
177
  - Return the entire updated file (no commentary and remove all implementation instructions).
154
178
  - Ensure the output is valid TypeScript.
179
+ - Use enum constants from domain-shared-types.ts when appropriate.
180
+ `;
181
+
182
+ function extractEnumExamples(sharedTypesContent: string): string {
183
+ const enumMatches = sharedTypesContent.matchAll(/export enum (\w+) \{([^}]+)\}/g);
184
+ const examples: string[] = [];
185
+
186
+ for (const match of enumMatches) {
187
+ const enumName = match[1];
188
+ const enumBody = match[2];
189
+ const firstConstant = enumBody.match(/\s*(\w+)\s*=\s*['"]([^'"]+)['"]/);
190
+
191
+ if (firstConstant !== null) {
192
+ const constantName = firstConstant[1];
193
+ const constantValue = firstConstant[2];
194
+ examples.push(` - ${enumName}.${constantName} (NOT '${constantValue}')`);
195
+ }
196
+ }
197
+
198
+ if (examples.length === 0) {
199
+ return '';
200
+ }
201
+
202
+ return `
203
+ 📌 CRITICAL: Enum Usage Examples from Your Context:
204
+ ${examples.join('\n')}
205
+
206
+ Pattern: Always use EnumName.CONSTANT_NAME for enum-typed fields.
207
+ Never use string literals like 'pending' or 'Pending' when an enum constant exists.
155
208
  `;
209
+ }
156
210
 
157
- // Build prompt for initial implementation
158
211
  function buildInitialPrompt(targetFile: string, context: Record<string, string>): string {
212
+ const sharedTypes = context['domain-shared-types.ts'];
213
+ const sliceFiles = Object.entries(context).filter(
214
+ ([name]) => name !== targetFile && name !== 'domain-shared-types.ts',
215
+ );
216
+
159
217
  return `
160
218
  ${SYSTEM_PROMPT}
161
219
 
@@ -164,20 +222,32 @@ ${SYSTEM_PROMPT}
164
222
 
165
223
  ${context[targetFile]}
166
224
 
167
- ---
225
+ ${
226
+ sharedTypes !== undefined
227
+ ? `---
228
+ 📦 Shared domain types (available via import from '../../../shared'):
229
+ ${sharedTypes}
230
+
231
+ ${extractEnumExamples(sharedTypes)}
232
+ IMPORTANT: Use enum constants (e.g., Status.PENDING) instead of string literals (e.g., 'pending') when working with enum types.
233
+
234
+ `
235
+ : ''
236
+ }---
168
237
  🧠 Other files in the same slice:
169
- ${Object.entries(context)
170
- .filter(([name]) => name !== targetFile)
171
- .map(([name, content]) => `// File: ${name}\n${content}`)
172
- .join('\n\n')}
238
+ ${sliceFiles.map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n')}
173
239
 
174
240
  ---
175
- Return only the whole updated file of ${targetFile}. Do not remove existing imports or types that are still referenced or required in the file. The file returned has to be production ready.
241
+ Return only the whole updated file of ${targetFile}. Do not remove existing imports or types that are still referenced or required in the file. The file returned has to be production ready. Remember to use enum constants from domain-shared-types.ts instead of string literals.
176
242
  `.trim();
177
243
  }
178
244
 
179
- // Build prompt for retry with context
180
245
  function buildRetryPrompt(targetFile: string, context: Record<string, string>, previousOutputs: string): string {
246
+ const sharedTypes = context['domain-shared-types.ts'];
247
+ const sliceFiles = Object.entries(context).filter(
248
+ ([name]) => name !== targetFile && name !== 'domain-shared-types.ts',
249
+ );
250
+
181
251
  return `
182
252
  ${SYSTEM_PROMPT}
183
253
 
@@ -190,18 +260,69 @@ ${previousOutputs}
190
260
 
191
261
  ${context[targetFile]}
192
262
 
263
+ ${
264
+ sharedTypes !== undefined
265
+ ? `---
266
+ 📦 Shared domain types (available via import from '../../../shared'):
267
+ ${sharedTypes}
268
+
269
+ ${extractEnumExamples(sharedTypes)}
270
+ IMPORTANT: Use enum constants (e.g., Status.PENDING) instead of string literals (e.g., 'pending') when working with enum types.
271
+
272
+ `
273
+ : ''
274
+ }---
193
275
  🧠 Other files in the same slice:
194
- ${Object.entries(context)
195
- .filter(([name]) => name !== targetFile)
196
- .map(([name, content]) => `// File: ${name}\n${content}`)
197
- .join('\n\n')}
276
+ ${sliceFiles.map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n')}
198
277
 
199
278
  ---
200
- Return only the corrected full contents of ${targetFile}, no commentary, no markdown.
279
+ Return only the corrected full contents of ${targetFile}, no commentary, no markdown. Remember to use enum constants from domain-shared-types.ts instead of string literals.
201
280
  `.trim();
202
281
  }
203
282
 
204
- // Main implementation function
283
+ async function addMarkersToFiles(slicePath: string, contextFiles: Record<string, string>): Promise<void> {
284
+ const filesToMark = Object.entries(contextFiles).filter(([, content]) => needsImplementation(content));
285
+ debugProcess(`Found ${filesToMark.length} files needing implementation markers`);
286
+
287
+ for (const [filename, content] of filesToMark) {
288
+ if (!hasImplementationMarker(content)) {
289
+ const markedContent = addImplementationMarker(content);
290
+ await writeFile(path.join(slicePath, filename), markedContent, 'utf-8');
291
+ contextFiles[filename] = markedContent;
292
+ debugProcess(`Added implementation marker to ${filename}`);
293
+ }
294
+ }
295
+ }
296
+
297
+ async function implementFile(
298
+ slicePath: string,
299
+ targetFile: string,
300
+ contextFiles: Record<string, string>,
301
+ retryContext?: { previousOutputs?: string; attemptNumber?: number },
302
+ ): Promise<void> {
303
+ debugProcess(`Implementing ${targetFile}`);
304
+
305
+ const previousOutputs = retryContext?.previousOutputs;
306
+ const isRetry = previousOutputs !== undefined && previousOutputs.length > 0;
307
+ const prompt = isRetry
308
+ ? buildRetryPrompt(targetFile, contextFiles, previousOutputs)
309
+ : buildInitialPrompt(targetFile, contextFiles);
310
+
311
+ if (isRetry) {
312
+ debugProcess(`Using retry prompt for attempt #${retryContext?.attemptNumber ?? 2}`);
313
+ }
314
+
315
+ const aiOutput = await generateTextWithAI(prompt, { maxTokens: 8000 });
316
+ let cleanedCode = extractCodeBlock(aiOutput);
317
+ cleanedCode = addImplementationMarker(cleanedCode);
318
+
319
+ const filePath = path.join(slicePath, targetFile);
320
+ await writeFile(filePath, cleanedCode, 'utf-8');
321
+ debugProcess(`Successfully implemented ${targetFile}`);
322
+
323
+ contextFiles[targetFile] = cleanedCode;
324
+ }
325
+
205
326
  async function implementSlice(
206
327
  slicePath: string,
207
328
  context?: { previousOutputs?: string; attemptNumber?: number },
@@ -211,46 +332,23 @@ async function implementSlice(
211
332
  debugProcess(`Implementing slice: ${sliceName}`);
212
333
 
213
334
  try {
214
- // Load all context files
215
335
  const contextFiles = await loadContextFiles(slicePath);
216
336
  debugProcess(`Loaded ${Object.keys(contextFiles).join(', ')} files from slice`);
217
- const hasErrors = Boolean(context?.previousOutputs);
218
- const filesToImplement = findFilesToImplement(contextFiles, hasErrors);
219
- debugProcess(`Found ${filesToImplement.length} files needing implementation`);
220
- const implementedFiles: string[] = [];
337
+
338
+ await addMarkersToFiles(slicePath, contextFiles);
339
+
340
+ const filesToImplement = findFilesToImplement(contextFiles);
341
+ debugProcess(`Found ${filesToImplement.length} files with markers to implement`);
221
342
 
222
343
  if (filesToImplement.length === 0) {
223
- debugProcess('No files with TODO or IMPLEMENTATION INSTRUCTIONS found, and no errors found');
344
+ debugProcess('No files with markers found');
224
345
  return { success: true, filesImplemented: [] };
225
346
  }
226
347
 
227
- // Implement each file that needs it
348
+ const implementedFiles: string[] = [];
228
349
  for (const [targetFile] of filesToImplement) {
229
- debugProcess(`Implementing ${targetFile}`);
230
-
231
- let prompt: string;
232
- if (context !== undefined && context.previousOutputs !== undefined && context.previousOutputs.length > 0) {
233
- // Use retry prompt if we have previous context
234
- prompt = buildRetryPrompt(targetFile, contextFiles, context.previousOutputs);
235
- debugProcess(`Using retry prompt for attempt #${context.attemptNumber ?? 2}`);
236
- } else {
237
- // Use initial prompt for first attempt
238
- prompt = buildInitialPrompt(targetFile, contextFiles);
239
- }
240
-
241
- // Generate implementation with AI
242
- const aiOutput = await generateTextWithAI(prompt);
243
- const cleanedCode = extractCodeBlock(aiOutput);
244
-
245
- // Write the implemented file
246
- const filePath = path.join(slicePath, targetFile);
247
- await writeFile(filePath, cleanedCode, 'utf-8');
248
-
249
- debugProcess(`Successfully implemented ${targetFile}`);
350
+ await implementFile(slicePath, targetFile, contextFiles, context);
250
351
  implementedFiles.push(targetFile);
251
-
252
- // Update context for next file
253
- contextFiles[targetFile] = cleanedCode;
254
352
  }
255
353
 
256
354
  return { success: true, filesImplemented: implementedFiles };
@@ -323,7 +421,6 @@ function logRetryContext(context: { previousOutputs?: string; attemptNumber?: nu
323
421
  export async function handleImplementSliceCommand(
324
422
  command: ImplementSliceCommand,
325
423
  ): Promise<SliceImplementedEvent | SliceImplementationFailedEvent> {
326
- // Handle both 'slicePath' and 'path' parameters for backward compatibility
327
424
  const rawData = command.data as ImplementSliceCommand['data'] & { path?: string };
328
425
  const slicePath = rawData.slicePath ?? rawData.path;
329
426
  const { context } = command.data;
@@ -367,5 +464,4 @@ export async function handleImplementSliceCommand(
367
464
  }
368
465
  }
369
466
 
370
- // Default export is the command handler
371
467
  export default commandHandler;