mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
- Introduced a new `package-lock.json` to manage dependencies.
- Removed obsolete `.automaker/feature_list.json` and replaced it with a new structure under `.automaker/features/{id}/feature.json` for better organization.
- Updated various components to utilize the new features API for managing features, including creation, updates, and deletions.
- Enhanced the UI to reflect changes in feature management, including updates to the sidebar and board view.
- Improved documentation and comments throughout the codebase to clarify the new feature management process.
453 lines
13 KiB
JavaScript
453 lines
13 KiB
JavaScript
const path = require("path");
|
|
const fs = require("fs/promises");
|
|
|
|
/**
|
|
* Context Manager - Handles reading, writing, and deleting context files for features
|
|
*/
|
|
class ContextManager {
|
|
/**
|
|
* Write output to feature context file
|
|
*/
|
|
async writeToContextFile(projectPath, featureId, content) {
|
|
if (!projectPath) return;
|
|
|
|
try {
|
|
const featureDir = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"features",
|
|
featureId
|
|
);
|
|
|
|
// Ensure feature directory exists
|
|
try {
|
|
await fs.access(featureDir);
|
|
} catch {
|
|
await fs.mkdir(featureDir, { recursive: true });
|
|
}
|
|
|
|
const filePath = path.join(featureDir, "agent-output.md");
|
|
|
|
// Append to existing file or create new one
|
|
try {
|
|
const existing = await fs.readFile(filePath, "utf-8");
|
|
await fs.writeFile(filePath, existing + content, "utf-8");
|
|
} catch {
|
|
await fs.writeFile(filePath, content, "utf-8");
|
|
}
|
|
} catch (error) {
|
|
console.error("[ContextManager] Failed to write to context file:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read context file for a feature
|
|
*/
|
|
async readContextFile(projectPath, featureId) {
|
|
try {
|
|
const contextPath = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"features",
|
|
featureId,
|
|
"agent-output.md"
|
|
);
|
|
const content = await fs.readFile(contextPath, "utf-8");
|
|
return content;
|
|
} catch (error) {
|
|
console.log(`[ContextManager] No context file found for ${featureId}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete agent context file for a feature
|
|
*/
|
|
async deleteContextFile(projectPath, featureId) {
|
|
if (!projectPath) return;
|
|
|
|
try {
|
|
const contextPath = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"features",
|
|
featureId,
|
|
"agent-output.md"
|
|
);
|
|
await fs.unlink(contextPath);
|
|
console.log(
|
|
`[ContextManager] Deleted agent context for feature ${featureId}`
|
|
);
|
|
} catch (error) {
|
|
// File might not exist, which is fine
|
|
if (error.code !== "ENOENT") {
|
|
console.error("[ContextManager] Failed to delete context file:", error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the memory.md file containing lessons learned and common issues
|
|
* Returns formatted string to inject into prompts
|
|
*/
|
|
async getMemoryContent(projectPath) {
|
|
if (!projectPath) return "";
|
|
|
|
try {
|
|
const memoryPath = path.join(projectPath, ".automaker", "memory.md");
|
|
|
|
// Check if file exists
|
|
try {
|
|
await fs.access(memoryPath);
|
|
} catch {
|
|
// File doesn't exist, return empty string
|
|
return "";
|
|
}
|
|
|
|
const content = await fs.readFile(memoryPath, "utf-8");
|
|
|
|
if (!content.trim()) {
|
|
return "";
|
|
}
|
|
|
|
return `
|
|
**🧠 Agent Memory - Previous Lessons Learned:**
|
|
|
|
The following memory file contains lessons learned from previous agent runs, including common issues and their solutions. Review this carefully to avoid repeating past mistakes.
|
|
|
|
<agent-memory>
|
|
${content}
|
|
</agent-memory>
|
|
|
|
**IMPORTANT:** If you encounter a new issue that took significant debugging effort to resolve, add it to the memory file at \`.automaker/memory.md\` in a concise format:
|
|
- Issue title
|
|
- Problem description (1-2 sentences)
|
|
- Solution/fix (with code example if helpful)
|
|
|
|
This helps future agent runs avoid the same pitfalls.
|
|
`;
|
|
} catch (error) {
|
|
console.error("[ContextManager] Failed to read memory file:", error);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List context files from .automaker/context/ directory and get previews
|
|
* Returns a formatted string with file names and first 50 lines of each file
|
|
*/
|
|
async getContextFilesPreview(projectPath) {
|
|
if (!projectPath) return "";
|
|
|
|
try {
|
|
const contextDir = path.join(projectPath, ".automaker", "context");
|
|
|
|
// Check if directory exists
|
|
try {
|
|
await fs.access(contextDir);
|
|
} catch {
|
|
// Directory doesn't exist, return empty string
|
|
return "";
|
|
}
|
|
|
|
// Read directory contents
|
|
const entries = await fs.readdir(contextDir, { withFileTypes: true });
|
|
const files = entries
|
|
.filter((entry) => entry.isFile())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
|
|
if (files.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
// Build preview string
|
|
const previews = [];
|
|
previews.push(`\n**📁 Context Files Available:**\n`);
|
|
previews.push(
|
|
`The following context files are available in \`.automaker/context/\` directory.`
|
|
);
|
|
previews.push(
|
|
`These files contain additional context that may be relevant to your work.`
|
|
);
|
|
previews.push(
|
|
`You can read them in full using the Read tool if needed.\n`
|
|
);
|
|
|
|
for (const fileName of files) {
|
|
try {
|
|
const filePath = path.join(contextDir, fileName);
|
|
const content = await fs.readFile(filePath, "utf-8");
|
|
const lines = content.split("\n");
|
|
const previewLines = lines.slice(0, 50);
|
|
const preview = previewLines.join("\n");
|
|
const hasMore = lines.length > 50;
|
|
|
|
previews.push(`\n**File: ${fileName}**`);
|
|
if (hasMore) {
|
|
previews.push(
|
|
`(Showing first 50 of ${lines.length} lines - use Read tool to see full content)`
|
|
);
|
|
}
|
|
previews.push(`\`\`\``);
|
|
previews.push(preview);
|
|
previews.push(`\`\`\`\n`);
|
|
} catch (error) {
|
|
console.error(
|
|
`[ContextManager] Failed to read context file ${fileName}:`,
|
|
error
|
|
);
|
|
previews.push(`\n**File: ${fileName}** (Error reading file)\n`);
|
|
}
|
|
}
|
|
|
|
return previews.join("\n");
|
|
} catch (error) {
|
|
console.error("[ContextManager] Failed to list context files:", error);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the initial git state before a feature starts executing
|
|
* This captures all files that were already modified before the AI agent started
|
|
* @param {string} projectPath - Path to the project
|
|
* @param {string} featureId - Feature ID
|
|
* @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[]}>}
|
|
*/
|
|
async saveInitialGitState(projectPath, featureId) {
|
|
if (!projectPath) return { modifiedFiles: [], untrackedFiles: [] };
|
|
|
|
try {
|
|
const { execSync } = require("child_process");
|
|
const featureDir = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"features",
|
|
featureId
|
|
);
|
|
|
|
// Ensure feature directory exists
|
|
try {
|
|
await fs.access(featureDir);
|
|
} catch {
|
|
await fs.mkdir(featureDir, { recursive: true });
|
|
}
|
|
|
|
// Get list of modified files (both staged and unstaged)
|
|
let modifiedFiles = [];
|
|
try {
|
|
const modifiedOutput = execSync("git diff --name-only HEAD", {
|
|
cwd: projectPath,
|
|
encoding: "utf-8",
|
|
}).trim();
|
|
if (modifiedOutput) {
|
|
modifiedFiles = modifiedOutput.split("\n").filter(Boolean);
|
|
}
|
|
} catch (error) {
|
|
console.log(
|
|
"[ContextManager] No modified files or git error:",
|
|
error.message
|
|
);
|
|
}
|
|
|
|
// Get list of untracked files
|
|
let untrackedFiles = [];
|
|
try {
|
|
const untrackedOutput = execSync(
|
|
"git ls-files --others --exclude-standard",
|
|
{
|
|
cwd: projectPath,
|
|
encoding: "utf-8",
|
|
}
|
|
).trim();
|
|
if (untrackedOutput) {
|
|
untrackedFiles = untrackedOutput.split("\n").filter(Boolean);
|
|
}
|
|
} catch (error) {
|
|
console.log(
|
|
"[ContextManager] Error getting untracked files:",
|
|
error.message
|
|
);
|
|
}
|
|
|
|
// Save the initial state to a JSON file
|
|
const stateFile = path.join(featureDir, "git-state.json");
|
|
const state = {
|
|
timestamp: new Date().toISOString(),
|
|
modifiedFiles,
|
|
untrackedFiles,
|
|
};
|
|
|
|
await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
console.log(
|
|
`[ContextManager] Saved initial git state for ${featureId}:`,
|
|
{
|
|
modifiedCount: modifiedFiles.length,
|
|
untrackedCount: untrackedFiles.length,
|
|
}
|
|
);
|
|
|
|
return state;
|
|
} catch (error) {
|
|
console.error(
|
|
"[ContextManager] Failed to save initial git state:",
|
|
error
|
|
);
|
|
return { modifiedFiles: [], untrackedFiles: [] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the initial git state saved before a feature started executing
|
|
* @param {string} projectPath - Path to the project
|
|
* @param {string} featureId - Feature ID
|
|
* @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[], timestamp: string} | null>}
|
|
*/
|
|
async getInitialGitState(projectPath, featureId) {
|
|
if (!projectPath) return null;
|
|
|
|
try {
|
|
const stateFile = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"features",
|
|
featureId,
|
|
"git-state.json"
|
|
);
|
|
const content = await fs.readFile(stateFile, "utf-8");
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
console.log(
|
|
`[ContextManager] No initial git state found for ${featureId}`
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the git state file for a feature
|
|
* @param {string} projectPath - Path to the project
|
|
* @param {string} featureId - Feature ID
|
|
*/
|
|
async deleteGitStateFile(projectPath, featureId) {
|
|
if (!projectPath) return;
|
|
|
|
try {
|
|
const stateFile = path.join(
|
|
projectPath,
|
|
".automaker",
|
|
"features",
|
|
featureId,
|
|
"git-state.json"
|
|
);
|
|
await fs.unlink(stateFile);
|
|
console.log(`[ContextManager] Deleted git state file for ${featureId}`);
|
|
} catch (error) {
|
|
// File might not exist, which is fine
|
|
if (error.code !== "ENOENT") {
|
|
console.error(
|
|
"[ContextManager] Failed to delete git state file:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate which files were changed during the AI session
|
|
* by comparing current git state with the saved initial state
|
|
* @param {string} projectPath - Path to the project
|
|
* @param {string} featureId - Feature ID
|
|
* @returns {Promise<{newFiles: string[], modifiedFiles: string[]}>}
|
|
*/
|
|
async getFilesChangedDuringSession(projectPath, featureId) {
|
|
if (!projectPath) return { newFiles: [], modifiedFiles: [] };
|
|
|
|
try {
|
|
const { execSync } = require("child_process");
|
|
|
|
// Get initial state
|
|
const initialState = await this.getInitialGitState(
|
|
projectPath,
|
|
featureId
|
|
);
|
|
|
|
// Get current state
|
|
let currentModified = [];
|
|
try {
|
|
const modifiedOutput = execSync("git diff --name-only HEAD", {
|
|
cwd: projectPath,
|
|
encoding: "utf-8",
|
|
}).trim();
|
|
if (modifiedOutput) {
|
|
currentModified = modifiedOutput.split("\n").filter(Boolean);
|
|
}
|
|
} catch (error) {
|
|
console.log("[ContextManager] No modified files or git error");
|
|
}
|
|
|
|
let currentUntracked = [];
|
|
try {
|
|
const untrackedOutput = execSync(
|
|
"git ls-files --others --exclude-standard",
|
|
{
|
|
cwd: projectPath,
|
|
encoding: "utf-8",
|
|
}
|
|
).trim();
|
|
if (untrackedOutput) {
|
|
currentUntracked = untrackedOutput.split("\n").filter(Boolean);
|
|
}
|
|
} catch (error) {
|
|
console.log("[ContextManager] Error getting untracked files");
|
|
}
|
|
|
|
if (!initialState) {
|
|
// No initial state - all current changes are considered from this session
|
|
console.log(
|
|
"[ContextManager] No initial state found, returning all current changes"
|
|
);
|
|
return {
|
|
newFiles: currentUntracked,
|
|
modifiedFiles: currentModified,
|
|
};
|
|
}
|
|
|
|
// Calculate files that are new since the session started
|
|
const initialModifiedSet = new Set(initialState.modifiedFiles || []);
|
|
const initialUntrackedSet = new Set(initialState.untrackedFiles || []);
|
|
|
|
// New files = current untracked - initial untracked
|
|
const newFiles = currentUntracked.filter(
|
|
(f) => !initialUntrackedSet.has(f)
|
|
);
|
|
|
|
// Modified files = current modified - initial modified
|
|
const modifiedFiles = currentModified.filter(
|
|
(f) => !initialModifiedSet.has(f)
|
|
);
|
|
|
|
console.log(
|
|
`[ContextManager] Files changed during session for ${featureId}:`,
|
|
{
|
|
newFilesCount: newFiles.length,
|
|
modifiedFilesCount: modifiedFiles.length,
|
|
newFiles,
|
|
modifiedFiles,
|
|
}
|
|
);
|
|
|
|
return { newFiles, modifiedFiles };
|
|
} catch (error) {
|
|
console.error(
|
|
"[ContextManager] Failed to calculate changed files:",
|
|
error
|
|
);
|
|
return { newFiles: [], modifiedFiles: [] };
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new ContextManager();
|