UNPKG

@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
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