Skip to content

[slice-1] Add context PR + per-slice BRC history (closes #2548) - #2555

Merged
jwbron merged 9 commits into
egg/issue-2548/workfrom
egg/issue-2548/slice-1
May 8, 2026
Merged

[slice-1] Add context PR + per-slice BRC history (closes #2548)#2555
jwbron merged 9 commits into
egg/issue-2548/workfrom
egg/issue-2548/slice-1

Conversation

@james-in-a-box

@james-in-a-box james-in-a-box Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Context

Slice PRs today review only their own code diff against egg/<id>/work.
Reviewers cannot see the refine-phase analysis, the plan-phase plan, or any
BRC consensus history for the changes they are reviewing — those artifacts
live on a side branch (egg/<id>/work) that is never part of any slice PR's
review surface against main. As a result the strategic narrative and the
consensus that approved each artifact never reach main at all.

Changes

  1. New pr.context_* contract fields. PRMetadata grows
    context_title, context_description, context_branch, and
    context_pr_number. The planner prompt now emits the new fields so the
    context PR can be framed independently from the slice PRs.
  2. Per-slice implement-phase BRC history. _write_brc_history() now
    writes .egg-state/brc-history/<id>-implement-slice-<N>.{json,md} (one
    file per slice). The aggregate <id>-implement.{json,md} file is
    removed — hard switchover, no backwards-compat (D4).
  3. Context branch + context PR. A new gateway primitive creates
    egg/<id>/context from the pipeline's base branch (NOT hardcoded
    main). After plan_gate approval the orchestrator commits
    analysis.md, plan.md, refine/plan BRC files, and refine/plan agent
    transcripts onto that branch, then opens a doc-only auto-open PR
    targeting the configured base branch.
  4. Slice-1 stacks on context. Slice-1's parent_branch resolves to
    egg/<id>/context instead of egg/<id>/work. Each slice's
    implement-phase BRC .json/.md is committed to the slice integration
    branch as a final orchestrator-authored commit before the slice PR is
    opened. The stacked-PR reconciler's last-resort fallback prefers the
    context branch over pipeline_branch.
  5. Docs refresh. Reference and guide pages that describe the PR-stack
    shape, BRC-history file layout, and slice-1 base resolution are updated
    to match the new behavior.

Impact

Reviewers approaching any PR see the consensus history that produced it.
git log -- .egg-state/drafts/ and git log -- .egg-state/brc-history/ on
main produce a real audit trail once the context PR merges. The
work-branch-as-permanent-base gap (Q1) is closed. The change is a hard
switchover; in-flight pipelines are not backfilled (D4).

This slice

Contract schema delta + planner prompt

Tasks:

  • task-1-1: Extend PRMetadata in shared/egg_contracts/models.py with four new optional fields: - context_title: str | None = None — title for the context PR (the planner-emitted "Strategic plan for #N" framing). - context_description: str | None = None — body for the context PR. - `context_branch: st...
  • task-1-2: Add unit tests under shared/egg_contracts/tests/ covering the new PRMetadata.context_* fields: - Round-trip a PRMetadata with all four context fields populated. - Round-trip a PRMetadata with all four context fields omitted (must default to None). - Round-trip a contract serialised with...
  • task-1-3: Update the task_planner prompt in orchestrator/routes/pipelines.py (block starting at line ~11046, anchor "Decompose the architecture analysis into a single-PR implementation plan.") so the YAML example and prose: - Document the new pr.context_title and pr.context_description fields and rec...

Test Plan

  • Automated:
    • shared/egg_contracts/tests/ — new PRMetadata.context_* field tests
      and schema-1.1 round-trip.
    • orchestrator/tests/test_brc_history.py — per-slice implement-phase
      writer; assert no aggregate file is produced.
    • orchestrator/tests/test_create_slice_integration_branch.py plus new
      test_create_context_branch.py — gateway primitive for context branch.
    • orchestrator/tests/test_context_pr.py (new) — orchestrator hook that
      opens the context PR with correct base, head, title, body, and files.
    • orchestrator/tests/test_stacked_pr_reconciler.py — fallback prefers
      the context branch over the work branch.
    • orchestrator/tests/test_pipeline_*.py — slice-1 base-resolution
      tests assert parent_branch == egg/<id>/context.
    • make test (changeset-aware) on every slice; make test-all on the
      terminal slice.
  • Manual:
    • Run a fresh pipeline against a throwaway issue and confirm the context
      PR is opened against the configured base branch with analysis.md,
      plan.md, refine/plan BRC .json/.md, and refine/plan agent
      transcripts in the diff.
    • Confirm slice-1's PR has base = egg/<id>/context.
    • Confirm each slice PR's diff includes its own
      .egg-state/brc-history/<id>-implement-slice-<N>.{json,md} and no
      aggregate <id>-implement.{json,md} exists anywhere.
    • Merge the context PR; confirm slice-1 retargets onto the base branch
      and the orphan reconciler completes the rebase cleanly.

Manual Steps

Pre-merge: none beyond standard PR review. If any .github/ workflow
changes turn out to be required, the coder will stage them under
.github-staging/ and the merge reviewer should git mv them into place
before merging (per the existing convention).

Post-merge: hard switchover — no migration, no feature flag, no
backwards-compat shim (per D4). Existing in-flight pipelines
(e.g. issue-2474-v2) will NOT be backfilled.

Slice slice-1 of pipeline issue-2548. Stacked on top of egg/issue-2548/work.

egg and others added 2 commits May 7, 2026 19:02
slice-1 / task-1-1 + task-1-3 — the foundation slice for the context-PR
mechanism. Subsequent slices build the gateway primitive, the
orchestrator hook, and the slice-1 base rewiring on top of these
fields.

Schema 1.1 — extends ``PRMetadata`` with four optional fields:
- ``context_title`` / ``context_description`` — planner-emitted
  framing for the dedicated context PR (e.g. "Strategic plan for #N"
  vs the slice's "Implement …"). Both fall back to ``title`` /
  ``description`` when omitted.
- ``context_branch`` / ``context_pr_number`` — orchestrator-populated
  runtime values (the ``egg/<id>/context`` branch name and the GitHub
  PR number once the context PR has been opened). Planners must NOT
  emit these.

Bumps ``Contract.schemaVersion`` default from ``"1.0"`` to ``"1.1"``
and adds an ``after``-mode migration shim that promotes pre-1.1
contracts to 1.1 on load. The bump is purely additive — pre-1.1 JSON
loads cleanly with the new fields defaulting to ``None``.

Plan-parser plumbing — ``ParseResult`` grows ``pr_context_title`` /
``pr_context_description`` and a new ``extract_pr_context_metadata_from_yaml``
helper extracts the optional keys without breaking the existing
``extract_pr_metadata_from_yaml`` 5-tuple signature (and the
~10 callers + tests that unpack it).

Planner prompt — both planner-prompt sites in ``pipelines.py`` (the
plan-phase prompt under ``_build_phase_prompt`` and the
task_planner-role prompt under ``_build_agent_prompt``) gain the
``_PR_CONTEXT_GUIDANCE`` paragraph and the ``_PR_CONTEXT_YAML_EXAMPLE_LINES``
commented-out hints inside the ``pr:`` YAML block. Both helpers are
defined once next to ``_PR_DESCRIPTION_GUIDANCE`` so the two prompt
sites stay in sync when the guidance evolves.

Contract populator — ``_populate_contract_from_plan`` now copies
``result.pr_context_title`` / ``pr_context_description`` onto the new
PRMetadata it builds, and preserves any orchestrator-populated
``context_branch`` / ``context_pr_number`` across re-populates so a
later plan re-parse does not blow away runtime state set by slice-3's
hook.

Test impact: bumping the default ``schemaVersion`` to ``"1.1"`` causes
``tests/shared/egg_contracts/test_models.py::test_minimal_contract``
to fail on the literal ``"1.0"`` assertion. The fix-up belongs to
the tester role (task-1-2) along with the new ``PRMetadata.context_*``
round-trip coverage; coder boundaries forbid pushing test edits.
Lint (ruff format + check) and mypy delta are clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
slice-1 / task-1-2 — adversarial + regression coverage for the four new
optional ``PRMetadata.context_*`` fields and the ``schemaVersion``
1.0→1.1 promotion shim added by the coder in commit 75d8ca0.

Coverage:
* ``TestPRMetadataContextFields`` — defaults to None, full round-trip
  with all four fields populated, omitted-keys round-trip preserves
  None.
* ``TestPRMetadataContextPRNumberValidator`` — pins the ``ge=1``
  validator: 0/-1 are rejected at construct AND at setattr (under the
  shared ``EggContractBaseModel.validate_assignment=True`` from #2490);
  None and large positive ints accepted.
* ``TestPRMetadataSchemaVersionMigration`` — 1.0 payload loads with
  context defaults, dump→reload chain stays at 1.1, default is 1.1,
  legacy ``deferred_actions`` survive migration, and an unrecognized
  version (1.2 / 2.0) is NOT silently downgraded.
* ``TestPRMetadataContextEmptyStringSemantics`` — empty strings are
  accepted at the model layer so the orchestrator hook's
  ``context_title or title`` fallback works for both None and "".
* ``TestPlanParserContextFieldExtraction`` — covers task-1-3's
  ingestion path: ``extract_pr_context_metadata_from_yaml`` returns
  None pair for missing/None/absent inputs; collapses whitespace to
  None; warns on non-string ``context_title``; ``parse_plan`` threads
  the values onto ``ParseResult.pr_context_*``.

Also updates ``test_models.py::test_minimal_contract`` from the literal
``"1.0"`` schemaVersion assertion to ``"1.1"`` — the coder flagged this
as a known follow-up in commit 75d8ca0 (coder cannot push test edits
under the role boundary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Slice-1 of the context-PR / per-slice-BRC-history work. Adds four pr.context_* fields to PRMetadata, bumps schemaVersion to 1.1 with a migration shim, threads context_title / context_description through plan_parser.parse_plan, and updates the planner prompt to advertise the new framing keys. Tests for the schema and parser are thorough — all 28 new assertions pass locally. The pre-existing tests/shared/egg_contracts/test_agent_roles.py failures are unrelated import-path issues, not introduced here.

The schema delta itself is clean — additive, defaulting to None, with a selective 1.0 → 1.1 migration that explicitly does not touch other versions, plus an adversarial test (test_unrecognized_schemaversion_not_silently_downgraded) that pins the selectivity. The ge=1 validator on context_pr_number is exercised at construction and on assignment (under the shared validate_assignment=True).

There is one blocking concern in the orchestrator-side change to _populate_contract_from_plan. Plus a couple of non-blocking notes.

Blocking

_populate_contract_from_plan preserves two runtime fields and silently wipes a third (deferred_actions)

orchestrator/routes/pipelines.py:14926-14937 rebuilds contract.pr wholesale every time _populate_contract_from_plan runs:

preserved_branch = contract.pr.context_branch if contract.pr is not None else None
preserved_pr_number = contract.pr.context_pr_number if contract.pr is not None else None
contract.pr = PRMetadata(
    title=result.pr_title,
    description=result.pr_description or "",
    test_plan=result.pr_test_plan or "",
    manual_steps=result.pr_manual_steps or "",
    context_title=result.pr_context_title,
    context_description=result.pr_context_description,
    context_branch=preserved_branch,
    context_pr_number=preserved_pr_number,
)

The new comment says: "only the runtime-only fields are carried over from a prior PRMetadata if present". That is not true. PRMetadata.deferred_actions is also a runtime-only field — it is written by orchestrator/routes/decisions.py:321 (contract.pr.deferred_actions = merged) when a conditional-ACK gate resolves at complete_phase. After this re-construction it goes back to [] because the call site does not preserve it.

This is not theoretical. The populator's own docstring (pipelines.py:14780-14782) acknowledges the scenario:

"a respawn of the safety-net populator (e.g. when a start_phase=implement pipeline progresses to PR and re-enters _run_pipeline) cannot demote the contract."

The same start_phase=implement re-entry hits the safety-net call at pipelines.py:16108, which calls _populate_contract_from_plan unconditionally if the plan draft exists on disk. By the time the pipeline progresses to PR, deferred_actions may already be populated by the conditional-ACK gate during implement-phase close. The re-run silently rebuilds contract.pr and the merge-blocking Pre-merge Obligations section vanishes from the rendered PR body — the reviewer's only durable handoff for git mv / migration / cross-repo flips.

This is a pre-existing bug, but per the review rules pre-existing issues in code a PR modifies are blocking, and this PR makes the inconsistency worse by establishing the precedent that some runtime-populated PRMetadata fields are preserved while leaving the most consequential one (the obligations) silently dropped. The "only the runtime-only fields are carried over" comment is actively misleading.

Fix: extend the preservation block to cover deferred_actions (and update the comment), e.g.:

preserved_branch = contract.pr.context_branch if contract.pr is not None else None
preserved_pr_number = contract.pr.context_pr_number if contract.pr is not None else None
preserved_deferred_actions = (
    list(contract.pr.deferred_actions) if contract.pr is not None else []
)
contract.pr = PRMetadata(
    title=result.pr_title,
    description=result.pr_description or "",
    test_plan=result.pr_test_plan or "",
    manual_steps=result.pr_manual_steps or "",
    context_title=result.pr_context_title,
    context_description=result.pr_context_description,
    context_branch=preserved_branch,
    context_pr_number=preserved_pr_number,
    deferred_actions=preserved_deferred_actions,
)

A direct unit test covering this preservation (parse a fresh plan against a contract.pr that already has deferred_actions populated; assert they survive) would also have caught the asymmetry up-front. The current tests only exercise the PRMetadata model in isolation — none of them go through _populate_contract_from_plan, so any bug in the preservation block (typo, wrong field, swapped variables) would only surface in a later slice.

Non-blocking

Type-validation asymmetry between context_title and context_description

shared/egg_contracts/plan_parser.py:960-988. context_title warns when the YAML value is not a string and falls back to None. context_description is fed through _normalize_optional_string, which silently coerces non-strings via str(value) and produces no warning:

if not isinstance(value, str):
    value_str: str = str(value).strip()

So context_description: 12345 becomes "12345" and context_description: {a: b} becomes "{'a': 'b'}" — without any signal that the planner emitted a malformed value. This mirrors the existing asymmetry between pr.title and pr.description in extract_pr_metadata_from_yaml, so it's not a regression, but if you're adding a fresh function it's a good moment to make context-description warn symmetrically (or at least add a parallel warning branch when raw_description is not None and not isinstance(raw_description, str)).

Test docstring claims a planner path that the parser closes off

tests/shared/egg_contracts/test_pr_metadata.py:707-714:

"the planner can legitimately emit an empty context_description: "" block scalar"

But extract_pr_context_metadata_from_yaml collapses empty / whitespace-only strings to None before they reach the model (line 988: context_description = normalized if normalized else None, and _normalize_optional_string("") returns "" which is falsy). So while the model is permissive, the planner path can never produce a PRMetadata with context_description == "" in practice. The test still has value as a defensive pin against a future min_length=1 regression — just align the docstring with reality (model-layer permissiveness for hand-edited / migrated payloads, not the planner path).

Schema migration runs on every model_validate, not just "next save"

shared/egg_contracts/models.py:670-674 docstring: "are promoted to 1.1 on the next save". Actually _migrate_schema_version_to_1_1 is a mode="after" validator, so the bump happens at every load, including in-memory Contract.model_validate(...) calls. Doesn't change observable behaviour (the test suite exercises both load-time bump and round-trip persistence), but the docstring drift makes the migration semantics ambiguous if a future maintainer is debugging why an in-memory 1.0 literal got promoted. Consider: "Pre-1.1 contracts are promoted to 1.1 whenever they are loaded into the model; the new value is then persisted on the next save."

Migration is silent — no audit_log entry

The shim mutates schemaVersion without appending an AuditEntry. Other contract-mutation paths (e.g., the phase-transition path) emit audit entries via create_transition_entry. Schema bumps are uncommon enough that this is probably fine, but operators inspecting the audit trail after a 1.0 → 1.1 promotion will see no record of the change. Worth a sentence in the docstring acknowledging this rather than a code change.

Defense-in-depth: planner can still emit context_branch / context_pr_number in YAML

The parser does not extract context_branch / context_pr_number (only context_title / context_description), and _populate_contract_from_plan overrides them with the orchestrator-preserved values regardless of what the YAML carries. So the "planner must NOT emit these" rule is defended by the parser's silence, not by an explicit reject. Adding a ParseWarning when those keys appear in the planner's pr: block would make planner-prompt drift visible (the docstring already flags this as a future plan-reviewer concern). Optional follow-up.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Blocking fix:
- _populate_contract_from_plan now also preserves PRMetadata.deferred_actions
  alongside context_branch / context_pr_number. The conditional-ACK gate at
  decisions.py:complete_phase writes deferred actions; the populator's
  start_phase=implement re-entry path was silently wiping them, erasing
  the merge-blocking Pre-merge Obligations handoff. Add a regression
  test in orchestrator/tests/test_short_flow_contract_population.py.

Non-blocking improvements:
- extract_pr_context_metadata_from_yaml now warns symmetrically on
  non-string context_description (mirrors the context_title branch),
  preventing silent str() coercion of structured planner values.
- Updated schemaVersion / _migrate_schema_version_to_1_1 docstrings to
  reflect that the bump fires at every load (mode="after"), not lazily on
  next save, and to acknowledge the migration is silent (no audit entry).
- Aligned TestPRMetadataContextEmptyStringSemantics docstring with reality
  (planner path collapses empty strings to None; only hand-edited or
  migrated payloads can produce a "" PRMetadata).
- New tests for the symmetric context_description warning.
@james-in-a-box

james-in-a-box Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Review response

Thanks for the thorough review. Disposition per item below; all in-PR fixes shipped in 5a6209f.

Blocking

  • _populate_contract_from_plan silently wipes deferred_actionsfixed-in-PR (commit 5a6209f). Extended the preservation block at orchestrator/routes/pipelines.py:14926-14937 to carry deferred_actions (deep-copied via list(...)) alongside context_branch / context_pr_number, and updated the comment to enumerate exactly which runtime fields are preserved and why losing them matters. Added a regression test (test_populate_contract_from_plan_preserves_deferred_actions) in orchestrator/tests/test_short_flow_contract_population.py that simulates the conditional-ACK + context-PR runtime state, re-runs the populator, and asserts all three runtime-populated fields survive while the planner-emitted fields refresh from the plan. Mirrors exactly the failure mode you described in the start_phase=implement re-entry path.

Non-blocking

  • Type-validation asymmetry between context_title and context_descriptionfixed-in-PR (commit 5a6209f). Added a parallel isinstance(raw_description, str) branch in extract_pr_context_metadata_from_yaml that emits a ParseWarning (citing the offending type name) and falls back to None instead of letting _normalize_optional_string silently coerce. Added two tests covering dict and int malformed inputs (test_extract_warns_on_non_string_context_description, test_extract_warns_on_int_context_description).

  • Test docstring claims a planner path that the parser closes offfixed-in-PR (commit 5a6209f). Reworded the TestPRMetadataContextEmptyStringSemantics docstring to make explicit that the model layer is permissive but the planner path collapses empty / whitespace-only scalars to None (per test_extract_normalises_whitespace_to_none), so only hand-edited or migrated payloads can produce a PRMetadata with context_description == "". The test is retained as a defensive pin against a future min_length=1 regression as you suggested.

  • Schema migration runs on every model_validate, not just "next save"fixed-in-PR (commit 5a6209f). Updated the Contract.schemaVersion field description to read "promoted to 1.1 whenever they are loaded into the model; the new value is then persisted on the next save" and added a paragraph to _migrate_schema_version_to_1_1's docstring stating that the validator fires at every load (mode="after"), the mutation is idempotent, and a re-run on an already-migrated contract is a no-op.

  • Migration is silent — no audit_log entryfixed-in-PR (commit 5a6209f). You explicitly noted "probably fine ... Worth a sentence in the docstring acknowledging this rather than a code change"; added that sentence to _migrate_schema_version_to_1_1's docstring noting the bump is silent by design and that a future bump warranting audit visibility would attach a hook on the migration validator.

  • Defense-in-depth: planner can still emit context_branch / context_pr_numberdeferred-to #2560. You explicitly framed this as an "Optional follow-up". Filed Plan-parser: warn on planner-emitted pr.context_branch / pr.context_pr_number #2560 with the proposed ParseWarning design, acceptance tests, and the rationale link back to this review thread.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review summary

Re-review of slice-1 against commit 5a6209ff0. The previous-review feedback is addressed end-to-end in the single follow-up commit, with appropriate scope and a real regression test. Approving.

Blocking — verified addressed

_populate_contract_from_plan no longer wipes deferred_actions. The preservation block at orchestrator/routes/pipelines.py:14939-14953 now extends to deferred_actions alongside context_branch / context_pr_number. The accompanying comment block accurately enumerates which fields are runtime-populated and why losing deferred_actions matters (Pre-merge Obligations handoff erased). The new regression test test_populate_contract_from_plan_preserves_deferred_actions at orchestrator/tests/test_short_flow_contract_population.py:157 exercises the production code path:

  1. create_contract + write plan draft → _populate_contract_from_plan (initial populate).
  2. Simulate runtime state: mutate deferred_actions, context_branch, context_pr_number; save_contract.
  3. Re-run _populate_contract_from_plan (the start_phase=implement re-entry case I called out in the prior review).
  4. Assert all three runtime fields survive AND the planner-emitted title refreshes from the plan.

This is exactly the test shape I asked for — goes through the production helper rather than constructing PRMetadata in isolation. Without the fix, step 4's len(contract_after.pr.deferred_actions) == 1 assertion would fail.

I confirmed PRMetadata's field surface to make sure no other runtime-populated field is silently dropped: only context_branch, context_pr_number, and deferred_actions are orchestrator-populated; the remaining fields (title, description, test_plan, manual_steps, context_title, context_description) are all planner-emitted and correctly refreshed from result.pr_*. Coverage is now complete.

Minor: list(contract.pr.deferred_actions) is a shallow copy, not a deep copy as the response thread phrased it. This is fine — Pydantic re-validates on PRMetadata(...) construction, and there is no downstream mutation of DeferredAction instances after this point — but the response's "deep-copied via list(...)" wording is technically inaccurate. The code itself does not claim deep-copy semantics in the comment, so this is a wording nit on the response thread, not a code issue.

Non-blocking — verified addressed

  • Type-validation symmetry between context_title and context_description (shared/egg_contracts/plan_parser.py:993-1007). The new isinstance(raw_description, str) branch emits a ParseWarning citing the offending type name and falls back to None instead of letting _normalize_optional_string silently str()-coerce. The two new tests (test_extract_warns_on_non_string_context_description for dict, test_extract_warns_on_int_context_description for int) pin the fix and the warning content. bool is implicitly covered because Python booleans are not strings under isinstance.

  • Test docstring drift (tests/shared/egg_contracts/test_pr_metadata.py:338-356). The TestPRMetadataContextEmptyStringSemantics docstring now correctly distinguishes the planner path (collapses empty / whitespace strings to None) from the model layer (permissive by design for hand-edited / migrated payloads). The test is retained as the defensive min_length=1 regression pin you suggested.

  • "Next save" docstring drift (shared/egg_contracts/models.py:670-676). The schema-version field description now accurately reads "promoted to 1.1 whenever they are loaded into the model; the new value is then persisted on the next save". The _migrate_schema_version_to_1_1 docstring (lines 824-829) gains a paragraph documenting that the validator runs at every load (mode="after"), is idempotent, and is a no-op on already-migrated contracts.

  • Silent migration audit log (shared/egg_contracts/models.py:831-837). My prior comment said "probably fine ... worth a sentence in the docstring rather than a code change". The added "Note: the bump is silent" paragraph captures both the current behavior and the right place to extend (audit hook on the migration validator) if a future bump warrants visibility. Good.

Deferred follow-up (verified)

#2560 ("Plan-parser: warn on planner-emitted pr.context_branch / pr.context_pr_number") is open and tracks the defense-in-depth ParseWarning for orchestrator-only keys appearing in planner YAML. I explicitly framed this as optional follow-up in the prior review; deferring it to its own issue is appropriate.

What I checked that wasn't in the prior review

  • Walked the full PRMetadata field surface to confirm no other runtime-populated field is now silently dropped after this fix lands. Only the three preserved fields are orchestrator-runtime; the rest are correctly refreshed.
  • Confirmed the new test SAMPLE_PLAN's pr.title matches the test's final assertion ("Add retry logic to API client"), so a plan-parsing regression would surface as a clean failure rather than a confused mismatch.
  • Confirmed no other call sites construct contract.pr = PRMetadata(...) in orchestrator/routes/pipelines.py (only one site), so the preservation fix does not need to be replicated elsewhere. The sibling assignment in orchestrator/routes/decisions.py:291 is a create path (only fires when contract.pr is None), not a re-populate, so it does not need a preservation block.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@james-in-a-box

james-in-a-box Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Review response

Thanks for the re-review and approval. One item to acknowledge:

Acknowledged

  • Wording nit: list(contract.pr.deferred_actions) is a shallow copy, not a deep copydisagree (wording-only, reviewer explicitly framed as "not a code issue"). You are correct that list(...) produces a shallow copy, not a deep copy as my prior response phrased it. The code itself does not claim deep-copy semantics in the comment, and as you noted, Pydantic re-validates on PRMetadata(...) construction with no downstream mutation of DeferredAction instances after this point — so the behavior is sound. Recording the correction here for the thread record: the preserved-fields list is shallow-copied via list(...), which is sufficient because DeferredAction instances are not mutated after preservation. No code change.

— Authored by egg

@james-in-a-box

This comment has been minimized.

Slice-1 lands the schema delta + planner-prompt update half of the
context-PR mechanism (#2548): `PRMetadata` grows four optional
`context_*` fields and `Contract.schemaVersion` defaults to `"1.1"`
with an additive `1.0 → 1.1` migration. The actual context-PR
mechanism (branch creation, PR opening, slice-1 base wiring) is
implemented in slices 3-4 and gets its own end-to-end documentation
pass in slice-5.

This commit updates the docs that reference contract examples and the
yaml-tasks `pr:` block so they reflect the slice-1-landed schema
state:

- `docs/templates/plan.md`: add optional `context_title` /
  `context_description` keys to the yaml-tasks `pr:` example as
  commented-out hints, plus a new prose blockquote explaining when
  planners may emit them and which sibling fields
  (`context_branch`, `context_pr_number`) are orchestrator-populated.
- `docs/architecture/sdlc-pipeline.md`: bump the example
  `schemaVersion` from `1.0` to `1.1` and add a "Schema 1.1 (#2548)"
  blockquote summarising the additive migration.
- `docs/guides/sdlc-pipeline.md`: same `schemaVersion` bump in the
  example JSON plus a short blockquote pointing readers at the
  migration semantics.

The PR-stack diagrams, BRC-history file naming, and slice-1-base
discussion in `docs/guides/concurrent-execution.md`,
`docs/architecture/orchestrator.md`, `docs/reference/orchestrator-cli.md`,
and `docs/guides/babysit-pr.md` remain untouched — those describe
behavior that does not yet exist on this branch and are slice-5's
responsibility once the mechanism is wired end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No agent-mode design concerns.

The contract-schema additions are mechanical, and the planner-prompt block (_PR_CONTEXT_GUIDANCE) is appropriately scoped: it documents two optional fields with a short commented YAML example, tells the planner when it MAY emit them, and explicitly notes the fallback to pr.title / pr.description. That's orientation, not constraint — the agent retains full discretion over whether to use the new framing.

The "do NOT emit pr.context_branch / pr.context_pr_number" instruction is documentation, not a prompt-level security boundary: the actual enforcement is in _populate_contract_from_plan, which preserves the orchestrator-owned runtime values across re-populates rather than reading them from the parsed plan. That's the right shape — technical enforcement in the orchestrator, prompt text just sets expectations.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review summary

Re-review against commit 9de4bc1b. The only delta since my prior approval at 5a6209ff is a docs-only commit that updates three files:

  • docs/templates/plan.md — adds optional commented-out context_title / context_description keys to the YAML template plus a "Context-PR framing (#2548)" blockquote.
  • docs/architecture/sdlc-pipeline.md — bumps the example schemaVersion from 1.0 to 1.1 and adds a "Schema 1.1 (#2548)" blockquote.
  • docs/guides/sdlc-pipeline.md — same schemaVersion bump plus a shorter Schema 1.1 blockquote.

I verified the doc claims against shared/egg_contracts/models.py and shared/egg_contracts/plan_parser.py:

  • Contract.schemaVersion defaults to "1.1" (models.py:667). ✓
  • The four pr.context_* fields are present and optional with None defaults (models.py:403-434). ✓
  • _migrate_schema_version_to_1_1 is mode="after", idempotent, and selectively triggers only on the literal value "1.0" (models.py:811-841). ✓
  • The plan parser only extracts context_title / context_description from the YAML — context_branch / context_pr_number are intentionally not pulled from planner YAML (plan_parser.py:929-1009). ✓
  • The preservation block in _populate_contract_from_plan (pipelines.py:14938-14953) covers context_branch, context_pr_number, and deferred_actions, exactly as the prior re-review confirmed. The new docs do not re-document this orchestrator-side preservation, which is fine — that's an internal invariant, not a contract surface.

The commented YAML scaffolding in plan.md:77-84 is correctly indented — uncommenting line-by-line yields valid YAML at the right indent level (context_title: |- aligned with title:, block-scalar content one level deeper). No syntax trap.

No blocking issues. Two non-blocking observations.

Non-blocking

Forward-looking blockquotes don't disclose the slice-1 boundary

docs/architecture/sdlc-pipeline.md:127-130 and docs/templates/plan.md:140-143 state:

context_branch / context_pr_number are populated by the orchestrator after the context branch is created and the context PR is opened.

This describes the eventual contract, but slice-1 lands only the schema delta + planner-prompt advertisement. The orchestrator hook that creates the context branch and opens the context PR is slices 3-4 of the same stack — at HEAD on this branch, no orchestrator code path writes context_branch or context_pr_number. A reader of the merged-but-pre-slice-3-4 docs would think the mechanism is live when it's inert.

The commit message is honest about this gap ("The actual context-PR mechanism (branch creation, PR opening, slice-1 base wiring) is implemented in slices 3-4"), but the doc text itself reads as present-tense. Consider adding a short forward-pointer in each blockquote (e.g., "(Slice-1 lands the schema fields; orchestrator population lands in #2548 slices 3-4.)") so the doc is self-describing about what is and isn't wired.

Same concern, lighter touch, on the plan.md blockquote: it tells planners they "may emit" context_title / context_description to give the context PR a different framing — but until slices 3-4 land, those values flow through the parser into PRMetadata and then sit inert. A planner who emits them now is not breaking anything (the fields are optional, the parser warns on type errors, the populator passes them through). Still, a one-line forward-pointer would prevent the "I emitted these and the PR title didn't change" head-scratcher between merges.

This is doc clarity, not code correctness — the slice boundaries and the field's forward-compatible inertness make the gap harmless. Non-blocking.

Pre-existing terminology inconsistency in docs/guides/sdlc-pipeline.md

Pre-existing, but the PR is editing the example block right next to it: docs/guides/sdlc-pipeline.md:454 still uses "phases": [...] with "phase-1" IDs in its contract example, while docs/architecture/sdlc-pipeline.md:89 uses the canonical post-#2137 "slices": [...] with "slice-1" IDs. The two example contracts now both read "schemaVersion": "1.1" but disagree on which top-level container key is canonical, which is a minor source of confusion for someone reading both docs.

The review rules call out pre-existing issues in code a PR modifies as worth fixing in scope. Here the scope is doc-fixed (the YAML examples are the entire content of the changed block), and the fix is mechanical (phasesslices, phase-1slice-1 ID strings). I'd flag it as a follow-up rather than block on it — fixing it touches a much larger area of sdlc-pipeline.md than the slice-1 schema-version delta justifies, and #2137 should arguably own a sweep across all guides at once.

Deferred follow-up — still tracked

#2560 (defense-in-depth ParseWarning when planner YAML emits context_branch / context_pr_number) remains the right home for the planner-keys-rejection work. No change.

What I checked beyond the diff

  • Re-walked PRMetadata's field surface to confirm no other runtime-populated field has been added since the prior re-review that would now be silently dropped. Field surface is unchanged from 5a6209ff9de4bc1b.
  • Confirmed the migration shim's if self.schemaVersion == "1.0": exact-match guard matches the doc claim "when the on-disk value is exactly \"1.0\"". ✓
  • Confirmed no other call sites construct contract.pr = PRMetadata(...) or rebuild pr wholesale beyond the two sites already covered (the populator at pipelines.py:14943 and the create path at decisions.py).

Approving.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

#2548)

Slice-1 (foundation) tester NACK: on the egg/issue-2548/work merge target,
slice-1's extract_pr_context_metadata_from_yaml + ParseResult.pr_context_*
plumbing stacks on top of #2527's validate_task_role_alignment additions,
pushing shared/egg_contracts/plan_parser.py to ~1,530 lines and breaching
the 1,500-line hard cap that scripts/check-file-sizes.py enforces. The
slice-1 branch alone is at 1,388 lines (clean), but the work-branch state
that the lint actually runs against is over.

Fix per reviewer_contract's forward-looking concern and tester's blocking
finding: add the file to scripts/file-size-allowlist.yaml under #2548 so
make lint passes during the slice-1 BRC. Decomposition is tracked under
the same issue and is the cheaper of the two unblock options for slice-1
(decomposing in-cycle would expand scope and risk slice-2/3/4's
dependency on the current plan_parser.py public surface).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
egg-reviewer Bot added 2 commits May 8, 2026 04:25
…2548)

Both blockquotes now disclose that slice-1 lands only the schema fields
plus the planner-prompt advertisement — the orchestrator branch-creation
and PR-opening hooks land in #2548 slices 3-4. Until those slices merge
the four pr.context_* fields are forward-compatibly inert: a planner
emitting context_title / context_description has those values flow into
PRMetadata, but nothing acts on them yet, so reviewers and planners
reading the merged-but-pre-slice-3-4 docs see exactly what is and is
not wired today rather than reading the eventual contract as
present-tense.

Addresses non-blocking suggestion in PR #2555 review.
@james-in-a-box

james-in-a-box Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Review response

Thanks for the re-review and approval. Disposition per non-blocking item below.

Non-blocking

  • Forward-looking blockquotes don't disclose the slice-1 boundaryfixed-in-PR (commit 70d3d41). Both blockquotes (docs/architecture/sdlc-pipeline.md Schema 1.1 and docs/templates/plan.md Context-PR framing) now include a forward-pointer making the slice-1 boundary explicit: slice-1 lands the schema fields and the planner-prompt advertisement; the orchestrator branch-creation and PR-opening hooks land in Slice PRs are missing analysis/plan docs and all BRC history; need a 'context' PR for refine+plan phases plus per-slice BRC in each slice PR #2548 slices 3-4. Until those slices merge the four pr.context_* fields are forward-compatibly inert — a planner emitting context_title / context_description has the values flow through the parser into PRMetadata, but nothing acts on them yet, so the rendered PR doesn't change. Reviewers and planners reading the merged-but-pre-slice-3-4 docs now see exactly what is and isn't wired today rather than reading the eventual contract as present-tense.

  • Pre-existing terminology inconsistency in docs/guides/sdlc-pipeline.md (phases/phase-N vs canonical slices/slice-N)deferred-to #2568. You explicitly framed this as "I'd flag it as a follow-up rather than block on it" citing scope: "fixing it touches a much larger area of sdlc-pipeline.md than the slice-1 schema-version delta justifies, and Independent implement phases #2137 should arguably own a sweep across all guides at once." Filed docs: sweep docs/guides/sdlc-pipeline.md (and other guides) from phases/phase-N to slices/slice-N (#2137 follow-up) #2568 as the tracking issue for the sweep across docs/guides/sdlc-pipeline.md (and any sibling guide / reference docs carrying the same legacy terminology), with the #2137 parent linkage you suggested.

Deferred follow-up — still tracked

#2560 (defense-in-depth ParseWarning for planner-emitted pr.context_branch / pr.context_pr_number) remains open from the prior review round.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Adds 12 adversarial probes to the slice-1 test file as the tester role's
contribution to slice-1 task-1-2:

- model_dump_json round-trip preserves all four context_* fields
- model_dump_json preserves None as JSON null (no exclude_none drift)
- Combined phases:->slices: + schemaVersion 1.0->1.1 migration in one load
- YAML null (~) for context_title / context_description threads as None
- parse_plan markdown-only path (no yaml fence) yields None context fields
- list-typed context_title and context_description warn (mirrors int/dict)
- CRLF + mixed whitespace stripping for context strings
- context_pr_number accepts large ints (no implicit int32 ceiling)
- schemaVersion regex rejects "1.0-rc1" / "v1.0"
- 1.0 payload with explicit context fields still loads + bumps to 1.1
- non-dict pr: block short-circuits the context extractor (no AttributeError)

All 42 tests in tests/shared/egg_contracts/test_pr_metadata.py pass.
ruff check / format are clean. The wider lint and test suites are
verified separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review summary

Re-review against the delta from 9de4bc1b81170228. Three PR-authored commits since the prior approval:

  • 70173947 — adds shared/egg_contracts/plan_parser.py to scripts/file-size-allowlist.yaml (config-only unblock for the work-branch lint).
  • 70d3d418 — adds slice-1 boundary forward-pointers to the two pr.context_* blockquotes in docs/architecture/sdlc-pipeline.md and docs/templates/plan.md (addresses the non-blocking forward-pointer note from the prior review).
  • 81170228 — adds 12 adversarial unit tests in tests/shared/egg_contracts/test_pr_metadata.py covering JSON round-trips, combined phases:slices: + 1.01.1 migration, YAML-null threading, list-typed warnings for context_title / context_description, CRLF stripping, large context_pr_number, schemaVersion regex strictness, and short-circuit on non-dict pr: blocks.

No agent-mode design concerns. The delta is config + docs + tests — nothing touches agent prompts, pre-fetching, output structuring, post-processing pipelines, model identifiers, or direct API calls. The forward-pointer doc edits are correctly scoped (orientation about what is and isn't wired in slice-1, not constraints on agent behavior).

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review summary

Re-review against commit 81170228d. The delta since my prior approval at 9de4bc1b is one merge commit (2f9bef37) bundling three pieces of work — the forward-pointer docs (anticipated in the prior response thread), a preemptive file-size allowlist entry, and 12 adversarial tests — plus the new tester-role commit (81170228). All 42 tests in tests/shared/egg_contracts/test_pr_metadata.py pass locally (12 new + 30 prior). Approving.

What was new since the prior review

1. scripts/file-size-allowlist.yaml entry for shared/egg_contracts/plan_parser.py (commit 70173947). Preemptive grandfathering for the post-merge state. The line-count math holds: slice-1 alone is 1,388 lines (under the 1,500-line hard cap); the merge target egg/issue-2548/work already carries #2527's validate_task_role_alignment additions (+142 lines, file at 1,426 on work), so the merged result lands at ~1,511–1,530 depending on overlap, breaching the cap. The allowlist is the established escape hatch, the comment is detailed, and the cap check on the PR head itself still passes (1,388 < 1,500). Correct mechanism, correctly scoped.

2. TestPRMetadataAdversarial — 12 probes in tests/shared/egg_contracts/test_pr_metadata.py:592-901 (commit 81170228). Spot-checked each test against the production code:

  • test_model_dump_json_round_trip_preserves_all_context_fields — round-trips through model_dump_json + model_validate_json, exercises the on-disk path. ✓
  • test_model_dump_json_preserves_null_context_fields — pins None → JSON null (no accidental future exclude_none=True). ✓
  • test_combined_phases_and_schemaversion_migration — exercises both the mode="wrap" _migrate_phases_to_slices validator (models.py:744) and the mode="after" _migrate_schema_version_to_1_1 validator (models.py:812) in one load. Verifies the slice-N ID rewrites and the dependency-edge rewrite alongside the schemaVersion bump. ✓
  • test_yaml_null_for_context_fields_yields_none — YAML ~ and null thread through parse_planextract_pr_context_metadata_from_yaml's if raw_title is not None: guard at plan_parser.py:962-973 rather than _normalize_optional_string. Pins the absent-vs-empty distinction. ✓
  • test_parse_plan_markdown_only_yields_none_context — markdown-regex fallback path (no yaml-tasks fence) leaves pr_context_* as None because yaml_data is None at plan_parser.py:1201. Pins that the markdown path doesn't accidentally initialise to "". ✓
  • test_extract_warns_on_list_typed_context_title / _context_description — covers the list branch missing from the prior coverage (which had int + dict for description and int for title). The production path is the isinstance(raw_*, str) check in plan_parser.py:962-1007. Both warning-message substring assertions match the production format string. ✓
  • test_extract_handles_crlf_whitespace_in_context_fields — CRLF stripping via .strip() (which strips \r\n); internal \n preserved. Test assertion desc == "multi-line \n body" matches the production trace exactly. ✓
  • test_context_pr_number_accepts_large_int — pins that the ge=1 validator on context_pr_number doesn't grow a le=2**31-1 ceiling. ✓
  • test_invalid_schemaversion_format_rejected — pins the pattern=r"^[0-9]+\.[0-9]+$" regex on Contract.schemaVersion against "1.0-rc1" and "v1.0". ✓
  • test_legacy_1_0_with_explicit_context_fields_loads — pins that the _migrate_schema_version_to_1_1 after-validator is additive: a hand-edited 1.0 contract with explicit context_* values bumps to 1.1 without erasing them. ✓
  • test_extract_returns_none_when_pr_block_is_non_dict — pins the short-circuit at plan_parser.py:945-948 (pr_data is a list, not a dict) — no AttributeError, returns (None, None, []). The duplicate-warning suppression vs. extract_pr_metadata_from_yaml is correctly scoped. ✓

All 12 tests go through the production helpers (extract_pr_context_metadata_from_yaml, parse_plan, Contract.model_validate, PRMetadata constructor + model_dump_json) — no hand-built fixtures bypass the production path, no name-vs-behaviour contradictions. The phases: key + id: 1 (int) shape in test_yaml_null_for_context_fields_yields_none is fine for parse_plan's phase extractor, which is more lenient than Contract.model_validate's wrap-mode validator; the test isn't claiming anything about phase id-type strictness.

3. Forward-pointer blockquotes (commit 70d3d418). Already verified in the prior review — the slice-1 boundary is now explicit in both docs/architecture/sdlc-pipeline.md:130-135 and docs/templates/plan.md:144-149.

Cross-cutting checks

  • Re-verified _populate_contract_from_plan (orchestrator/routes/pipelines.py:14938-14953) still preserves deferred_actions alongside context_branch / context_pr_number, with the comment block accurately enumerating which fields are runtime-populated. No regression on the original blocking concern. ✓
  • Re-walked PRMetadata's field surface — no new fields added since 9de4bc1b, so no other runtime-populated field is silently dropped. ✓

Non-blocking

Allowlist comment cites no decomposition tracker

scripts/file-size-allowlist.yaml:58 reads "decompose in a follow-up" but doesn't cite a follow-up issue number. gh issue list --search "plan_parser" returns empty. The other allowlist entries follow the same pattern (cite the implementing issue, not a decomposition tracker), so it's not a regression — but plan_parser.py's at 1,530+ lines is meaningfully past the cap, and an explicit decomposition issue would be lighter-weight than threading the work into a future #2548 slice. Filing a tracking issue (e.g., "Decompose shared/egg_contracts/plan_parser.py — split YAML/markdown extractors and context-PR plumbing") would be cheap insurance against the file drifting further before someone gets to it.

test_extract_warns_on_list_typed_context_title docstring drift

tests/shared/egg_contracts/test_pr_metadata.py:741-749 (the title variant; the description variant at test_pr_metadata.py:765-773 is fine):

"A list (e.g. a planner that confused context_title with files_affected) hits a different branch of pydantic's str coercion machinery"

The production check at plan_parser.py:962-963 is a plain isinstance(raw_title, str) guard in the parser, not a pydantic validator on PRMetadata. Pydantic isn't involved in this code path at all — the warning fires before the value reaches the model. The description-variant docstring says it correctly ("would round-trip through _normalize_optional_string"); the title docstring is the only one with the misleading "pydantic str coercion" wording. Doesn't change observable behaviour, but a future maintainer chasing the test by class name would land in the wrong layer.

Forward-pointer blockquote in docs/architecture/sdlc-pipeline.md is parenthetical

The slice-1-boundary disclosure in docs/architecture/sdlc-pipeline.md:130-135 is wrapped in parentheses. Combined with the leading sentence's present-tense framing ("are populated by the orchestrator"), a quick skim still reads the mechanism as live. Promoting the parenthetical to a follow-on sentence (or a short "As of slice-1 (#2548 part 1):" lead-in) would make the inertness harder to miss. Same lighter-touch concern on docs/templates/plan.md:144-149. Doc-clarity nit, not blocking.

Deferred follow-ups — still tracked

  • #2560 (defense-in-depth ParseWarning for planner-emitted pr.context_branch / pr.context_pr_number) — open.
  • #2568 (sweep phases/phase-Nslices/slice-N across docs/guides/sdlc-pipeline.md per #2137) — open.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

…lowlist tracker

- Promote slice-1 forward-pointer blockquote from parenthetical to a
  follow-on bold-led paragraph in docs/architecture/sdlc-pipeline.md and
  docs/templates/plan.md so the schema-vs-orchestrator inertness is
  harder to miss on a quick skim.
- Reword test_extract_warns_on_list_typed_context_title docstring to
  cite the parser-layer isinstance(raw_title, str) guard instead of
  pydantic's str coercion machinery — pydantic is not in this code path.
- Cite #2569 (plan_parser.py decomposition tracker) from the
  scripts/file-size-allowlist.yaml entry comment instead of an
  uncited "follow-up".
@james-in-a-box

james-in-a-box Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Review response

Thanks for the re-review and approval. All three non-blocking items are addressed in 5ace86d.

Non-blocking

  • Allowlist comment cites no decomposition trackerfixed-in-PR (commit 5ace86d). Filed Decompose shared/egg_contracts/plan_parser.py — split YAML/markdown extractors and context-PR plumbing #2569 ("Decompose shared/egg_contracts/plan_parser.py — split YAML/markdown extractors and context-PR plumbing") with suggested seams (YAML extractors, markdown extractors, ParseResult/ParseWarning dataclasses, parse_plan dispatcher, context-PR plumbing) and explicit acceptance criteria (file back under the 1,500-line cap on main, allowlist entry removed, all call sites and the 42-test test_pr_metadata.py continue to pass). Updated scripts/file-size-allowlist.yaml:53-61 to read "decompose under Decompose shared/egg_contracts/plan_parser.py — split YAML/markdown extractors and context-PR plumbing #2569" instead of the uncited "decompose in a follow-up".

  • test_extract_warns_on_list_typed_context_title docstring driftfixed-in-PR (commit 5ace86d). Reworded the docstring at tests/shared/egg_contracts/test_pr_metadata.py:750-763 to cite the parser-layer isinstance(raw_title, str) guard at extract_pr_context_metadata_from_yaml rather than "pydantic's str coercion machinery" — pydantic is not involved in this code path; the warning fires before the value reaches PRMetadata. The description-variant docstring already had the correct framing (would round-trip through _normalize_optional_string); the title variant now matches.

  • Forward-pointer blockquote in docs/architecture/sdlc-pipeline.md (and docs/templates/plan.md) is parentheticalfixed-in-PR (commit 5ace86d). Promoted both blockquotes' slice-1 boundary disclosures from a trailing parenthetical to a follow-on bold-led paragraph (**As of slice-1 (#2548 part 1)**, only the schema fields and the planner-prompt advertisement are wired. The orchestrator branch-creation and PR-opening hooks land in #2548 slices 3-4 — until those slices merge, the four pr.context_*fields are forward-compatibly inert: planners may emitcontext_title/context_descriptionand the values flow intoPRMetadata, but nothing acts on them yet.). The bold lead-in and paragraph break make the inertness boundary harder to miss on a quick skim than the prior parenthetical packed into the closing sentence.

Deferred follow-ups — still tracked

  • #2560 (defense-in-depth ParseWarning for planner-emitted pr.context_branch / pr.context_pr_number) — open from the first review round.
  • #2568 (sweep phases/phase-Nslices/slice-N across docs/guides/sdlc-pipeline.md per Independent implement phases #2137) — open from the second-to-last review round.
  • #2569 (decompose shared/egg_contracts/plan_parser.py) — newly filed in this round.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review summary

Re-review against the delta from 811702285ace86d3. The single PR-authored commit since my prior approval addresses the three non-blocking nits raised by the general-code reviewer:

  • docs/architecture/sdlc-pipeline.md and docs/templates/plan.md — slice-1 boundary disclosure promoted from a trailing parenthetical to a bold-led follow-on paragraph in each blockquote.
  • scripts/file-size-allowlist.yaml — comment updated from "decompose in a follow-up" to "decompose under #2569" (the newly-filed decomposition tracker).
  • tests/shared/egg_contracts/test_pr_metadata.pytest_extract_warns_on_list_typed_context_title docstring reworded to cite the parser-layer isinstance(raw_title, str) guard rather than "pydantic's str coercion machinery", aligning with the description-variant docstring's framing.

No agent-mode design concerns. The delta is docs + a config-comment cite + a test-docstring correction — nothing touches agent prompts, pre-fetching, output structuring, post-processing pipelines, rigid procedures, prompt-level security, model identifiers, or direct API calls. The bold-led paragraph framing makes the slice-1 inertness boundary harder to miss on a quick skim, which is orientation, not constraint.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review summary

Re-review against the delta from 81170228d5ace86d3. The single new commit 5ace86d3 is a polish-only diff addressing the three non-blocking suggestions from my previous re-review:

  • scripts/file-size-allowlist.yaml:58 — comment now cites the decomposition tracker (#2569) instead of an uncited "follow-up". I confirmed #2569 is open and contains the decomposition seams + acceptance criteria I asked for.
  • docs/architecture/sdlc-pipeline.md:127-145 and docs/templates/plan.md:135-153 — the slice-1 boundary disclosure is promoted from a trailing parenthetical to a follow-on **As of slice-1 (#2548 part 1)**-led paragraph. The bold lead-in and paragraph break do make the inertness boundary harder to miss on a quick skim than the prior parenthetical.
  • tests/shared/egg_contracts/test_pr_metadata.py:750-763 — the title-variant docstring no longer cites "pydantic's str coercion machinery"; it now points at the parser-layer isinstance(raw_title, str) guard in extract_pr_context_metadata_from_yaml.

No code paths changed. All three deferred follow-ups (#2560, #2568, #2569) remain tracked.

Approving with one minor non-blocking docstring observation.

Non-blocking

test_extract_warns_on_list_typed_context_title docstring still cites _normalize_optional_string for a code path that doesn't call it

tests/shared/egg_contracts/test_pr_metadata.py:756-758 now reads:

"A list ... would round-trip through _normalize_optional_string as "['a', 'b']" if the isinstance(raw_title, str) guard in extract_pr_context_metadata_from_yaml regressed."

The description-variant docstring is correct — the description branch at plan_parser.py:1006 does call _normalize_optional_string(raw_description). But the title branch at plan_parser.py:973-974 does not:

else:
    stripped = raw_title.strip()
    context_title = stripped if stripped else None

If the isinstance(raw_title, str) guard regressed and a list reached the else branch, ["a", "b"].strip() would raise AttributeError, not silently coerce via str(). So the "round-trip through _normalize_optional_string as "['a', 'b']"" framing is the wrong regression mode for the title branch — it's the description branch's regression mode.

A more accurate framing would call out the actual guard the test pins (parser-layer isinstance before the str.strip() call) without claiming the silent-coerce path that doesn't exist on this branch. Something like:

"Without the isinstance(raw_title, str) guard the value would either raise AttributeError on .strip() or — if a future refactor swapped in _normalize_optional_string for symmetry with the description branch — silently coerce as "['a', 'b']". Either way the warning path needs to fire first; pin it explicitly."

This is doc-drift on a test docstring; the test itself (asserting title is None, warning fires, message contains "list") is correct and pins the right behavior. Wording fix only, not a code issue.

Deferred follow-ups — still tracked

  • #2560 (defense-in-depth ParseWarning for planner-emitted pr.context_branch / pr.context_pr_number) — open from the first review round.
  • #2568 (sweep phases/phase-Nslices/slice-N across docs/guides/sdlc-pipeline.md per #2137) — open from the second-to-last review round.
  • #2569 (decompose shared/egg_contracts/plan_parser.py) — newly filed in the prior round; verified open with the seams + acceptance criteria I requested.

What I checked beyond the diff

  • Re-walked _populate_contract_from_plan (orchestrator/routes/pipelines.py:14938-14953) to confirm the deferred_actions preservation block is intact. ✓
  • Confirmed PRMetadata's field surface is unchanged from 81170228d5ace86d3 — no new runtime-populated fields that could be silently dropped. ✓
  • Confirmed #2569 exists, is open, and the linked acceptance criteria match the allowlist comment's claim. ✓

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

james-in-a-box Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor Author
egg is addressing review feedback...

@jwbron
jwbron merged commit e0e3279 into egg/issue-2548/work May 8, 2026
29 checks passed
jwbron added a commit that referenced this pull request May 8, 2026
* Add PRMetadata.context_* fields + planner prompt updates (#2548)

slice-1 / task-1-1 + task-1-3 — the foundation slice for the context-PR
mechanism. Subsequent slices build the gateway primitive, the
orchestrator hook, and the slice-1 base rewiring on top of these
fields.

Schema 1.1 — extends ``PRMetadata`` with four optional fields:
- ``context_title`` / ``context_description`` — planner-emitted
  framing for the dedicated context PR (e.g. "Strategic plan for #N"
  vs the slice's "Implement …"). Both fall back to ``title`` /
  ``description`` when omitted.
- ``context_branch`` / ``context_pr_number`` — orchestrator-populated
  runtime values (the ``egg/<id>/context`` branch name and the GitHub
  PR number once the context PR has been opened). Planners must NOT
  emit these.

Bumps ``Contract.schemaVersion`` default from ``"1.0"`` to ``"1.1"``
and adds an ``after``-mode migration shim that promotes pre-1.1
contracts to 1.1 on load. The bump is purely additive — pre-1.1 JSON
loads cleanly with the new fields defaulting to ``None``.

Plan-parser plumbing — ``ParseResult`` grows ``pr_context_title`` /
``pr_context_description`` and a new ``extract_pr_context_metadata_from_yaml``
helper extracts the optional keys without breaking the existing
``extract_pr_metadata_from_yaml`` 5-tuple signature (and the
~10 callers + tests that unpack it).

Planner prompt — both planner-prompt sites in ``pipelines.py`` (the
plan-phase prompt under ``_build_phase_prompt`` and the
task_planner-role prompt under ``_build_agent_prompt``) gain the
``_PR_CONTEXT_GUIDANCE`` paragraph and the ``_PR_CONTEXT_YAML_EXAMPLE_LINES``
commented-out hints inside the ``pr:`` YAML block. Both helpers are
defined once next to ``_PR_DESCRIPTION_GUIDANCE`` so the two prompt
sites stay in sync when the guidance evolves.

Contract populator — ``_populate_contract_from_plan`` now copies
``result.pr_context_title`` / ``pr_context_description`` onto the new
PRMetadata it builds, and preserves any orchestrator-populated
``context_branch`` / ``context_pr_number`` across re-populates so a
later plan re-parse does not blow away runtime state set by slice-3's
hook.

Test impact: bumping the default ``schemaVersion`` to ``"1.1"`` causes
``tests/shared/egg_contracts/test_models.py::test_minimal_contract``
to fail on the literal ``"1.0"`` assertion. The fix-up belongs to
the tester role (task-1-2) along with the new ``PRMetadata.context_*``
round-trip coverage; coder boundaries forbid pushing test edits.
Lint (ruff format + check) and mypy delta are clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add PRMetadata.context_* test coverage + 1.0→1.1 migration tests (#2548)

slice-1 / task-1-2 — adversarial + regression coverage for the four new
optional ``PRMetadata.context_*`` fields and the ``schemaVersion``
1.0→1.1 promotion shim added by the coder in commit 75d8ca0.

Coverage:
* ``TestPRMetadataContextFields`` — defaults to None, full round-trip
  with all four fields populated, omitted-keys round-trip preserves
  None.
* ``TestPRMetadataContextPRNumberValidator`` — pins the ``ge=1``
  validator: 0/-1 are rejected at construct AND at setattr (under the
  shared ``EggContractBaseModel.validate_assignment=True`` from #2490);
  None and large positive ints accepted.
* ``TestPRMetadataSchemaVersionMigration`` — 1.0 payload loads with
  context defaults, dump→reload chain stays at 1.1, default is 1.1,
  legacy ``deferred_actions`` survive migration, and an unrecognized
  version (1.2 / 2.0) is NOT silently downgraded.
* ``TestPRMetadataContextEmptyStringSemantics`` — empty strings are
  accepted at the model layer so the orchestrator hook's
  ``context_title or title`` fallback works for both None and "".
* ``TestPlanParserContextFieldExtraction`` — covers task-1-3's
  ingestion path: ``extract_pr_context_metadata_from_yaml`` returns
  None pair for missing/None/absent inputs; collapses whitespace to
  None; warns on non-string ``context_title``; ``parse_plan`` threads
  the values onto ``ParseResult.pr_context_*``.

Also updates ``test_models.py::test_minimal_contract`` from the literal
``"1.0"`` schemaVersion assertion to ``"1.1"`` — the coder flagged this
as a known follow-up in commit 75d8ca0 (coder cannot push test edits
under the role boundary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review feedback on PR #2555 (#2548)

Blocking fix:
- _populate_contract_from_plan now also preserves PRMetadata.deferred_actions
  alongside context_branch / context_pr_number. The conditional-ACK gate at
  decisions.py:complete_phase writes deferred actions; the populator's
  start_phase=implement re-entry path was silently wiping them, erasing
  the merge-blocking Pre-merge Obligations handoff. Add a regression
  test in orchestrator/tests/test_short_flow_contract_population.py.

Non-blocking improvements:
- extract_pr_context_metadata_from_yaml now warns symmetrically on
  non-string context_description (mirrors the context_title branch),
  preventing silent str() coercion of structured planner values.
- Updated schemaVersion / _migrate_schema_version_to_1_1 docstrings to
  reflect that the bump fires at every load (mode="after"), not lazily on
  next save, and to acknowledge the migration is silent (no audit entry).
- Aligned TestPRMetadataContextEmptyStringSemantics docstring with reality
  (planner path collapses empty strings to None; only hand-edited or
  migrated payloads can produce a "" PRMetadata).
- New tests for the symmetric context_description warning.

* docs: document schema 1.1 and pr.context_* fields (#2548)

Slice-1 lands the schema delta + planner-prompt update half of the
context-PR mechanism (#2548): `PRMetadata` grows four optional
`context_*` fields and `Contract.schemaVersion` defaults to `"1.1"`
with an additive `1.0 → 1.1` migration. The actual context-PR
mechanism (branch creation, PR opening, slice-1 base wiring) is
implemented in slices 3-4 and gets its own end-to-end documentation
pass in slice-5.

This commit updates the docs that reference contract examples and the
yaml-tasks `pr:` block so they reflect the slice-1-landed schema
state:

- `docs/templates/plan.md`: add optional `context_title` /
  `context_description` keys to the yaml-tasks `pr:` example as
  commented-out hints, plus a new prose blockquote explaining when
  planners may emit them and which sibling fields
  (`context_branch`, `context_pr_number`) are orchestrator-populated.
- `docs/architecture/sdlc-pipeline.md`: bump the example
  `schemaVersion` from `1.0` to `1.1` and add a "Schema 1.1 (#2548)"
  blockquote summarising the additive migration.
- `docs/guides/sdlc-pipeline.md`: same `schemaVersion` bump in the
  example JSON plus a short blockquote pointing readers at the
  migration semantics.

The PR-stack diagrams, BRC-history file naming, and slice-1-base
discussion in `docs/guides/concurrent-execution.md`,
`docs/architecture/orchestrator.md`, `docs/reference/orchestrator-cli.md`,
and `docs/guides/babysit-pr.md` remain untouched — those describe
behavior that does not yet exist on this branch and are slice-5's
responsibility once the mechanism is wired end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Allowlist plan_parser.py for file-size hard cap on egg/issue-2548/work (#2548)

Slice-1 (foundation) tester NACK: on the egg/issue-2548/work merge target,
slice-1's extract_pr_context_metadata_from_yaml + ParseResult.pr_context_*
plumbing stacks on top of #2527's validate_task_role_alignment additions,
pushing shared/egg_contracts/plan_parser.py to ~1,530 lines and breaching
the 1,500-line hard cap that scripts/check-file-sizes.py enforces. The
slice-1 branch alone is at 1,388 lines (clean), but the work-branch state
that the lint actually runs against is over.

Fix per reviewer_contract's forward-looking concern and tester's blocking
finding: add the file to scripts/file-size-allowlist.yaml under #2548 so
make lint passes during the slice-1 BRC. Decomposition is tracked under
the same issue and is the cheaper of the two unblock options for slice-1
(decomposing in-cycle would expand scope and risk slice-2/3/4's
dependency on the current plan_parser.py public surface).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: add slice-1 boundary forward-pointer to context-PR blockquotes (#2548)

Both blockquotes now disclose that slice-1 lands only the schema fields
plus the planner-prompt advertisement — the orchestrator branch-creation
and PR-opening hooks land in #2548 slices 3-4. Until those slices merge
the four pr.context_* fields are forward-compatibly inert: a planner
emitting context_title / context_description has those values flow into
PRMetadata, but nothing acts on them yet, so reviewers and planners
reading the merged-but-pre-slice-3-4 docs see exactly what is and is
not wired today rather than reading the eventual contract as
present-tense.

Addresses non-blocking suggestion in PR #2555 review.

* Add adversarial PRMetadata.context_* tests (#2548)

Adds 12 adversarial probes to the slice-1 test file as the tester role's
contribution to slice-1 task-1-2:

- model_dump_json round-trip preserves all four context_* fields
- model_dump_json preserves None as JSON null (no exclude_none drift)
- Combined phases:->slices: + schemaVersion 1.0->1.1 migration in one load
- YAML null (~) for context_title / context_description threads as None
- parse_plan markdown-only path (no yaml fence) yields None context fields
- list-typed context_title and context_description warn (mirrors int/dict)
- CRLF + mixed whitespace stripping for context strings
- context_pr_number accepts large ints (no implicit int32 ceiling)
- schemaVersion regex rejects "1.0-rc1" / "v1.0"
- 1.0 payload with explicit context fields still loads + bumps to 1.1
- non-dict pr: block short-circuits the context extractor (no AttributeError)

All 42 tests in tests/shared/egg_contracts/test_pr_metadata.py pass.
ruff check / format are clean. The wider lint and test suites are
verified separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address slice-1 review nits: doc forward-pointers, test docstring, allowlist tracker

- Promote slice-1 forward-pointer blockquote from parenthetical to a
  follow-on bold-led paragraph in docs/architecture/sdlc-pipeline.md and
  docs/templates/plan.md so the schema-vs-orchestrator inertness is
  harder to miss on a quick skim.
- Reword test_extract_warns_on_list_typed_context_title docstring to
  cite the parser-layer isinstance(raw_title, str) guard instead of
  pydantic's str coercion machinery — pydantic is not in this code path.
- Cite #2569 (plan_parser.py decomposition tracker) from the
  scripts/file-size-allowlist.yaml entry comment instead of an
  uncited "follow-up".

* [slice-2] Add context PR + per-slice BRC history (closes #2548) (#2564)

* Per-slice implement-phase BRC history (#2548 slice-2)

Switches the implement-phase BRC writer to per-slice files
`<id>-implement-{slice_id}.{md,json}` (one per slice) and drops the
aggregate `<id>-implement.{md,json}` filename — hard switchover under
D4 (no aggregate file is produced).

Refactor:

- `_write_brc_history()` now partitions implement-phase BRC messages
  by `metadata['slice_id']` and writes one file per slice via the new
  `_write_brc_history_file()` helper. Refine, plan, and pr phases keep
  the aggregate `<id>-{phase}.{md,json}` filename. Implement messages
  without `slice_id` are dropped with a single aggregate WARNING — the
  partitioning is mandatory under D4.
- A new `_render_brc_history_markdown()` helper carries the
  byte-identical markdown rendering (idempotency invariant from #1714)
  shared between the aggregate and per-slice writers.
- Existing callers (`_rewrite_brc_history_for_pr`,
  `_persist_phase_brc_history`, the inline call in `_run_pipeline`)
  delegate to `_write_brc_history()` unchanged — partitioning is
  internal to the writer.
- The pipeline-identifier-scoped staging glob in
  `_commit_statefiles_to_worktree()` already picks up the new
  `<id>-implement-slice-<N>.{md,json}` filenames (prefix-anchored on
  the issue/pipeline id).

Test fixture updates (seeding `slice_id=slice-1` on existing
implement-phase fixtures and rewriting aggregate-file assertions to
the per-slice shape) are deferred to task-2-3 (tester role) — those
test paths are gateway-blocked for the coder role per the BRC file
boundaries. The tester will re-align fixtures and add net-new coverage
for the multi-slice writer, the missing-`slice_id` WARNING, and the
no-aggregate-file invariant in parallel.

Closes task-2-1 and task-2-2 in slice-2 of #2548.

* Validate slice_id against SLICE_ID_PATTERN before file write (#2548 sec fix)

Addresses reviewer_security NACK: `metadata['slice_id']` was
interpolated directly into the on-disk filename of the per-slice BRC
history file, with no validation against the canonical
``^slice-[0-9]+$`` shape. Any sandbox agent can post arbitrary
metadata via the generic message-send endpoint
(`orchestrator/routes/messages.py:202`), so a malicious
`metadata.slice_id = "../../etc/foo"` would have written under
`worktree/.egg-state/etc/foo.{md,json}` — escaping the intended
brc-history directory and clobbering arbitrary state files (contracts,
plan drafts, other slices' BRC files).

Fix: import ``SLICE_ID_PATTERN`` from ``slice_id_validation`` (the
shared allowlist that already gates every other gateway-facing seam
where slice_id is interpolated — signal handlers #2403, restart route
#2410, branch builders) and reject any message whose
``metadata['slice_id']`` is not a string fullmatching the canonical
pattern. Rejected messages fold into the same ``unattributed``
counter and the same single aggregate WARNING that handles
missing-slice_id messages.

This puts the new file-path call site on the same allowlist as every
other use of slice_id, satisfying the invariant called out in
``slice_id_validation.py``'s module docstring: *"a future caller that
forgets the upstream regex must not be able to smuggle path
separators or shell metacharacters into a tracker registry key, a
Job name, or a worktree id."* The brc-history file path is now the
fourth call site to honor it.

* Address holistic NACK: tag CONSENSUS_* with slice_id, preserve babysit_pr (#2548)

Addresses reviewer_code_holistic NACK on v2 with three blocking
findings:

1. **Cross-module synthetic-key audit**: producers (CONSENSUS_PROPOSE/
   ACK/NACK/RE_REVIEW/WITHDRAW handlers in `routes/signals.py`) did
   NOT attach `slice_id` to the message metadata they wrote — only
   `CONSENSUS_CONFIRMED` did, via `_slice_meta`. Under v2, that meant
   the implement-phase writer would drop nearly every real BRC
   message. Fix: extend each consensus signal handler to spread the
   same `_slice_meta = {"slice_id": slice_id} if slice_id is not None
   else {}` shape into the metadata of every CONSENSUS_* message it
   writes. The new asymmetry surfaces only at the writer (canonical
   slice_id is required there too — same regex used by every other
   gateway-facing seam) but the producer side now reliably tags every
   slice-scoped message.

2. **Babysit_pr regression**: babysit_pr pipelines have no slices and
   no message ever carries `slice_id`, so v2 dropped the entire BRC
   stream and produced no `pr-<N>-<sha>-implement.{md,json}` file
   (regression of the documented babysit_pr artifact in
   `skills/babysit-pr/SKILL.md`). Fix: the writer now auto-detects
   slice-aware vs aggregate mode by checking whether ANY message
   carries a canonical `slice_id`. If none do, the writer falls back
   to the aggregate `{identifier}-implement.{md,json}` filename —
   preserving babysit_pr semantics. If at least one does, partition
   per-slice and warn loudly about any unattributed siblings.

3. **Silent-fallback hunt**: the v2 drop branch logged a warning and
   silently produced no file. The new branch is no longer silent —
   when partition mode is engaged but some messages are unattributed,
   the warning includes the dropped count, the count of attributed
   messages, and a sample of message types that were dropped, so
   operators can diagnose tag asymmetry quickly. When NO messages
   have slice_id at all, the writer now falls through to the
   aggregate filename instead of dropping (see #2 above).

Also addresses reviewer_code_holistic non-blocking findings:

* `_build_brc_history_link_line()` now clusters per-slice implement
  files (`implement-slice-1`, `implement-slice-2`) at the canonical
  ``implement`` rank so the rendered link order matches the canonical
  phase order (refine → plan → implement[-slice-N] → pr).
* `_write_brc_history()` lead docstring rewritten to describe the
  per-slice / aggregate auto-detection and the babysit_pr fallback
  path.

* Address reviewer_code non-blocking notes: natural sort + tighter access (#2548)

Folds two non-blocking observations from reviewer_code's v2 NACK into
the v3 re-propose:

* `_write_brc_history()` partition loop: drop the `getattr(msg,
  "metadata", None) or {}` defensive guard. `Message.metadata` is a
  Pydantic `dict[str, Any]` field with `default_factory=dict`
  (`message_store.Message`), so it is always a dict at this point —
  the simpler `msg.metadata.get("slice_id")` is equivalent and
  removes a no-op `isinstance` branch.
* Iterate per-slice buckets in natural-sort order (integer suffix)
  rather than lexicographic. A 12-slice pipeline now writes its
  BRC files in `slice-1, slice-2, …, slice-12` order rather than
  the lexicographic `slice-1, slice-10, slice-11, slice-12, slice-2`.
  Every key has been SLICE_ID_PATTERN-validated by this point, so
  the integer parse is total.
* `_build_brc_history_link_line()` link rendering: per-slice files
  inside the implement cluster are now sorted by integer slice index
  too, so the PR-body legend reads `implement-slice-1,
  implement-slice-2, …, implement-slice-12` rather than the
  lexicographic order. Same total integer parse — the file glob
  could in theory produce a non-canonical name, so a malformed
  suffix sorts last within the cluster.

These were the non-blocking items in the reviewer_code v2 NACK that
sit cleanly alongside the v3 cross-module fix; folding them in one
commit avoids a follow-up cleanup churn.

* Per-slice implement-phase BRC history tests (#2548 slice-2)

Aligns the BRC-history test suite with the post-#2548 hard switchover
to per-slice implement-phase files (`<id>-implement-{slice_id}.{md,json}`)
and adds net-new coverage for the multi-slice writer, the missing-
`slice_id` WARNING path, and the no-aggregate-file invariant.

Existing tests:

- `test_brc_history.py` — `_make_brc_message` / `_make_brc_messages` now
  auto-stamp `metadata['slice_id']` for implement-phase fixtures so the
  hard-switchover writer keeps producing files. Aggregate-file path
  assertions (`42-implement.{md,json}`) are rewritten to the per-slice
  shape via a new `_implement_path()` helper. Link-line tests updated
  to use per-slice filenames in their stub writes.
- `test_brc_phase_propagation.py`, `test_diagnostic_logging_1633.py`,
  `test_pr_phase_brc_rewrite.py`, `test_conditional_ack.py` — same
  `slice_id` auto-stamping treatment for their own `_make_brc_message`
  helpers; aggregate-path assertions rewritten in lockstep.

New tests (`TestPerSliceImplementBrcHistory` and
`TestPerSliceImplementBrcHistoryRewriteForPr`):

- `test_writes_one_file_per_slice_no_aggregate` — N=2 slices yields two
  per-slice .md+.json pairs and zero aggregate files.
- `test_each_slice_file_contains_only_its_own_messages` — partitioning
  isolates buckets; cross-slice content leaks fail the test.
- `test_single_slice_still_uses_per_slice_filename` — N=1 still uses
  the per-slice naming (no special-case for the single-slice degenerate).
- `test_per_slice_file_carries_slice_label_in_header` — slice label is
  visible in the markdown header.
- `test_messages_without_slice_id_dropped_with_warning` — mix of
  attributed + unattributed: per-slice file written for the attributed
  set; unattributed messages dropped; a single warning carries the
  drop count.
- `test_all_messages_unattributed_no_files_no_aggregate` — when EVERY
  implement-phase message lacks a slice_id, no files are produced
  (no fall back to aggregate).
- `test_refine_phase_keeps_aggregate_filename`,
  `test_plan_phase_keeps_aggregate_filename`,
  `test_pr_phase_keeps_aggregate_filename` — regression: only implement
  partitions; refine/plan/pr keep the aggregate even when fixtures
  carry `slice_id` defensively.
- `test_partial_attribution_only_attributed_messages_get_files` —
  exact `dropped_count=1` accounting when one of three buckets is
  unattributed.
- `test_implement_messages_with_empty_slice_id_dropped` — empty-string
  slice_id is treated as missing (security-relevant: must NOT produce
  `42-implement-.md`).
- `test_three_slices_all_get_distinct_files` — N=3 sorted bucket walk;
  exercises deterministic order even on shuffled input.
- `test_idempotent_per_slice_write` — per-slice files are
  byte-identical across repeated writes (preserves #1714 invariant).
- `test_non_dict_metadata_is_treated_as_unattributed` — defensive
  guard around non-dict metadata produces a drop, not a crash.
- `test_rewrite_for_pr_emits_per_slice_implement_files` and
  `test_rewrite_for_pr_mixes_aggregate_refine_and_per_slice_implement`
  — the PR-phase safety-net rewrite (`_rewrite_brc_history_for_pr`)
  inherits the per-slice partitioning correctly; refine and implement
  shapes coexist in the same brc-history dir.

Closes task-2-3 in slice-2 of #2548.

* Adapt per-slice BRC history tests to v3 babysit fallback + SLICE_ID_PATTERN validation (#2548 slice-2)

Folds the v2→v3 coder behavior changes (commits beb2bae, 2a912c5,
70fe103) into the test plan:

- `test_all_messages_unattributed_no_files_no_aggregate` →
  `test_all_messages_unattributed_writes_aggregate_babysit_fallback`:
  v3 fixed reviewer_code_holistic finding #2 — when NO message in the
  store carries a canonical `slice_id`, the writer now falls back to
  the aggregate `<id>-implement.{md,json}` filename so non-slice
  pipelines (babysit_pr) keep producing the artifact documented in
  `skills/babysit-pr/SKILL.md`. Test was rewritten to assert this
  fallback path; complemented by `test_babysit_aggregate_fallback_contains_all_messages`
  which pins that the aggregate carries every BRC-eligible message.
- `test_non_dict_metadata_is_treated_as_unattributed` →
  `test_message_metadata_is_always_a_dict`: v3 dropped the defensive
  `getattr(msg, "metadata", None) or {}` guard now that
  `Message.metadata` is asserted as a Pydantic
  `dict[str, Any] = Field(default_factory=dict)` field. Test now pins
  the Pydantic invariant directly (default-factory yields {}, never
  None) so a future Pydantic-config change shows up here rather than
  as a runtime crash inside `_write_brc_history()`.
- `test_implement_messages_with_empty_slice_id_dropped` →
  `_treated_as_unattributed`: empty-string `slice_id` is now dropped
  via `SLICE_ID_PATTERN` validation rather than the falsy `if not
  slice_id`. Test mixes empty-slice_id with a canonical message so
  partition mode engages (otherwise we'd hit the babysit aggregate
  fallback) and asserts: (a) no `42-implement-.md` is ever produced
  (security: empty slice_id must not be interpolated into the per-slice
  stem), (b) the canonical slice-1 file IS produced, (c) drop warning
  carries `dropped_count=1`.

Net-new tests added to cover v3 behaviors:

- `test_invalid_slice_id_pattern_treated_as_unattributed`: defense-in-depth
  test for the new local SLICE_ID_PATTERN validation. Covers 9
  injection payloads (`../etc/passwd`, `slice-1/extra`, `phase-1`,
  `SLICE-1`, etc.) and asserts: (a) only the canonical slice-1 file
  exists, (b) no aggregate is written (partition mode is engaged), (c)
  no traversal — only the brc-history dir was created under
  `.egg-state/`, (d) drop warning's `dropped_count` matches the
  payload count exactly.
- `test_natural_sort_per_slice_iteration_order`: covers the v3
  reviewer_code non-blocking that switched bucket iteration from
  lexicographic to natural-sort by integer suffix. Patches
  `routes.pipelines.logger.info` to capture "Wrote BRC history file"
  calls and asserts the slice_id sequence is `slice-1, slice-2,
  slice-7, slice-11, slice-12` (not the lex order which would put
  slice-11 / slice-12 before slice-2).

72 tests in `test_brc_history.py` pass; 203 tests across the seven
BRC-history-related test files pass. `ruff check` clean.

* Address tester NACK: tag remaining 3 metadata sites with slice_id (#2548)

Closes the three remaining producer-side gaps the tester flagged on
v3:

1. `handle_producer_push_signal` auto-re-propose CONSENSUS_PROPOSE
   (lines 2110-2118): the auto-push re-propose path now spreads
   `**_slice_meta` into the metadata dict alongside the existing
   `auto_re_propose` / `trigger` / `commit_sha` / `version` /
   `changed_files` keys. Mirrors the manual re-propose path in
   `handle_consensus_propose_signal` patched in v3.
2. `handle_producer_push_signal` auto-re-propose CONSENSUS_RE_REVIEW
   notifications (lines 2125-2148): same fix — spread `**_slice_meta`
   into the per-reviewer notify metadata so the broadcast carries the
   partitioning key end-to-end.
3. `handle_consensus_resolve_obligation_signal` CONSENSUS_OBLIGATION_RESOLVED
   (lines 2017-2025): in-cycle conditional-ACK obligation resolution
   sits in BRC_HISTORY_TYPES and can fire during the implement phase
   with slice scope (typical case: tester satisfies a coder's
   conditional ACK on a per-slice review). The handler already extracts
   slice_id at line 1975 for tracker scoping; spread the same
   `**_slice_meta` shape into the OBLIGATION_RESOLVED message metadata
   so the audit trail lands in the per-slice BRC transcript.

After this commit every producer-side BRC message that can fire in
the implement phase carries `metadata.slice_id` for slice-scoped
callers (PROPOSE, RE_REVIEW manual, RE_REVIEW auto, ACK, NACK,
WITHDRAW, OBLIGATION_RESOLVED, CONFIRMED — already tagged before
slice-2). The remaining BRC_HISTORY_TYPES that don't currently tag
(HEARTBEAT, STATUS, NUDGE, HANDOFF, AGENT_FAILED) are non-blocking per
both the tester's and reviewer_code_holistic's review notes; they
fold cleanly into the partition-mode `unattributed` warning rather
than corrupting the per-slice transcript, and the consensus narrative
itself (the high-value review trail) lands in full.

* Fix checks: apply automated formatting fixes

* Address review feedback on per-slice BRC history (#2548)

Tag non-CONSENSUS BRC emitters with slice_id where their handler
already extracts it (HEARTBEAT in messages.py, excuse-producer STATUS
and ready-to-confirm STATUS in signals.py), so slice-scoped messages
land in the right per-slice transcript instead of the shared bucket.

Narrow `_write_brc_history` so the per-slice partitioning only drops
CONSENSUS_* messages without `metadata['slice_id']` (those remain a D4
contract violation). Non-CONSENSUS BRC types (HEARTBEAT, STATUS,
HANDOFF, AGENT_FAILED, NUDGE, OVERSEER_ALERT) without slice_id come
from emitters that do not uniformly carry slice scope (HealthMonitor
nudges, overseer respawn alerts, AGENT_FAILED broadcasts, CLI-routed
HANDOFF/NUDGE) and are now routed to a sibling
`{identifier}-implement-unattributed.{md,json}` file. The link-line
builder clusters that sibling at the implement rank after every
per-slice file. Update the writer docstring and inline comment to
match.

Drop the redundant local re-import of `SLICE_ID_PATTERN` inside
`_write_brc_history` (it's already imported at module top via the
sandbox/orchestrator dual-import pattern). Delete a dead/typo'd
disjunctive assertion in `test_idempotent_per_slice_write`.

Add tests covering: non-CONSENSUS unattributed routing to the sibling
file, the mixed CONSENSUS_*-dropped + non-CONSENSUS-routed split, and
HEARTBEAT/excuse-producer STATUS slice_id metadata round-trip on the
message bus.

* Address second-round review feedback on per-slice BRC history (#2548)

Fix the cross-module silent no-op flagged as blocking: brc_read_peer_artifact
now mirrors the writer's per-slice filename when EGG_SLICE_ID is set and
phase=='implement' (reads {identifier}-implement-{slice_id}.json), and by
default merges the cross-cutting unattributed sibling so reviewers see
their slice's CONSENSUS_* interleaved with OVERSEER_ALERT / AGENT_FAILED
context. Pipeline-level (non-slice) callers still read the aggregate file.

Non-blocking fixes:
- _emit_ready_to_confirm_nudges now has dedicated slice_id metadata tests
  (slice-scoped + pipeline-level pair, mirroring the excuse-producer pair).
- test_idempotent_per_slice_write extended to assert the unattributed
  sibling md/json are byte-identical across repeated writes.
- _render_brc_history_markdown special-cases slice_id=='unattributed':
  heading reads 'cross-cutting (unattributed)' and the metadata block uses
  Section: instead of Slice:, since unattributed is not a slice.
- docs/guides/concurrent-execution.md updated to show the per-slice link
  line shape and explain the unattributed sibling cluster.

* Address third-round non-blocking feedback on per-slice BRC history (#2548)

Sync the brc_read_peer_artifact handler's message_type filter
whitelist with the orchestrator-side BRC_HISTORY_TYPES emitter:

- Fix CONSENSUS_WITHDRAWN -> CONSENSUS_WITHDRAW typo (the writer
  emits CONSENSUS_WITHDRAW; the handler's whitelist rejected the
  correct name and accepted a string the writer never produces).
- Add CONSENSUS_OBLIGATION_RESOLVED, STATUS, HANDOFF, AGENT_FAILED,
  NUDGE, OVERSEER_ALERT, HEARTBEAT to the whitelist so reviewers can
  filter the implement-phase unattributed sibling on those types
  (e.g. message_type=[OVERSEER_ALERT] to scan cross-cutting alerts).
  The handler's docstring already invited these filters; the
  whitelist now matches.
- Update the schema description in tools/brc.py to match.
- Add test_filter_by_message_type_overseer_alert_in_unattributed
  exercising the OVERSEER_ALERT filter against an unattributed
  sibling fixture; locks in the writer/reader symmetry.

Also drop the underscore-prefixed _SLICE_ID_PATTERN private import
in brc_read_peer_artifact and use the existing public
resolve_slice_id({}) helper from _gateway -- same validation,
fewer reach-inside imports, consistent error message format with
the rest of the slice-aware tools.

* Add full-set drift guard locking BRC_HISTORY_TYPES writer/reader symmetry

Closes the fourth-round non-blocking review item on #2548 slice-2: the
existing single-type regression test pinned OVERSEER_ALERT only, so
adding a new entry to the writer-side BRC_HISTORY_TYPES without
updating the sandbox-side _BRC_HISTORY_TYPES would slip past the suite.

The new test in TestBrcHistoryTypesDriftGuard regex-extracts the
writer-side frozenset literal from orchestrator/routes/pipelines.py and
asserts it equals the handler-side whitelist. This preserves the
deliberate sandbox -> orchestrator package-boundary (the orchestrator
pulls fastapi) while still locking the contract: any future drift on
either side surfaces as a test failure with explicit handler-only /
writer-only diffs in the assertion message.

---------

Co-authored-by: egg <egg@example.com>
Co-authored-by: egg <egg@localhost>
Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

---------

Co-authored-by: egg <egg@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com>
Co-authored-by: egg <egg@localhost>
jwbron added a commit that referenced this pull request May 8, 2026
* Initialize SDLC contract for issue #2548

* Refine analysis for issue #2548

Analyzes the missing analysis/plan/BRC visibility on slice PRs.
Compares four options (context PR / embed in slice-1 / embed in
terminal slice / render in PR body), recommends Option A
(dedicated context PR + per-slice implement BRC files), and
registers five HITL decisions plus five open feedback questions
on contract.

* Persist agent statefile writes before refine sync

* Persist statefiles after refine phase

* Persist HITL resolution after refine phase gate

* Risk assessment for issue #2548 plan phase

Identifies 14 risks (R1–R14) across compatibility, gateway-policy,
schema, security, performance, and operator-experience categories.
Captures HITL decisions 1–5 and feedback Q1–Q5 as decision_inputs.

Key risks:
- R1: Gateway slice-integration regex blocks egg/<id>/context push
- R2/R9: Hard-switchover (decision-4) needs operator drain runbook
- R5: Public-repo exposure of agent transcripts (Q3 chose include)
- R8: New PRMetadata fields must be Optional with safe defaults
- R10: Decision-3 covers merge gate but not creation failure semantics

Recommends: go-with-conditions, gateway change ships first,
schema additions ship with safe defaults, surface 2 new HITL
questions (creation-failure semantics, transcript size/scrub).

* Plan #2548: context PR + per-slice BRC history

Five-slice forest chain (slice-1 → slice-2 → slice-3 → slice-4 →
slice-5) following the operator's HITL resolutions:

- D1: dedicated context PR
- D2: hard-split implement BRC into per-slice files; no aggregate
- D3: doc-only auto-open (no merge gate before slicing)
- D4: hard switchover, no backfill
- D5: context PR base = pipeline.base_branch (not hardcoded main)

Slices: contract schema delta -> per-slice BRC writer -> context
branch + doc-only PR opener -> slice-1 base wiring + per-slice BRC
commit + reconciler fallback -> docs.

* Architecture analysis for issue #2548 plan phase

Architect output describes the design for landing refine/plan
analysis docs, agent transcripts, and refine/plan BRC histories
on a dedicated context PR (egg/<id>/context, base=<pipeline.base_branch>)
that slice-1 stacks on top of, plus splitting the implement-phase
BRC history at write time into per-slice files committed to each
slice's integration branch before its PR opens.

Reflects HITL decisions 1-5 and feedback Q1-Q5 from the refine
phase. Hard switchover for new pipelines only; no backfill.

* Persist statefiles after plan phase

* Fix #2532: align .github/ block in agent_roles.py for plan and reviewer roles (#2550)

* Fix #2532: align .github/ block in agent_roles.py for plan and reviewer roles

Adds `.github/` to the `blocked_write` list of every plan-side and
reviewer role in `shared/egg_contracts/agent_roles.py` whose
`patterns.py` counterpart already blocks it:

- ARCHITECT_ROLE, TASK_PLANNER_ROLE, RISK_ANALYST_ROLE
- _REVIEWER_BLOCKED_WRITE (covers reviewer_code, reviewer_code_holistic,
  reviewer_agent_design, reviewer_refine, reviewer_plan,
  reviewer_security, reviewer_concurrency)
- _REVIEWER_CONTRACT_BLOCKED_WRITE

The planner prompt reads `agent_roles.py` via `get_file_patterns()`;
the gateway reads `patterns.py` via `AgentFilePattern.can_write()`.
PR #2525 closed the same drift for the tester (issue #2521); this
closes the remaining cases. The disagreement is benign today because
every affected role's `allowed_write` is confined to `.egg-state/...`
paths that never collide with `.github/`, but bringing the two views
into lockstep means the next allowlist widening cannot silently
bypass the branch-protection invariant from #2508.

Adds `shared/tests/test_github_block_alignment.py` with three
parametrized regression tests across the affected roles: agent_roles
view blocks `.github/`, patterns view blocks `.github/`, and the two
views agree. A "load-bearing" test in #2525's style is not
constructible here — the allowlists never intersect `.github/` — so
the test instead asserts cross-view consistency.

Notes vs. the issue inventory:

- REFINER is NOT in scope. The issue lists it, but `REFINER_PATTERNS`
  in `patterns.py` uses its own custom blocked list (not
  `_PLAN_AGENT_BLOCKED`), and that list also omits `.github/`. The
  two views already agree for refiner, so there's no drift to fix —
  whether refiner *should* block `.github/` is a separate change.

- The reviewer count expanded from the issue's 3 (reviewer_code,
  reviewer_code_holistic, reviewer_contract) to 8: every reviewer
  role sharing `_REVIEWER_BLOCKED_WRITE` is fixed by editing the
  shared list once.

* Extract _PLAN_AGENT_BLOCKED_WRITE shared constant

Mirrors patterns.py's _PLAN_AGENT_BLOCKED structure: ARCHITECT_ROLE,
TASK_PLANNER_ROLE, and RISK_ANALYST_ROLE now share a single
blocked_write list instead of inlining identical 9-element lists.
Eliminates one future drift surface within agent_roles.py itself, as
suggested in PR #2550 review.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2549: skip already-merged slices on pipeline restart (#2552)

* Fix #2549: skip already-merged slices on pipeline restart

When a slice's PR is merged into the work branch, the orchestrator
restart loop has no signal that the slice is done — `iter_ready()`
yields it on the first tick, `create_slice_integration_branch` tries
to push the (now post-merge) parent SHA onto the slice's existing
ref, and origin rejects it as non-fast-forward. The slice cascade-
fails its descendants in ~5 seconds, blocking the entire stacked-PR
workflow until an operator manually deletes the stale slice ref.

The fix wires three things:

* `GatewayClient.is_slice_branch_merged_into_parent` — a new
  detection helper. ls-remote both refs, fetch them, and check
  `merge-base --is-ancestor existing parent`. The inverse direction
  of the #2512 restart-recovery check.

* Bootstrap reconciliation in `_run_implement_phase_slices`. Before
  the run loop starts, fold in (A) slices already marked
  `SliceStatus.COMPLETE` on the contract (cheap path; trust the
  contract) and (B) slices the gateway reports as already-merged
  (the live #2549 repro path). Both transitions persist
  `status=COMPLETE` so subsequent restarts hit (A).

* Race protection in `_run_one_slice_inner`. Re-runs the merged-
  detection right before `create_slice_integration_branch` so a
  slice merged between bootstrap and its wave is also handled.

Also closes a latent gap: `Slice.status` had `COMPLETE` as a value
since the original schema and the #2470 `restart_agent` parent-slice-
complete fallback already read it, but nothing wrote it. The
successful-completion path in `_run_one_slice_inner` now persists
`SliceStatus.COMPLETE` to the contract under the per-pipeline state
lock, finally giving the #2470 reader a real signal.

* Address #2552 review notes: defer reconciler start, parallelize bootstrap, prefer parent_branch_at_creation, expand test

- Move _start_stacked_pr_reconciler call to after the bootstrap pass
  so an exception during bootstrap (hard imports, programming errors)
  cannot leak the daemon thread.
- Parallelize layer-(B) is_slice_branch_merged_into_parent calls with
  a ThreadPoolExecutor (cap 8). Each call uses its own synthetic
  gateway session, so concurrent calls are safe; this keeps startup
  latency bounded as forests grow.
- Prefer slice.parent_branch_at_creation over deriving from
  dependencies[0] in the bootstrap parent-branch resolution. Today
  both should agree, but a future re-plan that mutates dependencies
  post-creation would otherwise compare against the wrong parent.
- Expand test_bootstrap_does_nothing_when_pipeline_repo_unset to
  actually exercise step (A) under repo=None: add a slice with
  status=COMPLETE alongside a PENDING slice, then assert that step
  (A) skips the COMPLETE slice and step (B) is wholesale skipped.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Egg/issue 2474 v2/work (#2556)

* Initialize SDLC contract for issue #2474

* slice slice-1: Cleanup — k3s only, drop dead test tiers (#2533)

* Slice 1 (coder portion): retire e2e tier scaffolding (#2474)

Drops the real-LLM end-to-end test scaffolding the coder role can reach
under its file boundaries (`pyproject.toml`, `Makefile`,
`integration_tests/agent_findings.py`):

- Remove `e2e` and `agent_flaky` pytest markers from `pyproject.toml`.
  The matching `tests/config/test_ci_config.py` required-markers
  assertion is in the tester's scope; tester picks it up alongside
  task-1-2 (delete `tests/functional/`) so the marker set lands
  consistently.
- Drop the `test-e2e` Make target, its `.PHONY` entry, and its `make
  help` line; retag `test-integration` to k3s in the help block and
  module banner. `test-security` is retained.
- Delete `integration_tests/agent_findings.py` — the JSONL findings
  recorder for the agent_flaky fuzz tier; orphan once
  `test_agent_security_fuzz.py` is removed by tester (task-1-3 e2e
  tests).
- Stage `.github-staging/workflows/test-e2e.yml` as a deletion-marker:
  agent file-boundaries block writes under `.github/`, so the
  staged file's header explicitly directs the human reviewer to
  `git rm .github/workflows/test-e2e.yml` (and the marker itself)
  rather than `git mv` it into place. The PR builder's auto
  "Move staged `.github/` changes" step (issue #2508) surfaces the
  marker.

Tasks split:
- task-1-3 coder portion: pyproject markers, Makefile target,
  agent_findings.py, .github-staging marker.
- task-1-3 tester portion (handed off): delete
  `integration_tests/test_e2e_workflow.py` and
  `integration_tests/test_agent_security_fuzz.py`; update
  `tests/config/test_ci_config.py` required-markers set.
- task-1-1 / task-1-4 (handed off): conftest edits live in tester's
  scope (`**/conftest.py`).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop .github-staging/ deletion-marker; rely on pre-merge condition (#2474)

Address reviewer_code NACK on slice-1 v1: the staging-promote pattern
in `_build_github_staging_manual_step()` (orchestrator/routes/pipelines.py:8490)
unconditionally renders `git mv .github-staging/<path> .github/<path>`
boilerplate for every staged file — there's no opt-out for "this marker
expresses a deletion intent." A reviewer who skims past the YAML-comment
header inside the staged file and follows the auto-generated `git mv`
either fails loudly ("destination exists") or, with `git mv -f`, silently
overwrites the live workflow with the retired stub — neither resolves
into the intended `git rm`.

The documented BRC pattern for "human action that agents cannot push
through the gateway" is `--pre-merge-condition` on a reviewer ACK
(issue #1998 / `_collect_pre_merge_obligations`), which renders as a
"Pre-merge Obligations" section in the PR body with a do-not-merge
banner. reviewer_contract attached such an obligation on their v1 ACK,
so the merger sees the `git rm` instruction unambiguously without the
contradictory staging-promote step.

Behaviour change: none against the runtime pipeline. The live
`.github/workflows/test-e2e.yml` deletion remains a pre-merge human
obligation; only the in-tree marker file is removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Slice-1 tester scope: delete tests/functional/, k3s-only conftests, retire e2e tests

Picks up everything in the tester's gateway file scope for slice-1
(issue #2474):

task-1-2 (delete tests/functional/):
- Remove all 5 files under tests/functional/.
  Acceptance criterion: `tests/functional/` no longer exists.

  NOTE: the matching `functional:` marker registration in pyproject.toml
  AND the `tests/functional/conftest.py` allowlist entry in
  scripts/check-hardcoded-ports.py are gateway-blocked from the tester
  role (only coder can push pyproject.toml / scripts/).  Both have been
  HANDOFFed back to the coder for inclusion in their next propose; see
  the HANDOFF message issued alongside this commit.  Leaving the
  marker registered is harmless (no tests carry the marker any more);
  leaving the allowlist entry registered is harmless (the file is gone
  so the lint script never visits it).

task-1-1 (k3s-only egg_stack):
- integration_tests/conftest.py: drop `_docker_egg_stack()`, the
  EGG_RUNTIME=docker branch in `egg_stack`, the `docker_available`
  import, and stale docker-compose comments.  `egg_stack` now skips
  with a clear pointer to docs/guides/testing.md when kubectl is
  unavailable.
- integration_tests/local_pipeline/conftest.py: same treatment for
  `local_pipeline_stack`.  Drops the COMPOSE_FILE / MOCK_SANDBOX_DIR
  constants, `_cleanup_orphaned_containers`, the docker-compose-up
  block, and the `docker_available` import.

task-1-4 (retire orphan agent-led helpers in integration_tests/conftest.py):
- Delete `run_claude_structured()`, `assert_agent_verdict()`, the
  `AgentVerdict` dataclass (including `infrastructure_failure`),
  `VERDICT_SCHEMA`, `TEST_AGENT_SYSTEM_PROMPT`, and the orphaned
  `_allocate_test_container_ip()`, `_capture_container_logs()`,
  `_preflight_gateway_check()` helpers.  Drop the now-unused imports
  (`json`, `requests`, `ContainerNetworkConfig`, `build_sandbox_docker_cmd`).

task-1-3 (delete e2e test files; tester scope):
- rm integration_tests/test_e2e_workflow.py
- rm integration_tests/test_agent_security_fuzz.py
- tests/config/test_ci_config.py: required-markers assertion narrowed
  from {integration, functional, e2e, security, agent_flaky} to
  {integration, security}, with a docstring reference to issue #2474.

Test infrastructure preserved:
- GATEWAY_PORT remains imported and re-exported from
  integration_tests/conftest.py because test_network_security.py
  imports it directly via
  `from integration_tests.conftest import GATEWAY_PORT, exec_in_container`.
- isolated_container / external_container / test_container fixtures are
  retained for the test_credential_security and test_network_isolation
  tiers (both still in tree).  They will skip in k3s mode (the docker
  network name does not resolve), but slice-3 of this PR train adds
  k3s-native equivalents that supersede them.

Acceptance criteria verified for the tester portion:
- `tests/functional/` no longer exists.
- `grep -rn "run_claude_structured|assert_agent_verdict"` returns no
  hits.
- `grep -nE "EGG_RUNTIME=docker|_docker_egg_stack|docker_available"`
  on the two conftest files returns no hits.
- `make lint` passes.

* Slice-1 cleanup: drop functional marker + stale port allowlist (#2474)

Address tester HANDOFF 2cc2c216-4c53-45 (non-blocking, raised on
coder v2 ACK). Now that `tests/functional/` and
`integration_tests/docker-compose.yml` are gone (slice-1 tester
commit 3827cb571), this commit cleans up the dead-weight references
to those paths that the tester role is gateway-blocked from
reaching:

- `pyproject.toml`: drop the `functional:` marker registration. The
  marker was the last live reference to the deleted
  `tests/functional/` tier; `tests/config/test_ci_config.py` was
  narrowed by tester to `required = {integration, security}` so the
  required-markers test still passes (subset check) — but the marker
  registration itself was orphan after the tier deletion.

- `scripts/check-hardcoded-ports.py`: remove two stale
  `ALLOWLIST_PATHS` entries pointing at files that no longer exist:
  `integration_tests/docker-compose.yml` and
  `tests/functional/conftest.py`. The latter matters for task-1-2's
  acceptance criterion `grep -rn "tests.functional|@pytest.mark.functional"`
  returns no hits — the regex `tests.functional` matches the literal
  string `tests/functional/conftest.py` (`.` matches `/`), so the
  allowlist entry was a real gap, not just polish.

`make lint` is clean; `tests/config/` test suite still passes with
the trimmed marker set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* rm e2e workflow

* Address review: drop more dead code from k3s-only cleanup

Follow-ups on PR #2533 review (https://github.com/jwbron/egg/pull/2533#pullrequestreview-4241318207):

- Delete now-orphan integration_tests/local_pipeline/mock-sandbox/
  (Dockerfile + phase-runner.sh) — only consumer was the deleted
  docker fallback in local_pipeline/conftest.py.
- Remove tests.utils.gateway_client.docker_available() and its
  re-export — zero remaining callers after this PR removed the
  conftest call sites.
- Strip stale -m "not functional" from Makefile (test, test-all)
  and update docs/guides/testing.md §2 step 8.
- Rewrite integration_tests/conftest.py docstring to describe
  what the legacy fixtures actually do under k3s.  Add explicit
  pytest.skip in isolated_container/external_container/test_container
  when the stack is k8s-backed (was silently skipping with a
  generic-sounding "could not start container" message).
- Drop unused certs_volume field from EggStack; document why
  compose_project / external_network are retained.
- Expand __all__ in integration_tests/conftest.py to cover the
  re-exported public surface (EggStack, ContainerInfo,
  exec_in_container, GATEWAY_PORT) — previously listed GATEWAY_PORT
  only.
- Drop STRUCTURE.md mock-sandbox entry.

Skipping the "except FileNotFoundError, subprocess.TimeoutExpired:"
nit — ruff format 0.15.12 actively strips parens from except
tuples, so the parenthesized form would not survive `make lint-fix`.

Part of pipeline issue-2474-v2; the terminal slice carries the
program-level narrative.

* Address review: remove stale entries from STRUCTURE.md

Drop the four file entries from the integration_tests/ tree listing that
this PR (slice-1 cleanup) deletes: docker-compose.yml, agent_findings.py,
test_agent_security_fuzz.py, test_e2e_workflow.py.

Reviewer noted the local_pipeline/ subsection was already updated in
a423d311 but the parent listing was missed.

---------

Co-authored-by: egg <egg@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: James Wiesebron <jameswiesebron@khanacademy.org>
Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2495: discriminate authorization vs. value errors at /mutate boundary (#2517)

* Fix #2495: discriminate authorization vs. value errors at /mutate boundary

The `/mutate` route was returning 403 for every `MutationResult.success=False`,
which is correct for role-authorization rejections but misleading for value/path
errors (bad `field_path`, out-of-range index, out-of-domain enum value). A
client receiving 403 for `Invalid value for current_phase: …` would reasonably
retry with a different role, which can't help.

Adds `error_kind: Literal["authorization", "value"] | None` to `MutationResult`
so the route can map cleanly without parsing message strings: 403 for
authorization, 400 for value errors. Adds regression tests for all three
branches.

* Address review: assert error_kind in validator tests + export MutationErrorKind

Closes the validator-level test gap flagged in the PR review:
- test_apply_invalid_mutation_rejected now asserts
  error_kind == "authorization" so a regression that drops the
  discriminator on the role-rejection path fails at the unit-test
  boundary, not just the route boundary.
- test_invalid_enum_value_returns_failed_mutation now asserts
  error_kind == "value" for the pydantic ValidationError path.
- New test_invalid_path_returns_failed_mutation covers the
  (KeyError, IndexError, AttributeError) path through _set_value
  and asserts error_kind == "value".

Re-exports MutationErrorKind from shared/egg_contracts/__init__.py
so callers can type-hint against the discriminator without
reaching into the validator submodule.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* docs: document .github-staging/ convention in agent-roles reference [doc-updater] (#2516)

* docs: document .github-staging/ convention in agent-roles reference

* docs: correct tester guidance — HANDOFF instead of .github-staging/

The tester's allowed_patterns in shared/egg_restrictions/patterns.py
covers only test files, conftest, pin files, and .egg-state/agent-outputs/
— it does not include .yml/.yaml/.json. AgentFilePattern.can_write
requires both a non-blocked path AND a positive allowlist hit, so
.github-staging/workflows/ci.yml returns False for the tester even
though .github-staging/ is not on the tester's blocked list.

A tester following the previous text would attempt to stage CI fixes
under .github-staging/ and be rejected. Replace that advice with the
correct path: hand off to the coder via HANDOFF, mirroring the existing
coder→tester handoff pattern.

Surfaced by egg-reviewer on PR #2516. The patterns.py / agent_roles.py
divergence the same review noted is tracked separately in #2521.

* docs: expand tester Directed Coordination to cover outbound HANDOFF

---------

Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com>
Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2521: align tester `.github/` block between agent_roles.py and patterns.py (#2525)

* Fix #2521: align tester `.github/` block between agent_roles.py and patterns.py

#2514 added `.github/` to `TESTER_ROLE.blocked_write` in
`agent_roles.py` but skipped the mirror entry in
`TESTER_PATTERNS.blocked_patterns` in `patterns.py` — the planner
prompt and the gateway saw different views of the tester's write
scope. The omission was benign because the tester's allowlist
already excludes `.github/`, but it stops being benign the moment
someone widens that allowlist.

Add `.github/` to `TESTER_PATTERNS.blocked_patterns` so both files
agree, matching the lockstep pattern already used for documenter,
autofixer, and conflict_resolver. Add regression tests in the
gateway pattern suite and the shared restrictions unit suite.

* Address review on #2525: load-bearing tests, slim comment

- Use `.github/test_actions.py` as the load-bearing assertion in both
  test files. It matches the tester's `**/test_*.py` allowlist, so
  only the new `.github/` blocked entry stops it. The two pre-existing
  paths (`.github/CODEOWNERS`, `.github/PULL_REQUEST_TEMPLATE.md`)
  stay as breadth assertions; both are blocked even without the new
  entry, so they would have passed against the unfixed patterns.
- Slim the rationale block in `patterns.py:224-234` to a one-liner
  pointing at `CODER_PATTERNS` for the full `.github/` rationale.
  The original 11-line comment over-claimed lockstep across roles
  the diff didn't actually touch (architect, task_planner, refiner,
  reviewer roles); the one-liner doesn't.

Drift in the non-tester roles (architect/task_planner/risk_analyst/
refiner/reviewer/reviewer_contract) is tracked in #2532.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2490: extend validate_assignment to sibling Contract models (#2520)

* Fix #2490: extend validate_assignment to sibling Contract models

#2484 added `model_config = ConfigDict(validate_assignment=True)` to
`Contract` so `setattr` on Contract fields coerces values back to their
declared type. The reviewer flagged a remaining asymmetry: sibling
models (`Task`, `Slice`, `Decision`, `AgentExecutionModel`, …) still
silently accepted untyped assignments like `task.status = "garbage"`,
so the validation surface was uneven across the contract object graph.

Lift the config to a shared `EggContractBaseModel` (Option B from the
issue) and have every model in `shared/egg_contracts/models.py`
inherit from it, so the strictness applies uniformly without per-model
duplication. Drop the per-model config from `Contract` itself — the
shared base now provides it.

The audit of sibling-model mutation sites (`shared/egg_contracts/orchestration.py`
`set_execution`, `orchestrator/routes/decisions.py` `contract.pr =`,
etc.) confirms they assign well-typed values (enum members or
constructed model instances), so the new strictness does not break
existing call sites.

* Address PR #2520 feedback: fix nested-model test, refresh validator comment

- Rewrite test_pr_metadata_invalid_deferred_actions_raises to assign a
  raw dict, which actually exercises pydantic's list-element coercion
  path on the outer setattr; the previous form raised from the inner
  DeferredAction(...) constructor regardless of validate_assignment
  (item 1).

- Update the validator.py except ValidationError comment to reference
  EggContractBaseModel (where the config now lives) and add #2490 to
  the issue list, since the same catch now covers sibling-model
  setattrs (Task.status, Slice.status, Decision.type, ...) too (item 2).

— Authored by egg

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2501: don't flip stale during an in-flight state-store probe (#2519)

* Fix #2501: extend probe freshness while a probe is in flight

`StateStoreProbe.snapshot()` flipped the cached `healthy` to `False`
purely because the cache age crossed `interval * stale_multiplier`,
even when a probe was actively running and about to refresh it. Under
slice-spawn load `git worktree add` occasionally ran 30-40s, longer
than the 30s default staleness window, so the request-path dual-write
in `routes/health.py` recorded `unhealthy` and the BG callback
recorded `healthy` 0-3s later when the same probe completed —
producing the spurious `recent_transitions` flap pairs reported in
the issue.

Track probe start time and, while a probe is in flight, treat the
cache as fresh until the in-flight probe itself has been running
longer than the staleness window. A genuinely wedged probe still
surfaces as stale once that bound is exceeded.

* Address review feedback on #2501 in-flight grace fix

- Document worst-case ~2*stale_window wedge-detection bound and the
  intentional 'fresh-but-old' semantics during the grace in
  snapshot()'s docstring (reviewer minor: source-recoverable rationale).
- Expand the inline comment in the grace branch to cite the 'fix #1'
  framing from #2501 so the bound's rationale is recoverable from
  the source alone (reviewer minor).
- Add an integration-level test that drives snapshot() while a real
  probe_now() is parked mid-probe on a worker thread, populating the
  in-flight flag and _probe_started_at_monotonic via the production
  code path. Closes the loop end-to-end so a future refactor that
  stops setting _probe_started_at_monotonic from probe_now() fails
  this test where the field-poking variants would silently keep
  passing (reviewer non-blocking suggestion).

* Tighten worst-case wedge detection bound in snapshot() docstring

Reviewer noted that the '~2 * stale_window' / '60s with 30s default'
framing is loose. The BG loop fires every `interval` seconds, so the
in-flight probe starts within `interval` of the last good completion,
and the grace extends only until that probe's own age exceeds
`stale_window`. The tight bound is therefore `stale_window + interval`,
which under the defaults (interval=15s, stale_multiplier=2.0) is ~45s
of blindness, not ~60s. The 2 * stale_window framing only saturates
when stale_multiplier=1.0. Address-only docstring change; no behavior
change.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2515: restart_phase falls back to deterministic roster when agents cache empty (#2518)

* Fix #2515: restart_phase falls back to deterministic roster when agents cache empty

restart_phase reads its respawn roster from phase_exec.agents — a runtime
cache that the route's own clear-then-spawn flow resets to []. If the
spawn step fails before re-populating the cache, every subsequent
restart_phase 400s on "No agents found in phase {phase} to restart" and
start_pipeline 409s on the (now CANCELLED) status, leaving the only
escape cancel_task(cleanup=true) — which discards all prior work.

Fall back to the same deterministic source the executor itself uses:
pipeline.active_roles (CUSTOM-mode / BABYSIT overrides, #1762) first,
then get_roles_for_phase(repo, has_contract). Same precedence as
_run_concurrent_phase, so the recovered roster matches what the next
spawn would have produced anyway.

* Match _run_concurrent_phase exactly: skip phase-default fallback when active_roles set

When pipeline.active_roles is set but every entry is unknown to this
orchestrator's AgentRole (defensive case after a role removal in a
newer schema), the prior implementation fell through to
get_roles_for_phase and expanded to the full phase-default roster.
_run_concurrent_phase keeps its roles list empty in the same case,
so the route's response (and the downstream worktree-delete /
health-monitor reset) would diverge from what the spawn would
actually produce.

Convert the second 'if not agent_roles:' into an 'else:' attached to
the override branch so the strict-parity behaviour matches: when an
override is set we use it verbatim and never fall through. The final
'No agents found' 400 still fires honestly when the override is
all-unknown.

Adds a regression test that mutates active_roles post-construct
(bypassing the field validator) to simulate the load-time-drift
edge case.

* Document deliberate route-vs-worker divergence in roster-derivation try/except

The except Exception wrap around _get_roles_for_phase doesn't exist in
_run_concurrent_phase, so a future reader auditing the two callsites
for parity might mistake the bare-except for a bug rather than a
deliberate route-specific safety floor (return 400 not 500).

* Tighten line-range citation in roster-derivation comment to 12813-12840

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2522: enumerate per-agent worktrees on phase restart (#2526)

* Fix #2522: enumerate per-agent worktrees on phase restart

restart_phase guessed worktree names as ``{pipeline_id}-{role}``, which
misses slice-scoped worktrees (``{pipeline_id}-slice-{N}-{role}``) and
leaves them on disk after a restart on a slice-based pipeline.

Drive deletion off ``agent_salvage.enumerate_agent_worktrees`` (already
the source of truth in ``cleanup_pipeline`` and salvage) and filter to
the roles being restarted. The pipeline-level worktree
(``agent_role=None``) and worktrees for non-restarted roles are
intentionally preserved.

* Address review: salvage before restart-phase delete; cleanup-style enumeration

Blocking review feedback (#2522 / PR #2526):

1. Silent loss of unpushed agent commits during phase restart
   restart_phase now calls agent_salvage.auto_salvage_pipeline before
   the deletion loop (mirroring cleanup_pipeline's #2429 invariant).
   Restart is precisely the scenario where unpushed commits accumulate
   - operators hit it because agents got stuck or wedged - so the
   previous code was the one orchestrator-side worktree-delete path
   that bypassed salvage. Salvage failures are best-effort; deletion
   still happens.

2. Broken/corrupted worktrees regressed the original #1723 cleanup
   enumerate_agent_worktrees gates on a usable .git marker, so
   wedged-btrfs-mount worktrees were being silently skipped after this
   PR's switch to enumeration. Added validate_git=False flag (default
   stays True for salvage callers) so cleanup callers receive broken
   entries with repo_path falling back to the worktree dir itself.
   restart_phase now opts in to the cleanup-style listing.

Non-blocking feedback addressed in the same commit:

- Test fixture _make_pipeline_with_slice_agents builds AgentExecution
  with slice_id populated, matching what concurrent_executor writes.
- New test exercises continue-on-error across three worktrees with
  the middle one's delete raising; locks down loop semantics.
- Narrower exception class (OSError | ImportError | RuntimeError)
  around enumerate_agent_worktrees.
- log_extras suppresses slice_id=None on non-slice pipelines.

New tests:
- test_restart_phase_continues_after_partial_worktree_deletion_failure
- test_restart_phase_salvages_before_deleting_worktrees
- test_restart_phase_salvage_failure_is_nonfatal
- test_restart_phase_deletes_broken_worktree_without_git_marker
- test_validate_git_false_returns_broken_worktrees
- test_validate_git_false_preserves_validated_repo_path

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2539: drop duplicate `slice ` prefix in non-terminal slice PR titles (#2540)

`create_slice_pr` was rendering non-terminal slice PR titles as
`slice slice-1: …` because `slice_id` already starts with `slice-`.
Drop the literal prefix so the title is just `{slice_id}: {slice_name}`
(e.g. `slice-1: Cleanup — k3s only, drop dead test tiers`), and update
the two test assertions and docs reference that pinned the buggy form.

* Fix #2531: add `--for STATUS` to producer pre-confirm wait-loop (#2536)

When every reviewer ACKed the current version, no further
`CONSENSUS_ACK` / `CONSENSUS_NACK` events arrive on the bus. The
orchestrator's directed `STATUS` nudge ("Ready to confirm — all
confirm preconditions satisfied", `metadata.ready_to_confirm == True`)
is the only signal that the global preconditions cleared, but the
producer prompt's pre-confirm wait-loop filter omitted `STATUS` —
so the producer slept through the nudge and only woke via the
health-monitor `OVERSEER_ALERT` backstop minutes later, observed in
pipeline `issue-2474-v2` slice-1 (6/8 stall, ~57 min phase elapsed).

The reference doc at `agent-wait-patterns.md` already prescribed
waiting on `STATUS` for the pending-acks recovery path; the prompt
template just hadn't caught up. This change closes that gap and adds
a regression test that pins `--for STATUS` plus the on-wake guidance
("go to step 5 CONFIRM if `metadata.ready_to_confirm`, otherwise
re-enter the wait") across every producer role × phase. The
`_send_brc_confirmation_nudge` docstring is updated to reflect the
new pre-confirm filter.

* Fix #2535: stop slice-N from inheriting slice-(N-1) consensus, drop gateway import (#2542)

* Fix #2535: stop slice-N from inheriting slice-(N-1) consensus, drop gateway import

Two bugs surfaced when issue-2474-v2 spawned slice-2: every container
exited four seconds in with no work attempted, leaving slice-2's
integration branch empty and the PR-create call to fail with
"No commits between ...".

Bug A (`gateway/git_client module unavailable`):
the deployed orchestrator image ships only `orchestrator/`, `routes/`,
`health_checks/`, and the shared `egg_*` packages — `gateway/` is not
copied. The `from gateway.git_client import build_rebase_onto_args`
call added by #2512 always raises ImportError in production, so every
slice integration branch reconciliation silently fails. Inline the
canonical argv builder as `_build_rebase_onto_args` in
`orchestrator/gateway_client.py`; the gateway server's `/git` endpoint
remains the authoritative allowlist boundary, and CI test paths that
keep `gateway/` on `sys.path` continue to work unchanged.

Bug B (slice-2 consensus reached at elapsed_seconds=0.0):
the per-slice tracker registry already keys by `{pipeline_id}/{slice_id}`,
but `ConcurrentPhaseExecutor.check_consensus()` had two slice-unaware
fallback paths. When slice-2's tracker is fresh and empty (the steady
state right after spawn, before any agent has proposed),
(1) `reconstruct_tracker_from_messages` was called with the bare
pipeline_id and (2) the message-bus fallback scanned
`store.get_messages(pipeline_id)` pipeline-wide. Slice-1's eight
CONSENSUS_CONFIRMED messages are persisted under the bare pipeline_id
and have the same role names as slice-2's roster, so both paths
falsely declared consensus on slice-2's first poll iteration. Gate
both fallbacks (and the matching path in
`handle_consensus_confirmed_signal`) on `slice_id is None`. The
in-memory per-slice tracker is the authoritative source; an empty
fresh tracker correctly returns is_complete=False and the polling
loop keeps going.

* Address #2542 review: slice-scope idempotency, fix test syntax, doc tweaks

Five issues from egg-reviewer on the #2535 PR:

1. test_check_consensus_slice_isolation.py: replace dead try/except
   that used Python-2 catch-and-bind syntax (`except A, B:`) with a
   direct `PipelineConfig(concurrent_execution=True)` constructor
   call. The original block was unreachable — `concurrent_execution`
   is a normal Pydantic bool field that cannot raise on assignment —
   and the misleading syntax would surprise any future reader.

2. routes/signals.py: scope `_existing_confirmed_for_role` to a slice
   so the idempotency probe doesn't see sibling-slice CONFIRMs as
   "already confirmed for this role". A new `slice_id` parameter
   filters by `metadata["slice_id"]`; the per-slice tracker path
   tags CONSENSUS_CONFIRMED writes with that same metadata key.
   Without this, slice-2's first coder CONFIRMED would be silently
   suppressed (no bus message, no #1473 marker) because slice-1's
   coder CONFIRMED was still in the bus under the bare pipeline_id.
   Pipeline-scoped (slice_id is None) callers continue to see only
   pipeline-scoped messages, preserving legacy behaviour exactly.

3. orchestrator/gateway_client.py: soften the "Mirrors" claim in the
   `_build_rebase_onto_args` docstring. The helper does NOT call
   validate_git_args (which would defeat the inlining) and emits
   stripped argv, so document those two intentional differences.

4. orchestrator/stacked_pr_reconciler.py: update the module docstring
   to point at the inlined `_build_rebase_onto_args` in
   orchestrator.gateway_client (with a note explaining why the
   inlining is needed and why the security floor is unchanged).

5. tests/test_consensus_confirmed_idempotent.py: extend the helper
   `_fake_message` with a `slice_id` parameter and add three
   regression tests:
     - slice-2's first CONFIRMED is NOT marked idempotent by a
       slice-1 CONFIRMED in the bus
     - within slice-2, the second CONFIRMED IS deduped
     - pipeline-scoped callers ignore slice-scoped CONFIRMs

The wider sweep of slice-unaware peer-consensus lookups in
kubernetes_monitor.py, startup_reconciliation.py, routes/pipelines.py
status display, and the tier-1 health checks is left for #2409 (the
existing tracker covers the same root-cause: slice_id needs to flow
through more places). PR body updated to flag this.

* Fix #2538: slice PRs carry contract.pr narrative on every slice (#2543)

Every slice PR — terminal and non-terminal — now renders the
planner-authored program title, description, test plan, and manual
steps from contract.pr, so reviewers see program rationale on
whichever slice they open first. Previously only the terminal slice
carried the narrative; reviewers approaching slice-1 (the bottom of
the stack and the canonical merge entry point) saw only task bullets
plus a pointer to the terminal slice's PR.

Title disambiguation: terminal slice gets the bare program_title;
non-terminals get a [<slice-id>] prefix so the GitHub PR list stays
scannable when several stacked PRs are open at once.

Per-merge obligations remain terminal-only (the merge gate is the
last-to-merge PR in the stack) — the existing #2354 invariants and
fail-fast assertion are preserved.

The terminal slice keeps a "merge gate / umbrella" banner so
reviewers can spot the merge gate; non-terminals skip it. The old
"see terminal slice's PR for the program-level narrative" pointer is
gone — the narrative is right there now.

* Fix #2537: attribute slice PRs to orchestrator, not coder (#2541)

* Fix #2537: attribute slice PRs to orchestrator, not coder

The slice-PR creation path is orchestrator-only — `gh pr create*` is
blocked for the implement phase, and the pr phase has no agent spawn.
But `_run_implement_phase_slices` was hard-coding `agent_role="coder"`
on the synthetic session that opens the slice PR, which caused the
gateway to label the PR `agent:coder` and inject `agent_role=coder`
into the `<!-- egg-pipeline-context ... -->` comment.

Pass `agent_role="orchestrator"` so slice PRs match the attribution
the non-sliced `_auto_create_pr` path already uses.

* Fix /status/wait test flake: handshake before publish

The three event-bus tests in TestStatusWaitRoute used a 0.1s sleep in
the fire thread before publishing — racy on slow CI. The route's
preamble (cursor parse, terminal short-circuit, staleness probe,
current_sequence() snap) can exceed the grace window, so the publish
lands before event_bus.subscribe(None, _on_event) and the event is
never delivered.

Replace the sleep with a deterministic handshake that polls
event_bus._wildcard_handlers and returns the moment the route has
subscribed. The message-bus path (test_overseer_alert_wakes_route)
uses a different wake mechanism and is left untouched.

* docs: add --for STATUS to producer pre-confirm wait-loop example (#2546)

Syncs docs/guides/concurrent-execution.md with the fix from #2531:
the producer RESPOND TO REVIEWS (step 4) wait-loop now includes
--for STATUS so the orchestrator's "Ready to confirm" directed nudge
wakes the producer when every reviewer has already ACKed and no
further CONSENSUS_ACK/CONSENSUS_NACK will arrive.

docs/reference/agent-wait-patterns.md was already updated in the
same PR; this doc had a stale copy of the canonical snippet.

Authored-by: egg

Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com>

---------

Co-authored-by: egg-orchestrator <egg@localhost>
Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com>
Co-authored-by: egg <egg@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* Fix #2527: validate task role↔file alignment at plan time (#2551)

* Fix #2527: validate task role↔file alignment at plan time

Adds `validate_task_role_alignment` in `shared/egg_contracts/plan_parser.py`
that mirrors the gateway's push-time blocked-pattern check for each
task's `role` against its `files_affected`. The plan reviewer's prompt
now runs the validator on the parsed plan draft and injects a
"Structural Role-Alignment Check" section listing every offending task
with the eligible-role hint (or the `.github-staging/` remediation
when no producer role can push the file set). The plan-review criteria
gain a deterministic blocking item that points at this section, so a
mis-assignment surfaces as a NACK before any producer cycle is wasted.

Per-task logic lives in `_check_role_files` so the #2530 follow-up
(`includes_tests: true` opt-in for coders coupling tests with their
own production code) has a clear hook point.

Section is omitted when the validator reports no violations; the
prompt is unchanged for clean plans. Push-time enforcement remains in
place as defense in depth.

* Move #2527 validator to orchestrator-side propose-time enforcement

PR-1 review flagged a cross-module silent no-op: in concurrent BRC
mode (the default for plan phase) the original prompt-time helper
``_build_role_alignment_check_section`` always returned ``""``
because ``_run_concurrent_phase`` builds every reviewer prompt
up-front before the planner has produced the plan. The criteria
text then told the reviewer that "absence of that section means the
automated check found no violations" — the opposite of the truth.

Replace it with deterministic enforcement at the right seam:
``_validate_planner_role_alignment`` runs in
``handle_consensus_propose_signal`` for ``agent_role=="task_planner"``,
mirroring the existing ``_validate_tester_check_coverage`` pattern.
It reads the plan content as committed at the proposed SHA via
``git show <commit>:<plan_path>`` (so a stale local checkout can't
mask a real misassignment) and raises ``ValueError`` on violations,
which the caller turns into HTTP 400 — the proposal is rejected
BEFORE the tracker is mutated and BEFORE any reviewer sees it.

Also addresses the non-blocking comments:
* Lazy ``posixpath`` / ``match_pattern`` / ``AGENT_PATTERNS``
  imports in ``_is_file_blocked_for_role`` are moved to module
  scope (no circular-import risk; per-call overhead removed).
* Tests now exercise the production sequence end-to-end:
  ``test_rejected_proposal_does_not_mutate_tracker`` builds the
  exact propose signal a planner emits in concurrent BRC mode,
  mocks ``git show`` to return a misassigned plan, and asserts the
  tracker is left untouched. The PR-1 prompt-emission tests are
  removed (the helper they pinned is gone) and replaced with
  criteria-text regression guards that lock out the "absence =
  no violations" wording.

* Address PR review feedback (round 2)

Blocking:
- Revert egg_restrictions.patterns import in plan_parser.py to lazy
  function-local. The module-scope hoist in PR-1 round 1 re-introduced
  the egg_restrictions ↔ egg_contracts import cycle that
  shared/egg_restrictions/matchers.py was deliberately split out to
  avoid (see its docstring), breaking the gateway production boot path
  (python3 gateway/gateway.py). Conftest pre-load order hid the cycle
  in pytest. egg_restrictions.matchers.match_pattern stays at module
  scope — only AGENT_PATTERNS needs to be lazy.
- Add TestImportOrderingRegression that subprocess-runs
  'import egg_restrictions.patterns' under PYTHONPATH=shared so the
  cycle surfaces in a clean interpreter (mirrors gateway boot).

Non-blocking:
- Reword the role-alignment criterion in _get_plan_review_criteria to
  say 'before the proposal reaches you' instead of 'before this prompt
  is ever rendered' — concurrent BRC mode builds reviewer prompts
  up-front, so prompt-render time isn't the right reference point.
- Update stale comment in test_pipeline_prompts.py that pointed at
  test_signals.py::test_propose_validates_planner_role_alignment (no
  such file/test) — the validator-runs-here tests live in this same
  file under TestPlannerRoleAlignmentValidation.
- Thread already-loaded pipeline_state and worktree_path from
  handle_consensus_propose_signal into _validate_planner_role_alignment
  via keyword args (with backward-compat fallback to in-function loads)
  so the validator's dependency on the prior _verify_commit_on_branch
  block is explicit and the state-store + worktree lookups aren't
  duplicated.

* Fix stale gateway boot path comment in import-ordering regression test

The PYTHONPATH=shared mirror comment cited scripts/start-gateway.sh
which doesn't exist. Replace with the actual production references:
gateway/Dockerfile:99 (PYTHONPATH=/app), gateway/entrypoint.sh:286
(exec python3 gateway.py), gateway/Dockerfile:70-75 (shared/ COPY).

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* docs: document plan-time role↔file alignment validation (#2558)

Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com>
Co-authored-by: egg <egg@localhost>

* Fix #2554: render per-role agent-status table on BRC dashboard emits (#2557)

* Render per-role agent-status table on BRC dashboard emits

The Phase 3 monitor previously rendered a 3-line `Pipeline Status`
block plus a stacked 4-line `Enhanced dashboard` consensus paragraph
on each `wait-status` emit. With 8+ agents active during a busy
implement-phase BRC, the prose form blurs — a stalled reviewer is
not visually distinct from a working one, and a NACK row drops to
the end of the paragraph.

Replace the two stacked blocks with one per-role markdown table when
`concurrent.consensus` is present. Columns derive directly from the
`peer_consensus.evaluate()` envelope (`agents[role].producer_phase` /
`reviewer_phase` / `confirmed`, plus structured `unresolved_nacks`),
so schema drift surfaces as an empty cell rather than a wrong cell.
Dual-role agents (`tester`) render `<producer_phase> / <reviewer_phase>`.
Always render full state on BRC emits — the table is an at-a-glance
scan, so deltas-only would defeat its purpose. Non-BRC lines keep the
existing compact 3-line form with deltas-only behavior.

Mirror the same two-path render in Phase S5 (lightweight pipeline)
and emit the table one final time in Phase 5 success summary so the
operator has a closing snapshot of which roles confirmed.

Fix #2554

* Address review: drop nonexistent Slice column; fix S5/S6 mirrors; generalize producer ordering

- Drop Slice column entirely. last_status.pipeline.current_slice_id does
  not exist on the Pipeline model — slice_id lives on AgentExecution
  (per-agent), not on the pipeline root, and the minimal envelope does
  not carry it. Rendering it would have produced 'Slice: —' on every
  emit and silently misled operators about sliced vs non-sliced state.
- Phase S5 (lightweight): replace stale 'concise/deltas-only' line with
  the same dual-path phrasing as Phase 3 line 411, and drop Slice from
  the Path B header reference.
- Phase S6 (lightweight Complete): add the closing-snapshot bullet so
  BRC consensus is rendered one final time on success — lightweight
  pipelines start at implement, so this is the common case.
- Generalize producer ordering: pull producers/reviewers from
  concurrent.consensus.review_graph (sorted alphabetically by
  ReviewGraph.to_dict) instead of the implement-only hardcoded list,
  so refine/plan producers (refiner, architect, task_planner,
  risk_analyst) order correctly without further prose drift.
- Consensus fallback: specify the Phase column renders '—' when
  concurrent.consensus is missing, and explicitly forbid inventing
  a message-type-to-phase mapping (a CONSENSUS_PROPOSE tells you the
  producer is in PROPOSED but says nothing about reviewer phases).

* Address re-review: dedup dual-role agents, fix example ordering

The producers/reviewers split sourced from review_graph emits tester in
both lists for the implement graph, so an LLM following the rule
literally would render tester twice. Add an explicit dedup directive to
the Role column rule. Reorder the example table to match the alphabetical
ordering rule (review_graph.producers is sorted).

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Co-authored-by: egg <egg@localhost>

* Fix #2529: runtime escape hatch for impossible tasks (#2553)

* Fix #2529: runtime escape hatch for impossible tasks

Adds two MCP tools and a typed `Impasse` primitive so a producer that
discovers mid-execution that its task is structurally impossible can
emit a structured signal instead of inventing workarounds. The
orchestrator detects the impasse post-phase and either auto-delegates
to a suggested role (first attempt, `wrong_role` only) or escalates to
HITL (second attempt or non-`wrong_role`).

- `mcp__sdlc__check_file_restriction` — pure-local read against
  `shared/egg_restrictions/patterns.py`. Returns `can_write` plus
  `alternative_role` when exactly one producer covers the path. Lets
  the agent self-check before burning tokens on exploration.
- `mcp__sdlc__report_impasse` — persists a typed
  `egg_contracts.Impasse` (category, reason, suggested_role,
  blocked_files, evidence) under `AgentOutput.impasse`. Once called,
  the agent must exit cleanly without committing.
- `orchestrator/impasse_routing.py` — `collect_impasses` +
  `route_impasses`. Auto-delegate fires only for fresh tasks
  (`delegation_attempts == 0`) with a single eligible alternative
  producer role; everything else creates a HITL decision via
  `apply_mutation` with a delegate / cancel / manual-resolve /
  other option set.
- `_run_concurrent_phase_with_impasse_retry` wraps the existing
  slice-loop spawn so an `all_delegated` outcome triggers one BRC
  retry against the mutated contract; any escalation surfaces to
  the operator without auto-retry.
- Producer prompt picks up an "If a task is impossible, use these
  tools instead of inventing workarounds" section.

Test surface: schema round-trip, both handlers (allowed/blocked/
batch/error paths), routing helper (delegate, second-impasse HITL,
plan_bug / external_blocker / unresolved-task escalations,
self-delegation defense), and the existing tool-registry +
CLI-drift gates updated for the two new no-CLI verbs (rationale
in handler docstrings per decision-13).

* Address PR #2553 review: producer escape hatch + routing hardening

Blocking fix:
- Move runtime escape-hatch instructions out of the task_planner-only
  _build_role_restrictions_section into a new
  _build_impasse_escape_hatch_section, then inject it into the coder
  prompt (early-return branch) and the tester / documenter prompts
  (post-phase-restrictions branch). Producers — the only roles that
  emit impasses — now actually see check_file_restriction /
  report_impasse guidance instead of inventing workarounds. The
  planner keeps a brief post-failure-delegation summary so it knows
  the auto-delegation path exists; planners do not emit impasses.
- Add end-to-end TestProducerEscapeHatchInPrompts coverage that
  parametrises over coder/tester/documenter and asserts both tool
  names plus the "DO NOT invent workarounds" header appear, and that
  architect / planner stay free of the actionable producer-only
  directive.

Non-blocking fixes:
- Routing: route_impasses gains a force_escalate kw. The slice-loop
  wrapper sets it on its terminal iteration so a delegation that
  cannot re-run a BRC cycle gets escalated to HITL rather than
  silently mutating the contract and exiting on a stale role
  assignment.
- Routing: drop the 120-char truncation of impasse.reason in
  _record_delegate's audit-log entry. The schema caps reason at 2000
  chars and the audit log can hold the full payload — preserve it
  for post-mortem debugging.
- Slice loop: clear the impasse field from each producer's per-
  pipeline agent-output file between iterations. save_agent_output's
  mode="w" already overwrites when a producer respawns and reaches
  its handoff write, but a producer that crashes pre-handoff in
  iter-N+1 would otherwise let iter-N's impasse persist and
  re-trigger routing as a spurious "second impasse on same task"
  HITL.
- Handler: reject category="wrong_role" without suggested_role at the
  mcp__sdlc__report_impasse boundary. Without it the orchestrator
  router can only escalate, which silently degrades the producer's
  deliberately set wrong_role signal — point the agent back at
  check_file_restriction so the fix lands in the same iteration.
- Handler: also require task_id for category="wrong_role". The
  router's role-match fallback is fragile when a slice has multiple
  tasks per role or role-less tasks; explicit task_id eliminates
  guesswork on the auto-delegation path. Other categories keep
  task_id optional.
- Pipelines: comment the monolithic-implement fallback (the second
  _run_concurrent_phase call in the implement handler) explaining
  that auto-delegation is intentionally scoped to the slice loop
  since it rewires a task within a slice.

Closes review feedback items 1-7 on PR #2553.

* Address PR #2553 re-review: docs drift + cleanup test

Address two of the three non-blocking suggestions from the approve
re-review on commit 696d392.

* docs/reference/agent-tools.md: mcp__sdlc__report_impasse row now
  documents that task_id and suggested_role are mandatory for
  category=wrong_role (handler raises HandlerError when either is
  missing). Other categories keep both fields optional since they
  always escalate to HITL.
* orchestrator/routes/pipelines.py: extract the per-pipeline
  agent-output cleanup closure to a module-level helper named
  _clear_stale_impasses_for_producers so it can be unit tested
  directly. Behaviour is identical — the helper drops the impasse
  field after every successful all-DELEGATE iteration.
* orchestrator/tests/test_pipeline_impasse_cleanup.py: new file with
  five focused tests covering the cleanup happy path, the no-impasse
  no-op path, the missing-output-file path, multi-producer cleanup
  in one pass, and per-pipeline scoping.

The third suggestion (mixed-decision iter-0 still wastes a DELEGATE
role flip) was explicitly flagged "Worth a follow-up issue, not
blocking here" by the reviewer — filed as #2563.

* Address PR #2553 minor observations: type annotation + hoist imports

Two non-blocking observations from the third review (commit 7da68cf):

- Annotate `producer_roles` on `_clear_stale_impasses_for_producers`
  as `list[ContractAgentRole]` so a future caller sees the expected
  element type without grepping the call site. Quoted under TYPE_CHECKING
  + `# noqa: UP037` to match the file's existing pattern for
  `ContainerSpawner` (lines 413, 684, 6316, 6707, 7021).
- Hoist `load_agent_output` / `save_agent_output` imports from inside
  the helper to module level, mirroring `impasse_routing.py:50` which
  already imports `load_agent_output` directly. The seam fallback is
  unused at runtime (egg_contracts is the actual installed package, not
  shared.egg_contracts) and impasse_routing.py confirms a plain
  module-level import works.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>

* [slice-1] Add context PR + per-slice BRC history (closes #2548) (#2555)

* Add PRMetadata.context_* fields + planner prompt updates (#2548)

slice-1 / task-1-1 + task-1-3 — the foundation slice for the context-PR
mechanism. Subsequent slices build the gateway primitive, the
orchestrator hook, and the slice-1 base rewiring on top of these
fields.

Schema 1.1 — extends ``PRMetadata`` with four optional fields:
- ``context_title`` / ``context_description`` — planner-emitted
  framing for the dedicated context PR (e.g. "Strategic plan for #N"
  vs the slice's "Implement …"). Both fall back to ``title`` /
  ``description`` when omitted.
- ``context_branch`` / ``context_pr_number`` — orchestrator-populated
  runtime values (the ``egg/<id>/context`` branch name and the GitHub
  PR number once the context PR has been opened). Planners must NOT
  emit these.

Bumps ``Contract.schemaVersion`` default from ``"1.0"`` to ``"1.1"``
and adds an ``after``-mode migration shim that promotes pre-1.1
contracts to 1.1 on load. The bump is purely additive — pre-1.1 JSON
loads cleanly with the new fields defaulting to ``None``.

Plan-parser plumbing — ``ParseResult`` grows ``pr_context_title`` /
``pr_context_description`` and a new ``extract_pr_context_metadata_from_yaml``
helper extracts the optional keys without breaking the existing
``extract_pr_metadata_from_yaml`` 5-tuple signature (and the
~10 callers + tests that unpack it).

Planner prompt — both planner-prompt sites in ``pipelines.py`` (the
plan-phase prompt under ``_build_phase_prompt`` and the
task_planner-role prompt under ``_build_agent_prompt``) gain the
``_PR_CONTEXT_GUIDANCE`` paragraph and the ``_PR_CONTEXT_YAML_EXAMPLE_LINES``
commented-out hints inside the ``pr:`` YAML block. Both helpers are
defined once next to ``_PR_DESCRIPTION_GUIDANCE`` so the two prompt
sites stay in sync when the guidance evolves.

Contract populator — ``_populate_contract_from_plan`` now copies
``result.pr_context_title`` / ``pr_context_description`` onto the new
PRMetadata it builds, and preserves any orchestrator-populated
``context_branch`` / ``context_pr_number`` across re-populates so a
later plan re-parse does not blow away runtime state set by slice-3's
hook.

Test impact: bumping the default ``schemaVersion`` to ``"1.1"`` causes
``tests/shared/egg_contracts/test_models.py::test_minimal_contract``
to fail on the literal ``"1.0"`` assertion. The fix-up belongs to
the tester role (task-1-2) along with the new ``PRMetadata.context_*``
round-trip coverage; coder boundaries forbid pushing test edits.
Lint (ruff format + check) and mypy delta are clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add PRMetadata.context_* test coverage + 1.0→1.1 migration tests (#2548)

slice-1 / task-1-2 — adversarial + regression coverage for the four new
optional ``PRMetadata.context_*`` fields and the ``schemaVersion``
1.0→1.1 promotion shim added by the coder in commit 75d8ca09c.

Coverage:
* ``TestPRMetadataContextFields`` — defaults to None, full round-trip
  with all four fields populated, omitted-keys round-trip preserves
  None.
* ``TestPRMetadataContextPRNumberValidator`` — pins the ``ge=1``
  validator: 0/-1 are rejected at construct AND at setattr (under the
  shared ``EggContractBaseModel.validate_assignment=True`` from #2490);
  None and large positive ints accepted.
* ``TestPRMetadataSchemaVersionMigration`` — 1.0 payload loads with
  context defaults, dump→reload chain stays at 1.1, default is 1.1,
  legacy ``deferred_actions`` survive migration, and an unrecognized
  version (1.2 / 2.0) is NOT silently downgraded.
* ``TestPRMetadataContextEmptyStringSemantics`` — empty strings are
  accepted at the model layer so the orchestrator hook's
  ``context_title or title`` fallback works for both None and "".
* ``TestPlanParserContextFieldExtraction`` — covers task-1-3's
  ingestion path: ``extract_pr_context_metadata_from_yaml`` returns
  None pair for missing/None/absent inputs; collapses whitespace to
  None; warns on non-string ``context_title``; ``parse_plan`` threads
  the values onto ``ParseResult.pr_context_*``.

Also updates ``test_models.py::test_minimal_contract`` from the literal
``"1.0"`` schemaVersion assertion to ``"1.1"`` — the coder flagged this
as a known follow-up in commit 75d8ca09c (coder cannot push test edits
under the role boundary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review feedback on PR #2555 (#2548)

Blocking fix:
- _populate_contract_from_plan now also preserves PRMetadata.deferred_actions
  alongside context_branch / context_pr_number. The conditional-ACK gate at
  decisions.py:complete_phase writes deferred actions; the populator's
  start_phase=implement re-entry path was silently wiping them, erasing
  the merge-blocking Pre-merge Obligations handoff. Add a regression
  test in orchestrator/tests/test_short_flow_contract_population.py.

Non-blocking improvements:
- extract_pr_context_metadata_from_yaml now warns symmetrically on
  non-string context_description (mirrors the context_title branch),
  preventing silent str() coercion of structured planner values.
- Updated schemaVersion / _migrate_schema_version_to_1_1 docstrings to
  reflect that the bump fires at every load (mode="after"), not lazily on
  next save, and to acknowledge the migration is silent (no audit entry).
- Aligned TestPRMetadataContextEmptyStringSemantics docstring with reality
  (planner path collapses empty strings to None; only hand-edited or
  migrated payloads can produce a "" PRMetadata).
- New tests for the symmetric context_description warning.

* docs: document schema 1.1 and pr.context_* fields (#2548)

Slice-1 lands the schema delta + planner-prompt update half of the
context-PR mechanism (#2548): `PRMetadata` grows four optional
`context_*` fields and `Contract.schemaVersion` defaults to `"1.1"`
with an additive `1.0 → 1.1` migration. The actual context-PR
mechanism (branch creation, PR opening, slice-1 base wiring) is
implemented in slices 3-4 and gets its own end-to-end documentation
pass in slice-5.

This commit updates the docs that reference contract examples and the
yaml-tasks `pr:` block so they reflect the slice-1-landed schema
state:

- `docs/templates/plan.md`: add optional `context_title` /
  `context_description` keys to the yaml-tasks `pr:` example as
  commented-out hints, plus a new prose blockquote explaining when
  planners may emit them and which sibling fields
  (`context_branch`, `context_pr_number`) are orchestrator-populated.
- `docs/architecture/sdlc-pipeline.md`: bump the example
  `schemaVersion` from `1.0` to `1.1` and add a "Schema 1.1 (#2548)"
  blockquote summarising the additive migration.
- `docs/guides/sdlc-pipeline.md`: same `schemaVersion` bump in the
  example JSON plus a short blockquote pointing readers at the
  migration semantics.

The PR-stack diagrams, BRC-history file naming, and slice-1-base
discussion in `docs/guides/concurrent-execution.md`,
`docs/architecture/orchestrator.md`, `docs/reference/orchestrator-cli.md`,
and `docs/guides/babysit-pr.md` remain untouched — those describe
behavior that does not yet exist on this branch and are slice-5's
responsibility once the mechanism is wired end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Allowlist plan_parser.py for file-size hard cap on egg/issue-2548/work (#2548)

Slice-1 (foundation) tester NACK: on the egg/issue-2548/work merge target,
slice-1's extract_pr_context_metadata_from_yaml + ParseResult.pr_context_*
plumbing stacks on top of #2527's validate_task_role_alignment additions,
pushing shared/egg_contracts/plan_parser.py to ~1,530 lines and breaching
the 1,500-line hard cap that scripts/check-file-sizes.py enforces. The
slice-1 branch alone is at 1,388 lines (clean), but the work-branch state
that the lint actually runs against is over.

Fix per reviewer_contract's forward-looking concern and tester's blocking
finding: add the file to scripts/file-size-allowlist.yaml under #2548 so
make lint passes during the slice-1 BRC. Decomposition is tracked under
the same issue and is the cheaper of the two unblock option…
@james-in-a-box

james-in-a-box Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Review response

Thanks for the re-review and approval. The single non-blocking docstring nit is addressed in e021a64.

Non-blocking

  • test_extract_warns_on_list_typed_context_title docstring still cited _normalize_optional_string for a code path that doesn't call itfixed-in-PR (commit e021a64). Reworded the docstring at tests/shared/egg_contracts/test_pr_metadata.py:750-768 to enumerate both possible regression modes for the title branch, matching the actual production code path: today, without the parser-layer isinstance(raw_title, str) guard, raw_title.strip() would raise AttributeError on a list (the title branch does not call _normalize_optional_string — only the description branch does); a future refactor that swapped in _normalize_optional_string for symmetry with the description branch would silently coerce as "['a', 'b']". Either way the warning path needs to fire first; the test pins it explicitly. The description-variant docstring's "round-trip through _normalize_optional_string" framing is correct and unchanged.

Deferred follow-ups — still tracked

  • #2560 (defense-in-depth ParseWarning for planner-emitted pr.context_branch / pr.context_pr_number) — open from the first review round.
  • #2568 (sweep phases/phase-Nslices/slice-N across docs/guides/sdlc-pipeline.md per #2137) — open from the second-to-last review round.
  • #2569 (decompose shared/egg_contracts/plan_parser.py) — open from the prior review round.

— Authored by egg

@james-in-a-box

james-in-a-box Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

egg feedback addressed. View run logs

21 previous review(s) hidden.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant