[slice-1] Add context PR + per-slice BRC history (closes #2548) - #2555
Conversation
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>
There was a problem hiding this comment.
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=implementpipeline 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
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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.
Review responseThanks for the thorough review. Disposition per item below; all in-PR fixes shipped in 5a6209f. Blocking
Non-blocking
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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:
create_contract+ write plan draft →_populate_contract_from_plan(initial populate).- Simulate runtime state: mutate
deferred_actions,context_branch,context_pr_number;save_contract. - Re-run
_populate_contract_from_plan(thestart_phase=implementre-entry case I called out in the prior review). - Assert all three runtime fields survive AND the planner-emitted
titlerefreshes 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_titleandcontext_description(shared/egg_contracts/plan_parser.py:993-1007). The newisinstance(raw_description, str)branch emits aParseWarningciting the offending type name and falls back toNoneinstead of letting_normalize_optional_stringsilentlystr()-coerce. The two new tests (test_extract_warns_on_non_string_context_descriptionfordict,test_extract_warns_on_int_context_descriptionforint) pin the fix and the warning content.boolis implicitly covered because Python booleans are not strings underisinstance. -
Test docstring drift (
tests/shared/egg_contracts/test_pr_metadata.py:338-356). TheTestPRMetadataContextEmptyStringSemanticsdocstring now correctly distinguishes the planner path (collapses empty / whitespace strings toNone) from the model layer (permissive by design for hand-edited / migrated payloads). The test is retained as the defensivemin_length=1regression pin you suggested. -
"Next save" docstring drift (
shared/egg_contracts/models.py:670-676). The schema-version field description now accurately reads "promoted to1.1whenever they are loaded into the model; the new value is then persisted on the next save". The_migrate_schema_version_to_1_1docstring (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
PRMetadatafield 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.titlematches 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(...)inorchestrator/routes/pipelines.py(only one site), so the preservation fix does not need to be replicated elsewhere. The sibling assignment inorchestrator/routes/decisions.py:291is a create path (only fires whencontract.pr is None), not a re-populate, so it does not need a preservation block.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Review responseThanks for the re-review and approval. One item to acknowledge: Acknowledged
— Authored by egg |
This comment has been minimized.
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>
There was a problem hiding this comment.
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
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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-outcontext_title/context_descriptionkeys to the YAML template plus a "Context-PR framing (#2548)" blockquote.docs/architecture/sdlc-pipeline.md— bumps the exampleschemaVersionfrom1.0to1.1and adds a "Schema 1.1 (#2548)" blockquote.docs/guides/sdlc-pipeline.md— sameschemaVersionbump 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.schemaVersiondefaults to"1.1"(models.py:667). ✓- The four
pr.context_*fields are present and optional withNonedefaults (models.py:403-434). ✓ _migrate_schema_version_to_1_1ismode="after", idempotent, and selectively triggers only on the literal value"1.0"(models.py:811-841). ✓- The plan parser only extracts
context_title/context_descriptionfrom the YAML —context_branch/context_pr_numberare intentionally not pulled from planner YAML (plan_parser.py:929-1009). ✓ - The preservation block in
_populate_contract_from_plan(pipelines.py:14938-14953) coverscontext_branch,context_pr_number, anddeferred_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_numberare 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 (phases → slices, phase-1 → slice-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 from5a6209ff→9de4bc1b. - 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 rebuildprwholesale beyond the two sites already covered (the populator atpipelines.py:14943and the create path atdecisions.py).
Approving.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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>
…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.
…/egg-exec-20260508-042216-3164/work
Review responseThanks for the re-review and approval. Disposition per non-blocking item below. Non-blocking
Deferred follow-up — still tracked
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
Re-review against the delta from 9de4bc1b → 81170228. Three PR-authored commits since the prior approval:
70173947— addsshared/egg_contracts/plan_parser.pytoscripts/file-size-allowlist.yaml(config-only unblock for the work-branch lint).70d3d418— adds slice-1 boundary forward-pointers to the twopr.context_*blockquotes indocs/architecture/sdlc-pipeline.mdanddocs/templates/plan.md(addresses the non-blocking forward-pointer note from the prior review).81170228— adds 12 adversarial unit tests intests/shared/egg_contracts/test_pr_metadata.pycovering JSON round-trips, combinedphases:→slices:+1.0→1.1migration, YAML-null threading, list-typed warnings forcontext_title/context_description, CRLF stripping, largecontext_pr_number,schemaVersionregex strictness, and short-circuit on non-dictpr: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
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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 throughmodel_dump_json+model_validate_json, exercises the on-disk path. ✓test_model_dump_json_preserves_null_context_fields— pinsNone→ JSONnull(no accidental futureexclude_none=True). ✓test_combined_phases_and_schemaversion_migration— exercises both themode="wrap"_migrate_phases_to_slicesvalidator (models.py:744) and themode="after"_migrate_schema_version_to_1_1validator (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~andnullthread throughparse_plan→extract_pr_context_metadata_from_yaml'sif raw_title is not None:guard atplan_parser.py:962-973rather 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) leavespr_context_*asNonebecauseyaml_dataisNoneatplan_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 hadint+dictfor description andintfor title). The production path is theisinstance(raw_*, str)check inplan_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\npreserved. Test assertiondesc == "multi-line \n body"matches the production trace exactly. ✓test_context_pr_number_accepts_large_int— pins that thege=1validator oncontext_pr_numberdoesn't grow ale=2**31-1ceiling. ✓test_invalid_schemaversion_format_rejected— pins thepattern=r"^[0-9]+\.[0-9]+$"regex onContract.schemaVersionagainst"1.0-rc1"and"v1.0". ✓test_legacy_1_0_with_explicit_context_fields_loads— pins that the_migrate_schema_version_to_1_1after-validator is additive: a hand-edited 1.0 contract with explicitcontext_*values bumps to 1.1 without erasing them. ✓test_extract_returns_none_when_pr_block_is_non_dict— pins the short-circuit atplan_parser.py:945-948(pr_datais a list, not a dict) — noAttributeError, returns(None, None, []). The duplicate-warning suppression vs.extract_pr_metadata_from_yamlis 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 preservesdeferred_actionsalongsidecontext_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 since9de4bc1b, 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 confusedcontext_titlewithfiles_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-depthParseWarningfor planner-emittedpr.context_branch/pr.context_pr_number) — open.#2568(sweepphases/phase-N→slices/slice-Nacrossdocs/guides/sdlc-pipeline.mdper #2137) — open.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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".
Review responseThanks for the re-review and approval. All three non-blocking items are addressed in 5ace86d. Non-blocking
Deferred follow-ups — still tracked
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
Re-review against the delta from 81170228 → 5ace86d3. 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.mdanddocs/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.py—test_extract_warns_on_list_typed_context_titledocstring reworded to cite the parser-layerisinstance(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
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
Re-review against the delta from 81170228d → 5ace86d3. 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#2569is open and contains the decomposition seams + acceptance criteria I asked for.docs/architecture/sdlc-pipeline.md:127-145anddocs/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-layerisinstance(raw_title, str)guard inextract_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_stringas"['a', 'b']"if theisinstance(raw_title, str)guard inextract_pr_context_metadata_from_yamlregressed."
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 NoneIf 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 raiseAttributeErroron.strip()or — if a future refactor swapped in_normalize_optional_stringfor 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-depthParseWarningfor planner-emittedpr.context_branch/pr.context_pr_number) — open from the first review round.#2568(sweepphases/phase-N→slices/slice-Nacrossdocs/guides/sdlc-pipeline.mdper#2137) — open from the second-to-last review round.#2569(decomposeshared/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 thedeferred_actionspreservation block is intact. ✓ - Confirmed
PRMetadata's field surface is unchanged from81170228d→5ace86d3— no new runtime-populated fields that could be silently dropped. ✓ - Confirmed
#2569exists, is open, and the linked acceptance criteria match the allowlist comment's claim. ✓
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
* 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>
* 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…
Review responseThanks for the re-review and approval. The single non-blocking docstring nit is addressed in e021a64. Non-blocking
Deferred follow-ups — still tracked
— Authored by egg |
|
egg feedback addressed. View run logs 21 previous review(s) hidden. |
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'sreview surface against
main. As a result the strategic narrative and theconsensus that approved each artifact never reach
mainat all.Changes
pr.context_*contract fields.PRMetadatagrowscontext_title,context_description,context_branch, andcontext_pr_number. The planner prompt now emits the new fields so thecontext PR can be framed independently from the slice PRs.
_write_brc_history()nowwrites
.egg-state/brc-history/<id>-implement-slice-<N>.{json,md}(onefile per slice). The aggregate
<id>-implement.{json,md}file isremoved — hard switchover, no backwards-compat (D4).
egg/<id>/contextfrom the pipeline's base branch (NOT hardcodedmain). After plan_gate approval the orchestrator commitsanalysis.md,plan.md, refine/plan BRC files, and refine/plan agenttranscripts onto that branch, then opens a doc-only auto-open PR
targeting the configured base branch.
parent_branchresolves toegg/<id>/contextinstead ofegg/<id>/work. Each slice'simplement-phase BRC
.json/.mdis committed to the slice integrationbranch 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.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/andgit log -- .egg-state/brc-history/onmainproduce a real audit trail once the context PR merges. Thework-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:
PRMetadatainshared/egg_contracts/models.pywith 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...shared/egg_contracts/tests/covering the newPRMetadata.context_*fields: - Round-trip aPRMetadatawith all four context fields populated. - Round-trip aPRMetadatawith all four context fields omitted (must default toNone). - Round-trip a contract serialised with...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 newpr.context_titleandpr.context_descriptionfields and rec...Test Plan
shared/egg_contracts/tests/— newPRMetadata.context_*field testsand schema-1.1 round-trip.
orchestrator/tests/test_brc_history.py— per-slice implement-phasewriter; assert no aggregate file is produced.
orchestrator/tests/test_create_slice_integration_branch.pyplus newtest_create_context_branch.py— gateway primitive for context branch.orchestrator/tests/test_context_pr.py(new) — orchestrator hook thatopens the context PR with correct base, head, title, body, and files.
orchestrator/tests/test_stacked_pr_reconciler.py— fallback prefersthe context branch over the work branch.
orchestrator/tests/test_pipeline_*.py— slice-1 base-resolutiontests assert
parent_branch == egg/<id>/context.make test(changeset-aware) on every slice;make test-allon theterminal slice.
PR is opened against the configured base branch with
analysis.md,plan.md, refine/plan BRC.json/.md, and refine/plan agenttranscripts in the diff.
base = egg/<id>/context..egg-state/brc-history/<id>-implement-slice-<N>.{json,md}and noaggregate
<id>-implement.{json,md}exists anywhere.and the orphan reconciler completes the rebase cleanly.
Manual Steps
Pre-merge: none beyond standard PR review. If any
.github/workflowchanges turn out to be required, the coder will stage them under
.github-staging/and the merge reviewer shouldgit mvthem into placebefore 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.