mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
Add comprehensive worktree management system to enable task isolation and rollback capabilities. This allows users to revert agent changes if they don't satisfy requirements or break functionality. Key components: - New WorktreeManager service for branch and worktree operations - GitDiffPanel component for visualizing changes - Enhanced UI components with worktree integration - Auto-mode service enhancements for worktree workflow Modified files: worktree-manager.js, git-diff-panel.tsx, main.js, preload.js, feature-loader.js, agent-output-modal.tsx, board-view.tsx, kanban-card.tsx, electron.ts, app-store.ts, electron.d.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
239 lines
7.9 KiB
JavaScript
239 lines
7.9 KiB
JavaScript
const path = require("path");
|
|
const fs = require("fs/promises");
|
|
|
|
/**
|
|
* Feature Loader - Handles loading and selecting features from feature_list.json
|
|
*/
|
|
class FeatureLoader {
|
|
/**
|
|
* Load features from .automaker/feature_list.json
|
|
*/
|
|
async loadFeatures(projectPath) {
|
|
const featuresPath = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"feature_list.json"
|
|
);
|
|
|
|
try {
|
|
const content = await fs.readFile(featuresPath, "utf-8");
|
|
const features = JSON.parse(content);
|
|
|
|
// Ensure each feature has an ID
|
|
return features.map((f, index) => ({
|
|
...f,
|
|
id: f.id || `feature-${index}-${Date.now()}`,
|
|
}));
|
|
} catch (error) {
|
|
console.error("[FeatureLoader] Failed to load features:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update feature status in .automaker/feature_list.json
|
|
* @param {string} featureId - The ID of the feature to update
|
|
* @param {string} status - The new status
|
|
* @param {string} projectPath - Path to the project
|
|
* @param {string} [summary] - Optional summary of what was done
|
|
* @param {string} [error] - Optional error message if feature errored
|
|
*/
|
|
async updateFeatureStatus(featureId, status, projectPath, summary, error) {
|
|
const featuresPath = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"feature_list.json"
|
|
);
|
|
|
|
// 🛡️ SAFETY: Create backup before any modification
|
|
const backupPath = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"feature_list.backup.json"
|
|
);
|
|
|
|
try {
|
|
const originalContent = await fs.readFile(featuresPath, "utf-8");
|
|
await fs.writeFile(backupPath, originalContent, "utf-8");
|
|
console.log(`[FeatureLoader] Created backup at ${backupPath}`);
|
|
} catch (error) {
|
|
console.warn(`[FeatureLoader] Could not create backup: ${error.message}`);
|
|
}
|
|
|
|
const features = await this.loadFeatures(projectPath);
|
|
|
|
// 🛡️ VALIDATION: Ensure we loaded features successfully
|
|
if (!Array.isArray(features)) {
|
|
throw new Error("CRITICAL: features is not an array - aborting to prevent data loss");
|
|
}
|
|
|
|
if (features.length === 0) {
|
|
console.warn(`[FeatureLoader] WARNING: Feature list is empty. This may indicate corruption.`);
|
|
// Try to restore from backup
|
|
try {
|
|
const backupContent = await fs.readFile(backupPath, "utf-8");
|
|
const backupFeatures = JSON.parse(backupContent);
|
|
if (Array.isArray(backupFeatures) && backupFeatures.length > 0) {
|
|
console.log(`[FeatureLoader] Restored ${backupFeatures.length} features from backup`);
|
|
// Use backup features instead
|
|
features.length = 0;
|
|
features.push(...backupFeatures);
|
|
}
|
|
} catch (backupError) {
|
|
console.error(`[FeatureLoader] Could not restore from backup: ${backupError.message}`);
|
|
}
|
|
}
|
|
|
|
const feature = features.find((f) => f.id === featureId);
|
|
|
|
if (!feature) {
|
|
console.error(`[FeatureLoader] Feature ${featureId} not found`);
|
|
return;
|
|
}
|
|
|
|
// Update the status field
|
|
feature.status = status;
|
|
|
|
// Update the summary field if provided
|
|
if (summary) {
|
|
feature.summary = summary;
|
|
}
|
|
|
|
// Update the error field (set or clear)
|
|
if (error) {
|
|
feature.error = error;
|
|
} else {
|
|
// Clear any previous error when status changes without error
|
|
delete feature.error;
|
|
}
|
|
|
|
// Save back to file
|
|
const toSave = features.map((f) => {
|
|
const featureData = {
|
|
id: f.id,
|
|
category: f.category,
|
|
description: f.description,
|
|
steps: f.steps,
|
|
status: f.status,
|
|
};
|
|
// Preserve optional fields if they exist
|
|
if (f.skipTests !== undefined) {
|
|
featureData.skipTests = f.skipTests;
|
|
}
|
|
if (f.images !== undefined) {
|
|
featureData.images = f.images;
|
|
}
|
|
if (f.imagePaths !== undefined) {
|
|
featureData.imagePaths = f.imagePaths;
|
|
}
|
|
if (f.startedAt !== undefined) {
|
|
featureData.startedAt = f.startedAt;
|
|
}
|
|
if (f.summary !== undefined) {
|
|
featureData.summary = f.summary;
|
|
}
|
|
if (f.model !== undefined) {
|
|
featureData.model = f.model;
|
|
}
|
|
if (f.thinkingLevel !== undefined) {
|
|
featureData.thinkingLevel = f.thinkingLevel;
|
|
}
|
|
if (f.error !== undefined) {
|
|
featureData.error = f.error;
|
|
}
|
|
// Preserve worktree info
|
|
if (f.worktreePath !== undefined) {
|
|
featureData.worktreePath = f.worktreePath;
|
|
}
|
|
if (f.branchName !== undefined) {
|
|
featureData.branchName = f.branchName;
|
|
}
|
|
return featureData;
|
|
});
|
|
|
|
// 🛡️ FINAL VALIDATION: Ensure we're not writing an empty array
|
|
if (!Array.isArray(toSave) || toSave.length === 0) {
|
|
throw new Error("CRITICAL: Attempted to save empty feature list - aborting to prevent data loss");
|
|
}
|
|
|
|
await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8");
|
|
console.log(`[FeatureLoader] Updated feature ${featureId}: status=${status}${summary ? `, summary="${summary}"` : ""}`);
|
|
console.log(`[FeatureLoader] Successfully saved ${toSave.length} features to feature_list.json`);
|
|
}
|
|
|
|
/**
|
|
* Select the next feature to implement
|
|
* Prioritizes: earlier features in the list that are not verified or waiting_approval
|
|
*/
|
|
selectNextFeature(features) {
|
|
// Find first feature that is in backlog or in_progress status
|
|
// Skip verified and waiting_approval (which needs user input)
|
|
return features.find((f) => f.status !== "verified" && f.status !== "waiting_approval");
|
|
}
|
|
|
|
/**
|
|
* Update worktree info for a feature
|
|
* @param {string} featureId - The ID of the feature to update
|
|
* @param {string} projectPath - Path to the project
|
|
* @param {string|null} worktreePath - Path to the worktree (null to clear)
|
|
* @param {string|null} branchName - Name of the feature branch (null to clear)
|
|
*/
|
|
async updateFeatureWorktree(featureId, projectPath, worktreePath, branchName) {
|
|
const featuresPath = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"feature_list.json"
|
|
);
|
|
|
|
const features = await this.loadFeatures(projectPath);
|
|
|
|
if (!Array.isArray(features) || features.length === 0) {
|
|
console.error("[FeatureLoader] Cannot update worktree: feature list is empty");
|
|
return;
|
|
}
|
|
|
|
const feature = features.find((f) => f.id === featureId);
|
|
|
|
if (!feature) {
|
|
console.error(`[FeatureLoader] Feature ${featureId} not found`);
|
|
return;
|
|
}
|
|
|
|
// Update or clear worktree info
|
|
if (worktreePath) {
|
|
feature.worktreePath = worktreePath;
|
|
feature.branchName = branchName;
|
|
} else {
|
|
delete feature.worktreePath;
|
|
delete feature.branchName;
|
|
}
|
|
|
|
// Save back to file (reuse the same mapping logic)
|
|
const toSave = features.map((f) => {
|
|
const featureData = {
|
|
id: f.id,
|
|
category: f.category,
|
|
description: f.description,
|
|
steps: f.steps,
|
|
status: f.status,
|
|
};
|
|
if (f.skipTests !== undefined) featureData.skipTests = f.skipTests;
|
|
if (f.images !== undefined) featureData.images = f.images;
|
|
if (f.imagePaths !== undefined) featureData.imagePaths = f.imagePaths;
|
|
if (f.startedAt !== undefined) featureData.startedAt = f.startedAt;
|
|
if (f.summary !== undefined) featureData.summary = f.summary;
|
|
if (f.model !== undefined) featureData.model = f.model;
|
|
if (f.thinkingLevel !== undefined) featureData.thinkingLevel = f.thinkingLevel;
|
|
if (f.error !== undefined) featureData.error = f.error;
|
|
if (f.worktreePath !== undefined) featureData.worktreePath = f.worktreePath;
|
|
if (f.branchName !== undefined) featureData.branchName = f.branchName;
|
|
return featureData;
|
|
});
|
|
|
|
await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8");
|
|
console.log(`[FeatureLoader] Updated feature ${featureId}: worktreePath=${worktreePath}, branchName=${branchName}`);
|
|
}
|
|
}
|
|
|
|
module.exports = new FeatureLoader();
|