@stackmemoryai/stackmemory 1.5.2 → 1.5.4

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
@@ -24,7 +24,8 @@ StackMemory is a **production-ready memory runtime** for AI coding tools that pr
24
24
  - **Automatic hooks** for task tracking, Linear sync, and spec progress
25
25
  - **Snapshot capture** — post-run context snapshots for session handoff (`snapshot save`)
26
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
27
+ - **Conductor orchestrator** — polls Linear, creates worktrees, spawns agents with bounded concurrency and auto team detection
28
+ - **Loop/Watch command** — poll any shell command until a condition is met (monitor CI, deploys, logs)
28
29
  - **Memory monitor daemon** with automatic capture/clear on RAM pressure
29
30
  - **Auto-save service** for periodic context persistence
30
31
  - **Comprehensive test coverage** across all core modules
@@ -68,6 +69,8 @@ Tools forget decisions and constraints between sessions. StackMemory makes conte
68
69
  - **Safe branches**: worktree isolation with `--worktree` or `-w`
69
70
  - **Snapshot**: post-run context capture — records what changed, commits, and decisions (`stackmemory snapshot`)
70
71
  - **Pre-flight check**: file overlap prediction before parallel task dispatch (`stackmemory preflight`)
72
+ - **Loop/Watch**: poll shell commands until conditions are met — monitor CI, deploys, logs (`stackmemory loop`)
73
+ - **Conductor**: autonomous orchestrator — polls Linear, creates worktrees, spawns agents with auto team detection
71
74
  - **Persistent context**: frames, anchors, decisions, retrieval
72
75
  - **Integrations**: Linear (API key + OAuth), DiffMem, Browser MCP, log-mcp (log analysis)
73
76
 
@@ -389,6 +392,26 @@ stackmemory pf "auth work" "db migration" -f "task1:src/auth.ts;task2:src/db.ts"
389
392
  stackmemory pf "task A" "task B" "task C" --json
390
393
  ```
391
394
 
395
+ ### Loop / Watch (`loop` / `watch`)
396
+
397
+ Poll a shell command until a condition is met. Useful for monitoring CI runs, deploy logs, inboxes, or any external state.
398
+
399
+ ```bash
400
+ # Monitor GitHub Actions until complete
401
+ stackmemory loop "gh run view <id> --json status -q .status" --until "completed"
402
+
403
+ # Watch deploy logs for success
404
+ stackmemory watch "railway logs --latest" --until "deployed" -i 5s
405
+
406
+ # Wait for health check to pass
407
+ stackmemory loop "curl -s http://localhost:3000/health" --until "ok" -i 3s -t 5m
408
+
409
+ # JSON output for programmatic use
410
+ stackmemory loop "gh pr checks 42 --json state -q '.[0].state'" --until "SUCCESS" --json
411
+ ```
412
+
413
+ Options: `--until`, `--until-not`, `--until-empty`, `--until-non-empty`, `--until-exit`, `-i/--interval` (default 10s), `-t/--timeout` (default 30m), `--json`, `-q/--quiet`
414
+
392
415
  ---
393
416
 
394
417
  ## Documentation
@@ -8,7 +8,7 @@ import Table from "cli-table3";
8
8
  import { SessionManager } from "../../core/session/session-manager.js";
9
9
  import Database from "better-sqlite3";
10
10
  import { join } from "path";
11
- import { existsSync } from "fs";
11
+ import { existsSync, readFileSync } from "fs";
12
12
  import { getModelTokenLimit } from "../../core/models/model-router.js";
13
13
  const dashboardCommand = {
14
14
  command: "dashboard",
@@ -150,6 +150,52 @@ const dashboardCommand = {
150
150
  console.log(chalk.yellow.bold("\u{1F4BE} Context Usage"));
151
151
  console.log(`${usageBar} ${contextUsage}%`);
152
152
  console.log();
153
+ const conductorStatusPath = join(
154
+ projectRoot,
155
+ ".stackmemory",
156
+ "conductor-status.json"
157
+ );
158
+ if (existsSync(conductorStatusPath)) {
159
+ try {
160
+ const raw = readFileSync(conductorStatusPath, "utf-8");
161
+ const conductorStatus = JSON.parse(raw);
162
+ const stale = Date.now() - conductorStatus.updatedAt > 12e4;
163
+ const header = stale ? chalk.gray.bold("\u2699 Conductor (stale)") : chalk.yellow.bold("\u2699 Conductor");
164
+ console.log(header);
165
+ if (conductorStatus.running.length > 0) {
166
+ const conductorTable = new Table({
167
+ head: [
168
+ chalk.white("Issue"),
169
+ chalk.white("Status"),
170
+ chalk.white("Title"),
171
+ chalk.white("Runtime")
172
+ ],
173
+ style: { head: [], border: [] },
174
+ colWidths: [12, 14, 36, 10]
175
+ });
176
+ conductorStatus.running.forEach((r) => {
177
+ const mins = Math.round(r.runtime / 6e4);
178
+ const statusColor = r.status === "running" ? chalk.green : r.status === "completed" ? chalk.cyan : chalk.red;
179
+ conductorTable.push([
180
+ r.identifier,
181
+ statusColor(r.status),
182
+ r.title.slice(0, 34),
183
+ `${mins}m`
184
+ ]);
185
+ });
186
+ console.log(conductorTable.toString());
187
+ } else {
188
+ console.log(chalk.gray(" No agents running"));
189
+ }
190
+ console.log(
191
+ chalk.gray(
192
+ ` Completed: ${conductorStatus.completed} Failed: ${conductorStatus.failed} Max: ${conductorStatus.maxConcurrent}`
193
+ )
194
+ );
195
+ console.log();
196
+ } catch {
197
+ }
198
+ }
153
199
  db.close();
154
200
  if (argv.watch) {
155
201
  console.log(
@@ -3,13 +3,20 @@ import { dirname as __pathDirname } from 'path';
3
3
  const __filename = __fileURLToPath(import.meta.url);
4
4
  const __dirname = __pathDirname(__filename);
5
5
  import { Command } from "commander";
6
- import { execSync } from "child_process";
7
- import { existsSync, mkdirSync, writeFileSync } from "fs";
6
+ import { execSync, spawn as cpSpawn } from "child_process";
7
+ import {
8
+ existsSync,
9
+ mkdirSync,
10
+ writeFileSync,
11
+ readFileSync,
12
+ readdirSync
13
+ } from "fs";
8
14
  import { join } from "path";
9
15
  import { homedir, tmpdir } from "os";
10
16
  import Database from "better-sqlite3";
11
17
  import { logger } from "../../core/monitoring/logger.js";
12
18
  import { Conductor } from "./orchestrator.js";
19
+ import { getAgentStatusDir } from "./orchestrator.js";
13
20
  function getGlobalStorePath() {
14
21
  const dir = join(homedir(), ".stackmemory", "conductor");
15
22
  if (!existsSync(dir)) {
@@ -43,6 +50,16 @@ function getGlobalDb() {
43
50
  `);
44
51
  return db;
45
52
  }
53
+ function formatElapsed(ms) {
54
+ const seconds = Math.floor(ms / 1e3);
55
+ if (seconds < 60) return `${seconds}s ago`;
56
+ const minutes = Math.floor(seconds / 60);
57
+ if (minutes < 60) return `${minutes}m ago`;
58
+ const hours = Math.floor(minutes / 60);
59
+ if (hours < 24) return `${hours}h ago`;
60
+ const days = Math.floor(hours / 24);
61
+ return `${days}d ago`;
62
+ }
46
63
  function createConductorCommands() {
47
64
  const cmd = new Command("conductor");
48
65
  cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
@@ -263,6 +280,60 @@ function createConductorCommands() {
263
280
  }
264
281
  globalDb.close();
265
282
  });
283
+ cmd.command("status").description("Show running agent status table").action(async () => {
284
+ const agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
285
+ if (!existsSync(agentsDir)) {
286
+ console.log("No agent status files found");
287
+ return;
288
+ }
289
+ const entries = readdirSync(agentsDir, { withFileTypes: true });
290
+ const statuses = [];
291
+ for (const entry of entries) {
292
+ if (!entry.isDirectory()) continue;
293
+ const statusPath = join(agentsDir, entry.name, "status.json");
294
+ if (!existsSync(statusPath)) continue;
295
+ try {
296
+ const data = JSON.parse(readFileSync(statusPath, "utf-8"));
297
+ statuses.push(data);
298
+ } catch {
299
+ }
300
+ }
301
+ if (statuses.length === 0) {
302
+ console.log("No agent status files found");
303
+ return;
304
+ }
305
+ statuses.sort(
306
+ (a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
307
+ );
308
+ const header = `${"Issue".padEnd(12)}${"Phase".padEnd(16)}${"Tools".padStart(7)}${"Files".padStart(7)}${"Tokens".padStart(9)} Last Update`;
309
+ console.log(header);
310
+ for (const s of statuses) {
311
+ const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
312
+ const lastUpdate = formatElapsed(elapsed);
313
+ const line = `${s.issue.padEnd(12)}${s.phase.padEnd(16)}${String(s.toolCalls).padStart(7)}${String(s.filesModified).padStart(7)}${String(s.tokensUsed).padStart(9)} ${lastUpdate}`;
314
+ console.log(line);
315
+ }
316
+ });
317
+ cmd.command("logs").description("Tail agent output log").argument("<issue-id>", "Issue identifier (e.g., STA-499)").option("-f, --follow", "Follow the log (tail -f)", false).option("-n, --lines <n>", "Number of lines to show", "50").action(async (issueId, options) => {
318
+ const logPath = join(getAgentStatusDir(issueId), "output.log");
319
+ if (!existsSync(logPath)) {
320
+ console.error(`No log file found for ${issueId} at ${logPath}`);
321
+ return;
322
+ }
323
+ const lines = parseInt(options.lines, 10);
324
+ const args = options.follow ? ["-f", "-n", String(lines), logPath] : ["-n", String(lines), logPath];
325
+ const tail = cpSpawn("tail", args, { stdio: "inherit" });
326
+ await new Promise((resolve) => {
327
+ tail.on("close", () => {
328
+ resolve();
329
+ });
330
+ const forward = () => {
331
+ tail.kill("SIGTERM");
332
+ };
333
+ process.on("SIGINT", forward);
334
+ process.on("SIGTERM", forward);
335
+ });
336
+ });
266
337
  cmd.command("start").description("Start the orchestrator daemon").option("--team <id>", "Linear team ID").option(
267
338
  "--states <states>",
268
339
  "Comma-separated issue states to pick up",
@@ -275,7 +346,11 @@ function createConductorCommands() {
275
346
  "--in-review <state>",
276
347
  "State name for completed review",
277
348
  "In Review"
278
- ).option("--poll <ms>", "Polling interval in milliseconds", "30000").option("--concurrency <n>", "Max concurrent agents", "3").option("--workspace-root <path>", "Workspace root directory").option("--repo <path>", "Git repo root for worktrees", process.cwd()).option("--branch <name>", "Base branch for worktrees", "main").option("--retries <n>", "Max retries per issue", "1").option("--turn-timeout <ms>", "Agent turn timeout in ms", "3600000").action(async (options) => {
349
+ ).option("--poll <ms>", "Polling interval in milliseconds", "30000").option("--concurrency <n>", "Max concurrent agents", "3").option("--workspace-root <path>", "Workspace root directory").option("--repo <path>", "Git repo root for worktrees", process.cwd()).option("--branch <name>", "Base branch for worktrees", "main").option("--retries <n>", "Max retries per issue", "1").option("--turn-timeout <ms>", "Agent turn timeout in ms", "3600000").option(
350
+ "--mode <mode>",
351
+ 'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
352
+ "cli"
353
+ ).action(async (options) => {
279
354
  const conductor = new Conductor({
280
355
  teamId: options.team,
281
356
  activeStates: options.states.split(",").map((s) => s.trim()),
@@ -287,12 +362,14 @@ function createConductorCommands() {
287
362
  repoRoot: options.repo,
288
363
  baseBranch: options.branch,
289
364
  maxRetries: parseInt(options.retries, 10),
290
- turnTimeoutMs: parseInt(options.turnTimeout, 10)
365
+ turnTimeoutMs: parseInt(options.turnTimeout, 10),
366
+ agentMode: options.mode === "adapter" ? "adapter" : "cli"
291
367
  });
292
368
  await conductor.start();
293
369
  });
294
370
  return cmd;
295
371
  }
296
372
  export {
297
- createConductorCommands
373
+ createConductorCommands,
374
+ formatElapsed
298
375
  };