@stackmemoryai/stackmemory 1.9.0 → 1.10.2

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.
@@ -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
- if (existsSync(templatePath)) {
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(templatePath, "utf-8");
1600
- 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(/\{\{ATTEMPT\}\}/g, String(attempt)).replace(/\{\{PRIOR_CONTEXT\}\}/g, priorContext);
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
+ };