@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
441 lines (397 loc) • 12.1 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 * as fs from "fs";
import * as path from "path";
import { logger } from "../core/monitoring/logger.js";
const SPEC_DIR = "docs/specs";
const SPEC_CONFIGS = {
"one-pager": {
filename: "ONE_PAGER.md",
title: "One-Pager",
sections: [
"Problem",
"Audience",
"Platform",
"Core Flow",
"MVP Features",
"Non-Goals",
"Metrics"
],
inputs: []
},
"dev-spec": {
filename: "DEV_SPEC.md",
title: "Development Specification",
sections: [
"Architecture",
"Tech Stack",
"API Contracts",
"Data Models",
"Auth",
"Error Handling",
"Deployment"
],
inputs: ["one-pager"]
},
"prompt-plan": {
filename: "PROMPT_PLAN.md",
title: "Prompt Plan",
sections: [
"Stage A: Project Setup",
"Stage B: Core Data Models",
"Stage C: API Layer",
"Stage D: Business Logic",
"Stage E: Frontend / UI",
"Stage F: Integration & Testing",
"Stage G: Deploy & Polish"
],
inputs: ["one-pager", "dev-spec"]
},
agents: {
filename: "AGENTS.md",
title: "AGENTS.md",
sections: [
"Repo Files",
"Responsibilities",
"Guardrails",
"Testing",
"When to Ask"
],
inputs: ["one-pager", "dev-spec", "prompt-plan"]
}
};
function onePagerTemplate(title) {
return `# ${title} \u2014 One-Pager
## Problem
<!-- What problem does this solve? Who has this problem? -->
## Audience
<!-- Primary users and their context -->
## Platform
<!-- Web / Mobile / CLI / API \u2014 and why -->
## Core Flow
<!-- Happy-path user journey in 3-5 steps -->
1.
2.
3.
## MVP Features
<!-- Minimum set of features for first release -->
- [ ]
- [ ]
- [ ]
## Non-Goals
<!-- Explicitly out of scope for MVP -->
-
## Metrics
<!-- How will you measure success? -->
-
`;
}
function devSpecTemplate(title, onePagerContent) {
return `# ${title} \u2014 Development Specification
> Generated from ONE_PAGER.md
<details><summary>Source: ONE_PAGER.md</summary>
${onePagerContent}
</details>
## Architecture
<!-- High-level system diagram / component breakdown -->
## Tech Stack
<!-- Languages, frameworks, databases, infra -->
| Layer | Choice | Rationale |
|-------|--------|-----------|
| Frontend | | |
| Backend | | |
| Database | | |
| Hosting | | |
## API Contracts
<!-- Key endpoints with request/response shapes -->
## Data Models
<!-- Core entities and relationships -->
## Auth
<!-- Authentication and authorization strategy -->
## Error Handling
<!-- Error codes, retry strategies, user-facing messages -->
## Deployment
<!-- CI/CD, environments, rollback strategy -->
`;
}
function promptPlanTemplate(title, onePagerContent, devSpecContent) {
return `# ${title} \u2014 Prompt Plan
> Generated from ONE_PAGER.md and DEV_SPEC.md
> Each stage has TDD checkboxes \u2014 check off as tasks complete.
<details><summary>Source: ONE_PAGER.md</summary>
${onePagerContent}
</details>
<details><summary>Source: DEV_SPEC.md</summary>
${devSpecContent}
</details>
## Stage A: Project Setup
- [ ] Initialize repository and tooling
- [ ] Configure CI/CD pipeline
- [ ] Set up development environment
## Stage B: Core Data Models
- [ ] Define database schema
- [ ] Create model layer
- [ ] Write model tests
## Stage C: API Layer
- [ ] Implement API endpoints
- [ ] Add input validation
- [ ] Write API tests
## Stage D: Business Logic
- [ ] Implement core business rules
- [ ] Add edge case handling
- [ ] Write integration tests
## Stage E: Frontend / UI
- [ ] Build core UI components
- [ ] Implement user flows
- [ ] Write UI tests
## Stage F: Integration & Testing
- [ ] End-to-end test suite
- [ ] Performance testing
- [ ] Security audit
## Stage G: Deploy & Polish
- [ ] Production deployment
- [ ] Monitoring and alerting
- [ ] Documentation
`;
}
function agentsTemplate(title, inputs) {
const sourceBlocks = Object.entries(inputs).map(
([name, content]) => `<details><summary>Source: ${name}</summary>
${content}
</details>`
).join("\n\n");
return `# ${title} \u2014 AGENTS.md
> Auto-generated agent configuration for Claude Code / Cursor / Windsurf.
${sourceBlocks}
## Repo Files
<!-- Key files and their purpose -->
| File | Purpose |
|------|---------|
| | |
## Responsibilities
<!-- What this agent should and shouldn't do -->
### DO
-
### DON'T
-
## Guardrails
<!-- Safety constraints and limits -->
- Never commit secrets or credentials
- Always run tests before committing
- Keep changes focused and atomic
## Testing
<!-- How to validate changes -->
\`\`\`bash
npm test
npm run lint
npm run build
\`\`\`
## When to Ask
<!-- Situations where the agent should ask for human input -->
- Architectural changes affecting multiple systems
- Security-sensitive modifications
- Breaking API changes
- Ambiguous requirements
`;
}
class SpecGeneratorSkill {
constructor(context) {
this.context = context;
this.baseDir = process.cwd();
}
baseDir;
/** Generate a spec document by type */
async generate(type, title, opts) {
const config = SPEC_CONFIGS[type];
if (!config) {
return { success: false, message: `Unknown spec type: ${type}` };
}
const specDir = path.join(this.baseDir, SPEC_DIR);
const outputPath = path.join(specDir, config.filename);
if (fs.existsSync(outputPath) && !opts?.force) {
return {
success: false,
message: `${config.filename} already exists. Use --force to overwrite.`,
data: { path: outputPath }
};
}
const inputContents = {};
for (const inputType of config.inputs) {
const inputConfig = SPEC_CONFIGS[inputType];
const inputPath = path.join(specDir, inputConfig.filename);
if (fs.existsSync(inputPath)) {
inputContents[inputConfig.filename] = fs.readFileSync(
inputPath,
"utf-8"
);
}
}
const content = this.renderTemplate(type, title, inputContents);
fs.mkdirSync(specDir, { recursive: true });
fs.writeFileSync(outputPath, content, "utf-8");
logger.info(`Generated spec: ${config.filename}`, { type, title });
return {
success: true,
message: `Created ${config.filename}`,
data: {
path: outputPath,
type,
sections: config.sections,
inputsUsed: Object.keys(inputContents)
},
action: `Generated ${SPEC_DIR}/${config.filename}`
};
}
/** List existing spec documents */
async list() {
const specDir = path.join(this.baseDir, SPEC_DIR);
const specs = [];
for (const [type, config] of Object.entries(SPEC_CONFIGS)) {
const filePath = path.join(specDir, config.filename);
specs.push({
type,
filename: config.filename,
exists: fs.existsSync(filePath),
path: filePath
});
}
const existing = specs.filter((s) => s.exists);
const missing = specs.filter((s) => !s.exists);
return {
success: true,
message: `${existing.length}/${specs.length} specs exist`,
data: { specs, existing, missing }
};
}
/** Update a spec — primarily for checking off PROMPT_PLAN items */
async update(filePath, changes) {
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(this.baseDir, filePath);
if (!fs.existsSync(resolvedPath)) {
return { success: false, message: `File not found: ${resolvedPath}` };
}
let content = fs.readFileSync(resolvedPath, "utf-8");
const checkboxPattern = /^(Stage [A-G])(?::(\d+))?$/;
const match = changes.match(checkboxPattern);
if (match) {
const [, stageName, itemNum] = match;
content = this.checkItem(
content,
stageName,
itemNum ? parseInt(itemNum) : void 0
);
} else {
const escaped = changes.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`- \\[ \\] ${escaped}`, "i");
if (re.test(content)) {
content = content.replace(re, `- [x] ${changes}`);
} else {
return {
success: false,
message: `No unchecked item matching "${changes}" found`
};
}
}
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
const changelog = `
<!-- Updated: ${timestamp} | ${changes} -->
`;
content += changelog;
fs.writeFileSync(resolvedPath, content, "utf-8");
logger.info("Updated spec", { path: resolvedPath, changes });
return {
success: true,
message: `Updated: ${changes}`,
data: { path: resolvedPath, changes },
action: `Checked off item in ${path.basename(resolvedPath)}`
};
}
/** Validate completeness of a spec */
async validate(filePath) {
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(this.baseDir, filePath);
if (!fs.existsSync(resolvedPath)) {
return { success: false, message: `File not found: ${resolvedPath}` };
}
const content = fs.readFileSync(resolvedPath, "utf-8");
const unchecked = (content.match(/- \[ \]/g) || []).length;
const checked = (content.match(/- \[x\]/gi) || []).length;
const total = checked + unchecked;
const emptySections = [];
const sectionRegex = /^## (.+)$/gm;
let sectionMatch;
while ((sectionMatch = sectionRegex.exec(content)) !== null) {
const sectionName = sectionMatch[1];
const sectionStart = content.indexOf(sectionMatch[0]);
const nextSection = content.indexOf("\n## ", sectionStart + 1);
const sectionContent = nextSection === -1 ? content.slice(sectionStart + sectionMatch[0].length) : content.slice(sectionStart + sectionMatch[0].length, nextSection);
const stripped = sectionContent.replace(/<!--.*?-->/gs, "").replace(/\s+/g, "").trim();
if (stripped.length === 0 || stripped === "||") {
emptySections.push(sectionName);
}
}
const isComplete = unchecked === 0 && emptySections.length === 0;
return {
success: true,
message: isComplete ? "Spec is complete" : `Spec incomplete: ${unchecked} unchecked items, ${emptySections.length} empty sections`,
data: {
path: resolvedPath,
checkboxes: { checked, unchecked, total },
emptySections,
isComplete,
completionPercent: total > 0 ? Math.round(checked / total * 100) : 100
}
};
}
// --- Private helpers ---
renderTemplate(type, title, inputs) {
switch (type) {
case "one-pager":
return onePagerTemplate(title);
case "dev-spec":
return devSpecTemplate(
title,
inputs["ONE_PAGER.md"] || "*ONE_PAGER.md not found \u2014 generate it first.*"
);
case "prompt-plan":
return promptPlanTemplate(
title,
inputs["ONE_PAGER.md"] || "*ONE_PAGER.md not found*",
inputs["DEV_SPEC.md"] || "*DEV_SPEC.md not found \u2014 generate it first.*"
);
case "agents":
return agentsTemplate(title, inputs);
default:
return `# ${title}
Unknown spec type: ${type}
`;
}
}
/** Check off a specific checkbox item in a stage */
checkItem(content, stageName, itemIndex) {
const lines = content.split("\n");
let inStage = false;
let itemCount = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("## ") && lines[i].includes(stageName)) {
inStage = true;
itemCount = 0;
continue;
}
if (inStage && lines[i].startsWith("## ")) {
break;
}
if (inStage && lines[i].match(/^- \[ \]/)) {
itemCount++;
if (itemIndex === void 0 || itemCount === itemIndex) {
lines[i] = lines[i].replace("- [ ]", "- [x]");
if (itemIndex !== void 0) break;
}
}
}
return lines.join("\n");
}
}
export {
SpecGeneratorSkill
};