@stackmemoryai/stackmemory 1.6.0 → 1.6.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.
Files changed (34) hide show
  1. package/dist/src/cli/commands/orchestrate.js +334 -4
  2. package/dist/src/cli/commands/orchestrator.js +283 -24
  3. package/package.json +1 -2
  4. package/scripts/gepa/.before-optimize.md +0 -112
  5. package/scripts/gepa/README.md +0 -275
  6. package/scripts/gepa/config.json +0 -59
  7. package/scripts/gepa/evals/coding-tasks.jsonl +0 -5
  8. package/scripts/gepa/evals/fixtures/api-endpoint.ts +0 -31
  9. package/scripts/gepa/evals/fixtures/brittle-integration.ts +0 -38
  10. package/scripts/gepa/evals/fixtures/buggy-loop.js +0 -18
  11. package/scripts/gepa/evals/fixtures/callback-hell.js +0 -53
  12. package/scripts/gepa/evals/fixtures/fts5-triggers.sql +0 -23
  13. package/scripts/gepa/evals/fixtures/leaky-service.ts +0 -70
  14. package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +0 -39
  15. package/scripts/gepa/evals/fixtures/pr-diff.txt +0 -24
  16. package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +0 -34
  17. package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +0 -42
  18. package/scripts/gepa/evals/stackmemory-tasks.jsonl +0 -8
  19. package/scripts/gepa/generations/gen-000/baseline.md +0 -112
  20. package/scripts/gepa/generations/gen-001/baseline.md +0 -112
  21. package/scripts/gepa/generations/gen-001/variant-a.md +0 -107
  22. package/scripts/gepa/generations/gen-001/variant-b.md +0 -216
  23. package/scripts/gepa/generations/gen-001/variant-c.md +0 -83
  24. package/scripts/gepa/generations/gen-001/variant-d.md +0 -90
  25. package/scripts/gepa/hooks/auto-optimize.js +0 -494
  26. package/scripts/gepa/hooks/eval-tracker.js +0 -203
  27. package/scripts/gepa/hooks/reflect.js +0 -350
  28. package/scripts/gepa/optimize.js +0 -853
  29. package/scripts/gepa/results/eval-1-baseline.json +0 -218
  30. package/scripts/gepa/results/eval-1-variant-a.json +0 -218
  31. package/scripts/gepa/results/eval-1-variant-b.json +0 -218
  32. package/scripts/gepa/results/eval-1-variant-c.json +0 -218
  33. package/scripts/gepa/results/eval-1-variant-d.json +0 -218
  34. 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"], {
@@ -306,6 +394,68 @@ function spawnClaudePrint(prompt, timeoutMs = 12e4) {
306
394
  child.stdin.end();
307
395
  });
308
396
  }
397
+ function printSimpleDiff(oldText, newText) {
398
+ const oldLines = oldText.split("\n");
399
+ const newLines = newText.split("\n");
400
+ const oldSet = new Set(oldLines);
401
+ const newSet = new Set(newLines);
402
+ const removed = oldLines.filter((l) => !newSet.has(l));
403
+ const added = newLines.filter((l) => !oldSet.has(l));
404
+ const removedSet = new Set(removed);
405
+ const addedSet = new Set(added);
406
+ const CONTEXT = 2;
407
+ const changedOld = /* @__PURE__ */ new Set();
408
+ for (let i = 0; i < oldLines.length; i++) {
409
+ if (removedSet.has(oldLines[i])) changedOld.add(i);
410
+ }
411
+ const changedNew = /* @__PURE__ */ new Set();
412
+ for (let i = 0; i < newLines.length; i++) {
413
+ if (addedSet.has(newLines[i])) changedNew.add(i);
414
+ }
415
+ let lastPrinted = -1;
416
+ for (let i = 0; i < oldLines.length; i++) {
417
+ let nearChange = false;
418
+ for (let j = Math.max(0, i - CONTEXT); j <= Math.min(oldLines.length - 1, i + CONTEXT); j++) {
419
+ if (changedOld.has(j)) {
420
+ nearChange = true;
421
+ break;
422
+ }
423
+ }
424
+ if (!nearChange) continue;
425
+ if (lastPrinted >= 0 && i > lastPrinted + 1) {
426
+ console.log(` ${c.d}...${c.r}`);
427
+ }
428
+ if (removedSet.has(oldLines[i])) {
429
+ console.log(` ${c.red}- ${oldLines[i]}${c.r}`);
430
+ } else {
431
+ console.log(` ${oldLines[i]}`);
432
+ }
433
+ lastPrinted = i;
434
+ }
435
+ if (removed.length > 0 && added.length > 0) {
436
+ console.log(` ${c.d}---${c.r}`);
437
+ }
438
+ lastPrinted = -1;
439
+ for (let i = 0; i < newLines.length; i++) {
440
+ let nearChange = false;
441
+ for (let j = Math.max(0, i - CONTEXT); j <= Math.min(newLines.length - 1, i + CONTEXT); j++) {
442
+ if (changedNew.has(j)) {
443
+ nearChange = true;
444
+ break;
445
+ }
446
+ }
447
+ if (!nearChange) continue;
448
+ if (lastPrinted >= 0 && i > lastPrinted + 1) {
449
+ console.log(` ${c.d}...${c.r}`);
450
+ }
451
+ if (addedSet.has(newLines[i])) {
452
+ console.log(` ${c.green}+ ${newLines[i]}${c.r}`);
453
+ } else {
454
+ console.log(` ${newLines[i]}`);
455
+ }
456
+ lastPrinted = i;
457
+ }
458
+ }
309
459
  async function evolvePromptTemplate(input) {
310
460
  const {
311
461
  templatePath,
@@ -314,7 +464,8 @@ async function evolvePromptTemplate(input) {
314
464
  failPhases,
315
465
  errorPatterns,
316
466
  recs,
317
- outcomes
467
+ outcomes,
468
+ dryRun
318
469
  } = input;
319
470
  let currentTemplate;
320
471
  if (existsSync(templatePath)) {
@@ -387,6 +538,17 @@ OUTPUT THE IMPROVED TEMPLATE:`;
387
538
  );
388
539
  return;
389
540
  }
541
+ if (dryRun) {
542
+ console.log(`
543
+ ${c.b}${c.cyan}Dry-run diff:${c.r}
544
+ `);
545
+ printSimpleDiff(currentTemplate, evolved.trim());
546
+ console.log(
547
+ `
548
+ ${c.d}Dry run \u2014 no files modified. Run without --dry-run to apply.${c.r}`
549
+ );
550
+ return;
551
+ }
390
552
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
391
553
  const backupPath = templatePath.replace(".md", `.backup-${timestamp}.md`);
392
554
  copyFileSync(templatePath, backupPath);
@@ -825,6 +987,14 @@ function createConductorCommands() {
825
987
  "--evolve",
826
988
  "Auto-mutate prompt template using GEPA-style evolution from failure data",
827
989
  false
990
+ ).option(
991
+ "--dry-run",
992
+ "Show evolved template without writing (use with --evolve)",
993
+ false
994
+ ).option(
995
+ "--predict",
996
+ "Show difficulty predictions alongside actual outcomes",
997
+ false
828
998
  ).action(async (options) => {
829
999
  const logPath = getOutcomesLogPath();
830
1000
  if (!existsSync(logPath)) {
@@ -1015,11 +1185,161 @@ function createConductorCommands() {
1015
1185
  failPhases,
1016
1186
  errorPatterns,
1017
1187
  recs,
1018
- outcomes
1188
+ outcomes,
1189
+ dryRun: options.dryRun
1019
1190
  });
1020
1191
  }
1192
+ if (options.predict) {
1193
+ console.log(
1194
+ `
1195
+ ${c.b}${c.purple}Difficulty Predictions vs Actual${c.r}
1196
+ `
1197
+ );
1198
+ const byIssue = /* @__PURE__ */ new Map();
1199
+ for (const o of outcomes) {
1200
+ byIssue.set(o.issue, o);
1201
+ }
1202
+ const difficultyColor = {
1203
+ easy: c.green,
1204
+ medium: c.yellow,
1205
+ hard: c.red
1206
+ };
1207
+ for (const [issue, outcome] of byIssue) {
1208
+ const issueLabels = outcome.labels || [];
1209
+ const otherOutcomes = outcomes.filter((o) => o.issue !== issue);
1210
+ const pred = predictDifficulty(
1211
+ issueLabels,
1212
+ "",
1213
+ // no description in outcome data
1214
+ 0,
1215
+ // no priority in outcome data
1216
+ otherOutcomes
1217
+ );
1218
+ const actualDifficulty = outcome.outcome === "success" && outcome.toolCalls < 40 ? "easy" : outcome.outcome === "failure" || outcome.toolCalls > 80 ? "hard" : "medium";
1219
+ const match = pred.difficulty === actualDifficulty;
1220
+ const matchIcon = match ? `${c.green}\u2713${c.r}` : `${c.red}\u2717${c.r}`;
1221
+ console.log(
1222
+ ` ${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}`
1223
+ );
1224
+ }
1225
+ const issueList = [...byIssue.values()];
1226
+ const correct = issueList.filter((o) => {
1227
+ const otherOutcomes = outcomes.filter((oo) => oo.issue !== o.issue);
1228
+ const pred = predictDifficulty(o.labels || [], "", 0, otherOutcomes);
1229
+ const actual = o.outcome === "success" && o.toolCalls < 40 ? "easy" : o.outcome === "failure" || o.toolCalls > 80 ? "hard" : "medium";
1230
+ return pred.difficulty === actual;
1231
+ }).length;
1232
+ const accuracy = Math.round(correct / issueList.length * 100);
1233
+ console.log(
1234
+ `
1235
+ ${c.b}Accuracy${c.r}: ${accuracy}% (${correct}/${issueList.length})`
1236
+ );
1237
+ }
1021
1238
  console.log("");
1022
1239
  });
1240
+ 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(
1241
+ "--labels <labels>",
1242
+ "Comma-separated labels (for testing without Linear)"
1243
+ ).option("--priority <n>", "Priority 0-4 (for testing without Linear)", "0").option("--json", "Output as JSON", false).action(async (issueId, options) => {
1244
+ let title = options.title || "";
1245
+ let labels = options.labels ? options.labels.split(",").map((l) => l.trim()) : [];
1246
+ let description = "";
1247
+ let priority = parseInt(options.priority, 10) || 0;
1248
+ if (issueId && !options.title && !options.labels) {
1249
+ try {
1250
+ const { LinearClient } = await import("../../integrations/linear/client.js");
1251
+ const { LinearAuthManager } = await import("../../integrations/linear/auth.js");
1252
+ let client;
1253
+ try {
1254
+ const authManager = new LinearAuthManager(process.cwd());
1255
+ const token = await authManager.getValidToken();
1256
+ client = new LinearClient({ apiKey: token, useBearer: true });
1257
+ } catch {
1258
+ const apiKey = process.env.LINEAR_API_KEY;
1259
+ if (!apiKey) {
1260
+ console.log(
1261
+ `${c.red}No Linear auth found.${c.r} Use --title/--labels/--priority for testing.`
1262
+ );
1263
+ return;
1264
+ }
1265
+ client = new LinearClient({ apiKey });
1266
+ }
1267
+ const issue = await client.getIssue(issueId);
1268
+ if (!issue) {
1269
+ console.log(`${c.red}Issue ${issueId} not found.${c.r}`);
1270
+ return;
1271
+ }
1272
+ title = issue.title;
1273
+ description = issue.description || "";
1274
+ labels = issue.labels.map((l) => l.name);
1275
+ priority = issue.priority;
1276
+ } catch (err) {
1277
+ console.log(
1278
+ `${c.red}Failed to fetch issue:${c.r} ${err.message}`
1279
+ );
1280
+ return;
1281
+ }
1282
+ }
1283
+ if (!issueId && !options.title) {
1284
+ console.log(
1285
+ `${c.yellow}Provide an issue ID or --title/--labels for testing.${c.r}`
1286
+ );
1287
+ return;
1288
+ }
1289
+ const outcomes = loadOutcomes();
1290
+ const pred = predictDifficulty(labels, description, priority, outcomes);
1291
+ if (options.json) {
1292
+ console.log(
1293
+ JSON.stringify(
1294
+ {
1295
+ issueId: issueId || "inline",
1296
+ title,
1297
+ labels,
1298
+ priority,
1299
+ ...pred
1300
+ },
1301
+ null,
1302
+ 2
1303
+ )
1304
+ );
1305
+ return;
1306
+ }
1307
+ const difficultyColor = {
1308
+ easy: c.green,
1309
+ medium: c.yellow,
1310
+ hard: c.red
1311
+ };
1312
+ console.log(`
1313
+ ${c.b}${c.purple}Difficulty Prediction${c.r}
1314
+ `);
1315
+ if (title) {
1316
+ console.log(` ${c.b}Issue${c.r} ${issueId || "inline"}`);
1317
+ console.log(` ${c.b}Title${c.r} ${title}`);
1318
+ }
1319
+ if (labels.length > 0) {
1320
+ console.log(` ${c.b}Labels${c.r} ${labels.join(", ")}`);
1321
+ }
1322
+ if (priority > 0) {
1323
+ console.log(` ${c.b}Priority${c.r} ${priority}`);
1324
+ }
1325
+ console.log(
1326
+ `
1327
+ ${c.b}Difficulty${c.r} ${difficultyColor[pred.difficulty]}${pred.difficulty.toUpperCase()}${c.r}`
1328
+ );
1329
+ console.log(
1330
+ ` ${c.b}Confidence${c.r} ${Math.round(pred.confidence * 100)}%`
1331
+ );
1332
+ console.log(`
1333
+ ${c.b}Signals${c.r}`);
1334
+ for (const reason of pred.reasons) {
1335
+ console.log(` ${c.cyan}\u2192${c.r} ${reason}`);
1336
+ }
1337
+ console.log(
1338
+ `
1339
+ ${c.d}Based on ${outcomes.length} historical outcomes${c.r}
1340
+ `
1341
+ );
1342
+ });
1023
1343
  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
1344
  const statusPath = join(
1025
1345
  process.cwd(),
@@ -1084,6 +1404,13 @@ function createConductorCommands() {
1084
1404
  "--mode <mode>",
1085
1405
  'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
1086
1406
  "cli"
1407
+ ).option(
1408
+ "--model <model>",
1409
+ 'Model routing: "auto" (complexity-based) or a specific model ID',
1410
+ "auto"
1411
+ ).option(
1412
+ "--no-pr",
1413
+ "Disable automatic GitHub PR creation after agent success"
1087
1414
  ).action(async (options) => {
1088
1415
  ensureDefaultPromptTemplate();
1089
1416
  const conductor = new Conductor({
@@ -1098,7 +1425,9 @@ function createConductorCommands() {
1098
1425
  baseBranch: options.branch,
1099
1426
  maxRetries: parseInt(options.retries, 10),
1100
1427
  turnTimeoutMs: parseInt(options.turnTimeout, 10),
1101
- agentMode: options.mode === "adapter" ? "adapter" : "cli"
1428
+ agentMode: options.mode === "adapter" ? "adapter" : "cli",
1429
+ model: options.model,
1430
+ autoPR: options.pr
1102
1431
  });
1103
1432
  await conductor.start();
1104
1433
  });
@@ -1379,5 +1708,6 @@ function createConductorCommands() {
1379
1708
  }
1380
1709
  export {
1381
1710
  createConductorCommands,
1382
- formatElapsed
1711
+ formatElapsed,
1712
+ predictDifficulty
1383
1713
  };