@stackmemoryai/stackmemory 1.2.0 → 1.2.2
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/claude-sm.js +65 -0
- package/dist/src/cli/commands/skills.js +123 -1
- 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/webhook.js +16 -0
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +456 -0
- package/dist/src/integrations/mcp/server.js +136 -0
- package/dist/src/integrations/mcp/tool-definitions.js +53 -1
- package/dist/src/skills/claude-skills.js +46 -1
- package/dist/src/skills/parallel-agent-skill.js +514 -0
- package/dist/src/utils/hook-installer.js +155 -0
- package/package.json +2 -2
- package/scripts/gepa/.before-optimize.md +140 -0
- 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 +48 -0
- package/scripts/gepa/generations/gen-001/baseline.md +172 -0
- package/scripts/gepa/generations/gen-001/variant-a.md +176 -0
- package/scripts/gepa/generations/gen-001/variant-b.md +266 -0
- package/scripts/gepa/generations/gen-001/variant-c.md +142 -0
- package/scripts/gepa/generations/gen-001/variant-d.md +172 -0
- package/scripts/gepa/hooks/reflect.js +44 -5
- package/scripts/gepa/optimize.js +281 -39
- package/scripts/gepa/results/eval-1-baseline.json +218 -0
- package/scripts/gepa/results/eval-1-variant-a.json +218 -0
- package/scripts/gepa/results/eval-1-variant-b.json +218 -0
- package/scripts/gepa/results/eval-1-variant-c.json +198 -0
- package/scripts/gepa/results/eval-1-variant-d.json +198 -0
- package/scripts/gepa/state.json +44 -5
- package/scripts/install-claude-hooks-auto.js +176 -44
- package/templates/claude-hooks/auto-checkpoint.js +174 -0
- package/templates/claude-hooks/chime-on-stop.sh +22 -0
- package/templates/claude-hooks/session-rescue.sh +15 -0
- package/templates/claude-hooks/stop-checkpoint.js +120 -0
|
@@ -13,7 +13,8 @@ class MCPToolDefinitions {
|
|
|
13
13
|
...this.getLinearTools(),
|
|
14
14
|
...this.getTraceTools(),
|
|
15
15
|
...this.getDiscoveryTools(),
|
|
16
|
-
...this.getEditTools()
|
|
16
|
+
...this.getEditTools(),
|
|
17
|
+
...this.getGraphitiTools()
|
|
17
18
|
];
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
@@ -705,6 +706,55 @@ class MCPToolDefinitions {
|
|
|
705
706
|
}
|
|
706
707
|
];
|
|
707
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Graphiti knowledge graph tools
|
|
711
|
+
*/
|
|
712
|
+
getGraphitiTools() {
|
|
713
|
+
return [
|
|
714
|
+
{
|
|
715
|
+
name: "graphiti_status",
|
|
716
|
+
description: "Check Graphiti temporal knowledge graph connection status",
|
|
717
|
+
inputSchema: {
|
|
718
|
+
type: "object",
|
|
719
|
+
properties: {}
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
name: "graphiti_query",
|
|
724
|
+
description: "Query the Graphiti temporal knowledge graph for entities, relations, and episodes",
|
|
725
|
+
inputSchema: {
|
|
726
|
+
type: "object",
|
|
727
|
+
properties: {
|
|
728
|
+
query: {
|
|
729
|
+
type: "string",
|
|
730
|
+
description: "Semantic text query"
|
|
731
|
+
},
|
|
732
|
+
entityTypes: {
|
|
733
|
+
type: "array",
|
|
734
|
+
items: { type: "string" },
|
|
735
|
+
description: 'Entity types to filter (e.g., ["Person", "File", "Issue"])'
|
|
736
|
+
},
|
|
737
|
+
validFrom: {
|
|
738
|
+
type: "number",
|
|
739
|
+
description: "Start of time window (epoch ms)"
|
|
740
|
+
},
|
|
741
|
+
validTo: {
|
|
742
|
+
type: "number",
|
|
743
|
+
description: "End of time window (epoch ms)"
|
|
744
|
+
},
|
|
745
|
+
maxHops: {
|
|
746
|
+
type: "number",
|
|
747
|
+
description: "Graph traversal depth (default 2)"
|
|
748
|
+
},
|
|
749
|
+
k: {
|
|
750
|
+
type: "number",
|
|
751
|
+
description: "Top-k results (default 20)"
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
];
|
|
757
|
+
}
|
|
708
758
|
/**
|
|
709
759
|
* Get tool definition by name
|
|
710
760
|
*/
|
|
@@ -728,6 +778,8 @@ class MCPToolDefinitions {
|
|
|
728
778
|
return this.getDiscoveryTools();
|
|
729
779
|
case "edit":
|
|
730
780
|
return this.getEditTools();
|
|
781
|
+
case "graphiti":
|
|
782
|
+
return this.getGraphitiTools();
|
|
731
783
|
default:
|
|
732
784
|
return [];
|
|
733
785
|
}
|
|
@@ -948,6 +948,25 @@ class ClaudeSkillsManager {
|
|
|
948
948
|
};
|
|
949
949
|
}
|
|
950
950
|
}
|
|
951
|
+
case "agent": {
|
|
952
|
+
const { ParallelAgentSkill } = await import("./parallel-agent-skill.js");
|
|
953
|
+
const agentSkill = new ParallelAgentSkill(this.context);
|
|
954
|
+
const sub = args[0];
|
|
955
|
+
const agentOpts = { timeout: options?.timeout };
|
|
956
|
+
switch (sub) {
|
|
957
|
+
case "research":
|
|
958
|
+
return agentSkill.research(args.slice(1).join(" "), agentOpts);
|
|
959
|
+
case "maintain":
|
|
960
|
+
return agentSkill.maintain(args.slice(1).join(" "), agentOpts);
|
|
961
|
+
case "spec-run":
|
|
962
|
+
return agentSkill.specRun(args[1], agentOpts);
|
|
963
|
+
default:
|
|
964
|
+
return {
|
|
965
|
+
success: false,
|
|
966
|
+
message: `Unknown: agent ${sub}. Use: research|maintain|spec-run`
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
951
970
|
case "linear-run": {
|
|
952
971
|
if (!this.linearTaskRunner) {
|
|
953
972
|
return {
|
|
@@ -983,7 +1002,15 @@ class ClaudeSkillsManager {
|
|
|
983
1002
|
}
|
|
984
1003
|
}
|
|
985
1004
|
getAvailableSkills() {
|
|
986
|
-
const skills = [
|
|
1005
|
+
const skills = [
|
|
1006
|
+
"handoff",
|
|
1007
|
+
"checkpoint",
|
|
1008
|
+
"dig",
|
|
1009
|
+
"dashboard",
|
|
1010
|
+
"api",
|
|
1011
|
+
"spec",
|
|
1012
|
+
"agent"
|
|
1013
|
+
];
|
|
987
1014
|
if (this.repoIngestionSkill) {
|
|
988
1015
|
skills.push("repo");
|
|
989
1016
|
}
|
|
@@ -1145,6 +1172,24 @@ Examples:
|
|
|
1145
1172
|
/spec dev-spec
|
|
1146
1173
|
/spec update prompt-plan "Initialize repository and tooling"
|
|
1147
1174
|
/spec validate prompt-plan
|
|
1175
|
+
`;
|
|
1176
|
+
case "agent":
|
|
1177
|
+
return `
|
|
1178
|
+
/agent research "How does the FTS5 search work?"
|
|
1179
|
+
/agent maintain "Fix the deprecation warning in webhook.ts"
|
|
1180
|
+
/agent spec-run docs/specs/my-feature.md
|
|
1181
|
+
|
|
1182
|
+
Parallel agent skill (Willison patterns) \u2014 spawn isolated Claude agents:
|
|
1183
|
+
research \u2014 Explore codebase, save findings as a frame (read-only)
|
|
1184
|
+
maintain \u2014 Low-stakes fix, produces a .patch file
|
|
1185
|
+
spec-run \u2014 Implement a spec on a branch, validate with lint+test+build
|
|
1186
|
+
|
|
1187
|
+
Options:
|
|
1188
|
+
--timeout <ms> Agent timeout in milliseconds (default: 300000)
|
|
1189
|
+
|
|
1190
|
+
Each agent runs in a disposable /tmp workspace (git clone --depth=1).
|
|
1191
|
+
Patches: git apply .stackmemory/patches/<file>.patch
|
|
1192
|
+
Spec branches: cd /tmp/sm-spec-* && git log --oneline
|
|
1148
1193
|
`;
|
|
1149
1194
|
case "linear-run":
|
|
1150
1195
|
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
|
+
};
|