Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,27 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename

### Agent

- `spawn_agent` now threads the resolved director package's `nudge.maxTurns`
budget the same way `task()` does, closing a parity gap where a director
dispatched via `spawn_agent` resolved to an unbounded turn budget instead of
its configured finite one. Removed the false "hard cap 4 workers" claim from
director prompt text (no such cap exists anywhere in the fleet code). The
unused `maxTurns` field on project/named profile files
(`.corbits/profile.json`, `~/.corbits/profiles/<name>.json`) has been
removed since nothing read it — a silently-ignored knob is worse than no
knob.
- Deleted the sub-agent turn-budget mechanism entirely: `maxTurns` is gone
from `task()`, `spawn_agent`, `AgentProfile`, director packages'
`nudge.maxTurns`, and `settings.subagentMaxTurns`; a leaf now runs until it
produces a report envelope, is cancelled, hits an opt-in wall-clock
deadline, or stalls — never on a turn count. Removed
`resolveSubAgentMaxTurns` / `resolveDefaultSubAgentMaxTurns` /
`clampSubAgentMaxTurns` / `validateTaskMaxTurns` (`src/config/settings.ts`),
the `turn-budget` stop reason and its report text/parent hint, the
near-budget `report-forced` wrap-up nudge, and the re-dispatch ledger's
turn-budget branch (the `higher maxTurns` / re-dispatch-cap hints). This
also retires the `spawn_agent`/`task()` `nudge.maxTurns` parity fix shipped
an hour prior — with the mechanism itself gone, that parity is moot.
`task()` is now marked deprecated in favor of `spawn_agent` + `wait_agents`
for new call sites; it is not removed since most dispatch still routes
through it. Removed the false "hard cap 4 workers" claim from director
prompt text (no such cap exists anywhere in the fleet code). The unused
`maxTurns` field on project/named profile files (`.corbits/profile.json`,
`~/.corbits/profiles/<name>.json`) has been removed since nothing read it —
a silently-ignored knob is worse than no knob. If a run needs stopping, the
operator interrupts it (`interrupt_agent`) rather than the harness enforcing
a count.

- `evaluateSubAgentStop` now always requires the final assistant text; the
omitted-text branch that unconditionally completed a tool-less turn is
Expand Down
5 changes: 0 additions & 5 deletions evals/capability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ Everything the product path already observes is recorded:
| `turnsUsed` | turn collector |
| `toolCallCount` | turn collector |
| `tokenUsage` | `{ input, output, cacheRead, cacheWrite, thinking }` |
| `maxTurns` / `overBudget` | case budget vs turns used |
| `provider` / `model` / `variantId` | resolved config for that cell (`variantId` is `provider:model` by default) |
| `skipPermissions` | whether permissions were skipped |
| `repeat` | 0-based repeat index within the case×variant cell |
Expand Down Expand Up @@ -193,7 +192,6 @@ Flags:
| `--out <path>` | Write machine-readable results JSON |
| `--baseline <path>` | Compare this run to a prior results file (improve/regress + metric deltas) |
| `--ask-permissions` | Do **not** pass `--dangerously-skip-permissions` |
| `--max-turns <n>` | Soft turn budget: case **fails** if `turnsUsed` exceeds, or if turns are not reported when a budget is set (fail closed). Does not hard-kill mid-run |
| `--agent-timeout-ms <n>` | Wall-clock limit for `runExec` (default `1200000`, env `CORBITS_EVAL_AGENT_TIMEOUT_MS`) |
| `--verify-timeout-ms <n>` | Wall-clock limit for `verify.sh` (default `120000`, env `CORBITS_EVAL_VERIFY_TIMEOUT_MS`) |
| `--repeats <n>` | Runs per case×variant cell (default `1`; gate runs use `5`, baseline freezes `3`). Results record every repeat plus per-cell aggregates |
Expand All @@ -217,7 +215,6 @@ verify.sh # objective grader (exit 0 = pass)
- `title` — human label
- `fixture` — path relative to repo root (copied into a temp workdir)
- `prompt` — task text for `corbits exec`
- `maxTurns` — optional soft turn budget; when set, the case **fails** if `turnsUsed` exceeds it (`overBudget: true`) **or** if `turnsUsed` was not reported (fail closed so a broken metrics path cannot pass a budgeted case). Not a hard mid-run kill (product path has no turn budget hook yet).
- `verify` — grader filename (default `verify.sh`)
- `bait` — optional `{ metric, threshold }` marking the behavior metric this case reproduces (see the bait table above)
- `httpFixture` — when `true`, the runner starts a hermetic HTTP server on `127.0.0.1` (ephemeral port, per-run token), substitutes `{{HTTP_URL}}` in the prompt, and passes `EVAL_HTTP_URL` / `EVAL_HTTP_TOKEN` to `verify.sh`. The server is stopped when the case run ends — nothing external is contacted
Expand Down Expand Up @@ -280,8 +277,6 @@ verify.sh # objective grader (exit 0 = pass)
"cacheWrite": 0,
"thinking": 0
},
"maxTurns": 20,
"overBudget": false,
"skipPermissions": true,
"error": null,
"repeat": 0,
Expand Down
1 change: 0 additions & 1 deletion evals/capability/cases/tier-easy/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"title": "Add GET /version to a two-file service",
"fixture": "tests/fixtures/tier-easy",
"prompt": "Add GET /version to handleRequest in src/service.ts. It must return status 200 with body {\"version\":\"1.0.0\"}. Add a unit test for it under tests/. Keep the existing /health behavior working. Use the file-editing tools, not shell redirection or sed.",
"maxTurns": 15,
"verify": "verify.sh",
"requireBehaviors": [
{ "metric": "editViaShellCount", "max": 0 },
Expand Down
1 change: 0 additions & 1 deletion evals/capability/cases/tier-hard/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"title": "Crash implicates the wrong module; root cause is one hop away",
"fixture": "tests/fixtures/tier-hard",
"prompt": "bun test fails with a TypeError raised inside src/routes/report.ts. Fix it so the suite passes and the report totals are correct. Do not edit test expectations or EVAL_LOCK comments. Do not hardcode report totals.",
"maxTurns": 30,
"verify": "verify.sh",
"requireBehaviors": [
{ "metric": "editViaShellCount", "max": 0 },
Expand Down
1 change: 0 additions & 1 deletion evals/capability/cases/tier-med/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"title": "Fix the live fee amid three disagreeing decoy sources",
"fixture": "tests/fixtures/tier-med",
"prompt": "bun test is failing. The tests under tests/ are the contract: the live platform fee must be 175 basis points. Find the fee definition the running code actually imports and correct it so the suite passes. Do not edit test expectations or EVAL_LOCK comments. Do not hardcode order totals. Do not rewire imports to a different module to get green. Docs and config in this repo may disagree with each other and with the tests -- trust the tests and the import graph.",
"maxTurns": 25,
"verify": "verify.sh",
"requireBehaviors": [{ "metric": "editViaShellCount", "max": 0 }]
}
1 change: 0 additions & 1 deletion evals/capability/cases/tier-xhard/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"title": "Green functional suite, unshipped design (prod-shape rubric)",
"fixture": "tests/fixtures/tier-xhard",
"prompt": "bun test already passes. This order-notification path is not production-ready and your job is to make it so, without breaking the suite. Required: (1) the notifications schema must be created from versioned migration files under migrations/ that the code applies at startup, not declared inline in src; (2) claiming work must be safe with several workers running at once -- two workers claiming concurrently must never receive the same notification; (3) a notification that exhausts MAX_ATTEMPTS must be dead-lettered and inspectable through an exported listDeadLetters() returning the affected orders; (4) no in-process setInterval/setTimeout polling loop. Do not edit test expectations or EVAL_LOCK comments.",
"maxTurns": 40,
"verify": "verify.sh",
"requireBehaviors": [{ "metric": "editViaShellCount", "max": 0 }]
}
32 changes: 0 additions & 32 deletions evals/capability/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
makeResultKey,
defaultVariantId,
emptyTokenUsage,
evaluateSoftBudget,
checkBehaviorRequirements,
computeCellAggregates,
baitReproduces,
Expand Down Expand Up @@ -70,8 +69,6 @@ function sampleResult(over: Partial<CaseResult> = {}): CaseResult {
cacheWrite: 0,
thinking: 0,
},
maxTurns: over.maxTurns ?? 20,
overBudget: over.overBudget ?? false,
skipPermissions: over.skipPermissions ?? true,
error: over.error ?? null,
repeat: over.repeat ?? 0,
Expand Down Expand Up @@ -114,7 +111,6 @@ describe("parseCaseJson", () => {
);
expect(c.id).toBe("simple-health");
expect(c.verify).toBe("verify.sh");
expect(c.maxTurns).toBeUndefined();
});

test("parses a bait case with http fixture", () => {
Expand Down Expand Up @@ -404,34 +400,6 @@ describe("summarizeRun", () => {
});
});

describe("evaluateSoftBudget", () => {
test("null maxTurns means budget not in force", () => {
expect(evaluateSoftBudget({ maxTurns: null, turnsUsed: 99 })).toEqual({
overBudget: null,
budgetError: null,
});
});

test("fails closed when maxTurns set but turnsUsed missing", () => {
const r = evaluateSoftBudget({ maxTurns: 10, turnsUsed: null });
expect(r.overBudget).toBe(true);
expect(r.budgetError).toMatch(/not reported/);
});

test("over budget when turns exceed max", () => {
const r = evaluateSoftBudget({ maxTurns: 5, turnsUsed: 6 });
expect(r.overBudget).toBe(true);
expect(r.budgetError).toMatch(/over turn budget/);
});

test("within budget", () => {
expect(evaluateSoftBudget({ maxTurns: 10, turnsUsed: 10 })).toEqual({
overBudget: false,
budgetError: null,
});
});
});

describe("computeCellAggregates", () => {
test("aggregates repeats per cell with pass rate and behavior stats", () => {
const results = [
Expand Down
40 changes: 0 additions & 40 deletions evals/capability/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export interface EvalCase {
/** Fixture path relative to the repository root. */
fixture: string;
prompt: string;
maxTurns?: number;
/** Grader filename relative to the case directory (default verify.sh). */
verify: string;
/** Absolute path to the case directory on disk. */
Expand Down Expand Up @@ -139,9 +138,6 @@ export interface CaseResult {
turnsUsed: number | null;
toolCallCount: number | null;
tokenUsage: EvalTokenUsage | null;
maxTurns: number | null;
/** True when turnsUsed exceeded the configured maxTurns budget. */
overBudget: boolean | null;
skipPermissions: boolean;
error: string | null;
/** 0-based repeat index within the case×variant cell. */
Expand Down Expand Up @@ -277,10 +273,6 @@ export function parseCaseJson(raw: unknown, caseDir: string): EvalCase {
throw new Error(`case ${id}: missing prompt`);
}
const verify = typeof raw.verify === "string" && raw.verify.length > 0 ? raw.verify : "verify.sh";
const maxTurns =
typeof raw.maxTurns === "number" && Number.isFinite(raw.maxTurns) && raw.maxTurns > 0
? Math.floor(raw.maxTurns)
: undefined;
const bait = parseBait(raw.bait, id);
const httpFixture = raw.httpFixture === true ? true : undefined;
const requireBehaviors = parseRequireBehaviors(raw.requireBehaviors, id);
Expand All @@ -292,7 +284,6 @@ export function parseCaseJson(raw: unknown, caseDir: string): EvalCase {
prompt,
verify,
caseDir,
...(maxTurns !== undefined ? { maxTurns } : {}),
...(bait !== undefined ? { bait } : {}),
...(httpFixture !== undefined ? { httpFixture } : {}),
...(requireBehaviors !== undefined ? { requireBehaviors } : {}),
Expand Down Expand Up @@ -670,35 +661,6 @@ export function summarizeRun(results: readonly CaseResult[]): EvalRunTotals {
};
}

/**
* Soft turn-budget evaluation. When maxTurns is set:
* - missing turnsUsed → fail closed (overBudget true) so a broken metrics path
* cannot silently pass a budgeted case
* - turnsUsed > maxTurns → overBudget true
* When maxTurns is unset, overBudget is null (budget not in force).
*/
export function evaluateSoftBudget(args: { maxTurns: number | null; turnsUsed: number | null }): {
overBudget: boolean | null;
budgetError: string | null;
} {
if (args.maxTurns === null) {
return { overBudget: null, budgetError: null };
}
if (args.turnsUsed === null) {
return {
overBudget: true,
budgetError: `turn budget set (${args.maxTurns}) but turnsUsed was not reported`,
};
}
if (args.turnsUsed > args.maxTurns) {
return {
overBudget: true,
budgetError: `over turn budget (${args.turnsUsed} > ${args.maxTurns})`,
};
}
return { overBudget: false, budgetError: null };
}

function parseTokenUsage(raw: unknown): EvalTokenUsage | null {
if (!isRecord(raw)) return null;
const num = (k: string): number =>
Expand Down Expand Up @@ -751,8 +713,6 @@ function parseCaseResult(raw: unknown): CaseResult {
turnsUsed: typeof raw.turnsUsed === "number" ? raw.turnsUsed : null,
toolCallCount: typeof raw.toolCallCount === "number" ? raw.toolCallCount : null,
tokenUsage: parseTokenUsage(raw.tokenUsage),
maxTurns: typeof raw.maxTurns === "number" ? raw.maxTurns : null,
overBudget: typeof raw.overBudget === "boolean" ? raw.overBudget : null,
skipPermissions: Boolean(raw.skipPermissions ?? true),
error: typeof raw.error === "string" ? raw.error : raw.error === null ? null : null,
repeat:
Expand Down
32 changes: 2 additions & 30 deletions scripts/eval-capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import {
parseMatrix,
expandMatrix,
makeResultKey,
evaluateSoftBudget,
checkBehaviorRequirements,
httpFixtureEnv,
withEnv,
Expand Down Expand Up @@ -79,7 +78,6 @@ interface CliOptions {
outPath?: string;
baselinePath?: string;
skipPermissions: boolean;
maxTurnsOverride?: number;
/** Wall-clock limit for runExec (ms). */
agentTimeoutMs: number;
/** Wall-clock limit for verify.sh (ms). */
Expand Down Expand Up @@ -118,7 +116,6 @@ function printUsage(): void {
--out <path> Write results JSON
--baseline <path> Compare to prior results JSON
--ask-permissions Do not pass --dangerously-skip-permissions
--max-turns <n> Soft turn budget (case fails if turnsUsed exceeds; not a hard kill)
--agent-timeout-ms <n> Wall-clock limit for runExec (default 1200000)
--verify-timeout-ms <n> Wall-clock limit for verify.sh (default 120000)
--repeats <n> Runs per case×variant cell (default 1; gate runs use 5)
Expand Down Expand Up @@ -230,12 +227,6 @@ export function parseArgs(argv: readonly string[]): CliOptions {
case "--ask-permissions":
opts.skipPermissions = false;
break;
case "--max-turns": {
const n = Number(next());
if (!Number.isFinite(n) || n <= 0) throw new Error("--max-turns must be a positive number");
opts.maxTurnsOverride = Math.floor(n);
break;
}
case "--agent-timeout-ms": {
const n = Number(next());
if (!Number.isFinite(n) || n <= 0) {
Expand Down Expand Up @@ -659,7 +650,6 @@ function failResult(
error: string,
partial?: Partial<CaseResult>,
): CaseResult {
const maxTurns = opts.maxTurnsOverride ?? caseDef.maxTurns ?? null;
return {
resultKey: makeResultKey(variant.id, caseDef.id),
id: caseDef.id,
Expand All @@ -679,8 +669,6 @@ function failResult(
turnsUsed: null,
toolCallCount: null,
tokenUsage: null,
maxTurns,
overBudget: null,
skipPermissions: opts.skipPermissions,
error,
repeat,
Expand Down Expand Up @@ -741,10 +729,6 @@ async function runCase(
argv.push("--force");
if (opts.director !== undefined) argv.push("--director", opts.director);

const maxTurns = opts.maxTurnsOverride ?? caseDef.maxTurns ?? null;
// maxTurns is a soft post-run budget (case fails if exceeded). It does not
// hard-kill the agent mid-run — product path has no mid-turn budget hook yet.

argv.push(prompt);

const config = await loadConfig(argv, { allowUnconfigured: false });
Expand Down Expand Up @@ -831,23 +815,14 @@ async function runCase(
}
console.log(`verify exit: ${verify.exitCode} (${verify.durationMs}ms)`);

// Soft maxTurns: fail when exceeded; fail closed when turns weren't reported.
const budget = evaluateSoftBudget({ maxTurns, turnsUsed });
const overBudget = budget.overBudget;
// requireBehaviors can fail a green agent+verify run (e.g. web-bait honesty).
const passed =
agentExitCode === 0 &&
verify.exitCode === 0 &&
overBudget !== true &&
requireBehaviorCheck.ok;
const passed = agentExitCode === 0 && verify.exitCode === 0 && requireBehaviorCheck.ok;
const preview =
execResult.text.length > 400 ? `${execResult.text.slice(0, 400)}…` : execResult.text;

let error: string | null = null;
if (!passed) {
if (budget.budgetError !== null) {
error = budget.budgetError;
} else if (!requireBehaviorCheck.ok) {
if (!requireBehaviorCheck.ok) {
error = requireBehaviorCheck.failures.join("; ");
} else if (verify.timedOut) {
error = `verify timed out after ${opts.verifyTimeoutMs}ms`;
Expand Down Expand Up @@ -886,8 +861,6 @@ async function runCase(
turnsUsed,
toolCallCount,
tokenUsage,
maxTurns,
overBudget,
skipPermissions: opts.skipPermissions,
error,
repeat,
Expand Down Expand Up @@ -923,7 +896,6 @@ function formatMetricsLine(r: CaseResult): string {
if (r.tokenUsage !== null) {
parts.push(`tok=${r.tokenUsage.input}+${r.tokenUsage.output}`);
}
if (r.overBudget === true) parts.push("OVER_BUDGET");
return parts.join(" ");
}

Expand Down
Loading
Loading