@stackmemoryai/stackmemory 1.0.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/claude-sm.js +65 -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/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 +16 -4
- 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 +316 -1
- package/dist/src/integrations/mcp/tool-definitions.js +90 -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/dist/src/utils/hook-installer.js +155 -0
- package/package.json +6 -2
- package/scripts/gepa/.before-optimize.md +159 -0
- package/scripts/gepa/generations/gen-000/baseline.md +159 -124
- package/scripts/gepa/generations/gen-001/baseline.md +159 -0
- package/scripts/gepa/generations/gen-001/variant-a.md +166 -0
- package/scripts/gepa/generations/gen-001/variant-b.md +237 -0
- package/scripts/gepa/generations/gen-001/variant-c.md +61 -0
- package/scripts/gepa/generations/gen-001/variant-d.md +119 -0
- package/scripts/gepa/results/eval-1-baseline.json +41 -0
- package/scripts/gepa/results/eval-1-variant-a.json +41 -0
- package/scripts/gepa/results/eval-1-variant-b.json +41 -0
- package/scripts/gepa/results/eval-1-variant-c.json +41 -0
- package/scripts/gepa/results/eval-1-variant-d.json +41 -0
- package/scripts/gepa/state.json +41 -2
- package/scripts/install-claude-hooks-auto.js +176 -44
- package/templates/claude-hooks/auto-checkpoint.js +174 -0
- package/templates/claude-hooks/chime-on-stop.sh +22 -0
- package/templates/claude-hooks/session-rescue.sh +15 -0
- package/templates/claude-hooks/stop-checkpoint.js +120 -0
|
@@ -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) {
|
|
@@ -3,6 +3,7 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
|
+
import { STRUCTURED_RESPONSE_SUFFIX } from "./constants.js";
|
|
6
7
|
async function callClaude(prompt, options) {
|
|
7
8
|
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
8
9
|
if (!apiKey) {
|
|
@@ -15,7 +16,7 @@ async function callClaude(prompt, options) {
|
|
|
15
16
|
const { Anthropic } = await import("@anthropic-ai/sdk");
|
|
16
17
|
const client = new Anthropic({ apiKey });
|
|
17
18
|
const model = options.model || "claude-sonnet-4-20250514";
|
|
18
|
-
const system = options.system || "You are a precise software planning assistant.";
|
|
19
|
+
const system = (options.system || "You are a precise software planning assistant.") + STRUCTURED_RESPONSE_SUFFIX;
|
|
19
20
|
try {
|
|
20
21
|
const msg = await client.messages.create({
|
|
21
22
|
model,
|
|
@@ -79,6 +80,113 @@ function callCodexCLI(prompt, args = [], dryRun = true, cwd) {
|
|
|
79
80
|
return { ok: false, output: e?.message || String(e), command: printable };
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
function captureGitDiff(cwd, maxLen = 12e3) {
|
|
84
|
+
try {
|
|
85
|
+
const unstaged = spawnSync("git", ["diff"], {
|
|
86
|
+
cwd,
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
timeout: 1e4
|
|
89
|
+
});
|
|
90
|
+
const staged = spawnSync("git", ["diff", "--cached"], {
|
|
91
|
+
cwd,
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
timeout: 1e4
|
|
94
|
+
});
|
|
95
|
+
const untracked = spawnSync(
|
|
96
|
+
"git",
|
|
97
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
98
|
+
{ cwd, encoding: "utf8", timeout: 1e4 }
|
|
99
|
+
);
|
|
100
|
+
let diff = "";
|
|
101
|
+
if (staged.stdout?.trim()) diff += staged.stdout;
|
|
102
|
+
if (unstaged.stdout?.trim()) diff += (diff ? "\n" : "") + unstaged.stdout;
|
|
103
|
+
if (untracked.stdout?.trim()) {
|
|
104
|
+
const newFiles = untracked.stdout.trim().split("\n").slice(0, 10);
|
|
105
|
+
diff += (diff ? "\n" : "") + `New untracked files:
|
|
106
|
+
${newFiles.join("\n")}`;
|
|
107
|
+
}
|
|
108
|
+
if (!diff.trim()) return "(no changes detected)";
|
|
109
|
+
if (diff.length > maxLen) {
|
|
110
|
+
return diff.slice(0, maxLen) + `
|
|
111
|
+
... (truncated, ${diff.length} total chars)`;
|
|
112
|
+
}
|
|
113
|
+
return diff;
|
|
114
|
+
} catch {
|
|
115
|
+
return "(git diff failed)";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function runPostImplChecks(cwd) {
|
|
119
|
+
const maxOutput = 2e3;
|
|
120
|
+
function truncate(s) {
|
|
121
|
+
if (s.length <= maxOutput) return s;
|
|
122
|
+
return s.slice(0, maxOutput) + `
|
|
123
|
+
... (truncated, ${s.length} total chars)`;
|
|
124
|
+
}
|
|
125
|
+
let lintOk = false;
|
|
126
|
+
let lintOutput = "";
|
|
127
|
+
try {
|
|
128
|
+
const lint = spawnSync("npm", ["run", "lint"], {
|
|
129
|
+
cwd,
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
timeout: 3e4
|
|
132
|
+
});
|
|
133
|
+
lintOk = lint.status === 0;
|
|
134
|
+
lintOutput = truncate((lint.stdout || "") + (lint.stderr || ""));
|
|
135
|
+
} catch (e) {
|
|
136
|
+
lintOutput = truncate(e instanceof Error ? e.message : String(e));
|
|
137
|
+
}
|
|
138
|
+
let testsOk = false;
|
|
139
|
+
let testOutput = "";
|
|
140
|
+
try {
|
|
141
|
+
const tests = spawnSync(
|
|
142
|
+
"npx",
|
|
143
|
+
["vitest", "run", "--reporter=dot", "--bail=1"],
|
|
144
|
+
{
|
|
145
|
+
cwd,
|
|
146
|
+
encoding: "utf8",
|
|
147
|
+
timeout: 12e4
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
testsOk = tests.status === 0;
|
|
151
|
+
testOutput = truncate((tests.stdout || "") + (tests.stderr || ""));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
testOutput = truncate(e instanceof Error ? e.message : String(e));
|
|
154
|
+
}
|
|
155
|
+
return { lintOk, lintOutput, testsOk, testOutput };
|
|
156
|
+
}
|
|
157
|
+
function parseEditMetrics(diff) {
|
|
158
|
+
if (!diff || diff.startsWith("(")) {
|
|
159
|
+
return { editAttempts: 0, editSuccesses: 0, editFuzzyFallbacks: 0 };
|
|
160
|
+
}
|
|
161
|
+
const lines = diff.split("\n");
|
|
162
|
+
let currentFileHunks = 0;
|
|
163
|
+
let currentFileHasConflict = false;
|
|
164
|
+
let totalAttempts = 0;
|
|
165
|
+
let totalSuccesses = 0;
|
|
166
|
+
const flushFile = () => {
|
|
167
|
+
totalAttempts += currentFileHunks;
|
|
168
|
+
if (!currentFileHasConflict) {
|
|
169
|
+
totalSuccesses += currentFileHunks;
|
|
170
|
+
}
|
|
171
|
+
currentFileHunks = 0;
|
|
172
|
+
currentFileHasConflict = false;
|
|
173
|
+
};
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
if (/^diff --git /.test(line)) {
|
|
176
|
+
flushFile();
|
|
177
|
+
} else if (/^@@ /.test(line)) {
|
|
178
|
+
currentFileHunks++;
|
|
179
|
+
} else if (/^[<>=]{7}/.test(line)) {
|
|
180
|
+
currentFileHasConflict = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
flushFile();
|
|
184
|
+
return {
|
|
185
|
+
editAttempts: totalAttempts,
|
|
186
|
+
editSuccesses: totalSuccesses,
|
|
187
|
+
editFuzzyFallbacks: 0
|
|
188
|
+
};
|
|
189
|
+
}
|
|
82
190
|
async function implementWithClaude(prompt, options) {
|
|
83
191
|
try {
|
|
84
192
|
const out = await callClaude(prompt, {
|
|
@@ -93,5 +201,8 @@ async function implementWithClaude(prompt, options) {
|
|
|
93
201
|
export {
|
|
94
202
|
callClaude,
|
|
95
203
|
callCodexCLI,
|
|
96
|
-
|
|
204
|
+
captureGitDiff,
|
|
205
|
+
implementWithClaude,
|
|
206
|
+
parseEditMetrics,
|
|
207
|
+
runPostImplChecks
|
|
97
208
|
};
|
|
@@ -8,6 +8,7 @@ import { logger } from "../core/monitoring/logger.js";
|
|
|
8
8
|
import { ParallelExecutor } from "../core/execution/parallel-executor.js";
|
|
9
9
|
import { RecursiveContextManager } from "../core/context/recursive-context-manager.js";
|
|
10
10
|
import { ClaudeCodeSubagentClient } from "../integrations/claude-code/subagent-client.js";
|
|
11
|
+
import { STRUCTURED_RESPONSE_SUFFIX } from "../orchestrators/multimodal/constants.js";
|
|
11
12
|
class RecursiveAgentOrchestrator {
|
|
12
13
|
frameManager;
|
|
13
14
|
contextRetriever;
|
|
@@ -72,7 +73,7 @@ Rules:
|
|
|
72
73
|
- Maximize parallelism \u2014 independent tasks run concurrently
|
|
73
74
|
- Each subtask names its agent type: planning, code, testing, linting, review, improve, context, publish
|
|
74
75
|
- Include failure modes and rollback steps for risky operations
|
|
75
|
-
- Keep subtask descriptions actionable (verb + object + constraint)
|
|
76
|
+
- Keep subtask descriptions actionable (verb + object + constraint)` + STRUCTURED_RESPONSE_SUFFIX,
|
|
76
77
|
capabilities: ["decompose", "analyze", "strategize", "prioritize"]
|
|
77
78
|
});
|
|
78
79
|
configs.set("code", {
|
|
@@ -87,7 +88,7 @@ Rules:
|
|
|
87
88
|
- Add .js extensions to relative TypeScript imports (ESM)
|
|
88
89
|
- Return undefined over throwing; log+continue over crash
|
|
89
90
|
- No emojis, no unnecessary comments, functions under 20 lines
|
|
90
|
-
- Validate inputs at system boundaries only
|
|
91
|
+
- Validate inputs at system boundaries only` + STRUCTURED_RESPONSE_SUFFIX,
|
|
91
92
|
capabilities: ["implement", "refactor", "optimize", "document"]
|
|
92
93
|
});
|
|
93
94
|
configs.set("testing", {
|
|
@@ -102,7 +103,7 @@ Rules:
|
|
|
102
103
|
- Prioritize: critical paths > edge cases > happy paths
|
|
103
104
|
- Each test should assert meaningful behavior, not implementation details
|
|
104
105
|
- Use parameterized tests (it.each) to consolidate similar cases
|
|
105
|
-
- Run tests after writing: npm run test:run
|
|
106
|
+
- Run tests after writing: npm run test:run` + STRUCTURED_RESPONSE_SUFFIX,
|
|
106
107
|
capabilities: [
|
|
107
108
|
"generate-tests",
|
|
108
109
|
"validate",
|
|
@@ -121,7 +122,7 @@ Rules:
|
|
|
121
122
|
- Run: npm run lint (ESLint + Prettier)
|
|
122
123
|
- Auto-fix: npm run lint:fix
|
|
123
124
|
- ESM imports require .js extension on relative paths
|
|
124
|
-
- Report unfixable issues with file:line format
|
|
125
|
+
- Report unfixable issues with file:line format` + STRUCTURED_RESPONSE_SUFFIX,
|
|
125
126
|
capabilities: ["lint", "format", "type-check", "security-scan"]
|
|
126
127
|
});
|
|
127
128
|
configs.set("review", {
|
|
@@ -136,7 +137,7 @@ Rules:
|
|
|
136
137
|
- Flag: SQL injection, XSS, secret exposure, command injection
|
|
137
138
|
- Flag: functions > 20 lines, cyclomatic complexity > 5
|
|
138
139
|
- Flag: missing error handling at system boundaries
|
|
139
|
-
- Suggest specific fixes, not vague improvements
|
|
140
|
+
- Suggest specific fixes, not vague improvements` + STRUCTURED_RESPONSE_SUFFIX,
|
|
140
141
|
capabilities: [
|
|
141
142
|
"review",
|
|
142
143
|
"critique",
|
|
@@ -155,7 +156,7 @@ Rules:
|
|
|
155
156
|
- Apply only the specific improvements requested \u2014 no scope creep
|
|
156
157
|
- Maintain backward compatibility unless explicitly breaking
|
|
157
158
|
- Run lint + tests after changes to verify nothing regressed
|
|
158
|
-
- Keep changes minimal and focused
|
|
159
|
+
- Keep changes minimal and focused` + STRUCTURED_RESPONSE_SUFFIX,
|
|
159
160
|
capabilities: ["enhance", "refactor", "optimize", "polish"]
|
|
160
161
|
});
|
|
161
162
|
configs.set("context", {
|
|
@@ -169,7 +170,7 @@ Rules:
|
|
|
169
170
|
- Check docs/specs/ for ONE_PAGER.md, DEV_SPEC.md, PROMPT_PLAN.md
|
|
170
171
|
- Check CLAUDE.md and AGENTS.md for project conventions
|
|
171
172
|
- Search src/ for relevant implementations
|
|
172
|
-
- Return concise summaries, not full file contents
|
|
173
|
+
- Return concise summaries, not full file contents` + STRUCTURED_RESPONSE_SUFFIX,
|
|
173
174
|
capabilities: ["search", "retrieve", "summarize", "contextualize"]
|
|
174
175
|
});
|
|
175
176
|
configs.set("publish", {
|
|
@@ -183,7 +184,7 @@ Rules:
|
|
|
183
184
|
- Verify lint + tests + build pass before any publish
|
|
184
185
|
- Follow semver: breaking=major, feature=minor, fix=patch
|
|
185
186
|
- Generate changelog from git log since last tag
|
|
186
|
-
- Never force-push or skip pre-publish hooks
|
|
187
|
+
- Never force-push or skip pre-publish hooks` + STRUCTURED_RESPONSE_SUFFIX,
|
|
187
188
|
capabilities: ["publish-npm", "github-release", "deploy", "document"]
|
|
188
189
|
});
|
|
189
190
|
return configs;
|
|
@@ -557,9 +558,13 @@ ${sections.join("\n\n")}` : "";
|
|
|
557
558
|
const pricing = {
|
|
558
559
|
"claude-sonnet-4-5-20250929": 15,
|
|
559
560
|
"claude-haiku-4-5-20251001": 1,
|
|
560
|
-
"claude-opus-4-6": 75
|
|
561
|
+
"claude-opus-4-6": 75,
|
|
562
|
+
// External providers — much cheaper
|
|
563
|
+
"llama-4-scout-17b-16e-instruct": 0.35,
|
|
564
|
+
"THUDM/glm-4-9b-chat": 0.06
|
|
561
565
|
};
|
|
562
|
-
|
|
566
|
+
const modelName = typeof model === "string" ? model : model.model;
|
|
567
|
+
return tokens / 1e6 * (pricing[modelName] || 10);
|
|
563
568
|
}
|
|
564
569
|
countGeneratedTests(node) {
|
|
565
570
|
let count = 0;
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
function levenshtein(a, b) {
|
|
6
|
+
const m = a.length;
|
|
7
|
+
const n = b.length;
|
|
8
|
+
const dp = Array.from(
|
|
9
|
+
{ length: m + 1 },
|
|
10
|
+
() => new Array(n + 1).fill(0)
|
|
11
|
+
);
|
|
12
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
13
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
14
|
+
for (let i = 1; i <= m; i++) {
|
|
15
|
+
for (let j = 1; j <= n; j++) {
|
|
16
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
17
|
+
dp[i][j] = Math.min(
|
|
18
|
+
dp[i - 1][j] + 1,
|
|
19
|
+
dp[i][j - 1] + 1,
|
|
20
|
+
dp[i - 1][j - 1] + cost
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return dp[m][n];
|
|
25
|
+
}
|
|
26
|
+
function normalizeWhitespace(text) {
|
|
27
|
+
return text.split("\n").map((line) => line.trim().replace(/\s+/g, " ")).join("\n");
|
|
28
|
+
}
|
|
29
|
+
function stripIndentation(text) {
|
|
30
|
+
return text.split("\n").map((line) => line.trimStart()).join("\n");
|
|
31
|
+
}
|
|
32
|
+
function fuzzyMatch(content, oldString, threshold = 0.85) {
|
|
33
|
+
if (!oldString) return null;
|
|
34
|
+
const exactIdx = content.indexOf(oldString);
|
|
35
|
+
if (exactIdx !== -1) {
|
|
36
|
+
return {
|
|
37
|
+
found: true,
|
|
38
|
+
startIndex: exactIdx,
|
|
39
|
+
endIndex: exactIdx + oldString.length,
|
|
40
|
+
confidence: 1,
|
|
41
|
+
matchedText: oldString,
|
|
42
|
+
method: "exact"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const normContent = normalizeWhitespace(content);
|
|
46
|
+
const normOld = normalizeWhitespace(oldString);
|
|
47
|
+
const normIdx = normContent.indexOf(normOld);
|
|
48
|
+
if (normIdx !== -1) {
|
|
49
|
+
const match = mapNormalizedToOriginal(
|
|
50
|
+
content,
|
|
51
|
+
normContent,
|
|
52
|
+
normOld,
|
|
53
|
+
normIdx
|
|
54
|
+
);
|
|
55
|
+
if (match) {
|
|
56
|
+
return {
|
|
57
|
+
found: true,
|
|
58
|
+
startIndex: match.start,
|
|
59
|
+
endIndex: match.end,
|
|
60
|
+
confidence: 0.95,
|
|
61
|
+
matchedText: content.slice(match.start, match.end),
|
|
62
|
+
method: "whitespace-normalized"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const stripContent = stripIndentation(content);
|
|
67
|
+
const stripOld = stripIndentation(oldString);
|
|
68
|
+
const stripIdx = stripContent.indexOf(stripOld);
|
|
69
|
+
if (stripIdx !== -1) {
|
|
70
|
+
const match = mapStrippedToOriginal(
|
|
71
|
+
content,
|
|
72
|
+
stripContent,
|
|
73
|
+
stripOld,
|
|
74
|
+
stripIdx
|
|
75
|
+
);
|
|
76
|
+
if (match) {
|
|
77
|
+
return {
|
|
78
|
+
found: true,
|
|
79
|
+
startIndex: match.start,
|
|
80
|
+
endIndex: match.end,
|
|
81
|
+
confidence: 0.9,
|
|
82
|
+
matchedText: content.slice(match.start, match.end),
|
|
83
|
+
method: "indentation-insensitive"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const contentLines = content.split("\n");
|
|
88
|
+
const oldLines = oldString.split("\n");
|
|
89
|
+
const windowSize = oldLines.length;
|
|
90
|
+
if (windowSize === 0 || contentLines.length < windowSize) return null;
|
|
91
|
+
let bestScore = 0;
|
|
92
|
+
let bestStart = -1;
|
|
93
|
+
let bestEnd = -1;
|
|
94
|
+
for (let i = 0; i <= contentLines.length - windowSize; i++) {
|
|
95
|
+
const windowText = contentLines.slice(i, i + windowSize).join("\n");
|
|
96
|
+
const maxLen = Math.max(windowText.length, oldString.length);
|
|
97
|
+
if (maxLen === 0) continue;
|
|
98
|
+
const dist = levenshtein(windowText, oldString);
|
|
99
|
+
const similarity = 1 - dist / maxLen;
|
|
100
|
+
if (similarity > bestScore) {
|
|
101
|
+
bestScore = similarity;
|
|
102
|
+
bestStart = i;
|
|
103
|
+
bestEnd = i + windowSize;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (bestScore >= threshold && bestStart >= 0) {
|
|
107
|
+
let startCharIdx = 0;
|
|
108
|
+
for (let i = 0; i < bestStart; i++) {
|
|
109
|
+
startCharIdx += contentLines[i].length + 1;
|
|
110
|
+
}
|
|
111
|
+
let endCharIdx = startCharIdx;
|
|
112
|
+
for (let i = bestStart; i < bestEnd; i++) {
|
|
113
|
+
endCharIdx += contentLines[i].length + (i < bestEnd - 1 ? 1 : 0);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
found: true,
|
|
117
|
+
startIndex: startCharIdx,
|
|
118
|
+
endIndex: endCharIdx,
|
|
119
|
+
confidence: Math.round(bestScore * 100) / 100,
|
|
120
|
+
matchedText: contentLines.slice(bestStart, bestEnd).join("\n"),
|
|
121
|
+
method: "line-fuzzy"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function fuzzyEdit(content, oldString, newString, threshold = 0.85) {
|
|
127
|
+
const match = fuzzyMatch(content, oldString, threshold);
|
|
128
|
+
if (!match) return null;
|
|
129
|
+
const result = content.slice(0, match.startIndex) + newString + content.slice(match.endIndex);
|
|
130
|
+
return { content: result, match };
|
|
131
|
+
}
|
|
132
|
+
function mapNormalizedToOriginal(original, _normalized, normNeedle, normIdx) {
|
|
133
|
+
const normLines = _normalized.split("\n");
|
|
134
|
+
const origLines = original.split("\n");
|
|
135
|
+
let charCount = 0;
|
|
136
|
+
let startLine = 0;
|
|
137
|
+
for (let i = 0; i < normLines.length; i++) {
|
|
138
|
+
if (charCount + normLines[i].length >= normIdx) {
|
|
139
|
+
startLine = i;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
charCount += normLines[i].length + 1;
|
|
143
|
+
}
|
|
144
|
+
const needleLineCount = normNeedle.split("\n").length;
|
|
145
|
+
const endLine = startLine + needleLineCount;
|
|
146
|
+
let startChar = 0;
|
|
147
|
+
for (let i = 0; i < startLine; i++) {
|
|
148
|
+
startChar += origLines[i].length + 1;
|
|
149
|
+
}
|
|
150
|
+
let endChar = startChar;
|
|
151
|
+
for (let i = startLine; i < endLine && i < origLines.length; i++) {
|
|
152
|
+
endChar += origLines[i].length + (i < endLine - 1 ? 1 : 0);
|
|
153
|
+
}
|
|
154
|
+
return { start: startChar, end: endChar };
|
|
155
|
+
}
|
|
156
|
+
function mapStrippedToOriginal(original, stripped, stripNeedle, stripIdx) {
|
|
157
|
+
return mapNormalizedToOriginal(original, stripped, stripNeedle, stripIdx);
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
fuzzyEdit,
|
|
161
|
+
fuzzyMatch
|
|
162
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
const CANONICAL_HOOKS = [
|
|
9
|
+
{
|
|
10
|
+
scriptName: "session-rescue.sh",
|
|
11
|
+
eventType: "Stop",
|
|
12
|
+
timeout: 12,
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
scriptName: "stop-checkpoint.js",
|
|
17
|
+
eventType: "Stop",
|
|
18
|
+
timeout: 5,
|
|
19
|
+
commandPrefix: "node",
|
|
20
|
+
required: true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
scriptName: "chime-on-stop.sh",
|
|
24
|
+
eventType: "Stop",
|
|
25
|
+
timeout: 2,
|
|
26
|
+
required: true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
scriptName: "auto-checkpoint.js",
|
|
30
|
+
eventType: "PostToolUse",
|
|
31
|
+
timeout: 2,
|
|
32
|
+
commandPrefix: "node",
|
|
33
|
+
required: true
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
const DEAD_HOOKS = ["sms-response-handler.js"];
|
|
37
|
+
function buildCommand(entry, hooksDir) {
|
|
38
|
+
const scriptPath = path.join(hooksDir, entry.scriptName);
|
|
39
|
+
if (entry.commandPrefix) {
|
|
40
|
+
return `${entry.commandPrefix} ${scriptPath}`;
|
|
41
|
+
}
|
|
42
|
+
return scriptPath;
|
|
43
|
+
}
|
|
44
|
+
function hookExists(settings, entry) {
|
|
45
|
+
const groups = settings.hooks?.[entry.eventType];
|
|
46
|
+
if (!groups) return false;
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
for (const hook of group.hooks) {
|
|
49
|
+
if (hook.command.includes(entry.scriptName)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function hasDeadHooks(settings) {
|
|
57
|
+
if (!settings.hooks) return false;
|
|
58
|
+
for (const groups of Object.values(settings.hooks)) {
|
|
59
|
+
for (const group of groups) {
|
|
60
|
+
for (const hook of group.hooks) {
|
|
61
|
+
for (const dead of DEAD_HOOKS) {
|
|
62
|
+
if (hook.command.includes(dead)) return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function removeDeadHooks(settings) {
|
|
70
|
+
if (!settings.hooks) return false;
|
|
71
|
+
let removed = false;
|
|
72
|
+
for (const eventType of Object.keys(settings.hooks)) {
|
|
73
|
+
const groups = settings.hooks[eventType];
|
|
74
|
+
for (const group of groups) {
|
|
75
|
+
const before = group.hooks.length;
|
|
76
|
+
group.hooks = group.hooks.filter((hook) => {
|
|
77
|
+
for (const dead of DEAD_HOOKS) {
|
|
78
|
+
if (hook.command.includes(dead)) return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
if (group.hooks.length < before) removed = true;
|
|
83
|
+
}
|
|
84
|
+
settings.hooks[eventType] = groups.filter((g) => g.hooks.length > 0);
|
|
85
|
+
if (settings.hooks[eventType].length === 0) {
|
|
86
|
+
delete settings.hooks[eventType];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return removed;
|
|
90
|
+
}
|
|
91
|
+
function addHook(settings, entry, hooksDir) {
|
|
92
|
+
if (!settings.hooks) settings.hooks = {};
|
|
93
|
+
const eventGroups = settings.hooks[entry.eventType] || [];
|
|
94
|
+
const command = buildCommand(entry, hooksDir);
|
|
95
|
+
const hookCmd = { type: "command", command };
|
|
96
|
+
if (entry.timeout) hookCmd.timeout = entry.timeout;
|
|
97
|
+
const matcherValue = entry.matcher ?? void 0;
|
|
98
|
+
const targetGroup = eventGroups.find((g) => {
|
|
99
|
+
if (matcherValue) return g.matcher === matcherValue;
|
|
100
|
+
return !g.matcher;
|
|
101
|
+
});
|
|
102
|
+
if (targetGroup) {
|
|
103
|
+
targetGroup.hooks.push(hookCmd);
|
|
104
|
+
} else {
|
|
105
|
+
const newGroup = { hooks: [hookCmd] };
|
|
106
|
+
if (matcherValue) newGroup.matcher = matcherValue;
|
|
107
|
+
eventGroups.push(newGroup);
|
|
108
|
+
}
|
|
109
|
+
settings.hooks[entry.eventType] = eventGroups;
|
|
110
|
+
}
|
|
111
|
+
function mergeSettings(existing, hooksDir) {
|
|
112
|
+
const merged = JSON.parse(JSON.stringify(existing));
|
|
113
|
+
removeDeadHooks(merged);
|
|
114
|
+
for (const entry of CANONICAL_HOOKS) {
|
|
115
|
+
if (!hookExists(merged, entry)) {
|
|
116
|
+
addHook(merged, entry, hooksDir);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return merged;
|
|
120
|
+
}
|
|
121
|
+
function getSettingsPath() {
|
|
122
|
+
return path.join(os.homedir(), ".claude", "settings.json");
|
|
123
|
+
}
|
|
124
|
+
function readSettings(settingsPath) {
|
|
125
|
+
const p = settingsPath ?? getSettingsPath();
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(p)) {
|
|
128
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
function writeSettingsAtomic(settings, settingsPath) {
|
|
135
|
+
const p = settingsPath ?? getSettingsPath();
|
|
136
|
+
const dir = path.dirname(p);
|
|
137
|
+
if (!fs.existsSync(dir)) {
|
|
138
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
const tmp = p + ".tmp";
|
|
141
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
142
|
+
fs.renameSync(tmp, p);
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
CANONICAL_HOOKS,
|
|
146
|
+
DEAD_HOOKS,
|
|
147
|
+
buildCommand,
|
|
148
|
+
getSettingsPath,
|
|
149
|
+
hasDeadHooks,
|
|
150
|
+
hookExists,
|
|
151
|
+
mergeSettings,
|
|
152
|
+
readSettings,
|
|
153
|
+
removeDeadHooks,
|
|
154
|
+
writeSettingsAtomic
|
|
155
|
+
};
|