@stackmemoryai/stackmemory
Version:
Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a
515 lines (514 loc) • 14.4 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { spawn } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { execSync } from "child_process";
import { logger } from "../core/monitoring/logger.js";
import { hookEmitter } from "../hooks/events.js";
const DEFAULT_TIMEOUT = 5 * 60 * 1e3;
function spawnAgent(prompt, opts) {
return new Promise((resolve, reject) => {
let workDir;
try {
workDir = fs.mkdtempSync(path.join("/tmp", `${opts.prefix}-`));
} catch (err) {
return reject(
new Error(`Failed to create temp dir: ${err.message}`)
);
}
try {
execSync(`git clone --depth=1 "${opts.repoRoot}" "${workDir}"`, {
stdio: "pipe",
timeout: 3e4
});
} catch (err) {
cleanupDir(workDir);
return reject(new Error(`git clone failed: ${err.message}`));
}
const child = spawn("claude", ["--print"], {
cwd: workDir,
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env }
});
let stdout = "";
let stderr = "";
let killed = false;
const timer = setTimeout(() => {
killed = true;
child.kill("SIGTERM");
}, opts.timeout);
if (child.stdout) {
child.stdout.on("data", (d) => stdout += d);
}
if (child.stderr) {
child.stderr.on("data", (d) => stderr += d);
}
child.on("close", (code) => {
clearTimeout(timer);
resolve({ stdout, stderr, exitCode: code, workDir, timedOut: killed });
});
child.on("error", (err) => {
clearTimeout(timer);
resolve({
stdout,
stderr: err.message,
exitCode: 1,
workDir,
timedOut: false
});
});
if (child.stdin) {
child.stdin.write(prompt);
child.stdin.end();
}
});
}
function cleanupDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
}
}
function slugify(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
}
class ParallelAgentSkill {
constructor(context) {
this.context = context;
this.repoRoot = process.cwd();
}
repoRoot;
/**
* Research — explore codebase, save findings as a frame.
*/
async research(question, options) {
if (!question?.trim()) {
return { success: false, message: "Question is required" };
}
const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
const prompt = [
"Explore this codebase and answer the following question.",
"Do NOT modify any files. Output your findings as structured markdown.",
"",
`Question: ${question}`
].join("\n");
let result;
try {
hookEmitter.emitHook({
type: "agent_start",
timestamp: Date.now(),
data: { agentType: "research", workDir: "", task: question }
}).catch(() => {
});
result = await spawnAgent(prompt, {
prefix: "sm-research",
timeout,
repoRoot: this.repoRoot
});
} catch (err) {
const msg = err.message;
hookEmitter.emitHook({
type: "agent_error",
timestamp: Date.now(),
data: { agentType: "research", error: msg }
}).catch(() => {
});
return {
success: false,
message: `Agent failed to start: ${msg}`
};
}
try {
if (result.timedOut) {
hookEmitter.emitHook({
type: "agent_complete",
timestamp: Date.now(),
data: {
agentType: "research",
workDir: result.workDir,
exitCode: result.exitCode,
timedOut: true
}
}).catch(() => {
});
return {
success: false,
message: `Research agent timed out after ${timeout / 1e3}s`,
data: { partial: result.stdout.slice(0, 2e3) }
};
}
const findings = result.stdout || "(no output)";
let frameId;
try {
frameId = await this.saveFrame("research", question, findings);
} catch (err) {
logger.warn("Failed to save research frame:", err);
}
hookEmitter.emitHook({
type: "agent_complete",
timestamp: Date.now(),
data: {
agentType: "research",
workDir: result.workDir,
exitCode: result.exitCode,
timedOut: false,
frameId
}
}).catch(() => {
});
return {
success: true,
message: `Research complete${frameId ? ` (frame: ${frameId})` : ""}`,
data: { findings, frameId, exitCode: result.exitCode }
};
} finally {
cleanupDir(result.workDir);
}
}
/**
* Maintain — low-stakes fix, produces a .patch file.
*/
async maintain(task, options) {
if (!task?.trim()) {
return { success: false, message: "Task description is required" };
}
const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
const prompt = [
task,
"",
"After making changes, verify by running: npm run lint && npm run test:run"
].join("\n");
let result;
try {
hookEmitter.emitHook({
type: "agent_start",
timestamp: Date.now(),
data: { agentType: "maintain", workDir: "", task }
}).catch(() => {
});
result = await spawnAgent(prompt, {
prefix: "sm-maint",
timeout,
repoRoot: this.repoRoot
});
} catch (err) {
const msg = err.message;
hookEmitter.emitHook({
type: "agent_error",
timestamp: Date.now(),
data: { agentType: "maintain", error: msg }
}).catch(() => {
});
return {
success: false,
message: `Agent failed to start: ${msg}`
};
}
try {
if (result.timedOut) {
hookEmitter.emitHook({
type: "agent_complete",
timestamp: Date.now(),
data: {
agentType: "maintain",
workDir: result.workDir,
exitCode: result.exitCode,
timedOut: true
}
}).catch(() => {
});
return {
success: false,
message: `Maintenance agent timed out after ${timeout / 1e3}s`,
data: { partial: result.stdout.slice(0, 2e3) }
};
}
let diff = "";
try {
diff = execSync("git diff HEAD", {
cwd: result.workDir,
encoding: "utf-8",
timeout: 1e4
});
} catch {
}
if (!diff.trim()) {
hookEmitter.emitHook({
type: "agent_complete",
timestamp: Date.now(),
data: {
agentType: "maintain",
workDir: result.workDir,
exitCode: result.exitCode,
timedOut: false
}
}).catch(() => {
});
return {
success: false,
message: "Agent made no changes (empty diff)",
data: { output: result.stdout.slice(0, 2e3) }
};
}
let validated = false;
try {
execSync("npm run lint", {
cwd: result.workDir,
stdio: "pipe",
timeout: 6e4
});
validated = true;
} catch {
}
const patchDir = path.join(this.repoRoot, ".stackmemory", "patches");
fs.mkdirSync(patchDir, { recursive: true });
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
const slug = slugify(task);
const patchFile = `${timestamp}-${slug}.patch`;
const patchPath = path.join(patchDir, patchFile);
fs.writeFileSync(patchPath, diff);
const diffLines = diff.split("\n");
const filesChanged = diffLines.filter(
(l) => l.startsWith("diff --git")
).length;
const additions = diffLines.filter((l) => /^\+[^+]/.test(l)).length;
const deletions = diffLines.filter((l) => /^-[^-]/.test(l)).length;
hookEmitter.emitHook({
type: "agent_complete",
timestamp: Date.now(),
data: {
agentType: "maintain",
workDir: result.workDir,
exitCode: result.exitCode,
timedOut: false,
patchPath,
validated
}
}).catch(() => {
});
return {
success: true,
message: `Patch saved: ${patchFile}${validated ? "" : " (lint failed)"}`,
data: {
patchPath,
validated,
filesChanged,
additions,
deletions,
output: result.stdout.slice(0, 2e3)
},
action: `Apply with: git apply ${patchPath}`
};
} finally {
cleanupDir(result.workDir);
}
}
/**
* SpecRun — implement a spec file on a branch, validate.
*/
async specRun(specPath, options) {
if (!specPath?.trim()) {
return { success: false, message: "Spec file path is required" };
}
const absSpec = path.isAbsolute(specPath) ? specPath : path.join(this.repoRoot, specPath);
if (!fs.existsSync(absSpec)) {
return { success: false, message: `Spec file not found: ${specPath}` };
}
const specContent = fs.readFileSync(absSpec, "utf-8");
const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
const prompt = [
"Implement the following specification. Follow it precisely.",
"",
"--- SPEC START ---",
specContent,
"--- SPEC END ---"
].join("\n");
let workDir;
try {
workDir = fs.mkdtempSync(path.join("/tmp", "sm-spec-"));
} catch (err) {
return {
success: false,
message: `Failed to create temp dir: ${err.message}`
};
}
try {
execSync(`git clone --depth=1 "${this.repoRoot}" "${workDir}"`, {
stdio: "pipe",
timeout: 3e4
});
} catch (err) {
const msg = err.message;
cleanupDir(workDir);
hookEmitter.emitHook({
type: "agent_error",
timestamp: Date.now(),
data: { agentType: "spec-run", error: msg, workDir }
}).catch(() => {
});
return {
success: false,
message: `git clone failed: ${msg}`
};
}
const branchName = `agent/spec-${Date.now()}`;
try {
execSync(`git checkout -b "${branchName}"`, {
cwd: workDir,
stdio: "pipe",
timeout: 5e3
});
} catch (err) {
const msg = err.message;
cleanupDir(workDir);
hookEmitter.emitHook({
type: "agent_error",
timestamp: Date.now(),
data: { agentType: "spec-run", error: msg, workDir }
}).catch(() => {
});
return {
success: false,
message: `Failed to create branch: ${msg}`
};
}
hookEmitter.emitHook({
type: "agent_start",
timestamp: Date.now(),
data: { agentType: "spec-run", workDir, task: specPath }
}).catch(() => {
});
const child = spawn("claude", ["--print"], {
cwd: workDir,
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env }
});
let stdout = "";
let stderr = "";
let killed = false;
const agentDone = new Promise((resolve) => {
const timer = setTimeout(() => {
killed = true;
child.kill("SIGTERM");
}, timeout);
if (child.stdout) {
child.stdout.on("data", (d) => stdout += d);
}
if (child.stderr) {
child.stderr.on("data", (d) => stderr += d);
}
child.on("close", (code) => {
clearTimeout(timer);
resolve({ stdout, stderr, exitCode: code, timedOut: killed });
});
child.on("error", (err) => {
clearTimeout(timer);
resolve({
stdout,
stderr: err.message,
exitCode: 1,
timedOut: false
});
});
});
if (child.stdin) {
child.stdin.write(prompt);
child.stdin.end();
}
const agentResult = await agentDone;
if (agentResult.timedOut) {
return {
success: false,
message: `Spec agent timed out after ${timeout / 1e3}s`,
data: {
workDir,
branch: branchName,
partial: agentResult.stdout.slice(0, 2e3)
}
};
}
const validation = { lint: false, test: false, build: false };
try {
execSync("npm run lint", {
cwd: workDir,
stdio: "pipe",
timeout: 6e4
});
validation.lint = true;
} catch {
}
try {
execSync("npm run test:run", {
cwd: workDir,
stdio: "pipe",
timeout: 12e4
});
validation.test = true;
} catch {
}
try {
execSync("npm run build", {
cwd: workDir,
stdio: "pipe",
timeout: 6e4
});
validation.build = true;
} catch {
}
let diffStat = "";
try {
diffStat = execSync("git diff --stat HEAD~1", {
cwd: workDir,
encoding: "utf-8",
timeout: 5e3
});
} catch {
}
const allPassed = validation.lint && validation.test && validation.build;
hookEmitter.emitHook({
type: "agent_complete",
timestamp: Date.now(),
data: {
agentType: "spec-run",
workDir,
exitCode: agentResult.exitCode,
timedOut: false,
branch: branchName,
validation
}
}).catch(() => {
});
return {
success: allPassed,
message: allPassed ? `Spec implemented on branch ${branchName}` : `Spec implemented but validation failed on branch ${branchName}`,
data: {
workDir,
branch: branchName,
validation,
diffStat: diffStat.trim(),
output: agentResult.stdout.slice(0, 2e3)
},
action: allPassed ? `Review: cd ${workDir} && git log --oneline` : `Inspect: cd ${workDir} && git diff`
};
}
async saveFrame(type, query, content) {
const db = this.context.database;
if (!db) return void 0;
const frameId = await db.createFrame({
run_id: `agent-${Date.now()}`,
project_id: this.context.projectId,
type,
name: `Agent ${type}: ${query.slice(0, 60)}`,
state: "completed",
inputs: { query, agentType: type },
digest_text: content
});
return frameId;
}
}
export {
ParallelAgentSkill
};