@slowcook-ai/cli 0.12.12 → 0.13.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/dist/cli.js +41 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/brew/agent.d.ts.map +1 -1
- package/dist/commands/brew/agent.js +19 -6
- package/dist/commands/brew/agent.js.map +1 -1
- package/dist/commands/chef/classify.d.ts +65 -0
- package/dist/commands/chef/classify.d.ts.map +1 -0
- package/dist/commands/chef/classify.js +102 -0
- package/dist/commands/chef/classify.js.map +1 -0
- package/dist/commands/chef/index.d.ts +22 -0
- package/dist/commands/chef/index.d.ts.map +1 -0
- package/dist/commands/chef/index.js +287 -0
- package/dist/commands/chef/index.js.map +1 -0
- package/dist/commands/dispatch/index.d.ts.map +1 -1
- package/dist/commands/dispatch/index.js +10 -3
- package/dist/commands/dispatch/index.js.map +1 -1
- package/dist/commands/investigate/agent.d.ts +68 -0
- package/dist/commands/investigate/agent.d.ts.map +1 -0
- package/dist/commands/investigate/agent.js +503 -0
- package/dist/commands/investigate/agent.js.map +1 -0
- package/dist/commands/investigate/index.d.ts +43 -0
- package/dist/commands/investigate/index.d.ts.map +1 -0
- package/dist/commands/investigate/index.js +413 -0
- package/dist/commands/investigate/index.js.map +1 -0
- package/dist/commands/investigate/prompts.d.ts +90 -0
- package/dist/commands/investigate/prompts.d.ts.map +1 -0
- package/dist/commands/investigate/prompts.js +237 -0
- package/dist/commands/investigate/prompts.js.map +1 -0
- package/dist/commands/investigate/schema.d.ts +91 -0
- package/dist/commands/investigate/schema.d.ts.map +1 -0
- package/dist/commands/investigate/schema.js +87 -0
- package/dist/commands/investigate/schema.js.map +1 -0
- package/dist/commands/on-brew-merged/index.d.ts.map +1 -1
- package/dist/commands/on-brew-merged/index.js +27 -6
- package/dist/commands/on-brew-merged/index.js.map +1 -1
- package/dist/commands/recipe-regression/agent.d.ts +94 -0
- package/dist/commands/recipe-regression/agent.d.ts.map +1 -0
- package/dist/commands/recipe-regression/agent.js +442 -0
- package/dist/commands/recipe-regression/agent.js.map +1 -0
- package/dist/commands/recipe-regression/index.d.ts +61 -0
- package/dist/commands/recipe-regression/index.d.ts.map +1 -0
- package/dist/commands/recipe-regression/index.js +187 -0
- package/dist/commands/recipe-regression/index.js.map +1 -0
- package/dist/commands/sift/agent.d.ts +52 -0
- package/dist/commands/sift/agent.d.ts.map +1 -0
- package/dist/commands/sift/agent.js +392 -0
- package/dist/commands/sift/agent.js.map +1 -0
- package/dist/commands/sift/index.d.ts +23 -0
- package/dist/commands/sift/index.d.ts.map +1 -0
- package/dist/commands/sift/index.js +314 -0
- package/dist/commands/sift/index.js.map +1 -0
- package/dist/commands/sift/prompts.d.ts +114 -0
- package/dist/commands/sift/prompts.d.ts.map +1 -0
- package/dist/commands/sift/prompts.js +193 -0
- package/dist/commands/sift/prompts.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `slowcook recipe --regression --bug B-<n>` — emits a regression
|
|
3
|
+
* test from a bug-profile.
|
|
4
|
+
*
|
|
5
|
+
* **Status: alpha.3a**. Stub-only emitter: writes a deterministic
|
|
6
|
+
* vitest skeleton at `tests/regression/B-<n>-<slug>.test.ts` that
|
|
7
|
+
* asserts via `expect.fail()` so the test is red until sift replaces
|
|
8
|
+
* the body with real assertions (alpha.4) or alpha.3b upgrades this
|
|
9
|
+
* emitter to write real tests via an LLM agent.
|
|
10
|
+
*
|
|
11
|
+
* The skeleton structure is what sift expects to see:
|
|
12
|
+
* - one `describe` named for the bug id + title
|
|
13
|
+
* - one `it` per regression_assertion line
|
|
14
|
+
* - body of each `it` calls `expect.fail(...)` referencing the bug
|
|
15
|
+
* profile so the failure message points the operator at the right
|
|
16
|
+
* artefact when the test runs.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* slowcook recipe --regression --bug B-1 [--cwd <path>]
|
|
20
|
+
*
|
|
21
|
+
* Internally invoked from the CLI's `recipe`/`testgen` dispatch when
|
|
22
|
+
* `--regression` is present (see cli.ts wiring).
|
|
23
|
+
*/
|
|
24
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { validateBugProfile } from "../investigate/schema.js";
|
|
27
|
+
import { parseSimpleYaml } from "../investigate/agent.js";
|
|
28
|
+
import { runRegressionRecipe } from "./agent.js";
|
|
29
|
+
export function parseRecipeRegressionArgs(argv) {
|
|
30
|
+
const args = {
|
|
31
|
+
bugId: "",
|
|
32
|
+
repoRoot: process.cwd(),
|
|
33
|
+
dryRun: false,
|
|
34
|
+
useLlm: false,
|
|
35
|
+
model: "claude-sonnet-4-6",
|
|
36
|
+
};
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
const arg = argv[i];
|
|
39
|
+
const next = argv[i + 1];
|
|
40
|
+
if (arg === "--bug" && next) {
|
|
41
|
+
args.bugId = normaliseBugId(next);
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
else if (arg === "--cwd" && next) {
|
|
45
|
+
args.repoRoot = next;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else if (arg === "--dry-run") {
|
|
49
|
+
args.dryRun = true;
|
|
50
|
+
}
|
|
51
|
+
else if (arg === "--llm") {
|
|
52
|
+
args.useLlm = true;
|
|
53
|
+
}
|
|
54
|
+
else if (arg === "--model" && next) {
|
|
55
|
+
args.model = next;
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Accept "1", "B-1", or "B1" and normalise to "B-<n>".
|
|
63
|
+
*/
|
|
64
|
+
export function normaliseBugId(raw) {
|
|
65
|
+
const m = raw.trim().match(/^B?-?(\d+)$/i);
|
|
66
|
+
if (!m || !m[1])
|
|
67
|
+
return raw;
|
|
68
|
+
return `B-${m[1]}`;
|
|
69
|
+
}
|
|
70
|
+
export async function recipeRegression(argv, cliVersion) {
|
|
71
|
+
const args = parseRecipeRegressionArgs(argv);
|
|
72
|
+
if (!args.bugId) {
|
|
73
|
+
console.error("slowcook recipe --regression: --bug <id> is required");
|
|
74
|
+
process.exit(64);
|
|
75
|
+
}
|
|
76
|
+
const profile = loadBugProfile(args.repoRoot, args.bugId);
|
|
77
|
+
const slug = slugFromTitle(profile.title);
|
|
78
|
+
const relPath = `tests/regression/${profile.bug_id}-${slug}.test.ts`;
|
|
79
|
+
let contents;
|
|
80
|
+
if (args.useLlm) {
|
|
81
|
+
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
82
|
+
if (!apiKey) {
|
|
83
|
+
console.error("slowcook recipe --regression --llm: ANTHROPIC_API_KEY required (or omit --llm for the stub emitter)");
|
|
84
|
+
process.exit(78);
|
|
85
|
+
}
|
|
86
|
+
console.error(`slowcook recipe --regression (${cliVersion}) — LLM mode (model ${args.model}) for ${profile.bug_id}.`);
|
|
87
|
+
const result = await runRegressionRecipe({
|
|
88
|
+
repoRoot: args.repoRoot,
|
|
89
|
+
anthropicApiKey: apiKey,
|
|
90
|
+
model: args.model,
|
|
91
|
+
bugProfile: profile,
|
|
92
|
+
cliVersion,
|
|
93
|
+
});
|
|
94
|
+
console.error(`Agent done: ${result.rounds} round(s), $${result.spendUsd.toFixed(4)} spent.`);
|
|
95
|
+
if (!result.emitted) {
|
|
96
|
+
console.error(`slowcook recipe --regression: agent halted. ${result.haltReason ?? "(no reason)"}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
contents = result.testContents ?? "";
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const file = renderRegressionStub(profile, cliVersion);
|
|
103
|
+
contents = file.contents;
|
|
104
|
+
}
|
|
105
|
+
if (args.dryRun) {
|
|
106
|
+
console.log(contents);
|
|
107
|
+
console.error(`\n(dry-run: would write ${relPath})`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const fullPath = join(args.repoRoot, relPath);
|
|
111
|
+
mkdirSync(join(args.repoRoot, "tests/regression"), { recursive: true });
|
|
112
|
+
writeFileSync(fullPath, contents, "utf8");
|
|
113
|
+
console.error(`Wrote ${relPath}${args.useLlm ? " (LLM-emitted)" : " (alpha.3a stub — sift will replace expect.fail bodies)"}.`);
|
|
114
|
+
}
|
|
115
|
+
function slugFromTitle(title) {
|
|
116
|
+
return title
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
119
|
+
.replace(/^-+|-+$/g, "")
|
|
120
|
+
.slice(0, 60);
|
|
121
|
+
}
|
|
122
|
+
export function loadBugProfile(repoRoot, bugId) {
|
|
123
|
+
const path = join(repoRoot, ".brewing/bug-profiles", `${bugId}.yaml`);
|
|
124
|
+
if (!existsSync(path)) {
|
|
125
|
+
throw new Error(`bug profile not found: ${path}. Run 'slowcook investigate --issue <n>' first.`);
|
|
126
|
+
}
|
|
127
|
+
const raw = parseSimpleYaml(readFileSync(path, "utf8"));
|
|
128
|
+
const validation = validateBugProfile(raw);
|
|
129
|
+
if (!validation.ok) {
|
|
130
|
+
throw new Error(`bug profile at ${path} is invalid:\n ${validation.errors.join("\n ")}`);
|
|
131
|
+
}
|
|
132
|
+
return validation.profile;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build the stub regression test for a bug profile. Deterministic —
|
|
136
|
+
* same profile → same file. Sift overwrites with real assertions in
|
|
137
|
+
* alpha.4; alpha.3b will replace this stub emitter with an LLM-written
|
|
138
|
+
* real test.
|
|
139
|
+
*/
|
|
140
|
+
export function renderRegressionStub(profile, cliVersion) {
|
|
141
|
+
const slug = slugify(profile.title);
|
|
142
|
+
const path = `tests/regression/${profile.bug_id}-${slug}.test.ts`;
|
|
143
|
+
const lines = [];
|
|
144
|
+
lines.push(`// slowcook ${cliVersion} regression test — ${profile.bug_id}`);
|
|
145
|
+
lines.push(`//`);
|
|
146
|
+
lines.push(`// Bug profile: .brewing/bug-profiles/${profile.bug_id}.yaml`);
|
|
147
|
+
lines.push(`// Source issue: ${profile.source_issue}`);
|
|
148
|
+
lines.push(`// Failure locus: ${profile.failure_locus.file}${profile.failure_locus.line ? `:${profile.failure_locus.line}` : ""}`);
|
|
149
|
+
lines.push(`//`);
|
|
150
|
+
lines.push(`// alpha.3a stub: each \`it\` block calls expect.fail(...) so the`);
|
|
151
|
+
lines.push(`// test is red against current code. Sift (alpha.4) replaces the`);
|
|
152
|
+
lines.push(`// body with real assertions that exercise the regression. Once`);
|
|
153
|
+
lines.push(`// alpha.3b ships an LLM-backed emitter, this comment header goes`);
|
|
154
|
+
lines.push(`// away and the body is real from the start.`);
|
|
155
|
+
lines.push(``);
|
|
156
|
+
lines.push(`import { describe, it, expect } from "vitest";`);
|
|
157
|
+
lines.push(``);
|
|
158
|
+
lines.push(`describe(${jsonString(`${profile.bug_id} regression — ${profile.title}`)}, () => {`);
|
|
159
|
+
for (let i = 0; i < profile.regression_assertion.length; i++) {
|
|
160
|
+
const assertion = profile.regression_assertion[i] ?? "";
|
|
161
|
+
lines.push(` it(${jsonString(assertion)}, () => {`);
|
|
162
|
+
lines.push(` // Diagnosis: ${oneLine(profile.failure_locus.diagnosis)}`);
|
|
163
|
+
lines.push(` // Expected: ${oneLine(profile.expected[0] ?? "(not specified)")}`);
|
|
164
|
+
lines.push(` // alpha.3a stub — sift / alpha.3b replaces this body.`);
|
|
165
|
+
lines.push(` expect.fail(${jsonString(`Regression test stub for ${profile.bug_id}. See .brewing/bug-profiles/${profile.bug_id}.yaml. Sift will replace this body once it lands.`)});`);
|
|
166
|
+
lines.push(` });`);
|
|
167
|
+
if (i < profile.regression_assertion.length - 1)
|
|
168
|
+
lines.push(``);
|
|
169
|
+
}
|
|
170
|
+
lines.push(`});`);
|
|
171
|
+
lines.push(``);
|
|
172
|
+
return { path, contents: lines.join("\n") };
|
|
173
|
+
}
|
|
174
|
+
function slugify(s) {
|
|
175
|
+
return s
|
|
176
|
+
.toLowerCase()
|
|
177
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
178
|
+
.replace(/^-+|-+$/g, "")
|
|
179
|
+
.slice(0, 60);
|
|
180
|
+
}
|
|
181
|
+
function oneLine(s) {
|
|
182
|
+
return s.split("\n").join(" ").trim();
|
|
183
|
+
}
|
|
184
|
+
function jsonString(s) {
|
|
185
|
+
return JSON.stringify(s);
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/recipe-regression/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sift agent loop. Narrow red→green ratchet for bug fixes.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors brew's iter loop in shape but smaller in every dimension:
|
|
5
|
+
* - max 3 iterations (vs brew's 10)
|
|
6
|
+
* - $0.50 budget cap (vs brew's $10)
|
|
7
|
+
* - allowed_paths restricted to bug-profile.fix_scope
|
|
8
|
+
* - test-runner is scoped to the regression test only
|
|
9
|
+
* - no story manifest; the contract is just the regression test
|
|
10
|
+
*
|
|
11
|
+
* **Status: alpha.4a**. Single-iteration loop with no ratchet/revert.
|
|
12
|
+
* Iteration loop + multi-turn ratchet ships in alpha.4b alongside
|
|
13
|
+
* a brew/sift shared-engine refactor.
|
|
14
|
+
*/
|
|
15
|
+
import { type StackConfig, type RunResult } from "@slowcook-ai/stack-ts";
|
|
16
|
+
import { type BugProfile } from "../investigate/schema.js";
|
|
17
|
+
export interface SiftContext {
|
|
18
|
+
repoRoot: string;
|
|
19
|
+
anthropicApiKey: string;
|
|
20
|
+
model: string;
|
|
21
|
+
bugProfile: BugProfile;
|
|
22
|
+
/** Repo-relative path to the regression test file. */
|
|
23
|
+
regressionTestPath: string;
|
|
24
|
+
/** Read once at construction; passed through prompts. */
|
|
25
|
+
regressionTestSrc: string;
|
|
26
|
+
stackConfig: StackConfig;
|
|
27
|
+
/** Hard cap (seconds spend); default 3. */
|
|
28
|
+
maxIterations: number;
|
|
29
|
+
/** USD spend cap; default 0.5. */
|
|
30
|
+
budgetUsd: number;
|
|
31
|
+
now?: () => Date;
|
|
32
|
+
}
|
|
33
|
+
export interface SiftResult {
|
|
34
|
+
/** True when the regression test ended green. */
|
|
35
|
+
green: boolean;
|
|
36
|
+
/** Total iterations the agent took. */
|
|
37
|
+
iterations: number;
|
|
38
|
+
/** Total spend in USD. */
|
|
39
|
+
spendUsd: number;
|
|
40
|
+
/** Files written across the run. */
|
|
41
|
+
filesTouched: string[];
|
|
42
|
+
/** When green=false, why the run ended (`budget`, `iters`, `halt:<reason>`, etc.). */
|
|
43
|
+
haltReason?: string;
|
|
44
|
+
/** Last test result for diagnostics. */
|
|
45
|
+
lastTestResult: RunResult | null;
|
|
46
|
+
}
|
|
47
|
+
export declare function runSift(ctx: SiftContext): Promise<SiftResult>;
|
|
48
|
+
export declare function isInFixScope(path: string, fixScope: ReadonlyArray<string>): boolean;
|
|
49
|
+
export declare function regressionStatus(result: RunResult, regressionPath: string): "red" | "green";
|
|
50
|
+
export declare function regressionFailureMessage(result: RunResult, regressionPath: string): string | undefined;
|
|
51
|
+
export declare function parseHalt(text: string): string | null;
|
|
52
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/commands/sift/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,OAAO,EAEL,KAAK,WAAW,EAEhB,KAAK,SAAS,EACf,MAAM,uBAAuB,CAAC;AAQ/B,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAK3D,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,UAAU,CAAC;IACvB,sDAAsD;IACtD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yDAAyD;IACzD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,iDAAiD;IACjD,KAAK,EAAE,OAAO,CAAC;IACf,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,cAAc,EAAE,SAAS,GAAG,IAAI,CAAC;CAClC;AAED,wBAAsB,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAqInE;AAwJD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAC9B,OAAO,CAWT;AAMD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,cAAc,EAAE,MAAM,GACrB,KAAK,GAAG,OAAO,CAWjB;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,SAAS,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAKpB;AAMD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMrD"}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sift agent loop. Narrow red→green ratchet for bug fixes.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors brew's iter loop in shape but smaller in every dimension:
|
|
5
|
+
* - max 3 iterations (vs brew's 10)
|
|
6
|
+
* - $0.50 budget cap (vs brew's $10)
|
|
7
|
+
* - allowed_paths restricted to bug-profile.fix_scope
|
|
8
|
+
* - test-runner is scoped to the regression test only
|
|
9
|
+
* - no story manifest; the contract is just the regression test
|
|
10
|
+
*
|
|
11
|
+
* **Status: alpha.4a**. Single-iteration loop with no ratchet/revert.
|
|
12
|
+
* Iteration loop + multi-turn ratchet ships in alpha.4b alongside
|
|
13
|
+
* a brew/sift shared-engine refactor.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, } from "node:fs";
|
|
16
|
+
import { dirname, resolve, isAbsolute } from "node:path";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
19
|
+
import { validateStackConfig, runTests, } from "@slowcook-ai/stack-ts";
|
|
20
|
+
import { outlineFile } from "../brew/agent.js";
|
|
21
|
+
import { findReferences, findDefinition, renderReferences, } from "../brew/retrieval.js";
|
|
22
|
+
import { SIFT_SYSTEM, SIFT_TOOLS, buildSiftTurnPrompt } from "./prompts.js";
|
|
23
|
+
const MAX_ROUNDS_PER_ITER = 8;
|
|
24
|
+
const MAX_FILE_READ_BYTES = 20000;
|
|
25
|
+
export async function runSift(ctx) {
|
|
26
|
+
const anthropic = new Anthropic({ apiKey: ctx.anthropicApiKey });
|
|
27
|
+
const filesTouched = new Set();
|
|
28
|
+
let spendUsd = 0;
|
|
29
|
+
let lastTestResult = null;
|
|
30
|
+
const priorEdits = [];
|
|
31
|
+
for (let iter = 1; iter <= ctx.maxIterations; iter++) {
|
|
32
|
+
if (spendUsd >= ctx.budgetUsd) {
|
|
33
|
+
return {
|
|
34
|
+
green: false,
|
|
35
|
+
iterations: iter - 1,
|
|
36
|
+
spendUsd,
|
|
37
|
+
filesTouched: [...filesTouched],
|
|
38
|
+
haltReason: `budget (spent $${spendUsd.toFixed(4)} of $${ctx.budgetUsd})`,
|
|
39
|
+
lastTestResult,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const turnPrompt = buildSiftTurnPrompt({
|
|
43
|
+
iteration: iter,
|
|
44
|
+
maxIterations: ctx.maxIterations,
|
|
45
|
+
bugProfileYaml: bugProfileToYamlSummary(ctx.bugProfile),
|
|
46
|
+
regressionTestPath: ctx.regressionTestPath,
|
|
47
|
+
regressionTestSrc: ctx.regressionTestSrc,
|
|
48
|
+
testResult: lastTestResult
|
|
49
|
+
? {
|
|
50
|
+
status: regressionStatus(lastTestResult, ctx.regressionTestPath),
|
|
51
|
+
failureMessage: regressionFailureMessage(lastTestResult, ctx.regressionTestPath),
|
|
52
|
+
}
|
|
53
|
+
: undefined,
|
|
54
|
+
priorEdits,
|
|
55
|
+
});
|
|
56
|
+
const messages = [
|
|
57
|
+
{ role: "user", content: turnPrompt },
|
|
58
|
+
];
|
|
59
|
+
let haltReason = null;
|
|
60
|
+
let editsThisTurn = 0;
|
|
61
|
+
for (let round = 0; round < MAX_ROUNDS_PER_ITER; round++) {
|
|
62
|
+
const response = await anthropic.messages.create({
|
|
63
|
+
model: ctx.model,
|
|
64
|
+
max_tokens: 4096,
|
|
65
|
+
system: SIFT_SYSTEM,
|
|
66
|
+
tools: SIFT_TOOLS,
|
|
67
|
+
messages,
|
|
68
|
+
});
|
|
69
|
+
spendUsd += costUsd(response, ctx.model);
|
|
70
|
+
// Look for <halt> in any text block.
|
|
71
|
+
for (const block of response.content) {
|
|
72
|
+
if (block.type === "text") {
|
|
73
|
+
const halt = parseHalt(block.text);
|
|
74
|
+
if (halt !== null)
|
|
75
|
+
haltReason = halt;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (haltReason !== null)
|
|
79
|
+
break;
|
|
80
|
+
const toolUses = response.content.filter((b) => b.type === "tool_use");
|
|
81
|
+
if (response.stop_reason !== "tool_use" || toolUses.length === 0)
|
|
82
|
+
break;
|
|
83
|
+
const toolResults = toolUses.map((t) => {
|
|
84
|
+
const result = executeTool(ctx, t);
|
|
85
|
+
if (result.touched) {
|
|
86
|
+
filesTouched.add(result.touched);
|
|
87
|
+
editsThisTurn += 1;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
type: "tool_result",
|
|
91
|
+
tool_use_id: t.id,
|
|
92
|
+
content: result.content,
|
|
93
|
+
is_error: result.is_error,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
messages.push({ role: "assistant", content: response.content });
|
|
97
|
+
messages.push({ role: "user", content: toolResults });
|
|
98
|
+
}
|
|
99
|
+
if (haltReason !== null) {
|
|
100
|
+
return {
|
|
101
|
+
green: haltReason.toLowerCase().includes("regression green"),
|
|
102
|
+
iterations: iter,
|
|
103
|
+
spendUsd,
|
|
104
|
+
filesTouched: [...filesTouched],
|
|
105
|
+
haltReason: `halt:${haltReason}`,
|
|
106
|
+
lastTestResult,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (editsThisTurn === 0) {
|
|
110
|
+
return {
|
|
111
|
+
green: false,
|
|
112
|
+
iterations: iter,
|
|
113
|
+
spendUsd,
|
|
114
|
+
filesTouched: [...filesTouched],
|
|
115
|
+
haltReason: "agent made no edits this iteration",
|
|
116
|
+
lastTestResult,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
priorEdits.push(...[...filesTouched].slice(priorEdits.length));
|
|
120
|
+
// Run the regression test scoped to the bug's test file.
|
|
121
|
+
lastTestResult = runTests(ctx.stackConfig, {
|
|
122
|
+
cwd: ctx.repoRoot,
|
|
123
|
+
scopeFiles: [ctx.regressionTestPath],
|
|
124
|
+
});
|
|
125
|
+
const status = regressionStatus(lastTestResult, ctx.regressionTestPath);
|
|
126
|
+
if (status === "green") {
|
|
127
|
+
return {
|
|
128
|
+
green: true,
|
|
129
|
+
iterations: iter,
|
|
130
|
+
spendUsd,
|
|
131
|
+
filesTouched: [...filesTouched],
|
|
132
|
+
lastTestResult,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
green: false,
|
|
138
|
+
iterations: ctx.maxIterations,
|
|
139
|
+
spendUsd,
|
|
140
|
+
filesTouched: [...filesTouched],
|
|
141
|
+
haltReason: `iters (${ctx.maxIterations} reached without green)`,
|
|
142
|
+
lastTestResult,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function executeTool(ctx, tool) {
|
|
146
|
+
const input = tool.input;
|
|
147
|
+
try {
|
|
148
|
+
switch (tool.name) {
|
|
149
|
+
case "read_file":
|
|
150
|
+
return readFile(ctx.repoRoot, String(input["path"] ?? ""));
|
|
151
|
+
case "outline_file":
|
|
152
|
+
return outlineCmd(ctx.repoRoot, String(input["path"] ?? ""));
|
|
153
|
+
case "list_directory":
|
|
154
|
+
return listDir(ctx.repoRoot, String(input["path"] ?? ""));
|
|
155
|
+
case "find_references": {
|
|
156
|
+
const symbol = String(input["symbol"] ?? "").trim();
|
|
157
|
+
if (!symbol)
|
|
158
|
+
return { content: "symbol is required", is_error: true };
|
|
159
|
+
const refs = findReferences(ctx.repoRoot, symbol, { excludeDefinitions: false });
|
|
160
|
+
return { content: renderReferences(refs), is_error: false };
|
|
161
|
+
}
|
|
162
|
+
case "find_definition": {
|
|
163
|
+
const symbol = String(input["symbol"] ?? "").trim();
|
|
164
|
+
if (!symbol)
|
|
165
|
+
return { content: "symbol is required", is_error: true };
|
|
166
|
+
const def = findDefinition(ctx.repoRoot, symbol);
|
|
167
|
+
if (!def)
|
|
168
|
+
return { content: `(no declaration found for ${symbol})`, is_error: false };
|
|
169
|
+
return {
|
|
170
|
+
content: `${def.kind} | ${def.file}:${def.line}:${def.column} | ${def.context}`,
|
|
171
|
+
is_error: false,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
case "grep":
|
|
175
|
+
return runGrep(ctx.repoRoot, String(input["pattern"] ?? ""), input["glob"] ? String(input["glob"]) : undefined);
|
|
176
|
+
case "write_file":
|
|
177
|
+
return writeFile(ctx, String(input["path"] ?? ""), String(input["contents"] ?? ""));
|
|
178
|
+
default:
|
|
179
|
+
return { content: `Unknown tool: ${tool.name}`, is_error: true };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
return { content: `Tool error: ${e.message}`, is_error: true };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function readFile(repoRoot, p) {
|
|
187
|
+
if (!isPathSafe(repoRoot, p))
|
|
188
|
+
return { content: `Path escape forbidden: ${p}`, is_error: true };
|
|
189
|
+
const full = resolve(repoRoot, p);
|
|
190
|
+
if (!existsSync(full))
|
|
191
|
+
return { content: `File not found: ${p}`, is_error: true };
|
|
192
|
+
if (!statSync(full).isFile())
|
|
193
|
+
return { content: `Not a file: ${p}`, is_error: true };
|
|
194
|
+
const txt = readFileSync(full, "utf8");
|
|
195
|
+
return {
|
|
196
|
+
content: txt.length > MAX_FILE_READ_BYTES
|
|
197
|
+
? txt.slice(0, MAX_FILE_READ_BYTES) + "\n…(truncated)"
|
|
198
|
+
: txt,
|
|
199
|
+
is_error: false,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function outlineCmd(repoRoot, p) {
|
|
203
|
+
if (!isPathSafe(repoRoot, p))
|
|
204
|
+
return { content: `Path escape forbidden: ${p}`, is_error: true };
|
|
205
|
+
const full = resolve(repoRoot, p);
|
|
206
|
+
if (!existsSync(full))
|
|
207
|
+
return { content: `File not found: ${p}`, is_error: true };
|
|
208
|
+
if (!statSync(full).isFile())
|
|
209
|
+
return { content: `Not a file: ${p}`, is_error: true };
|
|
210
|
+
const txt = readFileSync(full, "utf8");
|
|
211
|
+
return { content: outlineFile(p, txt), is_error: false };
|
|
212
|
+
}
|
|
213
|
+
function listDir(repoRoot, p) {
|
|
214
|
+
if (!isPathSafe(repoRoot, p))
|
|
215
|
+
return { content: `Path escape forbidden: ${p}`, is_error: true };
|
|
216
|
+
const full = resolve(repoRoot, p);
|
|
217
|
+
if (!existsSync(full))
|
|
218
|
+
return { content: `Not found: ${p}`, is_error: true };
|
|
219
|
+
if (!statSync(full).isDirectory())
|
|
220
|
+
return { content: `Not a directory: ${p}`, is_error: true };
|
|
221
|
+
const entries = readdirSync(full, { withFileTypes: true })
|
|
222
|
+
.map((e) => `${e.name}${e.isDirectory() ? "/" : ""}`)
|
|
223
|
+
.sort()
|
|
224
|
+
.join("\n");
|
|
225
|
+
return { content: entries, is_error: false };
|
|
226
|
+
}
|
|
227
|
+
function writeFile(ctx, p, contents) {
|
|
228
|
+
if (!isPathSafe(ctx.repoRoot, p)) {
|
|
229
|
+
return { content: `Path escape forbidden: ${p}`, is_error: true };
|
|
230
|
+
}
|
|
231
|
+
if (!isInFixScope(p, ctx.bugProfile.fix_scope)) {
|
|
232
|
+
return {
|
|
233
|
+
content: `${p} is outside the bug profile's fix_scope (${ctx.bugProfile.fix_scope.join(", ")}). Halt voluntarily and ask the operator to widen scope.`,
|
|
234
|
+
is_error: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const full = resolve(ctx.repoRoot, p);
|
|
238
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
239
|
+
writeFileSync(full, contents, "utf8");
|
|
240
|
+
return {
|
|
241
|
+
content: `Wrote ${contents.split("\n").length} lines to ${p}`,
|
|
242
|
+
is_error: false,
|
|
243
|
+
touched: p,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function runGrep(repoRoot, pattern, glob) {
|
|
247
|
+
if (!pattern)
|
|
248
|
+
return { content: "pattern is required", is_error: true };
|
|
249
|
+
const safePattern = pattern.replace(/'/g, "'\\''");
|
|
250
|
+
const cmd = glob
|
|
251
|
+
? `rg --line-number --max-count=50 -e '${safePattern}' --glob '${glob.replace(/'/g, "'\\''")}'`
|
|
252
|
+
: `rg --line-number --max-count=50 -e '${safePattern}'`;
|
|
253
|
+
try {
|
|
254
|
+
const out = execSync(cmd, {
|
|
255
|
+
cwd: repoRoot,
|
|
256
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
257
|
+
maxBuffer: 1024 * 256,
|
|
258
|
+
encoding: "utf8",
|
|
259
|
+
});
|
|
260
|
+
if (!out.trim())
|
|
261
|
+
return { content: "(no matches)", is_error: false };
|
|
262
|
+
return {
|
|
263
|
+
content: out.length > MAX_FILE_READ_BYTES
|
|
264
|
+
? out.slice(0, MAX_FILE_READ_BYTES) + "\n…(truncated)"
|
|
265
|
+
: out,
|
|
266
|
+
is_error: false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
const exit = e.status;
|
|
271
|
+
if (exit === 1)
|
|
272
|
+
return { content: "(no matches)", is_error: false };
|
|
273
|
+
return { content: `grep error: ${e.message}`, is_error: true };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function isPathSafe(repoRoot, relPath) {
|
|
277
|
+
if (isAbsolute(relPath))
|
|
278
|
+
return false;
|
|
279
|
+
const resolved = resolve(repoRoot, relPath);
|
|
280
|
+
return resolved.startsWith(resolve(repoRoot));
|
|
281
|
+
}
|
|
282
|
+
export function isInFixScope(path, fixScope) {
|
|
283
|
+
if (fixScope.length === 0)
|
|
284
|
+
return false;
|
|
285
|
+
const norm = path.replace(/^\.\/+/, "");
|
|
286
|
+
for (const scope of fixScope) {
|
|
287
|
+
const s = scope.replace(/^\.\/+/, "");
|
|
288
|
+
if (norm === s)
|
|
289
|
+
return true;
|
|
290
|
+
// Directory prefix match (with or without trailing slash).
|
|
291
|
+
const sDir = s.endsWith("/") ? s : `${s}/`;
|
|
292
|
+
if (norm.startsWith(sDir))
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
// -------------------------------------------------------------------------
|
|
298
|
+
// Test-result helpers
|
|
299
|
+
// -------------------------------------------------------------------------
|
|
300
|
+
export function regressionStatus(result, regressionPath) {
|
|
301
|
+
// Sift owns the regression test file; consider its tests only. If
|
|
302
|
+
// any are red OR errored, status is red.
|
|
303
|
+
const ours = result.tests.filter((t) => t.file.endsWith(regressionPath));
|
|
304
|
+
if (ours.length === 0) {
|
|
305
|
+
// Vitest may have crashed before reporting; treat as red.
|
|
306
|
+
return "red";
|
|
307
|
+
}
|
|
308
|
+
return ours.every((t) => t.status === "passed" || t.status === "skipped")
|
|
309
|
+
? "green"
|
|
310
|
+
: "red";
|
|
311
|
+
}
|
|
312
|
+
export function regressionFailureMessage(result, regressionPath) {
|
|
313
|
+
const ours = result.tests.filter((t) => t.file.endsWith(regressionPath) && t.status !== "passed");
|
|
314
|
+
return ours[0]?.failure_message;
|
|
315
|
+
}
|
|
316
|
+
// -------------------------------------------------------------------------
|
|
317
|
+
// Halt block parsing
|
|
318
|
+
// -------------------------------------------------------------------------
|
|
319
|
+
export function parseHalt(text) {
|
|
320
|
+
const m = text.match(/<halt>[\s\S]*?<reason>([\s\S]*?)<\/reason>[\s\S]*?<\/halt>/);
|
|
321
|
+
if (m && m[1])
|
|
322
|
+
return m[1].trim();
|
|
323
|
+
// Fallback: simple <halt>reason</halt>.
|
|
324
|
+
const fallback = text.match(/<halt>([\s\S]*?)<\/halt>/);
|
|
325
|
+
return fallback ? (fallback[1] ?? "").trim() : null;
|
|
326
|
+
}
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
// Cost accounting (mirrors investigate's; same pricing table)
|
|
329
|
+
// -------------------------------------------------------------------------
|
|
330
|
+
const PRICING_PER_M_TOKENS = {
|
|
331
|
+
"claude-opus-4-7": { input: 15, output: 75 },
|
|
332
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
333
|
+
"claude-sonnet-4-5": { input: 3, output: 15 },
|
|
334
|
+
"claude-haiku-4-5": { input: 1, output: 5 },
|
|
335
|
+
};
|
|
336
|
+
function costUsd(response, model) {
|
|
337
|
+
const pricing = PRICING_PER_M_TOKENS[model] ??
|
|
338
|
+
Object.entries(PRICING_PER_M_TOKENS).find(([k]) => model.startsWith(k))?.[1];
|
|
339
|
+
if (!pricing)
|
|
340
|
+
return 0;
|
|
341
|
+
const usage = response.usage;
|
|
342
|
+
const input = usage?.input_tokens ?? 0;
|
|
343
|
+
const output = usage?.output_tokens ?? 0;
|
|
344
|
+
const cacheRead = usage?.cache_read_input_tokens ?? 0;
|
|
345
|
+
const cacheCreate = usage?.cache_creation_input_tokens ?? 0;
|
|
346
|
+
const effectiveInput = input + cacheRead * 0.1 + cacheCreate * 1.25;
|
|
347
|
+
return (effectiveInput / 1_000_000) * pricing.input + (output / 1_000_000) * pricing.output;
|
|
348
|
+
}
|
|
349
|
+
// -------------------------------------------------------------------------
|
|
350
|
+
// Bug profile rendering (compact YAML for prompt — full schema in
|
|
351
|
+
// .brewing/bug-profiles/B-N.yaml; we only need the fields sift cares
|
|
352
|
+
// about, kept compact to save tokens).
|
|
353
|
+
// -------------------------------------------------------------------------
|
|
354
|
+
function bugProfileToYamlSummary(p) {
|
|
355
|
+
const lines = [];
|
|
356
|
+
lines.push(`bug_id: ${p.bug_id}`);
|
|
357
|
+
lines.push(`title: ${JSON.stringify(p.title)}`);
|
|
358
|
+
lines.push(`source_issue: "${p.source_issue}"`);
|
|
359
|
+
if (p.symptom.length > 0) {
|
|
360
|
+
lines.push(`symptom:`);
|
|
361
|
+
for (const s of p.symptom)
|
|
362
|
+
lines.push(` - ${JSON.stringify(s)}`);
|
|
363
|
+
}
|
|
364
|
+
if (p.expected.length > 0) {
|
|
365
|
+
lines.push(`expected:`);
|
|
366
|
+
for (const s of p.expected)
|
|
367
|
+
lines.push(` - ${JSON.stringify(s)}`);
|
|
368
|
+
}
|
|
369
|
+
lines.push(`failure_locus:`);
|
|
370
|
+
lines.push(` file: ${JSON.stringify(p.failure_locus.file)}`);
|
|
371
|
+
if (p.failure_locus.line !== undefined)
|
|
372
|
+
lines.push(` line: ${p.failure_locus.line}`);
|
|
373
|
+
if (p.failure_locus.function !== undefined) {
|
|
374
|
+
lines.push(` function: ${JSON.stringify(p.failure_locus.function)}`);
|
|
375
|
+
}
|
|
376
|
+
lines.push(` diagnosis: |`);
|
|
377
|
+
for (const l of p.failure_locus.diagnosis.split("\n"))
|
|
378
|
+
lines.push(` ${l}`);
|
|
379
|
+
if (p.regression_assertion.length > 0) {
|
|
380
|
+
lines.push(`regression_assertion:`);
|
|
381
|
+
for (const s of p.regression_assertion)
|
|
382
|
+
lines.push(` - ${JSON.stringify(s)}`);
|
|
383
|
+
}
|
|
384
|
+
if (p.fix_scope.length > 0) {
|
|
385
|
+
lines.push(`fix_scope:`);
|
|
386
|
+
for (const s of p.fix_scope)
|
|
387
|
+
lines.push(` - ${JSON.stringify(s)}`);
|
|
388
|
+
}
|
|
389
|
+
return lines.join("\n");
|
|
390
|
+
}
|
|
391
|
+
void validateStackConfig;
|
|
392
|
+
//# sourceMappingURL=agent.js.map
|