@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.
- package/dist/src/cli/codex-sm.js +6 -8
- package/dist/src/cli/commands/config.js +0 -81
- package/dist/src/cli/commands/context-rehydrate.js +133 -47
- package/dist/src/cli/commands/db.js +35 -8
- package/dist/src/cli/commands/handoff.js +1 -1
- package/dist/src/cli/commands/linear.js +9 -0
- package/dist/src/cli/commands/ralph.js +2 -2
- package/dist/src/cli/commands/setup.js +2 -2
- package/dist/src/cli/commands/signup.js +3 -1
- package/dist/src/cli/commands/skills.js +123 -1
- package/dist/src/cli/commands/storage-tier.js +26 -8
- package/dist/src/cli/index.js +1 -57
- package/dist/src/core/config/feature-flags.js +0 -4
- package/dist/src/core/context/dual-stack-manager.js +10 -3
- package/dist/src/core/context/frame-database.js +32 -0
- package/dist/src/core/context/frame-handoff-manager.js +2 -2
- package/dist/src/core/context/{refactored-frame-manager.js → frame-manager.js} +3 -3
- package/dist/src/core/context/index.js +2 -2
- package/dist/src/core/database/sqlite-adapter.js +161 -1
- package/dist/src/core/digest/frame-digest-integration.js +1 -1
- package/dist/src/core/digest/index.js +1 -1
- package/dist/src/core/execution/parallel-executor.js +5 -1
- package/dist/src/core/projects/project-isolation.js +18 -4
- package/dist/src/core/security/index.js +2 -0
- package/dist/src/core/security/input-sanitizer.js +23 -0
- package/dist/src/core/utils/update-checker.js +10 -6
- package/dist/src/daemon/daemon-config.js +2 -1
- package/dist/src/daemon/services/auto-save-service.js +121 -0
- package/dist/src/daemon/services/maintenance-service.js +76 -1
- package/dist/src/features/sweep/prompt-builder.js +2 -2
- package/dist/src/hooks/graphiti-hooks.js +149 -0
- package/dist/src/hooks/session-summary.js +30 -0
- package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +115 -0
- package/dist/src/integrations/greptile/client.js +101 -0
- package/dist/src/integrations/greptile/config.js +14 -0
- package/dist/src/integrations/greptile/index.js +11 -0
- package/dist/src/integrations/greptile/types.js +4 -0
- package/dist/src/integrations/linear/config.js +3 -1
- package/dist/src/integrations/linear/sync.js +18 -5
- package/dist/src/integrations/linear/webhook.js +16 -0
- package/dist/src/integrations/mcp/handlers/code-execution-handlers.js +33 -7
- package/dist/src/integrations/mcp/handlers/cord-handlers.js +397 -0
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +456 -0
- package/dist/src/integrations/mcp/handlers/index.js +55 -1
- package/dist/src/integrations/mcp/handlers/task-handlers.js +55 -12
- package/dist/src/integrations/mcp/handlers/team-handlers.js +211 -0
- package/dist/src/integrations/mcp/handlers/trace-handlers.js +25 -9
- package/dist/src/integrations/mcp/index.js +2 -2
- package/dist/src/integrations/mcp/refactored-server.js +31 -10
- package/dist/src/integrations/mcp/server.js +27 -0
- package/dist/src/integrations/mcp/tool-definitions.js +215 -1
- package/dist/src/integrations/ralph/context/context-budget-manager.js +10 -2
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +54 -22
- package/dist/src/integrations/ralph/learning/pattern-learner.js +59 -24
- package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +81 -35
- package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +12 -4
- package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +32 -9
- package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +25 -8
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +17 -5
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +73 -22
- package/dist/src/skills/claude-skills.js +46 -103
- package/dist/src/skills/parallel-agent-skill.js +514 -0
- package/dist/src/utils/hook-installer.js +8 -0
- package/package.json +5 -5
- package/scripts/gepa/.before-optimize.md +140 -159
- package/scripts/gepa/config.json +7 -1
- package/scripts/gepa/evals/fixtures/api-endpoint.ts +31 -0
- package/scripts/gepa/evals/fixtures/brittle-integration.ts +38 -0
- package/scripts/gepa/evals/fixtures/fts5-triggers.sql +23 -0
- package/scripts/gepa/evals/fixtures/leaky-service.ts +70 -0
- package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +39 -0
- package/scripts/gepa/evals/fixtures/pr-diff.txt +24 -0
- package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +34 -0
- package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +42 -0
- package/scripts/gepa/evals/stackmemory-tasks.jsonl +8 -0
- package/scripts/gepa/generations/gen-000/baseline.md +172 -159
- package/scripts/gepa/generations/gen-001/baseline.md +172 -159
- package/scripts/gepa/generations/gen-001/variant-a.md +156 -146
- package/scripts/gepa/generations/gen-001/variant-b.md +199 -170
- package/scripts/gepa/generations/gen-001/variant-c.md +127 -46
- package/scripts/gepa/generations/gen-001/variant-d.md +160 -107
- package/scripts/gepa/hooks/reflect.js +44 -5
- package/scripts/gepa/optimize.js +281 -39
- package/scripts/gepa/results/eval-1-baseline.json +187 -10
- package/scripts/gepa/results/eval-1-variant-a.json +188 -11
- package/scripts/gepa/results/eval-1-variant-b.json +188 -11
- package/scripts/gepa/results/eval-1-variant-c.json +168 -11
- package/scripts/gepa/results/eval-1-variant-d.json +169 -12
- package/scripts/gepa/state.json +18 -18
- package/scripts/install-claude-hooks-auto.js +8 -0
- package/templates/claude-hooks/cord-trace.js +225 -0
- package/dist/src/core/config/storage-config.js +0 -114
- package/dist/src/core/storage/chromadb-adapter.js +0 -379
- package/dist/src/integrations/claude-code/enhanced-pre-clear-hooks.js +0 -458
- package/dist/src/integrations/ralph/coordination/enhanced-coordination.js +0 -409
- package/dist/src/skills/repo-ingestion-skill.js +0 -631
- package/templates/claude-hooks/chromadb-wrapper +0 -21
- /package/dist/src/core/context/{enhanced-rehydration.js → rehydration.js} +0 -0
- /package/dist/src/core/digest/{enhanced-hybrid-digest.js → hybrid-digest.js} +0 -0
- /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 = [
|
|
987
|
-
|
|
988
|
-
|
|
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
|
+
};
|