Egg/issue 2548/work - #2572
Conversation
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.
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).
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.
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.
…er 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 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>
* 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 3827cb5), 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 (#2533 (review)): - 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 a423d31 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 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>
Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com> Co-authored-by: egg <egg@localhost>
…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 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>
* 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". --------- 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>
* 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>
There was a problem hiding this comment.
No agent-mode design concerns.
Reviewed the new/changed surfaces against the agent-mode design guidelines:
orchestrator/impasse_routing.py(new) — pure orchestration over the typedImpassepayload the sandbox handler emits. No LLM calls, no prompt construction.sandbox/egg_agent_tools/handlers/restrictions.py(new) — sandbox-side fast-fail helper backed by the sameshared/egg_restrictions/patterns.pyregistry the gateway commit-validator enforces. This is correctly a sandbox/gateway boundary, not a prompt-level instruction.shared/egg_contracts/plan_parser.py+docs/templates/plan.md— the YAML appendix is consumed by the orchestrator (Slice/Task contract objects), not by humans, so the structured-output requirement is legitimate. Human-facing prose above it stays free-form.orchestrator/routes/pipelines.py— the new_read_phase_draftembed in implement-phase prompts (line 9748) is capped at 32K chars and only fires onreview_cycle == 0. It's borderline but bounded, justified by the comment ("avoids file-I/O turns inside the sandbox"), and provides orientation rather than constraining what the agent can fetch. Not a clear anti-pattern.- No new direct Anthropic API calls in orchestrator/gateway/shared. No new pinned model identifiers. No new post-processing pipelines parsing natural-language agent output.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Contract Verification — PR #2572 (issue #2548)
This review verifies the merged PR against the contract at
.egg-state/contracts/issue-2548.json. Posted as a comment because
the PR has already merged into main — request-changes / approve are
no-ops at this point. Verdict at the bottom.
TL;DR
- Slice-1 implementation (PRMetadata + planner prompt) matches its
contract acceptance criteria. The model delta, schema-version bump,
migration shim, tests, planner prompt, and YAML ingestion are all in
place and behave as specified. - Contract integrity is broken. The on-disk contract is still in
current_phase: refinewith all 14 tasks across all 5 slices in
status: pendingand zero linked commits. Top-level
acceptance_criteria: []— there is nothing for
egg-contract verify-criterion --criterion ac-Nto mark, and
egg-contractis in any case unreachable
(Orchestrator unreachable — try again). - Scope is much larger than slice-1 of #2548. The merged diff
includes work for issues #2532, #2549, #2474, #2527, #2554, #2529
alongside slice-1 (and apparently slice-2) of #2548. None of those
appear on the #2548 contract. Reviewers approaching this PR from
the contract have no traceable mapping from the contract's tasks to
the merged commits.
Method
- Loaded
.egg-state/contracts/issue-2548.jsondirectly (orchestrator
unreachable, soegg-contract showcould not be used). - Walked each slice / task
acceptance_criteriablock and checked
the merged diff (gh pr diff 2572) against the criteria. - Spot-checked the slice-1-specific child PR (#2571) to disambiguate
issue-#2548 work from the rollup of unrelated merges this work
branch caught up to.
Slice-by-slice findings
Slice-1 — Contract schema delta + planner prompt
task-1-1 (PRMetadata + schemaVersion 1.0 → 1.1) — VERIFIED.
shared/egg_contracts/models.pyadds the four required fields with
the prescribed defaults:context_title: str | None = Nonecontext_description: str | None = Nonecontext_branch: str | None = Nonecontext_pr_number: int | None = Nonewithge=1validator
(matches the recommendation in the original task description).
Contract.schemaVersiondefault bumped from"1.0"to"1.1".- Model-level migration
_migrate_schema_version_to_1_1runs in
mode="after", idempotent, only fires on exactly"1.0". - Acceptance criteria for task-1-1 are met:
- The four fields are exposed with
Nonedefaults. - A 1.0 contract round-trips and gets promoted to 1.1.
- The four fields are exposed with
task-1-2 (PRMetadata tests) — VERIFIED, with one deviation.
- Tests live at
tests/shared/egg_contracts/test_pr_metadata.py
(906 lines), notshared/egg_contracts/tests/test_pr_metadata.py
as the contract'sfiles_affectedlists. The test docstring
documents this as intentional — thetests/...tree is what
pytestcollects, and the in-package path is not intestpaths.
This is a sensible pragmatic deviation; flagging it for traceability. - Coverage matches the acceptance criteria:
- Round-trip with all four context fields populated.
- Round-trip with all four context fields omitted (defaults
None). - Round-trip of
schemaVersion=1.0payload, confirming migration to
1.1andNonedefaults. context_pr_number=0and negative values raiseValidationError;
Noneis accepted;validate_assignmentre-runs onsetattr.
- Tests live in the diff, would discover under
make test/
make test-all.
task-1-3 (Planner prompt + YAML ingestion) — VERIFIED, with
one minor scope deviation.
orchestrator/routes/pipelines.pyadds:_PR_CONTEXT_GUIDANCE— prose explaining
pr.context_title/pr.context_description, when to emit them,
and thatpr.context_branch/pr.context_pr_numberare
orchestrator-populated (must NOT be emitted by the planner)._PR_CONTEXT_YAML_EXAMPLE_LINES— commented-out YAML hints
threaded into the# yaml-tasksexample inside_build_phase_prompt.
shared/egg_contracts/plan_parser.pygains
extract_pr_context_metadata_from_yaml()and threads
pr_context_title/pr_context_descriptionthroughParseResult
andparse_plan(). Non-string scalars produceParseWarningrather
than coercing silently.- The acceptance criterion "A planner-emitted YAML containing
context_title:andcontext_description:is parsed without error
and the values land oncontract.pr.context_title/
pr.context_description" is satisfied by the new extractor + the
optional fields onParseResult. - The acceptance criterion "A planner-emitted YAML omitting the new
keys still parses and both context fields default toNone" is
satisfied by theis Noneearly returns in the extractor. - Deviation:
.github/scripts/checks/plan_yaml_check.pyis not
modified in this PR — but inspection of the file shows it is a
structure-only validator (presence ofphases:/tasks:/
required ID fields). It does not reject unknownpr.*keys, so
the new keys pass through it untouched. The criterion
"plan_yaml_check.pyruns cleanly on both inputs" is therefore
satisfied de facto, but the contract'sfiles_affectedlisted it,
and the file was not touched. Non-blocking. - Deviation:
orchestrator/routes/phases.pyis also not modified.
Inspection showsphases.pydoes not have an independent
yaml-tasksingestion path — the canonical path is
plan_parser.py. Thefiles_affectedlisting in the contract
appears to have been speculative. Non-blocking.
Slices 2–5 — out of scope for this PR per the contract's task list
The contract's tasks 2-1 through 5-1 are all status: pending with no
linked commits. However, the merged diff clearly contains work that
overlaps slice-2 (BRC-history per-slice writes —
orchestrator/routes/pipelines.py _write_brc_history,
orchestrator/tests/test_brc_history.py +1073/-57) and surrounding
infrastructure. This is consistent with the child PR #2571 commit
[slice-2] Add context PR + per-slice BRC history (closes #2548) (#2564).
Without a contract-tracked mapping, I cannot reliably attribute lines
to slice-2/3/4/5 acceptance criteria. The contract simply does not
reflect what was merged. This is the contract-integrity issue called
out in the TL;DR — the tasks were never complete-task'd, no commits
were add-commit'd, and the phase was never advanced past refine.
Cross-cutting work that does NOT belong to issue #2548
The PR also bundles, per its commit list:
f94c7c1cFix #2532: align .github/ block in agent_roles.py5582aa03Fix #2549: skip already-merged slices (#2552)3cb7aef5Egg/issue 2474 v2/work (#2556)f87a5ea7Fix #2527: validate task role↔file alignment (#2551)14cf8879docs for #2527 (#2558)08fb8f8cFix #2554: agent-status table on BRC dashboard802dc2c9Fix #2529: runtime escape hatch for impossible tasks (#2553)
These show up in the diff (e.g. orchestrator/impasse_routing.py +554,
shared/egg_contracts/impasse.py +156, shared/egg_contracts/agent_roles.py,
shared/egg_contracts/plan_parser.py role alignment, etc.) and are
unrelated to issue #2548's contract. Some of them appear to have already
been merged to main via their own PRs (#2552, #2553, #2556, #2558),
so they are showing up here only as catch-up merges into the
long-running egg/issue-2548/work branch. That's how gh pr diff works
against main, not a sign that they were re-merged. Worth noting for
the reviewer audit trail.
Concerns / red flags
- Contract was not advanced. The on-disk contract is still in
current_phase: refine, all taskspending, no commits linked.
The orchestrator is unreachable so this could not be reconciled
live. A future agent or operator looking at this contract will
see "no work done" and that is materially false. - Top-level
acceptance_criteria: []. Theegg-contract verify-criterion
surface only acceptsac-NIDs against the top-level list, which is
empty. There is therefore no programmatic way to record the
verifications I performed in this review against the contract; the
trail lives only here. - Test path differs from contract. Tests are at
tests/shared/egg_contracts/test_pr_metadata.py(correct for
pytest discovery) instead ofshared/egg_contracts/tests/test_pr_metadata.py
(asfiles_affectedlisted). The author documents this in the test
docstring; not a blocker. .github/scripts/checks/plan_yaml_check.pyand
orchestrator/routes/phases.pynot modified. The contract's
files_affectedlisted both. The check is structure-only and does
not need updating to admit the new keys;phases.pydoes not have
an independent ingestion path. Non-blocking but flagged.- PR scope is much larger than the contract describes. Slice-2-5
work and unrelated bug-fix merges are bundled into the same merge
commit onmain. Reviewer hygiene around tracking what landed is
degraded.
Verdict
Comment — the slice-1 implementation is correct and matches its
acceptance criteria, but the PR cannot be cleanly verified against the
contract because (a) the contract was never advanced past refine,
(b) the orchestrator is unreachable to mark verification status,
(c) the merged diff materially exceeds slice-1's stated scope. The
implementation looks good; the process trail is broken.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review: PR #2572 (egg/issue-2548/work → main)
Scope: 80 files, +14,941 / −3,263. The bulk is auto-generated .egg-state/ artifacts (skipped per review rules) plus a docker-compose → k3s test-infra cleanup that's already justified in code (#2474). Production changes I focused on: the new Impasse runtime escape-hatch (shared/egg_contracts/impasse.py, orchestrator/impasse_routing.py, sandbox/egg_agent_tools/handlers/restrictions.py), the planner role↔file alignment validator (shared/egg_contracts/plan_parser.py, orchestrator/routes/signals.py), and the schema additions for the forthcoming context-PR (shared/egg_contracts/models.py).
Blocking
1. report_impasse handler does not enforce Impasse.reason length → orchestrator silently drops the impasse.
sandbox/egg_agent_tools/handlers/restrictions.py:208-210 only checks non-empty string, but egg_contracts.impasse.Impasse.reason is declared min_length=1, max_length=2000 (shared/egg_contracts/impasse.py:80-88). When an agent submits a multi-page traceback as the reason — exactly the natural thing to do for an external_blocker or unknown category — the handler returns ok: True and persists the over-long payload to the agent-output file. Post-phase, orchestrator/impasse_routing.py:138-147 calls Impasse.from_dict(impasse_raw), Pydantic raises ValidationError, the orchestrator logs "Discarding malformed impasse payload" at warning level, and the impasse is silently dropped. The producer's container has already been told to "Stop all further work for this task and exit cleanly" — so the task stalls with neither delegation nor HITL escalation, and the only signal is a single warning line in the orchestrator log.
This is the exact "operator-facing silent failure" / "cross-module dead-end" pattern the review rules call out: producer emits, downstream filter drops, no signal. Fix at restrictions.py:208-210: enforce len(reason) <= 2000 (and validate task_id shape if there is one) so the failure surfaces synchronously to the agent rather than being absorbed by a server-side warning.
While you're in this area, consider also capping evidence (line 247-249) — evidence: dict[str, Any] accepts arbitrary nesting/size, and Impasse.from_dict doesn't enforce a JSON-serialised limit either; an agent stuffing megabytes of structured data here would bloat HITL decision bodies.
2. Pre-existing Python 2-style except clauses survive in code this PR modifies.
shared/egg_contracts/plan_parser.py:529 (except ValueError, TypeError:) and shared/egg_contracts/agent_roles.py:1013 (except ValueError, KeyError:). These compile on 3.14 only because of PEP 758 (the project's requires-python = ">=3.14"), but:
- Anyone reading the diff without PEP 758 in mind will read these as Python-2 bugs (in pre-3.14 Python they'd be a SyntaxError or, worse, parse as
except ValueError as TypeError:and silently bind to the second name). - Most linters / type-checkers / IDE syntax highlighters ship parser tables that haven't tracked PEP 758 yet —
mypy,pyflakes, and Tree-sitter Python all flag these as errors at the time of writing. Anyone runningmake lintagainst a not-yet-updated venv will hit syntax errors here. - This PR rewrites a large fraction of
agent_roles.py(+24/-30) — it's the natural place to normalize.
Fix: parenthesize. except (ValueError, TypeError):, except (ValueError, KeyError):. Per the review rules' "pre-existing inconsistency in modified code" clause this is blocking — the PR is in the file already, do it now.
Non-blocking (but should be fixed soon)
3. git show in planner role-alignment validator omits -- separator.
orchestrator/routes/signals.py:997:
["git", "-C", str(worktree_path), "show", f"{commit_sha}:{plan_rel}"]commit_sha comes from the planner's BRC propose payload — (payload.get("commit_sha") or "").strip() at line 957 — with no shape validation. The sibling _verify_commit_on_branch at line 273 uses "--" correctly:
["git", "-C", ..., "branch", "-r", "--contains", "--", commit]For a SHA starting with -, git show parses it as an option (e.g. --ext-diff, -c key=val, etc.). The threat model is weak — the planner agent already has subprocess access in its sandbox, so this isn't a privilege escalation — but the asymmetry vs _verify_commit_on_branch is a regression in defensive hygiene. Fix: insert "--" before f"{commit_sha}:{plan_rel}", or re.fullmatch(r"[0-9a-f]{7,64}", commit_sha) upstream.
4. Silent git show non-zero / ImportError swallow loses operator signal.
Same function, signals.py:1003-1004:
if result.returncode != 0:
return # Plan not present at this commit — nothing to validate.This conflates "plan absent at commit" (legitimate) with "permission denied / git timeout / corrupted worktree" (bugs the operator wants to know about). Same at signals.py:1020-1021 — bare except ImportError: return for a routes/pipelines import. The function's docstring says "graceful degradation" — fine for the gateway-backstop case, but log result.stderr.strip() at debug or warning level so post-mortem forensics has something to read.
5. restrictions.py accepts caller-supplied identifier / repo_path without validation.
sandbox/egg_agent_tools/handlers/restrictions.py:268-271:
repo_path = Path(req.get("repo_path") or get_repo_path())
identifier = req.get("identifier") or req.get("issue") or req.get("pipeline_id")Both flow straight into save_agent_output(repo_path, contract_role, output, identifier=identifier) (line 316) which interpolates identifier into a filename: .egg-state/agent-outputs/{identifier}-{role}-output.json. A malicious identifier="../../../../tmp/foo" writes outside the agent-output dir; a malicious repo_path writes outside the repo. The agent already has full filesystem access in its sandbox so this isn't privilege escalation, but it's inconsistent with the very recent _resolve_env_identifier_for_brc_history hardening (sandbox/egg_agent_tools/handlers/brc.py:855-882) that ignores caller-supplied identifier and pulls only from env. Same hardening should apply here — particularly because a path-traversal write could spoof another role's agent-output file (the orchestrator's role-keyed loader trusts the filename → role mapping).
6. _alternative_role returns a producer role even when blocked_role is non-producer.
restrictions.py:47-67 loops for role in ("coder", "tester", "documenter") and skips the matching role. If an architect or overseer calls check_file_restriction, it gets a "helpful" alternative_role suggestion that the orchestrator-side router (impasse_routing._is_eligible_delegation, gated on _DELEGATION_ELIGIBLE_ROLES = {"coder", "tester", "documenter"}) will refuse to honor anyway. Return None early when blocked_role not in {"coder", "tester", "documenter"} so the agent gets honest "no auto-delegation possible" UX.
7. route_impasses swallows save_contract failure but returns in-memory decisions.
orchestrator/impasse_routing.py:393-401:
if mutated:
try:
save_contract(contract, repo_path)
except Exception as exc: # pragma: no cover - defensive
logger.error("Failed to persist contract after impasse routing", ...)
return decisionsThe caller proceeds with the in-memory decisions list, but on disk the contract is unchanged. If the orchestrator restarts before the next persist boundary, the routing decision (delegation or HITL escalation) is lost — the next iteration scans agent-output files, finds the same impasse, and re-routes. The force_escalate=True terminal path is particularly affected: an escalation that doesn't persist creates no operator-visible HITL decision. Either retry, or surface the failure to the slice-loop's exit code so it doesn't keep walking.
8. Decision IDs derived from len(contract.decisions) collide if anything else ever mutates the list.
impasse_routing.py:248-250 builds decision_id = f"decision-{next_idx + 1}" and field_path = f"decisions.{next_idx}" from the current length. Inside a single route_impasses call this is fine because apply_mutation appends and the loop re-reads length each iteration. But the contract is also mutated by HITL resolution paths and other routers; if any path ever shrinks decisions (e.g. a future cleanup task), this generator collides silently. Use a UUID or secrets.token_hex(4)-suffixed ID.
9. _find_task falls back to "single role match" with (t.role or "coder") == role.
impasse_routing.py:183-187. A task with role=None matches a coder impasse but not a tester/documenter impasse. If the planner ever emits role-less tasks (it currently shouldn't but neither the type nor the schema forbids it), tester/documenter impasses fall through to force_escalate instead of routing correctly. Either normalize task.role at plan-ingestion or document this assumption with an assert at the start of _find_task.
10. New tests in shared/egg_contracts/tests/ may not be picked up by make test.
shared/egg_contracts/tests/test_validate_task_role_alignment.py (286 lines, new) lives next to the source rather than in the tests/ tree configured under [tool.pytest.ini_options].testpaths. The comment at tests/shared/egg_contracts/test_pr_metadata.py:21-25 (also new in this PR) explicitly notes that the in-package test path is not in testpaths. Verify make test picks these up — run .venv/bin/pytest --collect-only shared/egg_contracts/tests/ and confirm. If not, either move the file under tests/ or add the path to testpaths. Same caveat applies to shared/tests/test_github_block_alignment.py and shared/tests/test_impasse_schema.py.
11. integration_tests/test_credential_security.py silently skips under k3s.
integration_tests/conftest.py skips isolated_container / external_container / test_container fixtures (their docstrings acknowledge this) and the conftest itself notes "the main consumer is currently uncovered in CI and needs a k3s-native replacement before it runs again." This is a security-critical property (credential isolation) producing green CI without exercising the property. Not blocking for this PR — it landed via #2474 — but file/link a follow-up issue and convert the skips to xfail(strict=True) so the gap surfaces.
Notes (not findings)
- The "context PR" feature is intentionally inert at this slice —
docs/architecture/sdlc-pipeline.md:132-138anddocs/templates/plan.md:136-150explicitly disclose that only the schema fields and planner-prompt advertisement are wired in slice-1; orchestrator branch-creation/PR-opening hooks land in slices 3-4. The_populate_contract_from_planpreservation block atroutes/pipelines.py:15771-15786and the planner-prompt guidance at:9379-9405are forward-compatible stubs, not silent dead-ends. Honest, not aspirational. - The
ImpassePydantic model is well-shaped:extra="forbid",min_length=1/max_length=2000on reason,default_factory(not mutable defaults) on list/dict fields. Tests cover empty-reason, unknown-category, and extra-field rejection at the model boundary. - The
.github/block alignment work (agent_roles.pyrewrite +test_github_block_alignment.py) cross-checksegg_contracts.agent_roles.get_file_patternsagainstegg_restrictions.get_agent_patternfor all 11 roles — clean. - The plan-time
_is_file_blocked_for_rolemirror of the gateway's push-timeFileRestriction.is_file_blockedis the right shape (1:1 normalization, blocked + block-exempt patterns, rejects../ leading-slash).
The two blocking items (silent over-long-reason drop, and the pre-existing PEP 758 except clauses in code this PR modifies) should be straightforward. The defense-in-depth items (3, 4, 5, 6) are also small fixes worth bundling. Item 7 (save_contract failure swallow) and item 11 (credential isolation skip) deserve follow-up issues if not addressed here.
— Authored by egg
|
egg review completed. View run logs 2 previous review(s) hidden. |
No description provided.