@stackmemoryai/stackmemory 1.5.2 → 1.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/dist/src/cli/commands/dashboard.js +47 -1
- package/dist/src/cli/commands/orchestrate.js +82 -5
- package/dist/src/cli/commands/orchestrator.js +428 -61
- package/package.json +2 -2
- package/scripts/gepa/.before-optimize.md +89 -53
- package/scripts/gepa/generations/gen-000/baseline.md +8 -0
- package/scripts/gepa/generations/gen-001/baseline.md +8 -0
- package/scripts/gepa/generations/gen-001/variant-a.md +81 -81
- package/scripts/gepa/generations/gen-001/variant-b.md +78 -64
- package/scripts/gepa/generations/gen-001/variant-c.md +54 -74
- package/scripts/gepa/generations/gen-001/variant-d.md +79 -113
- package/scripts/gepa/results/eval-1-baseline.json +74 -74
- package/scripts/gepa/results/eval-1-variant-a.json +64 -64
- package/scripts/gepa/results/eval-1-variant-b.json +65 -65
- package/scripts/gepa/results/eval-1-variant-c.json +78 -58
- package/scripts/gepa/results/eval-1-variant-d.json +77 -57
- package/scripts/gepa/state.json +14 -14
|
@@ -3,10 +3,17 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { spawn, execSync } from "child_process";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
createWriteStream
|
|
12
|
+
} from "fs";
|
|
7
13
|
import { join, dirname } from "path";
|
|
8
|
-
import { tmpdir } from "os";
|
|
14
|
+
import { tmpdir, homedir } from "os";
|
|
9
15
|
import { fileURLToPath } from "url";
|
|
16
|
+
import { Transform } from "stream";
|
|
10
17
|
import { logger } from "../../core/monitoring/logger.js";
|
|
11
18
|
import {
|
|
12
19
|
LinearClient
|
|
@@ -26,6 +33,95 @@ function findPackageRoot() {
|
|
|
26
33
|
}
|
|
27
34
|
return dirname(currentFile);
|
|
28
35
|
}
|
|
36
|
+
function getAgentStatusDir(issueIdentifier) {
|
|
37
|
+
return join(
|
|
38
|
+
homedir(),
|
|
39
|
+
".stackmemory",
|
|
40
|
+
"conductor",
|
|
41
|
+
"agents",
|
|
42
|
+
issueIdentifier
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function ensureAgentStatusDir(issueIdentifier) {
|
|
46
|
+
const dir = getAgentStatusDir(issueIdentifier);
|
|
47
|
+
if (!existsSync(dir)) {
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
class TeeTransform extends Transform {
|
|
53
|
+
logStream;
|
|
54
|
+
constructor(logStream) {
|
|
55
|
+
super();
|
|
56
|
+
this.logStream = logStream;
|
|
57
|
+
}
|
|
58
|
+
_transform(chunk, _encoding, callback) {
|
|
59
|
+
this.logStream.write(chunk);
|
|
60
|
+
this.push(chunk);
|
|
61
|
+
callback();
|
|
62
|
+
}
|
|
63
|
+
_flush(callback) {
|
|
64
|
+
this.logStream.end();
|
|
65
|
+
callback();
|
|
66
|
+
}
|
|
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
|
+
}
|
|
97
|
+
function inferPhase(msg) {
|
|
98
|
+
const method = msg.method;
|
|
99
|
+
const params = msg.params;
|
|
100
|
+
if (method === "item/commandExecution/started") {
|
|
101
|
+
const tool = params?.tool || params?.name || "";
|
|
102
|
+
const toolLower = tool.toLowerCase();
|
|
103
|
+
if (toolLower.includes("read") || toolLower.includes("glob") || toolLower.includes("grep") || toolLower.includes("search")) {
|
|
104
|
+
return "reading";
|
|
105
|
+
}
|
|
106
|
+
if (toolLower.includes("todowrite") || toolLower.includes("todo")) {
|
|
107
|
+
return "planning";
|
|
108
|
+
}
|
|
109
|
+
if (toolLower.includes("edit") || toolLower.includes("write") || toolLower.includes("bash")) {
|
|
110
|
+
return "implementing";
|
|
111
|
+
}
|
|
112
|
+
if (toolLower.includes("test")) {
|
|
113
|
+
return "testing";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (method === "item/commandExecution/started") {
|
|
117
|
+
const args = params?.arguments;
|
|
118
|
+
const command = (args?.command ?? params?.command) || "";
|
|
119
|
+
if (command.includes("git commit") || command.includes("git add")) {
|
|
120
|
+
return "committing";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
29
125
|
const DEFAULT_CONFIG = {
|
|
30
126
|
activeStates: ["Todo"],
|
|
31
127
|
terminalStates: ["Done", "Cancelled", "Canceled", "Closed"],
|
|
@@ -44,7 +140,8 @@ const DEFAULT_CONFIG = {
|
|
|
44
140
|
),
|
|
45
141
|
turnTimeoutMs: 36e5,
|
|
46
142
|
maxRetries: 1,
|
|
47
|
-
hookTimeoutMs: 6e4
|
|
143
|
+
hookTimeoutMs: 6e4,
|
|
144
|
+
agentMode: "cli"
|
|
48
145
|
};
|
|
49
146
|
class Conductor {
|
|
50
147
|
config;
|
|
@@ -130,6 +227,7 @@ class Conductor {
|
|
|
130
227
|
console.log(
|
|
131
228
|
`Orchestrator started \u2014 polling every ${this.config.pollIntervalMs / 1e3}s, max ${this.config.maxConcurrent} concurrent`
|
|
132
229
|
);
|
|
230
|
+
this.writeStatusFile();
|
|
133
231
|
const shutdown = () => this.stop();
|
|
134
232
|
process.on("SIGINT", shutdown);
|
|
135
233
|
process.on("SIGTERM", shutdown);
|
|
@@ -175,6 +273,7 @@ class Conductor {
|
|
|
175
273
|
}
|
|
176
274
|
this.running.clear();
|
|
177
275
|
this.claimed.clear();
|
|
276
|
+
this.clearStatusFile();
|
|
178
277
|
console.log(
|
|
179
278
|
`Orchestrator stopped. Completed: ${this.completeCount}, Failed: ${this.failCount}`
|
|
180
279
|
);
|
|
@@ -198,6 +297,82 @@ class Conductor {
|
|
|
198
297
|
issues
|
|
199
298
|
};
|
|
200
299
|
}
|
|
300
|
+
// ── Status File ──
|
|
301
|
+
/**
|
|
302
|
+
* Write current conductor state to .stackmemory/conductor-status.json
|
|
303
|
+
* for consumption by `stackmemory dashboard` and other tools.
|
|
304
|
+
*/
|
|
305
|
+
writeStatusFile() {
|
|
306
|
+
const statusDir = join(this.config.repoRoot, ".stackmemory");
|
|
307
|
+
if (!existsSync(statusDir)) return;
|
|
308
|
+
const status = {
|
|
309
|
+
pid: process.pid,
|
|
310
|
+
startedAt: this.startedAt,
|
|
311
|
+
updatedAt: Date.now(),
|
|
312
|
+
running: Array.from(this.running.values()).map((r) => ({
|
|
313
|
+
identifier: r.issue.identifier,
|
|
314
|
+
title: r.issue.title,
|
|
315
|
+
status: r.status,
|
|
316
|
+
attempt: r.attempt,
|
|
317
|
+
startedAt: r.startedAt,
|
|
318
|
+
runtime: Date.now() - r.startedAt
|
|
319
|
+
})),
|
|
320
|
+
queued: Array.from(this.claimed).filter(
|
|
321
|
+
(id) => !this.running.has(id) && !this.completed.has(id)
|
|
322
|
+
).length,
|
|
323
|
+
completed: this.completeCount,
|
|
324
|
+
failed: this.failCount,
|
|
325
|
+
totalAttempts: this.totalAttempts,
|
|
326
|
+
maxConcurrent: this.config.maxConcurrent,
|
|
327
|
+
stopping: this.stopping
|
|
328
|
+
};
|
|
329
|
+
try {
|
|
330
|
+
writeFileSync(
|
|
331
|
+
join(statusDir, "conductor-status.json"),
|
|
332
|
+
JSON.stringify(status, null, 2)
|
|
333
|
+
);
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
clearStatusFile() {
|
|
338
|
+
const statusPath = join(
|
|
339
|
+
this.config.repoRoot,
|
|
340
|
+
".stackmemory",
|
|
341
|
+
"conductor-status.json"
|
|
342
|
+
);
|
|
343
|
+
try {
|
|
344
|
+
if (existsSync(statusPath)) rmSync(statusPath);
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// ── Agent Status Files ──
|
|
349
|
+
/**
|
|
350
|
+
* Write per-agent status to ~/.stackmemory/conductor/agents/<issue-id>/status.json
|
|
351
|
+
*/
|
|
352
|
+
writeAgentStatus(issueIdentifier, run) {
|
|
353
|
+
try {
|
|
354
|
+
const dir = ensureAgentStatusDir(issueIdentifier);
|
|
355
|
+
const status = {
|
|
356
|
+
issue: issueIdentifier,
|
|
357
|
+
pid: run.process?.pid || process.pid,
|
|
358
|
+
started: new Date(run.startedAt).toISOString(),
|
|
359
|
+
lastUpdate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
360
|
+
phase: run.phase,
|
|
361
|
+
filesModified: run.filesModified,
|
|
362
|
+
toolCalls: run.toolCalls,
|
|
363
|
+
tokensUsed: run.tokensUsed
|
|
364
|
+
};
|
|
365
|
+
writeFileSync(join(dir, "status.json"), JSON.stringify(status, null, 2));
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Open a log file write stream for an agent.
|
|
371
|
+
*/
|
|
372
|
+
openAgentLogStream(issueIdentifier) {
|
|
373
|
+
const dir = ensureAgentStatusDir(issueIdentifier);
|
|
374
|
+
return createWriteStream(join(dir, "output.log"), { flags: "a" });
|
|
375
|
+
}
|
|
201
376
|
// ── Polling ──
|
|
202
377
|
async schedulePoll() {
|
|
203
378
|
while (!this.stopping) {
|
|
@@ -210,6 +385,7 @@ class Conductor {
|
|
|
210
385
|
} catch (err) {
|
|
211
386
|
logger.error("Poll cycle failed", { error: err.message });
|
|
212
387
|
}
|
|
388
|
+
this.writeStatusFile();
|
|
213
389
|
}
|
|
214
390
|
}
|
|
215
391
|
async poll() {
|
|
@@ -337,8 +513,13 @@ class Conductor {
|
|
|
337
513
|
process: null,
|
|
338
514
|
attempt: 1,
|
|
339
515
|
startedAt: Date.now(),
|
|
340
|
-
status: "starting"
|
|
516
|
+
status: "starting",
|
|
517
|
+
phase: "reading",
|
|
518
|
+
toolCalls: 0,
|
|
519
|
+
filesModified: 0,
|
|
520
|
+
tokensUsed: 0
|
|
341
521
|
};
|
|
522
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
342
523
|
this.running.set(issueId, run);
|
|
343
524
|
this.totalAttempts++;
|
|
344
525
|
console.log(`[${issue.identifier}] Dispatching: ${issue.title}`);
|
|
@@ -347,23 +528,26 @@ class Conductor {
|
|
|
347
528
|
run.workspacePath = workspacePath;
|
|
348
529
|
await this.transitionIssue(issue, this.config.inProgressState);
|
|
349
530
|
await this.runHook("after-create", workspacePath, issue);
|
|
350
|
-
|
|
351
|
-
await this.runAgent(issue, run);
|
|
352
|
-
run.status = "completed";
|
|
353
|
-
this.completeCount++;
|
|
354
|
-
await this.runHook("after-run", workspacePath, issue, run.attempt);
|
|
355
|
-
this.takeSnapshot(workspacePath, issue);
|
|
356
|
-
await this.transitionIssue(issue, this.config.inReviewState);
|
|
357
|
-
console.log(`[${issue.identifier}] Completed successfully`);
|
|
531
|
+
await this.attemptRun(issue, run);
|
|
358
532
|
} catch (err) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
533
|
+
this.failCount++;
|
|
534
|
+
console.log(`[${issue.identifier}] Failed: ${err.message}`);
|
|
535
|
+
} finally {
|
|
536
|
+
this.running.delete(issueId);
|
|
537
|
+
this.writeStatusFile();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Run the agent with retry logic. Throws on final failure.
|
|
542
|
+
*/
|
|
543
|
+
async attemptRun(issue, run) {
|
|
544
|
+
const maxAttempts = this.config.maxRetries + 1;
|
|
545
|
+
while (run.attempt <= maxAttempts) {
|
|
546
|
+
try {
|
|
547
|
+
run.status = "running";
|
|
548
|
+
await this.runAgent(issue, run);
|
|
549
|
+
run.status = "completed";
|
|
550
|
+
this.completeCount++;
|
|
367
551
|
await this.runHook(
|
|
368
552
|
"after-run",
|
|
369
553
|
run.workspacePath,
|
|
@@ -371,47 +555,44 @@ class Conductor {
|
|
|
371
555
|
run.attempt
|
|
372
556
|
).catch(() => {
|
|
373
557
|
});
|
|
374
|
-
|
|
375
|
-
|
|
558
|
+
this.takeSnapshot(run.workspacePath, issue);
|
|
559
|
+
await this.transitionIssue(issue, this.config.inReviewState);
|
|
376
560
|
console.log(
|
|
377
|
-
`[${issue.identifier}]
|
|
561
|
+
run.attempt === 1 ? `[${issue.identifier}] Completed successfully` : `[${issue.identifier}] Completed on retry ${run.attempt}`
|
|
378
562
|
);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
563
|
+
return;
|
|
564
|
+
} catch (err) {
|
|
565
|
+
run.status = "failed";
|
|
566
|
+
run.error = err.message;
|
|
567
|
+
logger.error("Agent run failed", {
|
|
568
|
+
identifier: issue.identifier,
|
|
569
|
+
error: run.error,
|
|
570
|
+
attempt: run.attempt
|
|
571
|
+
});
|
|
572
|
+
if (run.workspacePath) {
|
|
573
|
+
await this.runHook(
|
|
574
|
+
"after-run",
|
|
575
|
+
run.workspacePath,
|
|
576
|
+
issue,
|
|
577
|
+
run.attempt
|
|
578
|
+
).catch(() => {
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (run.attempt < maxAttempts && !this.stopping) {
|
|
582
|
+
console.log(
|
|
583
|
+
`[${issue.identifier}] Failed (attempt ${run.attempt}), retrying...`
|
|
584
|
+
);
|
|
585
|
+
run.attempt++;
|
|
586
|
+
this.totalAttempts++;
|
|
587
|
+
const backoffMs = Math.min(
|
|
588
|
+
1e3 * Math.pow(2, run.attempt - 1),
|
|
589
|
+
3e5
|
|
590
|
+
);
|
|
591
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
592
|
+
} else {
|
|
593
|
+
throw err;
|
|
408
594
|
}
|
|
409
|
-
} else {
|
|
410
|
-
this.failCount++;
|
|
411
|
-
console.log(`[${issue.identifier}] Failed: ${run.error}`);
|
|
412
595
|
}
|
|
413
|
-
} finally {
|
|
414
|
-
this.running.delete(issueId);
|
|
415
596
|
}
|
|
416
597
|
}
|
|
417
598
|
// ── Workspace Management ──
|
|
@@ -492,13 +673,167 @@ class Conductor {
|
|
|
492
673
|
return identifier.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
493
674
|
}
|
|
494
675
|
// ── Agent Execution ──
|
|
495
|
-
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) {
|
|
496
703
|
return new Promise((resolve, reject) => {
|
|
497
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) {
|
|
828
|
+
return new Promise((resolve, reject) => {
|
|
829
|
+
const prompt = this.buildPrompt(issue, run.attempt);
|
|
830
|
+
const env = { ...process.env };
|
|
831
|
+
delete env.CLAUDECODE;
|
|
832
|
+
delete env.ANTHROPIC_API_KEY;
|
|
498
833
|
const proc = spawn("node", [this.config.appServerPath], {
|
|
499
834
|
cwd: run.workspacePath,
|
|
500
835
|
env: {
|
|
501
|
-
...
|
|
836
|
+
...env,
|
|
502
837
|
SYMPHONY_WORKSPACE_DIR: run.workspacePath,
|
|
503
838
|
SYMPHONY_ISSUE_ID: issue.id,
|
|
504
839
|
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
@@ -507,6 +842,11 @@ class Conductor {
|
|
|
507
842
|
stdio: ["pipe", "pipe", "pipe"]
|
|
508
843
|
});
|
|
509
844
|
run.process = proc;
|
|
845
|
+
const logStream = this.openAgentLogStream(issue.identifier);
|
|
846
|
+
run.logStream = logStream;
|
|
847
|
+
const tee = new TeeTransform(logStream);
|
|
848
|
+
proc.stdout.pipe(tee);
|
|
849
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
510
850
|
let stderr = "";
|
|
511
851
|
let turnCompleted = false;
|
|
512
852
|
const timer = setTimeout(() => {
|
|
@@ -525,7 +865,7 @@ class Conductor {
|
|
|
525
865
|
proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
526
866
|
};
|
|
527
867
|
let lineBuffer = "";
|
|
528
|
-
|
|
868
|
+
tee.on("data", (chunk) => {
|
|
529
869
|
lineBuffer += chunk.toString();
|
|
530
870
|
const lines = lineBuffer.split("\n");
|
|
531
871
|
lineBuffer = lines.pop() || "";
|
|
@@ -534,11 +874,33 @@ class Conductor {
|
|
|
534
874
|
try {
|
|
535
875
|
const msg = JSON.parse(line);
|
|
536
876
|
this.handleAgentMessage(msg, issue, run);
|
|
877
|
+
const phase = inferPhase(msg);
|
|
878
|
+
if (phase) {
|
|
879
|
+
run.phase = phase;
|
|
880
|
+
}
|
|
881
|
+
if (msg.method === "item/commandExecution/started" || msg.method === "item/toolUse/started") {
|
|
882
|
+
run.toolCalls++;
|
|
883
|
+
}
|
|
884
|
+
const params = msg.params;
|
|
885
|
+
if (msg.method === "item/commandExecution/started" && params) {
|
|
886
|
+
const tool = (params.tool || params.name || "").toLowerCase();
|
|
887
|
+
if (tool.includes("edit") || tool.includes("write")) {
|
|
888
|
+
run.filesModified++;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (msg.method === "item/text" && params?.text) {
|
|
892
|
+
run.tokensUsed += Math.ceil(params.text.length / 4);
|
|
893
|
+
}
|
|
894
|
+
if (run.toolCalls % 5 === 0 || phase) {
|
|
895
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
896
|
+
}
|
|
537
897
|
if (msg.method === "turn/completed") {
|
|
538
898
|
turnCompleted = true;
|
|
899
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
539
900
|
}
|
|
540
901
|
if (msg.method === "turn/failed") {
|
|
541
902
|
turnCompleted = true;
|
|
903
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
542
904
|
const errMsg = msg.params?.error?.message || "Agent turn failed";
|
|
543
905
|
clearTimeout(timer);
|
|
544
906
|
reject(new Error(errMsg));
|
|
@@ -562,6 +924,10 @@ class Conductor {
|
|
|
562
924
|
proc.on("close", (code) => {
|
|
563
925
|
clearTimeout(timer);
|
|
564
926
|
run.process = null;
|
|
927
|
+
if (run.logStream && !run.logStream.destroyed) {
|
|
928
|
+
run.logStream.end();
|
|
929
|
+
}
|
|
930
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
565
931
|
if (turnCompleted) {
|
|
566
932
|
resolve();
|
|
567
933
|
} else if (code === 0) {
|
|
@@ -800,5 +1166,6 @@ class Conductor {
|
|
|
800
1166
|
}
|
|
801
1167
|
}
|
|
802
1168
|
export {
|
|
803
|
-
Conductor
|
|
1169
|
+
Conductor,
|
|
1170
|
+
getAgentStatusDir
|
|
804
1171
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.5.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",
|
|
7
7
|
"npm": ">=10.0.0"
|