feat: af install --path selector + Go SDK harness fixes#750
Open
AbirAbbas wants to merge 13 commits into
Open
Conversation
Add a --path flag to `af install` so a single repository can ship more than one installable agent node (e.g. a Python node at the root and a Go port under go/). By default `af install` finds the first agentfield-package.yaml root-first; with --path <subdir> it installs the node whose manifest lives at <root>/<subdir>/agentfield-package.yaml, and that subtree becomes the package root that is copied to ~/.agentfield/packages/<name>. - domain.InstallOptions gains Path (top-level source only; never propagated to recursively-installed node dependencies). - packages.ResolvePackageSubdir / ValidateSubdirSelector: shared, reusable resolution+validation. Reject absolute paths and paths that escape the source root via ".." (validated before any clone/copy work); a missing manifest is reported with the full expected path. - GitInstaller gains a Subdir field and resolvePackageRoot: no selector keeps the historical root-first walk; a selector resolves+validates the subdir post-clone. Composes with the existing @ref URL pin (parsed independently). - Local-directory installs honor --path with identical semantics (<local-dir>/<subdir>), resolved before any copy/registry mutation. - CLI help/examples document --path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Behavior-driven tests derived from the --path validation contract: - packages: ResolvePackageSubdir / ValidateSubdirSelector (root vs subdir selection, in-tree traversal allowed, absolute + escaping paths rejected, missing manifest names the expected path); GitInstaller.resolvePackageRoot against a faked clone (root install unchanged, --path go selects the go subtree, missing/absolute/escaping paths rejected — the last also proving InstallFromGit rejects a bad selector before any clone); @ref composes with --path; a Go subdir package builds relative to the subdir-as-root. - services: local-directory `af install --path <subdir>` selects the subdir node, bare install installs the root node, a missing/absolute/escaping --path errors and leaves the registry unmutated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a "One repo, many nodes: --path" section to the agent-node install guide covering git and local sources, @ref composition, the relative-path/escape rules, and that a bare install is unchanged. Add a --path row to the lifecycle reference table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
The claude provider appended the prompt as a trailing positional after
repeated --allowedTools flags; claude CLI 2.1.x treats --allowedTools
as variadic and swallows the positional, so every tool-passing call
exited 1 with 'Input must be provided'. The prompt now flows through
stdin (RunCLIWithStdin), which --print reads natively; codex/opencode/
gemini were inspected and are unaffected. Verified against the real
CLI via a gated integration test.
Note() posted to {base}/executions/note without the /api/v1 prefix,
404ing on the control plane (route registered under the /api/v1 group)
and silently dropping every progress note; existing tests masked it by
baking /api/v1 into the base URL, contrary to the SDK-wide bare-base
contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 7c6f11e)
Agent.Call was a synchronous POST /execute/{target} bounded by the
15s CallTimeout http.Client default, killing any caller whose child
reasoner worked longer. It now mirrors the Python SDK: submit via
/execute/async/{target}, then poll the execution record with
Python-parity pacing (0.25s doubling to 4s, jittered), CallTimeout
bounding each request rather than the overall wait, ctx-cancellable,
lineage headers on submit and polls so DAG parentage is unchanged.
The claude provider now uses --output-format stream-json --verbose:
per-event output keeps the CLI idle watchdog fed during long silent
turns (plain json emitted nothing until completion, so >120s turns
were SIGKILLed), and the terminal result event carries the same
fields including total_cost_usd. Real 2.1.191 capture committed as a
fixture; ctx cancellation now kills the whole CLI process group.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 18c3f3e)
…lt-runner fallback Adds tests closing the patch-coverage gap on PR #746's async-submit Agent.Call rewrite and the claudecode stdin/stream-json changes. agent_call_coverage_test.go (package agent) exercises the error/edge branches of Call's submit + poll loop that the happy-path tests miss: - nextCallPollInterval clamps (below floor / above ceiling) - submitAsyncExecution: build-request, transport, read-body, structured 4xx, and JSON-decode errors - awaitExecutionResult: ctx-already-cancelled, GET build error, ctx-cancel-during-poll, transport error, read-body error, poll HTTP-error status, poll decode error, succeeded-result decode error, and succeeded-null-result claudecode_defaultrunner_test.go covers the runCLI == nil fallback in ClaudeCodeProvider.Execute via a struct-literal provider (bypassing NewClaudeCodeProvider) driving the real default runner. Lifts sdk-go patch coverage from 61% to 98% (agent.go 98.29%, claudecode.go 100%), clearing the 80% min_patch gate. Tests only; no production code changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 5b763d8)
4 tasks
Contributor
Performance
✓ No regressions detected |
Port the codex harness patch behaviors into the Go codex provider so it matches the Python reference (runtime/codex_harness_patch.py): - Pass `-m <model>` (was ignored entirely; SWE-AF resolves gpt-5.5 vs gpt-5.3-codex by auth mode and the value must reach the CLI). - Add `--skip-git-repo-check` so the harness runs outside a git repo. - Replace deprecated `--full-auto` with the permission->sandbox mapping: "auto" -> --dangerously-bypass-approvals-and-sandbox; read-only / workspace-write / danger-full-access -> --sandbox <mode>; else --sandbox workspace-write. - Deliver the prompt via stdin (not argv), mirroring the claude provider. - Native structured output: when a schema is set, pass --output-schema (pointing at the STRICT rewrite) + --output-last-message, and read the last-message file when stdout yields no parseable final text. Schema plumbing uses a schemaAware interface the runner detects: the runner owns the strict-schema rewrite (codexStrictJSONSchema, ported from _codex_strict_json_schema), writes it, hands the provider the deterministic paths, and swaps in a codex-native prompt suffix while claude/opencode keep the Write-tool suffix. Options gains no schema field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ParseAndValidate was unmarshal-only, so output missing required fields, carrying invalid enum values, or (with additionalProperties:false) extra fields passed silently and the schema-retry loop never fired. Add validateAgainstSchema (github.com/santhosh-tekuri/jsonschema/v5) and run it at every parse-success point in handleSchemaWithRetry — after the initial ParseAndValidate/TryParseFromText and inside each retry iteration — so a validation failure sets err and the EXISTING retry branch fires (schemaMaxRetries default 2 preserved). Validation applies only when both a destination struct and a schema are provided; uncompilable schemas skip validation (return nil) so there is no regression versus unmarshal-only. This makes map-schema harness calls stricter than before; callers control strictness through the schema they pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the Python opencode provider behaviors (providers/opencode.py): - Add `--format json` and parse the JSONL event stream: recover the final assistant text (extract_final_text parity), sum per-step cost from step_finish events (nil when none report a cost, distinguishing "unknown" from "$0.00"), and count turns as one per step_start, falling back to tool_use events. - Surface in-band JSON "error" events and — critically — hard failures on which opencode exits 0 (Model not found, AuthenticationError, Unauthorized, APIError): when stderr matches one of those markers and no result was produced, mark IsError with the extracted error window instead of returning empty output that reads downstream as "no result". Non-JSON stdout still falls back to trimmed raw text, so older opencode versions keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a native ReasonerFailed error type (Message / Result / ErrorDetails) so a reasoner that ran but failed can report status=failed WITHOUT discarding its structured outcome — mirroring the Python SDK's ReasonerFailed exception. In executeReasonerAsync's error branch, detect it via errors.As and attach payload["result"] / payload["error_details"] (only when non-nil) so the single 5x-retried status post carries the result atomically; the control plane stores the result regardless of terminal status. The two sync HTTP paths (handleExecute / handleReasoner) mirror this by carrying the result/details onto their error response. A plain error synthesizes neither key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ages
The async submit+poll Agent.Call path (introduced earlier on this branch)
failed a call the instant a single HTTP request to the control plane failed.
Real-world testing exposed the impact: two ~hour-long SWE-AF builds died at
~55 minutes when the CP briefly went slow under system load — one timed-out
status poll ("context deadline exceeded while awaiting headers") aborted calls
that had been running for 30+ minutes, and a slow submit POST likewise killed
a call before the request could be confirmed accepted.
Poll resilience: a transient poll failure (transport error/timeout, 408/429,
or 5xx) no longer fails the call. Consecutive failures are retried with
exponential backoff (capped at 15s) for a configurable window of UNBROKEN
failure — default 5 minutes — after which the call fails with a clear
"control plane unreachable for Xs" error instead of the raw first error. A
single successful poll resets the window. A 404 on a just-submitted execution
(the CP can lag before the row is queryable) is retried within a shorter
bounded window, then fails with a distinct not-found message. Permanent 4xx
(auth, bad request) still abort immediately.
Submit safety: re-POSTing execute/async is NOT idempotent (a duplicate would
double-run a coder), so the submit is retried ONLY on errors that prove the
request never reached the server — dial/DNS/connection-refused, detected by
inspecting the error chain (syscall.ECONNREFUSED, net.DNSError, dial-phase
net.OpError). Ambiguous failures (a timeout awaiting headers — the request may
already have been accepted) are never blind-retried; instead the submit client
timeout is raised substantially (default 120s) so a slow-but-healthy CP does
not abort an accepted request, and the call fails with a clear message if it
still times out.
Config: three env knobs following the AGENTFIELD_* integer-seconds convention,
read once and cached on the agent — AGENTFIELD_CALL_POLL_TIMEOUT_SECONDS (60),
AGENTFIELD_CALL_RETRY_WINDOW_SECONDS (300), AGENTFIELD_CALL_SUBMIT_TIMEOUT_SECONDS
(120) — with dedicated submit/poll HTTP clients so these timeouts don't affect
other client traffic. Each retried failure is logged at warn via the existing
structured-log seam (call.outbound.submit_retry / call.outbound.poll_retry)
with attempt count and elapsed window so operators see degradation without the
call dying.
The Python SDK's async Call path (_submit_execution_sync / _await_execution_sync)
uses bare requests with raise_for_status() and no transient retry, so it shares
this fragility; this change improves on it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the async-Call test suite for the submit/poll resilience change: - Poll retries: 2 healthy polls, then 3 transient 5xx, then recovery — the call succeeds and each transient failure emits a warn call.outbound.poll_retry log with an attempt count. - Poll unreachable: a CP down past the (env-shortened) window fails with the "control plane unreachable" error, having retried rather than dying on the first blip. - 404 after submit: retried within the bounded window, then a distinct 404 not-found error. - Submit connection-refused: refused twice then accepted creates EXACTLY ONE execution (asserted server-side); refused beyond the window fails unreachable. - Submit ambiguous timeout: a hung-but-listening CP is NOT retried (exactly one request reaches the server) and fails with a clear message. - Env overrides read once and cached; invalid values fall back to defaults. - Direct unit tests for requestNeverReachedServer (dial/DNS/econnrefused vs ambiguous/opaque), isTransientPollStatus, nextRetryBackoff, and sleepCtx. Existing branch tests updated for the new semantics: submit/poll now use the dedicated call clients; a persistent poll transport error is retried to the unreachable-window error; a permanent poll status uses 403 (5xx is now retried). Also checks previously-unchecked json.Encode returns and switches to tagged switches so golangci-lint is clean on the branch delta. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Call() and Config.CallTimeout doc comments still claimed CallTimeout bounds the async submit and each status poll. That is no longer true: the Call path now uses dedicated submit/poll HTTP clients bounded by AGENTFIELD_CALL_SUBMIT_TIMEOUT_SECONDS / AGENTFIELD_CALL_POLL_TIMEOUT_SECONDS, with transient failures retried within AGENTFIELD_CALL_RETRY_WINDOW_SECONDS. Update both comments so the documented timeout semantics match the code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR consolidates two related changes into a single reviewable unit. It
supersedes and replaces #746 — the three Go SDK commits from that PR
(
7c6f11e1,18c3f3ea,5b763d87) are cherry-picked here (-x), and #746has been closed in favor of this one. The two changes are independent (one
touches
control-plane/, the other onlysdk/go/) but ship together so asingle node repo can both be installed via
af install --pathand drive a GoSDK harness that talks to the control plane correctly.
Part 1 —
af install --pathselector for one-repo multi-package installsProblem
The SWE-AF repository ships two installable nodes from one repo: a Python node
(manifest at the repo root) and a Go port under
go/(which will get its owngo/agentfield-package.yaml). Todayaf installhas no way to select asubdirectory —
GitInstaller.findPackageRootwalks the clone and takes thefirst
agentfield-package.yaml(root wins). Registry entries are keyed bymanifest
name, so two packages from one repo already coexist cleanly — onlythe selector was missing.
The root manifest must stay exactly what a bare
af install <repo>gets. This PRadds an opt-in
--path <subdir>selector without changing any default behavior.Design
af install <source> --path <subdir>:<subdir>is relative to the source root (e.g.go). The manifest MUST existat
<root>/<subdir>/agentfield-package.yaml, and that subdirectory becomes thepackage root — its subtree is what gets copied to
~/.agentfield/packages/<name>. A missing manifest fails with an error namingthe full expected path.
..(after
filepath.Clean, plus a post-join containment check) are rejected. Forthe git path this syntactic check runs before any clone work.
@refpin on git URLs (ref is parsed from the URL,subdir is resolved after clone — orthogonal).
with identical semantics (
<local-dir>/<subdir>), resolved before any copy orregistry mutation.
recursively-installed node dependencies.
Key pieces:
domain.InstallOptions.Path; sharedpackages.ResolvePackageSubdir/ValidateSubdirSelector;GitInstaller.Subdir+resolvePackageRoot;--pathflag + help text in theCLI; local path threaded through
installLocalPackage.Validation contract
--path goon a repo with manifests at both root andgo/installs thego/package (registry entry named per thego/manifest; package dir holdsthe
go/subtree at its root).af install <repo>on the same repo installs the ROOT package —unchanged behavior.
--path <dir>with no manifest there errors with the expected manifestpath; nothing installed, no registry mutation.
--path /absand--path ../escapeare rejected before any install work.--path gocombined with an@refon the URL — both honored.entrypoint.buildat the subdirroot) resolves its build relative to the subdir-as-package-root.
Test plan / gates run
New behavior-driven tests (derived from the contract, not the implementation):
control-plane/internal/packages/install_path_test.go— resolver + gitpost-clone resolution (faked clone, no network) +
@refcomposition + Gosubdir build via the existing stubbed toolchain.
control-plane/internal/core/services/install_path_test.go— local-directory--pathselection, root default, and rejection paths leaving the registryunmutated.
Part 2 — Go SDK harness fixes (consolidates #746)
Problem
The Go SDK's
claudecodeharness andAgent.Callhad three defects that brokelong-running child reasoners and the claude runtime path:
positional argument, but the variadic
--allowedToolsflag consumed it —claude never received the prompt.
/api/v1prefix.
Agent.Call.Agent.Callwas a synchronousPOST /execute/{target}bounded by the 15s defaulthttp.Clienttimeout, soany child reasoner that ran longer than 15s was killed.
incremental events, so the idle watchdog could fire mid-run.
Fixes
than as a positional arg, so the variadic
--allowedToolsflag can no longerswallow it.
/api/v1.Agent.Callrewritten to async submit + poll. It now mirrors the PythonSDK: submit via
/execute/async/{target}, then poll the execution record withPython-parity pacing (0.25s doubling to 4s, jittered), the call timeout
bounding each individual request rather than the overall wait, fully
ctx-cancellable, with lineage headers on submit and polls so DAG parentage is
preserved. This removes the sync endpoint's 15s cap.
--output-format stream-json --verbose, soincremental events keep the idle watchdog fed for the full duration of a run.
Tests
sdk/go/agent/agent_call_coverage_test.go— covers the asyncCallsubmit/poll paths: poll-interval clamping, submit-side request/transport/
read/decode/4xx errors, and await-side ctx-cancel, transport, read, HTTP-error,
and decode branches.
sdk/go/harness/claudecode_defaultrunner_test.go— covers theclaudecodedefault-runner fallback (therunCLI == nilpath inClaudeCodeProvider.Execute).claudecode_test.go,claudecode_integration_test.go, andagent_test.go/note_test.go.These lift sdk-go patch coverage above the 80%
min_patchgate (98% on thetouched lines).
Gates run (both parts)
Run locally against the CI-literal steps (Go 1.25.4,
GOWORK=off):go build ./...,go vet ./...,go test ./...— pass(one pre-existing, environment-dependent failure,
TestDevServiceRunDev,which spawns a real agent subprocess and polls its port; unrelated to and
untouched by this PR — it passes in CI).
go build ./...,go vet ./...,go test ./...— pass.gofmt -lclean on all changed files.golangci-lint run --new-from-rev=origin/mainon control-plane → 0 new issues.diff-covervsorigin/main) → 98%, above the80%
min_patchfloor.Part 3 — provider + failure-path parity fixes
Four verified Go SDK fixes bringing the
harnessproviders and the asyncfailure path to parity with the Python reference implementations. All changes
are under
sdk/go/(plus one newsdk/go/go.moddependency).Fix 1 — codex native structured output (
harness/codex.go)The codex provider ignored
options.Modeland built a deprecatedcodex exec --json [--full-auto]invocation. Ported from the codex harnesspatch:
-m <model>(SWE-AF resolves gpt-5.5 vs gpt-5.3-codex by auth mode andthe value must reach the CLI).
--skip-git-repo-check.--full-autowith the permission->sandbox mapping:auto->
--dangerously-bypass-approvals-and-sandbox;read-only/workspace-write/danger-full-access->--sandbox <mode>; else--sandbox workspace-write.--output-schema(pointing at the STRICT rewrite of the schema — defaults stripped, all
properties required,
additionalProperties:false, recursively) and--output-last-message, reading the last-message file when stdout yields noparseable final text. The runner owns the strict-schema computation + file
write and hands codex the paths via a
schemaAwareinterface, then swaps in acodex-native prompt suffix while claude/opencode keep the Write-tool suffix.
Optionsgains no schema field.Fix 2 — opencode JSON stream + exit-0 error surfacing (
harness/opencode.go)--format jsonand parse the JSONL event stream: recover the finalmessage, sum per-step cost from
step_finishevents (nil when none report acost), and count turns per
step_start(falling back totool_use).Model not found,AuthenticationError,Unauthorized,APIError) plus in-band JSONerrorevents, instead of returning empty output that reads downstream as "no
result". Non-JSON stdout still falls back to trimmed raw text.
Fix 3 — native
ReasonerFailedcarrier (agent/)New exported
ReasonerFailed{Message, Result, ErrorDetails}error type. InexecuteReasonerAsync's error branch it is detected viaerrors.Asand itsresult/error_detailsare attached to the single 5x-retried status post(only when non-nil), so a reasoner that ran but failed records
status=failedwithout discarding its structured outcome. The two sync HTTP paths
(
handleExecute/handleReasoner) mirror this on their error response; aplain error synthesizes neither key.
Fix 4 — real schema validation in the harness retry loop (
harness/)ParseAndValidatewas unmarshal-only, so output missing required fields,carrying invalid enum values, or (with
additionalProperties:false) extrafields passed silently and the schema-retry loop never fired. Add
validateAgainstSchema(github.com/santhosh-tekuri/jsonschema/v5) and run itat every parse-success point in
handleSchemaWithRetry, so a validation failuredrives the existing retry branch (
schemaMaxRetriesdefault 2 preserved).Validation applies only when both a destination and a schema are provided;
uncompilable schemas skip validation (no regression).
Note: this makes map-schema harness calls stricter than before (previously
unmarshal-only) — callers control strictness through the schema they pass.
Gates run (Part 3)
sdk/go:go build ./...,go vet ./...,go test ./...— pass;gofmt -lclean on all changed files.
golangci-lint run --new-from-rev=origin/mainonsdk/go-> 0 new issues(the 7 pre-existing findings on earlier commits are untouched).
diff-covervsorigin/main) -> 94% on 621touched lines, above the 80%
min_patchfloor.Part 4 — Agent.Call resilience to transient control-plane outages (
agent/)Real-LLM testing exposed a robustness bug in the async submit+poll
Agent.Callpath this branch introduced: a single failed HTTP request to the control plane
killed the whole call. Two ~hour-long SWE-AF builds died at ~55 minutes when the
CP briefly went slow under system load (load avg ~37 on a 16-core box; the CP
itself was healthy and recovered) — one status poll that timed out "awaiting
headers" aborted a call that had been running 30+ minutes, and a slow submit
POST likewise killed a call before it could be confirmed accepted.
Poll resilience. A transient poll failure (transport error/timeout, 408/429,
or 5xx) no longer fails the call. Consecutive failures retry with exponential
backoff (capped 15s) for a configurable window of unbroken failure — default
5 min — after which the call fails with a clear
control plane unreachable for Xserror rather than the raw first error. A single successful poll resets the
window. A 404 on a just-submitted execution (the CP can lag before the row is
queryable) retries within a shorter bounded window, then fails with a distinct
not-found message. Permanent 4xx (auth, bad request) still abort immediately.
Submit safety. Re-POSTing
execute/asyncis not idempotent (a duplicatewould double-run a coder), so the submit retries only on errors that prove the
request never reached the server — dial / DNS / connection-refused, detected by
inspecting the error chain (
syscall.ECONNREFUSED,net.DNSError, dial-phasenet.OpError). Ambiguous failures (a timeout awaiting headers — the request mayalready have been accepted) are never blind-retried; instead the submit client
timeout is raised substantially (default 120s) so a slow-but-healthy CP doesn't
abort an accepted request, failing with a clear message only if it still times
out.
Config. Three env knobs (
AGENTFIELD_*integer-seconds convention, readonce and cached):
AGENTFIELD_CALL_POLL_TIMEOUT_SECONDS(60),AGENTFIELD_CALL_RETRY_WINDOW_SECONDS(300),AGENTFIELD_CALL_SUBMIT_TIMEOUT_SECONDS(120), with dedicated submit/poll HTTPclients so these timeouts don't affect other client traffic. Each retried
failure logs at warn via the existing structured-log seam
(
call.outbound.submit_retry/call.outbound.poll_retry) with attempt countand elapsed window, so operators see degradation without the call dying.
Python parity. The Python SDK's async Call path (
_submit_execution_sync/_await_execution_syncinclient.py) uses barerequestswithraise_for_status()and no transient retry — its shared-sessionurllib3Retry(total=3)adapter is not applied to those calls — so it shares thisfragility (its only comparable retry is
wait_for_approval's catch-allback-off loop). This Go change improves on Python: a bounded failure window,
404-specific handling, and idempotency-safe submit retries.
Gates run (Part 4)
sdk/go:go build ./...,go vet ./...,go test ./...— pass;gofmt -lclean on all changed files.
golangci-lint run --new-from-rev=origin/mainonsdk/go-> 0 issues(also cleaned the pre-existing branch-delta findings this gate surfaced).
diff-covervsorigin/main) -> 89% on 779touched lines, above the 80%
min_patchfloor;agentpackage 92.3%.🤖 Generated with Claude Code