Skip to content

Commit 7ff97a4

Browse files
authored
Merge pull request #26 from AutoMaker-Org/bugfix/appspec-complete-overhaul
Complete overhaul for app spec system. Created logic to auto generate…
2 parents e6e2fa7 + e6a6b4c commit 7ff97a4

14 files changed

Lines changed: 1564 additions & 186 deletions

app/electron/auto-mode-service.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,7 @@ class AutoModeService {
391391
featureId,
392392
"waiting_approval",
393393
projectPath,
394-
null, // no summary
395-
error.message // pass error message
394+
{ error: error.message }
396395
);
397396
} catch (statusError) {
398397
console.error("[AutoMode] Failed to update feature status after error:", statusError);
@@ -495,8 +494,7 @@ class AutoModeService {
495494
featureId,
496495
"waiting_approval",
497496
projectPath,
498-
null, // no summary
499-
error.message // pass error message
497+
{ error: error.message }
500498
);
501499
} catch (statusError) {
502500
console.error("[AutoMode] Failed to update feature status after error:", statusError);
@@ -662,8 +660,7 @@ class AutoModeService {
662660
featureId,
663661
"waiting_approval",
664662
projectPath,
665-
null, // no summary
666-
error.message // pass error message
663+
{ error: error.message }
667664
);
668665
} catch (statusError) {
669666
console.error("[AutoMode] Failed to update feature status after error:", statusError);
@@ -859,8 +856,7 @@ class AutoModeService {
859856
featureId,
860857
"waiting_approval",
861858
projectPath,
862-
null, // no summary
863-
error.message // pass error message
859+
{ error: error.message }
864860
);
865861
} catch (statusError) {
866862
console.error("[AutoMode] Failed to update feature status after error:", statusError);
@@ -1102,8 +1098,7 @@ class AutoModeService {
11021098
featureId,
11031099
"waiting_approval",
11041100
projectPath,
1105-
null, // no summary
1106-
error.message // pass error message
1101+
{ error: error.message }
11071102
);
11081103
} catch (statusError) {
11091104
console.error("[AutoMode] Failed to update feature status after error:", statusError);

app/electron/main.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,7 @@ ipcMain.handle(
898898
featureId,
899899
status,
900900
projectPath,
901-
summary
901+
{ summary }
902902
);
903903

904904
// Notify renderer if window is available
@@ -1170,6 +1170,7 @@ ipcMain.handle("spec-regeneration:status", () => {
11701170
isRunning:
11711171
specRegenerationExecution !== null &&
11721172
specRegenerationExecution.isActive(),
1173+
currentPhase: specRegenerationService.getCurrentPhase(),
11731174
};
11741175
});
11751176

@@ -1234,6 +1235,62 @@ ipcMain.handle(
12341235
}
12351236
);
12361237

1238+
/**
1239+
* Generate features from existing app_spec.txt
1240+
* This allows users to generate features retroactively without regenerating the spec
1241+
*/
1242+
ipcMain.handle(
1243+
"spec-regeneration:generate-features",
1244+
async (_, { projectPath }) => {
1245+
try {
1246+
// Add project path to allowed paths
1247+
addAllowedPath(projectPath);
1248+
1249+
// Check if already running
1250+
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
1251+
return { success: false, error: "Spec regeneration is already running" };
1252+
}
1253+
1254+
// Create execution context
1255+
specRegenerationExecution = {
1256+
abortController: null,
1257+
query: null,
1258+
isActive: () => specRegenerationExecution !== null,
1259+
};
1260+
1261+
const sendToRenderer = (data) => {
1262+
if (mainWindow && !mainWindow.isDestroyed()) {
1263+
mainWindow.webContents.send("spec-regeneration:event", data);
1264+
}
1265+
};
1266+
1267+
// Start generating features (runs in background)
1268+
specRegenerationService
1269+
.generateFeaturesOnly(projectPath, sendToRenderer, specRegenerationExecution)
1270+
.catch((error) => {
1271+
console.error(
1272+
"[IPC] spec-regeneration:generate-features background error:",
1273+
error
1274+
);
1275+
sendToRenderer({
1276+
type: "spec_regeneration_error",
1277+
error: error.message,
1278+
});
1279+
})
1280+
.finally(() => {
1281+
specRegenerationExecution = null;
1282+
});
1283+
1284+
// Return immediately
1285+
return { success: true };
1286+
} catch (error) {
1287+
console.error("[IPC] spec-regeneration:generate-features error:", error);
1288+
specRegenerationExecution = null;
1289+
return { success: false, error: error.message };
1290+
}
1291+
}
1292+
);
1293+
12371294
/**
12381295
* Merge feature worktree changes back to main branch
12391296
*/

app/electron/preload.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
285285
projectDefinition,
286286
}),
287287

288+
// Generate features from existing app_spec.txt
289+
generateFeatures: (projectPath) =>
290+
ipcRenderer.invoke("spec-regeneration:generate-features", {
291+
projectPath,
292+
}),
293+
288294
// Stop regenerating spec
289295
stop: () => ipcRenderer.invoke("spec-regeneration:stop"),
290296

app/electron/services/feature-loader.js

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,38 @@ class FeatureLoader {
170170
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
171171

172172
try {
173+
// Read feature.json directly - handle ENOENT in catch block
174+
// This avoids TOCTOU race condition from checking with fs.access first
173175
const content = await fs.readFile(featureJsonPath, "utf-8");
174176
const feature = JSON.parse(content);
177+
178+
// Validate that the feature has required fields
179+
if (!feature.id) {
180+
console.warn(
181+
`[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping`
182+
);
183+
continue;
184+
}
185+
175186
features.push(feature);
176187
} catch (error) {
177-
console.error(
178-
`[FeatureLoader] Failed to load feature ${featureId}:`,
179-
error
180-
);
188+
// Handle different error types appropriately
189+
if (error.code === "ENOENT") {
190+
// File doesn't exist - this is expected for incomplete feature directories
191+
// Skip silently (feature.json not yet created or was removed)
192+
continue;
193+
} else if (error instanceof SyntaxError) {
194+
// JSON parse error - log as warning since file exists but is malformed
195+
console.warn(
196+
`[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}`
197+
);
198+
} else {
199+
// Other errors - log as error
200+
console.error(
201+
`[FeatureLoader] Failed to load feature ${featureId}:`,
202+
error.message || error
203+
);
204+
}
181205
// Continue loading other features
182206
}
183207
}
@@ -339,30 +363,93 @@ class FeatureLoader {
339363
/**
340364
* Update feature status (legacy API)
341365
* Features are stored in .automaker/features/{id}/feature.json
366+
* Creates the feature if it doesn't exist.
342367
* @param {string} featureId - The ID of the feature to update
343368
* @param {string} status - The new status
344369
* @param {string} projectPath - Path to the project
345-
* @param {string} [summary] - Optional summary of what was done
346-
* @param {string} [error] - Optional error message if feature errored
370+
* @param {Object} options - Options object for optional parameters
371+
* @param {string} [options.summary] - Optional summary of what was done
372+
* @param {string} [options.error] - Optional error message if feature errored
373+
* @param {string} [options.description] - Optional detailed description
374+
* @param {string} [options.category] - Optional category/phase
375+
* @param {string[]} [options.steps] - Optional array of implementation steps
347376
*/
348-
async updateFeatureStatus(featureId, status, projectPath, summary, error) {
377+
async updateFeatureStatus(featureId, status, projectPath, options = {}) {
378+
const { summary, error, description, category, steps } = options;
379+
// Check if feature exists
380+
const existingFeature = await this.get(projectPath, featureId);
381+
382+
if (!existingFeature) {
383+
// Feature doesn't exist - create it with all required fields
384+
console.log(`[FeatureLoader] Feature ${featureId} not found - creating new feature`);
385+
const newFeature = {
386+
id: featureId,
387+
title: featureId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
388+
description: description || summary || '', // Use provided description, fall back to summary
389+
category: category || "Uncategorized",
390+
steps: steps || [],
391+
status: status,
392+
images: [],
393+
imagePaths: [],
394+
skipTests: false, // Auto-generated features should run tests by default
395+
model: "sonnet",
396+
thinkingLevel: "none",
397+
summary: summary || description || '',
398+
createdAt: new Date().toISOString(),
399+
};
400+
if (error !== undefined) {
401+
newFeature.error = error;
402+
}
403+
await this.create(projectPath, newFeature);
404+
console.log(
405+
`[FeatureLoader] Created feature ${featureId}: status=${status}, category=${category || "Uncategorized"}, steps=${steps?.length || 0}${
406+
summary ? `, summary="${summary}"` : ""
407+
}`
408+
);
409+
return;
410+
}
411+
412+
// Feature exists - update it
349413
const updates = { status };
350414
if (summary !== undefined) {
351415
updates.summary = summary;
416+
// Also update description if it's empty or not set
417+
if (!existingFeature.description) {
418+
updates.description = summary;
419+
}
420+
}
421+
if (description !== undefined) {
422+
updates.description = description;
423+
}
424+
if (category !== undefined) {
425+
updates.category = category;
426+
}
427+
if (steps !== undefined && Array.isArray(steps)) {
428+
updates.steps = steps;
352429
}
353430
if (error !== undefined) {
354431
updates.error = error;
355432
} else {
356433
// Clear error if not provided
357-
const feature = await this.get(projectPath, featureId);
358-
if (feature && feature.error) {
434+
if (existingFeature.error) {
359435
updates.error = undefined;
360436
}
361437
}
438+
439+
// Ensure required fields exist (for features created before this fix)
440+
if (!existingFeature.category && !updates.category) updates.category = "Uncategorized";
441+
if (!existingFeature.steps && !updates.steps) updates.steps = [];
442+
if (!existingFeature.images) updates.images = [];
443+
if (!existingFeature.imagePaths) updates.imagePaths = [];
444+
if (existingFeature.skipTests === undefined) updates.skipTests = false;
445+
if (!existingFeature.model) updates.model = "sonnet";
446+
if (!existingFeature.thinkingLevel) updates.thinkingLevel = "none";
362447

363448
await this.update(projectPath, featureId, updates);
364449
console.log(
365450
`[FeatureLoader] Updated feature ${featureId}: status=${status}${
451+
category ? `, category="${category}"` : ""
452+
}${steps ? `, steps=${steps.length}` : ""}${
366453
summary ? `, summary="${summary}"` : ""
367454
}`
368455
);

app/electron/services/mcp-server-factory.js

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,58 @@ class McpServerFactory {
1919
tools: [
2020
tool(
2121
"UpdateFeatureStatus",
22-
"Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.",
22+
"Create or update a feature. Use this tool to create new features with detailed information or update existing feature status. When creating features, provide comprehensive description, category, and implementation steps.",
2323
{
24-
featureId: z.string().describe("The ID of the feature to update"),
25-
status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically."),
26-
summary: z.string().optional().describe("A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: 'Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx'")
24+
featureId: z.string().describe("The ID of the feature (lowercase, hyphens for spaces). Example: 'user-authentication', 'budget-tracking'"),
25+
status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. Use 'backlog' or 'todo' for new features."),
26+
summary: z.string().optional().describe("A brief summary of what was implemented/changed or what the feature does."),
27+
description: z.string().optional().describe("A detailed description of the feature. Be comprehensive - explain what the feature does, its purpose, and key functionality."),
28+
category: z.string().optional().describe("The category/phase for this feature. Example: 'Phase 1: Foundation', 'Phase 2: Core Logic', 'Phase 3: Polish', 'Authentication', 'UI/UX'"),
29+
steps: z.array(z.string()).optional().describe("Array of implementation steps. Each step should be a clear, actionable task. Example: ['Set up database schema', 'Create API endpoints', 'Build UI components', 'Add validation']")
2730
},
2831
async (args) => {
2932
try {
30-
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`);
33+
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}, category=${args.category || "(none)"}, steps=${args.steps?.length || 0}`);
34+
console.log(`[Feature Creation] Creating/updating feature "${args.featureId}" with status "${args.status}"`);
3135

3236
// Load the feature to check skipTests flag
3337
const features = await featureLoader.loadFeatures(projectPath);
3438
const feature = features.find((f) => f.id === args.featureId);
3539

3640
if (!feature) {
37-
throw new Error(`Feature ${args.featureId} not found`);
41+
console.log(`[Feature Creation] Feature ${args.featureId} not found - this might be a new feature being created`);
42+
// This might be a new feature - try to proceed anyway
3843
}
3944

4045
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
4146
let finalStatus = args.status;
42-
if (args.status === "verified" && feature.skipTests === true) {
47+
// Convert 'todo' to 'backlog' for consistency, but only for new features
48+
if (!feature && finalStatus === "todo") {
49+
finalStatus = "backlog";
50+
}
51+
if (feature && args.status === "verified" && feature.skipTests === true) {
4352
console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`);
4453
finalStatus = "waiting_approval";
4554
}
4655

47-
// Call the provided callback to update feature status with summary
48-
await updateFeatureStatusCallback(args.featureId, finalStatus, projectPath, args.summary);
56+
// Call the provided callback to update feature status
57+
await updateFeatureStatusCallback(
58+
args.featureId,
59+
finalStatus,
60+
projectPath,
61+
{
62+
summary: args.summary,
63+
description: args.description,
64+
category: args.category,
65+
steps: args.steps,
66+
}
67+
);
4968

5069
const statusMessage = finalStatus !== args.status
51-
? `Successfully updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}" because skipTests=true)${args.summary ? ` with summary: "${args.summary}"` : ""}`
52-
: `Successfully updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` with summary: "${args.summary}"` : ""}`;
70+
? `Successfully created/updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}")${args.summary ? ` - ${args.summary}` : ""}`
71+
: `Successfully created/updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` - ${args.summary}` : ""}`;
72+
73+
console.log(`[Feature Creation] ✓ ${statusMessage}`);
5374

5475
return {
5576
content: [{
@@ -59,6 +80,7 @@ class McpServerFactory {
5980
};
6081
} catch (error) {
6182
console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error);
83+
console.error(`[Feature Creation] ✗ Failed to create/update feature ${args.featureId}: ${error.message}`);
6284
return {
6385
content: [{
6486
type: "text",

app/electron/services/mcp-server-stdio.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ async function handleToolsCall(params, id) {
215215
// Call the update callback via IPC or direct call
216216
// Since we're in a separate process, we need to use IPC to communicate back
217217
// For now, we'll call the feature loader directly since it has the update method
218-
await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, summary);
218+
await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, { summary });
219219

220220
const statusMessage = finalStatus !== status
221221
? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}`

0 commit comments

Comments
 (0)