@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
293 lines (291 loc) • 9.39 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 { callClaude, callCodexCLI, implementWithClaude } from "./providers.js";
import * as fs from "fs";
import * as path from "path";
import { FrameManager } from "../../core/context/index.js";
import { deriveProjectId } from "./utils.js";
function heuristicPlan(input) {
return {
summary: `Plan for: ${input.task}`,
steps: [
{
id: "step-1",
title: "Analyze requirements",
rationale: "Understand the task scope and constraints",
acceptanceCriteria: [
"Requirements clearly defined",
"Edge cases identified"
]
},
{
id: "step-2",
title: "Implement core changes",
rationale: "Make the minimal changes needed to complete the task",
acceptanceCriteria: [
"Code compiles without errors",
"Core functionality works"
]
},
{
id: "step-3",
title: "Verify and test",
rationale: "Ensure changes work correctly",
acceptanceCriteria: ["Tests pass", "No regressions introduced"]
}
],
risks: [
"API key not configured - using heuristic plan",
"May need manual review of generated code"
]
};
}
async function runSpike(input, options = {}) {
const plannerSystem = `You write concise, actionable implementation plans. Output raw JSON only (no markdown code fences). Schema: { "summary": "string", "steps": [{ "id": "step-1", "title": "string", "rationale": "string", "acceptanceCriteria": ["string"] }], "risks": ["string"] }`;
const contextSummary = getLocalContextSummary(input.repoPath);
const plannerPrompt = `Task: ${input.task}
Repo: ${input.repoPath}
Notes: ${input.contextNotes || "(none)"}
${contextSummary}
Constraints: Keep the plan minimal and implementable in a single PR.`;
let plan;
try {
const raw = await callClaude(plannerPrompt, {
model: options.plannerModel,
system: plannerSystem
});
try {
const cleaned = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
plan = JSON.parse(cleaned);
} catch {
plan = heuristicPlan(input);
}
} catch {
plan = heuristicPlan(input);
}
const implementer = options.implementer || "codex";
const maxIters = Math.max(1, options.maxIters ?? 2);
const iterations = [];
let approved = false;
let lastCommand = "";
let lastOutput = "";
let lastCritique = {
approved: true,
issues: [],
suggestions: []
};
for (let i = 0; i < maxIters; i++) {
const stepsList = plan.steps.map((s, idx) => `${idx + 1}. ${s.title}`).join("\n");
const basePrompt = `Implement the following plan:
${stepsList}
Keep changes minimal and focused. Avoid unrelated edits.`;
const refine = i === 0 ? "" : `
Incorporate reviewer suggestions: ${lastCritique.suggestions.join("; ")}`;
const implPrompt = basePrompt + refine;
let ok = false;
if (implementer === "codex") {
const impl = callCodexCLI(
implPrompt,
[],
options.dryRun !== false,
input.repoPath
);
ok = impl.ok;
lastCommand = impl.command;
lastOutput = impl.output;
} else {
const impl = await implementWithClaude(implPrompt, {
model: options.plannerModel
});
ok = impl.ok;
lastCommand = `claude:${options.plannerModel || "sonnet"} prompt`;
lastOutput = impl.output;
}
const criticSystem = `You are a strict code reviewer. Return a JSON object: { approved: boolean, issues: string[], suggestions: string[] }`;
const criticPrompt = `Plan: ${plan.summary}
Attempt ${i + 1}/${maxIters}
Command: ${lastCommand}
Output: ${lastOutput.slice(0, 2e3)}`;
try {
const raw = await callClaude(criticPrompt, {
model: options.reviewerModel,
system: criticSystem
});
const cleaned = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
lastCritique = JSON.parse(cleaned);
} catch {
lastCritique = {
approved: ok,
issues: ok ? [] : ["Critique failed"],
suggestions: []
};
}
iterations.push({
command: lastCommand,
ok,
outputPreview: lastOutput.slice(0, 400),
critique: lastCritique
});
if (lastCritique.approved) {
approved = true;
break;
}
}
try {
const dir = options.auditDir || path.join(input.repoPath, ".stackmemory", "build");
fs.mkdirSync(dir, { recursive: true });
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
const file = path.join(dir, `spike-${stamp}.json`);
fs.writeFileSync(
file,
JSON.stringify(
{
input,
options: { ...options, auditDir: void 0 },
plan,
iterations
},
null,
2
)
);
} catch {
}
if (options.record) {
void recordContext(
input.repoPath,
"decision",
`Plan: ${plan.summary}`,
0.8
);
void recordContext(
input.repoPath,
"decision",
`Critique: ${lastCritique.approved ? "approved" : "needs_changes"}`,
0.6
);
}
if (options.recordFrame) {
void recordAsFrame(
input.repoPath,
input.task,
plan,
lastCritique,
iterations
);
}
return {
plan,
implementation: {
success: approved,
summary: approved ? "Implementation approved by critic" : "Implementation not approved",
commands: iterations.map((it) => it.command)
},
critique: lastCritique,
iterations
};
}
const runPlanAndCode = runSpike;
async function recordContext(repoPath, type, content, importance = 0.6) {
try {
const dbPath = path.join(repoPath, ".stackmemory", "context.db");
if (!fs.existsSync(path.dirname(dbPath)))
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const { default: Database } = await import("better-sqlite3");
const db = new Database(dbPath);
db.exec(`
CREATE TABLE IF NOT EXISTS contexts (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
content TEXT NOT NULL,
importance REAL DEFAULT 0.5,
created_at INTEGER DEFAULT (unixepoch()),
last_accessed INTEGER DEFAULT (unixepoch()),
access_count INTEGER DEFAULT 1
);
`);
const id = `ctx_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const stmt = db.prepare(
"INSERT OR REPLACE INTO contexts (id, type, content, importance) VALUES (?, ?, ?, ?)"
);
stmt.run(id, type, content, importance);
db.close();
} catch {
}
}
async function recordAsFrame(repoPath, task, plan, critique, iterations) {
try {
const dbPath = path.join(repoPath, ".stackmemory", "context.db");
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const { default: Database } = await import("better-sqlite3");
const db = new Database(dbPath);
const projectId = deriveProjectId(repoPath);
const fm = new FrameManager(db, projectId);
const frameId = fm.createFrame({
type: "task",
name: `Plan & Code: ${task}`,
inputs: { plan }
});
fm.addAnchor("DECISION", plan.summary, 8, { source: "build" }, frameId);
const commands = iterations.map((it) => it.command).filter(Boolean);
if (commands.length) {
fm.addAnchor(
"FACT",
`Commands: ${commands.join(" | ")}`,
5,
{ commands },
frameId
);
}
if (critique.issues?.length) {
critique.issues.slice(0, 5).forEach((issue) => fm.addAnchor("RISK", issue, 6, {}, frameId));
}
if (critique.suggestions?.length) {
critique.suggestions.slice(0, 5).forEach(
(s) => fm.addAnchor("TODO", s, 5, { from: "critic" }, frameId)
);
}
fm.closeFrame(frameId, { approved: critique.approved });
db.close();
} catch {
}
}
async function runPlanOnly(input, options = {}) {
const plannerSystem = `You write concise, actionable implementation plans. Output raw JSON only (no markdown code fences). Schema: { "summary": "string", "steps": [{ "id": "step-1", "title": "string", "rationale": "string", "acceptanceCriteria": ["string"] }], "risks": ["string"] }`;
const contextSummary = getLocalContextSummary(input.repoPath);
const plannerPrompt = `Task: ${input.task}
Repo: ${input.repoPath}
Notes: ${input.contextNotes || "(none)"}
${contextSummary}
Constraints: Keep the plan minimal and implementable in a single PR.`;
try {
const raw = await callClaude(plannerPrompt, {
model: options.plannerModel,
system: plannerSystem
});
try {
return JSON.parse(raw);
} catch {
return heuristicPlan(input);
}
} catch {
return heuristicPlan(input);
}
}
function getLocalContextSummary(repoPath) {
try {
const dbPath = path.join(repoPath, ".stackmemory", "context.db");
if (!fs.existsSync(dbPath)) return "Project context: (no local DB found)";
return "Project context: (available \u2014 DB present)";
} catch {
return "Project context: (unavailable \u2014 local DB not ready)";
}
}
export {
runPlanAndCode,
runPlanOnly,
runSpike
};
//# sourceMappingURL=harness.js.map