@stackmemoryai/stackmemory 1.0.0 → 1.2.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.
Files changed (39) hide show
  1. package/README.md +66 -270
  2. package/dist/src/cli/claude-sm.js +9 -0
  3. package/dist/src/cli/codex-sm.js +9 -0
  4. package/dist/src/cli/commands/audit.js +134 -0
  5. package/dist/src/cli/commands/bench.js +252 -0
  6. package/dist/src/cli/commands/dashboard.js +2 -1
  7. package/dist/src/cli/commands/stats.js +118 -0
  8. package/dist/src/cli/index.js +6 -0
  9. package/dist/src/core/config/feature-flags.js +7 -1
  10. package/dist/src/core/context/enhanced-rehydration.js +24 -5
  11. package/dist/src/core/extensions/cerebras-adapter.js +28 -0
  12. package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
  13. package/dist/src/core/extensions/provider-adapter.js +33 -240
  14. package/dist/src/core/models/complexity-scorer.js +154 -0
  15. package/dist/src/core/models/model-router.js +230 -36
  16. package/dist/src/core/models/provider-pricing.js +63 -0
  17. package/dist/src/core/models/sensitive-guard.js +112 -0
  18. package/dist/src/core/monitoring/feedback-loops.js +88 -0
  19. package/dist/src/features/sweep/pty-wrapper.js +9 -0
  20. package/dist/src/hooks/daemon.js +8 -0
  21. package/dist/src/hooks/graphiti-hooks.js +104 -0
  22. package/dist/src/hooks/schemas.js +12 -1
  23. package/dist/src/integrations/anthropic/batch-client.js +256 -0
  24. package/dist/src/integrations/anthropic/client.js +87 -72
  25. package/dist/src/integrations/claude-code/subagent-client.js +133 -12
  26. package/dist/src/integrations/graphiti/client.js +115 -0
  27. package/dist/src/integrations/graphiti/config.js +17 -0
  28. package/dist/src/integrations/graphiti/types.js +4 -0
  29. package/dist/src/integrations/mcp/handlers/index.js +25 -1
  30. package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
  31. package/dist/src/integrations/mcp/server.js +207 -1
  32. package/dist/src/integrations/mcp/tool-definitions.js +38 -1
  33. package/dist/src/orchestrators/multimodal/baselines.js +128 -0
  34. package/dist/src/orchestrators/multimodal/constants.js +9 -1
  35. package/dist/src/orchestrators/multimodal/harness.js +86 -6
  36. package/dist/src/orchestrators/multimodal/providers.js +113 -2
  37. package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
  38. package/dist/src/utils/fuzzy-edit.js +162 -0
  39. package/package.json +5 -1
@@ -30,6 +30,7 @@ import { TraceDetector } from "../../core/trace/trace-detector.js";
30
30
  import { LLMContextRetrieval } from "../../core/retrieval/index.js";
31
31
  import { DiscoveryHandlers } from "./handlers/discovery-handlers.js";
32
32
  import { DiffMemHandlers } from "./handlers/diffmem-handlers.js";
33
+ import { fuzzyEdit } from "../../utils/fuzzy-edit.js";
33
34
  import { v4 as uuidv4 } from "uuid";
34
35
  import {
35
36
  DEFAULT_PLANNER_MODEL,
@@ -62,6 +63,7 @@ class LocalStackMemoryMCP {
62
63
  contextRetrieval;
63
64
  discoveryHandlers;
64
65
  diffMemHandlers;
66
+ providerHandlers = null;
65
67
  pendingPlans = /* @__PURE__ */ new Map();
66
68
  constructor() {
67
69
  this.projectRoot = this.findProjectRoot();
@@ -91,6 +93,18 @@ class LocalStackMemoryMCP {
91
93
  influence_score REAL,
92
94
  timestamp INTEGER DEFAULT (unixepoch())
93
95
  );
96
+
97
+ CREATE TABLE IF NOT EXISTS edit_telemetry (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
100
+ session_id TEXT,
101
+ tool_name TEXT NOT NULL,
102
+ file_path TEXT,
103
+ success INTEGER NOT NULL DEFAULT 1,
104
+ error_type TEXT,
105
+ error_message TEXT
106
+ );
107
+ CREATE INDEX IF NOT EXISTS idx_edit_telemetry_ts ON edit_telemetry(timestamp);
94
108
  `);
95
109
  this.frameManager = new FrameManager(this.db, this.projectId);
96
110
  this.initLinearIfEnabled();
@@ -122,6 +136,7 @@ class LocalStackMemoryMCP {
122
136
  projectRoot: this.projectRoot
123
137
  });
124
138
  this.diffMemHandlers = new DiffMemHandlers();
139
+ this.initProviderHandlers();
125
140
  this.setupHandlers();
126
141
  this.loadInitialContext();
127
142
  this.loadPendingPlans();
@@ -243,6 +258,16 @@ ${summary}...`, 0.8);
243
258
  this.contexts.set(ctx.id, ctx);
244
259
  });
245
260
  }
261
+ async initProviderHandlers() {
262
+ if (!isFeatureEnabled("multiProvider")) return;
263
+ try {
264
+ const { ProviderHandlers } = await import("./handlers/provider-handlers.js");
265
+ this.providerHandlers = new ProviderHandlers();
266
+ logger.info("Provider handlers initialized (multiProvider enabled)");
267
+ } catch (error) {
268
+ logger.warn("Failed to initialize provider handlers", { error });
269
+ }
270
+ }
246
271
  setupHandlers() {
247
272
  this.server.setRequestHandler(
248
273
  z.object({
@@ -1047,7 +1072,108 @@ ${summary}...`, 0.8);
1047
1072
  type: "object",
1048
1073
  properties: {}
1049
1074
  }
1050
- }
1075
+ },
1076
+ // Provider tools (only active when STACKMEMORY_MULTI_PROVIDER=true)
1077
+ ...isFeatureEnabled("multiProvider") ? [
1078
+ {
1079
+ name: "delegate_to_model",
1080
+ description: "Route a prompt to a specific provider/model. Uses smart cost-based routing by default.",
1081
+ inputSchema: {
1082
+ type: "object",
1083
+ properties: {
1084
+ prompt: {
1085
+ type: "string",
1086
+ description: "The prompt to send"
1087
+ },
1088
+ provider: {
1089
+ type: "string",
1090
+ enum: [
1091
+ "anthropic",
1092
+ "cerebras",
1093
+ "deepinfra",
1094
+ "openai",
1095
+ "openrouter"
1096
+ ],
1097
+ description: "Override provider (auto-routes if omitted)"
1098
+ },
1099
+ model: {
1100
+ type: "string",
1101
+ description: "Override model name"
1102
+ },
1103
+ taskType: {
1104
+ type: "string",
1105
+ enum: [
1106
+ "linting",
1107
+ "context",
1108
+ "code",
1109
+ "testing",
1110
+ "review",
1111
+ "plan"
1112
+ ],
1113
+ description: "Task type for auto-routing"
1114
+ },
1115
+ maxTokens: {
1116
+ type: "number",
1117
+ description: "Max tokens"
1118
+ },
1119
+ temperature: { type: "number" },
1120
+ system: {
1121
+ type: "string",
1122
+ description: "System prompt"
1123
+ }
1124
+ },
1125
+ required: ["prompt"]
1126
+ }
1127
+ },
1128
+ {
1129
+ name: "batch_submit",
1130
+ description: "Submit prompts to Anthropic Batch API (50% discount, async)",
1131
+ inputSchema: {
1132
+ type: "object",
1133
+ properties: {
1134
+ prompts: {
1135
+ type: "array",
1136
+ items: {
1137
+ type: "object",
1138
+ properties: {
1139
+ id: { type: "string" },
1140
+ prompt: { type: "string" },
1141
+ model: { type: "string" },
1142
+ maxTokens: { type: "number" },
1143
+ system: { type: "string" }
1144
+ },
1145
+ required: ["id", "prompt"]
1146
+ },
1147
+ description: "Array of prompts to batch"
1148
+ },
1149
+ description: {
1150
+ type: "string",
1151
+ description: "Batch job description"
1152
+ }
1153
+ },
1154
+ required: ["prompts"]
1155
+ }
1156
+ },
1157
+ {
1158
+ name: "batch_check",
1159
+ description: "Check status or retrieve results for a batch job",
1160
+ inputSchema: {
1161
+ type: "object",
1162
+ properties: {
1163
+ batchId: {
1164
+ type: "string",
1165
+ description: "Batch job ID"
1166
+ },
1167
+ retrieve: {
1168
+ type: "boolean",
1169
+ description: "Retrieve results if complete",
1170
+ default: false
1171
+ }
1172
+ },
1173
+ required: ["batchId"]
1174
+ }
1175
+ }
1176
+ ] : []
1051
1177
  ]
1052
1178
  };
1053
1179
  }
@@ -1195,6 +1321,58 @@ ${summary}...`, 0.8);
1195
1321
  case "diffmem_status":
1196
1322
  result = await this.diffMemHandlers.handleStatus();
1197
1323
  break;
1324
+ case "sm_edit":
1325
+ result = await this.handleSmEdit(args);
1326
+ break;
1327
+ // Provider tools
1328
+ case "delegate_to_model":
1329
+ if (!this.providerHandlers) {
1330
+ result = {
1331
+ content: [
1332
+ {
1333
+ type: "text",
1334
+ text: "Multi-provider routing is disabled."
1335
+ }
1336
+ ]
1337
+ };
1338
+ } else {
1339
+ result = await this.providerHandlers.handleDelegateToModel(
1340
+ args
1341
+ );
1342
+ }
1343
+ break;
1344
+ case "batch_submit":
1345
+ if (!this.providerHandlers) {
1346
+ result = {
1347
+ content: [
1348
+ {
1349
+ type: "text",
1350
+ text: "Multi-provider routing is disabled."
1351
+ }
1352
+ ]
1353
+ };
1354
+ } else {
1355
+ result = await this.providerHandlers.handleBatchSubmit(
1356
+ args
1357
+ );
1358
+ }
1359
+ break;
1360
+ case "batch_check":
1361
+ if (!this.providerHandlers) {
1362
+ result = {
1363
+ content: [
1364
+ {
1365
+ type: "text",
1366
+ text: "Multi-provider routing is disabled."
1367
+ }
1368
+ ]
1369
+ };
1370
+ } else {
1371
+ result = await this.providerHandlers.handleBatchCheck(
1372
+ args
1373
+ );
1374
+ }
1375
+ break;
1198
1376
  default:
1199
1377
  throw new Error(`Unknown tool: ${name}`);
1200
1378
  }
@@ -2639,6 +2817,34 @@ ${typeBreakdown}`
2639
2817
  };
2640
2818
  }
2641
2819
  }
2820
+ async handleSmEdit(args) {
2821
+ const {
2822
+ file_path: filePath,
2823
+ old_string: oldString,
2824
+ new_string: newString,
2825
+ threshold = 0.85
2826
+ } = args;
2827
+ if (!filePath || !oldString || newString === void 0) {
2828
+ throw new Error("file_path, old_string, and new_string are required");
2829
+ }
2830
+ const { readFileSync: readFileSync2, writeFileSync: writeFileSync2 } = await import("fs");
2831
+ const content = readFileSync2(filePath, "utf-8");
2832
+ const editResult = fuzzyEdit(content, oldString, newString, threshold);
2833
+ if (!editResult) {
2834
+ return {
2835
+ success: false,
2836
+ error: "No match found above threshold",
2837
+ threshold
2838
+ };
2839
+ }
2840
+ writeFileSync2(filePath, editResult.content, "utf-8");
2841
+ return {
2842
+ success: true,
2843
+ method: editResult.match.method,
2844
+ confidence: editResult.match.confidence,
2845
+ matchedText: editResult.match.matchedText.length > 200 ? editResult.match.matchedText.slice(0, 200) + "..." : editResult.match.matchedText
2846
+ };
2847
+ }
2642
2848
  async start() {
2643
2849
  const transport = new StdioServerTransport();
2644
2850
  await this.server.connect(transport);
@@ -12,7 +12,8 @@ class MCPToolDefinitions {
12
12
  ...this.getTaskTools(),
13
13
  ...this.getLinearTools(),
14
14
  ...this.getTraceTools(),
15
- ...this.getDiscoveryTools()
15
+ ...this.getDiscoveryTools(),
16
+ ...this.getEditTools()
16
17
  ];
17
18
  }
18
19
  /**
@@ -670,6 +671,40 @@ class MCPToolDefinitions {
670
671
  }
671
672
  ];
672
673
  }
674
+ /**
675
+ * Edit tools (fuzzy edit fallback)
676
+ */
677
+ getEditTools() {
678
+ return [
679
+ {
680
+ name: "sm_edit",
681
+ description: "Fuzzy file edit \u2014 fallback when Claude Code's Edit tool fails on whitespace or indentation mismatches. Uses four-tier matching: exact, whitespace-normalized, indentation-insensitive, and line-level fuzzy (Levenshtein).",
682
+ inputSchema: {
683
+ type: "object",
684
+ properties: {
685
+ file_path: {
686
+ type: "string",
687
+ description: "Absolute path to the file to edit"
688
+ },
689
+ old_string: {
690
+ type: "string",
691
+ description: "The text to find and replace"
692
+ },
693
+ new_string: {
694
+ type: "string",
695
+ description: "The replacement text"
696
+ },
697
+ threshold: {
698
+ type: "number",
699
+ default: 0.85,
700
+ description: "Minimum similarity threshold for fuzzy matching (0-1). Default 0.85."
701
+ }
702
+ },
703
+ required: ["file_path", "old_string", "new_string"]
704
+ }
705
+ }
706
+ ];
707
+ }
673
708
  /**
674
709
  * Get tool definition by name
675
710
  */
@@ -691,6 +726,8 @@ class MCPToolDefinitions {
691
726
  return this.getTraceTools();
692
727
  case "discovery":
693
728
  return this.getDiscoveryTools();
729
+ case "edit":
730
+ return this.getEditTools();
694
731
  default:
695
732
  return [];
696
733
  }
@@ -0,0 +1,128 @@
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
+ const SWE_BENCH_BASELINES = [
6
+ {
7
+ agent: "Claude Code",
8
+ model: "claude-sonnet-4",
9
+ benchmark: "swe-bench-verified",
10
+ resolveRate: 0.704,
11
+ date: "2025-12-01",
12
+ source: "https://www.swebench.com/"
13
+ },
14
+ {
15
+ agent: "Devin",
16
+ model: "mixed",
17
+ benchmark: "swe-bench-verified",
18
+ resolveRate: 0.551,
19
+ date: "2025-10-01",
20
+ source: "https://www.swebench.com/"
21
+ },
22
+ {
23
+ agent: "OpenHands",
24
+ model: "claude-sonnet-4",
25
+ benchmark: "swe-bench-verified",
26
+ resolveRate: 0.535,
27
+ date: "2025-09-01",
28
+ source: "https://www.swebench.com/"
29
+ },
30
+ {
31
+ agent: "Aider",
32
+ model: "claude-sonnet-4",
33
+ benchmark: "swe-bench-verified",
34
+ resolveRate: 0.489,
35
+ date: "2025-10-01",
36
+ source: "https://www.swebench.com/"
37
+ }
38
+ ];
39
+ const HARNESS_TARGETS = {
40
+ /** Plan generation should complete within 10s */
41
+ planLatencyP95Ms: 1e4,
42
+ /**
43
+ * Full cycle (plan + implement + critique) within 5 minutes.
44
+ * Codex execution benchmarks show 89-231s for real runs (1-2 iterations).
45
+ * 300s allows for 2-iteration runs with margin.
46
+ */
47
+ totalLatencyP95Ms: 3e5,
48
+ /**
49
+ * Single-iteration (first-pass) latency ceiling: 2.5 minutes.
50
+ * Based on observed single-pass runs of 89-115s with headroom.
51
+ */
52
+ singleIterLatencyP95Ms: 15e4,
53
+ /** First-pass approval rate (no retries needed) */
54
+ firstPassApprovalRate: 0.7,
55
+ /** Edit success rate (exact + fuzzy combined) */
56
+ editSuccessRate: 0.9,
57
+ /** Edit fuzzy fallback rate (lower = better, means exact match works) */
58
+ editFuzzyFallbackRate: 0.15,
59
+ /** Context overhead should be < 6000 tokens */
60
+ contextTokenBudget: 6e3
61
+ };
62
+ function summarizeRuns(runs) {
63
+ if (runs.length === 0) {
64
+ return {
65
+ totalRuns: 0,
66
+ approvalRate: 0,
67
+ firstPassRate: 0,
68
+ avgIterations: 0,
69
+ avgPlanLatencyMs: 0,
70
+ avgTotalLatencyMs: 0,
71
+ p95PlanLatencyMs: 0,
72
+ p95TotalLatencyMs: 0,
73
+ p95SingleIterLatencyMs: 0,
74
+ editSuccessRate: 0,
75
+ editFuzzyRate: 0,
76
+ avgContextTokens: 0,
77
+ passesTargets: {}
78
+ };
79
+ }
80
+ const approvedRuns = runs.filter((r) => r.approved);
81
+ const firstPassRuns = runs.filter((r) => r.approved && r.iterations <= 1);
82
+ const totalEdits = runs.reduce((s, r) => s + r.editAttempts, 0);
83
+ const totalEditSuccesses = runs.reduce((s, r) => s + r.editSuccesses, 0);
84
+ const totalFuzzy = runs.reduce((s, r) => s + r.editFuzzyFallbacks, 0);
85
+ const planLatencies = runs.map((r) => r.planLatencyMs).sort((a, b) => a - b);
86
+ const totalLatencies = runs.map((r) => r.totalLatencyMs).sort((a, b) => a - b);
87
+ const p95Idx = Math.min(Math.ceil(runs.length * 0.95) - 1, runs.length - 1);
88
+ const approvalRate = approvedRuns.length / runs.length;
89
+ const firstPassRate = firstPassRuns.length / runs.length;
90
+ const editSuccessRate = totalEdits > 0 ? totalEditSuccesses / totalEdits : 1;
91
+ const editFuzzyRate = totalEditSuccesses > 0 ? totalFuzzy / totalEditSuccesses : 0;
92
+ const avgContextTokens = runs.reduce((s, r) => s + r.contextTokens, 0) / runs.length;
93
+ const p95Plan = planLatencies[p95Idx];
94
+ const p95Total = totalLatencies[p95Idx];
95
+ const singleIterLatencies = runs.filter((r) => r.approved && r.iterations <= 1).map((r) => r.totalLatencyMs).sort((a, b) => a - b);
96
+ const p95SingleIter = singleIterLatencies.length > 0 ? singleIterLatencies[Math.min(
97
+ Math.ceil(singleIterLatencies.length * 0.95) - 1,
98
+ singleIterLatencies.length - 1
99
+ )] : 0;
100
+ return {
101
+ totalRuns: runs.length,
102
+ approvalRate,
103
+ firstPassRate,
104
+ avgIterations: runs.reduce((s, r) => s + r.iterations, 0) / runs.length,
105
+ avgPlanLatencyMs: runs.reduce((s, r) => s + r.planLatencyMs, 0) / runs.length,
106
+ avgTotalLatencyMs: runs.reduce((s, r) => s + r.totalLatencyMs, 0) / runs.length,
107
+ p95PlanLatencyMs: p95Plan,
108
+ p95TotalLatencyMs: p95Total,
109
+ p95SingleIterLatencyMs: p95SingleIter,
110
+ editSuccessRate,
111
+ editFuzzyRate,
112
+ avgContextTokens,
113
+ passesTargets: {
114
+ planLatency: p95Plan <= HARNESS_TARGETS.planLatencyP95Ms,
115
+ totalLatency: p95Total <= HARNESS_TARGETS.totalLatencyP95Ms,
116
+ singleIterLatency: singleIterLatencies.length === 0 || p95SingleIter <= HARNESS_TARGETS.singleIterLatencyP95Ms,
117
+ firstPassApproval: firstPassRate >= HARNESS_TARGETS.firstPassApprovalRate,
118
+ editSuccess: editSuccessRate >= HARNESS_TARGETS.editSuccessRate,
119
+ editFuzzyRate: editFuzzyRate <= HARNESS_TARGETS.editFuzzyFallbackRate,
120
+ contextBudget: avgContextTokens <= HARNESS_TARGETS.contextTokenBudget
121
+ }
122
+ };
123
+ }
124
+ export {
125
+ HARNESS_TARGETS,
126
+ SWE_BENCH_BASELINES,
127
+ summarizeRuns
128
+ };
@@ -8,9 +8,17 @@ const DEFAULT_IMPLEMENTER = process.env.STACKMEMORY_MM_IMPLEMENTER || "codex";
8
8
  const DEFAULT_MAX_ITERS = Number(
9
9
  process.env.STACKMEMORY_MM_MAX_ITERS || 2
10
10
  );
11
+ const STRUCTURED_RESPONSE_SUFFIX = `
12
+
13
+ Response Format:
14
+ - When asking a question, ALWAYS end with structured choices: Yes/No, or numbered options (1, 2, 3, 4), or lettered options (A, B, C, D).
15
+ - When completing a task, end with "Done." followed by suggested next steps as numbered options (1, 2, 3, 4).
16
+ - Never leave responses open-ended. Always provide explicit options the user can select.
17
+ - Keep options concise (one line each). Use the format that best fits: Yes/No for confirmations, 1-4 for action choices, A-D for category selections.`;
11
18
  export {
12
19
  DEFAULT_IMPLEMENTER,
13
20
  DEFAULT_MAX_ITERS,
14
21
  DEFAULT_PLANNER_MODEL,
15
- DEFAULT_REVIEWER_MODEL
22
+ DEFAULT_REVIEWER_MODEL,
23
+ STRUCTURED_RESPONSE_SUFFIX
16
24
  };
@@ -2,11 +2,20 @@ import { fileURLToPath as __fileURLToPath } from 'url';
2
2
  import { dirname as __pathDirname } from 'path';
3
3
  const __filename = __fileURLToPath(import.meta.url);
4
4
  const __dirname = __pathDirname(__filename);
5
- import { callClaude, callCodexCLI, implementWithClaude } from "./providers.js";
5
+ import {
6
+ callClaude,
7
+ callCodexCLI,
8
+ captureGitDiff,
9
+ implementWithClaude,
10
+ parseEditMetrics,
11
+ runPostImplChecks
12
+ } from "./providers.js";
6
13
  import * as fs from "fs";
7
14
  import * as path from "path";
8
15
  import { FrameManager } from "../../core/context/index.js";
9
16
  import { deriveProjectId } from "./utils.js";
17
+ import { HARNESS_TARGETS, summarizeRuns } from "./baselines.js";
18
+ import { feedbackLoops } from "../../core/monitoring/feedback-loops.js";
10
19
  function heuristicPlan(input) {
11
20
  return {
12
21
  summary: `Plan for: ${input.task}`,
@@ -50,6 +59,7 @@ Repo: ${input.repoPath}
50
59
  Notes: ${input.contextNotes || "(none)"}
51
60
  ${contextSummary}
52
61
  Constraints: Keep the plan minimal and implementable in a single PR.`;
62
+ const t0 = Date.now();
53
63
  let plan;
54
64
  try {
55
65
  const raw = await callClaude(plannerPrompt, {
@@ -65,6 +75,7 @@ Constraints: Keep the plan minimal and implementable in a single PR.`;
65
75
  } catch {
66
76
  plan = heuristicPlan(input);
67
77
  }
78
+ const planLatencyMs = Date.now() - t0;
68
79
  const implementer = options.implementer || "codex";
69
80
  const maxIters = Math.max(1, options.maxIters ?? 2);
70
81
  const iterations = [];
@@ -104,11 +115,25 @@ Incorporate reviewer suggestions: ${lastCritique.suggestions.join("; ")}`;
104
115
  lastCommand = `claude:${options.plannerModel || "sonnet"} prompt`;
105
116
  lastOutput = impl.output;
106
117
  }
107
- const criticSystem = `You are a strict code reviewer. Return a JSON object: { approved: boolean, issues: string[], suggestions: string[] }`;
118
+ const diff = options.dryRun !== false ? "(dry run \u2014 no diff)" : captureGitDiff(input.repoPath);
119
+ const checks = options.dryRun !== false ? null : runPostImplChecks(input.repoPath);
120
+ const checksSection = checks ? `
121
+
122
+ Post-implementation checks:
123
+ Lint: ${checks.lintOk ? "PASS" : "FAIL"}
124
+ ${checks.lintOutput}
125
+ Tests: ${checks.testsOk ? "PASS" : "FAIL"}
126
+ ${checks.testOutput}` : "";
127
+ const criticSystem = `You are a strict code reviewer. Review the git diff against the plan. Check for: correctness, missing steps, unrelated changes, bugs, security issues. Also review lint and test results if provided. Return raw JSON only (no markdown fences): { "approved": boolean, "issues": ["string"], "suggestions": ["string"] }`;
108
128
  const criticPrompt = `Plan: ${plan.summary}
129
+ Acceptance criteria:
130
+ ${plan.steps.map((s) => s.acceptanceCriteria?.join(", ") || s.title).join("\n")}
131
+
109
132
  Attempt ${i + 1}/${maxIters}
110
- Command: ${lastCommand}
111
- Output: ${lastOutput.slice(0, 2e3)}`;
133
+ Implementer exit: ${ok ? "success" : "failed"}
134
+
135
+ Git diff:
136
+ ${diff}${checksSection}`;
112
137
  try {
113
138
  const raw = await callClaude(criticPrompt, {
114
139
  model: options.reviewerModel,
@@ -126,7 +151,7 @@ Output: ${lastOutput.slice(0, 2e3)}`;
126
151
  iterations.push({
127
152
  command: lastCommand,
128
153
  ok,
129
- outputPreview: lastOutput.slice(0, 400),
154
+ outputPreview: diff.slice(0, 2e3),
130
155
  critique: lastCritique
131
156
  });
132
157
  if (lastCritique.approved) {
@@ -134,6 +159,24 @@ Output: ${lastOutput.slice(0, 2e3)}`;
134
159
  break;
135
160
  }
136
161
  }
162
+ const totalLatencyMs = Date.now() - t0;
163
+ const finalDiff = options.dryRun !== false ? "(dry run \u2014 no diff)" : captureGitDiff(input.repoPath);
164
+ const editMetrics = parseEditMetrics(finalDiff);
165
+ const runMetrics = {
166
+ timestamp: Date.now(),
167
+ task: input.task,
168
+ plannerModel: options.plannerModel || "default",
169
+ reviewerModel: options.reviewerModel || "default",
170
+ implementer,
171
+ planLatencyMs,
172
+ totalLatencyMs,
173
+ iterations: iterations.length,
174
+ approved,
175
+ editAttempts: editMetrics.editAttempts,
176
+ editSuccesses: editMetrics.editSuccesses,
177
+ editFuzzyFallbacks: editMetrics.editFuzzyFallbacks,
178
+ contextTokens: Math.ceil(finalDiff.length / 4)
179
+ };
137
180
  try {
138
181
  const dir = options.auditDir || path.join(input.repoPath, ".stackmemory", "build");
139
182
  fs.mkdirSync(dir, { recursive: true });
@@ -146,12 +189,49 @@ Output: ${lastOutput.slice(0, 2e3)}`;
146
189
  input,
147
190
  options: { ...options, auditDir: void 0 },
148
191
  plan,
149
- iterations
192
+ iterations,
193
+ metrics: runMetrics
150
194
  },
151
195
  null,
152
196
  2
153
197
  )
154
198
  );
199
+ const metricsFile = path.join(dir, "harness-metrics.jsonl");
200
+ fs.appendFileSync(metricsFile, JSON.stringify(runMetrics) + "\n");
201
+ try {
202
+ const lines = fs.readFileSync(metricsFile, "utf-8").split("\n").filter((l) => l.trim());
203
+ const recent = lines.slice(-10).map((l) => JSON.parse(l));
204
+ if (recent.length >= 3) {
205
+ const summary = summarizeRuns(recent);
206
+ if (summary.approvalRate < HARNESS_TARGETS.firstPassApprovalRate) {
207
+ feedbackLoops.fire(
208
+ "harnessRegression",
209
+ "metrics_append",
210
+ {
211
+ metric: "approvalRate",
212
+ current: summary.approvalRate,
213
+ target: HARNESS_TARGETS.firstPassApprovalRate,
214
+ window: recent.length
215
+ },
216
+ "regression_alert"
217
+ );
218
+ }
219
+ if (summary.p95TotalLatencyMs > HARNESS_TARGETS.totalLatencyP95Ms) {
220
+ feedbackLoops.fire(
221
+ "harnessRegression",
222
+ "metrics_append",
223
+ {
224
+ metric: "totalLatencyP95",
225
+ current: summary.p95TotalLatencyMs,
226
+ target: HARNESS_TARGETS.totalLatencyP95Ms,
227
+ window: recent.length
228
+ },
229
+ "regression_alert"
230
+ );
231
+ }
232
+ }
233
+ } catch {
234
+ }
155
235
  } catch {
156
236
  }
157
237
  if (options.record) {