@stackmemoryai/stackmemory 1.3.0 → 1.3.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/commands/symphony-orchestrator.js +676 -0
- package/dist/src/cli/commands/symphony.js +30 -1
- package/dist/src/core/database/sqlite-adapter.js +152 -8
- package/dist/src/integrations/claude-code/subagent-client.js +97 -161
- package/dist/src/integrations/claude-code/task-coordinator.js +69 -21
- package/dist/src/integrations/mcp/handlers/linear-handlers.js +62 -48
- package/package.json +1 -1
|
@@ -5,9 +5,10 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
|
-
import { homedir } from "os";
|
|
8
|
+
import { homedir, tmpdir } from "os";
|
|
9
9
|
import Database from "better-sqlite3";
|
|
10
10
|
import { logger } from "../../core/monitoring/logger.js";
|
|
11
|
+
import { SymphonyOrchestrator } from "./symphony-orchestrator.js";
|
|
11
12
|
function getGlobalStorePath() {
|
|
12
13
|
const dir = join(homedir(), ".stackmemory", "symphony");
|
|
13
14
|
if (!existsSync(dir)) {
|
|
@@ -260,6 +261,34 @@ function createSymphonyCommands() {
|
|
|
260
261
|
}
|
|
261
262
|
globalDb.close();
|
|
262
263
|
});
|
|
264
|
+
cmd.command("start").description("Start the Symphony orchestrator daemon").option("--team <id>", "Linear team ID").option(
|
|
265
|
+
"--states <states>",
|
|
266
|
+
"Comma-separated issue states to pick up",
|
|
267
|
+
"Todo"
|
|
268
|
+
).option(
|
|
269
|
+
"--in-progress <state>",
|
|
270
|
+
"State name for in-progress",
|
|
271
|
+
"In Progress"
|
|
272
|
+
).option(
|
|
273
|
+
"--in-review <state>",
|
|
274
|
+
"State name for completed review",
|
|
275
|
+
"In Review"
|
|
276
|
+
).option("--poll <ms>", "Polling interval in milliseconds", "30000").option("--concurrency <n>", "Max concurrent agents", "3").option("--workspace-root <path>", "Workspace root directory").option("--repo <path>", "Git repo root for worktrees", process.cwd()).option("--branch <name>", "Base branch for worktrees", "main").option("--retries <n>", "Max retries per issue", "1").option("--turn-timeout <ms>", "Agent turn timeout in ms", "3600000").action(async (options) => {
|
|
277
|
+
const orchestrator = new SymphonyOrchestrator({
|
|
278
|
+
teamId: options.team,
|
|
279
|
+
activeStates: options.states.split(",").map((s) => s.trim()),
|
|
280
|
+
inProgressState: options.inProgress,
|
|
281
|
+
inReviewState: options.inReview,
|
|
282
|
+
pollIntervalMs: parseInt(options.poll, 10),
|
|
283
|
+
maxConcurrent: parseInt(options.concurrency, 10),
|
|
284
|
+
workspaceRoot: options.workspaceRoot || join(tmpdir(), "symphony_workspaces"),
|
|
285
|
+
repoRoot: options.repo,
|
|
286
|
+
baseBranch: options.branch,
|
|
287
|
+
maxRetries: parseInt(options.retries, 10),
|
|
288
|
+
turnTimeoutMs: parseInt(options.turnTimeout, 10)
|
|
289
|
+
});
|
|
290
|
+
await orchestrator.start();
|
|
291
|
+
});
|
|
263
292
|
return cmd;
|
|
264
293
|
}
|
|
265
294
|
export {
|
|
@@ -292,11 +292,16 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
292
292
|
logger.info("FTS5 index rebuilt");
|
|
293
293
|
}
|
|
294
294
|
/**
|
|
295
|
-
* Incremental garbage collection
|
|
296
|
-
*
|
|
295
|
+
* Incremental garbage collection with generational compression.
|
|
296
|
+
*
|
|
297
|
+
* Phase 1 — Compress: Apply generational strategies to mature/old frames.
|
|
298
|
+
* - 'digest_only': Strip inputs/outputs/events, keep digest + anchors
|
|
299
|
+
* - 'anchors_only': Strip inputs/outputs/events/digest_json, keep digest_text + anchors
|
|
300
|
+
* - 'keep_all': No compression
|
|
301
|
+
*
|
|
302
|
+
* Phase 2 — Delete: Remove frames past retention cutoffs.
|
|
297
303
|
* - 'keep_forever': never deleted
|
|
298
|
-
* - 'default': deleted after retentionDays (default 90)
|
|
299
|
-
* - 'archive': same as default
|
|
304
|
+
* - 'default'/'archive': deleted after retentionDays (default 90)
|
|
300
305
|
* - 'ttl_30d': deleted after 30 days
|
|
301
306
|
* - 'ttl_7d': deleted after 7 days
|
|
302
307
|
*/
|
|
@@ -314,6 +319,62 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
314
319
|
const defaultCutoff = nowSec - retentionDays * 86400;
|
|
315
320
|
const ttl30dCutoff = nowSec - 30 * 86400;
|
|
316
321
|
const ttl7dCutoff = nowSec - 7 * 86400;
|
|
322
|
+
let framesCompressed = 0;
|
|
323
|
+
const gc = options.generationalGc;
|
|
324
|
+
if (gc && !dryRun) {
|
|
325
|
+
const youngDays = gc.youngCutoffDays ?? 1;
|
|
326
|
+
const matureDays = gc.matureCutoffDays ?? 7;
|
|
327
|
+
const oldDays = gc.oldCutoffDays ?? 30;
|
|
328
|
+
const youngCutoff = nowSec - youngDays * 86400;
|
|
329
|
+
const matureCutoff = nowSec - matureDays * 86400;
|
|
330
|
+
const matureStrategy = gc.mature_strategy ?? "digest_only";
|
|
331
|
+
if (matureStrategy !== "keep_all") {
|
|
332
|
+
const matureFrames = this.db.prepare(
|
|
333
|
+
`SELECT frame_id FROM frames
|
|
334
|
+
WHERE created_at < ? AND created_at >= ?
|
|
335
|
+
AND state = 'closed'
|
|
336
|
+
AND retention_policy != 'keep_forever'
|
|
337
|
+
AND inputs != '{}'
|
|
338
|
+
AND run_id NOT IN (SELECT value FROM json_each(?))
|
|
339
|
+
LIMIT ?`
|
|
340
|
+
).all(
|
|
341
|
+
youngCutoff,
|
|
342
|
+
matureCutoff,
|
|
343
|
+
JSON.stringify(protectedRunIds),
|
|
344
|
+
batchSize
|
|
345
|
+
);
|
|
346
|
+
if (matureFrames.length > 0) {
|
|
347
|
+
framesCompressed += this.compressFrames(
|
|
348
|
+
matureFrames.map((f) => f.frame_id),
|
|
349
|
+
matureStrategy
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const oldStrategy = gc.old_strategy ?? "anchors_only";
|
|
354
|
+
if (oldStrategy !== "keep_all") {
|
|
355
|
+
const oldCutoff = nowSec - oldDays * 86400;
|
|
356
|
+
const oldFrames = this.db.prepare(
|
|
357
|
+
`SELECT frame_id FROM frames
|
|
358
|
+
WHERE created_at < ? AND created_at >= ?
|
|
359
|
+
AND state = 'closed'
|
|
360
|
+
AND retention_policy != 'keep_forever'
|
|
361
|
+
AND inputs != '{}'
|
|
362
|
+
AND run_id NOT IN (SELECT value FROM json_each(?))
|
|
363
|
+
LIMIT ?`
|
|
364
|
+
).all(
|
|
365
|
+
matureCutoff,
|
|
366
|
+
oldCutoff,
|
|
367
|
+
JSON.stringify(protectedRunIds),
|
|
368
|
+
batchSize
|
|
369
|
+
);
|
|
370
|
+
if (oldFrames.length > 0) {
|
|
371
|
+
framesCompressed += this.compressFrames(
|
|
372
|
+
oldFrames.map((f) => f.frame_id),
|
|
373
|
+
oldStrategy
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
317
378
|
const candidates = this.db.prepare(
|
|
318
379
|
`SELECT frame_id FROM frames
|
|
319
380
|
WHERE (
|
|
@@ -340,7 +401,8 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
340
401
|
eventsDeleted: 0,
|
|
341
402
|
anchorsDeleted: 0,
|
|
342
403
|
embeddingsDeleted: 0,
|
|
343
|
-
ftsEntriesDeleted: 0
|
|
404
|
+
ftsEntriesDeleted: 0,
|
|
405
|
+
framesCompressed
|
|
344
406
|
};
|
|
345
407
|
}
|
|
346
408
|
if (dryRun) {
|
|
@@ -362,8 +424,9 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
362
424
|
eventsDeleted: eventsCount,
|
|
363
425
|
anchorsDeleted: anchorsCount,
|
|
364
426
|
embeddingsDeleted: embeddingsCount,
|
|
365
|
-
ftsEntriesDeleted: frameIds.length
|
|
427
|
+
ftsEntriesDeleted: frameIds.length,
|
|
366
428
|
// FTS has one entry per frame
|
|
429
|
+
framesCompressed
|
|
367
430
|
};
|
|
368
431
|
}
|
|
369
432
|
const placeholders = frameIds.map(() => "?").join(",");
|
|
@@ -392,14 +455,16 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
392
455
|
framesDeleted: frameIds.length,
|
|
393
456
|
eventsDeleted,
|
|
394
457
|
anchorsDeleted,
|
|
395
|
-
embeddingsDeleted
|
|
458
|
+
embeddingsDeleted,
|
|
459
|
+
framesCompressed
|
|
396
460
|
});
|
|
397
461
|
return {
|
|
398
462
|
framesDeleted: frameIds.length,
|
|
399
463
|
eventsDeleted,
|
|
400
464
|
anchorsDeleted,
|
|
401
465
|
embeddingsDeleted,
|
|
402
|
-
ftsEntriesDeleted: frameIds.length
|
|
466
|
+
ftsEntriesDeleted: frameIds.length,
|
|
467
|
+
framesCompressed
|
|
403
468
|
};
|
|
404
469
|
}
|
|
405
470
|
/**
|
|
@@ -432,6 +497,85 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
432
497
|
if (nowSec - frame.created_at < 86400) score += 0.1;
|
|
433
498
|
return Math.round(Math.min(score, 1) * 100) / 100;
|
|
434
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Compress a frame by stripping data according to strategy.
|
|
502
|
+
*
|
|
503
|
+
* - 'digest_only': Remove inputs, outputs, events. Keep digest_text, digest_json, anchors.
|
|
504
|
+
* - 'anchors_only': Remove inputs, outputs, events, digest_json. Keep digest_text, anchors.
|
|
505
|
+
*
|
|
506
|
+
* Returns true if the frame was compressed, false if not found.
|
|
507
|
+
*/
|
|
508
|
+
compressFrame(frameId, strategy) {
|
|
509
|
+
if (!this.db)
|
|
510
|
+
throw new DatabaseError(
|
|
511
|
+
"Database not connected",
|
|
512
|
+
ErrorCode.DB_CONNECTION_FAILED
|
|
513
|
+
);
|
|
514
|
+
const frame = this.db.prepare("SELECT frame_id FROM frames WHERE frame_id = ?").get(frameId);
|
|
515
|
+
if (!frame) return false;
|
|
516
|
+
this.db.prepare("BEGIN").run();
|
|
517
|
+
try {
|
|
518
|
+
if (strategy === "digest_only") {
|
|
519
|
+
this.db.prepare(
|
|
520
|
+
"UPDATE frames SET inputs = '{}', outputs = '{}' WHERE frame_id = ?"
|
|
521
|
+
).run(frameId);
|
|
522
|
+
} else {
|
|
523
|
+
this.db.prepare(
|
|
524
|
+
"UPDATE frames SET inputs = '{}', outputs = '{}', digest_json = '{}' WHERE frame_id = ?"
|
|
525
|
+
).run(frameId);
|
|
526
|
+
}
|
|
527
|
+
this.db.prepare("DELETE FROM events WHERE frame_id = ?").run(frameId);
|
|
528
|
+
if (this.vecEnabled) {
|
|
529
|
+
this.db.prepare("DELETE FROM frame_embeddings WHERE frame_id = ?").run(frameId);
|
|
530
|
+
}
|
|
531
|
+
this.db.prepare("COMMIT").run();
|
|
532
|
+
return true;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
this.db.prepare("ROLLBACK").run();
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Compress multiple frames in a single transaction.
|
|
540
|
+
* Returns count of frames compressed.
|
|
541
|
+
*/
|
|
542
|
+
compressFrames(frameIds, strategy) {
|
|
543
|
+
if (!this.db || frameIds.length === 0) return 0;
|
|
544
|
+
const placeholders = frameIds.map(() => "?").join(",");
|
|
545
|
+
this.db.prepare("BEGIN").run();
|
|
546
|
+
try {
|
|
547
|
+
if (strategy === "digest_only") {
|
|
548
|
+
this.db.prepare(
|
|
549
|
+
`UPDATE frames SET inputs = '{}', outputs = '{}' WHERE frame_id IN (${placeholders})`
|
|
550
|
+
).run(...frameIds);
|
|
551
|
+
} else {
|
|
552
|
+
this.db.prepare(
|
|
553
|
+
`UPDATE frames SET inputs = '{}', outputs = '{}', digest_json = '{}' WHERE frame_id IN (${placeholders})`
|
|
554
|
+
).run(...frameIds);
|
|
555
|
+
}
|
|
556
|
+
this.db.prepare(`DELETE FROM events WHERE frame_id IN (${placeholders})`).run(...frameIds);
|
|
557
|
+
if (this.vecEnabled) {
|
|
558
|
+
this.db.prepare(
|
|
559
|
+
`DELETE FROM frame_embeddings WHERE frame_id IN (${placeholders})`
|
|
560
|
+
).run(...frameIds);
|
|
561
|
+
}
|
|
562
|
+
this.db.prepare("COMMIT").run();
|
|
563
|
+
return frameIds.length;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
this.db.prepare("ROLLBACK").run();
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get database file size in bytes.
|
|
571
|
+
*/
|
|
572
|
+
getDatabaseSize() {
|
|
573
|
+
if (!this.db) return 0;
|
|
574
|
+
const result = this.db.prepare(
|
|
575
|
+
"SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"
|
|
576
|
+
).get();
|
|
577
|
+
return result?.size ?? 0;
|
|
578
|
+
}
|
|
435
579
|
/**
|
|
436
580
|
* Recompute importance scores for frames still at default score (0.5).
|
|
437
581
|
* Processes oldest frames first in batches.
|
|
@@ -4,8 +4,7 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { logger } from "../../core/monitoring/logger.js";
|
|
6
6
|
import { STRUCTURED_RESPONSE_SUFFIX } from "../../orchestrators/multimodal/constants.js";
|
|
7
|
-
import {
|
|
8
|
-
import { promisify } from "util";
|
|
7
|
+
import { spawn } from "child_process";
|
|
9
8
|
import * as fs from "fs";
|
|
10
9
|
import * as path from "path";
|
|
11
10
|
import * as os from "os";
|
|
@@ -18,12 +17,10 @@ import {
|
|
|
18
17
|
createProvider
|
|
19
18
|
} from "../../core/extensions/provider-adapter.js";
|
|
20
19
|
import { AnthropicBatchClient } from "../anthropic/batch-client.js";
|
|
21
|
-
const execAsync = promisify(exec);
|
|
22
20
|
class ClaudeCodeSubagentClient {
|
|
23
21
|
tempDir;
|
|
24
|
-
activeSubagents = /* @__PURE__ */ new Map();
|
|
25
22
|
mockMode;
|
|
26
|
-
constructor(mockMode =
|
|
23
|
+
constructor(mockMode = false) {
|
|
27
24
|
this.mockMode = mockMode;
|
|
28
25
|
this.tempDir = path.join(os.tmpdir(), "stackmemory-rlm");
|
|
29
26
|
if (!fs.existsSync(this.tempDir)) {
|
|
@@ -148,7 +145,8 @@ class ClaudeCodeSubagentClient {
|
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
/**
|
|
151
|
-
*
|
|
148
|
+
* Execute subagent via Claude Code CLI (`claude -p --output-format stream-json`).
|
|
149
|
+
* Spawns a real Claude Code process with full tool use.
|
|
152
150
|
*/
|
|
153
151
|
async executeSubagentViaCLI(request, startTime, subagentId) {
|
|
154
152
|
try {
|
|
@@ -158,31 +156,24 @@ class ClaudeCodeSubagentClient {
|
|
|
158
156
|
contextFile,
|
|
159
157
|
JSON.stringify(request.context, null, 2)
|
|
160
158
|
);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
contextFile,
|
|
166
|
-
resultFile
|
|
167
|
-
);
|
|
168
|
-
const result = await this.executeTaskTool(taskCommand, request.timeout);
|
|
169
|
-
let subagentResult = {};
|
|
170
|
-
if (fs.existsSync(resultFile)) {
|
|
171
|
-
const resultContent = await fs.promises.readFile(resultFile, "utf-8");
|
|
172
|
-
try {
|
|
173
|
-
subagentResult = JSON.parse(resultContent);
|
|
174
|
-
} catch {
|
|
175
|
-
subagentResult = { rawOutput: resultContent };
|
|
176
|
-
}
|
|
177
|
-
}
|
|
159
|
+
const fullPrompt = `${prompt}
|
|
160
|
+
|
|
161
|
+
Context (JSON): ${JSON.stringify(request.context)}`;
|
|
162
|
+
const result = await this.spawnClaude(fullPrompt, request.timeout);
|
|
178
163
|
this.cleanup(subagentId);
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(result.text);
|
|
167
|
+
} catch {
|
|
168
|
+
parsed = { rawOutput: result.text };
|
|
169
|
+
}
|
|
179
170
|
return {
|
|
180
171
|
success: true,
|
|
181
|
-
result:
|
|
182
|
-
output: result.
|
|
172
|
+
result: parsed,
|
|
173
|
+
output: result.text,
|
|
183
174
|
duration: Date.now() - startTime,
|
|
184
175
|
subagentType: request.type,
|
|
185
|
-
tokens: this.estimateTokens(
|
|
176
|
+
tokens: this.estimateTokens(fullPrompt + result.text)
|
|
186
177
|
};
|
|
187
178
|
} catch (error) {
|
|
188
179
|
logger.error(`Subagent CLI execution failed: ${request.type}`, {
|
|
@@ -347,66 +338,88 @@ class ClaudeCodeSubagentClient {
|
|
|
347
338
|
return (request.systemPrompt || prompts[request.type] || prompts.planning) + STRUCTURED_RESPONSE_SUFFIX;
|
|
348
339
|
}
|
|
349
340
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*/
|
|
353
|
-
buildTaskCommand(request, prompt, contextFile, resultFile) {
|
|
354
|
-
const scriptContent = `
|
|
355
|
-
#!/bin/bash
|
|
356
|
-
# Subagent execution script for ${request.type}
|
|
357
|
-
|
|
358
|
-
# Read context
|
|
359
|
-
CONTEXT=$(cat "${contextFile}")
|
|
360
|
-
|
|
361
|
-
# Execute task based on type
|
|
362
|
-
case "${request.type}" in
|
|
363
|
-
"testing")
|
|
364
|
-
# For testing subagent, actually run tests
|
|
365
|
-
echo "Generating and running tests..."
|
|
366
|
-
# The subagent will generate test files and run them
|
|
367
|
-
;;
|
|
368
|
-
"linting")
|
|
369
|
-
# For linting subagent, run actual linters
|
|
370
|
-
echo "Running linters..."
|
|
371
|
-
npm run lint || true
|
|
372
|
-
;;
|
|
373
|
-
"code")
|
|
374
|
-
# For code generation, create implementation files
|
|
375
|
-
echo "Generating implementation..."
|
|
376
|
-
;;
|
|
377
|
-
*)
|
|
378
|
-
# Default behavior
|
|
379
|
-
echo "Executing ${request.type} task..."
|
|
380
|
-
;;
|
|
381
|
-
esac
|
|
382
|
-
|
|
383
|
-
# Write result
|
|
384
|
-
echo '{"status": "completed", "type": "${request.type}"}' > "${resultFile}"
|
|
385
|
-
`;
|
|
386
|
-
const scriptFile = path.join(this.tempDir, `${request.type}-script.sh`);
|
|
387
|
-
fs.writeFileSync(scriptFile, scriptContent);
|
|
388
|
-
fs.chmodSync(scriptFile, "755");
|
|
389
|
-
return scriptFile;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Execute via Task tool (simulated for now)
|
|
393
|
-
* In production, this would use Claude Code's actual Task tool API
|
|
341
|
+
* Spawn `claude -p --output-format stream-json` and collect the result.
|
|
342
|
+
* Parses stream-json events to extract the final assistant text.
|
|
394
343
|
*/
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
344
|
+
spawnClaude(prompt, timeout) {
|
|
345
|
+
return new Promise((resolve, reject) => {
|
|
346
|
+
const args = ["-p", "--output-format", "stream-json", prompt];
|
|
347
|
+
const claude = spawn("claude", args, {
|
|
348
|
+
cwd: process.cwd(),
|
|
349
|
+
env: { ...process.env },
|
|
350
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
402
351
|
});
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
352
|
+
const timeoutMs = timeout || 3e5;
|
|
353
|
+
const timer = setTimeout(() => {
|
|
354
|
+
claude.kill("SIGTERM");
|
|
355
|
+
reject(new Error(`Subagent timeout after ${timeoutMs}ms`));
|
|
356
|
+
}, timeoutMs);
|
|
357
|
+
let lastAssistantText = "";
|
|
358
|
+
let toolUseCount = 0;
|
|
359
|
+
let lineBuffer = "";
|
|
360
|
+
let stderr = "";
|
|
361
|
+
claude.stdout.on("data", (chunk) => {
|
|
362
|
+
lineBuffer += chunk.toString();
|
|
363
|
+
const lines = lineBuffer.split("\n");
|
|
364
|
+
lineBuffer = lines.pop() || "";
|
|
365
|
+
for (const line of lines) {
|
|
366
|
+
if (!line.trim()) continue;
|
|
367
|
+
try {
|
|
368
|
+
const event = JSON.parse(line);
|
|
369
|
+
if (event.type === "assistant" && event.message) {
|
|
370
|
+
const textBlocks = (event.message.content || []).filter((b) => b.type === "text").map((b) => b.text);
|
|
371
|
+
if (textBlocks.length > 0) {
|
|
372
|
+
lastAssistantText = textBlocks.join("\n");
|
|
373
|
+
}
|
|
374
|
+
const toolBlocks = (event.message.content || []).filter(
|
|
375
|
+
(b) => b.type === "tool_use"
|
|
376
|
+
);
|
|
377
|
+
toolUseCount += toolBlocks.length;
|
|
378
|
+
}
|
|
379
|
+
if (event.type === "result" && event.result) {
|
|
380
|
+
lastAssistantText = event.result;
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
claude.stderr.on("data", (data) => {
|
|
387
|
+
stderr += data.toString();
|
|
388
|
+
});
|
|
389
|
+
claude.on("close", (code) => {
|
|
390
|
+
clearTimeout(timer);
|
|
391
|
+
if (lineBuffer.trim()) {
|
|
392
|
+
try {
|
|
393
|
+
const event = JSON.parse(lineBuffer);
|
|
394
|
+
if (event.type === "result" && event.result) {
|
|
395
|
+
lastAssistantText = event.result;
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
logger.info("Claude subagent completed", {
|
|
401
|
+
code,
|
|
402
|
+
toolUseCount,
|
|
403
|
+
outputLength: lastAssistantText.length
|
|
404
|
+
});
|
|
405
|
+
if (code === 0 && lastAssistantText) {
|
|
406
|
+
resolve({ text: lastAssistantText, toolUseCount });
|
|
407
|
+
} else if (code === 0) {
|
|
408
|
+
resolve({
|
|
409
|
+
text: "(Claude completed but produced no text output)",
|
|
410
|
+
toolUseCount
|
|
411
|
+
});
|
|
412
|
+
} else {
|
|
413
|
+
reject(
|
|
414
|
+
new Error(`Claude exited code ${code}: ${stderr.slice(0, 500)}`)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
claude.on("error", (err) => {
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
421
|
+
});
|
|
422
|
+
});
|
|
410
423
|
}
|
|
411
424
|
/**
|
|
412
425
|
* Get mock response for testing
|
|
@@ -528,84 +541,11 @@ function greetUser(name: string): string {
|
|
|
528
541
|
}
|
|
529
542
|
}
|
|
530
543
|
}
|
|
531
|
-
/**
|
|
532
|
-
* Create a mock Task tool response for development
|
|
533
|
-
* This simulates what Claude Code's Task tool would return
|
|
534
|
-
*/
|
|
535
|
-
async mockTaskToolExecution(request) {
|
|
536
|
-
const startTime = Date.now();
|
|
537
|
-
await new Promise(
|
|
538
|
-
(resolve) => setTimeout(resolve, 1e3 + Math.random() * 2e3)
|
|
539
|
-
);
|
|
540
|
-
const mockResponses = {
|
|
541
|
-
planning: {
|
|
542
|
-
tasks: [
|
|
543
|
-
{ id: "1", type: "analyze", description: "Analyze requirements" },
|
|
544
|
-
{ id: "2", type: "implement", description: "Implement solution" },
|
|
545
|
-
{ id: "3", type: "test", description: "Test implementation" },
|
|
546
|
-
{ id: "4", type: "review", description: "Review and improve" }
|
|
547
|
-
],
|
|
548
|
-
dependencies: { "2": ["1"], "3": ["2"], "4": ["3"] }
|
|
549
|
-
},
|
|
550
|
-
code: {
|
|
551
|
-
implementation: `
|
|
552
|
-
export class Solution {
|
|
553
|
-
constructor(private config: any) {}
|
|
554
|
-
|
|
555
|
-
async execute(input: string): Promise<string> {
|
|
556
|
-
// Implementation generated by Code subagent
|
|
557
|
-
return this.process(input);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
private process(input: string): string {
|
|
561
|
-
return \`Processed: \${input}\`;
|
|
562
|
-
}
|
|
563
|
-
}`,
|
|
564
|
-
files: ["src/solution.ts"]
|
|
565
|
-
},
|
|
566
|
-
testing: {
|
|
567
|
-
tests: `
|
|
568
|
-
describe('Solution', () => {
|
|
569
|
-
it('should process input correctly', () => {
|
|
570
|
-
const solution = new Solution({});
|
|
571
|
-
const result = solution.execute('test');
|
|
572
|
-
expect(result).toBe('Processed: test');
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it('should handle edge cases', () => {
|
|
576
|
-
// Edge case tests
|
|
577
|
-
});
|
|
578
|
-
});`,
|
|
579
|
-
coverage: { lines: 95, branches: 88, functions: 100 }
|
|
580
|
-
},
|
|
581
|
-
review: {
|
|
582
|
-
quality: 0.82,
|
|
583
|
-
issues: [
|
|
584
|
-
{ severity: "high", message: "Missing error handling" },
|
|
585
|
-
{ severity: "medium", message: "Could improve type safety" }
|
|
586
|
-
],
|
|
587
|
-
suggestions: [
|
|
588
|
-
"Add try-catch blocks",
|
|
589
|
-
"Use stricter TypeScript types",
|
|
590
|
-
"Add input validation"
|
|
591
|
-
]
|
|
592
|
-
}
|
|
593
|
-
};
|
|
594
|
-
return {
|
|
595
|
-
success: true,
|
|
596
|
-
result: mockResponses[request.type] || { status: "completed" },
|
|
597
|
-
output: `Mock ${request.type} subagent completed successfully`,
|
|
598
|
-
duration: Date.now() - startTime,
|
|
599
|
-
subagentType: request.type,
|
|
600
|
-
tokens: Math.floor(Math.random() * 5e3) + 1e3
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
544
|
/**
|
|
604
545
|
* Get active subagent statistics
|
|
605
546
|
*/
|
|
606
547
|
getStats() {
|
|
607
548
|
return {
|
|
608
|
-
activeSubagents: this.activeSubagents.size,
|
|
609
549
|
tempDir: this.tempDir
|
|
610
550
|
};
|
|
611
551
|
}
|
|
@@ -613,10 +553,6 @@ describe('Solution', () => {
|
|
|
613
553
|
* Cleanup all resources
|
|
614
554
|
*/
|
|
615
555
|
async cleanupAll() {
|
|
616
|
-
for (const [id, controller] of this.activeSubagents) {
|
|
617
|
-
controller.abort();
|
|
618
|
-
}
|
|
619
|
-
this.activeSubagents.clear();
|
|
620
556
|
if (fs.existsSync(this.tempDir)) {
|
|
621
557
|
const files = await fs.promises.readdir(this.tempDir);
|
|
622
558
|
for (const file of files) {
|
|
@@ -3,6 +3,7 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { v4 as uuidv4 } from "uuid";
|
|
6
|
+
import { spawn } from "child_process";
|
|
6
7
|
import { logger } from "../../core/monitoring/logger.js";
|
|
7
8
|
class ClaudeCodeTaskCoordinator {
|
|
8
9
|
activeTasks = /* @__PURE__ */ new Map();
|
|
@@ -165,11 +166,16 @@ class ClaudeCodeTaskCoordinator {
|
|
|
165
166
|
completedTasks: this.completedTasks.length
|
|
166
167
|
});
|
|
167
168
|
if (this.activeTasks.size > 0) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
let timer;
|
|
170
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
171
|
+
timer = setTimeout(resolve, 3e4);
|
|
172
|
+
});
|
|
171
173
|
const completionPromise = this.waitForTaskCompletion();
|
|
172
|
-
|
|
174
|
+
try {
|
|
175
|
+
await Promise.race([completionPromise, timeoutPromise]);
|
|
176
|
+
} finally {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
}
|
|
173
179
|
if (this.activeTasks.size > 0) {
|
|
174
180
|
logger.warn("Force terminating active tasks", {
|
|
175
181
|
remainingTasks: this.activeTasks.size
|
|
@@ -205,26 +211,68 @@ class ClaudeCodeTaskCoordinator {
|
|
|
205
211
|
agentType: agentConfig.type,
|
|
206
212
|
promptTokens: this.estimateTokenUsage(prompt, "")
|
|
207
213
|
});
|
|
208
|
-
return this.
|
|
214
|
+
return this.spawnClaudeCode(agentName, prompt, agentConfig);
|
|
209
215
|
}
|
|
210
216
|
/**
|
|
211
|
-
*
|
|
217
|
+
* Spawn Claude Code CLI as a subprocess.
|
|
218
|
+
* Uses `claude --print` for non-interactive execution with stdout capture.
|
|
219
|
+
* Workspace cwd is inherited from the coordinator's process.
|
|
212
220
|
*/
|
|
213
|
-
|
|
214
|
-
return new Promise((resolve) => {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
spawnClaudeCode(agentName, prompt, agentConfig) {
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
const args = ["--print"];
|
|
224
|
+
if (agentConfig.type === "oracle") {
|
|
225
|
+
args.push("--model", "opus");
|
|
226
|
+
}
|
|
227
|
+
if (agentConfig.capabilities.includes("code_implementation")) {
|
|
228
|
+
args.push("--allowedTools", "Edit,Write,Bash,Read,Glob,Grep");
|
|
229
|
+
} else {
|
|
230
|
+
args.push("--allowedTools", "Read,Glob,Grep,Bash");
|
|
231
|
+
}
|
|
232
|
+
args.push(prompt);
|
|
233
|
+
logger.info("Spawning claude CLI", {
|
|
234
|
+
agentName,
|
|
235
|
+
agentType: agentConfig.type,
|
|
236
|
+
cwd: process.cwd(),
|
|
237
|
+
promptLength: prompt.length
|
|
238
|
+
});
|
|
239
|
+
const proc = spawn("claude", args, {
|
|
240
|
+
cwd: process.cwd(),
|
|
241
|
+
env: { ...process.env },
|
|
242
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
243
|
+
});
|
|
244
|
+
let stdout = "";
|
|
245
|
+
let stderr = "";
|
|
246
|
+
proc.stdout.on("data", (data) => {
|
|
247
|
+
stdout += data.toString();
|
|
248
|
+
});
|
|
249
|
+
proc.stderr.on("data", (data) => {
|
|
250
|
+
stderr += data.toString();
|
|
251
|
+
});
|
|
252
|
+
proc.on("error", (err) => {
|
|
253
|
+
logger.error("Failed to spawn claude CLI", {
|
|
254
|
+
agentName,
|
|
255
|
+
error: err.message
|
|
256
|
+
});
|
|
257
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
258
|
+
});
|
|
259
|
+
proc.on("close", (code) => {
|
|
260
|
+
if (code === 0) {
|
|
261
|
+
logger.debug("Claude CLI completed", {
|
|
262
|
+
agentName,
|
|
263
|
+
outputLength: stdout.length
|
|
264
|
+
});
|
|
265
|
+
resolve(stdout.trim());
|
|
266
|
+
} else {
|
|
267
|
+
const errMsg = stderr.slice(0, 500) || `exit code ${code}`;
|
|
268
|
+
logger.warn("Claude CLI failed", {
|
|
269
|
+
agentName,
|
|
270
|
+
exitCode: code,
|
|
271
|
+
stderr: errMsg
|
|
272
|
+
});
|
|
273
|
+
reject(new Error(`Claude CLI exited with code ${code}: ${errMsg}`));
|
|
274
|
+
}
|
|
275
|
+
});
|
|
228
276
|
});
|
|
229
277
|
}
|
|
230
278
|
/**
|