Skip to content

feat(lifecycle): close the self-improve loop end-to-end over the SWE Docker judge#544

Draft
drewstone wants to merge 45 commits into
mainfrom
feat/self-improve-e2e
Draft

feat(lifecycle): close the self-improve loop end-to-end over the SWE Docker judge#544
drewstone wants to merge 45 commits into
mainfrom
feat/self-improve-e2e

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

What

Closes the agent self-improvement loop end-to-end over the real SWE-bench Verified Docker judge, and fills every surface it can optimize: prompt, tools, MCP, memory, external connections.

The one previously-missing piece was the scorer — EvalRunner (src/lifecycle/marginal-lift.ts) was a type with zero real implementations, so runLifecycle (the clean generate → measure → promote → compose loop) had no way to produce a number and no real callers. This PR writes that function and everything downstream of it.

Base branch

⚠️ Stacked on the local WIP branch meta/swe-code-surface (38 commits, its own in-flight work). This PR's diff includes those base commits and is not cleanly mergeable to main yet — it is a draft to surface the self-improve work for review. The 7 self-improve commits are 16f98ae2..fb636d71.

The 6 phases (+ 1 fix)

Commit Phase
16f98ae2 P1 sweEvalRunner — drives each Verified instance with the multi-turn driver→worker atom (runAgentic + refine) under the profile's prompt, captures the git diff, grades it with the swebench Docker judge held OUTSIDE the agent. Closes the loop.
7132f9ae P2 senior scientific-method optimizer prompt (hypothesis → sub-goal decomposition → isolate the mechanism → predicted lift → reflect), seeded from GEPA's reflection prompt + the evolve/pursue skills; the driver→worker atom wired as a candidate builder (replaces the respawn loop).
24cfced2 P3 same-host stdio MCP sandbox client — a self-built MCP tool is materialized live and callable while scored.
aee7d691 P4 typed trace ingestion + spans → OTLP so the trace-native proposers consume real runs (stops the unknown[] down-cast).
13cb8024 P5 memory as a first-class profile surface, served live via a stdio memory MCP (memory_search/memory_get); lesson-curation seam + retrieval log for memory-as-treatment.
32a3116d P6 external-MCP adoption (adopt-not-build) + declarative API-key provisioning (names only, injected at spawn, never logged) + a research seam on the build driver.
fb636d71 fix canonical bench invocation (pnpm --filter ./bench self-improve) so the e2e resolves local src instead of the published dist.

Verified at HEAD

  • pnpm -s typecheck → exit 0
  • pnpm -s test1372 passed / 0 failed (2 skipped), 139 files
  • Real end-to-end run (WORKER_MODEL=google/gemini-2.5-flash-lite, django__django-11532, n=1, budget=1): generate → drive (real 1036-byte patch) → grade (official Docker judge, container exec'd, both arms returned a real verdict) → gate → compose all fired. baseline composite=0.0000, candidate scoreDelta=0.0000, promoted=false — the gate correctly held a zero-lift candidate.

Honest limits (follow-ups, not blockers)

  • No lift demonstrated yet. The e2e proves the machine runs end-to-end and emits real Docker-graded numbers; it does not yet show a candidate improving the agent. gemini-flash-lite solves ~nothing on django-11532 at n=1, so both arms fail and Δ=0. A lift proof needs a stronger worker (glm, currently provider-down) on a non-saturated multi-task holdout.
  • P3/P5/P6 surfaces (built-MCP scoring, memory-as-treatment ablation, adopted-connection run) are proven by unit tests + live stdio spawns, not yet by a live SWE-graded ablation.
  • Boundary shims flagged in-code: AgentProfile.memory/connections extensions ride a local type because the published agent-interface has no such field yet (source repo absent); the spans→OTLP converter lives here but belongs in agent-eval; no HTTP MCP client for same-host adopted http grants yet.

Architecture map

The grounded inventory that drove this (what existed, why it couldn't self-improve an agent, where it was dumb, the phased plan) is at research/…/EXP-037-selfimprove-architecture-search/INVENTORY-one-harness.md.

drewstone and others added 30 commits July 8, 2026 02:53
Root-fix a scoring bug proven live: on django-12419 @ glm-4.6 the agent made the
EXACT gold fix but scored 0 because the harness extracted the patch by parsing the
model reply for a fenced diff block, and the model wrote prose instead. Two changes,
standard SWE-bench practice (SWE-agent/OpenHands read the diff from repo state and
pre-stage the checkout):

- boxSetup(task): harness clones the instance repo at base_commit into a fixed
  /work BEFORE the agent shot (same session) so the agent only edits. A stochastic
  model cannot be trusted to clone to an exact path (observed cannot-change-to-/work
  failures). Adapter-agnostic seam on BenchmarkAdapter.
- boxExtract(): git diff of the agent edits in /work (test files excluded; the judge
  applies the gold test_patch itself), preferred over the event-stream parse which
  stays the fallback. Prompt updated: repo pre-staged, agent only edits.

Typecheck clean; calibration still green. Live glm-5.2 verification blocked on
transient sandbox box-provisioning failures (box unavailable before start on a
green health) - re-run when the platform host-agent recovers.
…proposer

The code-surface proposer fed the coding agent the ~400-char DISTILLED
findings (generationFailureDistiller), not the raw traces. The meta-harness
edge (yoonholee.com/meta-harness) is raw-trace filesystem context: the coding
agent greps/cats the FULL run traces of failed candidates to diagnose, rather
than reading a pre-summary.

Add rawTraceDistiller() — an additive analyzeGeneration producer that, instead
of summarizing, points the proposer at the prior generation's real run traces
already durable on disk under runDir (per-cell spans.jsonl event logs +
cached-result.json scores + artifacts) with a grep/cat-to-diagnose instruction.
It emits AnalystFinding[] with ABSOLUTE paths (the harness runs from a worktree
cwd) so it drops into the same analyzeGeneration slot and renders through the
same agenticGenerator prompt path. The existing distiller stays the default.

improve() gains a one-line enable: opts.rawTraceContext = true wires
rawTraceDistiller() when the caller has not supplied their own analyzeGeneration
(explicit analyzeGeneration still wins; null still disables).

Self-test builds a real tmp runDir with fake candidate + trace files and asserts
the findings reference the actual absolute trace-file paths + the grep/cat
instruction, plus worst-candidate-first ordering and the clean-generation
fallback. tsc clean, biome clean, 3/3 new tests + 7/7 existing improve tests pass.
# Conflicts:
#	bench/src/benchmarks/swe-bench.ts
#	bench/src/run-benchmarks.ts
#	docs/api/primitive-catalog.md
#	src/improvement/raw-trace-distiller.test.ts
#	src/improvement/raw-trace-distiller.ts
…point

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…f-k + self-repair on docstring examples)

Selection + repair grounded only on visible docstring >>> examples (doctest,
nonce-sentinel), hidden check() grading physically after all harness decisions.
Paired bootstrap + exact sign test. Calibrated: 164/164 hidden self-check,
75/164 coverage, 2/75 false-fail (dataset bugs). First result: Llama-3-8B
44.3%->57.3% pass@1 (+13.0pp, CI [+7.8,+18.4], p=1.5e-5, n=164).
humaneval.ts: HUMANEVAL_GZ local-cache support (GitHub raw 429s under reps).
…ructural-lever rig

Model writes single-line asserts from the prompt alone, BEFORE any candidate
exists; asserts run individually in the jail and add to the doctest signal
(split counts kept for audit). Extends honest-oracle coverage from 46% of
HumanEval toward 100%. Opt-in via TESTGEN=<count>; default off keeps prior
runs reproducible.
…EN), hidden=remaining asserts

Same architecture as hev-structural (nonce judges, phase separation, incremental
persistence, bootstrap+sign-test). Calibration: 427/427 hidden self-check, 0/427
visible false-fail, 0 dropped. Official-first scoring: MBPP one-sentence specs
make model-guessed asserts ~70% wrong on passing code — shown assert ranks first,
guesses break ties (pilot: selection -6.7pp -> +16.7pp).
… only net-new seam; extend strategy family, verifier-environment, field routing
…tool-loop conversation

runBrainLoop returned on a no-tool-call turn WITHOUT pushing the final assistant
message, so ToolLoopResult.messages violated its own contract (seed + every
assistant/tool turn): a shot that answers without tools came back with a transcript
missing its own answer. Depth continuation then criticized a solution absent from the
model's history, and structuralRollout's fenced-code candidate extraction
(defaultExtractCandidate's designed fallback) read an empty conversation on
non-tool-calling models (Llama-3-8B-Instruct-Lite never tool-calls under
tool_choice:auto — measured on Together).

Proven by the runtime smoke (bench/src/smoke-structural-rollout.mts): candidates now
flow through the strategy on a real non-tool-calling model.
…ory-less consults

consultAnalyst rendered EVERY raw consult in analyst framing (instruction as system,
'TASK: ...TRAJECTORY:' as the user turn). With no trajectory — the pre-task case,
i.e. structuralRollout's authored-check generation — that user turn is just the coding
task, which a weak model answers directly, ignoring the system instruction: measured
on Llama-3-8B-Instruct-Lite, authored-assert yield 6/18 reps (0 asserts on the rest)
vs 18/18 with the instruction and task fused into one user message (the proven
hev-structural rig shape). Trajectory-bearing consults keep the analyst framing
unchanged.

Runtime smoke after the fix: authored checks on 20/20 HumanEval tasks (was 14/20).
…Eval, hidden grading script-side

The ship gate for the ported strategy: runAgentic + structuralRollout(default policy:
k=5, repairs<=2, testgen=6) over createVerifierEnvironment with an INERT check, so no
hidden-test signal reaches selection/repair; visible checks are the strategy's own
model-authored asserts run by the shipped sandboxCheckRunner over a docker
--network=none exec channel (the script's thin adapter). The task's own check() suite
grades every locked candidate script-side (runChecker) after each rollout finishes.
Receipts are hard-verified against the recorded outcomes per candidate.

Llama-3-8B-Instruct-Lite (Together), first 20 tasks: blind mean-of-k 63.0% ->
selected 80.0% -> final 85.0% (+22pp), authored checks 20/20, 4 tasks rescued
(selection or guarded repair passes hidden where sample 0 fails), 0 crashes.
…g (arms A-D, hard/control sets)

Implements supervisor-lab PREREG-supervisor-showdown.md: fixed 62-hard/20-control
HumanEval sets, equal-compute worker loop (k=5, testgen=6, <=2 repairs), visible-only
evidence rendering, no-code plan contract with leak stripping + planLeaked audit,
Phase A/B separation with the rig-local nonce hidden judge, per-model token+cost rows.
…glm plan calls exceed 240s under sustained load
…ator

Per PREREG-swe-frontier Stage 0: glm-5.2 authors a repro script from the
issue text (+ up to 3 requested file reads); validity = nonzero exit on the
unpatched tree (1 feedback retry), soundness = exit 0 after the gold patch
applied host-side to a COPY of the tree, run in the same :ro-mount jail as
the env's run tool. Gold is script-side only, enforced by a leak assert on
every outbound message. PYTHONPATH=/testbed pins imports to the mounted
tree (script-path sys.path would silently test the image's baked-in
install). swe-bench-env: image resolution extracted to exported
resolveImageForMetadata (no behavior change).
…e are not a script

Observed live on astropy__astropy-13033: the model fenced its READ: lines,
extractScript shipped them to the jail, and the NameError crash registered
as a false 'bug detected'. A fenced block consisting solely of READ: lines
now routes to the read round.
…t the image's built /testbed

Stage 0 measured the mount substrate killing 6/23 instances (astropy x2,
matplotlib, sklearn x2, pytest) with import errors a fresh un-built clone
cannot avoid (compiled extensions / generated version files live only in
the image's own /testbed). The same 6 scripts were all valid+sound when
executed in-image with the gold applied in-container (writable layer,
discarded by --rm; __GOLD_APPLIED__ sentinel separates apply-failure from
still-failing). REPRO_EXEC=image makes that substrate a first-class mode.
…ttempts)

The 56s total backoff lost 5/23 instances to a sustained zai 429 burst at
the tail of a conc-3 run (0 model calls, whole instances recorded as
infra-error). Rate-limit retries now climb 30s/60s/120s/240s while
transient errors keep the short ladder.
…script extraction

Observed live on pylint-dev__pylint-7080: a reply opening with two
consecutive ```python lines made the non-greedy fence match capture the
empty span between them, discarding a complete script as authoring-failed.
… the grading substrate

- canary: with the gold patch applied to the tree under test, import <pkg>
  must succeed AND resolve INTO that tree (exit 3 = site-packages shadow);
  a canary failure marks the instance env-unresolvable in that mode before
  any model call is spent
- CANARY_ONLY=1: substrate-trust sweep with zero model calls (image mode
  also skips the host clone)
- mount substrate: gold applied host-side up front, so the canary observes
  the tree exactly as the soundness run mounts it
- 429 ladder raised to 60s/120s/240s over 7 attempts (the overnight
  code-1305 storm zeroed 6/23 instances on the 30s ladder)

Measured on the 23-instance Stage 0 backbone: image canary 23/23 pass,
mount canary 17/23 (the 6 compiled-package instances fail, matching the
prereg amendment's diagnosis).
…ction + guarded repair vs solo

swe-structural.mts runs both PREREG-swe-frontier Stage-1 arms on the cached backbone:
ARM=system = canary-asserted image substrate, Stage-0 repro reuse (re-verified per instance:
validity without gold, soundness with gold in-container), k independent emit-patch attempts,
in-image candidate scoring (git apply to /testbed + repro), argmax (repro-pass first,
crash-lowest, first-index ties), <=2 strictly-improving repair rounds, then the official
swebench judge serialized in Phase B after every arm decision locks. ARM=solo = one
emit-patch attempt, the July-protocol reference.

- swe-jail.ts: the calibrator's canary-verified jail/zai-ladder/canary primitives extracted
  verbatim (swe-repro-calibrate now imports them — one jail, no fork) + assertNoHiddenLeak,
  the transport-chokepoint judge-separation guard (system/user messages only: assistant/tool
  text is the model's own or reads of the visible tree).
- swe-bench-env.ts: opt-in cloneCache — one GitHub clone per instance, local copies per open,
  so k-attempt sampling cannot die on a clone flake mid-run. Default off, baseline unchanged.
- Rows are incremental (OUT.phaseA then OUT post-judge) and both phases resume by instance id.
…ralRollout dials on real HumanEval with the library's held-out gate

The first live campaign on the merged machinery: the 'rollout-policy'
improve() surface (deterministic neighbor proposer) drives a REAL evaluator
— runAgentic + structuralRollout over an inert verifier surface, visible
checks via sandboxCheckRunner on a semaphored docker --network=none channel,
script-side hidden grading by the nonce-sentinel judge — with fixed disjoint
slices (DEV = HumanEval [0,60), HELD-OUT = [60,120)) passed as explicit
budget.holdoutScenarios so the library enforces disjointness and its own
defaultProductionGate (paired bootstrap, ship iff CI.low > 0.05) makes the
ship/hold call. analyzeGeneration: null keeps the improver's context to DEV
composites only. Worker default is Qwen2.5-7B-Instruct-Turbo: the original
Meta-Llama-3-8B-Instruct-Lite and every 8B Llama variant were retired from
Together serverless (model_not_available, verified 2026-07).
…ow-up on a headroom config

Identical improve() rollout-policy loop as live-improve-campaign.mts (HumanEval,
DEV saturated at 90%, gate HELD), moved to sanitized MBPP where Qwen2.5-7B
measures 76.7% at k5/r2/t6 vs an 85.9% pass@5 bound. Shown assert rides
task.meta.visibleChecks into officialChecksFromMeta so the official check
outranks authored guesses; test_imports prepended for both judges; DEV =
usable index [0,150), HELD-OUT = [150,300); MAX_CALLS hard cap + fail-loud
model preflight + recompute-from-raw holdout cross-check added.

mbpp-structural.mts gains an entrypoint guard (corpus-replay precedent) and
exports basePrompt so the campaign imports the loader without launching the
benchmark; direct execution is unchanged.
…mpty-holdout crash

When the worker stops answering mid-run (Together HTTP 402 credit exceeded /
429 rate limit / outage), the runtime swallows exhausted-retry shots as null,
so cells silently degrade (calls/cell 15 -> 3) and the run dies hundreds of
cells later at the holdout with a cryptic 'no scorable cells' gate error.

Now: when a cell produces zero candidates (repairStop=no-candidates, every shot
null), probe the worker directly and abort loud with the real HTTP status
(e.g. 402 credit exceeded) so the operator sees the actual cause immediately.
A healthy probe means the null was a one-off and only that cell fails.

Motivated by a live run: the loop completed all 1350 DEV/gen cells and picked
{k9,r2,t6} (+5.3pp DEV over the 82.0% baseline) but exhausted the Together
credit balance at the holdout phase before the gate could render a verdict.
…river over SWE-bench Verified

Contract: supervisor-lab/docs/design/stream-loop.md. Base refactor of the
Stage-1 structural pilot (swe-structural.mts @ 393ee50) into a running
stream: F (frozen-v0: canary, manifest-repro reuse else fresh authoring,
k=4 repro-argmax, <=2 guarded repairs) and L (byte-identical + memory
recall via promptAppendix over an initially-empty agent-knowledge store,
templated outcome-anchored notes, failure-class tally), interleaved per
instance, official judge serialized per instance after arm decisions lock.

Hard driver requirements (each traces to a measured Stage-1 failure):
- per-instance DEADLINE_MS race (29h hang) -> error rows w/ partial
  receipts, stream continues; deadline reaches into the zai retry ladder
- per-candidate TURN_CAP at the transport chokepoint (160-call blowup) ->
  synthetic no-tool-call completion, candidate scored as-is, receipted
- zai conc <=2 total, 429 ladder 60/120/240s, 480s client timeout
- explicit image pull -> run -> delete rotation with a hard presence
  assert; SWEBENCH_CACHE_LEVEL env override (default env) because the
  judge's cache_level=env silently pruned the 23-image fingerprint cache

Ledger: one JSONL row per (instance x arm) with all structural receipts +
recall/write receipts + cumulative resolved/$ per arm; events.jsonl incl.
the stubbed every-25 batch-look; append-only persisted plan (stable
streamIndex across continuations); resume by instance id.

Shared protocol lift: repro-authoring prompt/parsers move from the Stage-0
calibrator into swe-jail (byte-identical), zaiChatRaw gains an optional
deadlineAt, IMPORT_NAME covers seaborn/xarray.
…125 (5 attempts, backoff)

Two defects the stream's all-error run exposed: (1) the docker-failure path read
err.stdout but docker writes exit-125/mount/OCI errors to STDERR, so every infra
failure surfaced as a bare 'docker: ' with no cause; (2) a transient daemon
window (125) fatal-errored the instance — and since erroring instances burn
through instantly with no model calls, one bad window nuked all 23 in ~3 min.
Now: stderr captured, and 125/daemon-unreachable/overlay/no-space retried with
backoff before being surfaced as infra error.
drewstone added 15 commits July 12, 2026 12:06
…r() honors TEMP as the temp DIR

TEMP=0.8 (intended as sampling temperature) made os.tmpdir() return '0.8', so
mkdtemp built relative paths like '0.8/swe-repro-XXXX' and docker rejected every
-v mount as an invalid volume name — the true cause behind the swallowed
'docker: ' error that failed all 23 instances. TEMP still accepted iff it parses
< 2 so a stray temp-DIR value can't leak in as a temperature.
Swap the worker to glm-4.5-air (both arms; SWE headroom) via WORKER_MODEL, and
inject at the shared makeTransport chokepoint + the repro-author call the
reasoning-budget knob the zai coding endpoint actually honors — thinking:{type:
enabled} — so arms F and L send byte-identical bodies (REASONING_EFFORT env,
default enabled; symmetry guaranteed by the shared transport). Probe on the live
endpoint: thinking lifts glm-4.5-air reasoning 831->1820 tok on a plain
completion, while reasoning_effort returns 200 with zero lift (silently ignored).
MAX_TOKENS stays 12000 (probe showed no content starvation). Record
maxTokens/reasoningEffort on each row so the worker-budget-identical audit reads
the ledger directly.
…failure gate)

Arm L now fetches a STRONGER model's (SUPERVISOR_MODEL=glm-5.2) no-code diagnosis
before repair, ONCE per instance, gated on a VERIFIED failure: the selected
candidate diff applied AND the gold-verified repro still reports the bug
(applyOk && severity===1). The strength gap over the glm-4.5-air worker is the
arm-C effect; a same-model pair would be the arm-B null.

Primitives (renderRepairEvidence, planContract, supervisorPrompt, stripPlanCode)
are REPLICATED from supervisor-arena.mts (that file runs main() on import, so it
cannot be imported). Evidence is execution-verified / model-visible ONLY — the
issue, the worker's own candidate diff, and the repro-output tail — never
FAIL_TO_PASS, never gold, never worker self-report. The supervisor call is
guarded by assertNoHiddenLeak (a trip HOLDS, never crashes) + the instance
deadline, runs on its OWN token budget (SUPERVISOR_MAX_TOKENS), and its plan is
injected into L's repair appendix; F's appendix is byte-identical (raw failure =
control). Receipts: Row.supervisorPlan {fired, reason, plan, leaked,
groundedOnReproTail, planTokensIn/Out} (populated L, null F) + supervisor-fired/
held events. Supervisor cost is added to L's $ but kept out of the worker token
counters so the worker-budget-identical audit stays clean.
…), not the worker

The repro is a shared MEASUREMENT INSTRUMENT — it grades both arms' candidates and
defines the supervisor-fire predicate (severity===1) — so authoring it with the
weaker glm-4.5-air worker leaks worker weakness into the instrument (observed: a
degraded-unsound matplotlib repro that fails on buggy code but the gold patch does
not clear). Fresh authoring now runs on REPRO_MODEL (default = SUPERVISOR_MODEL,
glm-5.2), the same author as the Stage-0 manifest, so the reused (18) and fresh (5)
repros across the fingerprint-23 share one strong instrument. No thinking knob on
the author call (Stage-0 authored without one; glm-5.2 reasons at baseline). The
Stage-0 soundness gate is unchanged: a degraded-unsound/invalid/timeout fresh repro
keeps script=null so it never gates. Worker solve/repair stay glm-4.5-air (the
strength gap for the solve-vs-supervise contrast is preserved). Log the sound-repro
rate per run; refine held-reason granularity (empty-diff vs apply-failed).
…— the weak worker's dominant failure

Smoke (6 inst, glm-4.5-air worker): supervisor fired 0/6 — reasons apply-failed=4, no-repro=2.
The gate required severity===1 (patch applied but repro still fails), but glm-4.5-air's dominant
failure is severity 3 (git apply rejects the diff — stale context). apply-failed is
execution-verified (objective, non-credulous) and stale-diff re-anchoring is exactly where a
stronger reviewer helps. Gate now fires on severity 1 OR 3; evidence rendering + supervisor prompt
branch on failureKind (wrong-fix vs apply-failed) so the plan targets the real problem.
…uced nothing, the case a plan helps most; hold only already-passing + repro-timeout
…ashboard from a stream dir's ledger/events (reproducible, re-runnable for live state)
…with labeled nodes (role/model/harness/transport), and correct the mental model (best-of-k + supervised-repair, NOT supervisor-spawns-workers)
…SWE Docker judge

Fills the EvalRunner hole (marginal-lift.ts): drive each pinned Verified
instance with the multi-turn atom (runAgentic + refine) under the profile's
prompt, capture the git diff, grade it with the swebench Docker judge held
outside the agent, and return { composite: mean(resolved), costUsd }.

The environment/tasks/judge are injected (bench owns them; importing bench
from src would invert the package dependency). The driven system prompt folds
profile.prompt.instructions in after systemPrompt — the surface applyArtifact
mutates for prompt artifacts — so a prompt candidate measurably changes the
worker instead of scoring a fake zero delta. All-judge-failures throws rather
than reporting a fabricated composite.

bench/src/self-improve-swe.mts wires it end-to-end: promptGenerator (one
deterministic candidate via the injected refine seam) -> runLifecycle ->
thresholdPromotionGate(0) -> composeProfile, with an SMOKE=1 import-only path.
…-loop tool builder

Replace the thin builder/author prompts with one shared senior doctrine
(src/improvement/optimizer-prompt.ts): diagnose the dominant failure mode,
state a hypothesis with a predicted lift, decompose into checkable sub-goals,
design to isolate the mechanism, generalize past the shown findings, preserve
what works, verify for real, then reflect. Seeded from GEPA's
REFLECTION_SYSTEM and the /evolve, /pursue, self-improving-loop skill docs;
embedded by toolBuildPrompt / mcpBuildPrompt / defaultBuildPrompt and by
authorStrategy's authoring instruction.

Add driverLoopGenerator: the driver->worker CandidateGenerator on the shipped
atom (runBrainLoop + ToolLoopChat, the same seam driverAgent runs on). A
driver LLM authors each worker instruction, observes the session's diff /
files / verifier output, rates it, and decides refine / re-scope / decompose
- replacing agenticGenerator's canned EMPTY_TREE_NOTE/failureNote respawn.
Workers stay runLocalHarness in the candidate worktree; agenticGenerator is
kept intact as the offline path. worktreeBuildCandidate wires the driver loop
as the tool/mcp build default whenever a driver brain is configured.

Completion oracle stays code-owned: after the driver stops, ground truth
(dirty tree + raw-trace evidence + verifier exit) decides applied, never the
driver's prose. Scripted-brain unit tests prove instruction threading, refine
on red verifier, fail-closed gating, and the session cap.
…P into the scorer

Phase 3 of the self-improve loop: a worktree-BUILT MCP server (stdio, cwd on
the host) was unreachable by every shipped backend — none spawned profile.mcp
stdio servers same-host, so a built tool could never be scored live.

- connectStdioMcp (runtime/stdio-mcp-client): the ONE persistent
  newline-JSON-RPC spawn+handshake, extracted from mcpServeVerifier's probe;
  the verifier is rebased on it so 'verified it serves' and 'served while
  scored' can never drift. Spawn faults stay McpSpawnFault (setup bug, thrown);
  crash/timeout stays a failed candidate with the stderr tail.
- materializeLocalMcp: spawn every enabled stdio server in profile.mcp,
  namespace tools <server>__<tool> (provider-safe schemas via the shared
  sanitizeMcpToolSchema), fail-closed when a declared server cannot boot.
- localSandboxClient + resolveSandboxClient backend:'local': the same-host
  pseudo-box — router-brain tool loop (runBrainLoop) with the profile's MCP
  tools live; profile arrives per-create on backend.profile; delete() kills
  the children. Event protocol matches inlineSandboxClient.
- sweEvalRunner opts.materializeMcp: overlay the live MCP tools onto the
  driven surface (tools()/call()) for the eval's duration, close in finally.
  Default stays prompt-only.
- bench self-improve-swe.mts SURFACE=mcp: buildableGenerator over
  worktreeBuildCandidate (driver-loop brain via routerBrain), ranked/gated by
  the SAME eval runner with materializeMcp on.

Verified: tsc green (root+examples); vitest 1318 passed 0 failed; chain smoke
mcpServeVerifier -> applyArtifact -> materializeLocalMcp round-trips a live
cwd-bound tools/call.
…sers consume real runs

Phase 4 of the self-improve loop: kill the trace-ingestion rot between what
the loop RECORDS and what its analysts READ.

- campaign-otlp: the missing converter from the campaign trace writer's
  per-cell spans.jsonl ({name, cellId, startMs, durationMs, ...attrs}) to the
  OTLP-flat JSONL OtlpFileTraceStore/the trace-analyst registry parse, plus
  campaignTraceResolver(runDir) — the resolveTraces implementation for
  traceAnalystProposer/haloProposer: proposing generation g reads gen-(g-1)
  (baseline for g=0) under the same runDir the loop records into. One trace
  per cell keyed on the cell's on-disk path (the same cellId recurs across
  baseline and every candidate); deterministic FNV folds to 32/16-hex ids.
- findings: isAnalystFinding + toAnalystFindings replace the blind
  'as AnalystFinding[]' cast in improvement-driver — real findings pass by
  reference, untyped seeds are lifted into makeFinding envelopes (claim = the
  curator extraction order, original under metadata.raw), garbage dropped
  without throwing. Build prompts can no longer render undefined claims.
- improve(): rawTraceDistiller is now the DEFAULT analyzeGeneration for
  durable runs (real runDir — where the traces live); the in-memory digest
  distiller stays for mem:// runs and now emits TYPED AnalystFindings too,
  so exactly one findings shape crosses the wire. rawTraceContext becomes the
  explicit override in either direction.

Proof: vitest drives the real published runCampaign recording -> resolver ->
OtlpFileTraceStore (2 traces indexed, tool names visible) ->
traceAnalystProposer returns a candidate, offline via its analyze/fetch seams.
Converter lives in this repo because agent-runtime-swe consumes the published
agent-eval (0.108.0); lifting it into agent-eval next release is the follow-up.
… via stdio MCP

Adds the 'memory' ArtifactKind + AgentMemorySpec payload. applyArtifact
mounts it twice: the typed spec on profile.metadata.memory (local
extension — the published AgentProfile has no memory field yet) and a
live stdio server entry on profile.mcp, so the same-host client
(materializeLocalMcp) boots the memory during a scored run and
sweEvalRunner({ materializeMcp: true }) measures memory-as-treatment
lift with zero scorer changes.

The memory MCP server (memory_search / memory_get, deterministic
lexical retrieval) serves on the extracted createStdioToolServer core
(committed here; the memory server is its first consumer), with a bin
entry (agent-runtime-memory-mcp) resolved per install: dist/mcp/
memory-bin.js under node when built, the TS source through tsx in
dev/test. Fail-closed: an empty memory never serves; a broken store
fails the materialization instead of faking the ablation.

Lesson ingestion: memoryArtifactFromLessons /
memoryArtifactFromCuratedSurface adapt agent-eval's
memoryCurationProposer block (markers duplicated by convention —
flagged) into memory artifacts; memoryGenerator is the native
CandidateGenerator (observed findings only — judge-derived rows are
firewalled; carried memory accumulates; no-change generations emit
nothing). AgentMemorySpec.logPath writes a JSONL row per memory_search
— the retrieval-log seam for agent-knowledge's RetrievalHoldout, which
stays cross-package (not a dependency of this repo).

Also fixes a stale improvement-driver test: its partial report-finding
fixture now conforms to the P4 toAnalystFindings pass-through contract
instead of asserting a pre-lift id.
…or the research optimizer

- 'connection' ArtifactKind: an ExternalMcpGrant (http url / stdio launch,
  literal headers/env, secrets by provider KEY NAME, optional hub grant)
  promotes onto profile.mcp[key] via connectionMcpServer and onto
  profile.connections (artifact wins per connectionId)
- buildableGenerator: BuiltCandidate.remote branches the mcp emit off the
  hardcoded stdio transport — the adopt-not-build path emits a
  { transport:'http', url, headers, env } server with adopt provenance
- KeyProvider (runtime/key-provider.ts): declarative secretEnv
  (mcp[key].metadata.secretEnv, names only) resolved at materialize time;
  envKeyProvider reads the dotenvx-loaded env; materializeLocalMcp injects
  values into the spawned server child env only — fail-closed on a missing
  provider/key, values never on the profile or in logs; sweEvalRunner
  forwards keys so an adopted server scores with its credential live
- research seam: driverLoopGenerator gains an optional research{query} tool
  + researchDriverNote (adopt-before-build doctrine), offered only when
  wired; mcpBuildPrompt instructs adopt-over-build; no live web backend
  ships yet (the seam is worktreeBuildCandidate driver.research)
The e2e driver imports sweEvalRunner, which lives in ../src and resolves only
through bench/tsconfig path aliases (tsx applies them at cwd=bench). Running
tsx bench/src/... from the repo root resolved the published agent-runtime in
bench/node_modules and crashed on the missing export. Add a bench package
script (self-improve) so the invocation always runs at cwd=bench, and document
the requirement in the driver header.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant