@stackmemoryai/stackmemory 1.6.0 → 1.6.1
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/orchestrate.js +249 -2
- package/dist/src/cli/commands/orchestrator.js +206 -23
- package/package.json +1 -2
- package/scripts/gepa/.before-optimize.md +0 -112
- package/scripts/gepa/README.md +0 -275
- package/scripts/gepa/config.json +0 -59
- package/scripts/gepa/evals/coding-tasks.jsonl +0 -5
- package/scripts/gepa/evals/fixtures/api-endpoint.ts +0 -31
- package/scripts/gepa/evals/fixtures/brittle-integration.ts +0 -38
- package/scripts/gepa/evals/fixtures/buggy-loop.js +0 -18
- package/scripts/gepa/evals/fixtures/callback-hell.js +0 -53
- package/scripts/gepa/evals/fixtures/fts5-triggers.sql +0 -23
- package/scripts/gepa/evals/fixtures/leaky-service.ts +0 -70
- package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +0 -39
- package/scripts/gepa/evals/fixtures/pr-diff.txt +0 -24
- package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +0 -34
- package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +0 -42
- package/scripts/gepa/evals/stackmemory-tasks.jsonl +0 -8
- package/scripts/gepa/generations/gen-000/baseline.md +0 -112
- package/scripts/gepa/generations/gen-001/baseline.md +0 -112
- package/scripts/gepa/generations/gen-001/variant-a.md +0 -107
- package/scripts/gepa/generations/gen-001/variant-b.md +0 -216
- package/scripts/gepa/generations/gen-001/variant-c.md +0 -83
- package/scripts/gepa/generations/gen-001/variant-d.md +0 -90
- package/scripts/gepa/hooks/auto-optimize.js +0 -494
- package/scripts/gepa/hooks/eval-tracker.js +0 -203
- package/scripts/gepa/hooks/reflect.js +0 -350
- package/scripts/gepa/optimize.js +0 -853
- package/scripts/gepa/results/eval-1-baseline.json +0 -218
- package/scripts/gepa/results/eval-1-variant-a.json +0 -218
- package/scripts/gepa/results/eval-1-variant-b.json +0 -218
- package/scripts/gepa/results/eval-1-variant-c.json +0 -218
- package/scripts/gepa/results/eval-1-variant-d.json +0 -218
- package/scripts/gepa/state.json +0 -49
|
@@ -275,6 +275,94 @@ function ensureDefaultPromptTemplate() {
|
|
|
275
275
|
}
|
|
276
276
|
return templatePath;
|
|
277
277
|
}
|
|
278
|
+
function predictDifficulty(labels, description, priority, outcomes) {
|
|
279
|
+
let difficulty = "medium";
|
|
280
|
+
let confidence = 0.5;
|
|
281
|
+
const reasons = [];
|
|
282
|
+
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
283
|
+
if (outcomes.length > 0 && labels.length > 0) {
|
|
284
|
+
const matching = outcomes.filter(
|
|
285
|
+
(o) => o.labels && o.labels.some((ol) => lowerLabels.includes(ol.toLowerCase()))
|
|
286
|
+
);
|
|
287
|
+
if (matching.length >= 3) {
|
|
288
|
+
const failRate = matching.filter((o) => o.outcome === "failure").length / matching.length;
|
|
289
|
+
if (failRate > 0.6) {
|
|
290
|
+
difficulty = "hard";
|
|
291
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
292
|
+
reasons.push(
|
|
293
|
+
`Historical failure rate ${Math.round(failRate * 100)}% for similar labels`
|
|
294
|
+
);
|
|
295
|
+
} else if (failRate < 0.2) {
|
|
296
|
+
difficulty = "easy";
|
|
297
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
298
|
+
reasons.push(
|
|
299
|
+
`Historical failure rate ${Math.round(failRate * 100)}% for similar labels`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const hasBugOrFix = lowerLabels.some(
|
|
305
|
+
(l) => l === "bug" || l === "fix" || l.includes("bugfix")
|
|
306
|
+
);
|
|
307
|
+
if (description.length < 100 && hasBugOrFix) {
|
|
308
|
+
if (difficulty !== "easy") difficulty = "easy";
|
|
309
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
310
|
+
reasons.push("Short description with bug/fix label suggests simple fix");
|
|
311
|
+
}
|
|
312
|
+
const hasComplexLabel = lowerLabels.some(
|
|
313
|
+
(l) => l === "feature" || l === "refactor" || l === "refactoring" || l === "architecture"
|
|
314
|
+
);
|
|
315
|
+
if (description.length > 500 || hasComplexLabel) {
|
|
316
|
+
if (difficulty !== "hard") {
|
|
317
|
+
difficulty = description.length > 500 && hasComplexLabel ? "hard" : difficulty === "easy" ? "medium" : "hard";
|
|
318
|
+
}
|
|
319
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
320
|
+
if (description.length > 500) {
|
|
321
|
+
reasons.push(
|
|
322
|
+
`Long description (${description.length} chars) suggests complexity`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (hasComplexLabel) {
|
|
326
|
+
reasons.push("Feature/refactor label suggests higher complexity");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (priority === 1 || priority === 2) {
|
|
330
|
+
if (difficulty === "easy") difficulty = "medium";
|
|
331
|
+
else if (difficulty === "medium") difficulty = "hard";
|
|
332
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
333
|
+
reasons.push(
|
|
334
|
+
`Priority ${priority} (${priority === 1 ? "urgent" : "high"}) \u2014 higher difficulty expected`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (outcomes.length > 0 && labels.length > 0) {
|
|
338
|
+
const matching = outcomes.filter(
|
|
339
|
+
(o) => o.labels && o.labels.some((ol) => lowerLabels.includes(ol.toLowerCase()))
|
|
340
|
+
);
|
|
341
|
+
if (matching.length >= 3) {
|
|
342
|
+
const avgToolCalls = matching.reduce((s, o) => s + o.toolCalls, 0) / matching.length;
|
|
343
|
+
if (avgToolCalls > 80) {
|
|
344
|
+
difficulty = "hard";
|
|
345
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
346
|
+
reasons.push(
|
|
347
|
+
`Historical avg tool calls ${Math.round(avgToolCalls)} for similar labels (>80)`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (reasons.length === 0) {
|
|
353
|
+
reasons.push("No strong signals \u2014 defaulting to medium");
|
|
354
|
+
}
|
|
355
|
+
return { difficulty, confidence, reasons };
|
|
356
|
+
}
|
|
357
|
+
function loadOutcomes() {
|
|
358
|
+
const logPath = getOutcomesLogPath();
|
|
359
|
+
if (!existsSync(logPath)) return [];
|
|
360
|
+
try {
|
|
361
|
+
return readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
362
|
+
} catch {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
278
366
|
function spawnClaudePrint(prompt, timeoutMs = 12e4) {
|
|
279
367
|
return new Promise((resolve, reject) => {
|
|
280
368
|
const child = cpSpawn("claude", ["--print"], {
|
|
@@ -825,6 +913,10 @@ function createConductorCommands() {
|
|
|
825
913
|
"--evolve",
|
|
826
914
|
"Auto-mutate prompt template using GEPA-style evolution from failure data",
|
|
827
915
|
false
|
|
916
|
+
).option(
|
|
917
|
+
"--predict",
|
|
918
|
+
"Show difficulty predictions alongside actual outcomes",
|
|
919
|
+
false
|
|
828
920
|
).action(async (options) => {
|
|
829
921
|
const logPath = getOutcomesLogPath();
|
|
830
922
|
if (!existsSync(logPath)) {
|
|
@@ -1018,8 +1110,157 @@ function createConductorCommands() {
|
|
|
1018
1110
|
outcomes
|
|
1019
1111
|
});
|
|
1020
1112
|
}
|
|
1113
|
+
if (options.predict) {
|
|
1114
|
+
console.log(
|
|
1115
|
+
`
|
|
1116
|
+
${c.b}${c.purple}Difficulty Predictions vs Actual${c.r}
|
|
1117
|
+
`
|
|
1118
|
+
);
|
|
1119
|
+
const byIssue = /* @__PURE__ */ new Map();
|
|
1120
|
+
for (const o of outcomes) {
|
|
1121
|
+
byIssue.set(o.issue, o);
|
|
1122
|
+
}
|
|
1123
|
+
const difficultyColor = {
|
|
1124
|
+
easy: c.green,
|
|
1125
|
+
medium: c.yellow,
|
|
1126
|
+
hard: c.red
|
|
1127
|
+
};
|
|
1128
|
+
for (const [issue, outcome] of byIssue) {
|
|
1129
|
+
const issueLabels = outcome.labels || [];
|
|
1130
|
+
const otherOutcomes = outcomes.filter((o) => o.issue !== issue);
|
|
1131
|
+
const pred = predictDifficulty(
|
|
1132
|
+
issueLabels,
|
|
1133
|
+
"",
|
|
1134
|
+
// no description in outcome data
|
|
1135
|
+
0,
|
|
1136
|
+
// no priority in outcome data
|
|
1137
|
+
otherOutcomes
|
|
1138
|
+
);
|
|
1139
|
+
const actualDifficulty = outcome.outcome === "success" && outcome.toolCalls < 40 ? "easy" : outcome.outcome === "failure" || outcome.toolCalls > 80 ? "hard" : "medium";
|
|
1140
|
+
const match = pred.difficulty === actualDifficulty;
|
|
1141
|
+
const matchIcon = match ? `${c.green}\u2713${c.r}` : `${c.red}\u2717${c.r}`;
|
|
1142
|
+
console.log(
|
|
1143
|
+
` ${matchIcon} ${c.white}${issue.padEnd(12)}${c.r} predicted: ${difficultyColor[pred.difficulty]}${pred.difficulty.padEnd(6)}${c.r} actual: ${difficultyColor[actualDifficulty]}${actualDifficulty.padEnd(6)}${c.r} ${c.gray}(${Math.round(pred.confidence * 100)}% conf)${c.r}`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
const issueList = [...byIssue.values()];
|
|
1147
|
+
const correct = issueList.filter((o) => {
|
|
1148
|
+
const otherOutcomes = outcomes.filter((oo) => oo.issue !== o.issue);
|
|
1149
|
+
const pred = predictDifficulty(o.labels || [], "", 0, otherOutcomes);
|
|
1150
|
+
const actual = o.outcome === "success" && o.toolCalls < 40 ? "easy" : o.outcome === "failure" || o.toolCalls > 80 ? "hard" : "medium";
|
|
1151
|
+
return pred.difficulty === actual;
|
|
1152
|
+
}).length;
|
|
1153
|
+
const accuracy = Math.round(correct / issueList.length * 100);
|
|
1154
|
+
console.log(
|
|
1155
|
+
`
|
|
1156
|
+
${c.b}Accuracy${c.r}: ${accuracy}% (${correct}/${issueList.length})`
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1021
1159
|
console.log("");
|
|
1022
1160
|
});
|
|
1161
|
+
cmd.command("predict [issue-id]").description("Predict difficulty for an issue based on historical outcomes").option("--title <title>", "Issue title (for testing without Linear)").option(
|
|
1162
|
+
"--labels <labels>",
|
|
1163
|
+
"Comma-separated labels (for testing without Linear)"
|
|
1164
|
+
).option("--priority <n>", "Priority 0-4 (for testing without Linear)", "0").option("--json", "Output as JSON", false).action(async (issueId, options) => {
|
|
1165
|
+
let title = options.title || "";
|
|
1166
|
+
let labels = options.labels ? options.labels.split(",").map((l) => l.trim()) : [];
|
|
1167
|
+
let description = "";
|
|
1168
|
+
let priority = parseInt(options.priority, 10) || 0;
|
|
1169
|
+
if (issueId && !options.title && !options.labels) {
|
|
1170
|
+
try {
|
|
1171
|
+
const { LinearClient } = await import("../../integrations/linear/client.js");
|
|
1172
|
+
const { LinearAuthManager } = await import("../../integrations/linear/auth.js");
|
|
1173
|
+
let client;
|
|
1174
|
+
try {
|
|
1175
|
+
const authManager = new LinearAuthManager(process.cwd());
|
|
1176
|
+
const token = await authManager.getValidToken();
|
|
1177
|
+
client = new LinearClient({ apiKey: token, useBearer: true });
|
|
1178
|
+
} catch {
|
|
1179
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
1180
|
+
if (!apiKey) {
|
|
1181
|
+
console.log(
|
|
1182
|
+
`${c.red}No Linear auth found.${c.r} Use --title/--labels/--priority for testing.`
|
|
1183
|
+
);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
client = new LinearClient({ apiKey });
|
|
1187
|
+
}
|
|
1188
|
+
const issue = await client.getIssue(issueId);
|
|
1189
|
+
if (!issue) {
|
|
1190
|
+
console.log(`${c.red}Issue ${issueId} not found.${c.r}`);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
title = issue.title;
|
|
1194
|
+
description = issue.description || "";
|
|
1195
|
+
labels = issue.labels.map((l) => l.name);
|
|
1196
|
+
priority = issue.priority;
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
console.log(
|
|
1199
|
+
`${c.red}Failed to fetch issue:${c.r} ${err.message}`
|
|
1200
|
+
);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (!issueId && !options.title) {
|
|
1205
|
+
console.log(
|
|
1206
|
+
`${c.yellow}Provide an issue ID or --title/--labels for testing.${c.r}`
|
|
1207
|
+
);
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const outcomes = loadOutcomes();
|
|
1211
|
+
const pred = predictDifficulty(labels, description, priority, outcomes);
|
|
1212
|
+
if (options.json) {
|
|
1213
|
+
console.log(
|
|
1214
|
+
JSON.stringify(
|
|
1215
|
+
{
|
|
1216
|
+
issueId: issueId || "inline",
|
|
1217
|
+
title,
|
|
1218
|
+
labels,
|
|
1219
|
+
priority,
|
|
1220
|
+
...pred
|
|
1221
|
+
},
|
|
1222
|
+
null,
|
|
1223
|
+
2
|
|
1224
|
+
)
|
|
1225
|
+
);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const difficultyColor = {
|
|
1229
|
+
easy: c.green,
|
|
1230
|
+
medium: c.yellow,
|
|
1231
|
+
hard: c.red
|
|
1232
|
+
};
|
|
1233
|
+
console.log(`
|
|
1234
|
+
${c.b}${c.purple}Difficulty Prediction${c.r}
|
|
1235
|
+
`);
|
|
1236
|
+
if (title) {
|
|
1237
|
+
console.log(` ${c.b}Issue${c.r} ${issueId || "inline"}`);
|
|
1238
|
+
console.log(` ${c.b}Title${c.r} ${title}`);
|
|
1239
|
+
}
|
|
1240
|
+
if (labels.length > 0) {
|
|
1241
|
+
console.log(` ${c.b}Labels${c.r} ${labels.join(", ")}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (priority > 0) {
|
|
1244
|
+
console.log(` ${c.b}Priority${c.r} ${priority}`);
|
|
1245
|
+
}
|
|
1246
|
+
console.log(
|
|
1247
|
+
`
|
|
1248
|
+
${c.b}Difficulty${c.r} ${difficultyColor[pred.difficulty]}${pred.difficulty.toUpperCase()}${c.r}`
|
|
1249
|
+
);
|
|
1250
|
+
console.log(
|
|
1251
|
+
` ${c.b}Confidence${c.r} ${Math.round(pred.confidence * 100)}%`
|
|
1252
|
+
);
|
|
1253
|
+
console.log(`
|
|
1254
|
+
${c.b}Signals${c.r}`);
|
|
1255
|
+
for (const reason of pred.reasons) {
|
|
1256
|
+
console.log(` ${c.cyan}\u2192${c.r} ${reason}`);
|
|
1257
|
+
}
|
|
1258
|
+
console.log(
|
|
1259
|
+
`
|
|
1260
|
+
${c.d}Based on ${outcomes.length} historical outcomes${c.r}
|
|
1261
|
+
`
|
|
1262
|
+
);
|
|
1263
|
+
});
|
|
1023
1264
|
cmd.command("usage").description("Show token usage, budget, and time-to-exhaustion").option("--json", "Output as JSON", false).option("--scan", "Scan Claude Code JSONL logs for historical data", false).action(async (options) => {
|
|
1024
1265
|
const statusPath = join(
|
|
1025
1266
|
process.cwd(),
|
|
@@ -1084,6 +1325,10 @@ function createConductorCommands() {
|
|
|
1084
1325
|
"--mode <mode>",
|
|
1085
1326
|
'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
|
|
1086
1327
|
"cli"
|
|
1328
|
+
).option(
|
|
1329
|
+
"--model <model>",
|
|
1330
|
+
'Model routing: "auto" (complexity-based) or a specific model ID',
|
|
1331
|
+
"auto"
|
|
1087
1332
|
).action(async (options) => {
|
|
1088
1333
|
ensureDefaultPromptTemplate();
|
|
1089
1334
|
const conductor = new Conductor({
|
|
@@ -1098,7 +1343,8 @@ function createConductorCommands() {
|
|
|
1098
1343
|
baseBranch: options.branch,
|
|
1099
1344
|
maxRetries: parseInt(options.retries, 10),
|
|
1100
1345
|
turnTimeoutMs: parseInt(options.turnTimeout, 10),
|
|
1101
|
-
agentMode: options.mode === "adapter" ? "adapter" : "cli"
|
|
1346
|
+
agentMode: options.mode === "adapter" ? "adapter" : "cli",
|
|
1347
|
+
model: options.model
|
|
1102
1348
|
});
|
|
1103
1349
|
await conductor.start();
|
|
1104
1350
|
});
|
|
@@ -1379,5 +1625,6 @@ function createConductorCommands() {
|
|
|
1379
1625
|
}
|
|
1380
1626
|
export {
|
|
1381
1627
|
createConductorCommands,
|
|
1382
|
-
formatElapsed
|
|
1628
|
+
formatElapsed,
|
|
1629
|
+
predictDifficulty
|
|
1383
1630
|
};
|
|
@@ -38,6 +38,86 @@ function logAgentOutcome(entry) {
|
|
|
38
38
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
39
|
appendFileSync(getOutcomesLogPath(), JSON.stringify(entry) + "\n");
|
|
40
40
|
}
|
|
41
|
+
function getRetryStrategy(issue, outcomes) {
|
|
42
|
+
if (!outcomes) {
|
|
43
|
+
const logPath = getOutcomesLogPath();
|
|
44
|
+
if (!existsSync(logPath)) {
|
|
45
|
+
return { shouldRetry: true, adjustments: [] };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
49
|
+
outcomes = lines.map((l) => JSON.parse(l));
|
|
50
|
+
} catch {
|
|
51
|
+
return { shouldRetry: true, adjustments: [] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const issueOutcomes = outcomes.filter((o) => o.issue === issue);
|
|
55
|
+
const relevant = issueOutcomes.length > 0 ? issueOutcomes : outcomes;
|
|
56
|
+
const failures = relevant.filter(
|
|
57
|
+
(o) => o.outcome === "failure" || o.outcome === "partial"
|
|
58
|
+
);
|
|
59
|
+
if (failures.length === 0) {
|
|
60
|
+
return { shouldRetry: true, adjustments: [] };
|
|
61
|
+
}
|
|
62
|
+
const lastFailure = failures[failures.length - 1];
|
|
63
|
+
if (lastFailure.errorTail && /429|rate.?limit|too many requests|usage.?limit|overloaded/i.test(
|
|
64
|
+
lastFailure.errorTail
|
|
65
|
+
)) {
|
|
66
|
+
return {
|
|
67
|
+
shouldRetry: false,
|
|
68
|
+
reason: "Last failure was a rate limit (429) \u2014 retrying immediately will not help",
|
|
69
|
+
adjustments: []
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (issueOutcomes.length > 0) {
|
|
73
|
+
const issueFailures = issueOutcomes.filter(
|
|
74
|
+
(o) => o.outcome === "failure" || o.outcome === "partial"
|
|
75
|
+
);
|
|
76
|
+
const phaseFailCounts = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const f of issueFailures) {
|
|
78
|
+
phaseFailCounts.set(f.phase, (phaseFailCounts.get(f.phase) || 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
for (const [phase, count] of phaseFailCounts) {
|
|
81
|
+
if (count >= 2) {
|
|
82
|
+
return {
|
|
83
|
+
shouldRetry: false,
|
|
84
|
+
reason: `Issue has failed ${count} times in '${phase}' phase \u2014 likely a structural problem`,
|
|
85
|
+
adjustments: []
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const adjustments = [];
|
|
91
|
+
if (lastFailure.errorTail) {
|
|
92
|
+
const tail = lastFailure.errorTail;
|
|
93
|
+
if (/timeout|timed?.?out|ETIMEDOUT|ESOCKETTIMEDOUT/i.test(tail)) {
|
|
94
|
+
adjustments.push(
|
|
95
|
+
"Previous attempt timed out. Work in smaller increments and commit partial progress early."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (/lint|eslint|prettier|formatting|style.?error/i.test(tail)) {
|
|
99
|
+
adjustments.push(
|
|
100
|
+
"Previous attempt failed on linting. Run `npm run lint:fix` after each file change and fix all lint errors before committing."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (/test.?fail|assertion|expect|vitest|jest|FAIL/i.test(tail)) {
|
|
104
|
+
adjustments.push(
|
|
105
|
+
"Previous attempt failed tests. Run tests incrementally after each change. Check existing test expectations before modifying code."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (/build.?fail|tsc|type.?error|TS\d{4}|compile/i.test(tail)) {
|
|
109
|
+
adjustments.push(
|
|
110
|
+
"Previous attempt had build/type errors. Run `npm run build` after changes and fix type errors before committing."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (/cannot find module|ERR_MODULE_NOT_FOUND|import/i.test(tail)) {
|
|
114
|
+
adjustments.push(
|
|
115
|
+
"Previous attempt had module resolution errors. Ensure all imports use .js extensions for relative paths (ESM)."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { shouldRetry: true, adjustments };
|
|
120
|
+
}
|
|
41
121
|
function findPackageRoot() {
|
|
42
122
|
const currentFile = fileURLToPath(import.meta.url);
|
|
43
123
|
let dir = dirname(currentFile);
|
|
@@ -136,6 +216,52 @@ function inferPhase(msg) {
|
|
|
136
216
|
}
|
|
137
217
|
return null;
|
|
138
218
|
}
|
|
219
|
+
const SIMPLE_LABELS = ["bug", "fix", "typo", "chore", "docs", "hotfix"];
|
|
220
|
+
const COMPLEX_LABELS = [
|
|
221
|
+
"feature",
|
|
222
|
+
"refactor",
|
|
223
|
+
"architecture",
|
|
224
|
+
"migration",
|
|
225
|
+
"security",
|
|
226
|
+
"performance"
|
|
227
|
+
];
|
|
228
|
+
function estimateIssueComplexity(issue, attempt) {
|
|
229
|
+
let score = 0;
|
|
230
|
+
const descLen = (issue.description || "").length;
|
|
231
|
+
if (descLen > 800) score += 2;
|
|
232
|
+
else if (descLen > 400) score += 1;
|
|
233
|
+
else if (descLen < 200) score -= 1;
|
|
234
|
+
const labelNames = (issue.labels || []).map((l) => l.name.toLowerCase());
|
|
235
|
+
for (const label of labelNames) {
|
|
236
|
+
if (SIMPLE_LABELS.some((s) => label.includes(s))) score -= 1;
|
|
237
|
+
if (COMPLEX_LABELS.some((c) => label.includes(c))) score += 2;
|
|
238
|
+
}
|
|
239
|
+
const titleLower = issue.title.toLowerCase();
|
|
240
|
+
if (SIMPLE_LABELS.some((s) => titleLower.includes(s))) score -= 1;
|
|
241
|
+
if (COMPLEX_LABELS.some((c) => titleLower.includes(c))) score += 1;
|
|
242
|
+
if (issue.priority === 1) score += 2;
|
|
243
|
+
else if (issue.priority === 2) score += 1;
|
|
244
|
+
if (issue.estimate) {
|
|
245
|
+
if (issue.estimate >= 5) score += 2;
|
|
246
|
+
else if (issue.estimate >= 3) score += 1;
|
|
247
|
+
else if (issue.estimate <= 1) score -= 1;
|
|
248
|
+
}
|
|
249
|
+
if (attempt && attempt > 1) score += 2;
|
|
250
|
+
if (score >= 3) return "complex";
|
|
251
|
+
if (score >= 1) return "moderate";
|
|
252
|
+
return "simple";
|
|
253
|
+
}
|
|
254
|
+
function selectModelForIssue(complexity, config) {
|
|
255
|
+
const modelSetting = config.model || "auto";
|
|
256
|
+
if (modelSetting !== "auto") return modelSetting;
|
|
257
|
+
switch (complexity) {
|
|
258
|
+
case "simple":
|
|
259
|
+
case "moderate":
|
|
260
|
+
return "claude-sonnet-4-20250514";
|
|
261
|
+
case "complex":
|
|
262
|
+
return "claude-opus-4-20250514";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
139
265
|
const DEFAULT_CONFIG = {
|
|
140
266
|
activeStates: ["Todo"],
|
|
141
267
|
terminalStates: ["Done", "Cancelled", "Canceled", "Closed"],
|
|
@@ -655,7 +781,8 @@ class Conductor {
|
|
|
655
781
|
filesModified: run.filesModified,
|
|
656
782
|
tokensUsed: run.tokensUsed,
|
|
657
783
|
durationMs: Date.now() - run.startedAt,
|
|
658
|
-
hasCommits: true
|
|
784
|
+
hasCommits: true,
|
|
785
|
+
labels: issue.labels.map((l) => l.name)
|
|
659
786
|
});
|
|
660
787
|
await this.runHook(
|
|
661
788
|
"after-run",
|
|
@@ -678,6 +805,20 @@ class Conductor {
|
|
|
678
805
|
error: run.error,
|
|
679
806
|
attempt: run.attempt
|
|
680
807
|
});
|
|
808
|
+
logAgentOutcome({
|
|
809
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
810
|
+
issue: issue.identifier,
|
|
811
|
+
attempt: run.attempt,
|
|
812
|
+
outcome: "failure",
|
|
813
|
+
phase: run.phase,
|
|
814
|
+
toolCalls: run.toolCalls,
|
|
815
|
+
filesModified: run.filesModified,
|
|
816
|
+
tokensUsed: run.tokensUsed,
|
|
817
|
+
durationMs: Date.now() - run.startedAt,
|
|
818
|
+
hasCommits: false,
|
|
819
|
+
labels: issue.labels.map((l) => l.name),
|
|
820
|
+
errorTail: run.error?.slice(-500)
|
|
821
|
+
});
|
|
681
822
|
if (this.handleRateLimitError(run.error, issue.identifier)) {
|
|
682
823
|
throw err;
|
|
683
824
|
}
|
|
@@ -691,6 +832,23 @@ class Conductor {
|
|
|
691
832
|
});
|
|
692
833
|
}
|
|
693
834
|
if (run.attempt < maxAttempts && !this.stopping) {
|
|
835
|
+
const strategy = getRetryStrategy(issue.identifier);
|
|
836
|
+
if (!strategy.shouldRetry) {
|
|
837
|
+
console.log(
|
|
838
|
+
`[${issue.identifier}] Skipping retry: ${strategy.reason}`
|
|
839
|
+
);
|
|
840
|
+
logger.info("Retry skipped by intelligence", {
|
|
841
|
+
identifier: issue.identifier,
|
|
842
|
+
reason: strategy.reason
|
|
843
|
+
});
|
|
844
|
+
throw err;
|
|
845
|
+
}
|
|
846
|
+
if (strategy.adjustments.length > 0) {
|
|
847
|
+
run.retryAdjustments = strategy.adjustments;
|
|
848
|
+
console.log(
|
|
849
|
+
`[${issue.identifier}] Retry with adjustments: ${strategy.adjustments.length} hint(s)`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
694
852
|
console.log(
|
|
695
853
|
`[${issue.identifier}] Failed (attempt ${run.attempt}), retrying...`
|
|
696
854
|
);
|
|
@@ -1024,25 +1182,33 @@ class Conductor {
|
|
|
1024
1182
|
*/
|
|
1025
1183
|
runAgentCLI(issue, run) {
|
|
1026
1184
|
return new Promise((resolve, reject) => {
|
|
1027
|
-
const prompt = this.buildPrompt(issue, run.attempt);
|
|
1185
|
+
const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
|
|
1186
|
+
const complexity = estimateIssueComplexity(issue, run.attempt);
|
|
1187
|
+
const selectedModel = selectModelForIssue(complexity, this.config);
|
|
1188
|
+
logger.info("Agent model selected", {
|
|
1189
|
+
identifier: issue.identifier,
|
|
1190
|
+
complexity,
|
|
1191
|
+
model: selectedModel || "(default)",
|
|
1192
|
+
reason: this.config.model && this.config.model !== "auto" ? "config override" : `auto: ${complexity} issue`
|
|
1193
|
+
});
|
|
1028
1194
|
const claudeBin = this.findClaudeBinary();
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
);
|
|
1195
|
+
const args = [
|
|
1196
|
+
"-p",
|
|
1197
|
+
"--output-format",
|
|
1198
|
+
"stream-json",
|
|
1199
|
+
"--dangerously-skip-permissions",
|
|
1200
|
+
"--settings",
|
|
1201
|
+
'{"hooks":{}}'
|
|
1202
|
+
];
|
|
1203
|
+
if (selectedModel) {
|
|
1204
|
+
args.push("--model", selectedModel);
|
|
1205
|
+
}
|
|
1206
|
+
args.push(prompt);
|
|
1207
|
+
const proc = spawn(claudeBin, args, {
|
|
1208
|
+
cwd: run.workspacePath,
|
|
1209
|
+
env: this.buildAgentEnv(issue, run),
|
|
1210
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1211
|
+
});
|
|
1046
1212
|
run.process = proc;
|
|
1047
1213
|
proc.stdin.end();
|
|
1048
1214
|
const logStream = this.openAgentLogStream(issue.identifier);
|
|
@@ -1146,7 +1312,7 @@ class Conductor {
|
|
|
1146
1312
|
*/
|
|
1147
1313
|
runAgentAdapter(issue, run) {
|
|
1148
1314
|
return new Promise((resolve, reject) => {
|
|
1149
|
-
const prompt = this.buildPrompt(issue, run.attempt);
|
|
1315
|
+
const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
|
|
1150
1316
|
const proc = spawn("node", [this.config.appServerPath], {
|
|
1151
1317
|
cwd: run.workspacePath,
|
|
1152
1318
|
env: this.buildAgentEnv(issue, run),
|
|
@@ -1301,7 +1467,7 @@ class Conductor {
|
|
|
1301
1467
|
* Template variables: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}},
|
|
1302
1468
|
* {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
|
|
1303
1469
|
*/
|
|
1304
|
-
buildPrompt(issue, attempt) {
|
|
1470
|
+
buildPrompt(issue, attempt, retryAdjustments) {
|
|
1305
1471
|
const templatePath = join(
|
|
1306
1472
|
homedir(),
|
|
1307
1473
|
".stackmemory",
|
|
@@ -1310,7 +1476,20 @@ class Conductor {
|
|
|
1310
1476
|
);
|
|
1311
1477
|
const priority = ["None", "Urgent", "High", "Medium", "Low"][issue.priority] || "None";
|
|
1312
1478
|
const labels = issue.labels.length > 0 ? issue.labels.map((l) => l.name).join(", ") : "";
|
|
1313
|
-
const
|
|
1479
|
+
const contextParts = [];
|
|
1480
|
+
if (attempt > 1) {
|
|
1481
|
+
contextParts.push(
|
|
1482
|
+
`This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.`
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
if (retryAdjustments && retryAdjustments.length > 0) {
|
|
1486
|
+
contextParts.push(
|
|
1487
|
+
"## Retry Adjustments (from prior failure analysis)",
|
|
1488
|
+
"",
|
|
1489
|
+
...retryAdjustments.map((a) => `- ${a}`)
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
const priorContext = contextParts.join("\n");
|
|
1314
1493
|
if (existsSync(templatePath)) {
|
|
1315
1494
|
try {
|
|
1316
1495
|
let template = readFileSync(templatePath, "utf-8");
|
|
@@ -1547,6 +1726,7 @@ class Conductor {
|
|
|
1547
1726
|
tokensUsed: run.tokensUsed,
|
|
1548
1727
|
durationMs,
|
|
1549
1728
|
hasCommits,
|
|
1729
|
+
labels: run.issue.labels.map((l) => l.name),
|
|
1550
1730
|
errorTail
|
|
1551
1731
|
});
|
|
1552
1732
|
if (hasCommits) {
|
|
@@ -1631,6 +1811,9 @@ class Conductor {
|
|
|
1631
1811
|
}
|
|
1632
1812
|
export {
|
|
1633
1813
|
Conductor,
|
|
1814
|
+
estimateIssueComplexity,
|
|
1634
1815
|
getAgentStatusDir,
|
|
1635
|
-
getOutcomesLogPath
|
|
1816
|
+
getOutcomesLogPath,
|
|
1817
|
+
getRetryStrategy,
|
|
1818
|
+
selectModelForIssue
|
|
1636
1819
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.1",
|
|
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",
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"bin",
|
|
22
22
|
"dist/src",
|
|
23
|
-
"scripts/gepa",
|
|
24
23
|
"scripts/git-hooks",
|
|
25
24
|
"scripts/hooks",
|
|
26
25
|
"scripts/setup",
|