Skip to content

fix(bridge-cutover): unblock preparer planning against real git-backed projects - #7

Merged
ruby-dlee merged 4 commits into
mainfrom
claude/bridge-cutover-preparer-handoff-4d48bc
Jul 19, 2026
Merged

fix(bridge-cutover): unblock preparer planning against real git-backed projects#7
ruby-dlee merged 4 commits into
mainfrom
claude/bridge-cutover-preparer-handoff-4d48bc

Conversation

@ruby-dlee

Copy link
Copy Markdown
Owner

Intent

Fix the Bridge cutover preparer NO-GO from the 2026-07-19 live attempt: the sealed pure-planning guard in tools/bridge-cutover/prepare_bridge_cutover.py refuses process execution, and two real defects blocked prepare against any real git-backed trusted project. (1) agent-fleet's projects.py:_git_path shelled to 'git rev-parse' inside registered_trusted_projects, so closed_claude_state_payload tripped the guard. Deliberate fix: replace the subprocess with pure-Python git discovery mirroring git's own setup semantics - validate_headref readlink handling for symlinked HEADs, gitfile/commondir pointer resolution via strict realpath, is_git_directory HEAD/objects/refs checks, per-level .git-entry-then-bare ordering, one-filesystem ascent, git's dubious-ownership refusal on the discovered git dir, and a 1MB gitfile size cap. The guard itself is deliberately untouched. Parity was verified empirically against real git across 18 layouts including linked worktrees, relative/symlinked pointers, bare repos, detached and symlinked HEADs; deliberate fail-closed micro-divergences on hand-corrupted layouts are documented in the discovery docstring. (2) prepare's _lexical_candidate_registry built the candidate Registry without config_path, which the real provision_plan requires to compose managed hook commands - thread spec.live_registry through both prepare call sites, and in bridge_worker_state_transaction.py derive the live registry path from the cutover manifest's RegistryOperation so worker verification rebuilds hook commands against the exact path runtime hooks embed. The agent-fleet version deliberately stays 0.2.0: the Bridge exactness lattice (preparer, builder, worker-state driver, gate fixtures) pins candidate 0.2.0/contract-2 and 0.2.0 was never tagged, released, or adopted anywhere, so the fixed content becomes what that binding names; uv.lock churn is from an intermediate bump that was reverted. New regression gates close the synthetic-vs-real gap the incident exposed: an agent-fleet unit test runs the sealed planning APIs (closed_claude_state_payload, provision_plan, identity_bundle_path) under an exact mirror of the preparer's guard against a real git-backed trusted project; a bridge-suite gate (tests/test_prepare_bridge_cutover.py RealAgentFleetPurePlanningTest) loads the REAL agent_fleet package through prepare's own loader under the REAL guard; the synthetic Registry fixture gains config_path aligning with the real schema and the synthetic provision_plan now enforces the same pathless-registry refusal as the real API; and new project-bootstrap tests pin detached-HEAD registration, symlinked-HEAD readlink semantics in both accept and refuse directions, commondir trailing-space refusal, gitfile-pointer-through-symlink physical resolution, lexical-escape refusal, and bare/inside-gitdir/broken-gitfile refusals. Full verification already run locally: agent-fleet suite green, bridge control-plane suite 206/206, opt-in real-input module 2/2 with the fixed wheel, sealed v3 release rebuilt deterministically (builds=2) from commit d793ebb, and the real prepare and validate now produce a valid cutover bundle (GO) against the migrated live registry baseline.

What Changed

  • Replaced agent_fleet.projects._git_path's git rev-parse subprocess with pure-filesystem Git discovery (_discover_git_worktree), mirroring git's setup semantics: validate_headref readlink/symref handling, gitfile and commondir pointer resolution through strict realpath, is_git_directory HEAD/objects/refs checks, per-level .git-entry-then-bare ordering, one-filesystem ascent, dubious-ownership refusal on the discovered git dir, and a 1MB gitfile size cap — so closed_claude_state_payload no longer trips the preparer's pure-planning guard, which is left untouched.
  • Threaded the live registry path into the candidate Registry: _lexical_candidate_registry now takes a config_path argument supplied by both prepare call sites from spec.live_registry, and bridge_worker_state_transaction._candidate_api derives it from the cutover manifest's single RegistryOperation (erroring if there isn't exactly one) so worker verification rebuilds managed hook commands against the path runtime hooks embed rather than the bundled registry copy's path.
  • Added regression gates for the synthetic-vs-real gap: RealAgentFleetPurePlanningTest loads the real agent_fleet package through prepare's own loader and runs the sealed planning APIs under the real guard against a git-backed trusted project; the synthetic Registry fixture gains config_path and its provision_plan now raises the same pathless-registry refusal as the real API; and new test_project_bootstrap cases pin detached/symlinked HEAD handling in both directions, commondir trailing-space refusal, gitfile-pointer-through-symlink resolution, and lexical-escape / bare / inside-gitdir / broken-gitfile refusals. docs/bridge-cutover-sealed-runtimes.md records both invariants.

Risk Assessment

✅ Low: The change is well-bounded and thoroughly regression-tested with empirical git-parity verification; the pure-Python discovery is consistently fail-closed and the config_path/live-registry threading is correct across all call sites, leaving only two info-level parity/documentation nits.

Testing

Completed 1 recorded test check.

  • Outcome: ⚠️ 1 error across 1 run (58m16s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ tools/agent-fleet/src/agent_fleet/projects.py:110 - _git_common_dir strips trailing \r via raw.rstrip(b"\r\n"), but git's commondir reader (strbuf_getline_lf) strips only the trailing \n. This contradicts the line's own comment ("Git trims only trailing newline bytes here; other whitespace stays significant") and is an undocumented fail-OPEN micro-divergence: a hand-corrupted commondir with CRLF (e.g. "../..\r\n") is refused by git (the ..\r component fails objects/refs access) but accepted here after the \r is stripped. Practical impact is near-nil (requires write access to an already current-user-owned git dir, which git never writes with CRLF), but it diverges in the unsafe direction, unlike the deliberate fail-closed divergences the discovery docstring enumerates. Note the same rstrip(b"\r\n") in _read_gitfile (line 155) IS correct there, since git's read_gitfile_gently strips both \n and \r. Change to rstrip(b"\n") to match git and the comment.
  • ℹ️ tools/agent-fleet/src/agent_fleet/projects.py:196 - The comment "Git treats a malformed or dangling .git file as fatal, never skippable" is inaccurate for one case: a regular .git file that does NOT start with gitdir: (READ_GITFILE_ERR_NOT_A_FILE) is skippable in git — git falls through to the .git-directory / bare checks and then ASCENDS to a parent worktree, rather than dying. The code here correctly fail-closes (raises refused()), so behavior is safe (strictly more refusing); only the comment misdescribes git and could mislead a future maintainer reasoning about parity. Git is only truly fatal (GIT_DIR_INVALID) when the file DOES start with gitdir: but is dangling/oversized/not-a-repo.
⚠️ **Test** - 1 error
  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; uv run --directory tools/agent-fleet --locked pytest || rc=1; uv run --directory tools/agent-fleet --locked python -m compileall -q src || rc=1; exit "$rc"
⚠️ **Document** - 1 info
  • ℹ️ tests/test_prepare_bridge_cutover.py:73 - Follow-up, out of scope for a docs pass: tests/test_prepare_bridge_cutover.py builds its synthetic agent_fleet module by hand-copying the real agent_fleet.models schema (Registry, Settings, ProviderConfig, Profile) as source text. That hand-copy is exactly what let config_path drift out of the fixture and produce the 2026-07-19 preparer NO-GO. This change closes the planning-purity half of the gap (RealAgentFleetPurePlanningTest drives the real package under the real guard) and re-aligns config_path, but the remaining fixture schema is still hand-maintained with no drift check against the authoritative dataclasses. Worth a dedicated gate that asserts the synthetic model fields equal the real ones, so the next field added to Registry cannot silently diverge again.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The Bridge preparer invokes the sealed planning APIs under a pure-planning
guard that refuses process execution; projects.py's git rev-parse subprocess
tripped it for any real git-backed trusted project, hard-blocking the live
cutover at prepare (2026-07-19).

Replace _git_path with pure filesystem Git discovery mirroring git's own
setup semantics: validate_headref readlink handling, gitfile/commondir
realpath resolution, is_git_directory checks, one-filesystem ascent, and the
dubious-ownership refusal, verified against real git across every reviewed
layout. The version stays 0.2.0: the Bridge exactness lattice pins the
candidate at 0.2.0/contract-2, and 0.2.0 was never tagged or adopted, so the
fixed content becomes the release that binding names.

Add the offline gates the incident exposed as missing: an agent-fleet test
running the sealed planning APIs under a mirror of the preparer guard against
a real git-backed trusted project, and a bridge-suite gate loading the real
package through prepare's own loader under the real guard.
prepare's _lexical_candidate_registry constructed the candidate Registry
without config_path, so the real sealed provision API refused provision_plan
('loaded registry path is unavailable for managed hooks') as soon as the git
canonicalization blocker was fixed; the synthetic provision fixtures never
modeled that requirement.

Thread the spec's live registry through prepare's two construction sites,
derive it in the worker-state driver from the cutover manifest's registry
operation so plan verification rebuilds hook commands against the exact path
runtime hooks embed, align the synthetic Registry model with the real schema,
and make the synthetic provision_plan enforce the same refusal so every
existing gate now guards the contract.
@ruby-dlee
ruby-dlee merged commit 4860464 into main Jul 19, 2026
5 checks passed
@ruby-dlee
ruby-dlee deleted the claude/bridge-cutover-preparer-handoff-4d48bc branch July 19, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant