@stackmemoryai/stackmemory 1.5.3 → 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.
@@ -346,7 +346,11 @@ function createConductorCommands() {
346
346
  "--in-review <state>",
347
347
  "State name for completed review",
348
348
  "In Review"
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").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) => {
350
354
  const conductor = new Conductor({
351
355
  teamId: options.team,
352
356
  activeStates: options.states.split(",").map((s) => s.trim()),
@@ -358,7 +362,8 @@ function createConductorCommands() {
358
362
  repoRoot: options.repo,
359
363
  baseBranch: options.branch,
360
364
  maxRetries: parseInt(options.retries, 10),
361
- turnTimeoutMs: parseInt(options.turnTimeout, 10)
365
+ turnTimeoutMs: parseInt(options.turnTimeout, 10),
366
+ agentMode: options.mode === "adapter" ? "adapter" : "cli"
362
367
  });
363
368
  await conductor.start();
364
369
  });
@@ -65,6 +65,35 @@ class TeeTransform extends Transform {
65
65
  callback();
66
66
  }
67
67
  }
68
+ function inferPhaseFromStreamJson(event) {
69
+ if (event.type !== "assistant") return null;
70
+ const message = event.message;
71
+ const content = message?.content || [];
72
+ for (const block of content) {
73
+ if (block.type !== "tool_use") continue;
74
+ const toolLower = (block.name || "").toLowerCase();
75
+ if (toolLower.includes("read") || toolLower.includes("glob") || toolLower.includes("grep") || toolLower.includes("search")) {
76
+ return "reading";
77
+ }
78
+ if (toolLower.includes("todowrite") || toolLower.includes("todo")) {
79
+ return "planning";
80
+ }
81
+ if (toolLower.includes("edit") || toolLower.includes("write") || toolLower.includes("bash")) {
82
+ if (toolLower === "bash") {
83
+ const input = block.input;
84
+ const command = (input?.command ?? "").toLowerCase();
85
+ if (command.includes("git commit") || command.includes("git add")) {
86
+ return "committing";
87
+ }
88
+ }
89
+ return "implementing";
90
+ }
91
+ if (toolLower.includes("test")) {
92
+ return "testing";
93
+ }
94
+ }
95
+ return null;
96
+ }
68
97
  function inferPhase(msg) {
69
98
  const method = msg.method;
70
99
  const params = msg.params;
@@ -85,7 +114,8 @@ function inferPhase(msg) {
85
114
  }
86
115
  }
87
116
  if (method === "item/commandExecution/started") {
88
- const command = params?.command || "";
117
+ const args = params?.arguments;
118
+ const command = (args?.command ?? params?.command) || "";
89
119
  if (command.includes("git commit") || command.includes("git add")) {
90
120
  return "committing";
91
121
  }
@@ -110,7 +140,8 @@ const DEFAULT_CONFIG = {
110
140
  ),
111
141
  turnTimeoutMs: 36e5,
112
142
  maxRetries: 1,
113
- hookTimeoutMs: 6e4
143
+ hookTimeoutMs: 6e4,
144
+ agentMode: "cli"
114
145
  };
115
146
  class Conductor {
116
147
  config;
@@ -642,13 +673,167 @@ class Conductor {
642
673
  return identifier.replace(/[^A-Za-z0-9._-]/g, "_");
643
674
  }
644
675
  // ── Agent Execution ──
645
- runAgent(issue, run) {
676
+ async runAgent(issue, run) {
677
+ if (this.config.agentMode === "cli") {
678
+ try {
679
+ return await this.runAgentCLI(issue, run);
680
+ } catch (err) {
681
+ const msg = err.message || "";
682
+ if (msg.includes("usage limits") || msg.includes("rate_limit") || msg.includes("overloaded")) {
683
+ logger.warn("CLI mode hit limits, falling back to adapter", {
684
+ identifier: issue.identifier,
685
+ error: msg.slice(0, 200)
686
+ });
687
+ run.toolCalls = 0;
688
+ run.filesModified = 0;
689
+ run.tokensUsed = 0;
690
+ run.phase = "reading";
691
+ return this.runAgentAdapter(issue, run);
692
+ }
693
+ throw err;
694
+ }
695
+ }
696
+ return this.runAgentAdapter(issue, run);
697
+ }
698
+ /**
699
+ * CLI mode: spawn `claude -p --output-format stream-json` directly.
700
+ * Uses whatever auth the environment provides (session quota, API key, etc).
701
+ */
702
+ runAgentCLI(issue, run) {
703
+ return new Promise((resolve, reject) => {
704
+ const prompt = this.buildPrompt(issue, run.attempt);
705
+ const proc = spawn(
706
+ "claude",
707
+ [
708
+ "-p",
709
+ "--output-format",
710
+ "stream-json",
711
+ "--dangerously-skip-permissions",
712
+ prompt
713
+ ],
714
+ {
715
+ cwd: run.workspacePath,
716
+ env: (() => {
717
+ const env = { ...process.env };
718
+ delete env.CLAUDECODE;
719
+ delete env.ANTHROPIC_API_KEY;
720
+ return {
721
+ ...env,
722
+ SYMPHONY_WORKSPACE_DIR: run.workspacePath,
723
+ SYMPHONY_ISSUE_ID: issue.id,
724
+ SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
725
+ SYMPHONY_ATTEMPT: String(run.attempt)
726
+ };
727
+ })(),
728
+ stdio: ["pipe", "pipe", "pipe"]
729
+ }
730
+ );
731
+ run.process = proc;
732
+ const logStream = this.openAgentLogStream(issue.identifier);
733
+ run.logStream = logStream;
734
+ const tee = new TeeTransform(logStream);
735
+ proc.stdout.pipe(tee);
736
+ this.writeAgentStatus(issue.identifier, run);
737
+ let stderr = "";
738
+ let lastResultText = "";
739
+ const timer = setTimeout(() => {
740
+ logger.warn("Agent turn timeout (cli)", {
741
+ identifier: issue.identifier,
742
+ timeoutMs: this.config.turnTimeoutMs
743
+ });
744
+ proc.kill("SIGTERM");
745
+ reject(new Error(`Agent timeout after ${this.config.turnTimeoutMs}ms`));
746
+ }, this.config.turnTimeoutMs);
747
+ let lineBuffer = "";
748
+ tee.on("data", (chunk) => {
749
+ lineBuffer += chunk.toString();
750
+ const lines = lineBuffer.split("\n");
751
+ lineBuffer = lines.pop() || "";
752
+ for (const line of lines) {
753
+ if (!line.trim()) continue;
754
+ try {
755
+ const event = JSON.parse(line);
756
+ const phase = inferPhaseFromStreamJson(event);
757
+ if (phase) {
758
+ run.phase = phase;
759
+ }
760
+ if (event.type === "assistant") {
761
+ const message = event.message;
762
+ const content = message?.content || [];
763
+ for (const block of content) {
764
+ if (block.type === "tool_use") {
765
+ run.toolCalls++;
766
+ const toolLower = (block.name || "").toLowerCase();
767
+ if (toolLower.includes("edit") || toolLower.includes("write")) {
768
+ run.filesModified++;
769
+ }
770
+ }
771
+ if (block.type === "text" && block.text) {
772
+ run.tokensUsed += Math.ceil(
773
+ block.text.length / 4
774
+ );
775
+ }
776
+ }
777
+ }
778
+ if (event.type === "result" && event.result) {
779
+ lastResultText = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
780
+ }
781
+ if (run.toolCalls % 5 === 0 || phase) {
782
+ this.writeAgentStatus(issue.identifier, run);
783
+ }
784
+ } catch {
785
+ }
786
+ }
787
+ });
788
+ proc.stderr.on("data", (data) => {
789
+ stderr += data.toString();
790
+ const lines = data.toString().split("\n").filter((l) => l.trim());
791
+ for (const line of lines) {
792
+ logger.debug(`[${issue.identifier}] ${line}`);
793
+ }
794
+ });
795
+ proc.on("error", (err) => {
796
+ clearTimeout(timer);
797
+ reject(new Error(`Failed to spawn claude: ${err.message}`));
798
+ });
799
+ proc.on("close", (code) => {
800
+ clearTimeout(timer);
801
+ run.process = null;
802
+ if (run.logStream && !run.logStream.destroyed) {
803
+ run.logStream.end();
804
+ }
805
+ this.writeAgentStatus(issue.identifier, run);
806
+ if (code === 0) {
807
+ logger.info("Agent completed (cli)", {
808
+ identifier: issue.identifier,
809
+ toolCalls: run.toolCalls,
810
+ resultLength: lastResultText.length
811
+ });
812
+ resolve();
813
+ } else {
814
+ reject(
815
+ new Error(
816
+ `Claude exited with code ${code}: ${stderr.slice(0, 500)}`
817
+ )
818
+ );
819
+ }
820
+ });
821
+ });
822
+ }
823
+ /**
824
+ * Adapter mode: spawn claude-app-server.cjs via JSON-RPC protocol.
825
+ * Uses ANTHROPIC_API_KEY for auth.
826
+ */
827
+ runAgentAdapter(issue, run) {
646
828
  return new Promise((resolve, reject) => {
647
829
  const prompt = this.buildPrompt(issue, run.attempt);
830
+ const env = { ...process.env };
831
+ delete env.CLAUDECODE;
832
+ delete env.ANTHROPIC_API_KEY;
648
833
  const proc = spawn("node", [this.config.appServerPath], {
649
834
  cwd: run.workspacePath,
650
835
  env: {
651
- ...process.env,
836
+ ...env,
652
837
  SYMPHONY_WORKSPACE_DIR: run.workspacePath,
653
838
  SYMPHONY_ISSUE_ID: issue.id,
654
839
  SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
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",
@@ -21,26 +21,26 @@ docs/ # Documentation
21
21
 
22
22
  ## Key Files
23
23
 
24
- - Entry: src/cli/index.ts
25
- - MCP Server: src/integrations/mcp/server.ts
26
- - Frame Manager: src/core/context/frame-manager.ts
27
- - Database: src/core/database/sqlite-adapter.ts
28
-
29
- ## Detailed Guides
30
-
31
- Quick reference (agent_docs/):
32
- - linear_integration.md - Linear sync
33
- - mcp_server.md - MCP tools
34
- - database_storage.md - Storage
35
- - claude_hooks.md - Hooks
36
-
37
- Full documentation (docs/):
38
- - principles.md - Agent programming paradigm
39
- - architecture.md - Extension model and browser sandbox
40
- - SPEC.md - Technical specification
41
- - API_REFERENCE.md - API docs
42
- - DEVELOPMENT.md - Dev guide
43
- - SETUP.md - Installation
24
+ - Entry: `src/cli/index.ts`
25
+ - MCP Server: `src/integrations/mcp/server.ts`
26
+ - Frame Manager: `src/core/context/frame-manager.ts`
27
+ - Database: `src/core/database/sqlite-adapter.ts`
28
+
29
+ ## Reference Docs
30
+
31
+ `agent_docs/` (quick reference):
32
+ - `linear_integration.md` Linear sync
33
+ - `mcp_server.md` MCP tools
34
+ - `database_storage.md` Storage
35
+ - `claude_hooks.md` Hooks
36
+
37
+ `docs/` (full documentation):
38
+ - `principles.md` Agent programming paradigm
39
+ - `architecture.md` Extension model and browser sandbox
40
+ - `SPEC.md` Technical specification
41
+ - `API_REFERENCE.md` API docs
42
+ - `DEVELOPMENT.md` Dev guide
43
+ - `SETUP.md` Installation
44
44
 
45
45
  ## Commands
46
46
 
@@ -53,49 +53,53 @@ npm run test:run # Run tests once
53
53
  npm run linear:sync # Sync with Linear
54
54
 
55
55
  # StackMemory CLI
56
- stackmemory capture # Save session state for handoff
57
- stackmemory restore # Restore from captured state
56
+ stackmemory capture # Save session state for handoff
57
+ stackmemory restore # Restore from captured state
58
+ stackmemory snapshot save # Post-run context snapshot (alias: snap)
59
+ stackmemory snapshot list # List recent snapshots
60
+ stackmemory preflight # File overlap check for parallel tasks (alias: pf)
61
+ stackmemory conductor start # Autonomous Linear→worktree→agent orchestrator
58
62
  ```
59
63
 
60
64
  ## Working Directory
61
65
 
62
- - PRIMARY: /Users/jwu/Dev/stackmemory
66
+ - PRIMARY: `/Users/jwu/Dev/stackmemory`
63
67
  - ALLOWED: All subdirectories
64
- - TEMP: /tmp for temporary operations
68
+ - TEMP: `/tmp` for temporary operations
65
69
 
66
70
  ## Validation (MUST DO)
67
71
 
68
- After code changes:
69
- 1. `npm run lint` - fix any errors AND warnings
70
- 2. `npm run test:run` - verify no regressions
71
- 3. `npm run build` - ensure compilation
72
- 4. Run code to verify it works
72
+ Run these checks after every code change — in order:
73
73
 
74
- Test coverage:
74
+ 1. `npm run lint` — fix all errors and warnings
75
+ 2. `npm run test:run` — confirm no regressions
76
+ 3. `npm run build` — confirm compilation succeeds
77
+ 4. Execute the code and verify it works
78
+
79
+ Test coverage rules:
75
80
  - New features require tests in `src/**/__tests__/`
76
- - Maintain or improve coverage (no untested code paths)
81
+ - Maintain or improve coverage no untested code paths
77
82
  - Critical paths: context management, handoff, Linear sync
78
83
 
79
- Never: Assume success | Skip testing | Use mock data as fallback
84
+ **Never:** assume success | skip testing | use mock data as fallback
80
85
 
81
86
  ## Git Rules (CRITICAL)
82
87
 
83
- - NEVER use `--no-verify` on git push or commit
84
- - ALWAYS fix lint/test errors before pushing
85
- - If pre-push hooks fail, fix the underlying issue
86
- - Run `npm run lint && npm run test:run` before pushing
87
- - Commit message format: `type(scope): message`
88
- - Branch naming: `feature/STA-XXX-description` | `fix/STA-XXX-description` | `chore/description`
88
+ - NEVER use `--no-verify` on `git push` or `git commit`
89
+ - Fix lint/test errors before pushing — never bypass pre-push hooks
90
+ - Run `npm run lint && npm run test:run` before every push
91
+ - Commit format: `type(scope): message`
92
+ - Branch format: `feature/STA-XXX-description` | `fix/STA-XXX-description` | `chore/description`
89
93
 
90
94
  ## Task Management
91
95
 
92
- - Use TodoWrite for 3+ steps or multiple requests
93
- - Keep one task in_progress at a time
94
- - Update task status immediately on completion
96
+ - Use TodoWrite for 3+ steps or multiple concurrent requests
97
+ - Keep exactly one task `in_progress` at a time
98
+ - Update task status immediately when complete
95
99
 
96
100
  ## Security
97
101
 
98
- NEVER hardcode secrets - use process.env with dotenv/config
102
+ Never hardcode secrets always use `process.env` with `dotenv/config`:
99
103
 
100
104
  ```javascript
101
105
  import 'dotenv/config';
@@ -106,18 +110,18 @@ if (!API_KEY) {
106
110
  }
107
111
  ```
108
112
 
109
- Environment sources (check in order):
110
- 1. .env file
111
- 2. .env.local
112
- 3. ~/.zshrc
113
+ Check for API keys in this order:
114
+ 1. `.env`
115
+ 2. `.env.local`
116
+ 3. `~/.zshrc`
113
117
  4. Process environment
114
118
 
115
- Secret patterns to block: lin_api_* | lin_oauth_* | sk-* | npm_*
119
+ Block these secret patterns: `lin_api_*` | `lin_oauth_*` | `sk-*` | `npm_*`
116
120
 
117
121
  ## Deploy
118
122
 
119
123
  ```bash
120
- # npm publish (uses NPM_TOKEN from .env, no OTP needed)
124
+ # npm publish (NPM_TOKEN from .env no OTP required)
121
125
  git stash -- scripts/gepa/ # stash GEPA state (dirties working tree)
122
126
  NPM_TOKEN=$(grep '^NPM_TOKEN=' .env | cut -d= -f2) \
123
127
  npm publish --registry https://registry.npmjs.org/ \
@@ -127,14 +131,46 @@ git stash pop # restore GEPA state
127
131
  # Railway
128
132
  railway up
129
133
 
130
- # Pre-publish checks require clean git status — stash GEPA files first
134
+ # Pre-publish requires clean git status — always stash GEPA files first
131
135
  ```
132
136
 
137
+ ## Task Delegation Model
138
+
139
+ Match effort to task complexity:
140
+
141
+ **AUTOMATE** — Execute immediately; lint+test is sufficient:
142
+ - CRUD, boilerplate, formatting, simple transforms
143
+ - Adding a handler following an existing switch/case pattern
144
+ - Config additions (env var, feature flag)
145
+
146
+ **STANDARD** — Normal workflow; lint+test+build:
147
+ - Feature implementation, bug fixes, refactoring
148
+ - New tests, documentation updates
149
+ - Integration wiring (adding handler to `server.ts` dispatch)
150
+
151
+ **CAREFUL** — Review approach before writing code:
152
+ - API/schema changes, database migrations, auth flows
153
+ - New integration patterns (MCP tools, webhook handlers)
154
+ - Changes to `frame-manager`, `sqlite-adapter`, or daemon lifecycle
155
+ - Anything touching error handling chains
156
+
157
+ **ARCHITECT** — Require plan mode; explore existing patterns first:
158
+ - New service boundaries or system integrations
159
+ - Performance-critical paths (FTS5 queries, search scoring)
160
+ - Breaking changes to MCP protocol or CLI interface
161
+
162
+ **HUMAN** — Get explicit user approval before any changes:
163
+ - Security-critical decisions, secret handling
164
+ - Irreversible operations (data migrations, schema drops)
165
+ - Publishing (`npm publish`, Railway deploy)
166
+
167
+ Don't over-engineer AUTOMATE tasks. Don't under-review CAREFUL ones.
168
+
133
169
  ## Workflow
134
170
 
135
- - Check .env for API keys before asking
136
- - Run npm run linear:sync after task completion
171
+ - Check `.env` for API keys before asking the user
172
+ - Run `npm run linear:sync` after completing tasks
137
173
  - Use browser MCP for visual testing
138
- - Review recent commits and stackmemory.json on session start
174
+ - On session start: review recent commits and `stackmemory.json`
139
175
  - Use subagents for multi-step tasks
140
- - Ask 1-3 clarifying questions for complex commands (one at a time)
176
+ - Ask 13 clarifying questions for ambiguous requests (one at a time)
@@ -25,6 +25,10 @@ docs/ # Documentation
25
25
  - MCP Server: src/integrations/mcp/server.ts
26
26
  - Frame Manager: src/core/context/frame-manager.ts
27
27
  - Database: src/core/database/sqlite-adapter.ts
28
+ - Snapshot: src/core/worktree/capture.ts
29
+ - Preflight: src/core/worktree/preflight.ts
30
+ - Conductor: src/cli/commands/orchestrator.ts
31
+ - Shared Utils: src/core/utils/{git,text,fs}.ts
28
32
 
29
33
  ## Detailed Guides
30
34
 
@@ -55,6 +59,10 @@ npm run linear:sync # Sync with Linear
55
59
  # StackMemory CLI
56
60
  stackmemory capture # Save session state for handoff
57
61
  stackmemory restore # Restore from captured state
62
+ stackmemory snapshot save # Post-run context snapshot (alias: snap)
63
+ stackmemory snapshot list # List recent snapshots
64
+ stackmemory preflight # File overlap check for parallel tasks (alias: pf)
65
+ stackmemory conductor start # Autonomous Linear→worktree→agent orchestrator
58
66
  ```
59
67
 
60
68
  ## Working Directory
@@ -25,6 +25,10 @@ docs/ # Documentation
25
25
  - MCP Server: src/integrations/mcp/server.ts
26
26
  - Frame Manager: src/core/context/frame-manager.ts
27
27
  - Database: src/core/database/sqlite-adapter.ts
28
+ - Snapshot: src/core/worktree/capture.ts
29
+ - Preflight: src/core/worktree/preflight.ts
30
+ - Conductor: src/cli/commands/orchestrator.ts
31
+ - Shared Utils: src/core/utils/{git,text,fs}.ts
28
32
 
29
33
  ## Detailed Guides
30
34
 
@@ -55,6 +59,10 @@ npm run linear:sync # Sync with Linear
55
59
  # StackMemory CLI
56
60
  stackmemory capture # Save session state for handoff
57
61
  stackmemory restore # Restore from captured state
62
+ stackmemory snapshot save # Post-run context snapshot (alias: snap)
63
+ stackmemory snapshot list # List recent snapshots
64
+ stackmemory preflight # File overlap check for parallel tasks (alias: pf)
65
+ stackmemory conductor start # Autonomous Linear→worktree→agent orchestrator
58
66
  ```
59
67
 
60
68
  ## Working Directory