@stackmemoryai/stackmemory 1.5.9 → 1.6.1

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.
Files changed (40) hide show
  1. package/dist/src/cli/commands/orchestrate.js +506 -33
  2. package/dist/src/cli/commands/orchestrator.js +208 -31
  3. package/dist/src/daemon/daemon-config.js +2 -3
  4. package/dist/src/daemon/session-daemon.js +4 -5
  5. package/dist/src/daemon/unified-daemon.js +4 -5
  6. package/dist/src/features/sweep/sweep-server-manager.js +3 -6
  7. package/dist/src/hooks/daemon.js +5 -8
  8. package/dist/src/utils/process-cleanup.js +9 -0
  9. package/package.json +1 -2
  10. package/scripts/gepa/.before-optimize.md +0 -112
  11. package/scripts/gepa/README.md +0 -275
  12. package/scripts/gepa/config.json +0 -59
  13. package/scripts/gepa/evals/coding-tasks.jsonl +0 -5
  14. package/scripts/gepa/evals/fixtures/api-endpoint.ts +0 -31
  15. package/scripts/gepa/evals/fixtures/brittle-integration.ts +0 -38
  16. package/scripts/gepa/evals/fixtures/buggy-loop.js +0 -18
  17. package/scripts/gepa/evals/fixtures/callback-hell.js +0 -53
  18. package/scripts/gepa/evals/fixtures/fts5-triggers.sql +0 -23
  19. package/scripts/gepa/evals/fixtures/leaky-service.ts +0 -70
  20. package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +0 -39
  21. package/scripts/gepa/evals/fixtures/pr-diff.txt +0 -24
  22. package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +0 -34
  23. package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +0 -42
  24. package/scripts/gepa/evals/stackmemory-tasks.jsonl +0 -8
  25. package/scripts/gepa/generations/gen-000/baseline.md +0 -112
  26. package/scripts/gepa/generations/gen-001/baseline.md +0 -112
  27. package/scripts/gepa/generations/gen-001/variant-a.md +0 -107
  28. package/scripts/gepa/generations/gen-001/variant-b.md +0 -216
  29. package/scripts/gepa/generations/gen-001/variant-c.md +0 -83
  30. package/scripts/gepa/generations/gen-001/variant-d.md +0 -90
  31. package/scripts/gepa/hooks/auto-optimize.js +0 -494
  32. package/scripts/gepa/hooks/eval-tracker.js +0 -203
  33. package/scripts/gepa/hooks/reflect.js +0 -350
  34. package/scripts/gepa/optimize.js +0 -853
  35. package/scripts/gepa/results/eval-1-baseline.json +0 -218
  36. package/scripts/gepa/results/eval-1-variant-a.json +0 -218
  37. package/scripts/gepa/results/eval-1-variant-b.json +0 -218
  38. package/scripts/gepa/results/eval-1-variant-c.json +0 -218
  39. package/scripts/gepa/results/eval-1-variant-d.json +0 -218
  40. package/scripts/gepa/state.json +0 -49
@@ -20,6 +20,7 @@ import { createInterface } from "readline";
20
20
  import { fileURLToPath } from "url";
21
21
  import { Transform } from "stream";
22
22
  import { logger } from "../../core/monitoring/logger.js";
23
+ import { isProcessAlive } from "../../utils/process-cleanup.js";
23
24
  import {
24
25
  LinearClient
25
26
  } from "../../integrations/linear/client.js";
@@ -37,6 +38,86 @@ function logAgentOutcome(entry) {
37
38
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
38
39
  appendFileSync(getOutcomesLogPath(), JSON.stringify(entry) + "\n");
39
40
  }
41
+ function getRetryStrategy(issue, outcomes) {
42
+ if (!outcomes) {
43
+ const logPath = getOutcomesLogPath();
44
+ if (!existsSync(logPath)) {
45
+ return { shouldRetry: true, adjustments: [] };
46
+ }
47
+ try {
48
+ const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
49
+ outcomes = lines.map((l) => JSON.parse(l));
50
+ } catch {
51
+ return { shouldRetry: true, adjustments: [] };
52
+ }
53
+ }
54
+ const issueOutcomes = outcomes.filter((o) => o.issue === issue);
55
+ const relevant = issueOutcomes.length > 0 ? issueOutcomes : outcomes;
56
+ const failures = relevant.filter(
57
+ (o) => o.outcome === "failure" || o.outcome === "partial"
58
+ );
59
+ if (failures.length === 0) {
60
+ return { shouldRetry: true, adjustments: [] };
61
+ }
62
+ const lastFailure = failures[failures.length - 1];
63
+ if (lastFailure.errorTail && /429|rate.?limit|too many requests|usage.?limit|overloaded/i.test(
64
+ lastFailure.errorTail
65
+ )) {
66
+ return {
67
+ shouldRetry: false,
68
+ reason: "Last failure was a rate limit (429) \u2014 retrying immediately will not help",
69
+ adjustments: []
70
+ };
71
+ }
72
+ if (issueOutcomes.length > 0) {
73
+ const issueFailures = issueOutcomes.filter(
74
+ (o) => o.outcome === "failure" || o.outcome === "partial"
75
+ );
76
+ const phaseFailCounts = /* @__PURE__ */ new Map();
77
+ for (const f of issueFailures) {
78
+ phaseFailCounts.set(f.phase, (phaseFailCounts.get(f.phase) || 0) + 1);
79
+ }
80
+ for (const [phase, count] of phaseFailCounts) {
81
+ if (count >= 2) {
82
+ return {
83
+ shouldRetry: false,
84
+ reason: `Issue has failed ${count} times in '${phase}' phase \u2014 likely a structural problem`,
85
+ adjustments: []
86
+ };
87
+ }
88
+ }
89
+ }
90
+ const adjustments = [];
91
+ if (lastFailure.errorTail) {
92
+ const tail = lastFailure.errorTail;
93
+ if (/timeout|timed?.?out|ETIMEDOUT|ESOCKETTIMEDOUT/i.test(tail)) {
94
+ adjustments.push(
95
+ "Previous attempt timed out. Work in smaller increments and commit partial progress early."
96
+ );
97
+ }
98
+ if (/lint|eslint|prettier|formatting|style.?error/i.test(tail)) {
99
+ adjustments.push(
100
+ "Previous attempt failed on linting. Run `npm run lint:fix` after each file change and fix all lint errors before committing."
101
+ );
102
+ }
103
+ if (/test.?fail|assertion|expect|vitest|jest|FAIL/i.test(tail)) {
104
+ adjustments.push(
105
+ "Previous attempt failed tests. Run tests incrementally after each change. Check existing test expectations before modifying code."
106
+ );
107
+ }
108
+ if (/build.?fail|tsc|type.?error|TS\d{4}|compile/i.test(tail)) {
109
+ adjustments.push(
110
+ "Previous attempt had build/type errors. Run `npm run build` after changes and fix type errors before committing."
111
+ );
112
+ }
113
+ if (/cannot find module|ERR_MODULE_NOT_FOUND|import/i.test(tail)) {
114
+ adjustments.push(
115
+ "Previous attempt had module resolution errors. Ensure all imports use .js extensions for relative paths (ESM)."
116
+ );
117
+ }
118
+ }
119
+ return { shouldRetry: true, adjustments };
120
+ }
40
121
  function findPackageRoot() {
41
122
  const currentFile = fileURLToPath(import.meta.url);
42
123
  let dir = dirname(currentFile);
@@ -135,6 +216,52 @@ function inferPhase(msg) {
135
216
  }
136
217
  return null;
137
218
  }
219
+ const SIMPLE_LABELS = ["bug", "fix", "typo", "chore", "docs", "hotfix"];
220
+ const COMPLEX_LABELS = [
221
+ "feature",
222
+ "refactor",
223
+ "architecture",
224
+ "migration",
225
+ "security",
226
+ "performance"
227
+ ];
228
+ function estimateIssueComplexity(issue, attempt) {
229
+ let score = 0;
230
+ const descLen = (issue.description || "").length;
231
+ if (descLen > 800) score += 2;
232
+ else if (descLen > 400) score += 1;
233
+ else if (descLen < 200) score -= 1;
234
+ const labelNames = (issue.labels || []).map((l) => l.name.toLowerCase());
235
+ for (const label of labelNames) {
236
+ if (SIMPLE_LABELS.some((s) => label.includes(s))) score -= 1;
237
+ if (COMPLEX_LABELS.some((c) => label.includes(c))) score += 2;
238
+ }
239
+ const titleLower = issue.title.toLowerCase();
240
+ if (SIMPLE_LABELS.some((s) => titleLower.includes(s))) score -= 1;
241
+ if (COMPLEX_LABELS.some((c) => titleLower.includes(c))) score += 1;
242
+ if (issue.priority === 1) score += 2;
243
+ else if (issue.priority === 2) score += 1;
244
+ if (issue.estimate) {
245
+ if (issue.estimate >= 5) score += 2;
246
+ else if (issue.estimate >= 3) score += 1;
247
+ else if (issue.estimate <= 1) score -= 1;
248
+ }
249
+ if (attempt && attempt > 1) score += 2;
250
+ if (score >= 3) return "complex";
251
+ if (score >= 1) return "moderate";
252
+ return "simple";
253
+ }
254
+ function selectModelForIssue(complexity, config) {
255
+ const modelSetting = config.model || "auto";
256
+ if (modelSetting !== "auto") return modelSetting;
257
+ switch (complexity) {
258
+ case "simple":
259
+ case "moderate":
260
+ return "claude-sonnet-4-20250514";
261
+ case "complex":
262
+ return "claude-opus-4-20250514";
263
+ }
264
+ }
138
265
  const DEFAULT_CONFIG = {
139
266
  activeStates: ["Todo"],
140
267
  terminalStates: ["Done", "Cancelled", "Canceled", "Closed"],
@@ -654,7 +781,8 @@ class Conductor {
654
781
  filesModified: run.filesModified,
655
782
  tokensUsed: run.tokensUsed,
656
783
  durationMs: Date.now() - run.startedAt,
657
- hasCommits: true
784
+ hasCommits: true,
785
+ labels: issue.labels.map((l) => l.name)
658
786
  });
659
787
  await this.runHook(
660
788
  "after-run",
@@ -677,6 +805,20 @@ class Conductor {
677
805
  error: run.error,
678
806
  attempt: run.attempt
679
807
  });
808
+ logAgentOutcome({
809
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
810
+ issue: issue.identifier,
811
+ attempt: run.attempt,
812
+ outcome: "failure",
813
+ phase: run.phase,
814
+ toolCalls: run.toolCalls,
815
+ filesModified: run.filesModified,
816
+ tokensUsed: run.tokensUsed,
817
+ durationMs: Date.now() - run.startedAt,
818
+ hasCommits: false,
819
+ labels: issue.labels.map((l) => l.name),
820
+ errorTail: run.error?.slice(-500)
821
+ });
680
822
  if (this.handleRateLimitError(run.error, issue.identifier)) {
681
823
  throw err;
682
824
  }
@@ -690,6 +832,23 @@ class Conductor {
690
832
  });
691
833
  }
692
834
  if (run.attempt < maxAttempts && !this.stopping) {
835
+ const strategy = getRetryStrategy(issue.identifier);
836
+ if (!strategy.shouldRetry) {
837
+ console.log(
838
+ `[${issue.identifier}] Skipping retry: ${strategy.reason}`
839
+ );
840
+ logger.info("Retry skipped by intelligence", {
841
+ identifier: issue.identifier,
842
+ reason: strategy.reason
843
+ });
844
+ throw err;
845
+ }
846
+ if (strategy.adjustments.length > 0) {
847
+ run.retryAdjustments = strategy.adjustments;
848
+ console.log(
849
+ `[${issue.identifier}] Retry with adjustments: ${strategy.adjustments.length} hint(s)`
850
+ );
851
+ }
693
852
  console.log(
694
853
  `[${issue.identifier}] Failed (attempt ${run.attempt}), retrying...`
695
854
  );
@@ -1023,25 +1182,33 @@ class Conductor {
1023
1182
  */
1024
1183
  runAgentCLI(issue, run) {
1025
1184
  return new Promise((resolve, reject) => {
1026
- const prompt = this.buildPrompt(issue, run.attempt);
1185
+ const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
1186
+ const complexity = estimateIssueComplexity(issue, run.attempt);
1187
+ const selectedModel = selectModelForIssue(complexity, this.config);
1188
+ logger.info("Agent model selected", {
1189
+ identifier: issue.identifier,
1190
+ complexity,
1191
+ model: selectedModel || "(default)",
1192
+ reason: this.config.model && this.config.model !== "auto" ? "config override" : `auto: ${complexity} issue`
1193
+ });
1027
1194
  const claudeBin = this.findClaudeBinary();
1028
- const proc = spawn(
1029
- claudeBin,
1030
- [
1031
- "-p",
1032
- "--output-format",
1033
- "stream-json",
1034
- "--dangerously-skip-permissions",
1035
- "--settings",
1036
- '{"hooks":{}}',
1037
- prompt
1038
- ],
1039
- {
1040
- cwd: run.workspacePath,
1041
- env: this.buildAgentEnv(issue, run),
1042
- stdio: ["pipe", "pipe", "pipe"]
1043
- }
1044
- );
1195
+ const args = [
1196
+ "-p",
1197
+ "--output-format",
1198
+ "stream-json",
1199
+ "--dangerously-skip-permissions",
1200
+ "--settings",
1201
+ '{"hooks":{}}'
1202
+ ];
1203
+ if (selectedModel) {
1204
+ args.push("--model", selectedModel);
1205
+ }
1206
+ args.push(prompt);
1207
+ const proc = spawn(claudeBin, args, {
1208
+ cwd: run.workspacePath,
1209
+ env: this.buildAgentEnv(issue, run),
1210
+ stdio: ["pipe", "pipe", "pipe"]
1211
+ });
1045
1212
  run.process = proc;
1046
1213
  proc.stdin.end();
1047
1214
  const logStream = this.openAgentLogStream(issue.identifier);
@@ -1145,7 +1312,7 @@ class Conductor {
1145
1312
  */
1146
1313
  runAgentAdapter(issue, run) {
1147
1314
  return new Promise((resolve, reject) => {
1148
- const prompt = this.buildPrompt(issue, run.attempt);
1315
+ const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
1149
1316
  const proc = spawn("node", [this.config.appServerPath], {
1150
1317
  cwd: run.workspacePath,
1151
1318
  env: this.buildAgentEnv(issue, run),
@@ -1300,7 +1467,7 @@ class Conductor {
1300
1467
  * Template variables: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}},
1301
1468
  * {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
1302
1469
  */
1303
- buildPrompt(issue, attempt) {
1470
+ buildPrompt(issue, attempt, retryAdjustments) {
1304
1471
  const templatePath = join(
1305
1472
  homedir(),
1306
1473
  ".stackmemory",
@@ -1309,7 +1476,20 @@ class Conductor {
1309
1476
  );
1310
1477
  const priority = ["None", "Urgent", "High", "Medium", "Low"][issue.priority] || "None";
1311
1478
  const labels = issue.labels.length > 0 ? issue.labels.map((l) => l.name).join(", ") : "";
1312
- const priorContext = attempt > 1 ? `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.` : "";
1479
+ const contextParts = [];
1480
+ if (attempt > 1) {
1481
+ contextParts.push(
1482
+ `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.`
1483
+ );
1484
+ }
1485
+ if (retryAdjustments && retryAdjustments.length > 0) {
1486
+ contextParts.push(
1487
+ "## Retry Adjustments (from prior failure analysis)",
1488
+ "",
1489
+ ...retryAdjustments.map((a) => `- ${a}`)
1490
+ );
1491
+ }
1492
+ const priorContext = contextParts.join("\n");
1313
1493
  if (existsSync(templatePath)) {
1314
1494
  try {
1315
1495
  let template = readFileSync(templatePath, "utf-8");
@@ -1469,14 +1649,7 @@ class Conductor {
1469
1649
  if (elapsed < staleThresholdMs) continue;
1470
1650
  const elapsedMin = Math.round(elapsed / 6e4);
1471
1651
  const pid = run.process?.pid;
1472
- let alive = false;
1473
- if (pid) {
1474
- try {
1475
- process.kill(pid, 0);
1476
- alive = true;
1477
- } catch {
1478
- }
1479
- }
1652
+ const alive = pid ? isProcessAlive(pid) : false;
1480
1653
  if (!alive) {
1481
1654
  logger.warn("Stale agent process is dead, cleaning up", {
1482
1655
  identifier: run.issue.identifier,
@@ -1553,6 +1726,7 @@ class Conductor {
1553
1726
  tokensUsed: run.tokensUsed,
1554
1727
  durationMs,
1555
1728
  hasCommits,
1729
+ labels: run.issue.labels.map((l) => l.name),
1556
1730
  errorTail
1557
1731
  });
1558
1732
  if (hasCommits) {
@@ -1637,6 +1811,9 @@ class Conductor {
1637
1811
  }
1638
1812
  export {
1639
1813
  Conductor,
1814
+ estimateIssueComplexity,
1640
1815
  getAgentStatusDir,
1641
- getOutcomesLogPath
1816
+ getOutcomesLogPath,
1817
+ getRetryStrategy,
1818
+ selectModelForIssue
1642
1819
  };
@@ -5,6 +5,7 @@ const __dirname = __pathDirname(__filename);
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
6
  import { join } from "path";
7
7
  import { homedir } from "os";
8
+ import { isProcessAlive } from "../utils/process-cleanup.js";
8
9
  const DEFAULT_DAEMON_CONFIG = {
9
10
  version: "1.0.0",
10
11
  context: {
@@ -139,9 +140,7 @@ function readDaemonStatus() {
139
140
  try {
140
141
  const pidContent = readFileSync(pidFile, "utf8").trim();
141
142
  const pid = parseInt(pidContent, 10);
142
- try {
143
- process.kill(pid, 0);
144
- } catch {
143
+ if (!isProcessAlive(pid)) {
145
144
  return defaultStatus;
146
145
  }
147
146
  if (!existsSync(statusFile)) {
@@ -6,6 +6,7 @@ const __dirname = __pathDirname(__filename);
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
8
  import { execSync } from "child_process";
9
+ import { isProcessAlive } from "../utils/process-cleanup.js";
9
10
  class SessionDaemon {
10
11
  config;
11
12
  state;
@@ -69,16 +70,14 @@ class SessionDaemon {
69
70
  try {
70
71
  const existingPid = fs.readFileSync(this.pidFile, "utf8").trim();
71
72
  const pid = parseInt(existingPid, 10);
72
- try {
73
- process.kill(pid, 0);
73
+ if (isProcessAlive(pid)) {
74
74
  this.log("WARN", "Daemon already running for this session", {
75
75
  existingPid: pid
76
76
  });
77
77
  return false;
78
- } catch {
79
- this.log("INFO", "Cleaning up stale PID file", { stalePid: pid });
80
- fs.unlinkSync(this.pidFile);
81
78
  }
79
+ this.log("INFO", "Cleaning up stale PID file", { stalePid: pid });
80
+ fs.unlinkSync(this.pidFile);
82
81
  } catch {
83
82
  try {
84
83
  fs.unlinkSync(this.pidFile);
@@ -10,6 +10,7 @@ import {
10
10
  appendFileSync,
11
11
  readFileSync
12
12
  } from "fs";
13
+ import { isProcessAlive } from "../utils/process-cleanup.js";
13
14
  import {
14
15
  loadDaemonConfig,
15
16
  getDaemonPaths,
@@ -75,14 +76,12 @@ class UnifiedDaemon {
75
76
  try {
76
77
  const existingPid = readFileSync(this.paths.pidFile, "utf8").trim();
77
78
  const pid = parseInt(existingPid, 10);
78
- try {
79
- process.kill(pid, 0);
79
+ if (isProcessAlive(pid)) {
80
80
  this.log("WARN", "daemon", "Daemon already running", { pid });
81
81
  return false;
82
- } catch {
83
- this.log("INFO", "daemon", "Cleaning stale PID file", { pid });
84
- unlinkSync(this.paths.pidFile);
85
82
  }
83
+ this.log("INFO", "daemon", "Cleaning stale PID file", { pid });
84
+ unlinkSync(this.paths.pidFile);
86
85
  } catch {
87
86
  try {
88
87
  unlinkSync(this.paths.pidFile);
@@ -10,6 +10,7 @@ import {
10
10
  } from "./types.js";
11
11
  import { createPredictionClient } from "./prediction-client.js";
12
12
  import { logger } from "../../core/monitoring/logger.js";
13
+ import { isProcessAlive } from "../../utils/process-cleanup.js";
13
14
  const HOME = process.env["HOME"] || "/tmp";
14
15
  const PID_FILE = join(HOME, ".stackmemory", "sweep", "server.pid");
15
16
  const LOG_FILE = join(HOME, ".stackmemory", "sweep", "server.log");
@@ -149,9 +150,7 @@ Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-
149
150
  process.kill(status.pid, "SIGTERM");
150
151
  await new Promise((resolve) => {
151
152
  const checkInterval = setInterval(() => {
152
- try {
153
- process.kill(status.pid, 0);
154
- } catch {
153
+ if (!isProcessAlive(status.pid)) {
155
154
  clearInterval(checkInterval);
156
155
  resolve();
157
156
  }
@@ -186,9 +185,7 @@ Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-
186
185
  try {
187
186
  const data = JSON.parse(readFileSync(PID_FILE, "utf-8"));
188
187
  const { pid, port, host, modelPath, startedAt } = data;
189
- try {
190
- process.kill(pid, 0);
191
- } catch {
188
+ if (!isProcessAlive(pid)) {
192
189
  return { running: false };
193
190
  }
194
191
  const client = createPredictionClient({ port, host });
@@ -13,6 +13,7 @@ import {
13
13
  import { join, extname, relative } from "path";
14
14
  import { spawn } from "child_process";
15
15
  import { loadConfig } from "./config.js";
16
+ import { isProcessAlive } from "../utils/process-cleanup.js";
16
17
  import {
17
18
  hookEmitter
18
19
  } from "./events.js";
@@ -51,13 +52,11 @@ async function startDaemon(options = {}) {
51
52
  const pidFile = config.daemon.pid_file;
52
53
  if (existsSync(pidFile)) {
53
54
  const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
54
- try {
55
- process.kill(pid, 0);
55
+ if (isProcessAlive(pid)) {
56
56
  log("warn", "Daemon already running", { pid });
57
57
  return;
58
- } catch {
59
- unlinkSync(pidFile);
60
58
  }
59
+ unlinkSync(pidFile);
61
60
  }
62
61
  if (!options.foreground) {
63
62
  const child = spawn(
@@ -117,17 +116,15 @@ function getDaemonStatus() {
117
116
  return { running: false };
118
117
  }
119
118
  const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
120
- try {
121
- process.kill(pid, 0);
119
+ if (isProcessAlive(pid)) {
122
120
  return {
123
121
  running: true,
124
122
  pid,
125
123
  uptime: state.running ? Date.now() - state.startTime : void 0,
126
124
  eventsProcessed: state.eventsProcessed
127
125
  };
128
- } catch {
129
- return { running: false };
130
126
  }
127
+ return { running: false };
131
128
  }
132
129
  function setupLogStream() {
133
130
  const logFile = config.daemon.log_file;
@@ -7,6 +7,14 @@ import * as fs from "fs";
7
7
  import * as path from "path";
8
8
  import * as os from "os";
9
9
  import { logger } from "../core/monitoring/logger.js";
10
+ function isProcessAlive(pid) {
11
+ try {
12
+ process.kill(pid, 0);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
10
18
  const STACKMEMORY_PROCESS_PATTERNS = [
11
19
  "stackmemory",
12
20
  "ralph orchestrate",
@@ -131,5 +139,6 @@ export {
131
139
  cleanupStaleProcesses,
132
140
  findStaleProcesses,
133
141
  getStackmemoryProcesses,
142
+ isProcessAlive,
134
143
  killStaleProcesses
135
144
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.5.9",
3
+ "version": "1.6.1",
4
4
  "description": "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, and automatic hooks.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
@@ -20,7 +20,6 @@
20
20
  "files": [
21
21
  "bin",
22
22
  "dist/src",
23
- "scripts/gepa",
24
23
  "scripts/git-hooks",
25
24
  "scripts/hooks",
26
25
  "scripts/setup",
@@ -1,112 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- Sol is the monorepo for **Rize**, an automatic time tracking application. The stack consists of:
8
- - **api/** - Rails 7.1 GraphQL backend (Ruby 3.3.5)
9
- - **web/** - Next.js 14 React web app (Node 22)
10
- - **electron/** - Electron desktop app (Node 22)
11
- - **services/** - Bun-based TypeScript event consumers/workers
12
- - **vanity/** - Webflow marketing site scripts (deprecated)
13
- - **voyager/** - Marketing website for home and landing pageas (Next.js)
14
- - **puppet/** - Puppeteer server for images/PDFs
15
- - **chrome/** - Chrome browser extension
16
- - **docs/** - Docusaurus documentation site
17
- - **zapier/** - Zapier integration
18
-
19
- ## Development Commands
20
-
21
- ### Starting Development Environment
22
- ```bash
23
- # Start all services (requires iTerm2 on macOS)
24
- ./scripts/run-dev.sh
25
-
26
- # Or start individually:
27
- cd api && hivemind Procfile.dev # Rails + AnyCable + Sidekiq + Clockwork
28
- cd web && npm run dev # Next.js dev server
29
- cd electron && npm run dev # Electron with hot reload
30
- cd services && hivemind Procfile.dev # Bun services
31
- ```
32
-
33
- ### Docker Dependencies (api/docker-compose.yml)
34
- ```bash
35
- cd api && docker-compose up -d
36
- # TimescaleDB: localhost:15432
37
- # Redis: localhost:16379
38
- # Kafka: localhost:9092
39
- # MySQL: localhost:13306
40
- ```
41
-
42
- ### Testing
43
- ```bash
44
- # API (RSpec)
45
- cd api && bundle exec rspec
46
- cd api && bundle exec rspec spec/path/to/file_spec.rb # Single file
47
- cd api && bundle exec rspec spec/path/to/file_spec.rb:42 # Single test at line
48
-
49
- # Electron (Jest)
50
- cd electron && npm test
51
- cd electron && npm run test:watch
52
- cd electron && npm run test:coverage
53
-
54
- # Web - no active tests (exits 0)
55
- ```
56
-
57
- ### Building
58
- ```bash
59
- cd api && bundle install && rake db:migrate
60
- cd web && npm run build # Runs gql-gen, tailwind, next build
61
- cd electron && npm run build # Electron Forge make
62
- cd services && bun install
63
- ```
64
-
65
- ### GraphQL Code Generation
66
- ```bash
67
- cd web && npm run build # Includes gql codegen
68
- cd electron && npm run dev # Runs gql codegen as part of dev
69
- ```
70
-
71
- ## Architecture
72
-
73
- ### GraphQL API Structure
74
- The API exposes two GraphQL endpoints:
75
- - **api/v1** - Public API (OAuth consumers, Zapier)
76
- - **private/v1** - Private API (web, electron apps)
77
-
78
- Located at `api/app/graphql/{api,private}/v1/`
79
-
80
- ### Real-time Communication
81
- - **AnyCable** WebSocket server for subscriptions
82
- - ActionCable channels in `api/app/channels/`
83
- - WebSocket config: `api/config/cable.yml` and `api/config/anycable.yml`
84
-
85
- ### Background Jobs
86
- - **Sidekiq** for async job processing (`api/config/sidekiq.yml`)
87
- - **Clockwork** for scheduled jobs (`api/config/clock.rb`)
88
-
89
- ### Event Streaming
90
- - **Kafka** for event publishing/consumption
91
- - Services consume events via `services/consumers/`
92
- - Kafka config: `api/config/initializers/kafka.rb`
93
-
94
- ### Databases
95
- - **Primary PostgreSQL** - Main application data
96
- - **TimescaleDB** - Time-series data (separate connection in `database.yml`)
97
- - **MySQL** - Legacy/external integrations
98
- - **Redis** - Caching, ActionCable, Sidekiq
99
-
100
- ## Style Guidelines
101
-
102
- ### JavaScript/TypeScript
103
- - Use `test()` instead of `it()` in tests
104
- - Use `toBeCalled()` instead of `toHaveBeenCalledWith()` in jest assertions
105
-
106
- ## Key Configuration Files
107
-
108
- - `api/config/database.yml` - Database connections (primary + timescale)
109
- - `api/config/cable.yml` - AnyCable WebSocket config
110
- - `api/Procfile.dev` - Development processes (rails, anycable, sidekiq, clockwork)
111
- - `sol.code-workspace` - VS Code multi-folder workspace
112
- - Each project requires its own `.env` file (not in repo)