@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.
Files changed (34) hide show
  1. package/dist/src/cli/commands/orchestrate.js +249 -2
  2. package/dist/src/cli/commands/orchestrator.js +206 -23
  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"], {
@@ -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 proc = spawn(
1030
- claudeBin,
1031
- [
1032
- "-p",
1033
- "--output-format",
1034
- "stream-json",
1035
- "--dangerously-skip-permissions",
1036
- "--settings",
1037
- '{"hooks":{}}',
1038
- prompt
1039
- ],
1040
- {
1041
- cwd: run.workspacePath,
1042
- env: this.buildAgentEnv(issue, run),
1043
- stdio: ["pipe", "pipe", "pipe"]
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 priorContext = attempt > 1 ? `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.` : "";
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.0",
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",