feat(branch): assert HEAD is on impl branch around coder turn (AB#3210) - #401
Merged
Merged
Conversation
Closes AB#3210. ## Problem In apex run 3165, the impl agent for AB#3175 wrote its commit (`3f45269`) onto `impl/3165-3176` — a SIBLING task's impl branch — rather than `impl/3165-3175`. Result: - `impl/3165-3175` stayed empty. - `mg/3165_pg-3175` squash-merged a no-op. - AB#3175 was marked Done with zero code on `feature/3165`. - Recovery required a hand-shipped PR (#398) and a scope-review abort. The misroute was structurally invisible until the empty-MG check fired three reviews later. There was no fast-fail at the boundary where the impl agent acquired a worktree. ## Fix New verb `polyphony branch assert-on-impl --root-id N --item-id M`: - Reads `git symbolic-ref HEAD` (via existing `IGitClient`). - Compares against `BranchNameBuilder.Impl(root, item).Value` (`impl/{root}-{item}`). - Emits a routing-style envelope with discriminator `action`: - `ok` when current == expected - `mismatch` when current != expected (or detached HEAD, or git failure — both surface as mismatch with empty actual_branch) - `error` reserved for input validation failures (caller-side bug) `implement-merge-group.yaml` now invokes the assertion at TWO points: 1. Pre-coder — between guidance loader and coder dispatch. If the worktree is on the wrong branch BEFORE the coder runs, halt to a human gate so the operator can `git checkout` the right branch before any code is written. 2. Post-coder — between coder return and primary review. If the coder somehow ended up off-branch (or the worktree drifted mid-turn), halt before the misrouted commit can be promoted into the MG. Both gates offer Retry (re-run the assertion after operator intervention) or Abort. ## Test coverage - 10 unit tests in `BranchCommandsAssertOnImplTests` covering missing args, ok / mismatch / detached-HEAD / git-failure paths, and the snake_case JSON output contract. - The exact AB#3175 reproducer (`HeadOnSiblingImplBranch_EmitsMismatch`) asserts that `root_id=3165, item_id=3175, actual=impl/3165-3176` returns `action=mismatch`. - `tests/lint-jinja-resolver.ps1` over the live workflows reports no new errors and no new warnings. - Workflow YAML validates with `conductor validate`. ## Notes - Verb fixture (`tests/lint/fixtures/verb-output-schemas.json`) is hand-curated per the existing convention — manually added the new verb + result-type entries with `required: true` for inputs to preserve VERB003 lint coverage. - A harness scenario for these gates was deferred: the harness driver does not yet support scripted human-gate responses. Filing a follow-up to extend it would unblock E2E coverage. - The verb is a defense, not a root-cause fix. The misroute itself most likely originates in the per-task branch checkout in the upstream impl executor; tracking that down is a separate investigation. The assertion ensures that when the bug fires again, it halts loudly at the dispatch boundary instead of green-washing into the MG. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PolyphonyRequiem
added a commit
that referenced
this pull request
May 15, 2026
…3216 (#404) Three lint failures cascaded since PR #401/#402 merged: 1. lint-type-agnostic: implement-merge-group.yaml had `| Task | AB#... |` markdown table rows in failure-recovery prompts. The row label `Task` was generic prose meaning "work item being implemented", not the type. Renamed to `| Work item | AB#... |` (3 sites). 2. lint-bare-cross-branch-refs: squash_coverage_mismatch_gate prompt inlined `git diff/log` commands with `{{ assert_impl_pr_coverage.output.{comparison_base,impl_ref,mg_ref} }}` jinja templates. The verb returns origin/-prefixed values at runtime (PrCommands.AssertImplPrCoverage.cs lines 69-75) but the lint can only see the unresolved templates. Added `# bare-ref-ok: <reason>` comments above each line, and reworded the line 872 comment to drop the literal `git diff` verb. 3. lint-pwsh-jinja-bareword: this lint has `if: always()` so it runs even after step (1)/(2) fail. But `Install powershell-yaml` was sequenced AFTER the lint block; once a non-`if: always()` lint failed, the install skipped and the bareword lint errored with "FATAL: the powershell-yaml module is required". Reordering ci.yml hardens against this whole class: - Move `Install Pester` and `Install powershell-yaml` BEFORE the lint block so they always run. - Add `if: always()` to every lint step so a single failure surfaces ALL violations in one CI run instead of cascading via skip. Why both PRs slipped through: PR #401 and #402 squash-merged within 30s of each other (23:26:48Z and 23:27:19Z). Each PR's CI ran against its own merge commit on a clean base — neither saw the OTHER PR's regression that would land alongside on main. Classic merge-skew false-green. Verified: all three lints pass locally (lint-type-agnostic, lint-bare-cross-branch-refs, lint-pwsh-jinja-bareword). conductor validate implement-merge-group.yaml is clean. Co-authored-by: Daniel Green <dangreen@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PolyphonyRequiem
added a commit
that referenced
this pull request
May 15, 2026
…ses AB#3214 (#406) AB#3175 incident (apex run 3165, 2026-05-14): a child task reached state=Done in ADO via `primary_completer` while the implementing commit was stranded on a sibling task's impl branch and the squash-merge to the MG carried zero diff. Net result: ADO reported the work as Done while `feature/{root}` lacked any of its commits. PRs #401 (AB#3210, impl-branch routing assertion) and #402 (AB#3211, `pr assert-impl-pr-coverage`) plug the upstream root causes. After both ship, the failure mode is structurally impossible — PROVIDED `primary_completer` is reachable ONLY via the coverage-asserted path. This PR encodes that contract as a structural lint (`tests/lint-primary-completer-trust-chain.ps1`) so future routing edits cannot reintroduce a bypass path: I1. The ONLY route INTO `primary_completer` is from `delete_impl_branch`. I2. The ONLY routes INTO `delete_impl_branch` are from `assert_impl_pr_coverage` (action='ok') or `squash_coverage_mismatch_gate` (option='force_accept'). Together these guarantee that `primary_completer` fires only after diff-equivalence was asserted or the operator explicitly acknowledged a known mismatch. Approach. Added a "Trust chain (AB#3214)" comment block to `primary_completer` documenting the upstream defenses, the routing invariant, the historical incident, and the lint that enforces it. Did NOT add an inline coverage re-assertion: the upstream defense runs immediately before this step (via `delete_impl_branch`), so an inline call would be redundant duplicate work. Per AB#3214 AC, the "document trust chain explicitly" branch is the right answer, with the structural lint as the regression test. Validation: - `conductor validate` clean on implement-merge-group.yaml. - New lint PASSes against the real workflow (trust chain intact). - 5/5 Pester tests for the new lint: * 2 PASS scenarios (synthetic correct chain + real workflow) * 3 FAIL scenarios (rogue predecessor on primary_completer, rogue predecessor on delete_impl_branch, missing assert_impl_pr_coverage) - 74/74 existing trunk lints (type-agnostic, bare-cross-branch-refs, pwsh-jinja-bareword) still pass. References: AB#3214, AB#3210 (PR #401), AB#3211 (PR #402), AB#3175 (incident) Co-authored-by: Daniel Green <dangreen@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PolyphonyRequiem
added a commit
that referenced
this pull request
May 19, 2026
…3210) (#484) Adds pre_coder_branch_mismatch_aborts_via_scripted_gate scenario to implement-merge-group.yaml. Stubs the 5-step entry chain (branch_ensure_mg → primary_router → impl_branch_ensure → guidance_loader → branch_assert_on_impl_pre_coder returning action='mismatch') and uses the AB#3212 scripted-gates infrastructure to drive the gate's 'abort' option — the recovery path proven by AB#3210 (PR #401) but until now uncovered by automated tests because --skip-gates would auto-pick the first option (retry) and loop forever against the stubbed verb. Uses the gates_resolved trace assertion shipped in PR #483: gates_resolved: pre_coder_branch_mismatch_gate: abort No direct git, no agents — pure polyphony-verb entry chain, all shim-interceptable. Co-authored-by: Daniel Green <dangreen@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
AB#3210: assert HEAD is on impl branch around coder turn
Closes AB#3210.
Background — what bit us in apex run 3165
The impl agent for AB#3175 wrote its commit (
3f45269) ontoimpl/3165-3176— a SIBLING task's impl branch — instead ofimpl/3165-3175. This produced a perfectly silent failure:impl/3165-3175mg/3165_pg-3175feature/3165The misroute was structurally invisible until the empty-MG check
caught it three review cycles later. There was no fast-fail at the
boundary where the impl agent acquired the worktree — by the time
the reviewer saw the no-op MG, the misrouted commit was already
stranded on the wrong impl branch and AB#3175 had been state-
transitioned to Done.
Recovery required hand-shipping PR #398 (cherry-picking the verb
onto
impl/3165-3175) and aborting the scope-review cap gate.Fix — verb + workflow gates
New verb:
polyphony branch assert-on-impl --root-id N --item-id Mgit symbolic-ref HEADviaIGitClient.BranchNameBuilder.Impl(root, item).Value(
impl/{root}-{item}).actiondiscriminator:ok— current branch matches expectedmismatch— current ≠ expected, OR detached HEAD, OR git failure(all surface uniformly so the gate prompt covers every "not on the
right branch" case)
error— input validation failure (caller-side bug)Workflow integration —
implement-merge-group.yaml:The assertion runs at TWO points around every coder turn:
Pre-coder (between
guidance_loaderandcoder).If the worktree is on the wrong branch BEFORE the coder runs,
halt to a human gate so the operator can
git checkoutthe rightbranch before any code is written. Prevents the bug.
Post-coder (between
coderandfetch_for_primary_review).If the coder somehow ended up off-branch, halt before the
misrouted commit can be promoted into the MG. Catches any path
that bypassed the pre-check.
Each gate offers Retry (re-run the assertion after operator fixes
the worktree) or Abort. Pre-coder gate prevents the data loss;
post-coder gate is defense-in-depth.
Test coverage
BranchCommandsAssertOnImplTestscoveringmissing args, ok / mismatch / detached-HEAD / git-failure paths,
and the snake_case JSON output contract.
HeadOnSiblingImplBranch_EmitsMismatchasserts that
root_id=3165, item_id=3175, actual=impl/3165-3176returns
action=mismatch(the exact 3175 incident).tests/lint-jinja-resolver.ps1over the live workflows: no newerrors, no new warnings introduced by this change.
conductor validate .conductor/registry/workflows/implement-merge-group.yamlpasses.
Polyphony.Testssuite green (3511 pass, 4 skip, 0 fail).Notes
tests/lint/fixtures/verb-output-schemas.jsonis hand-curated topreserve VERB003 lint coverage (Move feat: Phase Detection, Routing Engine, and Command Implementations (PG-3) AB#2593 AB#2594 #2 sentinels would otherwise
flatten
required: truein the live schema). I added the new verbrequired: truefor inputs tomatch the existing pattern.
yet support scripted human-gate responses. A scenario covering
these two new gates would need driver-side work first. Worth filing
as a follow-up.
originates upstream in the per-task branch checkout in the impl
executor. Tracking that down is a separate investigation. This PR
ensures that when the bug fires again, it halts loudly at the
dispatch boundary instead of green-washing into the MG.
Files
src/Polyphony/Models/BranchAssertOnImplResult.cs(new)src/Polyphony/Commands/BranchCommands.AssertOnImpl.cs(new)src/Polyphony/PolyphonyJsonContext.cs(+1 line registration)tests/Polyphony.Tests/Commands/BranchCommandsAssertOnImplTests.cs(new, 10 tests).conductor/registry/workflows/implement-merge-group.yaml(+4 nodes: 2 assertions + 2 gates)
tests/lint/fixtures/verb-output-schemas.json(hand-curated entry)