@stackmemoryai/stackmemory 1.6.1 → 1.7.0
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.
|
@@ -0,0 +1,314 @@
|
|
|
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 Database from "better-sqlite3";
|
|
6
|
+
import { mkdirSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
function classifyErrorText(text) {
|
|
11
|
+
const lower = text.toLowerCase();
|
|
12
|
+
if (lower.includes("lint") || lower.includes("eslint") || lower.includes("prettier"))
|
|
13
|
+
return "lint_failure";
|
|
14
|
+
if (lower.includes("test") && (lower.includes("fail") || lower.includes("error")))
|
|
15
|
+
return "test_failure";
|
|
16
|
+
if (lower.includes("timeout") || lower.includes("timed out"))
|
|
17
|
+
return "timeout";
|
|
18
|
+
if (lower.includes("conflict") || lower.includes("merge"))
|
|
19
|
+
return "git_conflict";
|
|
20
|
+
if (lower.includes("429") || lower.includes("rate limit"))
|
|
21
|
+
return "rate_limit";
|
|
22
|
+
if (lower.includes("permission") || lower.includes("EACCES") || lower.includes("not found"))
|
|
23
|
+
return "permission_or_missing";
|
|
24
|
+
if (lower.includes("build") && lower.includes("error"))
|
|
25
|
+
return "build_failure";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function getTracesDbPath() {
|
|
29
|
+
return join(homedir(), ".stackmemory", "conductor", "traces.db");
|
|
30
|
+
}
|
|
31
|
+
function openTracesDb(dbPath) {
|
|
32
|
+
const path = dbPath ?? getTracesDbPath();
|
|
33
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
34
|
+
const db = new Database(path);
|
|
35
|
+
db.pragma("journal_mode = WAL");
|
|
36
|
+
db.pragma("foreign_keys = ON");
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS conductor_traces (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
issue_id TEXT NOT NULL,
|
|
41
|
+
session_id TEXT NOT NULL,
|
|
42
|
+
attempt INTEGER NOT NULL,
|
|
43
|
+
turn_number INTEGER NOT NULL,
|
|
44
|
+
timestamp INTEGER NOT NULL,
|
|
45
|
+
phase TEXT,
|
|
46
|
+
tool_names TEXT,
|
|
47
|
+
tool_count INTEGER DEFAULT 0,
|
|
48
|
+
files_modified INTEGER DEFAULT 0,
|
|
49
|
+
input_tokens INTEGER DEFAULT 0,
|
|
50
|
+
output_tokens INTEGER DEFAULT 0,
|
|
51
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
52
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
53
|
+
message_preview TEXT,
|
|
54
|
+
event_json TEXT NOT NULL,
|
|
55
|
+
UNIQUE(session_id, turn_number)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_traces_issue
|
|
59
|
+
ON conductor_traces(issue_id, attempt);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_traces_session
|
|
61
|
+
ON conductor_traces(session_id);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_traces_phase
|
|
63
|
+
ON conductor_traces(phase);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_traces_timestamp
|
|
65
|
+
ON conductor_traces(timestamp DESC);
|
|
66
|
+
`);
|
|
67
|
+
return db;
|
|
68
|
+
}
|
|
69
|
+
function withDb(db, fn) {
|
|
70
|
+
const ownDb = db ?? openTracesDb();
|
|
71
|
+
try {
|
|
72
|
+
return fn(ownDb);
|
|
73
|
+
} finally {
|
|
74
|
+
if (!db) ownDb.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class TraceCollector {
|
|
78
|
+
db;
|
|
79
|
+
ownsDb;
|
|
80
|
+
sessionId;
|
|
81
|
+
issueId;
|
|
82
|
+
attempt;
|
|
83
|
+
turnCounter = 0;
|
|
84
|
+
insertStmt;
|
|
85
|
+
constructor(opts) {
|
|
86
|
+
this.issueId = opts.issueId;
|
|
87
|
+
this.attempt = opts.attempt;
|
|
88
|
+
this.sessionId = `${opts.issueId}-${opts.attempt}-${randomUUID().slice(0, 8)}`;
|
|
89
|
+
this.ownsDb = !opts.db;
|
|
90
|
+
this.db = opts.db ?? openTracesDb();
|
|
91
|
+
this.insertStmt = this.db.prepare(`
|
|
92
|
+
INSERT INTO conductor_traces (
|
|
93
|
+
issue_id, session_id, attempt, turn_number, timestamp,
|
|
94
|
+
phase, tool_names, tool_count, files_modified,
|
|
95
|
+
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
|
|
96
|
+
message_preview, event_json
|
|
97
|
+
) VALUES (
|
|
98
|
+
@issue_id, @session_id, @attempt, @turn_number, @timestamp,
|
|
99
|
+
@phase, @tool_names, @tool_count, @files_modified,
|
|
100
|
+
@input_tokens, @output_tokens, @cache_creation_tokens, @cache_read_tokens,
|
|
101
|
+
@message_preview, @event_json
|
|
102
|
+
)
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
get session() {
|
|
106
|
+
return this.sessionId;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Record a turn using pre-extracted data from the orchestrator's stream parser.
|
|
110
|
+
* Avoids re-iterating content blocks — the caller already did that work.
|
|
111
|
+
*/
|
|
112
|
+
recordTurn(turnData, phase, eventJson) {
|
|
113
|
+
this.insertStmt.run({
|
|
114
|
+
issue_id: this.issueId,
|
|
115
|
+
session_id: this.sessionId,
|
|
116
|
+
attempt: this.attempt,
|
|
117
|
+
turn_number: this.turnCounter++,
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
phase: phase ?? null,
|
|
120
|
+
tool_names: turnData.toolNames.length > 0 ? JSON.stringify(turnData.toolNames) : null,
|
|
121
|
+
tool_count: turnData.toolCount,
|
|
122
|
+
files_modified: turnData.filesModified,
|
|
123
|
+
input_tokens: turnData.inputTokens,
|
|
124
|
+
output_tokens: turnData.outputTokens,
|
|
125
|
+
cache_creation_tokens: turnData.cacheCreationTokens,
|
|
126
|
+
cache_read_tokens: turnData.cacheReadTokens,
|
|
127
|
+
message_preview: turnData.textPreview,
|
|
128
|
+
event_json: eventJson
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Record a result event (final output) */
|
|
132
|
+
recordResult(event) {
|
|
133
|
+
const resultText = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
134
|
+
this.insertStmt.run({
|
|
135
|
+
issue_id: this.issueId,
|
|
136
|
+
session_id: this.sessionId,
|
|
137
|
+
attempt: this.attempt,
|
|
138
|
+
turn_number: this.turnCounter++,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
phase: "result",
|
|
141
|
+
tool_names: null,
|
|
142
|
+
tool_count: 0,
|
|
143
|
+
files_modified: 0,
|
|
144
|
+
input_tokens: 0,
|
|
145
|
+
output_tokens: 0,
|
|
146
|
+
cache_creation_tokens: 0,
|
|
147
|
+
cache_read_tokens: 0,
|
|
148
|
+
message_preview: (resultText || "").slice(0, 500),
|
|
149
|
+
event_json: JSON.stringify(event).slice(0, 5e4)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** Close the DB connection only if we own it */
|
|
153
|
+
close() {
|
|
154
|
+
if (!this.ownsDb) return;
|
|
155
|
+
try {
|
|
156
|
+
this.db.close();
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function stringifyEventTruncated(event) {
|
|
162
|
+
return JSON.stringify(event, (_key, value) => {
|
|
163
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
164
|
+
const obj = value;
|
|
165
|
+
if (obj.type === "tool_use" && obj.input) {
|
|
166
|
+
const inputStr = JSON.stringify(obj.input);
|
|
167
|
+
if (inputStr.length > 2e3) {
|
|
168
|
+
return {
|
|
169
|
+
...obj,
|
|
170
|
+
input: { _truncated: true, length: inputStr.length }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (obj.type === "tool_result" && obj.content) {
|
|
175
|
+
const contentStr = typeof obj.content === "string" ? obj.content : JSON.stringify(obj.content);
|
|
176
|
+
if (contentStr.length > 2e3) {
|
|
177
|
+
return { ...obj, content: `[truncated: ${contentStr.length} chars]` };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function listSessions(issueId, db) {
|
|
185
|
+
return withDb(db, (d) => {
|
|
186
|
+
const rows = d.prepare(
|
|
187
|
+
`
|
|
188
|
+
SELECT
|
|
189
|
+
issue_id,
|
|
190
|
+
session_id,
|
|
191
|
+
attempt,
|
|
192
|
+
COUNT(*) as total_turns,
|
|
193
|
+
SUM(tool_count) as total_tool_calls,
|
|
194
|
+
SUM(files_modified) as total_files_modified,
|
|
195
|
+
SUM(input_tokens) as total_input_tokens,
|
|
196
|
+
SUM(output_tokens) as total_output_tokens,
|
|
197
|
+
GROUP_CONCAT(DISTINCT phase) as phases,
|
|
198
|
+
MIN(timestamp) as started_at,
|
|
199
|
+
MAX(timestamp) as ended_at
|
|
200
|
+
FROM conductor_traces
|
|
201
|
+
WHERE issue_id = ?
|
|
202
|
+
GROUP BY session_id
|
|
203
|
+
ORDER BY started_at DESC
|
|
204
|
+
`
|
|
205
|
+
).all(issueId);
|
|
206
|
+
return rows.map((r) => ({
|
|
207
|
+
issue_id: r.issue_id,
|
|
208
|
+
session_id: r.session_id,
|
|
209
|
+
attempt: r.attempt,
|
|
210
|
+
total_turns: r.total_turns,
|
|
211
|
+
total_tool_calls: r.total_tool_calls || 0,
|
|
212
|
+
total_files_modified: r.total_files_modified || 0,
|
|
213
|
+
total_input_tokens: r.total_input_tokens || 0,
|
|
214
|
+
total_output_tokens: r.total_output_tokens || 0,
|
|
215
|
+
phases: (r.phases || "").split(",").filter(Boolean),
|
|
216
|
+
started_at: r.started_at,
|
|
217
|
+
ended_at: r.ended_at,
|
|
218
|
+
duration_ms: r.ended_at - r.started_at
|
|
219
|
+
}));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function getSessionTurns(sessionId, db) {
|
|
223
|
+
return withDb(
|
|
224
|
+
db,
|
|
225
|
+
(d) => d.prepare(
|
|
226
|
+
`
|
|
227
|
+
SELECT * FROM conductor_traces
|
|
228
|
+
WHERE session_id = ?
|
|
229
|
+
ORDER BY turn_number ASC
|
|
230
|
+
`
|
|
231
|
+
).all(sessionId)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
function getPhaseBreakdown(sessionId, db) {
|
|
235
|
+
return withDb(
|
|
236
|
+
db,
|
|
237
|
+
(d) => d.prepare(
|
|
238
|
+
`
|
|
239
|
+
SELECT
|
|
240
|
+
phase,
|
|
241
|
+
COUNT(*) as turns,
|
|
242
|
+
SUM(tool_count) as tool_calls,
|
|
243
|
+
SUM(input_tokens) as input_tokens,
|
|
244
|
+
SUM(output_tokens) as output_tokens
|
|
245
|
+
FROM conductor_traces
|
|
246
|
+
WHERE session_id = ? AND phase IS NOT NULL
|
|
247
|
+
GROUP BY phase
|
|
248
|
+
ORDER BY MIN(turn_number) ASC
|
|
249
|
+
`
|
|
250
|
+
).all(sessionId)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
function getToolFrequencies(issueId, db) {
|
|
254
|
+
return withDb(
|
|
255
|
+
db,
|
|
256
|
+
(d) => d.prepare(
|
|
257
|
+
`
|
|
258
|
+
SELECT j.value as tool_name, COUNT(*) as count
|
|
259
|
+
FROM conductor_traces, json_each(tool_names) j
|
|
260
|
+
WHERE issue_id = ? AND tool_names IS NOT NULL
|
|
261
|
+
GROUP BY j.value
|
|
262
|
+
ORDER BY count DESC
|
|
263
|
+
`
|
|
264
|
+
).all(issueId)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
function getFailureTurns(issueId, tailCount = 5, db) {
|
|
268
|
+
return withDb(
|
|
269
|
+
db,
|
|
270
|
+
(d) => d.prepare(
|
|
271
|
+
`
|
|
272
|
+
SELECT t.* FROM conductor_traces t
|
|
273
|
+
INNER JOIN (
|
|
274
|
+
SELECT session_id, MAX(turn_number) as max_turn
|
|
275
|
+
FROM conductor_traces
|
|
276
|
+
WHERE issue_id = ?
|
|
277
|
+
GROUP BY session_id
|
|
278
|
+
) latest ON t.session_id = latest.session_id
|
|
279
|
+
AND t.turn_number > latest.max_turn - ?
|
|
280
|
+
WHERE t.issue_id = ?
|
|
281
|
+
ORDER BY t.session_id, t.turn_number ASC
|
|
282
|
+
`
|
|
283
|
+
).all(issueId, tailCount, issueId)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function getTraceStats(db) {
|
|
287
|
+
return withDb(
|
|
288
|
+
db,
|
|
289
|
+
(d) => d.prepare(
|
|
290
|
+
`
|
|
291
|
+
SELECT
|
|
292
|
+
COUNT(DISTINCT session_id) as total_sessions,
|
|
293
|
+
COUNT(*) as total_turns,
|
|
294
|
+
SUM(input_tokens) as total_input_tokens,
|
|
295
|
+
SUM(output_tokens) as total_output_tokens,
|
|
296
|
+
COUNT(DISTINCT issue_id) as issues_traced
|
|
297
|
+
FROM conductor_traces
|
|
298
|
+
`
|
|
299
|
+
).get()
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
TraceCollector,
|
|
304
|
+
classifyErrorText,
|
|
305
|
+
getFailureTurns,
|
|
306
|
+
getPhaseBreakdown,
|
|
307
|
+
getSessionTurns,
|
|
308
|
+
getToolFrequencies,
|
|
309
|
+
getTraceStats,
|
|
310
|
+
getTracesDbPath,
|
|
311
|
+
listSessions,
|
|
312
|
+
openTracesDb,
|
|
313
|
+
stringifyEventTruncated
|
|
314
|
+
};
|
|
@@ -22,6 +22,16 @@ import {
|
|
|
22
22
|
getAgentStatusDir,
|
|
23
23
|
getOutcomesLogPath
|
|
24
24
|
} from "./orchestrator.js";
|
|
25
|
+
import {
|
|
26
|
+
openTracesDb,
|
|
27
|
+
listSessions,
|
|
28
|
+
getSessionTurns,
|
|
29
|
+
getPhaseBreakdown,
|
|
30
|
+
getToolFrequencies,
|
|
31
|
+
getFailureTurns,
|
|
32
|
+
getTraceStats,
|
|
33
|
+
classifyErrorText
|
|
34
|
+
} from "./conductor-traces.js";
|
|
25
35
|
function getGlobalStorePath() {
|
|
26
36
|
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
27
37
|
if (!existsSync(dir)) {
|
|
@@ -70,6 +80,17 @@ function fmtTokens(n) {
|
|
|
70
80
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
71
81
|
return String(n);
|
|
72
82
|
}
|
|
83
|
+
function formatTokens(n) {
|
|
84
|
+
return fmtTokens(n) + " tok";
|
|
85
|
+
}
|
|
86
|
+
function formatDuration(ms) {
|
|
87
|
+
const seconds = Math.floor(ms / 1e3);
|
|
88
|
+
if (seconds < 60) return `${seconds}s`;
|
|
89
|
+
const minutes = Math.floor(seconds / 60);
|
|
90
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
91
|
+
const hours = Math.floor(minutes / 60);
|
|
92
|
+
return `${hours}h ${minutes % 60}m`;
|
|
93
|
+
}
|
|
73
94
|
function budgetBar(pct, width = 30) {
|
|
74
95
|
const filled = Math.min(Math.round(pct / 100 * width), width);
|
|
75
96
|
const empty = width - filled;
|
|
@@ -354,6 +375,44 @@ function predictDifficulty(labels, description, priority, outcomes) {
|
|
|
354
375
|
}
|
|
355
376
|
return { difficulty, confidence, reasons };
|
|
356
377
|
}
|
|
378
|
+
function analyzeErrorsFromTraces(failedIssues) {
|
|
379
|
+
const patterns = {};
|
|
380
|
+
const evidence = [];
|
|
381
|
+
let db;
|
|
382
|
+
try {
|
|
383
|
+
db = openTracesDb();
|
|
384
|
+
} catch {
|
|
385
|
+
return { patterns, evidence };
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
for (const issueId of failedIssues) {
|
|
389
|
+
const turns = getFailureTurns(issueId, 3, db);
|
|
390
|
+
if (turns.length === 0) continue;
|
|
391
|
+
for (const turn of turns) {
|
|
392
|
+
const preview = turn.message_preview || "";
|
|
393
|
+
const tools = turn.tool_names ? JSON.parse(turn.tool_names) : [];
|
|
394
|
+
const pattern = classifyErrorText(preview);
|
|
395
|
+
if (pattern) {
|
|
396
|
+
patterns[pattern] = (patterns[pattern] || 0) + 1;
|
|
397
|
+
}
|
|
398
|
+
if (evidence.length < 15) {
|
|
399
|
+
evidence.push({
|
|
400
|
+
issue: issueId,
|
|
401
|
+
phase: turn.phase || "unknown",
|
|
402
|
+
tools,
|
|
403
|
+
preview: preview.slice(0, 300)
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
db.close();
|
|
410
|
+
}
|
|
411
|
+
if (Object.keys(patterns).length === 0 && failedIssues.length > 0) {
|
|
412
|
+
patterns["unknown"] = failedIssues.length;
|
|
413
|
+
}
|
|
414
|
+
return { patterns, evidence };
|
|
415
|
+
}
|
|
357
416
|
function loadOutcomes() {
|
|
358
417
|
const logPath = getOutcomesLogPath();
|
|
359
418
|
if (!existsSync(logPath)) return [];
|
|
@@ -394,6 +453,68 @@ function spawnClaudePrint(prompt, timeoutMs = 12e4) {
|
|
|
394
453
|
child.stdin.end();
|
|
395
454
|
});
|
|
396
455
|
}
|
|
456
|
+
function printSimpleDiff(oldText, newText) {
|
|
457
|
+
const oldLines = oldText.split("\n");
|
|
458
|
+
const newLines = newText.split("\n");
|
|
459
|
+
const oldSet = new Set(oldLines);
|
|
460
|
+
const newSet = new Set(newLines);
|
|
461
|
+
const removed = oldLines.filter((l) => !newSet.has(l));
|
|
462
|
+
const added = newLines.filter((l) => !oldSet.has(l));
|
|
463
|
+
const removedSet = new Set(removed);
|
|
464
|
+
const addedSet = new Set(added);
|
|
465
|
+
const CONTEXT = 2;
|
|
466
|
+
const changedOld = /* @__PURE__ */ new Set();
|
|
467
|
+
for (let i = 0; i < oldLines.length; i++) {
|
|
468
|
+
if (removedSet.has(oldLines[i])) changedOld.add(i);
|
|
469
|
+
}
|
|
470
|
+
const changedNew = /* @__PURE__ */ new Set();
|
|
471
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
472
|
+
if (addedSet.has(newLines[i])) changedNew.add(i);
|
|
473
|
+
}
|
|
474
|
+
let lastPrinted = -1;
|
|
475
|
+
for (let i = 0; i < oldLines.length; i++) {
|
|
476
|
+
let nearChange = false;
|
|
477
|
+
for (let j = Math.max(0, i - CONTEXT); j <= Math.min(oldLines.length - 1, i + CONTEXT); j++) {
|
|
478
|
+
if (changedOld.has(j)) {
|
|
479
|
+
nearChange = true;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!nearChange) continue;
|
|
484
|
+
if (lastPrinted >= 0 && i > lastPrinted + 1) {
|
|
485
|
+
console.log(` ${c.d}...${c.r}`);
|
|
486
|
+
}
|
|
487
|
+
if (removedSet.has(oldLines[i])) {
|
|
488
|
+
console.log(` ${c.red}- ${oldLines[i]}${c.r}`);
|
|
489
|
+
} else {
|
|
490
|
+
console.log(` ${oldLines[i]}`);
|
|
491
|
+
}
|
|
492
|
+
lastPrinted = i;
|
|
493
|
+
}
|
|
494
|
+
if (removed.length > 0 && added.length > 0) {
|
|
495
|
+
console.log(` ${c.d}---${c.r}`);
|
|
496
|
+
}
|
|
497
|
+
lastPrinted = -1;
|
|
498
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
499
|
+
let nearChange = false;
|
|
500
|
+
for (let j = Math.max(0, i - CONTEXT); j <= Math.min(newLines.length - 1, i + CONTEXT); j++) {
|
|
501
|
+
if (changedNew.has(j)) {
|
|
502
|
+
nearChange = true;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!nearChange) continue;
|
|
507
|
+
if (lastPrinted >= 0 && i > lastPrinted + 1) {
|
|
508
|
+
console.log(` ${c.d}...${c.r}`);
|
|
509
|
+
}
|
|
510
|
+
if (addedSet.has(newLines[i])) {
|
|
511
|
+
console.log(` ${c.green}+ ${newLines[i]}${c.r}`);
|
|
512
|
+
} else {
|
|
513
|
+
console.log(` ${newLines[i]}`);
|
|
514
|
+
}
|
|
515
|
+
lastPrinted = i;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
397
518
|
async function evolvePromptTemplate(input) {
|
|
398
519
|
const {
|
|
399
520
|
templatePath,
|
|
@@ -402,7 +523,9 @@ async function evolvePromptTemplate(input) {
|
|
|
402
523
|
failPhases,
|
|
403
524
|
errorPatterns,
|
|
404
525
|
recs,
|
|
405
|
-
outcomes
|
|
526
|
+
outcomes,
|
|
527
|
+
dryRun,
|
|
528
|
+
traceEvidence
|
|
406
529
|
} = input;
|
|
407
530
|
let currentTemplate;
|
|
408
531
|
if (existsSync(templatePath)) {
|
|
@@ -415,11 +538,24 @@ async function evolvePromptTemplate(input) {
|
|
|
415
538
|
}
|
|
416
539
|
const failPhaseSummary = Object.entries(failPhases).sort((a, b) => b[1] - a[1]).map(([phase, count]) => ` - ${phase}: ${count} failures`).join("\n");
|
|
417
540
|
const errorSummary = Object.entries(errorPatterns).sort((a, b) => b[1] - a[1]).map(([pattern, count]) => ` - ${pattern}: ${count} occurrences`).join("\n");
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
541
|
+
let failureEvidenceSection;
|
|
542
|
+
if (traceEvidence && traceEvidence.length > 0) {
|
|
543
|
+
const evidenceLines = traceEvidence.slice(0, 10).map((ev) => {
|
|
544
|
+
const tools = ev.tools.length > 0 ? ev.tools.join(", ") : "none";
|
|
545
|
+
return ` [${ev.issue}, phase: ${ev.phase}, tools: ${tools}]
|
|
546
|
+
${ev.preview.slice(0, 200)}`;
|
|
547
|
+
});
|
|
548
|
+
failureEvidenceSection = `ACTUAL FAILURE EVIDENCE (from conversation traces):
|
|
549
|
+
${evidenceLines.join("\n\n")}`;
|
|
550
|
+
} else {
|
|
551
|
+
const failedOutcomes = outcomes.filter((o) => o.outcome === "failure" && o.errorTail).slice(-5);
|
|
552
|
+
const errorTails = failedOutcomes.map(
|
|
553
|
+
(o) => ` [${o.issue} attempt ${o.attempt}, phase: ${o.phase}]
|
|
421
554
|
${o.errorTail}`
|
|
422
|
-
|
|
555
|
+
).join("\n\n");
|
|
556
|
+
failureEvidenceSection = `SAMPLE ERROR TAILS FROM RECENT FAILURES:
|
|
557
|
+
${errorTails || " (none)"}`;
|
|
558
|
+
}
|
|
423
559
|
const mutationPrompt = `You are optimizing a prompt template for autonomous AI coding agents managed by a conductor system.
|
|
424
560
|
|
|
425
561
|
CURRENT PROMPT TEMPLATE:
|
|
@@ -437,8 +573,7 @@ ${failPhaseSummary || " (none)"}
|
|
|
437
573
|
ERROR PATTERNS:
|
|
438
574
|
${errorSummary || " (none)"}
|
|
439
575
|
|
|
440
|
-
|
|
441
|
-
${errorTails || " (none)"}
|
|
576
|
+
${failureEvidenceSection}
|
|
442
577
|
|
|
443
578
|
RECOMMENDATIONS FROM ANALYSIS:
|
|
444
579
|
${recs.map((r) => `- ${r}`).join("\n")}
|
|
@@ -475,6 +610,17 @@ OUTPUT THE IMPROVED TEMPLATE:`;
|
|
|
475
610
|
);
|
|
476
611
|
return;
|
|
477
612
|
}
|
|
613
|
+
if (dryRun) {
|
|
614
|
+
console.log(`
|
|
615
|
+
${c.b}${c.cyan}Dry-run diff:${c.r}
|
|
616
|
+
`);
|
|
617
|
+
printSimpleDiff(currentTemplate, evolved.trim());
|
|
618
|
+
console.log(
|
|
619
|
+
`
|
|
620
|
+
${c.d}Dry run \u2014 no files modified. Run without --dry-run to apply.${c.r}`
|
|
621
|
+
);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
478
624
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
479
625
|
const backupPath = templatePath.replace(".md", `.backup-${timestamp}.md`);
|
|
480
626
|
copyFileSync(templatePath, backupPath);
|
|
@@ -913,6 +1059,13 @@ function createConductorCommands() {
|
|
|
913
1059
|
"--evolve",
|
|
914
1060
|
"Auto-mutate prompt template using GEPA-style evolution from failure data",
|
|
915
1061
|
false
|
|
1062
|
+
).option(
|
|
1063
|
+
"--dry-run",
|
|
1064
|
+
"Show evolved template without writing (use with --evolve)",
|
|
1065
|
+
false
|
|
1066
|
+
).option(
|
|
1067
|
+
"--no-evidence",
|
|
1068
|
+
"Disable trace-based evidence display (on by default when traces.db exists)"
|
|
916
1069
|
).option(
|
|
917
1070
|
"--predict",
|
|
918
1071
|
"Show difficulty predictions alongside actual outcomes",
|
|
@@ -961,22 +1114,23 @@ function createConductorCommands() {
|
|
|
961
1114
|
const retrySuccessRate = retries.length > 0 ? Math.round(
|
|
962
1115
|
retries.filter((o) => o.outcome === "success").length / retries.length * 100
|
|
963
1116
|
) : 0;
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1117
|
+
const failedIssueIds = [
|
|
1118
|
+
...new Set(
|
|
1119
|
+
outcomes.filter((o) => o.outcome === "failure").map((o) => o.issue)
|
|
1120
|
+
)
|
|
1121
|
+
];
|
|
1122
|
+
const traceAnalysis = analyzeErrorsFromTraces(failedIssueIds);
|
|
1123
|
+
let errorPatterns = traceAnalysis.patterns;
|
|
1124
|
+
let traceEvidence = traceAnalysis.evidence;
|
|
1125
|
+
if (Object.keys(errorPatterns).length === 0 || Object.keys(errorPatterns).length === 1 && errorPatterns["unknown"]) {
|
|
1126
|
+
errorPatterns = {};
|
|
1127
|
+
traceEvidence = [];
|
|
1128
|
+
for (const o of outcomes.filter(
|
|
1129
|
+
(o2) => o2.outcome === "failure" && o2.errorTail
|
|
1130
|
+
)) {
|
|
1131
|
+
const pattern = classifyErrorText(o.errorTail) ?? "unknown";
|
|
1132
|
+
errorPatterns[pattern] = (errorPatterns[pattern] || 0) + 1;
|
|
1133
|
+
}
|
|
980
1134
|
}
|
|
981
1135
|
if (options.export) {
|
|
982
1136
|
const analysis = {
|
|
@@ -1023,17 +1177,43 @@ function createConductorCommands() {
|
|
|
1023
1177
|
}
|
|
1024
1178
|
}
|
|
1025
1179
|
if (Object.keys(errorPatterns).length > 0) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1180
|
+
const sourceLabel = traceEvidence.length > 0 ? "(from traces)" : "(from errorTail)";
|
|
1181
|
+
console.log(
|
|
1182
|
+
`
|
|
1183
|
+
${c.b}Error Patterns${c.r} ${c.d}${sourceLabel}${c.r}`
|
|
1184
|
+
);
|
|
1028
1185
|
const sorted = Object.entries(errorPatterns).sort(
|
|
1029
1186
|
(a, b) => b[1] - a[1]
|
|
1030
1187
|
);
|
|
1031
1188
|
for (const [pattern, count] of sorted) {
|
|
1032
1189
|
console.log(
|
|
1033
|
-
` ${c.red}\u25CF${c.r} ${pattern.padEnd(
|
|
1190
|
+
` ${c.red}\u25CF${c.r} ${pattern.padEnd(20)} ${c.white}${count}${c.r}`
|
|
1034
1191
|
);
|
|
1035
1192
|
}
|
|
1036
1193
|
}
|
|
1194
|
+
if (options.evidence && traceEvidence.length > 0) {
|
|
1195
|
+
console.log(
|
|
1196
|
+
`
|
|
1197
|
+
${c.b}Failure Evidence${c.r} ${c.d}(from traces)${c.r}`
|
|
1198
|
+
);
|
|
1199
|
+
for (const ev of traceEvidence.slice(0, 15)) {
|
|
1200
|
+
const tools = ev.tools.length > 0 ? ev.tools.join(", ") : "-";
|
|
1201
|
+
console.log(
|
|
1202
|
+
` ${c.cyan}${ev.issue}${c.r} [${ev.phase}] tools: ${tools}`
|
|
1203
|
+
);
|
|
1204
|
+
if (ev.preview) {
|
|
1205
|
+
const lines = ev.preview.split("\n").slice(0, 3);
|
|
1206
|
+
for (const line of lines) {
|
|
1207
|
+
console.log(` ${c.d}${line.slice(0, 100)}${c.r}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
} else if (options.evidence && traceEvidence.length === 0) {
|
|
1212
|
+
console.log(
|
|
1213
|
+
`
|
|
1214
|
+
${c.d}No trace data available. Run conductor with trace logging enabled to collect evidence.${c.r}`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1037
1217
|
console.log(`
|
|
1038
1218
|
${c.b}Recommendations${c.r}`);
|
|
1039
1219
|
const recs = [];
|
|
@@ -1107,7 +1287,9 @@ function createConductorCommands() {
|
|
|
1107
1287
|
failPhases,
|
|
1108
1288
|
errorPatterns,
|
|
1109
1289
|
recs,
|
|
1110
|
-
outcomes
|
|
1290
|
+
outcomes,
|
|
1291
|
+
dryRun: options.dryRun,
|
|
1292
|
+
traceEvidence
|
|
1111
1293
|
});
|
|
1112
1294
|
}
|
|
1113
1295
|
if (options.predict) {
|
|
@@ -1329,6 +1511,9 @@ function createConductorCommands() {
|
|
|
1329
1511
|
"--model <model>",
|
|
1330
1512
|
'Model routing: "auto" (complexity-based) or a specific model ID',
|
|
1331
1513
|
"auto"
|
|
1514
|
+
).option(
|
|
1515
|
+
"--no-pr",
|
|
1516
|
+
"Disable automatic GitHub PR creation after agent success"
|
|
1332
1517
|
).action(async (options) => {
|
|
1333
1518
|
ensureDefaultPromptTemplate();
|
|
1334
1519
|
const conductor = new Conductor({
|
|
@@ -1344,7 +1529,8 @@ function createConductorCommands() {
|
|
|
1344
1529
|
maxRetries: parseInt(options.retries, 10),
|
|
1345
1530
|
turnTimeoutMs: parseInt(options.turnTimeout, 10),
|
|
1346
1531
|
agentMode: options.mode === "adapter" ? "adapter" : "cli",
|
|
1347
|
-
model: options.model
|
|
1532
|
+
model: options.model,
|
|
1533
|
+
autoPR: options.pr
|
|
1348
1534
|
});
|
|
1349
1535
|
await conductor.start();
|
|
1350
1536
|
});
|
|
@@ -1621,6 +1807,204 @@ function createConductorCommands() {
|
|
|
1621
1807
|
process.on("SIGTERM", cleanup);
|
|
1622
1808
|
});
|
|
1623
1809
|
});
|
|
1810
|
+
cmd.command("traces <issue-id>").description("Show conversation traces for an agent run").option("--session <id>", "Show specific session").option("--json", "Output as JSON", false).option("--tools", "Show tool frequency breakdown", false).option("--failures", "Show failure-turn details", false).option("-n, --tail <count>", "Failure tail turns", "5").action(
|
|
1811
|
+
(issueId, options) => {
|
|
1812
|
+
const db = openTracesDb();
|
|
1813
|
+
try {
|
|
1814
|
+
if (options.tools) {
|
|
1815
|
+
const freq = getToolFrequencies(issueId, db);
|
|
1816
|
+
if (freq.length === 0) {
|
|
1817
|
+
console.log(`No traces found for ${issueId}`);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
console.log(
|
|
1821
|
+
`
|
|
1822
|
+
${c.b}Tool Frequency${c.r} \u2014 ${c.cyan}${issueId}${c.r}
|
|
1823
|
+
`
|
|
1824
|
+
);
|
|
1825
|
+
for (const { tool_name, count } of freq.slice(0, 20)) {
|
|
1826
|
+
const bar = "\u2501".repeat(Math.min(count, 40));
|
|
1827
|
+
console.log(
|
|
1828
|
+
` ${c.d}${tool_name.padEnd(20)}${c.r} ${bar} ${count}`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
console.log("");
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (options.failures) {
|
|
1835
|
+
const turns = getFailureTurns(
|
|
1836
|
+
issueId,
|
|
1837
|
+
parseInt(options.tail, 10),
|
|
1838
|
+
db
|
|
1839
|
+
);
|
|
1840
|
+
if (turns.length === 0) {
|
|
1841
|
+
console.log(`No traces found for ${issueId}`);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
console.log(
|
|
1845
|
+
`
|
|
1846
|
+
${c.b}Failure Turns${c.r} \u2014 ${c.cyan}${issueId}${c.r}
|
|
1847
|
+
`
|
|
1848
|
+
);
|
|
1849
|
+
for (const t of turns) {
|
|
1850
|
+
const tools = t.tool_names ? JSON.parse(t.tool_names).join(", ") : "-";
|
|
1851
|
+
const ts = new Date(t.timestamp).toISOString().slice(11, 19);
|
|
1852
|
+
console.log(
|
|
1853
|
+
` ${c.d}[${ts}]${c.r} turn ${t.turn_number} | phase: ${t.phase || "-"} | tools: ${tools}`
|
|
1854
|
+
);
|
|
1855
|
+
if (t.message_preview) {
|
|
1856
|
+
const preview = t.message_preview.slice(0, 200);
|
|
1857
|
+
console.log(` ${c.d}${preview}${c.r}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
console.log("");
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (options.session) {
|
|
1864
|
+
const turns = getSessionTurns(options.session, db);
|
|
1865
|
+
if (turns.length === 0) {
|
|
1866
|
+
console.log(`No turns found for session ${options.session}`);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (options.json) {
|
|
1870
|
+
console.log(JSON.stringify(turns, null, 2));
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const phases = getPhaseBreakdown(options.session, db);
|
|
1874
|
+
console.log(
|
|
1875
|
+
`
|
|
1876
|
+
${c.b}Session${c.r} ${c.cyan}${options.session}${c.r}`
|
|
1877
|
+
);
|
|
1878
|
+
console.log(` ${turns.length} turns
|
|
1879
|
+
`);
|
|
1880
|
+
if (phases.length > 0) {
|
|
1881
|
+
console.log(` ${c.b}Phase Breakdown${c.r}`);
|
|
1882
|
+
for (const p of phases) {
|
|
1883
|
+
console.log(
|
|
1884
|
+
` ${(p.phase || "unknown").padEnd(14)} ${String(p.turns).padStart(3)} turns ${String(p.tool_calls).padStart(4)} tools ${formatTokens(p.input_tokens + p.output_tokens)}`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
console.log("");
|
|
1888
|
+
}
|
|
1889
|
+
console.log(` ${c.b}Turn Log${c.r}`);
|
|
1890
|
+
for (const t of turns) {
|
|
1891
|
+
const tools = t.tool_names ? JSON.parse(t.tool_names).join(", ") : "-";
|
|
1892
|
+
const ts = new Date(t.timestamp).toISOString().slice(11, 19);
|
|
1893
|
+
const tokens = t.input_tokens + t.output_tokens;
|
|
1894
|
+
console.log(
|
|
1895
|
+
` ${c.d}${String(t.turn_number).padStart(3)}${c.r} [${ts}] ${(t.phase || "-").padEnd(12)} tools: ${tools.padEnd(30)} ${formatTokens(tokens)}`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
console.log("");
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const sessions = listSessions(issueId, db);
|
|
1902
|
+
if (sessions.length === 0) {
|
|
1903
|
+
console.log(`No traces found for ${issueId}`);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (options.json) {
|
|
1907
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
console.log(
|
|
1911
|
+
`
|
|
1912
|
+
${c.b}Trace Sessions${c.r} \u2014 ${c.cyan}${issueId}${c.r}
|
|
1913
|
+
`
|
|
1914
|
+
);
|
|
1915
|
+
for (const s of sessions) {
|
|
1916
|
+
const dur = s.duration_ms > 0 ? formatDuration(s.duration_ms) : "-";
|
|
1917
|
+
const tokens = formatTokens(
|
|
1918
|
+
s.total_input_tokens + s.total_output_tokens
|
|
1919
|
+
);
|
|
1920
|
+
const time = new Date(s.started_at).toISOString().slice(0, 19);
|
|
1921
|
+
console.log(
|
|
1922
|
+
` ${c.d}attempt ${s.attempt}${c.r} ${s.total_turns} turns ${s.total_tool_calls} tools ${tokens} ${dur} ${c.d}${time}${c.r}`
|
|
1923
|
+
);
|
|
1924
|
+
console.log(` ${c.d}session: ${s.session_id}${c.r}`);
|
|
1925
|
+
console.log(` phases: ${s.phases.join(" \u2192 ")}`);
|
|
1926
|
+
console.log("");
|
|
1927
|
+
}
|
|
1928
|
+
} finally {
|
|
1929
|
+
db.close();
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
);
|
|
1933
|
+
cmd.command("replay <session-id>").description("Replay a full agent conversation from traces").option("-n, --turns <count>", "Show only last N turns").option("--json", "Output raw event JSON", false).action((sessionId, options) => {
|
|
1934
|
+
const db = openTracesDb();
|
|
1935
|
+
try {
|
|
1936
|
+
let turns = getSessionTurns(sessionId, db);
|
|
1937
|
+
if (turns.length === 0) {
|
|
1938
|
+
console.error(`No traces found for session ${sessionId}`);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (options.turns) {
|
|
1942
|
+
const n = parseInt(options.turns, 10);
|
|
1943
|
+
turns = turns.slice(-n);
|
|
1944
|
+
}
|
|
1945
|
+
if (options.json) {
|
|
1946
|
+
for (const t of turns) {
|
|
1947
|
+
console.log(t.event_json);
|
|
1948
|
+
}
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
console.log(
|
|
1952
|
+
`
|
|
1953
|
+
${c.b}Replay${c.r} ${c.cyan}${sessionId}${c.r} \u2014 ${turns.length} turns
|
|
1954
|
+
`
|
|
1955
|
+
);
|
|
1956
|
+
for (const t of turns) {
|
|
1957
|
+
const ts = new Date(t.timestamp).toISOString().slice(11, 19);
|
|
1958
|
+
const tokens = t.input_tokens + t.output_tokens;
|
|
1959
|
+
console.log(
|
|
1960
|
+
`${c.purple}\u2500\u2500 Turn ${t.turn_number} \u2500\u2500${c.r} [${ts}] ${t.phase || ""} ${tokens > 0 ? formatTokens(tokens) : ""}`
|
|
1961
|
+
);
|
|
1962
|
+
if (t.tool_names) {
|
|
1963
|
+
const tools = JSON.parse(t.tool_names);
|
|
1964
|
+
for (const tool of tools) {
|
|
1965
|
+
console.log(` ${c.cyan}\u25B8 ${tool}${c.r}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (t.message_preview) {
|
|
1969
|
+
const lines = t.message_preview.split("\n");
|
|
1970
|
+
for (const line of lines.slice(0, 10)) {
|
|
1971
|
+
console.log(` ${c.d}${line}${c.r}`);
|
|
1972
|
+
}
|
|
1973
|
+
if (lines.length > 10) {
|
|
1974
|
+
console.log(
|
|
1975
|
+
` ${c.d}... (${lines.length - 10} more lines)${c.r}`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
console.log("");
|
|
1980
|
+
}
|
|
1981
|
+
} finally {
|
|
1982
|
+
db.close();
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
cmd.command("trace-stats").description("Show aggregate trace statistics").option("--json", "Output as JSON", false).action((options) => {
|
|
1986
|
+
const stats = getTraceStats();
|
|
1987
|
+
if (options.json) {
|
|
1988
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
console.log(`
|
|
1992
|
+
${c.b}Conductor Trace Stats${c.r}
|
|
1993
|
+
`);
|
|
1994
|
+
console.log(` Sessions: ${stats.total_sessions}`);
|
|
1995
|
+
console.log(` Turns: ${stats.total_turns}`);
|
|
1996
|
+
console.log(` Issues: ${stats.issues_traced}`);
|
|
1997
|
+
console.log(
|
|
1998
|
+
` Tokens: ${formatTokens((stats.total_input_tokens || 0) + (stats.total_output_tokens || 0))}`
|
|
1999
|
+
);
|
|
2000
|
+
console.log(
|
|
2001
|
+
` Input: ${formatTokens(stats.total_input_tokens || 0)}`
|
|
2002
|
+
);
|
|
2003
|
+
console.log(
|
|
2004
|
+
` Output: ${formatTokens(stats.total_output_tokens || 0)}`
|
|
2005
|
+
);
|
|
2006
|
+
console.log("");
|
|
2007
|
+
});
|
|
1624
2008
|
return cmd;
|
|
1625
2009
|
}
|
|
1626
2010
|
export {
|
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
} from "../../core/worktree/preflight.js";
|
|
31
31
|
import { ContextCapture } from "../../core/worktree/capture.js";
|
|
32
32
|
import { extractKeywords } from "../../core/utils/text.js";
|
|
33
|
+
import {
|
|
34
|
+
TraceCollector,
|
|
35
|
+
stringifyEventTruncated
|
|
36
|
+
} from "./conductor-traces.js";
|
|
33
37
|
function getOutcomesLogPath() {
|
|
34
38
|
return join(homedir(), ".stackmemory", "conductor", "outcomes.jsonl");
|
|
35
39
|
}
|
|
@@ -38,6 +42,44 @@ function logAgentOutcome(entry) {
|
|
|
38
42
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
43
|
appendFileSync(getOutcomesLogPath(), JSON.stringify(entry) + "\n");
|
|
40
44
|
}
|
|
45
|
+
function createPullRequest(opts) {
|
|
46
|
+
try {
|
|
47
|
+
execSync(`git push -u origin "${opts.branch}"`, {
|
|
48
|
+
cwd: opts.workspacePath,
|
|
49
|
+
stdio: "pipe",
|
|
50
|
+
timeout: 6e4
|
|
51
|
+
});
|
|
52
|
+
const prTitle = `feat(conductor): ${opts.issueId} \u2014 ${opts.title}`;
|
|
53
|
+
const prBody = [
|
|
54
|
+
"## Summary",
|
|
55
|
+
"",
|
|
56
|
+
`Automated PR from conductor agent for **${opts.issueId}**.`,
|
|
57
|
+
"",
|
|
58
|
+
`- **Files modified:** ${opts.filesModified}`,
|
|
59
|
+
`- **Tool calls:** ${opts.toolCalls}`,
|
|
60
|
+
"",
|
|
61
|
+
"_This PR was auto-created by StackMemory Conductor._"
|
|
62
|
+
].join("\n");
|
|
63
|
+
const result = execSync(
|
|
64
|
+
`gh pr create --base "${opts.baseBranch}" --head "${opts.branch}" --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}"`,
|
|
65
|
+
{
|
|
66
|
+
cwd: opts.workspacePath,
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
69
|
+
timeout: 3e4
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
const prUrl = result.trim();
|
|
73
|
+
logger.info("Created PR", { issueId: opts.issueId, prUrl });
|
|
74
|
+
return prUrl;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.warn("Failed to create PR (best-effort)", {
|
|
77
|
+
issueId: opts.issueId,
|
|
78
|
+
error: err.message
|
|
79
|
+
});
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
41
83
|
function getRetryStrategy(issue, outcomes) {
|
|
42
84
|
if (!outcomes) {
|
|
43
85
|
const logPath = getOutcomesLogPath();
|
|
@@ -771,6 +813,24 @@ class Conductor {
|
|
|
771
813
|
await this.runAgent(issue, run);
|
|
772
814
|
run.status = "completed";
|
|
773
815
|
this.completeCount++;
|
|
816
|
+
let prUrl;
|
|
817
|
+
if (this.config.autoPR !== false) {
|
|
818
|
+
const wsKey = this.sanitizeIdentifier(issue.identifier);
|
|
819
|
+
const branchName = `conductor/${wsKey}`;
|
|
820
|
+
const url = createPullRequest({
|
|
821
|
+
branch: branchName,
|
|
822
|
+
baseBranch: this.config.baseBranch,
|
|
823
|
+
issueId: issue.identifier,
|
|
824
|
+
title: issue.title,
|
|
825
|
+
filesModified: run.filesModified,
|
|
826
|
+
toolCalls: run.toolCalls,
|
|
827
|
+
workspacePath: run.workspacePath
|
|
828
|
+
});
|
|
829
|
+
if (url) {
|
|
830
|
+
prUrl = url;
|
|
831
|
+
console.log(`[${issue.identifier}] PR created: ${url}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
774
834
|
logAgentOutcome({
|
|
775
835
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
776
836
|
issue: issue.identifier,
|
|
@@ -782,7 +842,8 @@ class Conductor {
|
|
|
782
842
|
tokensUsed: run.tokensUsed,
|
|
783
843
|
durationMs: Date.now() - run.startedAt,
|
|
784
844
|
hasCommits: true,
|
|
785
|
-
labels: issue.labels.map((l) => l.name)
|
|
845
|
+
labels: issue.labels.map((l) => l.name),
|
|
846
|
+
prUrl
|
|
786
847
|
});
|
|
787
848
|
await this.runHook(
|
|
788
849
|
"after-run",
|
|
@@ -1215,6 +1276,17 @@ class Conductor {
|
|
|
1215
1276
|
run.logStream = logStream;
|
|
1216
1277
|
const tee = new TeeTransform(logStream);
|
|
1217
1278
|
proc.stdout.pipe(tee);
|
|
1279
|
+
let traceCollector;
|
|
1280
|
+
try {
|
|
1281
|
+
traceCollector = new TraceCollector({
|
|
1282
|
+
issueId: issue.identifier,
|
|
1283
|
+
attempt: run.attempt
|
|
1284
|
+
});
|
|
1285
|
+
} catch {
|
|
1286
|
+
logger.warn("Failed to initialize trace collector", {
|
|
1287
|
+
identifier: issue.identifier
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1218
1290
|
this.writeAgentStatus(issue.identifier, run);
|
|
1219
1291
|
let stderr = "";
|
|
1220
1292
|
let lastResultText = "";
|
|
@@ -1246,23 +1318,53 @@ class Conductor {
|
|
|
1246
1318
|
this.trackUsage(issue.identifier, msgUsage);
|
|
1247
1319
|
}
|
|
1248
1320
|
const content = message?.content || [];
|
|
1321
|
+
const turnToolNames = [];
|
|
1322
|
+
let turnFilesModified = 0;
|
|
1323
|
+
const turnTextParts = [];
|
|
1249
1324
|
for (const block of content) {
|
|
1250
1325
|
if (block.type === "tool_use") {
|
|
1251
1326
|
run.toolCalls++;
|
|
1252
|
-
const
|
|
1327
|
+
const name = block.name || "";
|
|
1328
|
+
turnToolNames.push(name);
|
|
1329
|
+
const toolLower = name.toLowerCase();
|
|
1253
1330
|
if (toolLower.includes("edit") || toolLower.includes("write")) {
|
|
1254
1331
|
run.filesModified++;
|
|
1332
|
+
turnFilesModified++;
|
|
1255
1333
|
}
|
|
1256
1334
|
}
|
|
1257
1335
|
if (block.type === "text" && block.text) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1336
|
+
const text = block.text;
|
|
1337
|
+
run.tokensUsed += Math.ceil(text.length / 4);
|
|
1338
|
+
turnTextParts.push(text);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
if (traceCollector) {
|
|
1343
|
+
const turnData = {
|
|
1344
|
+
toolNames: turnToolNames,
|
|
1345
|
+
toolCount: turnToolNames.length,
|
|
1346
|
+
filesModified: turnFilesModified,
|
|
1347
|
+
textPreview: turnTextParts.length > 0 ? turnTextParts.join("\n").slice(0, 500) : null,
|
|
1348
|
+
inputTokens: msgUsage?.input_tokens ?? 0,
|
|
1349
|
+
outputTokens: msgUsage?.output_tokens ?? 0,
|
|
1350
|
+
cacheCreationTokens: msgUsage?.cache_creation_input_tokens ?? 0,
|
|
1351
|
+
cacheReadTokens: msgUsage?.cache_read_input_tokens ?? 0
|
|
1352
|
+
};
|
|
1353
|
+
traceCollector.recordTurn(
|
|
1354
|
+
turnData,
|
|
1355
|
+
phase,
|
|
1356
|
+
stringifyEventTruncated(event)
|
|
1260
1357
|
);
|
|
1261
1358
|
}
|
|
1359
|
+
} catch {
|
|
1262
1360
|
}
|
|
1263
1361
|
}
|
|
1264
1362
|
if (event.type === "result" && event.result) {
|
|
1265
1363
|
lastResultText = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
1364
|
+
try {
|
|
1365
|
+
traceCollector?.recordResult(event);
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1266
1368
|
}
|
|
1267
1369
|
if (run.toolCalls % 5 === 0 || phase) {
|
|
1268
1370
|
this.writeAgentStatus(issue.identifier, run);
|
|
@@ -1280,10 +1382,12 @@ class Conductor {
|
|
|
1280
1382
|
});
|
|
1281
1383
|
proc.on("error", (err) => {
|
|
1282
1384
|
clearTimeout(timer);
|
|
1385
|
+
traceCollector?.close();
|
|
1283
1386
|
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
1284
1387
|
});
|
|
1285
1388
|
proc.on("close", (code) => {
|
|
1286
1389
|
clearTimeout(timer);
|
|
1390
|
+
traceCollector?.close();
|
|
1287
1391
|
run.process = null;
|
|
1288
1392
|
if (run.logStream && !run.logStream.destroyed) {
|
|
1289
1393
|
run.logStream.end();
|
|
@@ -1714,6 +1818,24 @@ class Conductor {
|
|
|
1714
1818
|
} catch {
|
|
1715
1819
|
}
|
|
1716
1820
|
}
|
|
1821
|
+
let prUrl;
|
|
1822
|
+
if (hasCommits && this.config.autoPR !== false) {
|
|
1823
|
+
const wsKey = this.sanitizeIdentifier(run.issue.identifier);
|
|
1824
|
+
const branchName = `conductor/${wsKey}`;
|
|
1825
|
+
const url = createPullRequest({
|
|
1826
|
+
branch: branchName,
|
|
1827
|
+
baseBranch: this.config.baseBranch,
|
|
1828
|
+
issueId: run.issue.identifier,
|
|
1829
|
+
title: run.issue.title,
|
|
1830
|
+
filesModified: run.filesModified,
|
|
1831
|
+
toolCalls: run.toolCalls,
|
|
1832
|
+
workspacePath: wsPath
|
|
1833
|
+
});
|
|
1834
|
+
if (url) {
|
|
1835
|
+
prUrl = url;
|
|
1836
|
+
console.log(`[${run.issue.identifier}] PR created: ${url}`);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1717
1839
|
const durationMs = Date.now() - run.startedAt;
|
|
1718
1840
|
logAgentOutcome({
|
|
1719
1841
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1727,7 +1849,8 @@ class Conductor {
|
|
|
1727
1849
|
durationMs,
|
|
1728
1850
|
hasCommits,
|
|
1729
1851
|
labels: run.issue.labels.map((l) => l.name),
|
|
1730
|
-
errorTail
|
|
1852
|
+
errorTail,
|
|
1853
|
+
prUrl
|
|
1731
1854
|
});
|
|
1732
1855
|
if (hasCommits) {
|
|
1733
1856
|
logger.info("Stale agent had commits, transitioning to In Review", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
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",
|