Fix #2755: stop committing per-slice BRC files to work - #2758
Conversation
Per-slice implement-phase BRC history files
(`{identifier}-implement-{slice_id}.{md,json}`) are owned by each
slice's integration branch, where the slice hook commits them so the
slice PR's diff carries its consensus transcript. The orchestrator
was also writing those same files onto the work branch at
implement-phase completion and at PR-phase entry, which caused
add/add merge conflicts every time a slice PR (slice → work) tried
to merge — both sides added the identical path after they diverged.
This change keeps per-slice files solely on the slice branches:
* `_write_brc_history` now takes a `write_per_slice` keyword (default
`True` preserves the historical call shape). When `False` and the
phase is `implement` in a slice-aware pipeline, the per-slice
bucketing loop is skipped; the `unattributed` sibling and any
non-slice aggregate file are still produced.
* The three work-worktree commit sites — the end-of-implement-phase
inline write in `_run_pipeline`, the external-transition
`_persist_phase_brc_history`, and the PR-phase safety-net
`_rewrite_brc_history_for_pr` — now pass `write_per_slice=False`.
* `_commit_slice_brc_history_to_integration_branch` now stages
`_write_brc_history` output into a per-tick `mkdtemp`-rooted
staging directory (still under `WORKTREE_BASE_DIR` for gateway
allowlist compatibility, #2684) instead of the work worktree.
Concurrent slice hooks no longer share filesystem state, and the
work worktree is never touched by this hook.
The operator workaround documented in the issue (`git rm` the
per-slice files from work, recommit, push) is no longer necessary.
There was a problem hiding this comment.
Review
Thorough trace through the bug and the fix: the root-cause analysis in the PR description matches the code. Pre-fix, the slice hook called _write_brc_history(worktree_repo_path, ...) on the work worktree at line 11340-ish, which produced per-slice files for every slice (the writer iterates buckets without filtering). The hook then copied only this slice's files to the integration worktree — but the freshly-written per-slice files for ALL slices remained on work, where the end-of-phase commit (_persist_phase_brc_history), the per-iteration commit at the bottom of _run_pipeline (line 22085), and the PR-phase rewrite (_rewrite_brc_history_for_pr) then picked them up and committed them to work. Slice PRs (slice integration → work) re-added the same paths on the slice side — add/add conflict.
The fix correctly addresses this on both sides of the asymmetry:
- Producer side (slice hook,
_commit_slice_brc_history_to_integration_branch): renders into a per-tickmkdtemp-rootedstaging/subdir instead of the work worktree. The staging dir is freshly minted per hook tick (so concurrent slice consensuses don't share filesystem state) and cleaned up infinally. - Consumer side (work-worktree writers): all three callers —
_rewrite_brc_history_for_pr:9242,_persist_phase_brc_history:9327, and the inline_run_pipelinewrite at 22085 — passwrite_per_slice=False. The new kwarg short-circuits the per-slice bucket loop in_write_brc_historyafter the unattributed sibling and the warning for D4-violating unattributed CONSENSUS messages, so neither path is broken.
End-to-end trace of slice PR merges confirms no add/add conflict: each slice integration branch carries only its own {id}-implement-slice-N.{md,json}, work carries only the unattributed sibling (which slice branches don't touch), and slice-PR-into-work merges become pure adds.
Verified
- All 4
_write_brc_historycallers handled (work-worktree sites passwrite_per_slice=False; slice hook uses staging with defaultTrue). - The
write_per_slice=Falseearly return atpipelines.py:9166is positioned correctly — after both thenot bucketsaggregate write (line 9119, so babysit_pr aggregates still land on work) and the unattributed sibling write (line 9157, so the unattributed audit-trail file is still written). It returns before the per-slice loop, which is the only thing it gates. - Symlink defense moved with the scan path:
staging/.egg-state/brc-history/is checked instead of the work worktree. Defense-in-depth (writer-controlled now) but the cost is trivial. - Order shift inside the hook is safe: rendering and the no-files short-circuit happen before the integration-branch fetch (efficiency win; pre-fix did unnecessary fetch when the slice produced no files). All paths through the new try/finally still cleanup the staging dir via
shutil.rmtree(tmp_worktree, ignore_errors=True). TestWritePerSliceFlagcovers the four observable cases of the kwarg against the real writer (default writes per-slice,Falseskips,Falsestill writes unattributed when relevant,Falsestill writes the babysit_pr aggregate, no-op outside implement). Regression tests inTestWorkWorktreeIsolationpin the work-worktree-untouched invariant and the staging-dir cleanup.- CI is green (Unit Tests + Integration Tests both SUCCESS).
Non-blocking observations
-
Stale comment at
pipelines.py:22639-22642(pre-existing, not introduced by this PR, but the PR touches the adjacent_write_brc_historycall at line 22085 and is the natural place to fix it):# phase had started. ``_write_brc_history`` already ran # earlier in this iteration (line ~16753) so the BRCThere is no
_write_brc_historycall at or near line 16753 — the only_write_brc_historysite in_run_pipelineis the one this PR is editing at 22085 (which is later in the same iteration, not earlier). The comment was already wrong before this PR and is contradicted now that the PR's diff is right above it. Suggest tightening to "_write_brc_historyruns at the bottom of each phase iteration withwrite_per_slice=False(see #2755), so per-slice transcripts are on the slice integration branches, and the work commit picks up only the unattributed sibling before we wipe the message store here." -
_writer_stub's**kwargspermissiveness (test_per_slice_brc_commit.py:219): the stub silently ignores any kwarg passed to it. If a future regression caused the slice hook to passwrite_per_slice=False(which would skip the per-slice writes the hook depends on),test_calls_write_brc_history_to_refresh_fileswould not catch it directly via the spy's positional capture — it would only fail indirectly via the downstream "no per-slice BRC files produced" return path. The**kwargspermissiveness is justified for forward-compat, but consider adding one explicit assertion in the happy-path test thatwrite_per_sliceis either absent orTruein the hook's writer call kwargs, sincewrite_per_slice=Falsefrom the slice hook would silently break the feature. -
Per-tick
mkdtempallocation cost in fan-out: for an N-slice pipeline, each hook tick now calls the writer against its own staging dir, so the per-slice files get rendered N times (once per slice hook) instead of being written to a shared work worktree. Pre-fix did the same N renders but to the same path (with byte-identical content). Net change: same total work, no shared-path contention. Mentioned only because the docstring's "concurrent slice hooks do not cross-write each other" framing could lead a future reader to believe rendering became serialized — it didn't, it just stopped colliding. Not worth a code change.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…warg - pipelines.py:22639-22642: refresh the pre-existing stale comment referring to a nonexistent _write_brc_history call site (line ~16753). Point at the real, correct site at the bottom of each phase iteration and note the new write_per_slice=False behavior from #2755. - test_per_slice_brc_commit.py: pin the slice hook's writer-call kwargs in the happy-path spy. _writer_stub accepts **kwargs for forward-compat, which would silently swallow a future regression where the slice hook passes write_per_slice=False (the hook depends on per-slice rendering — that would break the feature). The new assertion fails fast on any such regression.
|
Thanks for the thorough trace and the suggestions — addressed in 2c54530:
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
Both non-blocking suggestions from the previous review at 21c2a81 have been addressed correctly in 2c54530.
Verified
1. Stale comment at pipelines.py:22639-22645 — Fixed. The new comment correctly:
- Drops the wrong
line ~16753reference. - Points at the real call site (the
_write_brc_historyinvocation atpipelines.py:22085, which runs at the bottom of each phase iteration before the auto-advance phase transition). - Calls out the new
write_per_slice=Falsebehavior with the #2755 cross-ref so a future reader can find the rationale.
The "per-slice implement-phase transcripts are on the slice integration branches, and the work commit picks up only the unattributed sibling (plus any non-implement aggregate)" framing accurately describes the writer's behavior under write_per_slice=False (see pipelines.py:9166-9179 — the early-return is positioned after the unattributed-sibling write and the non-slice aggregate write, so both still land on work).
2. Explicit write_per_slice kwarg pin in test_per_slice_brc_commit.py:344-347 — Fixed. The spy now captures dict(kwargs) in the call tuple (line 312, defensive copy so a later mutation of the writer's kwargs by the production code can't retroactively rewrite the captured value) and the unpacked kwargs is asserted to default to True:
assert kwargs.get("write_per_slice", True) is True, (
f"slice hook must not pass write_per_slice=False to the writer (#2755); "
f"got kwargs={kwargs!r}"
)This fails fast with a clear error message if a future change starts passing write_per_slice=False from the slice hook (which would break the feature by skipping the per-slice bucket loop the hook depends on, since the downstream scan at line 11457+ would find no files and the hook would return False). Verified the current production call at pipelines.py:11440 does not pass the kwarg, so the assertion is green today and only fires on regression.
CI
Unit Tests, Integration Tests, lint, security, Docker, custom checks — all SUCCESS on 2c54530.
Non-blocking observation
The new comment at pipelines.py:22643-22645 says "the work commit picks up only the unattributed sibling (plus any non-implement aggregate)" — for completeness, a non-slice implement-phase pipeline (the not buckets branch at line 9104) also writes its aggregate {identifier}-implement.{md,json} onto work before the write_per_slice=False early-return is reached. The comment's enumeration is narrower than the actual writer behavior. This is a precision nit, not a correctness issue — the operator-relevant invariant ("per-slice transcripts don't conflict on slice → work merges") is captured correctly. Tighten only if you want full coverage; happy either way.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…implement aggregate
The previous comment said the work commit picks up 'the unattributed
sibling (plus any non-implement aggregate)' before the message store
is wiped. That enumeration was too narrow: when a non-slice
implement-phase pipeline (babysit_pr or any other implement run that
never spawned slice scopes) reaches the writer, the not buckets
branch at routes/pipelines.py:9104 lands a {identifier}-implement.{md,json}
aggregate on work before the write_per_slice=False early-return is
reached, so the work commit picks that up too.
Reviewer flagged this in the re-review at 2c54530 as a precision nit
('Tighten only if you want full coverage; happy either way.'). No
behavior change; comment-only edit.
|
Thanks for the re-review — addressed the non-blocking precision nit in c096ee6:
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
The non-blocking precision nit from the previous re-review at 2c54530 has been addressed in c096ee6.
Verified
The new comment at pipelines.py:22639-22649 now accurately enumerates what the writer emits to the work worktree under write_per_slice=False:
- refine/plan/pr aggregates — written by the unconditional
_write_brc_history_fileatpipelines.py:9201-9207, which is the only path for non-implement phases. - non-slice-implement aggregate — written by the
if not buckets:branch atpipelines.py:9104-9126({identifier}-implement.{md,json}), which fires for babysit_pr and any other implement-phase run that never spawned slice scopes. This returns from_write_brc_historybefore reaching thewrite_per_slice=Falseearly-return at line 9166, so the caller'swrite_per_slice=Falseis a no-op on this path — correctly captured in the new comment. - unattributed sibling — written at
pipelines.py:9156-9164before thewrite_per_slice=Falseearly return. - per-slice files — skipped under
write_per_slice=Falsevia the early return at line 9166-9179, before the per-slice bucket loop at 9186.
The comment is now a faithful description of the writer's behavior across all four paths. Comment-only edit; no behavior change, no test surface.
CI
Unit Tests, Integration Tests, lint, security, Docker, custom checks — all SUCCESS on c096ee6.
Approval
No outstanding issues. Both rounds of non-blocking feedback (stale comment, kwarg pin, comment precision) have been addressed; the producer-side staging refactor and the consumer-side write_per_slice=False plumbing remain correct as verified in the prior reviews.
— Authored by egg
|
egg review completed. View run logs 6 previous review(s) hidden. |
…tion (#2781) * Fix #2778: detach the per-slice BRC worktree The per-slice BRC commit hook ran `git worktree add -B <integration_branch>`, which git rejects when the branch is already checked out by another linked worktree. The slice's own agent worktrees hold the integration branch for the slice run, so the hook lost that race (`fatal: '<branch>' is already used by worktree`) and silently dropped the slice PR's consensus transcript — confirmed from issue-2769's orchestrator logs. Switch to `git worktree add --detach`: the hook only builds one commit on `origin/<integration_branch>` and pushes HEAD (`push_worktree_branch` with `ref=None`), so it never needed the local branch ref. A detached worktree claims no ref and coexists with whatever holds the branch. The hook is best-effort, so this failed silently. It became outright data loss only after #2758 stopped also copying per-slice files onto `work`; before that the redundant copy masked the hook failure. * Clarify push_worktree_branch ref=None docstring for detached worktrees Address review feedback on #2781: the ref=None mode docstring claimed the worktree must be 'checked out to branch', but the per-slice BRC hook now adds its worktree with --detach. The actual contract is just 'push the worktree's HEAD' via HEAD:refs/heads/<branch> — attachment to the branch is not required. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
…apse, BLE001 audit, #2570 audit task-3-4 (cq-3): annotate the five SliceScheduler #2199 hooks with docstring banners pointing at the future per-slice MCP control work, plus a `# noqa: ARG002` + TODO(#2199) on the `hitl_escalator` constructor param (the only ARG-lint surface in the file per the architect's AC-12 NB#2 noqa precision). `poll_cascades` is LIVE and untouched. task-3-7: prune stale archaeology comments inside the slice run loop and adjacent helpers: - `_persist_slice_status_complete` docstring: collapse the closed-#2549 / closed-#2470 post-mortem into one explanatory sentence. - Bootstrap reconciliation block (Layer A/B): replace the 26-line closed-#2549 narrative with a 10-line summary of current behaviour. - `_run_one_slice_inner` race-protection block: replace the 11-line closed-#2549 narrative with 3 lines describing what the code does now. - Defensive slice-loop-entry context-PR opener: tighten the slice-1 cq-4/TASK-1-2 backstory (23 lines → 7 lines). - Per-slice BRC commit comment: collapse the 15-line closed-#2548 + closed-#2758/#2755 narrative into 6 lines describing current behaviour. - Drop the closed-#2549 reviewer-note paragraph above the stacked-PR reconciler start; keep one line explaining the ordering rationale. task-3-6: collapse 5 dual-path `try: from X import Y; except ImportError: from orchestrator.X import Y` shims inside `_run_implement_phase_slices` to single canonical `from orchestrator.X import Y` imports — slice_scheduler, global_slice_admit, peer_consensus, state_store, message_store, impasse_routing. Pre-slice-2 anchors 15045/15050/15147/15154/15161/ 15875/16026/16034/16209 mapped to post-slice-2 sites (slice-2 shifted the body ~800 lines). task-3-5 (feedback Q2): BLE001 audit on the 19 swallow-all handlers inside the slice loop. Per-site decisions: NARROW (4 sites): - :15533 → `except ImportError:` (symmetry-only `get_gateway_client` import that only fails if the module isn't importable). - :16054 → `except RuntimeError:` (`Thread.join` only raises RuntimeError; timeouts are silent). DOCUMENT (15 sites): each retains `except Exception ...: # noqa: BLE001` with an inline comment naming what's caught and why bare-Exception is intentional. Categories: - Contract load/save under per-pipeline state lock (3 sites): catches loader validation + atomic-rename I/O + pydantic re-serialisation errors; best-effort because in-memory state still reflects the change. - Gateway HTTP calls — `is_slice_branch_merged_into_parent` (2 sites), `create_slice_integration_branch` (1), `create_slice_pr` (1), `get_remote_branch_sha` (1): catches GatewayError + OSError; default to "not merged" / branch_ok=False / pr_created=False. - In-memory tracker pop (2 sites): only programming errors could fire; bare-except keeps the slice COMPLETE/return path crash-proof. - Per-slice BRC commit helper (1): unbounded exception surface (gateway push, git plumbing, message-store, file I/O); best-effort because the transcript commit is non-essential. - Slice PR pre-load (1): contract load + nested attribute traversal; AttributeError/KeyError guard against partially-populated rollups. - Slice worker `fut.result()` re-raise (1): unbounded surface; mark slice failed and continue the wave. - OVERSEER_ALERT message-store emission (1): already has body comment naming it as best-effort behind the always-on log. - Stale-impasse load + save (2 sites): file I/O + JSON decode + pydantic validation. - Slice-loop-entry context-PR safety net outer wrapper (1): already has body comment. - `_contract_loader` closure (1): best-effort "current contract or None". task-3-3 (#2570 silent rebase): audit complete; root cause confirmed to live inside `_sync_worktree_with_remote` at the bare-rebase fallback under the `local_ahead > 0 and remote_ahead > 0 + base_branch_for_reconcile=None` branch (the function's own comment identifies the vector and tags it `#2222 contamination risk`). `_sync_worktree_with_remote` is OUT OF SCOPE per refine decision-11 / cq-7 — AC-9a hard requirement fires. HITL registered via `mcp__sdlc__register_open_question` (decision id: cq-11) with the three options listed in the plan; default recommendation is option 3 (xfail in slice-3, follow-up issue co-scheduled with #2792). No code change to the OOS primitive from this slice. Audit artifact: `.egg-state/agent-outputs/issue-2777-replan-task-3-3-audit.md`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Closes #2755.
_write_brc_historyalways wrote per-slice{identifier}-implement-{slice_id}.{md,json}files into the worktreeit was called against. The slice hook (
_commit_slice_brc_history_to_integration_branch) used the work worktree as scratch space and committed those per-slice files onto each slice's integration branch (correct), but the end-of-implement-phase and PR-phase commits then also committed them ontowork— so both sides of every slice → work merge added the identical paths and GitHub reported add/add conflicts. The latent bug fires on every multi-slice pipeline that fans out fromwork._write_brc_historyoutput to a per-tickmkdtemp-rooted directory (still underWORKTREE_BASE_DIRfor gateway-allowlist compatibility, Slice BRC history never reaches the slice PR — temp-worktree push rejected by gateway path allowlist #2684) and copies its slice's files from staging to the integration worktree. The work-worktree commit sites — the inline write in_run_pipeline,_persist_phase_brc_history, and_rewrite_brc_history_for_pr— pass a newwrite_per_slice=Falsekwarg so the writer skips the per-slice bucketing loop. Theunattributedsibling and any non-slice-aware aggregate file are still produced.Why option (b), not the smaller patch
A minimal fix (skip per-slice writes plus unlink leftovers before committing to work) would have left the "use work worktree as scratch space" pattern in place and encoded the slice naming convention into two places — the writer skip and the unlink — neither of which is centrally documented. Moving the slice hook to its own staging directory makes
write_per_slice=Falsesufficient on its own: nothing else writes per-slice files to the work worktree, so there is nothing to unlink. The concurrency commentary at the top of the slice hook simplifies as a result.Behavior change
When the umbrella
work → mainPR opens, the work branch's.egg-state/brc-history/no longer contains the per-slice transcripts up-front. They arrive onworknaturally as slice PRs merge in (each slice PR's diff includes its own per-slice file). The umbrella PR's final state matches what's merged, which is the intended contract.Test plan
make test(full suite — 3055 passed)make lint(clean)orchestrator/tests/test_per_slice_brc_commit.py— slice-hook tests updated for staging behavior plus two new regression tests pinning work-worktree isolation and staging-dir cleanuporchestrator/tests/test_brc_history.py— newTestWritePerSliceFlagclass covers the four observable behaviors of the new kwarg (default writes per-slice;Falseskips per-slice;Falsestill writes unattributed when relevant;Falsestill writes the babysit_pr aggregate; flag is a no-op outside the implement phase)orchestrator/tests/test_pr_phase_brc_rewrite.pyandtest_diagnostic_logging_1633.py— updated to assert the new "per-slice files do NOT land on work" invariant