@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.
- package/README.md +66 -270
- package/dist/src/cli/claude-sm.js +9 -0
- package/dist/src/cli/codex-sm.js +9 -0
- package/dist/src/cli/commands/audit.js +134 -0
- package/dist/src/cli/commands/bench.js +252 -0
- package/dist/src/cli/commands/dashboard.js +2 -1
- package/dist/src/cli/commands/stats.js +118 -0
- package/dist/src/cli/index.js +6 -0
- package/dist/src/core/config/feature-flags.js +7 -1
- package/dist/src/core/context/enhanced-rehydration.js +24 -5
- package/dist/src/core/extensions/cerebras-adapter.js +28 -0
- package/dist/src/core/extensions/deepinfra-adapter.js +32 -0
- package/dist/src/core/extensions/provider-adapter.js +33 -240
- package/dist/src/core/models/complexity-scorer.js +154 -0
- package/dist/src/core/models/model-router.js +230 -36
- package/dist/src/core/models/provider-pricing.js +63 -0
- package/dist/src/core/models/sensitive-guard.js +112 -0
- package/dist/src/core/monitoring/feedback-loops.js +88 -0
- package/dist/src/features/sweep/pty-wrapper.js +9 -0
- package/dist/src/hooks/daemon.js +8 -0
- package/dist/src/hooks/graphiti-hooks.js +104 -0
- package/dist/src/hooks/schemas.js +12 -1
- package/dist/src/integrations/anthropic/batch-client.js +256 -0
- package/dist/src/integrations/anthropic/client.js +87 -72
- package/dist/src/integrations/claude-code/subagent-client.js +133 -12
- package/dist/src/integrations/graphiti/client.js +115 -0
- package/dist/src/integrations/graphiti/config.js +17 -0
- package/dist/src/integrations/graphiti/types.js +4 -0
- package/dist/src/integrations/mcp/handlers/index.js +25 -1
- package/dist/src/integrations/mcp/handlers/provider-handlers.js +227 -0
- package/dist/src/integrations/mcp/server.js +207 -1
- package/dist/src/integrations/mcp/tool-definitions.js +38 -1
- package/dist/src/orchestrators/multimodal/baselines.js +128 -0
- package/dist/src/orchestrators/multimodal/constants.js +9 -1
- package/dist/src/orchestrators/multimodal/harness.js +86 -6
- package/dist/src/orchestrators/multimodal/providers.js +113 -2
- package/dist/src/skills/recursive-agent-orchestrator.js +15 -10
- package/dist/src/utils/fuzzy-edit.js +162 -0
- 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 {
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
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:
|
|
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) {
|