@stackmemoryai/stackmemory 1.2.1 → 1.2.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.
Files changed (100) hide show
  1. package/dist/src/cli/codex-sm.js +6 -8
  2. package/dist/src/cli/commands/config.js +0 -81
  3. package/dist/src/cli/commands/context-rehydrate.js +133 -47
  4. package/dist/src/cli/commands/db.js +35 -8
  5. package/dist/src/cli/commands/handoff.js +1 -1
  6. package/dist/src/cli/commands/linear.js +9 -0
  7. package/dist/src/cli/commands/ralph.js +2 -2
  8. package/dist/src/cli/commands/setup.js +2 -2
  9. package/dist/src/cli/commands/signup.js +3 -1
  10. package/dist/src/cli/commands/skills.js +123 -1
  11. package/dist/src/cli/commands/storage-tier.js +26 -8
  12. package/dist/src/cli/index.js +1 -57
  13. package/dist/src/core/config/feature-flags.js +0 -4
  14. package/dist/src/core/context/dual-stack-manager.js +10 -3
  15. package/dist/src/core/context/frame-database.js +32 -0
  16. package/dist/src/core/context/frame-handoff-manager.js +2 -2
  17. package/dist/src/core/context/{refactored-frame-manager.js → frame-manager.js} +3 -3
  18. package/dist/src/core/context/index.js +2 -2
  19. package/dist/src/core/database/sqlite-adapter.js +161 -1
  20. package/dist/src/core/digest/frame-digest-integration.js +1 -1
  21. package/dist/src/core/digest/index.js +1 -1
  22. package/dist/src/core/execution/parallel-executor.js +5 -1
  23. package/dist/src/core/projects/project-isolation.js +18 -4
  24. package/dist/src/core/security/index.js +2 -0
  25. package/dist/src/core/security/input-sanitizer.js +23 -0
  26. package/dist/src/core/utils/update-checker.js +10 -6
  27. package/dist/src/daemon/daemon-config.js +2 -1
  28. package/dist/src/daemon/services/auto-save-service.js +121 -0
  29. package/dist/src/daemon/services/maintenance-service.js +76 -1
  30. package/dist/src/features/sweep/prompt-builder.js +2 -2
  31. package/dist/src/hooks/graphiti-hooks.js +149 -0
  32. package/dist/src/hooks/session-summary.js +30 -0
  33. package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +115 -0
  34. package/dist/src/integrations/greptile/client.js +101 -0
  35. package/dist/src/integrations/greptile/config.js +14 -0
  36. package/dist/src/integrations/greptile/index.js +11 -0
  37. package/dist/src/integrations/greptile/types.js +4 -0
  38. package/dist/src/integrations/linear/config.js +3 -1
  39. package/dist/src/integrations/linear/sync.js +18 -5
  40. package/dist/src/integrations/linear/webhook.js +16 -0
  41. package/dist/src/integrations/mcp/handlers/code-execution-handlers.js +33 -7
  42. package/dist/src/integrations/mcp/handlers/cord-handlers.js +397 -0
  43. package/dist/src/integrations/mcp/handlers/greptile-handlers.js +456 -0
  44. package/dist/src/integrations/mcp/handlers/index.js +55 -1
  45. package/dist/src/integrations/mcp/handlers/task-handlers.js +55 -12
  46. package/dist/src/integrations/mcp/handlers/team-handlers.js +211 -0
  47. package/dist/src/integrations/mcp/handlers/trace-handlers.js +25 -9
  48. package/dist/src/integrations/mcp/index.js +2 -2
  49. package/dist/src/integrations/mcp/refactored-server.js +31 -10
  50. package/dist/src/integrations/mcp/server.js +27 -0
  51. package/dist/src/integrations/mcp/tool-definitions.js +215 -1
  52. package/dist/src/integrations/ralph/context/context-budget-manager.js +10 -2
  53. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +54 -22
  54. package/dist/src/integrations/ralph/learning/pattern-learner.js +59 -24
  55. package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +81 -35
  56. package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +12 -4
  57. package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +32 -9
  58. package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +25 -8
  59. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +17 -5
  60. package/dist/src/integrations/ralph/visualization/ralph-debugger.js +73 -22
  61. package/dist/src/skills/claude-skills.js +46 -103
  62. package/dist/src/skills/parallel-agent-skill.js +514 -0
  63. package/dist/src/utils/hook-installer.js +8 -0
  64. package/package.json +5 -5
  65. package/scripts/gepa/.before-optimize.md +140 -159
  66. package/scripts/gepa/config.json +7 -1
  67. package/scripts/gepa/evals/fixtures/api-endpoint.ts +31 -0
  68. package/scripts/gepa/evals/fixtures/brittle-integration.ts +38 -0
  69. package/scripts/gepa/evals/fixtures/fts5-triggers.sql +23 -0
  70. package/scripts/gepa/evals/fixtures/leaky-service.ts +70 -0
  71. package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +39 -0
  72. package/scripts/gepa/evals/fixtures/pr-diff.txt +24 -0
  73. package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +34 -0
  74. package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +42 -0
  75. package/scripts/gepa/evals/stackmemory-tasks.jsonl +8 -0
  76. package/scripts/gepa/generations/gen-000/baseline.md +172 -159
  77. package/scripts/gepa/generations/gen-001/baseline.md +172 -159
  78. package/scripts/gepa/generations/gen-001/variant-a.md +156 -146
  79. package/scripts/gepa/generations/gen-001/variant-b.md +199 -170
  80. package/scripts/gepa/generations/gen-001/variant-c.md +127 -46
  81. package/scripts/gepa/generations/gen-001/variant-d.md +160 -107
  82. package/scripts/gepa/hooks/reflect.js +44 -5
  83. package/scripts/gepa/optimize.js +281 -39
  84. package/scripts/gepa/results/eval-1-baseline.json +187 -10
  85. package/scripts/gepa/results/eval-1-variant-a.json +188 -11
  86. package/scripts/gepa/results/eval-1-variant-b.json +188 -11
  87. package/scripts/gepa/results/eval-1-variant-c.json +168 -11
  88. package/scripts/gepa/results/eval-1-variant-d.json +169 -12
  89. package/scripts/gepa/state.json +18 -18
  90. package/scripts/install-claude-hooks-auto.js +8 -0
  91. package/templates/claude-hooks/cord-trace.js +225 -0
  92. package/dist/src/core/config/storage-config.js +0 -114
  93. package/dist/src/core/storage/chromadb-adapter.js +0 -379
  94. package/dist/src/integrations/claude-code/enhanced-pre-clear-hooks.js +0 -458
  95. package/dist/src/integrations/ralph/coordination/enhanced-coordination.js +0 -409
  96. package/dist/src/skills/repo-ingestion-skill.js +0 -631
  97. package/templates/claude-hooks/chromadb-wrapper +0 -21
  98. /package/dist/src/core/context/{enhanced-rehydration.js → rehydration.js} +0 -0
  99. /package/dist/src/core/digest/{enhanced-hybrid-digest.js → hybrid-digest.js} +0 -0
  100. /package/dist/src/core/session/{enhanced-handoff.js → handoff.js} +0 -0
@@ -3,9 +3,6 @@ import { dirname as __pathDirname } from 'path';
3
3
  const __filename = __fileURLToPath(import.meta.url);
4
4
  const __dirname = __pathDirname(__filename);
5
5
  import { logger } from "../core/monitoring/logger.js";
6
- import {
7
- RepoIngestionSkill
8
- } from "./repo-ingestion-skill.js";
9
6
  import {
10
7
  RecursiveAgentOrchestrator
11
8
  } from "./recursive-agent-orchestrator.js";
@@ -617,22 +614,6 @@ class ClaudeSkillsManager {
617
614
  this.dashboardLauncher = new module.DashboardLauncherSkill();
618
615
  logger.info("Dashboard launcher initialized (manual launch required)");
619
616
  });
620
- const chromaConfig = {
621
- apiKey: process.env["CHROMADB_API_KEY"] || "",
622
- tenant: process.env["CHROMADB_TENANT"] || "",
623
- database: process.env["CHROMADB_DATABASE"] || "stackmemory",
624
- collectionName: process.env["CHROMADB_COLLECTION"] || "stackmemory_repos"
625
- };
626
- if (chromaConfig.apiKey && chromaConfig.tenant) {
627
- this.repoIngestionSkill = new RepoIngestionSkill(
628
- chromaConfig,
629
- context.userId,
630
- process.env["CHROMADB_TEAM_ID"]
631
- );
632
- this.repoIngestionSkill.initialize().catch((error) => {
633
- logger.warn("Repo ingestion skill initialization failed:", error);
634
- });
635
- }
636
617
  import("../features/tasks/linear-task-manager.js").then((module) => {
637
618
  const taskStore = new module.LinearTaskManager();
638
619
  const frameManager = context.frameManager;
@@ -662,7 +643,6 @@ class ClaudeSkillsManager {
662
643
  checkpointSkill;
663
644
  archaeologistSkill;
664
645
  dashboardLauncher;
665
- repoIngestionSkill = null;
666
646
  rlmOrchestrator = null;
667
647
  apiSkill;
668
648
  specGeneratorSkill;
@@ -738,64 +718,6 @@ class ClaudeSkillsManager {
738
718
  };
739
719
  }
740
720
  return this.rlmOrchestrator.execute(args[0], options);
741
- case "repo":
742
- case "ingest":
743
- if (!this.repoIngestionSkill) {
744
- return {
745
- success: false,
746
- message: "Repo ingestion skill not initialized. Please configure ChromaDB."
747
- };
748
- }
749
- const repoCommand = args[0];
750
- switch (repoCommand) {
751
- case "ingest":
752
- const repoPath = args[1] || process.cwd();
753
- const repoName = args[2] || path.basename(repoPath);
754
- return await this.repoIngestionSkill.ingestRepository(
755
- repoPath,
756
- repoName,
757
- options
758
- );
759
- case "update":
760
- const updatePath = args[1] || process.cwd();
761
- const updateName = args[2] || path.basename(updatePath);
762
- return await this.repoIngestionSkill.updateRepository(
763
- updatePath,
764
- updateName,
765
- options
766
- );
767
- case "search":
768
- const query = args[1];
769
- if (!query) {
770
- return {
771
- success: false,
772
- message: "Search query required"
773
- };
774
- }
775
- const results = await this.repoIngestionSkill.searchCode(query, {
776
- repoName: options?.repoName,
777
- language: options?.language,
778
- limit: options?.limit,
779
- includeContext: options?.includeContext
780
- });
781
- return {
782
- success: true,
783
- message: `Found ${results.length} results`,
784
- data: results
785
- };
786
- case "stats":
787
- const stats = await this.repoIngestionSkill.getRepoStats(args[1]);
788
- return {
789
- success: true,
790
- message: "Repository statistics",
791
- data: stats
792
- };
793
- default:
794
- return {
795
- success: false,
796
- message: `Unknown repo command: ${repoCommand}. Use: ingest, update, search, or stats`
797
- };
798
- }
799
721
  case "dashboard":
800
722
  const dashboardCmd = args[0];
801
723
  if (!this.dashboardLauncher) {
@@ -948,6 +870,25 @@ class ClaudeSkillsManager {
948
870
  };
949
871
  }
950
872
  }
873
+ case "agent": {
874
+ const { ParallelAgentSkill } = await import("./parallel-agent-skill.js");
875
+ const agentSkill = new ParallelAgentSkill(this.context);
876
+ const sub = args[0];
877
+ const agentOpts = { timeout: options?.timeout };
878
+ switch (sub) {
879
+ case "research":
880
+ return agentSkill.research(args.slice(1).join(" "), agentOpts);
881
+ case "maintain":
882
+ return agentSkill.maintain(args.slice(1).join(" "), agentOpts);
883
+ case "spec-run":
884
+ return agentSkill.specRun(args[1], agentOpts);
885
+ default:
886
+ return {
887
+ success: false,
888
+ message: `Unknown: agent ${sub}. Use: research|maintain|spec-run`
889
+ };
890
+ }
891
+ }
951
892
  case "linear-run": {
952
893
  if (!this.linearTaskRunner) {
953
894
  return {
@@ -983,10 +924,15 @@ class ClaudeSkillsManager {
983
924
  }
984
925
  }
985
926
  getAvailableSkills() {
986
- const skills = ["handoff", "checkpoint", "dig", "dashboard", "api", "spec"];
987
- if (this.repoIngestionSkill) {
988
- skills.push("repo");
989
- }
927
+ const skills = [
928
+ "handoff",
929
+ "checkpoint",
930
+ "dig",
931
+ "dashboard",
932
+ "api",
933
+ "spec",
934
+ "agent"
935
+ ];
990
936
  if (this.rlmOrchestrator) {
991
937
  skills.push("rlm", "lint");
992
938
  }
@@ -1070,27 +1016,6 @@ Launch the StackMemory web dashboard for real-time monitoring
1070
1016
  - launch: Start the web dashboard and open in browser (default)
1071
1017
  - stop: Stop the dashboard server
1072
1018
  Auto-launches on new sessions when configured
1073
- `;
1074
- case "repo":
1075
- return `
1076
- /repo ingest [path] [name] [--incremental] [--include-tests] [--include-docs]
1077
- /repo update [path] [name] [--force-update]
1078
- /repo search "query" [--repo-name name] [--language lang] [--limit n]
1079
- /repo stats [repo-name]
1080
-
1081
- Ingest and search code repositories in ChromaDB:
1082
- - ingest: Index a new repository (defaults to current directory)
1083
- - update: Update an existing repository with changes
1084
- - search: Semantic search across ingested code
1085
- - stats: View statistics about ingested repositories
1086
-
1087
- Options:
1088
- - --incremental: Only process changed files
1089
- - --include-tests: Include test files in indexing
1090
- - --include-docs: Include documentation files
1091
- - --force-update: Force re-indexing of all files
1092
- - --language: Filter search by programming language
1093
- - --limit: Maximum search results (default: 20)
1094
1019
  `;
1095
1020
  case "recursive":
1096
1021
  return `
@@ -1145,6 +1070,24 @@ Examples:
1145
1070
  /spec dev-spec
1146
1071
  /spec update prompt-plan "Initialize repository and tooling"
1147
1072
  /spec validate prompt-plan
1073
+ `;
1074
+ case "agent":
1075
+ return `
1076
+ /agent research "How does the FTS5 search work?"
1077
+ /agent maintain "Fix the deprecation warning in webhook.ts"
1078
+ /agent spec-run docs/specs/my-feature.md
1079
+
1080
+ Parallel agent skill (Willison patterns) \u2014 spawn isolated Claude agents:
1081
+ research \u2014 Explore codebase, save findings as a frame (read-only)
1082
+ maintain \u2014 Low-stakes fix, produces a .patch file
1083
+ spec-run \u2014 Implement a spec on a branch, validate with lint+test+build
1084
+
1085
+ Options:
1086
+ --timeout <ms> Agent timeout in milliseconds (default: 300000)
1087
+
1088
+ Each agent runs in a disposable /tmp workspace (git clone --depth=1).
1089
+ Patches: git apply .stackmemory/patches/<file>.patch
1090
+ Spec branches: cd /tmp/sm-spec-* && git log --oneline
1148
1091
  `;
1149
1092
  case "linear-run":
1150
1093
  return `
@@ -0,0 +1,514 @@
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 { spawn } from "child_process";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import { execSync } from "child_process";
9
+ import { logger } from "../core/monitoring/logger.js";
10
+ import { hookEmitter } from "../hooks/events.js";
11
+ const DEFAULT_TIMEOUT = 5 * 60 * 1e3;
12
+ function spawnAgent(prompt, opts) {
13
+ return new Promise((resolve, reject) => {
14
+ let workDir;
15
+ try {
16
+ workDir = fs.mkdtempSync(path.join("/tmp", `${opts.prefix}-`));
17
+ } catch (err) {
18
+ return reject(
19
+ new Error(`Failed to create temp dir: ${err.message}`)
20
+ );
21
+ }
22
+ try {
23
+ execSync(`git clone --depth=1 "${opts.repoRoot}" "${workDir}"`, {
24
+ stdio: "pipe",
25
+ timeout: 3e4
26
+ });
27
+ } catch (err) {
28
+ cleanupDir(workDir);
29
+ return reject(new Error(`git clone failed: ${err.message}`));
30
+ }
31
+ const child = spawn("claude", ["--print"], {
32
+ cwd: workDir,
33
+ stdio: ["pipe", "pipe", "pipe"],
34
+ env: { ...process.env }
35
+ });
36
+ let stdout = "";
37
+ let stderr = "";
38
+ let killed = false;
39
+ const timer = setTimeout(() => {
40
+ killed = true;
41
+ child.kill("SIGTERM");
42
+ }, opts.timeout);
43
+ if (child.stdout) {
44
+ child.stdout.on("data", (d) => stdout += d);
45
+ }
46
+ if (child.stderr) {
47
+ child.stderr.on("data", (d) => stderr += d);
48
+ }
49
+ child.on("close", (code) => {
50
+ clearTimeout(timer);
51
+ resolve({ stdout, stderr, exitCode: code, workDir, timedOut: killed });
52
+ });
53
+ child.on("error", (err) => {
54
+ clearTimeout(timer);
55
+ resolve({
56
+ stdout,
57
+ stderr: err.message,
58
+ exitCode: 1,
59
+ workDir,
60
+ timedOut: false
61
+ });
62
+ });
63
+ if (child.stdin) {
64
+ child.stdin.write(prompt);
65
+ child.stdin.end();
66
+ }
67
+ });
68
+ }
69
+ function cleanupDir(dir) {
70
+ try {
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ } catch {
73
+ }
74
+ }
75
+ function slugify(text) {
76
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
77
+ }
78
+ class ParallelAgentSkill {
79
+ constructor(context) {
80
+ this.context = context;
81
+ this.repoRoot = process.cwd();
82
+ }
83
+ repoRoot;
84
+ /**
85
+ * Research — explore codebase, save findings as a frame.
86
+ */
87
+ async research(question, options) {
88
+ if (!question?.trim()) {
89
+ return { success: false, message: "Question is required" };
90
+ }
91
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
92
+ const prompt = [
93
+ "Explore this codebase and answer the following question.",
94
+ "Do NOT modify any files. Output your findings as structured markdown.",
95
+ "",
96
+ `Question: ${question}`
97
+ ].join("\n");
98
+ let result;
99
+ try {
100
+ hookEmitter.emitHook({
101
+ type: "agent_start",
102
+ timestamp: Date.now(),
103
+ data: { agentType: "research", workDir: "", task: question }
104
+ }).catch(() => {
105
+ });
106
+ result = await spawnAgent(prompt, {
107
+ prefix: "sm-research",
108
+ timeout,
109
+ repoRoot: this.repoRoot
110
+ });
111
+ } catch (err) {
112
+ const msg = err.message;
113
+ hookEmitter.emitHook({
114
+ type: "agent_error",
115
+ timestamp: Date.now(),
116
+ data: { agentType: "research", error: msg }
117
+ }).catch(() => {
118
+ });
119
+ return {
120
+ success: false,
121
+ message: `Agent failed to start: ${msg}`
122
+ };
123
+ }
124
+ try {
125
+ if (result.timedOut) {
126
+ hookEmitter.emitHook({
127
+ type: "agent_complete",
128
+ timestamp: Date.now(),
129
+ data: {
130
+ agentType: "research",
131
+ workDir: result.workDir,
132
+ exitCode: result.exitCode,
133
+ timedOut: true
134
+ }
135
+ }).catch(() => {
136
+ });
137
+ return {
138
+ success: false,
139
+ message: `Research agent timed out after ${timeout / 1e3}s`,
140
+ data: { partial: result.stdout.slice(0, 2e3) }
141
+ };
142
+ }
143
+ const findings = result.stdout || "(no output)";
144
+ let frameId;
145
+ try {
146
+ frameId = await this.saveFrame("research", question, findings);
147
+ } catch (err) {
148
+ logger.warn("Failed to save research frame:", err);
149
+ }
150
+ hookEmitter.emitHook({
151
+ type: "agent_complete",
152
+ timestamp: Date.now(),
153
+ data: {
154
+ agentType: "research",
155
+ workDir: result.workDir,
156
+ exitCode: result.exitCode,
157
+ timedOut: false,
158
+ frameId
159
+ }
160
+ }).catch(() => {
161
+ });
162
+ return {
163
+ success: true,
164
+ message: `Research complete${frameId ? ` (frame: ${frameId})` : ""}`,
165
+ data: { findings, frameId, exitCode: result.exitCode }
166
+ };
167
+ } finally {
168
+ cleanupDir(result.workDir);
169
+ }
170
+ }
171
+ /**
172
+ * Maintain — low-stakes fix, produces a .patch file.
173
+ */
174
+ async maintain(task, options) {
175
+ if (!task?.trim()) {
176
+ return { success: false, message: "Task description is required" };
177
+ }
178
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
179
+ const prompt = [
180
+ task,
181
+ "",
182
+ "After making changes, verify by running: npm run lint && npm run test:run"
183
+ ].join("\n");
184
+ let result;
185
+ try {
186
+ hookEmitter.emitHook({
187
+ type: "agent_start",
188
+ timestamp: Date.now(),
189
+ data: { agentType: "maintain", workDir: "", task }
190
+ }).catch(() => {
191
+ });
192
+ result = await spawnAgent(prompt, {
193
+ prefix: "sm-maint",
194
+ timeout,
195
+ repoRoot: this.repoRoot
196
+ });
197
+ } catch (err) {
198
+ const msg = err.message;
199
+ hookEmitter.emitHook({
200
+ type: "agent_error",
201
+ timestamp: Date.now(),
202
+ data: { agentType: "maintain", error: msg }
203
+ }).catch(() => {
204
+ });
205
+ return {
206
+ success: false,
207
+ message: `Agent failed to start: ${msg}`
208
+ };
209
+ }
210
+ try {
211
+ if (result.timedOut) {
212
+ hookEmitter.emitHook({
213
+ type: "agent_complete",
214
+ timestamp: Date.now(),
215
+ data: {
216
+ agentType: "maintain",
217
+ workDir: result.workDir,
218
+ exitCode: result.exitCode,
219
+ timedOut: true
220
+ }
221
+ }).catch(() => {
222
+ });
223
+ return {
224
+ success: false,
225
+ message: `Maintenance agent timed out after ${timeout / 1e3}s`,
226
+ data: { partial: result.stdout.slice(0, 2e3) }
227
+ };
228
+ }
229
+ let diff = "";
230
+ try {
231
+ diff = execSync("git diff HEAD", {
232
+ cwd: result.workDir,
233
+ encoding: "utf-8",
234
+ timeout: 1e4
235
+ });
236
+ } catch {
237
+ }
238
+ if (!diff.trim()) {
239
+ hookEmitter.emitHook({
240
+ type: "agent_complete",
241
+ timestamp: Date.now(),
242
+ data: {
243
+ agentType: "maintain",
244
+ workDir: result.workDir,
245
+ exitCode: result.exitCode,
246
+ timedOut: false
247
+ }
248
+ }).catch(() => {
249
+ });
250
+ return {
251
+ success: false,
252
+ message: "Agent made no changes (empty diff)",
253
+ data: { output: result.stdout.slice(0, 2e3) }
254
+ };
255
+ }
256
+ let validated = false;
257
+ try {
258
+ execSync("npm run lint", {
259
+ cwd: result.workDir,
260
+ stdio: "pipe",
261
+ timeout: 6e4
262
+ });
263
+ validated = true;
264
+ } catch {
265
+ }
266
+ const patchDir = path.join(this.repoRoot, ".stackmemory", "patches");
267
+ fs.mkdirSync(patchDir, { recursive: true });
268
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
269
+ const slug = slugify(task);
270
+ const patchFile = `${timestamp}-${slug}.patch`;
271
+ const patchPath = path.join(patchDir, patchFile);
272
+ fs.writeFileSync(patchPath, diff);
273
+ const diffLines = diff.split("\n");
274
+ const filesChanged = diffLines.filter(
275
+ (l) => l.startsWith("diff --git")
276
+ ).length;
277
+ const additions = diffLines.filter((l) => /^\+[^+]/.test(l)).length;
278
+ const deletions = diffLines.filter((l) => /^-[^-]/.test(l)).length;
279
+ hookEmitter.emitHook({
280
+ type: "agent_complete",
281
+ timestamp: Date.now(),
282
+ data: {
283
+ agentType: "maintain",
284
+ workDir: result.workDir,
285
+ exitCode: result.exitCode,
286
+ timedOut: false,
287
+ patchPath,
288
+ validated
289
+ }
290
+ }).catch(() => {
291
+ });
292
+ return {
293
+ success: true,
294
+ message: `Patch saved: ${patchFile}${validated ? "" : " (lint failed)"}`,
295
+ data: {
296
+ patchPath,
297
+ validated,
298
+ filesChanged,
299
+ additions,
300
+ deletions,
301
+ output: result.stdout.slice(0, 2e3)
302
+ },
303
+ action: `Apply with: git apply ${patchPath}`
304
+ };
305
+ } finally {
306
+ cleanupDir(result.workDir);
307
+ }
308
+ }
309
+ /**
310
+ * SpecRun — implement a spec file on a branch, validate.
311
+ */
312
+ async specRun(specPath, options) {
313
+ if (!specPath?.trim()) {
314
+ return { success: false, message: "Spec file path is required" };
315
+ }
316
+ const absSpec = path.isAbsolute(specPath) ? specPath : path.join(this.repoRoot, specPath);
317
+ if (!fs.existsSync(absSpec)) {
318
+ return { success: false, message: `Spec file not found: ${specPath}` };
319
+ }
320
+ const specContent = fs.readFileSync(absSpec, "utf-8");
321
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
322
+ const prompt = [
323
+ "Implement the following specification. Follow it precisely.",
324
+ "",
325
+ "--- SPEC START ---",
326
+ specContent,
327
+ "--- SPEC END ---"
328
+ ].join("\n");
329
+ let workDir;
330
+ try {
331
+ workDir = fs.mkdtempSync(path.join("/tmp", "sm-spec-"));
332
+ } catch (err) {
333
+ return {
334
+ success: false,
335
+ message: `Failed to create temp dir: ${err.message}`
336
+ };
337
+ }
338
+ try {
339
+ execSync(`git clone --depth=1 "${this.repoRoot}" "${workDir}"`, {
340
+ stdio: "pipe",
341
+ timeout: 3e4
342
+ });
343
+ } catch (err) {
344
+ const msg = err.message;
345
+ cleanupDir(workDir);
346
+ hookEmitter.emitHook({
347
+ type: "agent_error",
348
+ timestamp: Date.now(),
349
+ data: { agentType: "spec-run", error: msg, workDir }
350
+ }).catch(() => {
351
+ });
352
+ return {
353
+ success: false,
354
+ message: `git clone failed: ${msg}`
355
+ };
356
+ }
357
+ const branchName = `agent/spec-${Date.now()}`;
358
+ try {
359
+ execSync(`git checkout -b "${branchName}"`, {
360
+ cwd: workDir,
361
+ stdio: "pipe",
362
+ timeout: 5e3
363
+ });
364
+ } catch (err) {
365
+ const msg = err.message;
366
+ cleanupDir(workDir);
367
+ hookEmitter.emitHook({
368
+ type: "agent_error",
369
+ timestamp: Date.now(),
370
+ data: { agentType: "spec-run", error: msg, workDir }
371
+ }).catch(() => {
372
+ });
373
+ return {
374
+ success: false,
375
+ message: `Failed to create branch: ${msg}`
376
+ };
377
+ }
378
+ hookEmitter.emitHook({
379
+ type: "agent_start",
380
+ timestamp: Date.now(),
381
+ data: { agentType: "spec-run", workDir, task: specPath }
382
+ }).catch(() => {
383
+ });
384
+ const child = spawn("claude", ["--print"], {
385
+ cwd: workDir,
386
+ stdio: ["pipe", "pipe", "pipe"],
387
+ env: { ...process.env }
388
+ });
389
+ let stdout = "";
390
+ let stderr = "";
391
+ let killed = false;
392
+ const agentDone = new Promise((resolve) => {
393
+ const timer = setTimeout(() => {
394
+ killed = true;
395
+ child.kill("SIGTERM");
396
+ }, timeout);
397
+ if (child.stdout) {
398
+ child.stdout.on("data", (d) => stdout += d);
399
+ }
400
+ if (child.stderr) {
401
+ child.stderr.on("data", (d) => stderr += d);
402
+ }
403
+ child.on("close", (code) => {
404
+ clearTimeout(timer);
405
+ resolve({ stdout, stderr, exitCode: code, timedOut: killed });
406
+ });
407
+ child.on("error", (err) => {
408
+ clearTimeout(timer);
409
+ resolve({
410
+ stdout,
411
+ stderr: err.message,
412
+ exitCode: 1,
413
+ timedOut: false
414
+ });
415
+ });
416
+ });
417
+ if (child.stdin) {
418
+ child.stdin.write(prompt);
419
+ child.stdin.end();
420
+ }
421
+ const agentResult = await agentDone;
422
+ if (agentResult.timedOut) {
423
+ return {
424
+ success: false,
425
+ message: `Spec agent timed out after ${timeout / 1e3}s`,
426
+ data: {
427
+ workDir,
428
+ branch: branchName,
429
+ partial: agentResult.stdout.slice(0, 2e3)
430
+ }
431
+ };
432
+ }
433
+ const validation = { lint: false, test: false, build: false };
434
+ try {
435
+ execSync("npm run lint", {
436
+ cwd: workDir,
437
+ stdio: "pipe",
438
+ timeout: 6e4
439
+ });
440
+ validation.lint = true;
441
+ } catch {
442
+ }
443
+ try {
444
+ execSync("npm run test:run", {
445
+ cwd: workDir,
446
+ stdio: "pipe",
447
+ timeout: 12e4
448
+ });
449
+ validation.test = true;
450
+ } catch {
451
+ }
452
+ try {
453
+ execSync("npm run build", {
454
+ cwd: workDir,
455
+ stdio: "pipe",
456
+ timeout: 6e4
457
+ });
458
+ validation.build = true;
459
+ } catch {
460
+ }
461
+ let diffStat = "";
462
+ try {
463
+ diffStat = execSync("git diff --stat HEAD~1", {
464
+ cwd: workDir,
465
+ encoding: "utf-8",
466
+ timeout: 5e3
467
+ });
468
+ } catch {
469
+ }
470
+ const allPassed = validation.lint && validation.test && validation.build;
471
+ hookEmitter.emitHook({
472
+ type: "agent_complete",
473
+ timestamp: Date.now(),
474
+ data: {
475
+ agentType: "spec-run",
476
+ workDir,
477
+ exitCode: agentResult.exitCode,
478
+ timedOut: false,
479
+ branch: branchName,
480
+ validation
481
+ }
482
+ }).catch(() => {
483
+ });
484
+ return {
485
+ success: allPassed,
486
+ message: allPassed ? `Spec implemented on branch ${branchName}` : `Spec implemented but validation failed on branch ${branchName}`,
487
+ data: {
488
+ workDir,
489
+ branch: branchName,
490
+ validation,
491
+ diffStat: diffStat.trim(),
492
+ output: agentResult.stdout.slice(0, 2e3)
493
+ },
494
+ action: allPassed ? `Review: cd ${workDir} && git log --oneline` : `Inspect: cd ${workDir} && git diff`
495
+ };
496
+ }
497
+ async saveFrame(type, query, content) {
498
+ const db = this.context.database;
499
+ if (!db) return void 0;
500
+ const frameId = await db.createFrame({
501
+ run_id: `agent-${Date.now()}`,
502
+ project_id: this.context.projectId,
503
+ type,
504
+ name: `Agent ${type}: ${query.slice(0, 60)}`,
505
+ state: "completed",
506
+ inputs: { query, agentType: type },
507
+ digest_text: content
508
+ });
509
+ return frameId;
510
+ }
511
+ }
512
+ export {
513
+ ParallelAgentSkill
514
+ };