@stackmemoryai/stackmemory 1.5.9 → 1.6.0

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.
@@ -9,12 +9,14 @@ import {
9
9
  mkdirSync,
10
10
  writeFileSync,
11
11
  readFileSync,
12
- readdirSync
12
+ readdirSync,
13
+ copyFileSync
13
14
  } from "fs";
14
15
  import { join } from "path";
15
16
  import { homedir, tmpdir } from "os";
16
17
  import Database from "better-sqlite3";
17
18
  import { logger } from "../../core/monitoring/logger.js";
19
+ import { isProcessAlive } from "../../utils/process-cleanup.js";
18
20
  import { Conductor } from "./orchestrator.js";
19
21
  import {
20
22
  getAgentStatusDir,
@@ -123,14 +125,6 @@ const phaseColor = {
123
125
  testing: c.pink,
124
126
  committing: c.green
125
127
  };
126
- function isProcessAlive(pid) {
127
- try {
128
- process.kill(pid, 0);
129
- return true;
130
- } catch {
131
- return false;
132
- }
133
- }
134
128
  function phaseProgress(phase, toolCalls, stale, alive) {
135
129
  const basePct = {
136
130
  reading: 10,
@@ -231,6 +225,205 @@ function printUsageSummary(u) {
231
225
  ` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${c.d}~${fmtMinutes(mins20x)} left${c.r}`
232
226
  );
233
227
  }
228
+ const DEFAULT_PROMPT_TEMPLATE = `# Agent Prompt \u2014 {{ISSUE_ID}}
229
+
230
+ You are working on Linear issue **{{ISSUE_ID}}**: {{TITLE}}
231
+
232
+ ## Description
233
+
234
+ {{DESCRIPTION}}
235
+
236
+ ## Context
237
+
238
+ - Priority: {{PRIORITY}}
239
+ - Labels: {{LABELS}}
240
+ {{PRIOR_CONTEXT}}
241
+
242
+ ## Instructions
243
+
244
+ 1. Read the issue description and related code carefully
245
+ 2. Plan your approach before writing code
246
+ 3. Implement the requested changes
247
+ 4. Run \`npm run lint\` and fix any errors
248
+ 5. Run \`npm run test:run\` and fix any failures
249
+ 6. Commit your changes with format: \`type(scope): message\`
250
+
251
+ ## Rules
252
+
253
+ - Follow existing code conventions (ESM imports with .js extension, TypeScript strict)
254
+ - Keep changes focused \u2014 only modify what the issue requires
255
+ - Write or update tests for any new functionality
256
+ - Do not skip pre-commit hooks
257
+ - If stuck, leave a comment in the code explaining the blocker
258
+
259
+ Work in the current directory. All changes will be on a dedicated branch.
260
+ `;
261
+ function ensureDefaultPromptTemplate() {
262
+ const templatePath = join(
263
+ homedir(),
264
+ ".stackmemory",
265
+ "conductor",
266
+ "prompt-template.md"
267
+ );
268
+ if (!existsSync(templatePath)) {
269
+ const dir = join(homedir(), ".stackmemory", "conductor");
270
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
271
+ writeFileSync(templatePath, DEFAULT_PROMPT_TEMPLATE);
272
+ console.log(
273
+ ` ${c.d}Created default prompt template: ${templatePath}${c.r}`
274
+ );
275
+ }
276
+ return templatePath;
277
+ }
278
+ function spawnClaudePrint(prompt, timeoutMs = 12e4) {
279
+ return new Promise((resolve, reject) => {
280
+ const child = cpSpawn("claude", ["--print"], {
281
+ stdio: ["pipe", "pipe", "pipe"],
282
+ env: { ...process.env }
283
+ });
284
+ let stdout = "";
285
+ let stderr = "";
286
+ let killed = false;
287
+ const timer = setTimeout(() => {
288
+ killed = true;
289
+ child.kill("SIGTERM");
290
+ }, timeoutMs);
291
+ child.stdout.on("data", (d) => stdout += d.toString());
292
+ child.stderr.on("data", (d) => stderr += d.toString());
293
+ child.on("close", (code) => {
294
+ clearTimeout(timer);
295
+ if (killed)
296
+ return reject(new Error(`claude timed out after ${timeoutMs}ms`));
297
+ if (code !== 0 && !stdout)
298
+ return reject(new Error(stderr || `claude exited ${code}`));
299
+ resolve(stdout);
300
+ });
301
+ child.on("error", (err) => {
302
+ clearTimeout(timer);
303
+ reject(err);
304
+ });
305
+ child.stdin.write(prompt);
306
+ child.stdin.end();
307
+ });
308
+ }
309
+ async function evolvePromptTemplate(input) {
310
+ const {
311
+ templatePath,
312
+ successRate,
313
+ failures,
314
+ failPhases,
315
+ errorPatterns,
316
+ recs,
317
+ outcomes
318
+ } = input;
319
+ let currentTemplate;
320
+ if (existsSync(templatePath)) {
321
+ currentTemplate = readFileSync(templatePath, "utf-8");
322
+ } else {
323
+ currentTemplate = DEFAULT_PROMPT_TEMPLATE;
324
+ const dir = join(homedir(), ".stackmemory", "conductor");
325
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
326
+ writeFileSync(templatePath, currentTemplate);
327
+ }
328
+ const failPhaseSummary = Object.entries(failPhases).sort((a, b) => b[1] - a[1]).map(([phase, count]) => ` - ${phase}: ${count} failures`).join("\n");
329
+ const errorSummary = Object.entries(errorPatterns).sort((a, b) => b[1] - a[1]).map(([pattern, count]) => ` - ${pattern}: ${count} occurrences`).join("\n");
330
+ const failedOutcomes = outcomes.filter((o) => o.outcome === "failure" && o.errorTail).slice(-5);
331
+ const errorTails = failedOutcomes.map(
332
+ (o) => ` [${o.issue} attempt ${o.attempt}, phase: ${o.phase}]
333
+ ${o.errorTail}`
334
+ ).join("\n\n");
335
+ const mutationPrompt = `You are optimizing a prompt template for autonomous AI coding agents managed by a conductor system.
336
+
337
+ CURRENT PROMPT TEMPLATE:
338
+ \`\`\`markdown
339
+ ${currentTemplate}
340
+ \`\`\`
341
+
342
+ PERFORMANCE DATA:
343
+ - Success rate: ${successRate}%
344
+ - Total failures: ${failures}
345
+
346
+ FAILURE PHASE BREAKDOWN:
347
+ ${failPhaseSummary || " (none)"}
348
+
349
+ ERROR PATTERNS:
350
+ ${errorSummary || " (none)"}
351
+
352
+ SAMPLE ERROR TAILS FROM RECENT FAILURES:
353
+ ${errorTails || " (none)"}
354
+
355
+ RECOMMENDATIONS FROM ANALYSIS:
356
+ ${recs.map((r) => `- ${r}`).join("\n")}
357
+
358
+ YOUR TASK:
359
+ Improve the prompt template to reduce failures. Focus on:
360
+ 1. Adding specific instructions that address the most common failure modes
361
+ 2. Making implicit requirements explicit (lint rules, test commands, commit format)
362
+ 3. Adding guardrails for the error patterns seen (e.g., if lint failures are common, add lint-specific instructions)
363
+ 4. Keeping the template concise \u2014 agents work better with clear, structured prompts
364
+ 5. Preserving all {{VARIABLE}} placeholders exactly as-is
365
+
366
+ REQUIREMENTS:
367
+ - Output ONLY the improved markdown template
368
+ - Keep all {{VARIABLE}} placeholders: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}}, {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
369
+ - Do not add commentary, explanations, or markdown fences around the output
370
+ - Target similar length to the current template (no bloat)
371
+
372
+ OUTPUT THE IMPROVED TEMPLATE:`;
373
+ try {
374
+ console.log(
375
+ ` ${c.d}Calling Claude to generate improved template...${c.r}`
376
+ );
377
+ const evolved = await spawnClaudePrint(mutationPrompt);
378
+ if (!evolved.trim()) {
379
+ console.log(` ${c.red}Empty response from Claude \u2014 skipping.${c.r}`);
380
+ return;
381
+ }
382
+ const requiredVars = ["{{ISSUE_ID}}", "{{TITLE}}", "{{DESCRIPTION}}"];
383
+ const missing = requiredVars.filter((v) => !evolved.includes(v));
384
+ if (missing.length > 0) {
385
+ console.log(
386
+ ` ${c.red}Evolved template missing variables: ${missing.join(", ")} \u2014 skipping.${c.r}`
387
+ );
388
+ return;
389
+ }
390
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
391
+ const backupPath = templatePath.replace(".md", `.backup-${timestamp}.md`);
392
+ copyFileSync(templatePath, backupPath);
393
+ console.log(` ${c.d}Backed up to ${backupPath}${c.r}`);
394
+ writeFileSync(templatePath, evolved.trim() + "\n");
395
+ console.log(
396
+ ` ${c.green}Evolved template written to ${templatePath}${c.r}`
397
+ );
398
+ const evolutionLog = join(
399
+ homedir(),
400
+ ".stackmemory",
401
+ "conductor",
402
+ "evolution-log.jsonl"
403
+ );
404
+ const entry = {
405
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
406
+ successRate,
407
+ failures,
408
+ failPhases,
409
+ errorPatterns,
410
+ backupPath
411
+ };
412
+ const dir = join(homedir(), ".stackmemory", "conductor");
413
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
414
+ writeFileSync(
415
+ evolutionLog,
416
+ (existsSync(evolutionLog) ? readFileSync(evolutionLog, "utf-8") : "") + JSON.stringify(entry) + "\n"
417
+ );
418
+ console.log(` ${c.d}Evolution logged to ${evolutionLog}${c.r}`);
419
+ } catch (err) {
420
+ const msg = err instanceof Error ? err.message : String(err);
421
+ console.log(` ${c.red}Evolution failed: ${msg}${c.r}`);
422
+ console.log(
423
+ ` ${c.d}Tip: Ensure 'claude' CLI is available and authenticated.${c.r}`
424
+ );
425
+ }
426
+ }
234
427
  function createConductorCommands() {
235
428
  const cmd = new Command("conductor");
236
429
  cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
@@ -538,12 +731,25 @@ function createConductorCommands() {
538
731
  const elapsedStr = formatElapsed(s.elapsed).replace(" ago", "");
539
732
  let hasCommits = false;
540
733
  if (s.workspacePath && existsSync(s.workspacePath)) {
734
+ let baseBranch = "main";
541
735
  try {
542
- const log = execSync("git log origin/main..HEAD --oneline", {
736
+ const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
543
737
  cwd: s.workspacePath,
544
738
  encoding: "utf-8",
545
- timeout: 1e4
546
- });
739
+ timeout: 5e3
740
+ }).trim();
741
+ baseBranch = ref.replace("refs/remotes/origin/", "");
742
+ } catch {
743
+ }
744
+ try {
745
+ const log = execSync(
746
+ `git log origin/${baseBranch}..HEAD --oneline`,
747
+ {
748
+ cwd: s.workspacePath,
749
+ encoding: "utf-8",
750
+ timeout: 1e4
751
+ }
752
+ );
547
753
  hasCommits = log.trim().length > 0;
548
754
  } catch {
549
755
  }
@@ -600,20 +806,26 @@ function createConductorCommands() {
600
806
  const lines = parseInt(options.lines, 10);
601
807
  const args = options.follow ? ["-f", "-n", String(lines), logPath] : ["-n", String(lines), logPath];
602
808
  const tail = cpSpawn("tail", args, { stdio: "inherit" });
809
+ const forward = () => {
810
+ tail.kill("SIGTERM");
811
+ };
812
+ process.on("SIGINT", forward);
813
+ process.on("SIGTERM", forward);
603
814
  await new Promise((resolve) => {
604
815
  tail.on("close", () => {
816
+ process.removeListener("SIGINT", forward);
817
+ process.removeListener("SIGTERM", forward);
605
818
  resolve();
606
819
  });
607
- const forward = () => {
608
- tail.kill("SIGTERM");
609
- };
610
- process.on("SIGINT", forward);
611
- process.on("SIGTERM", forward);
612
820
  });
613
821
  });
614
822
  cmd.command("learn").description(
615
823
  "Analyze agent outcomes and generate improved prompt templates"
616
- ).option("--last <n>", "Analyze last N outcomes (default: all)", "0").option("--failures-only", "Only analyze failures", false).option("--export", "Export analysis as JSON", false).action(async (options) => {
824
+ ).option("--last <n>", "Analyze last N outcomes (default: all)", "0").option("--failures-only", "Only analyze failures", false).option("--export", "Export analysis as JSON", false).option(
825
+ "--evolve",
826
+ "Auto-mutate prompt template using GEPA-style evolution from failure data",
827
+ false
828
+ ).action(async (options) => {
617
829
  const logPath = getOutcomesLogPath();
618
830
  if (!existsSync(logPath)) {
619
831
  console.log(
@@ -792,6 +1004,20 @@ function createConductorCommands() {
792
1004
  console.log(`
793
1005
  ${c.d}Using custom template: ${templatePath}${c.r}`);
794
1006
  }
1007
+ if (options.evolve) {
1008
+ console.log(`
1009
+ ${c.b}${c.cyan}Evolving prompt template...${c.r}
1010
+ `);
1011
+ await evolvePromptTemplate({
1012
+ templatePath,
1013
+ successRate,
1014
+ failures,
1015
+ failPhases,
1016
+ errorPatterns,
1017
+ recs,
1018
+ outcomes
1019
+ });
1020
+ }
795
1021
  console.log("");
796
1022
  });
797
1023
  cmd.command("usage").description("Show token usage, budget, and time-to-exhaustion").option("--json", "Output as JSON", false).option("--scan", "Scan Claude Code JSONL logs for historical data", false).action(async (options) => {
@@ -859,6 +1085,7 @@ function createConductorCommands() {
859
1085
  'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
860
1086
  "cli"
861
1087
  ).action(async (options) => {
1088
+ ensureDefaultPromptTemplate();
862
1089
  const conductor = new Conductor({
863
1090
  teamId: options.team,
864
1091
  activeStates: options.states.split(",").map((s) => s.trim()),
@@ -1017,14 +1244,14 @@ function createConductorCommands() {
1017
1244
  if (!paused) await render();
1018
1245
  }, refreshInterval);
1019
1246
  await new Promise((resolve) => {
1020
- process.on("SIGINT", () => {
1021
- clearInterval(timer);
1022
- resolve();
1023
- });
1024
- process.on("SIGTERM", () => {
1247
+ const cleanup = () => {
1025
1248
  clearInterval(timer);
1249
+ process.removeListener("SIGINT", cleanup);
1250
+ process.removeListener("SIGTERM", cleanup);
1026
1251
  resolve();
1027
- });
1252
+ };
1253
+ process.on("SIGINT", cleanup);
1254
+ process.on("SIGTERM", cleanup);
1028
1255
  });
1029
1256
  return;
1030
1257
  }
@@ -1137,16 +1364,15 @@ function createConductorCommands() {
1137
1364
  }
1138
1365
  });
1139
1366
  await new Promise((resolve) => {
1140
- process.on("SIGINT", () => {
1367
+ const cleanup = () => {
1141
1368
  if (refreshTimer) clearTimeout(refreshTimer);
1142
1369
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1370
+ process.removeListener("SIGINT", cleanup);
1371
+ process.removeListener("SIGTERM", cleanup);
1143
1372
  resolve();
1144
- });
1145
- process.on("SIGTERM", () => {
1146
- if (refreshTimer) clearTimeout(refreshTimer);
1147
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
1148
- resolve();
1149
- });
1373
+ };
1374
+ process.on("SIGINT", cleanup);
1375
+ process.on("SIGTERM", cleanup);
1150
1376
  });
1151
1377
  });
1152
1378
  return cmd;
@@ -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";
@@ -1469,14 +1470,7 @@ class Conductor {
1469
1470
  if (elapsed < staleThresholdMs) continue;
1470
1471
  const elapsedMin = Math.round(elapsed / 6e4);
1471
1472
  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
- }
1473
+ const alive = pid ? isProcessAlive(pid) : false;
1480
1474
  if (!alive) {
1481
1475
  logger.warn("Stale agent process is dead, cleaning up", {
1482
1476
  identifier: run.issue.identifier,
@@ -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.0",
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",