@stackmemoryai/stackmemory 1.9.0 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/orchestrator.js +27 -6
- package/dist/src/cli/commands/ralph.js +43 -0
- package/dist/src/cli/commands/rules.js +250 -0
- package/dist/src/cli/commands/skill.js +406 -0
- package/dist/src/cli/index.js +64 -0
- package/dist/src/core/config/config-manager.js +2 -1
- package/dist/src/core/rules/built-in-rules.js +289 -0
- package/dist/src/core/rules/pr-review-rule.js +87 -0
- package/dist/src/core/rules/rule-engine.js +85 -0
- package/dist/src/core/rules/rule-store.js +99 -0
- package/dist/src/core/rules/types.js +4 -0
- package/dist/src/core/skills/index.js +21 -0
- package/dist/src/core/skills/skill-matcher.js +178 -0
- package/dist/src/core/skills/skill-registry.js +646 -0
- package/dist/src/core/skills/types.js +1 -47
- package/dist/src/core/storage/obsidian-vault-adapter.js +392 -0
- package/dist/src/integrations/mcp/handlers/skill-handlers.js +67 -167
- package/dist/src/integrations/mcp/server.js +2 -6
- package/dist/src/integrations/ralph/loopmax.js +488 -0
- package/package.json +2 -2
|
@@ -1255,11 +1255,10 @@ class Conductor {
|
|
|
1255
1255
|
const claudeBin = this.findClaudeBinary();
|
|
1256
1256
|
const args = [
|
|
1257
1257
|
"-p",
|
|
1258
|
+
"--bare",
|
|
1258
1259
|
"--output-format",
|
|
1259
1260
|
"stream-json",
|
|
1260
|
-
"--dangerously-skip-permissions"
|
|
1261
|
-
"--settings",
|
|
1262
|
-
'{"hooks":{}}'
|
|
1261
|
+
"--dangerously-skip-permissions"
|
|
1263
1262
|
];
|
|
1264
1263
|
if (selectedModel) {
|
|
1265
1264
|
args.push("--model", selectedModel);
|
|
@@ -1594,10 +1593,32 @@ class Conductor {
|
|
|
1594
1593
|
);
|
|
1595
1594
|
}
|
|
1596
1595
|
const priorContext = contextParts.join("\n");
|
|
1597
|
-
|
|
1596
|
+
const templateDir = join(
|
|
1597
|
+
__dirname,
|
|
1598
|
+
"..",
|
|
1599
|
+
"..",
|
|
1600
|
+
"..",
|
|
1601
|
+
"scripts",
|
|
1602
|
+
"conductor",
|
|
1603
|
+
"templates"
|
|
1604
|
+
);
|
|
1605
|
+
const labelNames = issue.labels.map((l) => l.name.toLowerCase());
|
|
1606
|
+
let templateName = "implement-feature.md";
|
|
1607
|
+
if (labelNames.includes("bug") || labelNames.includes("fix") || /\bfix\b|\bbug\b/i.test(issue.title)) {
|
|
1608
|
+
templateName = "fix-bug.md";
|
|
1609
|
+
} else if (labelNames.includes("test") || labelNames.includes("coverage") || /\btest\b|\bcoverage\b/i.test(issue.title)) {
|
|
1610
|
+
templateName = "write-tests.md";
|
|
1611
|
+
}
|
|
1612
|
+
const typedTemplatePath = join(templateDir, templateName);
|
|
1613
|
+
const selectedPath = existsSync(typedTemplatePath) ? typedTemplatePath : templatePath;
|
|
1614
|
+
if (existsSync(selectedPath)) {
|
|
1598
1615
|
try {
|
|
1599
|
-
let template = readFileSync(
|
|
1600
|
-
template = template.replace(
|
|
1616
|
+
let template = readFileSync(selectedPath, "utf-8");
|
|
1617
|
+
template = template.replace(/^---[\s\S]*?---\n?/, "");
|
|
1618
|
+
template = template.replace(/\{\{ISSUE_ID\}\}/g, issue.identifier).replace(/\{\{TITLE\}\}/g, issue.title).replace(/\{\{DESCRIPTION\}\}/g, issue.description || "").replace(/\{\{LABELS\}\}/g, labels).replace(/\{\{PRIORITY\}\}/g, priority).replace(
|
|
1619
|
+
/\{\{SCOPE\}\}/g,
|
|
1620
|
+
issue.identifier.toLowerCase().replace(/-\d+$/, "")
|
|
1621
|
+
).replace(/\{\{ATTEMPT\}\}/g, String(attempt)).replace(/\{\{PRIOR_CONTEXT\}\}/g, priorContext);
|
|
1601
1622
|
return template;
|
|
1602
1623
|
} catch {
|
|
1603
1624
|
}
|
|
@@ -13,6 +13,7 @@ import { ralphDebugger } from "../../integrations/ralph/visualization/ralph-debu
|
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
14
14
|
import { trace } from "../../core/trace/index.js";
|
|
15
15
|
import { SystemError, ErrorCode } from "../../core/errors/index.js";
|
|
16
|
+
import { LoopMaxRunner } from "../../integrations/ralph/loopmax.js";
|
|
16
17
|
function createRalphCommand() {
|
|
17
18
|
const ralph = new Command("ralph").description(
|
|
18
19
|
"Ralph Wiggum Loop integration with StackMemory"
|
|
@@ -1001,6 +1002,48 @@ ${contextResponse.context}`;
|
|
|
1001
1002
|
);
|
|
1002
1003
|
}
|
|
1003
1004
|
);
|
|
1005
|
+
ralph.command("loopmax").description(
|
|
1006
|
+
"Aggressive autonomous loop: no planning, just go until tests pass"
|
|
1007
|
+
).argument("<task>", "What to accomplish").option(
|
|
1008
|
+
"-c, --criteria <criteria>",
|
|
1009
|
+
"Completion criteria",
|
|
1010
|
+
"All tests pass, lint clean, build succeeds"
|
|
1011
|
+
).option("--no-worktree", "Skip git worktree isolation (work in-place)").option("--max-loops <n>", "Max loop iterations (0=infinite)", "0").option("--max-stuck <n>", "Respawn after N stuck loops", "3").option("--commit-every <n>", "Auto-commit every N tool calls", "25").option("--model <model>", "Claude model to use", "sonnet").action(
|
|
1012
|
+
async (task, options) => {
|
|
1013
|
+
return trace.command(
|
|
1014
|
+
"ralph-loopmax",
|
|
1015
|
+
{ task, ...options },
|
|
1016
|
+
async () => {
|
|
1017
|
+
try {
|
|
1018
|
+
console.log("LOOPMAX MODE ACTIVATED");
|
|
1019
|
+
console.log(`Task: ${task}`);
|
|
1020
|
+
console.log(`Criteria: ${options.criteria}`);
|
|
1021
|
+
console.log(
|
|
1022
|
+
`Worktree: ${options.worktree !== false ? "yes" : "no"}`
|
|
1023
|
+
);
|
|
1024
|
+
console.log(`Max loops: ${options.maxLoops || "infinite"}`);
|
|
1025
|
+
console.log("");
|
|
1026
|
+
const runner = new LoopMaxRunner({
|
|
1027
|
+
task,
|
|
1028
|
+
criteria: options.criteria,
|
|
1029
|
+
useWorktree: options.worktree !== false,
|
|
1030
|
+
maxLoops: parseInt(options.maxLoops) || 0,
|
|
1031
|
+
maxStuckBeforeRespawn: parseInt(options.maxStuck) || 3,
|
|
1032
|
+
commitEvery: parseInt(options.commitEvery) || 25,
|
|
1033
|
+
model: options.model,
|
|
1034
|
+
verbose: true
|
|
1035
|
+
});
|
|
1036
|
+
await runner.run();
|
|
1037
|
+
await runner.cleanup();
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
logger.error("LoopMax failed", error);
|
|
1040
|
+
console.error("LoopMax crashed:", error.message);
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
);
|
|
1004
1047
|
return ralph;
|
|
1005
1048
|
}
|
|
1006
1049
|
var ralph_default = createRalphCommand;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import { RuleEngine } from "../../core/rules/rule-engine.js";
|
|
11
|
+
import { filterByScope } from "../../core/rules/built-in-rules.js";
|
|
12
|
+
function getDb() {
|
|
13
|
+
const smDir = path.join(process.cwd(), ".stackmemory");
|
|
14
|
+
if (!fs.existsSync(smDir)) {
|
|
15
|
+
fs.mkdirSync(smDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
return new Database(path.join(smDir, "context.db"));
|
|
18
|
+
}
|
|
19
|
+
function severityColor(severity) {
|
|
20
|
+
switch (severity) {
|
|
21
|
+
case "error":
|
|
22
|
+
return chalk.red;
|
|
23
|
+
case "warn":
|
|
24
|
+
return chalk.yellow;
|
|
25
|
+
case "info":
|
|
26
|
+
return chalk.blue;
|
|
27
|
+
default:
|
|
28
|
+
return chalk.gray;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function severityIcon(severity) {
|
|
32
|
+
switch (severity) {
|
|
33
|
+
case "error":
|
|
34
|
+
return "x";
|
|
35
|
+
case "warn":
|
|
36
|
+
return "!";
|
|
37
|
+
case "info":
|
|
38
|
+
return "i";
|
|
39
|
+
default:
|
|
40
|
+
return "-";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function createRulesCommand() {
|
|
44
|
+
const cmd = new Command("rule").description(
|
|
45
|
+
"Manage project rules (lint, commit, migration checks)"
|
|
46
|
+
);
|
|
47
|
+
cmd.command("list").description("List configured rules").option("-t, --trigger <type>", "Filter by trigger type").option("-a, --all", "Include disabled rules").option("--json", "Output as JSON").action((options) => {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
try {
|
|
50
|
+
const engine = new RuleEngine(db);
|
|
51
|
+
const rules = engine.listRules({
|
|
52
|
+
trigger: options.trigger,
|
|
53
|
+
enabled: options.all ? false : void 0
|
|
54
|
+
});
|
|
55
|
+
if (options.json) {
|
|
56
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (rules.length === 0) {
|
|
60
|
+
console.log(chalk.gray("No rules found."));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log(chalk.cyan(`
|
|
64
|
+
Rules (${rules.length})
|
|
65
|
+
`));
|
|
66
|
+
for (const rule of rules) {
|
|
67
|
+
const enabled = rule.enabled ? chalk.green("on") : chalk.gray("off");
|
|
68
|
+
const sev = severityColor(rule.severity)(rule.severity.toUpperCase());
|
|
69
|
+
const builtin = rule.builtin ? chalk.gray(" [built-in]") : "";
|
|
70
|
+
console.log(
|
|
71
|
+
` ${enabled} ${sev} ${chalk.white(rule.id)}${builtin}`
|
|
72
|
+
);
|
|
73
|
+
console.log(` ${chalk.gray(rule.description)}`);
|
|
74
|
+
console.log(
|
|
75
|
+
` trigger: ${rule.trigger_type} scope: ${rule.scope}`
|
|
76
|
+
);
|
|
77
|
+
console.log();
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
cmd.command("check").description("Run rules against files or commit message").option("-t, --trigger <type>", "Trigger type filter", "on-demand").option("-f, --files <glob>", "File glob to check").option("-m, --commit-message <msg>", "Commit message to check").option("--all", "Run all rules regardless of trigger").option("--json", "Output as JSON").action(
|
|
84
|
+
(options) => {
|
|
85
|
+
const db = getDb();
|
|
86
|
+
try {
|
|
87
|
+
const engine = new RuleEngine(db);
|
|
88
|
+
const projectRoot = process.cwd();
|
|
89
|
+
let files = [];
|
|
90
|
+
if (options.files) {
|
|
91
|
+
files = collectFiles(projectRoot, options.files);
|
|
92
|
+
}
|
|
93
|
+
const content = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const fullPath = path.isAbsolute(file) ? file : path.join(projectRoot, file);
|
|
96
|
+
try {
|
|
97
|
+
content.set(file, fs.readFileSync(fullPath, "utf-8"));
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const ctx = {
|
|
102
|
+
trigger: options.trigger ?? "on-demand",
|
|
103
|
+
files,
|
|
104
|
+
content,
|
|
105
|
+
commitMessage: options.commitMessage ?? "",
|
|
106
|
+
projectRoot
|
|
107
|
+
};
|
|
108
|
+
const result = options.all ? engine.evaluateAll(ctx) : engine.evaluate(ctx);
|
|
109
|
+
if (options.json) {
|
|
110
|
+
console.log(JSON.stringify(result, null, 2));
|
|
111
|
+
process.exitCode = result.passed ? 0 : 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (result.passed) {
|
|
115
|
+
console.log(chalk.green("\n All rules passed.\n"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.red(`
|
|
120
|
+
${result.violations.length} violation(s) found
|
|
121
|
+
`)
|
|
122
|
+
);
|
|
123
|
+
for (const v of result.violations) {
|
|
124
|
+
const icon = severityIcon(v.severity);
|
|
125
|
+
const color = severityColor(v.severity);
|
|
126
|
+
const loc = v.file ? `${v.file}${v.line ? `:${v.line}` : ""}` : "";
|
|
127
|
+
console.log(` ${color(`[${icon}]`)} ${chalk.white(v.ruleName)}`);
|
|
128
|
+
console.log(` ${v.message}`);
|
|
129
|
+
if (loc) console.log(` ${chalk.gray(loc)}`);
|
|
130
|
+
if (v.suggestion) console.log(` ${chalk.cyan(v.suggestion)}`);
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
const errors = result.violations.filter(
|
|
134
|
+
(v) => v.severity === "error"
|
|
135
|
+
);
|
|
136
|
+
if (errors.length > 0) {
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
}
|
|
139
|
+
} finally {
|
|
140
|
+
db.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
cmd.command("enable <id>").description("Enable a rule").action((id) => {
|
|
145
|
+
const db = getDb();
|
|
146
|
+
try {
|
|
147
|
+
const engine = new RuleEngine(db);
|
|
148
|
+
if (engine.enableRule(id)) {
|
|
149
|
+
console.log(chalk.green(`Rule '${id}' enabled.`));
|
|
150
|
+
} else {
|
|
151
|
+
console.log(chalk.red(`Rule '${id}' not found.`));
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
db.close();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
cmd.command("disable <id>").description("Disable a rule").action((id) => {
|
|
159
|
+
const db = getDb();
|
|
160
|
+
try {
|
|
161
|
+
const engine = new RuleEngine(db);
|
|
162
|
+
if (engine.disableRule(id)) {
|
|
163
|
+
console.log(chalk.yellow(`Rule '${id}' disabled.`));
|
|
164
|
+
} else {
|
|
165
|
+
console.log(chalk.red(`Rule '${id}' not found.`));
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
db.close();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
cmd.command("seed").description("Re-seed built-in rules (useful after upgrades)").action(() => {
|
|
173
|
+
const db = getDb();
|
|
174
|
+
try {
|
|
175
|
+
const engine = new RuleEngine(db);
|
|
176
|
+
const rules = engine.listRules();
|
|
177
|
+
const builtins = rules.filter((r) => r.builtin);
|
|
178
|
+
console.log(chalk.green(`Seeded ${builtins.length} built-in rules.`));
|
|
179
|
+
} finally {
|
|
180
|
+
db.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
cmd.command("add <id>").description("Add a custom rule (metadata only)").requiredOption("-n, --name <name>", "Rule display name").option("-d, --description <desc>", "Rule description", "").option("-t, --trigger <type>", "Trigger type", "on-demand").option("-s, --severity <level>", "Severity level", "warn").option("--scope <glob>", "File scope glob", "**/*").action(
|
|
184
|
+
(id, options) => {
|
|
185
|
+
const db = getDb();
|
|
186
|
+
try {
|
|
187
|
+
const engine = new RuleEngine(db);
|
|
188
|
+
engine.getStore().upsert({
|
|
189
|
+
id,
|
|
190
|
+
name: options.name,
|
|
191
|
+
description: options.description,
|
|
192
|
+
trigger_type: options.trigger,
|
|
193
|
+
severity: options.severity,
|
|
194
|
+
scope: options.scope,
|
|
195
|
+
enabled: 1,
|
|
196
|
+
builtin: 0
|
|
197
|
+
});
|
|
198
|
+
console.log(chalk.green(`Rule '${id}' added.`));
|
|
199
|
+
} finally {
|
|
200
|
+
db.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
return cmd;
|
|
205
|
+
}
|
|
206
|
+
function collectFiles(root, pattern) {
|
|
207
|
+
const results = [];
|
|
208
|
+
if (pattern.includes("*")) {
|
|
209
|
+
walkDir(root, root, pattern, results);
|
|
210
|
+
} else {
|
|
211
|
+
const fullPath = path.join(root, pattern);
|
|
212
|
+
if (fs.existsSync(fullPath)) {
|
|
213
|
+
const stat = fs.statSync(fullPath);
|
|
214
|
+
if (stat.isFile()) {
|
|
215
|
+
results.push(pattern);
|
|
216
|
+
} else if (stat.isDirectory()) {
|
|
217
|
+
walkDir(fullPath, root, "**/*", results);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
function walkDir(dir, root, pattern, results) {
|
|
224
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
225
|
+
"node_modules",
|
|
226
|
+
".git",
|
|
227
|
+
"dist",
|
|
228
|
+
"coverage",
|
|
229
|
+
".stackmemory"
|
|
230
|
+
]);
|
|
231
|
+
try {
|
|
232
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (SKIP.has(entry.name)) continue;
|
|
235
|
+
const fullPath = path.join(dir, entry.name);
|
|
236
|
+
const relPath = path.relative(root, fullPath);
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
walkDir(fullPath, root, pattern, results);
|
|
239
|
+
} else if (entry.isFile()) {
|
|
240
|
+
if (filterByScope([relPath], pattern).length > 0) {
|
|
241
|
+
results.push(relPath);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export {
|
|
249
|
+
createRulesCommand
|
|
250
|
+
};
|