本文へ移動
cccskills
無料GitHub で公開

gepa-optimize

Optimize a pydantic-ai agent's instructions, tool descriptions, output type, and signature inputs using the gepa CLI. Trigger when the user asks to "optimize this agent", "improve tool descriptions", iterate on prompts driven by eval failures, or otherwise improve a pydantic-ai agent against a dataset. Operates in the user's repo with full filesystem access — YOU are the reflection model; the gepa CLI handles minibatches, evaluation, and Pareto bookkeeping.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md73.2 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

gepa-optimize

You are the reflection model. The gepa CLI is a small toolkit that handles minibatches, trace/report persistence, candidate evaluation, comparison, and history bookkeeping; you read failure reports and traces, edit component slots or source code, and continue until the run completes.

There is no propose or reflect verb on the CLI because that's the work you do — while gepa run is paused, or between manual gepa eval invocations — by editing files.

Driving an already-started run

gepa --gepa-dir /absolute/workspace run drive --run-id RUN --reflectors reflectors.json automates lane runs and single-checkout held-out runs with one short-lived reflector per step. Run it in the harness environment. It leases/reset lanes, dispatches the fixed training-reflection prompt, guards process exit, and calls run select or harness serve --once. No separate harness service, selector, or reflector may run for a driven run: the driver owns scoring timing. The existing held-out lane refusal remains until standalone lane repositories land.

The JSON configuration contains only allowed commands, for example:

{"pass_env":[],"reflectors":[{"label":"codex","argv":["codex","exec","--profile","gepa-reflector","-C","{checkout}","Read {prompt}; follow its instructions using {packet}."],"usage_limit":{"exit_codes":[9],"output_regexes":["(?i)usage limit"]}}]}

Configure detection for the actual command; exit 9 above is an example, not a Codex exit-code guarantee. Usage-limit matching applies only when no nomination or lane result exists. A legacy bare array means empty pass_env. Templates accept {checkout}, {packet}, {prompt}. The default is one Codex entry using --profile gepa-reflector and usage-limit output detection. In current exec --help, --profile loads a separate $CODEX_HOME/gepa-reflector.config.toml overlay; provision its actual permissions. It is not the codex sandbox -P permissions selector. Follow the README's dated verified-profile table and held-out read restrictions. Verification is the caller's responsibility; never list unverified Claude Code or Pi profiles for held-out runs.

--step-timeout defaults to 900 seconds; --max-attempts to 2; --max-steps optionally bounds sessions. Lane nomination adds --foreground; single-checkout nomination adds --wait-secs 0. Keep harness credentials out of the reflector. Provider keys are scrubbed by default; pass_env accepts explicit training-only variable names such as OPENAI_API_KEY, never patterns. Do not pass scoring keys. GEPA_HELDOUT_*, GEPA_HARNESS_*, names in GEPA_HARNESS_PASS_ENV, candidate components and trace variables cannot be allowlisted. Subscription configuration is retained; keep other harness secrets out of the launch environment. Git config appends gc.autoDetach=false and maintenance.autoDetach=false to existing entries.

Before an unattended held-out run, set a provider-side spend limit on each training-only reflector key passed through pass_env. This backs up the harness spend cap because the reflector's own model calls, including training evaluations run in its environment, are outside that cap.

Public step directories hold training-only prompts and logs. Held-out state lives beside the pin. Training-only state lives under ${XDG_STATE_HOME:-~/.local/state}/pydantic-ai-gepa/drive/<key>/drive.json, keyed by resolved GEPA_DIR and run ID. State/lock files are 0600 in a current-user-owned 0700 directory outside GEPA_DIR, project and lane worktrees. The reflector sandbox must not grant that directory. Legacy public run_dir/drive.json is refused, never loaded or migrated. Forging private state is outside the pin's threat model.

Restart the same drive command after a kill: unfinished reflectors are guarded first, interrupted lanes reset, and scoring reuses existing durable checkpoints. Events are acknowledged after recording the action; merge opportunities are never auto-merged. A changed list requires --accept-reflector-change. Exhausted fallback lists restart from the first entry. Exit 0 prints the final report; 2 is input/list refusal; 76 is an attempt/step/scoring pause; 77 is usage-limit exhaustion; 78 is process-guard refusal. Scoring errors do not retry: recover the service, explicitly resume the managed run, then drive again.

What the process guard does not detect

The macOS guard uses libproc's immutable process and original-parent unique IDs, polls at 50 ms, and retains observed ancestry across restarts. After group cleanup and a final snapshot it persists a step-end ceiling. Only IDs between the starting watermark and that ceiling, or descendants linked through those IDs, count. A restart of an interrupted step without a ceiling freezes it at the first restart snapshot; unrelated later births cannot keep extending the window.

The guard kills attributed reflector descendants and ignores processes provably rooted in another pre-existing process. An otherwise unknown Apple job is unrelated only when all three checks pass: its original-parent unique ID is launchd's (not merely its current PPID); proc_pidpath is under /System/ or /usr/libexec/ and csops reports CS_PLATFORM_BINARY; and its responsible PID is available and differs from both the driver's and the reflector root's responsible PIDs. A missing or failed check leaves it unknown. Unknown processes are never killed: after --survivor-grace (default 5 seconds) they pause scoring with PID/command diagnostics. Unsupported platforms refuse to drive.

  • Launchd/XPC/launchctl/open work with unknown ancestry pauses the drive. The Apple job exemption above is a gap if a reflector induces a job satisfying all its checks to do work on its behalf. Orphaning a platform binary alone does not qualify; the original-parent identity and responsibility checks still apply.
  • Work done by an already-running same-UID process (tmux server, another shell, a loaded LaunchAgent, an ssh ControlMaster) escapes attribution.
  • Processes that change UID/GID through setuid/setgid execution can leave the same-UID snapshot and escape inspection.
  • An ordinary double-fork can lose its intermediate before the 50 ms poll. That reader remains unknown and alive, so ordinary daemonizing tools can stall the drive. Git's no-detach settings reduce one benign source; no fork-event tracking is installed. Unknown survivors can deny service by keeping the drive paused.
  • Start the driver with no reflector running. Pre-existing reflectors and nominations are not guarded; a pending single-checkout nomination at startup is scored without a step guard.
  • Same-UID processes can hide or act before inspection. Snapshots and signals are not atomic; identity rechecks reduce but cannot eliminate PID reuse races. A future Linux PID-plus-start-time backend would also need to address reuse.
  • Reads during the reflector step remain possible. The guard separates reflection from scoring in time; it does not deny reads of other processes' argv while the reflector runs. The dated table shows raw KERN_PROCARGS2 reads remain possible; this is why process lifetime separation is needed.

A separate scoring UID would close these same-UID disclosure and interference channels; the driver does not provision one.

Outer Omni protocol for code candidates

gepa run --lanes is intentionally a single-parent managed run. Do not try to turn its lane count into a multi-parent Omni plan. For independently explored git/code candidates, a root orchestrator drives the separate durable outer controller and routes packets without reading reflection text:

gepa omni start --plan ./omni-plan.json
gepa omni next <omni-id> --json
# create only the workspace named in child_ready/phase2_ready, then launch your worker
gepa omni child-dispatched <omni-id> --receipt ./dispatch.json
gepa omni child-submit <omni-id> --receipt ./child-result.json
gepa omni compare-submit <omni-id> --receipt ./comparison.json
gepa omni reporting-submit <omni-id> --receipt ./report.json  # only if reporting was planned
gepa omni ack <omni-id> <event-id>

The plan uses SHA-256-pinned seed/minibatch/test artifacts, evaluator identity and digest, equal phase-one metric-call slices, an explicit repeated comparison budget, and a fresh phase-two workspace. Workspace paths may be intended paths at start; create or verify the exact isolated directory before submitting the dispatch receipt. A child_ready packet is all the worker needs: its child/engine ID, isolated workspace, immutable seed and minibatch paths/hashes, opaque SHA-pinned driver manifest, and reserved metric calls. The controller never executes the manifest; the orchestrator reads it to choose its engine adapter. Submit only immutable receipts with those same identities. The controller, not child self-reported scores, chooses a Pareto provisional winner, then accepts it only when the shared confidence-interval/practical-delta comparison clears the frozen threshold against seed/incumbent. Rejected, equivalent, or inconclusive votes retain the baseline; an optional bounded max_repetitions can collect more matched samples only for an inconclusive vote. Events redeliver unacked work after restart.

Keep the emitted packet untouched: its original SHA is recorded in the durable outbox, and dispatch/semantic receipts are rejected if the packet, plan, or frozen input bytes changed after emission.

Child optimization usage is an explicit child_receipt attestation. The outer controller enforces its reserved ceiling but does not independently meter an arbitrary worker; production adapters should bind manifests to evaluator-owned usage-ledger receipts.

Use error-submit only for an explicit, evidence-backed frozen evaluator target that is inconsistent or unattainable. Do not emit ERROR.md for ordinary code, test, provider, credential, or worker failures.

Setup (run once)

gepa init \
  --agent mypkg.agents:my_agent \
  --metric mypkg.metrics:my_metric \
  --install-skill

What each flag does:

  • --agent MODULE:ATTR — points at the pydantic-ai Agent instance; required in component mode and optional in git mode.
  • --candidate-source components|git — selects text-slot candidates (default) or whole-working-tree candidates.
  • --evaluate MODULE:ATTR — git-mode alternative to --agent; points at a plain task callable.
  • --metric MODULE:ATTR — optional. An async (or sync) callable (case, output) -> MetricResult | float. Omit it to use the default substring/equality scorer, which is only useful for trivial expected-output strings.
  • --install-skill — drops this SKILL.md into <repo>/.agents/skills/gepa-optimize/ so coding agents auto-discover it. Pass it the first time.

Write reflection-training cases at .gepa/dataset.jsonl, one JSON object per line. The harness provisions held-out selection cases in the same format at the harness-only GEPA_HELDOUT_DATASET path:

{"name": "case-1", "inputs": "...", "expected_output": "...", "metadata": {}}

For held-out runs, the harness process started by the orchestrator holds GEPA_HELDOUT_DATASET (an absolute path). Never give that variable, its value, or the held-out directory to the reflector or any process it launches. Scoring commands (run start, harness serve, run select, and eval) remove the variable from os.environ before importing or running candidate code and keep the path in controller memory during the command. gepa.toml must contain only the training dataset; the old validation_dataset setting and init --validation-dataset are refused with exit 2. Held-out entries in the repository's .env are also refused. Legacy state carrying a validation path is refused; start a new run in a clean workspace.

# Orchestrator / harness shell only:
gepa init --agent mypkg.agents:my_agent
export GEPA_HELDOUT_DATASET=/srv/gepa-heldout/my-project/validation.jsonl
gepa run start --heldout-required --max-iterations 50
gepa harness serve --run-id RUN_ID

# Separate reflector shell, launched without the private environment:
gepa run continue --run-id RUN_ID --reflector-epoch 1

continue atomically nominates the candidate and waits for a harness result. It never evaluates a held-out run, even if the variable was accidentally set in its environment. The harness takes the run lock, checks the epoch and clean candidate identity, evaluates the training gate, then confirms training winners on validation. It rechecks the tree before saving state and after scoring. Held-out runs require a committed, clean git candidate with scalar acceptance. They support unpinned code scoring or the trusted-scorer mode described below. Component candidates, vector comparators and pinned scoring without trusted_scorer fail closed before candidate imports. A dirty-tree refusal asks you to commit before nominating. Keep the nominated tree unchanged until the result arrives. The harness checks the shared checkout for stale nominations, but scores a private checkout of raw Git objects (the nominated commit for unpinned scoring, or the harness-pinned scorer revision for trusted scoring). No worktree registration, index update, or ref write exposes that checkout in the shared repository.

The reflector receives training feedback reports, aggregate validation verdicts and exit codes. Arbitrary child-written traces are kept private and discarded. --wait-secs defaults to 300; 0 enqueues and returns immediately. Nomination waits up to five seconds for a busy run lock; either a lock timeout or a result timeout exits 75. Run the same command again to reattach without duplicating the nomination. A different pending candidate or gate selection is refused. harness serve --once processes one current nomination (and retires old epochs), then returns. Interrupted scoring resumes from the existing paid evaluation ledger. Harness-side run resume issues a new epoch and packet without evaluating validation.

Results contain explicit controller messages only; candidate/evaluator stdout, stderr, and logging streams are never relayed to the reflector. Unexpected scoring exceptions produce a retryable exit 1 result naming only the exception type and the training/held-out phase. The harness keeps serving. Unpaid or changed-candidate continuations are retired while paid evaluations remain charged, so a fixed commit can be nominated without operator cleanup. After a run finishes, continue returns its public final status; any undelivered terminal result keeps its original output and exit code. Lane runs immediately direct callers to lane continue and run select without queuing a nomination.

Harness commands are held-out run start, harness serve, lane run select, eval --dataset-role validation, and held-out run resume (including --abandon-continuation). They fail closed without the harness environment. Reflector commands are run continue, run status, and reading reflector_packet.json. lane continue evaluates only training; the orchestrator's run select performs held-out selection. Runs started without the variable or --heldout-required retain in-process, training-only continuation.

The harness stores the dataset path and digest in a private .gepa-heldout/ record beside the dataset. Paired per-case evidence stays in the neighboring .gepa-validation-evidence/, with detailed spend checkpoints in .gepa-validation-spend/; all must remain inaccessible to the reflector. Public state records heldout_required and aggregates only. Dataset identity changes, paths inside any checkout or GEPA_DIR, and data recoverable from Git objects are refused. If Git ever held the data, use a fresh repository without those objects. Validation produces no public reports or traces.

The reflector's sandbox must restrict data reads to its worktree and GEPA_DIR, plus the runtime paths needed to execute commands. Keep the entire held-out directory outside every path the sandbox can read, including system temporary directories: Codex's :minimal grant includes /tmp, /private/tmp, and system temporary locations such as /var/folders and $TMPDIR. Putting validation outside the worktree and GEPA_DIR alone does not establish isolation. A home-directory sibling such as /Users/operator/.gepa-heldout/run-1/ works when no readable grant covers it.

This named profile was verified locally with Codex CLI 0.157.0. Write it to the launcher's CODEX_HOME/config.toml (a scratch CODEX_HOME works for probes):

[permissions.gepa-reflector.filesystem]
":minimal" = "read"
"/absolute/candidate-worktree" = "write"
"/absolute/training-gepa-dir" = "write"

[permissions.gepa-reflector.network]
enabled = false

Use codex sandbox -P gepa-reflector -C /absolute/candidate-worktree -- COMMAND with that CODEX_HOME. Define the profile in the config file rather than -c overrides: dotted path keys (for example ~/.gepa/...) can be split on their dots by override processing. Provision Python and its dependencies under the worktree's interpreter directory, or grant narrowly scoped runtime reads. Do not inherit :workspace/:read-only or grant "/" reads. See the Codex permissions reference.

The no-model probe verified that sandboxed environment/config/state/packet output contained no held-out path, a direct read of the home-directory held-out file failed with Operation not permitted, sandboxed continue enqueued, the external harness scored, and sandboxed continue returned the accepted verdict. Repeat this read-denial and scoring probe for the actual deployment's paths and grants; system-temp placement does not protect held-out data.

Which process scores a held-out run. GEPA_HELDOUT_DATASET alone gives held-out selection: winners are still confirmed on the held-out set, and every rollout, training and validation, is scored in the controller process, as in a training-only run. The harness-owned scoring child below, Seatbelt or trusted_in_process, runs only when the launching harness also sets GEPA_HARNESS_SCORING_SANDBOX=1; any other value is refused, so a misspelled request never drops the sandbox. In-process scoring imports the scorer (and, with an unpinned scorer, the candidate) into the process that holds the held-out path, so use it only on a trusted operator machine with a reflector that cannot reach that process. Validation feedback, traces and per-case scores stay private either way. The variable is reserved and never forwarded to a reflector.

When the harness requests it, candidate code is scored in a sandboxed child from a harness-owned detached checkout. The parent reads the held-out dataset and sends one case at a time over a pipe; the child returns bounded JSON results. Candidate agent, evaluator, metric, case-factory and pricing imports happen only in that child. Training scoring inside a held-out harness also uses the child; training-only runs retain the in-process evaluator. The parent never adds candidate import paths while running held-out harness commands. After each evaluation it kills the worker's process group, then sweeps same-user processes for the child's inherited Seatbelt permissions to catch descendants that called setsid(). Finding a survivor or failing process inspection refuses the evaluation instead of returning a quality score. This repeated sweep is not an atomic process container: enumeration and PID reuse races remain, and a descendant that changes UID is outside the sweep. Private checkout/scratch directories are removed on exit. Checkouts are built from raw Git tree/blob objects without worktree conversion; symlinks, gitlinks and special files are refused.

For trusted-scorer held-out scoring, freeze these settings in gepa.toml:

[acceptance]
mode = "scalar"
pinned_scorer = true
trusted_scorer = true
component_files = ["prompts/planner.md"]

The orchestrator must set GEPA_HARNESS_SCORER_REVISION to the trusted scorer's full commit SHA (40 or 64 hexadecimal characters) in the harness environment, including run start, harness serve and run select. The revision must exist in the original scorer project repository. With --candidate-root, component blobs come from the separate candidate repository; scorer history stays in the original repository. Missing, malformed or non-commit revisions are refused. The private checkout contains only that revision. The parent reads the nominated commit's component files as raw UTF-8 Git blobs, with no filters or candidate imports, and sends their text in the initialization message. The child sets GEPA_CANDIDATE_COMPONENTS_JSON before importing the scorer. Keys are the exact component_files paths, relative to the Python project root. Missing files, symlinks, non-blobs and invalid UTF-8 are refused. The scorer must consume this text as data. The candidate's tree is never written into the child's checkout.

For either held-out mode, the harness can set GEPA_HARNESS_FROZEN_FILES to a JSON object mapping Git repository root-relative paths to SHA-256 hashes, for example {"api/evals/scorer.py": "<64 hex characters>"}. The trusted parent verifies the exact private-checkout bytes before starting the child: candidate revision files in unpinned mode, trusted revision files in trusted mode. A bad manifest, unsafe path, missing/non-regular file or hash mismatch is a static infrastructure refusal, never a quality score. Omission means no frozen-file checks; launchers requiring a freeze must supply the complete mapping. Both variables come only from the orchestrator's environment, never nominations, reflector config or run state, and cannot be forwarded with GEPA_HARNESS_PASS_ENV. Training-only gepa eval behavior is unchanged.

The parent verifies the commit, every tree and every consumed blob against its Git object ID (SHA-1 or SHA-256) before launching the child. Shared object-store substitutions are refused. Frozen manifest paths must match verified tree names exactly, including case. When GEPA_HARNESS_FROZEN_FILES is set (even to {}), checkouts cannot contain __pycache__/, .pyc, .so or .pyd files, or package directories shadowing a frozen .py module. In unpinned code-engine mode, frozen hashes detect edits; they do not isolate the scorer from candidate code running in the same interpreter. Trusted mode is the scorer isolation boundary.

The default backend is macOS Seatbelt (/usr/bin/sandbox-exec). It denies writes outside private scratch, reads of the held-out directory outside the child's own checkout/scratch, and network connections except to the harness's loopback CONNECT proxy. The proxy listens on 127.0.0.1; Seatbelt's localhost:<port> rule covers loopback addresses on that TCP port. Fork/exec is allowed for per-case evaluator subprocesses; descendants inherit the same Seatbelt restrictions. Linux and hosts with an unusable backend refuse Seatbelt scoring; a requested sandbox has no unsandboxed fallback. macOS CI requires real OS adversarial tests. Seatbelt is deprecated; repeat those tests on each deployment OS. Local Codex may prohibit nested Seatbelt, so a local skip is not enforcement evidence.

Trusted text scoring with local services

For a frozen text-only scope whose trusted evaluator needs embedded local services, explicitly opt in to acceptance.scoring = "trusted_in_process":

[acceptance]
mode = "scalar"
pinned_scorer = true
trusted_scorer = true
scoring = "trusted_in_process"
component_files = ["prompts.py", "skills/SKILL.md"]
text_files = ["skills/SKILL.md"]
scope_verifier = "evals.freeze:verify_components" # optional

[acceptance.python_string_symbols]
"prompts.py" = ["INSTRUCTIONS", "chat_receipt_instructions", "Email.__doc__"]

The explicit text files (only .md and .txt) and Python symbol mappings must partition component_files. Python symbols name top-level functions, async functions, classes, assignments with Name targets, or annotated assignments. A named symbol masks every string constant in its AST node; X.__doc__ masks only X's existing docstring. Every declared symbol must exist. All remaining AST structure must equal the pinned scorer's copy. Candidate source is parsed only.

This mode requires scalar git candidates, both trusted/pinned flags, nonempty components, and the full harness-owned GEPA_HARNESS_SCORER_REVISION. Before every evaluation, changed paths and modes are checked against the harness-private lane repository's original Repositories.seed export commit. Thus history-free exports may omit scorer files. Single-checkout runs use the initial candidate HEAD recorded privately by run start. Changes outside components and untracked files (Git's normal non-ignored-file semantics) refuse scoring. Each Python component is separately checked against the pinned scorer revision, even if unchanged since export. Runs without a private candidate baseline are refused; start a new run to enable this mode on legacy records.

Despite its config name, scoring runs in a fresh trusted subprocess outside Seatbelt, from the same private pinned checkout. It retains the child protocol, timeouts, output/material caps, parent spend admission and private diagnostics. Candidate data arrives only through GEPA_CANDIDATE_COMPONENTS_JSON. Candidate, project and lane source paths are excluded from imports, including the operator's original scorer working tree. The private checkout's roots take precedence over editable installs. A meta-path guard and loaded-module checks refuse unpinned project imports, including modules loaded by startup .pth files. Interpreter site-packages dependencies remain allowed, even in an operator-local venv; editable source targets do not receive that exemption. Keep the harness and its dependencies outside reflector write grants. The optional pinned scope_verifier is synchronous and called as verify_components(components=dict[str, str]) before evaluator imports; it must return exactly True. Wrap an existing verifier to match that contract. Exceptions and refusals produce static public errors.

GEPA_HARNESS_PRIVATE_SCRATCH names a current-user-owned 0700 directory outside GEPA_DIR, project and lane roots. Put local Postgres/Redis/moto state there. It is removed after each evaluation phase, including failures. Both NO_PROXY and no_proxy are localhost,127.0.0.1,::1; local binds, Unix sockets and loopback connections work while provider clients retain the CONNECT proxy. Validation feedback, traces and diagnostics remain private; only aggregates are public. The worker process group is terminated on exit. Trusted services must clean up their resources and must not daemonize or escape that group; this mode has no Seatbelt descendant sweep.

Accepted residual risk: there is no OS sandbox on this trusted worker. Text candidates can still steer the model under test, and real external effects from scorer tools could leak held-out content. Use this mode only with local fake tools and an already-approved model route; the proxy is not an OS network boundary. The scorer must consume candidate text as data and never execute it. Every other requested scoring child continues to require Seatbelt with its unchanged profile; unsupported hosts never silently fall back to this mode.

Set GEPA_HARNESS_ALLOWED_HOSTS=api.openai.com:443,api.anthropic.com:443 in the harness environment to allow exact CONNECT destinations. The default is empty (no external network). Clients must honor HTTPS_PROXY/HTTP_PROXY; ordinary forward-proxy HTTP requests and direct connections are refused. The child inherits the fixed provider keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, GEMINI_API_KEY) and sandbox settings. To pass another evaluator setting, set GEPA_HARNESS_PASS_ENV=TYPESAFE_API_KEY,OTHER_NAME in the harness environment. Names are never read from a nomination, gepa.toml or .env. Reserved harness/runtime names (including GEPA_HELDOUT_DATASET and GEPA_HARNESS_ALLOWED_HOSTS, GEPA_HARNESS_SCORING_SANDBOX, GEPA_HARNESS_SCORER_REVISION, GEPA_HARNESS_FROZEN_FILES and GEPA_CANDIDATE_COMPONENTS_JSON) and values containing the held-out path are refused. Held-out harness commands skip candidate-owned .env files. Install the harness and its Python dependencies in storage the reflector cannot modify, and keep provider credentials out of the reflector environment.

Harness Git commands read reflector objects through a harness-owned repository, without loading reflector repository, global or system configuration, executable attribute drivers, hooks, fsmonitor, filters, textconv, external diff or lazy fetch. The harness pins an absolute Git executable once per process, requiring root ownership and no group/other write permissions on the executable and every ancestor directory. On macOS it appends usr/bin/git to the developer directory named by the root-owned /var/db/xcode_select_link, or falls back to /Library/Developer/CommandLineTools/usr/bin/git; it never executes the /usr/bin/git xcrun shim or honors DEVELOPER_DIR. Xcode under /Applications does not qualify while that directory is admin-group-writable (the default); such hosts need the Command Line Tools installed. On other systems it checks /usr/bin/git, then /bin/git. No qualifying binary means refusal. Git retains its compiled helper location; GIT_EXEC_PATH is not set. Reflector metadata reads refuse symlinks, hard links and special files, except that these info/exclude entries are treated as empty. Oversized metadata, including info/exclude, fails closed. Worktree attributes can still apply Git's built-in data conversions. Harness Git storage lives beside the held-out set and must be outside the reflector's writable roots, just like the private scoring checkout. Public candidate retention refs are written as data through directory handles that refuse symlinks, without invoking source Git hooks; ordinary branch resets and garbage collection preserve the retained commits.

Lane runs use independent repositories, including supported held-out scalar runs. run start --lanes N --candidate-root /absolute/export/project seeds them from a separate candidate project; omitting --candidate-root uses the current project. The caller must supply a history-free, training-only export when the original history contains private data. The library does not filter datasets.

Repositories live at <GEPA_DIR>.lanes/<run-id>/<lane>/ and have no shared refs, objects, alternates or remotes. Grant each reflector access only to its own lane, the public GEPA workspace and required runtimes; keep the source/export, other lanes and the controller's repository store outside every grant. Use an external absolute GEPA_DIR for that boundary. Unpinned training imports from its lane. Pinned lane continue loads evaluator, metric and scorer code from the controller-owned incumbent checkout at <GEPA_DIR>.scorers/<run-id>/<lane>/<incumbent-sha>/<prefix>. Grant that lane read-only access to this snapshot, never write access. It contains only the seeded incumbent and its ancestry, with no unrelated refs. When seeded from a history-free export, no source checkout history is introduced. The controller creates it at fan-out; no original scorer-checkout read is required by the reflector. Held-out scoring still uses the private trusted scorer. Snapshots are created only for pinned runs, repaired on re-fan after an interruption, and removed when the run finalizes.

Seed trees containing symlinks or gitlinks anywhere are refused. Exporters must drop or materialize those entries before starting lanes. Each lane needs its own <prefix>/.venv when the packet uses a workspace-local interpreter; provision it (e.g. uv sync from that lane's project) before executing the packet command. The packet's runtime_setup message identifies a missing interpreter.

The controller copies nominated objects without using lane config/hooks, keeps only their reachable closure in its own durable repository, and scores retained commits. Held-out stores live under the private dataset sibling's .gepa-heldout/repositories/; training-only stores live at <GEPA_DIR>.repositories/<run-id>/. Keep these stores for replay/adoption after lane cleanup. The original project is never reset to a winning candidate. Later iterations start from the retained winner. Old linked-worktree runs must restart; no migration is attempted. Pinned-scorer held-out runs require the trusted-scorer settings and harness revision described above.

Sandbox rollouts run serially; the parent retains cost admission and response-level accounting. Public spend uses a fixed sandbox model bucket to prevent model names from carrying held-out text. Training traces, UTF-8 sibling artifacts and JSON output/side-info return through a checked channel (1 MiB/file, 4 MiB/case, 32 MiB/eval; structured output/side-info: 256 KiB), with artifact links in the trace and case-specific sibling directories. Validation phases return only aggregates.

The orchestrator must enforce the reflector's sandbox: the harness cannot identify the reflector's profile or refuse an unverified one. Require denial of SysV IPC listing and use. Preventing concurrent process-argument disclosure also requires that the reflector cannot read other processes' arguments; the two Codex profiles below do not meet that requirement for same-user scoring. A separate scoring UID would close that argv channel; this library does not provision one. Unsandboxed reflectors (including Pi used without a sandbox) must not be used for code-engine held-out runs. Claude Code's sandbox and Pi/Kimi reflectors have not been verified and are not on the verified list.

Host probes on 2026-09-25, using macOS 26.6.2 and codex-cli 0.157.0, produced the following results. “Passed” means the probe was denied; “failed” means another same-user process's data was readable. These results verify only the listed operations, not complete isolation.

Reflector profileipcs listingSysV use/bin/psRaw KERN_PROCARGS2
README gepa-reflectorPassedPassedPassedFailed
Built-in :workspacePassedPassedPassedFailed

SysV use probes covered shmat, msgsnd, msgrcv, and semctl GETVAL; all were denied under both profiles. Those two profiles close the tested SysV channel on the reader side. The :workspace results do not replace the held-out file-read restrictions described above. Denial of /bin/ps does not prevent raw KERN_PROCARGS2 from returning another same-user process's arguments and environment.

What this does not cover:

  • Scores and numeric usage feed public aggregates, creating low-bandwidth covert channels across repeated evaluations.
  • Traffic to an allowlisted model endpoint may be readable through its provider account. Keep harness credentials out of the reflector, but this is not enough: candidate code can also supply its own credentials to an allowed host. CONNECT restricts destinations, not accounts, HTTP methods or request contents.
  • SysV shared memory, message queues and semaphore sets created by the scoring child can outlive it: Seatbelt does not mediate their creation. The library does not remove them. macOS records no creator PID for queues or semaphore sets, so it cannot distinguish them from objects created by other programs. The project handles these objects through the reflector-profile requirement; reader-side denial is verified only for the two Codex profiles above. A separate scoring UID or VM is a hardening follow-up.
  • Process arguments and environment visible to another same-user process during scoring. Raw KERN_PROCARGS2 works under both tested reflector profiles, and a process can rewrite its own argument strings to expose case data. No child Seatbelt profile prevents this reader-side channel. Survivor cleanup does not close the during-scoring window; a different scoring UID would.
  • Timing, CPU, memory, flock signalling on readable files, and denial-of-service channels. The sandbox is not a resource limiter (the result reader has a timeout and message-size bound).
  • OS/kernel exploits, changes to the trusted harness/runtime, or other private datasets outside the configured held-out directory.
  • Evaluators that need a local database or another local service. The child has no direct local-service ports; only the allowlisted model proxy is reachable.
  • Parallel rollouts within a scoring child; scoring is serial.
  • Separate OS ownership of the harness and reflector. A reflector that can write beside the held-out dataset can defeat the private record; filesystem isolation of that directory remains required.

For held-out runs, harness-written files in GEPA_DIR are views: the harness acts only on its private record beside the held-out dataset, refusing and restoring changed views. A missing private record fails closed; legacy held-out runs must be restarted. Runs without held-out validation keep their existing behavior. Held-out epoch changes and run resume require the harness or orchestrator with GEPA_HELDOUT_DATASET; reflector-side resume is refused. Harness run status on a finished held-out run rewrites final_report.md from the private record.

In component mode, gepa init introspects the agent, writes .gepa/gepa.toml, and pre-seeds .gepa/components/<slot>.md from each slot's docstring / declared description. Git mode writes the config without introspection or component seeding.

Slot names use colons — you type them with colons everywhere: instructions, tool:foo:description, tool:foo:param:query, etc. The CLI handles disk encoding for you (the on-disk filename uses __ instead of :, but you never have to type that — gepa components set tool:foo:description --content-file ... and gepa components show tool:foo:description both Just Work).

gepa auto-loads .env from the repo root, so OPENAI_API_KEY / ANTHROPIC_API_KEY / etc. are picked up automatically. Pass --no-dotenv to skip.

Git-native candidates

Use git mode when the candidate is the whole working tree — code plus instruction artifacts or other tracked program files — rather than a set of SignatureAgent text slots:

gepa init \
  --candidate-source git \
  --evaluate mypkg.eval:evaluate \
  --metric mypkg.eval:metric \
  --install-skill

This writes the following top-level configuration:

candidate_source = "git"
evaluate = "mypkg.eval:evaluate"
dataset = ".gepa/dataset.jsonl"
metric = "mypkg.eval:metric"

evaluate is a sync or async callable invoked once per case. By default it receives the complete pydantic_evals.Case; when case_factory is configured, it receives the factory's materialized value instead. It returns the pipeline output passed to metric. This is a plain task hook: it does not need to be a SignatureAgent, expose components, or accept a candidate override. You may configure agent instead of evaluate when a normal pydantic-ai agent already runs the working-tree pipeline; git mode invokes that agent with an empty component override map.

--candidate-source git on gepa eval or gepa run start overrides the configured source for that invocation/run. Managed runs persist the selected source, so later gepa run continue calls use the same mode.

Candidate identity and evaluation

  • A clean candidate id is the first 12 hexadecimal characters of HEAD.
  • A dirty candidate id is <short-sha>-dirty-<content-hash>. The hash covers tracked diffs plus non-ignored untracked file paths, modes, and contents, so an uncommitted tree has a stable, distinct identity.
  • The Pareto row and managed state also record the full commit_sha.
  • .gepa/components/*.md, component introspection, stage-and-confirm, and candidate-file overrides are bypassed. The files currently on disk are what evaluation runs.
  • --candidate-file is intentionally incompatible with git mode.

The CLI records the tree identity immediately before evaluation. CLI-owned artifacts for the active run are excluded from the dirty hash; repositories should still ignore generated run directories.

Git-mode identity ignores global/user excludes (core.excludesFile and ~/.config/git/ignore); move those patterns to .gitignore or a regular .git/info/exclude file. Git LFS and other filter-based repositories are not supported: filters never run, so materialized filter-based files look dirty and cannot be nominated.

Reflector contract

Drive the managed loop as a commit-producing coding reflector:

gepa run start --candidate-source git --size 5 --max-iterations 50
read reflection_baseline_report_path and reflection_baseline_trace_path
analyze gold-miss feedback and spans from every pipeline stage
edit code/artifacts in the allowed scope
git diff; git add <files>; git commit -m "Improve ..."
gepa run continue --run-id <run_id>

continue first evaluates the new commit on the same training minibatch as the reflection baseline. Only a training improvement spends a full held-out validation evaluation. Validation—not the training minibatch—decides whether best_candidate_id and best_commit_sha advance. The CLI persists no validation report, trace, per-case score, or minibatch manifest for reflection. Status/start/continue summaries, lane packets and events, Pareto history, and the final report expose only aggregate validation scores and outcomes. Training reports and traces retain their full feedback. Validation evaluations write no report or trace file. When either gate rejects the candidate, the CLI pauses with:

git reset --hard <reflection_baseline_commit_sha>

The CLI reports this destructive command but never executes it. Review the target and run it yourself to discard the losing candidate; the next continue recognizes the restored baseline and advances. To restore the best accepted result after the run, use the final report's value:

git checkout <best_commit_sha>

Per-stage trace file contract

Before each trace-enabled training evaluation, the CLI exports GEPA_TRACE_FILE with the absolute path:

<gepa-dir>/runs/<run_id>/traces/minibatches/<minibatch_id>/<iteration:04d>-<eval_id>-<candidate_id>.jsonl

(The eval summary JSON printed by `gepa eval` / `gepa run` carries the exact
`report_path`/`trace_path` — prefer those over constructing paths by hand.)

Task code may also read it with pydantic_ai_gepa.cli.eval.current_trace_path(). Append one compact OpenTelemetry span JSON object per line; do not truncate the file. For Logfire/OpenTelemetry spans, use the serializer consumed by StructuredTraceStore:

import os
from pathlib import Path

from pydantic_ai_gepa.gepa_graph.proposal.trace_store import span_to_jsonl_line

trace_path = Path(os.environ["GEPA_TRACE_FILE"])
with trace_path.open("a", encoding="utf-8") as trace_file:
    for span in finished_spans:
        trace_file.write(span_to_jsonl_line(span))

Tag each span with a stage attribute such as stage=classify, research, resolve, or extract. The reflector's structured trace tools load this same file and can then filter failures by stage.

Standard loop

Prefer the managed loop when you want a real max-iteration optimization run:

gepa run start --max-iterations 100 --size 5 --acceptance-repetitions 3 --acceptance-max-repetitions 5
read the printed report_path and trace_path
reflect on the failures; edit .gepa/components/<slot>.md or source code
gepa run continue --run-id <run_id>
if verdict=accepted, the candidate improved training and held-out validation; keep it and advance
if verdict=rejected or equivalent, discard/revise your edits and continue again
if verdict=inconclusive, do not count it as a failed hypothesis; revise it or end without a false decision
if it pauses_for_reflection, inspect the new report/trace and reflect again
repeat until the JSON summary says status=done and prints final_report_path

gepa run start scores the seed on held-out validation, then evaluates sampled reflection-training minibatches until at least one case falls below --threshold. It pauses and writes:

  • reflection_baseline_report_path
  • reflection_baseline_report_paths
  • reflection_baseline_trace_path
  • reflection_baseline_trace_paths
  • reflection_baseline_samples
  • state_path
  • next_command

gepa run continue nominates your edited baseline. The orchestrator's harness evaluates it against the same saved training minibatch. With repeated acceptance enabled, it compares rollout-level mean samples and reports the observed variance, confidence interval, practical minimum delta, and accepted, rejected, equivalent, or inconclusive training verdict. The harness then evaluates a training-accepted proposal on the complete held-out validation dataset, and only a validation improvement advances the baseline. Repetitions remain full end-to-end evaluations; they do not freeze intermediate pipeline output or pretend model randomness is seeded. At --max-iterations, the CLI prints and writes final_report.md.

Use --acceptance-repetitions 3 --acceptance-max-repetitions 5 as a practical starting point for stochastic agent pipelines. Set --acceptance-min-delta <score> when tiny positive changes are not worth adopting. A value of one repetition preserves the old exact single-rollout comparison for deterministic or compatibility-sensitive suites.

Use one-off gepa eval for manual probes or deterministic A/B checks:

gepa eval                                       # score the current baseline + write per-case report
read .gepa/runs/<run_id>/reports/<id>.md        # see what failed
edit .gepa/components/<slot>.md                 # via `gepa components set --content-file`
gepa eval --minibatch-id <id> --run-id <run_id> # clean A/B on the same minibatch
git commit + tag a good baseline                # checkpoint when the metric improves
repeat

The eval summary you parse is the last JSON line on stdout — it carries run_id, minibatch_id, mean_score, report_path, and iterations.

Resuming with another reflector

When the coding agent loses its session, hits quota, or must hand off, issue:

gepa run resume --run-id <run_id> --reason "session lost" --reflector <label>

Omit --run-id to resume the latest managed run. The command never evaluates, changes the candidate tree, or consumes budget. It works at every single-path pause and re-issues the final packet for a completed run. Its last JSON line is {"packet": ...}; start, continue, and status also expose reflector_packet_path. Hand the new agent that file at runs/<run_id>/reflector_packet.json. It contains the scored baseline, current tree identity, training reports and traces, last comparison, journal tail, notes index, best candidate, budget, and instructions. Validation remains aggregate-only. Local agents and Mighty threads receive the same packet.

Run next_command.argv in next_command.cwd, or use next_command.shell from any directory. Both select the absolute workspace and carry --reflector-epoch N. continue refuses a stale epoch with exit 2 and names the current epoch. Omitting the flag preserves existing CLI behavior.

The loop phase remains in status. The separate reflector block has an epoch, label, issue timestamp, and active or lost state. Resume records the previous epoch as lost, with its reason and timestamp in a bounded history, then issues an active epoch and appends a reflector_lost journal row. Repeating resume simply issues another packet and epoch. There is no heartbeat or automatic expiry; the operator decides when a reflector has been lost.

  • Death after commit, before continue: the packet identifies the committed, unscored proposal. Review it and run next_command, or explicitly restore the reported baseline. The CLI never discards the commit.
  • Death after continue completed: the packet retains the recorded verdict, including an accepted proposal after the loop advances. Repeating continue on the recorded candidate returns the result with exit 0 and no evaluations. In particular, after acceptance advances to paused_for_reflection, calling continue on the unchanged tree returns "already scored" without evaluating; edit the tree to move on. For a rejected proposal, restore the baseline to advance or revise the tree to compare a new proposal.
  • Death during continue: durable training samples and any completed validation evaluation are recovered before evaluating remaining samples, so paid evaluations are not duplicated. Gate samples use a separate checkpoint because they do not write Pareto rows. Keep the interrupted candidate unchanged until its comparison finishes; the packet preserves any --gate-case options.

If a pending candidate cannot be restored (especially overwritten components), use gepa run resume --run-id <run_id> --abandon-continuation. This journals and drops the checkpoint, preserving all paid budget charges, so the next continue starts a new comparison. A candidate change detected during evaluation also drops the checkpoint. Paired validation replay snapshots are private and removed when the continuation completes or is abandoned.

Continuation and resume share a run lock. A live holder causes exit 1; a process death releases the lock automatically. Lane runs refuse this command with exit 2: use gepa lane reset and gepa lane lease instead.

Concrete commands

# Start a managed optimization run.
gepa run start --size 5 --seed 0 --epoch 0 --max-iterations 50 \
  --acceptance-repetitions 3 --acceptance-max-repetitions 5 \
  --acceptance-confidence 0.9 --acceptance-min-delta 0.01

# Continue after editing components/source.
gepa run continue --run-id <run_id>

# Manual: score the current confirmed baseline once.
gepa eval --size 5 --seed 0 --epoch 0 --max-iterations 50 --capture-traces

# Read the report at the exact path printed in the summary line's report_path
# (filenames are <iteration:04d>-<eval_id>-<candidate_id>.md; eval_id is unique
# per eval, so never construct the path by hand).
cat .gepa/runs/<run_id>/reports/<iteration>-<eval_id>-<candidate_id>.md

# Edit a component slot. Content always comes from a file or stdin.
echo "Refined instructions about geography." > /tmp/new_instr.md
gepa components set instructions --content-file /tmp/new_instr.md

# Re-eval the new baseline against the same minibatch for a clean A/B.
gepa eval --minibatch-id <id> --run-id <run_id>

# Adopt a candidate JSON as the new baseline (optionally git-commit).
gepa apply --candidate-file ./candidate.json --commit

When to use apply --candidate-file vs. components set

  • gepa components set <slot> --content-file PATH — write directly to the live baseline at .gepa/components/<slot>.md. This is the default editing path during normal optimization.
  • gepa apply --candidate-file PATH — adopt a JSON file that bundles a set of slot overrides. Useful when:
    • You authored the candidate elsewhere (a branch, a snapshot, a script).
    • You want to apply many slot edits atomically (with --commit for a single git commit).

For routine single-slot edits, prefer set.

--seed and --epoch

Minibatch sampling is deterministic in (seed, epoch) over the dataset. Use the same --seed --epoch (or --minibatch-id) to get a clean A/B between slot edits. Bump --epoch (keeping --seed fixed) to get a fresh independent sample without changing your seeding regime.

--max-iterations

Hard cap on eval rows in a single run. Repeated training baseline/candidate evaluations and held-out validation evaluations all consume rows from this same budget. One validation row evaluates the complete validation dataset, so row count is a lifecycle bound rather than a direct model-call or cost bound. In the managed loop, gepa run start persists the budget and each gepa run continue advances until it either pauses for reflection or reaches status=done. In one-off gepa eval, exceeding the cap exits with code 70.

For held-out lane runs, max_iterations and max_token_cost bound only harness baseline, rebaseline, validation and confirmation evaluations. Reflector training spend is outside the run's cap. Lane training writes pareto.jsonl, vectors.jsonl and spend.jsonl under runs/<run>/lanes/<lane>/; these survive select and remain available to reflectors. The main run ledgers are harness-owned views and must not be edited. Their forged-state refusal and restoration remain in force.

Parallel reflection lanes

Use lanes when you want to explore several independent reflection directions at once in git candidate mode. gepa run start --lanes N fans the run out into N independent Git repositories — one per lane, each on branch gepa/lane/<run_id>/<lane>/<iteration> cut from the current best — evaluates every lane candidate in the background against one frozen shared baseline, and coordinates everything through an event stream. You play two roles: a thin orchestrator that consumes events and dispatches work, and short-lived reflector subagents (and occasionally a merge subagent) that do the actual reflection in isolated context.

Two operational notes from dogfooding:

  • Run the orchestrator where it can spawn children. In Prime Agent / RLM-style runtimes the orchestrator must sit at depth 0 (or dispatch through the host) — a depth-capped nested orchestrator cannot spawn reflectors itself.
  • Keep the controller store. Winning commits belong to the retained candidate repository, not the original checkout. Adoption maps the diff from the seed to the winner onto the original project in a reviewed change; do not assume git checkout <best_commit_sha> works in the original repo.

Requires candidate_source = "git" and a clean seed checkout — lane branches are always cut from a clean commit. Component mode stays on the single-path loop.

gepa run start --candidate-source git --lanes 3 --max-iterations 200 \
  --acceptance-repetitions 3 --acceptance-max-repetitions 5 \
  --straggler-timeout-secs 3600 --reflection-lease-secs 1800

Start emits one lane_ready event per lane. Each event payload carries packet_path and worktree_path — everything needed to dispatch a reflector without reading any other state.

Orchestrator event loop

Lane verbs resolve the workspace explicitly (never from cwd) — export the absolute workspace once and every orchestrator verb inherits it:

export GEPA_DIR="$(pwd)/.gepa"   # absolute; lane lease/continue/reset and run select require this

Drive the run by long-polling gepa next and dispatching on the event type:

loop:
    event = gepa next --wait --timeout 300 --json --run-id <run_id>
    # exit 0: event delivered; exit 4: timeout (retry the loop)
    # (without --wait, exit 3 means none pending)
    match event.type:
        lane_ready:
            gepa lane lease <lane> --run-id <run_id>
            # lease-refused (exit 1) means already dispatched — wait for the
            # lease to expire (lane_stalled) or `gepa lane reset` to reclaim
            dispatch reflector subagent with the packet/worktree paths
        verdict:
            record it (verdict, delta, comparison_path); nothing to dispatch
        selection_due:
            gepa run select --run-id <run_id>
        merge_opportunity:
            ask the trusted controller to copy the two retained commits into
            one lane, then dispatch a merge subagent (candidate SHAs are in
            the journal's lane_outcome entries if you need them)
        lane_stalled:
            gepa lane reset <lane> --run-id <run_id>
            re-dispatch the reflector with the same packet path
        budget_low:
            note remaining_evals; steer new dispatches toward cheap edits
        run_done:
            read final_report_path; stop the loop
            # a selection_due for a later iteration may arrive AFTER run_done
            # (the reaper synthesizes it before noticing done) — just ack it;
            # `gepa run select` on a done run refuses cleanly.
    gepa ack <event.id> --run-id <run_id>

Rules that keep the loop correct:

  • Route, never read. Reflection content — traces, reports, diffs — never enters orchestrator context. Event payloads carry paths and scalars only. Dispatch subagents with those paths; do not open the packet, reports, or traces yourself.
  • Ack discipline. gepa ack <event_id> only AFTER you have durably recorded your dispatch or decision (e.g. noted it in a scratch file or the subagent is launched). Events must be acked in delivery order — acking anything but the oldest unacked event is rejected (exit 5). If you crash, compact, or restart, gepa next redelivers unacked events verbatim; replay them to reconstruct exactly the pending work.
  • Lease before dispatch. gepa lane lease <lane> records the dispatch in lane state; a leased lane rejects re-dispatch and (for a fresh spawn) a second gepa lane continue until the lease is consumed by the reflector's continue, reclaimed with gepa lane reset, or --reflection-lease-secs expires. There is no release verb — the lease ends via continue, reset, or expiry.
  • One orchestrator per run. The event bus has a single consumer cursor; never run two orchestrator loops against the same run.
  • Stop on operator-required provider failures. If an evaluating verb reports expired credentials or a provider billing/quota code (insufficient_quota or credit_balance_exhausted), record the reason and stop the loop. Do not retry until a human restores credentials or credit.
  • Use gepa run status --run-id <run_id> for the lane board (status, candidate, verdict, progress per lane — always printed as JSON) whenever you need a ground-truth snapshot.

Supervision protocol

When a run-verb JSON summary contains a stall block, fork a separate supervisor session. The fork reviews the inherited trajectory, appends concise redirect advice with gepa journal append --strategy redirect, and exits. The primary loop continues normally; the next reflection receives that entry via its journal tail. The supervisor fork's only write surface is the journal: never edit components, commit candidates, mutate run state, or run gepa next / gepa ack.

Reflector notes

At session start, read the frontmatter index in .gepa/notes/; each <slug>.md note provides a name and description. Load bodies only when they are relevant. Promote proven journal insights into notes only BETWEEN runs: reflectors do not create, edit, or delete notes during an active managed run. Notes are reference material, never candidate components.

Reflector subagent dispatch template

The reflector gets exactly three inputs — the packet path, the worktree path, and an optional one-line steer. Never paste conversation history, other lanes' work, or your own analysis into the dispatch:

You are a reflection subagent for GEPA lane <lane>.

Inputs:
- Reflection packet: <packet_path> (read this first)
- Lane worktree: <worktree_path> (all edits happen here, on the lane branch)
- Steer: <one line, e.g. "focus on tool:lookup_order argument formatting">

The packet carries the baseline candidate's samples with report/trace paths,
metric side info, a journal tail of prior reflections, and the exact
`gepa lane continue` invocation. Work only from the packet plus the repo.

Do:
1. Read the packet, then the baseline reports/traces it points at.
2. Form one hypothesis and edit code/artifacts inside <worktree_path> ONLY.
3. Run the packet's continue_invocation verbatim as your terminal act:
   `gepa --gepa-dir <abs> lane continue <lane> --run-id <id>`
   It auto-commits the worktree onto the lane branch and starts the
   background acceptance eval. Your job ends there — do not report results
   back; the verdict arrives as an event.

Never:
- Touch the primary checkout or any other lane's worktree.
- Edit files under .gepa/ (run state, events, journal) by hand.
- Run `gepa run continue` / `gepa run select` — those belong to the
  orchestrator (and `run continue` errors out in a lane run by design).

The subagent's terminal act is gepa lane continue — it never relays completion by hand. The background eval streams progress into lane state and emits the verdict event; you will see it from gepa next.

Selection, merges, and stalls

  • selection_due → gepa run select --run-id <run_id>. Select is the single sequential authority: it validation-scores every training-accepted lane, promotes the strongest held-out improvement to the run's best, journals every loser (diff summary, verdict, delta) before deleting its branch, invalidates stragglers, enforces the budget, and re-fans every lane onto the new best with a fresh shared baseline and new lane_ready events. Never run gepa run continue in a lane run — it errors and points you at lane continue / run select.
  • merge_opportunity → dispatch a merge subagent. When two accepted lanes' diffs touch disjoint file sets, select names the pair (branch_a, branch_b, commit_a, commit_b, plus a diff_stat_path). The branch names describe the completed iteration; the commits survive in the controller store. The trusted controller must copy those commits into a chosen lane before dispatching a subagent to merge them. Merging needs code understanding, so the CLI never auto-merges. The merged tree enters the NEXT iteration as a lane candidate, never as an auto-accepted best: land the merge result in one lane's worktree after the re-fan (a normal reflector dispatch can start from it), so it goes through the same lane continue acceptance eval as any other candidate.
  • lane_stalled → reset and re-dispatch. A lane stalls when its reflection lease expires (reflector never ran lane continue) or its background eval died (stale heartbeat, dead pid). Run gepa lane reset <lane> --run-id <run_id> — it terminates a live recorded eval pid and returns the lane to paused; uncommitted worktree content is never auto-deleted. Then re-dispatch the reflector with the same packet path.
  • budget_low → tighten dispatches. Emitted when remaining evals fall below lanes × (--acceptance-max-repetitions + one validation eval). Prefer cheap, high-confidence edits from here and expect run_done soon.
  • run_done → stop. Read final_report_path for the outcome (including any budget overshoot). Adopt the retained seed-to-winner diff in a reviewed change to the original project.

Lanes vs the single-path loop

Prefer lanes when ALL of these hold:

  • The candidate source is git (component mode has one process-global agent and stays single-path).
  • You have several independent reflection directions worth trying against the same baseline — lanes pay the baseline evals once per iteration and overlap reflection with evaluation, so wall-clock per accepted candidate approaches max(reflection time, eval time / N) instead of their sum.
  • You can afford the coordination overhead: one orchestrator loop, N subagent dispatches per iteration, and a straggler timeout.

Stay on the single-path loop (gepa run start / gepa run continue without --lanes) when the budget is tight (lanes spend up to N × (--acceptance-max-repetitions + one validation eval) in flight per iteration), when reflections are sequential by nature (each depends on the last verdict), or when you are in component mode.

Candidate JSON schema

When you write a candidate file by hand (or read one produced by gepa eval history), use this shape:

{
  "id": "candidate-abc123",
  "components": {
    "instructions": "Refined instructions text...",
    "tool:lookup_order:description": "Look up an order by id (A-NNNN, B-NN, etc.)...",
    "tool:lookup_order:param:order_id": "The customer's order id."
  },
  "metadata": {}
}
  • id is optional — if omitted, gepa derives a stable hash from the component text.
  • components is the only required field. Each key is a slot name (same shape as gepa components list), and the value is the slot's text. Slots not in the map fall back to the current confirmed value.
  • metadata is free-form and ignored by the evaluator; use it to record origin (run id, source branch, etc.).

Content-file rule (strict)

Every text-content input goes through --content-file PATH or - (stdin). There is no --content "..." flag on:

  • gepa components set <slot> --content-file ...
  • gepa components confirm <slot> --content-file ... (optional override)
  • gepa journal append --content-file ...

This avoids quoting and heredoc bugs that plague multi-line text through shell flags. Use your Write tool to drop a file, then reference it.

Inline flags are OK for IDs, counts, seeds, and short tags (--strategy, --message, --minibatch-id, etc.).

Stage-and-confirm when adding tools

After editing source to add a new @agent.tool:

  1. gepa eval (no --candidate-file) detects the new slot via introspection, refuses to run (exit 2), and writes a stub under .gepa/staged/.
  2. Confirm — optionally overriding the docstring seed:
    gepa components confirm tool:new_tool:description
    # or:
    echo "A better description than the docstring." > /tmp/desc.md
    gepa components confirm tool:new_tool:description --content-file /tmp/desc.md
    
  3. Re-run gepa eval.

This is intentional — first eval after a code edit wastes budget if the new slot is weakly described.

Reflection Ledger (gepa journal)

.gepa/journal.jsonl is a small append-only log of insights you've learned across sessions: "case-04 fails when the customer mentions billing", "seed 7 over-represents shipping cases", "the gpt-4o-mini routing model paraphrases tool returns; instructions must demand verbatim echo". Use it as your own scratchpad across sessions.

  • At session start: gepa journal show --limit 20 to recall what you (or a previous session) discovered.
  • At session end: gepa journal append --content-file /tmp/insight.md --strategy minibatch-tuning to leave breadcrumbs for the next session.
  • --strategy is a short inline tag for grouping entries (minibatch-tuning, tool-renaming, metric-drift, etc.) — useful for grep-ing.

The journal is not automatically read by gepa eval. It exists so the coding agent has a persistent place to write reflections that survive /clear and outlive any one conversation.

Text fix vs. code edit — decision tree

Look at the per-case feedback field in the report:

Failure patternAction
Model picked wrong tool, or didn't call one it should haveImprove tool:foo:description (text component)
Tool argument was malformedImprove tool:foo:param:<path>
Output structure wrongImprove output:<name>:description
Tool genuinely missing — model would need a tool you don't haveEdit source: add @agent.tool, then gepa eval triggers stage-and-confirm for the new slots
Tool signature wrong (e.g. takes a string, should take a list)Edit source then gepa eval
Prompt instructions ambiguousImprove instructions

The library can't fix code-shape bugs by editing text. When the gap is structural, edit Python source.

Exit codes

CodeMeaningWhere
0SuccessAll verbs
1Recoverable error (missing file, invalid agent ref, dataset empty, orphan slots on apply, live run lock holder)All verbs
2Refusal — wrong input shape, unconfirmed slots, stale reflector epoch, or single-path resume of a lane rungepa eval / gepa run, every verb on argparse errors
70Hard cap — --max-iterations exceededgepa eval

When you see exit 2, read stderr for the recovery command: component confirmation, a fresh reflector packet, or the lane workflow.

Inspection

# Component overview.
gepa components list                    # table, default
gepa components list --format json      # programmatic
gepa components list --format tsv       # grep-friendly

# Read a single slot's current text. --source picks where it comes from
# (auto = confirmed > staged > seed; or pin to one explicitly).
gepa components show instructions
gepa components show instructions --source seed
gepa components show instructions --output-file /tmp/current.md

# Eval history for the latest run.
gepa pareto                             # default: full chronological history (json)
gepa pareto --format tsv                # | grep | awk
gepa pareto --front                     # only Pareto-dominant rows (multi-objective scoring)

# Managed run state.
gepa run status --run-id <run_id>

File layout reference

.gepa/
├── gepa.toml                  # agent + dataset + (optional) metric
├── dataset.jsonl              # case inputs + expected outputs
├── journal.jsonl              # Reflection Ledger (cross-session notes)
├── components/<slot>.md       # confirmed slot text (THE source of truth for values)
├── staged/<slot>.md           # stubs awaiting `gepa components confirm`
└── runs/<run_id>/
    ├── state.json             # managed `gepa run` controller state
    ├── reflector_packet.json  # versioned single-path handoff; training + validation aggregates
    ├── run.lock               # continuation/resume flock and holder PID
    ├── final_report.md        # written when managed run reaches done
    ├── pareto.jsonl           # append-only ParetoRow history (one row per eval)
    ├── minibatches/<mb_id>.json
    ├── reports/<iteration>-<eval_id>-<candidate_id>.md  # training only
    └── traces/minibatches/<mb_id>/<iteration>-<eval_id>-<candidate_id>.jsonl  # training only

The held-out validation dataset lives outside this repository; no validation case files, reports, or traces belong in this layout. Validation rows in pareto.jsonl contain only aggregate scores and outcomes.

In the default component mode, slot identity comes from live-agent introspection and slot values come from .gepa/components/<slot>.md (or the introspected seed when no file exists yet). Git mode bypasses both directories; the repository tree is its source of truth.

gepa.toml schema

agent = "mypkg.agents:my_agent"
candidate_source = "components"                # optional; "components" (default) or "git"
evaluate = "mypkg.eval:evaluate"               # git mode alternative to agent
dataset = ".gepa/dataset.jsonl"
metric = "mypkg.metrics:my_metric"           # optional; (case, output) -> MetricResult | float
case_factory = "mypkg.eval:my_case_factory"  # optional; (case) -> BaseModel (sync or async)
skills = "path/to/skills"                    # optional; enables list/search/load_skill tools

All keys are top-level — candidate_source / evaluate / metric / case_factory / skills MUST NOT be nested under any [section].

The metric callable signature:

from pydantic_evals import Case
from pydantic_ai_gepa.types import MetricResult, RolloutOutput
from typing import Any

async def my_metric(case: Case[Any, Any, Any], output: RolloutOutput[Any] | Any) -> MetricResult:
    # case.expected_output is whatever you put in dataset.jsonl
    # output is typically RolloutOutput; unwrap output.result for the agent's text
    text = output.result if hasattr(output, "result") else output
    return MetricResult(score=1.0 if text == case.expected_output else 0.0,
                        feedback="exact match" if text == case.expected_output else f"got {text!r}")

Binary inputs: the case_factory hook

dataset.jsonl is JSON, so anything that doesn't roundtrip JSON cleanly — PDF bytes, image buffers, audio blobs — can't live in inputs directly. The case_factory config field is the eval-time hook that bridges raw dataset rows to a fully-materialized agent input model:

agent = "mypkg.agents:school_calendar_extractor"
dataset = ".gepa/dataset.jsonl"
case_factory = "mypkg.eval:school_calendar_case_factory"
# mypkg/eval.py — eval-only module, NOT imported by the runtime agent
from pathlib import Path
from datetime import date
from typing import Any
from pydantic_ai import BinaryContent
from pydantic_evals import Case
from mypkg.agents.school_calendar import SchoolCalendarInput

def school_calendar_case_factory(case: Case[Any, Any, Any]) -> SchoolCalendarInput:
    raw = case.inputs
    attachments = [
        BinaryContent(data=Path(spec["path"]).read_bytes(), media_type=spec["media_type"])
        for spec in raw["attachments"]
    ]
    return SchoolCalendarInput(
        file_summaries=raw["file_summaries"],
        current_date=date.fromisoformat(raw["current_date"]),
    ).with_binary_attachments(attachments)
{"name": "ridgewood-2025-26", "inputs": {"attachments": [{"path": "fixtures/ridgewood.pdf", "media_type": "application/pdf"}], "file_summaries": [...], "current_date": "2025-09-01"}, "expected_output": {...}}

Rules:

  • The factory may be sync or async (return BaseModel or Awaitable[BaseModel]).
  • The returned model is used as both the agent's input AND deps, matching the no-factory path. Tools that read ctx.deps.attachments see the materialized binaries.
  • Only honored for SignatureAgent agents. gepa init --case-factory ... validates the dotted ref at scaffold time.
  • The factory lives in eval code, not in the runtime agent's input model — no BinaryContentRef or deferred-loading types leak into production. The runtime agent receives the same fully-materialized input whether called from production or eval.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

mighty

無料

Use the `mt` tool to work with this repo’s Mighty graph: run `mt prime` at session start; prefer `mt search`/`mt tree`/`mt show` before reading code; create specs/decisions/tasks and link evidence; triage `mt inbox`; and close out with `mt closeout` then `mt commit`. For full workflow + templates, run `mt prime`.

日本語の概要は準備中です。原文の説明を表示しています。

indexedlabs/pydantic-ai-gepa362026年10月7日 更新

Resolve `.mighty/loro/*.loro` merge conflicts safely using `mt merge`, then verify store health and sync.

日本語の概要は準備中です。原文の説明を表示しています。

indexedlabs/pydantic-ai-gepa362026年10月7日 更新

plan

無料

Plan work using Mighty (mt) by creating/updating specs and recording decisions, then spawning/structuring implementation tasks. Use when asked to plan/outline/break down a change, draft a spec (feature/rule/invariant/bug), record an ADR-style decision, or turn a fuzzy request into a structured Mighty spec tree with linked tasks.

日本語の概要は準備中です。原文の説明を表示しています。

indexedlabs/pydantic-ai-gepa362026年10月7日 更新

Convert roman numerals to integers.

日本語の概要は準備中です。原文の説明を表示しています。

indexedlabs/pydantic-ai-gepa362026年10月7日 更新

Audit and migrate specs to the Guarantees + Constraints format. Use when asked to review spec quality, find non-conforming specs, or batch-migrate specs from the old Behaviors/Interfaces format.

日本語の概要は準備中です。原文の説明を表示しています。

indexedlabs/pydantic-ai-gepa362026年10月7日 更新

tasks

無料

Track and execute implementation work using Mighty (mt) tasks, with progress comments, linked evidence, recorded decisions, and clean closeout. Use when asked to fix a bug, implement a feature/refactor, or “work through” changes in code while keeping Mighty tasks/specs/decisions updated.

日本語の概要は準備中です。原文の説明を表示しています。

indexedlabs/pydantic-ai-gepa362026年10月7日 更新

indexedlabs のスキルをすべて見る

このスキルの問題を報告する