@stackmemoryai/stackmemory 1.4.0 → 1.5.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.
package/README.md CHANGED
@@ -22,6 +22,9 @@ StackMemory is a **production-ready memory runtime** for AI coding tools that pr
22
22
  - **Multi-wrapper support** — `claude-sm`, `codex-sm`, `opencode-sm` with auto context loading
23
23
  - **Skills system** with `/spec` and `/linear-run` for Claude Code
24
24
  - **Automatic hooks** for task tracking, Linear sync, and spec progress
25
+ - **Snapshot capture** — post-run context snapshots for session handoff (`snapshot save`)
26
+ - **Pre-flight overlap check** — predict file conflicts before parallel task dispatch (`preflight`)
27
+ - **Conductor orchestrator** — polls Linear, creates worktrees, spawns agents with bounded concurrency
25
28
  - **Memory monitor daemon** with automatic capture/clear on RAM pressure
26
29
  - **Auto-save service** for periodic context persistence
27
30
  - **Comprehensive test coverage** across all core modules
@@ -63,6 +66,8 @@ Tools forget decisions and constraints between sessions. StackMemory makes conte
63
66
  - **Hooks**: automatic context save, task tracking, Linear sync, PROMPT_PLAN updates, cord tracing
64
67
  - **Prompt Forge**: watches CLAUDE.md and AGENTS.md for prompt optimization (GEPA)
65
68
  - **Safe branches**: worktree isolation with `--worktree` or `-w`
69
+ - **Snapshot**: post-run context capture — records what changed, commits, and decisions (`stackmemory snapshot`)
70
+ - **Pre-flight check**: file overlap prediction before parallel task dispatch (`stackmemory preflight`)
66
71
  - **Persistent context**: frames, anchors, decisions, retrieval
67
72
  - **Integrations**: Linear (API key + OAuth), DiffMem, Browser MCP, log-mcp (log analysis)
68
73
 
@@ -351,6 +356,39 @@ npm run mcp:dev
351
356
 
352
357
  See [docs/cli.md](https://github.com/stackmemoryai/stackmemory/blob/main/docs/cli.md) for the full command reference.
353
358
 
359
+ ### Snapshot (`snapshot` / `snap`)
360
+
361
+ Capture a point-in-time snapshot of what changed on your branch — files modified, commits, and key decisions. Useful for session handoff and post-task review.
362
+
363
+ ```bash
364
+ # Save a snapshot of current branch state
365
+ stackmemory snapshot save --task "add auth middleware"
366
+
367
+ # Save with explicit decisions
368
+ stackmemory snap save -d "chose JWT over session cookies" -d "switched to argon2"
369
+
370
+ # List recent snapshots
371
+ stackmemory snap list
372
+
373
+ # Show latest snapshot (or by branch)
374
+ stackmemory snap show feature/auth
375
+ ```
376
+
377
+ ### Pre-flight Check (`preflight` / `pf`)
378
+
379
+ Predict file overlaps before running parallel tasks. Uses git history, import graphs, and keyword matching to flag conflicts.
380
+
381
+ ```bash
382
+ # Check if two tasks can safely run in parallel
383
+ stackmemory preflight "add user auth" "refactor database layer"
384
+
385
+ # With explicit file hints
386
+ stackmemory pf "auth work" "db migration" -f "task1:src/auth.ts;task2:src/db.ts"
387
+
388
+ # JSON output for programmatic use
389
+ stackmemory pf "task A" "task B" "task C" --json
390
+ ```
391
+
354
392
  ---
355
393
 
356
394
  ## Documentation
@@ -0,0 +1,123 @@
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 { execSync } from "child_process";
7
+ import chalk from "chalk";
8
+ function parseSeconds(value) {
9
+ const match = value.match(/^(\d+)(s|m|h)?$/);
10
+ if (!match) return parseInt(value, 10) || 10;
11
+ const num = parseInt(match[1], 10);
12
+ switch (match[2]) {
13
+ case "h":
14
+ return num * 3600;
15
+ case "m":
16
+ return num * 60;
17
+ default:
18
+ return num;
19
+ }
20
+ }
21
+ function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+ function checkCondition(output, exitCode, opts) {
25
+ if (opts.untilExit) return exitCode === 0;
26
+ if (opts.untilEmpty) return output.trim().length === 0;
27
+ if (opts.untilNonEmpty) return output.trim().length > 0;
28
+ if (opts.until) return output.includes(opts.until);
29
+ if (opts.untilNot) return !output.includes(opts.untilNot);
30
+ return exitCode === 0;
31
+ }
32
+ function createLoopCommand() {
33
+ return new Command("loop").alias("watch").description(
34
+ "Run a command repeatedly until a condition is met (monitor CI, deploys, logs)"
35
+ ).argument("<command>", "Shell command to run on each iteration").option("--until <pattern>", "Stop when output contains this string").option(
36
+ "--until-not <pattern>",
37
+ "Stop when output no longer contains this string"
38
+ ).option("--until-empty", "Stop when output is empty", false).option("--until-non-empty", "Stop when output is non-empty", false).option(
39
+ "--until-exit",
40
+ "Stop when command exits with code 0 (ignore output)",
41
+ false
42
+ ).option("-i, --interval <duration>", "Check interval (e.g. 10s, 1m)", "10s").option("-t, --timeout <duration>", "Max wait time (e.g. 30m, 1h)", "30m").option("-q, --quiet", "Only show final result", false).option("--json", "Output result as JSON", false).option("-l, --label <name>", "Label for this loop (shown in output)").option("--shell <path>", "Shell to use", "/bin/sh").action(async (command, opts) => {
43
+ const intervalSec = parseSeconds(opts.interval);
44
+ const timeoutSec = parseSeconds(opts.timeout);
45
+ const label = opts.label || command.slice(0, 40);
46
+ const startTime = Date.now();
47
+ const deadline = startTime + timeoutSec * 1e3;
48
+ let iterations = 0;
49
+ let lastOutput = "";
50
+ if (!opts.quiet && !opts.json) {
51
+ console.log(
52
+ chalk.cyan(`[loop] ${label}`) + chalk.gray(` (every ${intervalSec}s, timeout ${timeoutSec}s)`)
53
+ );
54
+ }
55
+ while (Date.now() < deadline) {
56
+ iterations++;
57
+ let output = "";
58
+ let exitCode = 0;
59
+ try {
60
+ output = execSync(command, {
61
+ encoding: "utf8",
62
+ timeout: Math.min(intervalSec * 1e3, 6e4),
63
+ shell: opts.shell,
64
+ stdio: ["pipe", "pipe", "pipe"]
65
+ });
66
+ } catch (error) {
67
+ const e = error;
68
+ exitCode = e.status ?? 1;
69
+ output = (e.stdout || "") + (e.stderr || "");
70
+ }
71
+ lastOutput = output;
72
+ if (!opts.quiet && !opts.json) {
73
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
74
+ const preview = output.trim().split("\n").slice(-3).join("\n");
75
+ console.log(
76
+ chalk.gray(`[${elapsed}s #${iterations}]`) + (preview ? `
77
+ ${preview}` : chalk.gray(" (no output)"))
78
+ );
79
+ }
80
+ if (checkCondition(output, exitCode, opts)) {
81
+ const result2 = {
82
+ ok: true,
83
+ reason: "matched",
84
+ iterations,
85
+ elapsed: Date.now() - startTime,
86
+ lastOutput: lastOutput.trim()
87
+ };
88
+ if (opts.json) {
89
+ console.log(JSON.stringify(result2));
90
+ } else if (!opts.quiet) {
91
+ console.log(
92
+ chalk.green(`[loop] Condition met after ${iterations} iterations`)
93
+ );
94
+ }
95
+ return;
96
+ }
97
+ const remaining = deadline - Date.now();
98
+ if (remaining > 0) {
99
+ await sleep(Math.min(intervalSec * 1e3, remaining));
100
+ }
101
+ }
102
+ const result = {
103
+ ok: false,
104
+ reason: "timeout",
105
+ iterations,
106
+ elapsed: Date.now() - startTime,
107
+ lastOutput: lastOutput.trim()
108
+ };
109
+ if (opts.json) {
110
+ console.log(JSON.stringify(result));
111
+ } else {
112
+ console.log(
113
+ chalk.yellow(
114
+ `[loop] Timed out after ${timeoutSec}s (${iterations} iterations)`
115
+ )
116
+ );
117
+ }
118
+ process.exit(1);
119
+ });
120
+ }
121
+ export {
122
+ createLoopCommand
123
+ };
@@ -60,6 +60,7 @@ import { createDesiresCommands } from "./commands/desires.js";
60
60
  import { createConductorCommands } from "./commands/orchestrate.js";
61
61
  import { createPreflightCommand } from "./commands/preflight.js";
62
62
  import { createSnapshotCommand } from "./commands/snapshot.js";
63
+ import { createLoopCommand } from "./commands/loop.js";
63
64
  import chalk from "chalk";
64
65
  import * as fs from "fs";
65
66
  import * as path from "path";
@@ -513,6 +514,7 @@ program.addCommand(createDesiresCommands());
513
514
  program.addCommand(createConductorCommands());
514
515
  program.addCommand(createPreflightCommand());
515
516
  program.addCommand(createSnapshotCommand());
517
+ program.addCommand(createLoopCommand());
516
518
  registerSetupCommands(program);
517
519
  program.command("mm-spike").description(
518
520
  "Run multi-agent planning/implementation spike (planner/implementer/critic)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, Claude/Codex/OpenCode wrappers, Linear sync, automatic hooks, and log analysis.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
@@ -350,6 +350,12 @@ function evaluate(prompt) {
350
350
  if (config.showMatchReasons && match.reasons.length > 0) {
351
351
  output += ` Matched: ${match.reasons.slice(0, 3).join(', ')}\n`;
352
352
  }
353
+
354
+ // Show suggestion if the skill defines one (e.g. loop-monitor)
355
+ const skillDef = skills[match.name];
356
+ if (skillDef?.suggestion) {
357
+ output += ` Suggestion: ${skillDef.suggestion}\n`;
358
+ }
353
359
  }
354
360
 
355
361
  if (relatedSkills.length > 0) {
@@ -255,6 +255,22 @@
255
255
  },
256
256
  "relatedSkills": ["linear-task-runner"]
257
257
  },
258
+ "loop-monitor": {
259
+ "description": "Monitor CI, deploys, logs, or external state with stackmemory loop",
260
+ "priority": 9,
261
+ "triggers": {
262
+ "keywords": ["monitor", "watch", "poll", "wait for", "check status", "loop", "keep checking", "deploy status", "ci status", "action status", "run status"],
263
+ "keywordPatterns": ["\\b(?:monitor|watch|poll|loop)\\b", "wait\\s+(?:for|until)", "keep\\s+checking", "check.*(?:status|state|progress)", "(?:gh|github)\\s+(?:run|action|check)", "deploy.*(?:status|log|done)", "\\buntil\\b.*(?:done|complete|pass|success|fail)"],
264
+ "intentPatterns": [
265
+ "(?:monitor|watch|poll|check).*(?:ci|deploy|action|run|build|status|log|inbox)",
266
+ "(?:wait|loop).*(?:until|for).*(?:done|complete|pass|success|finish)",
267
+ "(?:keep|continue).*(?:checking|monitoring|watching)",
268
+ "(?:let me know|notify|alert).*(?:when|if).*(?:done|complete|ready|fail)"
269
+ ]
270
+ },
271
+ "suggestion": "Use `stackmemory loop` to poll a command until a condition is met:\n stackmemory loop \"<command>\" --until \"<pattern>\" -i 10s -t 30m\n stackmemory loop \"gh run view <id> --json status -q .status\" --until \"completed\"\n stackmemory loop \"curl -s http://localhost/health\" --until \"ok\" --json",
272
+ "relatedSkills": ["github-actions"]
273
+ },
258
274
  "linear-task-runner": {
259
275
  "description": "Execute Linear tasks via RLM orchestrator",
260
276
  "priority": 8,