Skip to content

Fix #2529: runtime escape hatch for impossible tasks - #2553

Merged
jwbron merged 4 commits into
mainfrom
egg/issue-2529
May 7, 2026
Merged

Fix #2529: runtime escape hatch for impossible tasks#2553
jwbron merged 4 commits into
mainfrom
egg/issue-2529

Conversation

@jwbron

@jwbron jwbron commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds the Impasse typed primitive plus two MCP tools (mcp__sdlc__check_file_restriction, mcp__sdlc__report_impasse) so a producer that discovers mid-execution that its task is structurally impossible can emit a structured signal instead of inventing workarounds (the .github-staging/ deletion-marker pattern from pipeline issue-2474-v2).
  • Orchestrator detects the impasse post-phase and auto-delegates to the suggested role on the first attempt (mutating task.role + bumping a new delegation_attempts counter), then re-runs the slice's BRC cycle. Second impasse on the same task — or any non-wrong_role category, no eligible alternative role, or unresolvable task_id — creates a HITL decision via apply_mutation and lets the operator decide between delegate / cancel / manual-resolve.
  • Producer prompts get a new section telling agents to call check_file_restriction before exploring a suspect file and emit report_impasse (not a workaround) when blocked.

The runtime escape hatch is independent of #2527 (plan-time prevention) — it's the defense-in-depth path for cases the planner check misses, plan bugs surfaced during execution, external blockers, and #2528 per-repo pattern drift.

Scoping per the latest comment: the orchestrator pre-validation option (option 2 in the original issue) is largely subsumed by #2527 and intentionally not added here. The deeper coupled-test population (~50% of BRC histories per #2530 audit) is tracked for a includes_tests opt-in follow-up — also not in scope.

Notable design choices

  • Why a top-level AgentOutput.impasse instead of a key in handoff_data: typed schema, drift-tested round-trip, and the orchestrator can scan a single field across producers without parsing untyped dicts.
  • Why Role.SYSTEM owns phases.*.tasks.*.role + delegation_attempts: producers must not be able to rewrite their own task assignment mid-flight. The agent emits a suggestion via Impasse; the orchestrator applies it.
  • _DELEGATION_ELIGIBLE_ROLES = {coder, tester, documenter}: cross-phase roles (overseer / autofixer / conflict_resolver / inspector) are not valid auto-delegation targets — auto-delegation rewires a producer task within the implement phase, not across phases.
  • HITL options on escalation: delegate (only when suggested_role is in the producer trio) / cancel slice + re-plan / resolve underlying blocker manually / other. Mirrors the existing register_open_question shape, with the orchestrator-side actor distinguishing it from agent-emitted decisions in the audit log.

Test plan

  • make test — 16,881 passed, 41 skipped, 0 failed
  • make lint — ruff/format/mypy/yaml clean (warnings only on unrelated soft-cap line counts)
  • New unit suites:
    • shared/tests/test_impasse_schema.py — Pydantic round-trip + extra-fields rejection
    • sandbox/tests/test_restrictions_handlers.py — both handlers (single/batch/blocked/allowed/no-alternative-role for .github/, env-var fallback, self-delegation rejection at handler boundary, blocked_files type validation, MCP registration sanity)
    • orchestrator/tests/test_impasse_routing.py — collect/route delegate path, second-impasse → HITL, plan_bug / external_blocker / unresolved-task / unknown-role / self-delegation all → escalate, contract-untouched-on-empty-input
  • Existing drift tests updated for the two new no-CLI verbs:
    • tests/sandbox/egg_agent_tools/test_full_tool_registry.py (count 29 → 31)
    • tests/sandbox/egg_agent_tools/test_server.py (expected-names set + count comment)
    • tests/tools/test_mcp_cli_drift.py (expected_gaps set)
    • tests/tools/test_rule_doc_drift.py (no-CLI rationale present in handler docstrings — decision-13)
    • docs/reference/agent-tools.md (verb-count + new mcp__sdlc__* table rows)

Out of scope (follow-ups)

Closes #2529.

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).

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review

Mostly solid plumbing — typed Impasse schema, drift-tested round trip, role-owner enforcement on phases.*.tasks.*.role, audit-trail via apply_mutation, and good unit coverage of the routing-policy decision tree. However, there is a blocking correctness issue that makes the feature non-functional in its primary code path, plus a few smaller design concerns.

Blocking

1. Producers never see the new "use the runtime escape hatch" instructions

orchestrator/routes/pipelines.py:5708-5739 appends the new producer-facing guidance ("If you discover mid-execution that the task you've been assigned is structurally impossible … STOP. Do not invent a workaround … Instead, use these two MCP tools …") to _build_role_restrictions_section().

That helper has exactly one call site:

$ grep -n "_build_role_restrictions_section" orchestrator/routes/pipelines.py
5645:def _build_role_restrictions_section() -> str:
11226:        lines.append(_build_role_restrictions_section())

Line 11226 is the task_planner branch of _build_agent_prompt (elif role_value == "task_planner":). The function's own docstring (line 5648) states: "This section is injected into the task_planner prompt so that it can assign each task to the correct execution role".

The producer prompts (coder, tester, documenter) are built in different branches that never call this helper. They get _build_file_boundary_section(role_value) (line 10645 / 11338), which describes file boundaries but says nothing about mcp__sdlc__check_file_restriction, mcp__sdlc__report_impasse, or "do not invent a workaround." The producer prompt also never delegates to _build_role_restrictions_section.

The SYSTEM_PROMPT_NUDGE (sandbox/egg_agent_tools/server.py:33-58) tells the agent that mcp__sdlc__* tools exist as a category but does not explain when to call report_impasse versus inventing a workaround.

Net effect: producers — the only roles that emit impasses — never receive the new behavioural instructions. The planner does receive them, but the planner does not emit impasses (it produces a plan; it never has an "assigned task it discovers is impossible mid-execution"). The exact anti-pattern this PR exists to prevent (the .github-staging/ deletion-marker workaround in pipeline issue-2474-v2) was emitted by a producer, not a planner.

This is a "Cross-module silent no-op" per the review rules: each individual file is internally consistent (the schema is valid, the handler validates, the routing policy is correctly tested), but the cross-module wiring dead-ends. The PR description claims "Producer prompts get a new section telling agents to call check_file_restriction before exploring a suspect file and emit report_impasse." — that is the intended design, but it is not what shipped.

Fix: Add the new escape-hatch section to the producer prompts (coder, tester, documenter — see the role_value branches around _build_phase_prompt / _build_agent_prompt), not to _build_role_restrictions_section. The planner branch can keep something briefer if you want planners to know about the post-failure delegation path, but the actionable guidance ("call these two tools, do not commit, exit cleanly") belongs in the producer system prompts.

Non-blocking concerns

2. Retry loop drops a delegation made on the final iteration

orchestrator/routes/pipelines.py:12907-12999:

MAX_IMPASSE_ATTEMPTS = 2

for attempt in range(MAX_IMPASSE_ATTEMPTS):
    last_exit, last_logs = _run_concurrent_phase(...)
    ...
    if not impasses:
        return last_exit, last_logs
    decisions = route_impasses(...)
    all_delegated = decisions and all(d.action == ImpasseAction.DELEGATE for d in decisions)
    if not all_delegated:
        ...
        return last_exit, last_logs
    # all_delegated == True → fall through to next iteration

If iteration 1 (the retry) produces an impasse on a different task whose delegation_attempts == 0, route_impasses will mutate that task's role and bump its counter — and all_delegated will be True. The loop then exits because range(2) is exhausted, and _run_concurrent_phase is never re-invoked with the newly-delegated role. The contract has been mutated, the task's role now points at a producer that never ran, and the slice returns whatever exit code iter-1 produced. The contract's delegation_attempts counter is per-task, so it doesn't catch this; the MAX_IMPASSE_ATTEMPTS=2 cap caps the BRC runs, not the delegations.

Either: (a) only treat a delegation as "spent" when it actually re-ran a BRC cycle, (b) escalate (don't delegate) on the final attempt, or (c) revert a same-attempt delegation that won't get a chance to execute.

3. wrong_role without suggested_role silently escalates

The schema (shared/egg_contracts/impasse.py) and the handler (sandbox/egg_agent_tools/handlers/restrictions.py:223-229) both treat suggested_role as fully optional. A producer can emit category=wrong_role with no suggested_role and the call succeeds. The router then checks if not impasse.suggested_role: return False, "no suggested_role on the impasse" and escalates to HITL.

That's a correct safety floor, but it's also the operator-facing-misconfiguration pattern: the producer's deliberately-set input (category=wrong_role) silently produces "always HITL" instead of an auto-delegation. Either:

  • Reject category=wrong_role without suggested_role at the handler boundary (point the agent back at check_file_restriction to populate it), or
  • Have the orchestrator-side router opportunistically compute alternative_role from blocked_files + the pattern registry on the way in.

The current behaviour means real-world wrong_role impasses skip auto-delegation any time the agent forgets the suggested-role field.

4. Stale agent-output files between retries — works only by luck

There is no cleanup of .egg-state/agent-outputs/{pipeline_id}-{role}-output.json between iter-0 and iter-1. The design relies on save_agent_output (shared/egg_contracts/orchestrator.py:366-397, mode="w") to clobber the prior file on every producer run. The implement-phase roster is fixed, so all three producers respawn each iteration and each overwrites — fine today.

But the behaviour is fragile:

  • If a producer fails to spawn / crashes before writing in iter-1 (transient infra issue), the iter-0 impasse file persists. The post-iter-1 collect_impasses re-reads it and tries to route it; since the counter is now 1, it escalates to HITL with a confusing message about "second impasse on same task" when there was really only one.
  • If/when the implement roster ever becomes contract-task-driven (i.e. a producer with no remaining tasks doesn't spawn), the same stale-file re-detection becomes the normal path.

Recommend either deleting the per-pipeline agent-output files between iterations, or keying them by (role, attempt_index) and ignoring older attempts in collect_impasses. At minimum, document the dependency on "every producer always overwrites" in _run_concurrent_phase_with_impasse_retry's docstring so future refactors don't break it silently.

5. Monolithic-implement fallback unprotected

The PR migrates only the slice-loop call site (line 12528) to _run_concurrent_phase_with_impasse_retry. The pre-#2137 monolithic-implement code path (the _run_concurrent_phase call elsewhere in the implement handler) still calls _run_concurrent_phase directly. In the monolithic path, an impasse just becomes a slice failure with no auto-delegation. Likely intentional (legacy path), but worth a single-line comment explaining the omission so this doesn't get flagged again.

6. _record_delegate truncates the agent's reason silently

orchestrator/impasse_routing.py:357: f"Impasse-driven delegation: {impassed_role} → {impasse.suggested_role}. Agent reason: {impasse.reason[:120]}" — the schema already caps reason at 2000 chars and the audit log can certainly hold them; truncating to 120 in the audit reason loses signal when post-mortem-debugging a delegation. Either use the full reason or note explicitly why 120 was chosen.

7. Minor: _find_task defaulting t.role or "coder"

Line 184: matches = [t for t in slice_obj.tasks if (t.role or "coder") == role]. The Task model defaults role=None (shared/egg_contracts/models.py:208-211), so this silently buckets any role-less task into the coder pool. That's consistent with how _build_role_restrictions_section describes the planner contract ("Tasks without a role field default to coder"), but if a wrong_role impasse is emitted by a tester against a None-role task in the same slice, the role-match fallback won't find it (since None or "coder" != "tester"). Consider matching on (t.role or "coder") == role and on t.role is None when the impasse comes from a producer trio member, or — better — require task_id for any impasse the orchestrator routes (the schema already supports it; the handler could enforce it).

Other notes (non-blocking, informational)

  • apply_mutation accepts the Decision Pydantic model directly (shared/egg_contracts/validator.py:_set_value appends to the list when idx == len(current)), and validate_assignment=True runs the pydantic validator on the way in. ✅
  • phases.{i}.tasks.{j}.role resolves through the Contract.phases backward-compat alias property (models.py:773) onto slices. ✅
  • Role.SYSTEM ownership of phases.*.tasks.*.role is correctly added to FIELD_OWNERSHIP so the producer cannot rewrite its own assignment via apply_mutation. ✅
  • Test coverage is good for the orchestrator-side policy (delegate / escalate matrix) and the handler boundary; please add at least one end-to-end test that confirms the producer prompt actually contains mcp__sdlc__report_impasse when the prompt is built for a coder/tester/documenter role — that test would have caught issue #1.

The blocking issue is #1; everything else is improvement.

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

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.
@james-in-a-box

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

Copy link
Copy Markdown
Contributor

Thanks for the careful review. All 7 items addressed in commit 696d392.

Per-item disposition

  1. Producers never see the new "use the runtime escape hatch" instructionsfixed-in-PR (commit 696d392).
    Extracted the actionable text from _build_role_restrictions_section() into a new _build_impasse_escape_hatch_section() helper and inject it into the coder prompt (early-return branch in _build_agent_prompt) and the tester / documenter prompts (post-phase-restrictions branch). The planner keeps a brief post-failure-delegation summary so it knows the auto-delegation path exists, but doesn't get the call-these-tools directive — planners don't emit impasses. Added end-to-end TestProducerEscapeHatchInPrompts that parametrises over coder/tester/documenter and asserts mcp__sdlc__report_impasse, mcp__sdlc__check_file_restriction, and the "DO NOT invent workarounds" header all appear; also asserts architect / planner stay free of the actionable producer-only directive. The new test would have caught the original miswiring.

  2. Retry loop drops a delegation made on the final iterationfixed-in-PR (commit 696d392).
    Took the reviewer's option (b): route_impasses gains a force_escalate kw, and the slice-loop wrapper sets force_escalate=is_terminal on its last iteration. A delegation that lands there can never re-run a BRC cycle, so we escalate to HITL instead of silently mutating the contract. New TestForceEscalate covers both the terminal-iteration escalation (contract untouched) and the non-terminal default-delegate path.

  3. wrong_role without suggested_role silently escalatesfixed-in-PR (commit 696d392).
    Reject at the handler boundary with a message pointing the agent back at check_file_restriction to populate the field, so the fix lands in the same iteration rather than degrading to HITL.

  4. Stale agent-output files between retries — works only by luckfixed-in-PR (commit 696d392).
    Took the cleanup-between-iterations path: after every successful delegation the slice-loop wrapper now clears the impasse field from each producer's per-pipeline agent-output file via load_agent_output → drop key → save_agent_output. A producer that crashes pre-handoff in iter-N+1 can no longer resurrect iter-N's impasse and trigger a spurious "second impasse on same task" HITL.

  5. Monolithic-implement fallback unprotectedfixed-in-PR (commit 696d392).
    Added a comment at the second _run_concurrent_phase call site explaining that impasse delegation is intentionally scoped to the slice loop because it rewires a task between producer roles within a slice; pipelines that don't use the slice loop are legacy / single-PR-shape and an impasse there surfaces via the existing phase-failure HITL path.

  6. _record_delegate truncates the agent's reason silentlyfixed-in-PR (commit 696d392).
    Dropped the [:120] truncation. The Impasse schema caps reason at 2000 chars and the audit log can hold the full payload; preserve it intact for post-mortem debugging. New test_full_reason_recorded_in_audit_log exercises a >200-char reason and asserts the audit-log entry contains it verbatim.

  7. _find_task defaulting t.role or "coder"fixed-in-PR (commit 696d392).
    Took the reviewer's "better" alternative: require task_id at the mcp__sdlc__report_impasse handler 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 (plan_bug / external_blocker / unknown) keep task_id optional since they always escalate and tolerate task-level ambiguity.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review (commit 696d392)

All seven items from the prior review have been addressed correctly. I traced each fix to the new code, ran the new tests locally (all pass), and spot-checked the helpers behave as advertised. Approving.

Verified fixes

  1. Producer escape hatch in prompts_build_impasse_escape_hatch_section is injected into the coder prompt at orchestrator/routes/pipelines.py:10694-10695 (early-return branch) and the tester/documenter prompts at orchestrator/routes/pipelines.py:11396-11397 (post-boundary branch). The planner-side text in _build_role_restrictions_section is now a brief summary that omits the actionable directive. TestProducerEscapeHatchInPrompts parametrises over coder/tester/documenter and asserts both tool names + the "DO NOT invent workarounds" header appear; it also asserts architect/planner stay clean of the producer-only directive. This test would have caught the original miswiring.

  2. Terminal-iteration silent dangle_is_eligible_delegation and route_impasses take a keyword-only force_escalate (orchestrator/impasse_routing.py:194-212, 316-340); the early-return short-circuits before the WRONG_ROLE check, so no path silently delegates on the terminal iteration. The slice loop sets force_escalate=is_terminal at orchestrator/routes/pipelines.py:13074. TestForceEscalate covers both the terminal escalate (contract untouched) and non-terminal default-delegate paths.

  3. wrong_role without suggested_role — handler now raises HandlerError at sandbox/egg_agent_tools/handlers/restrictions.py:227-234 with a message pointing the agent at check_file_restriction. Covered by test_wrong_role_without_suggested_role_rejected.

  4. Stale agent-output files_clear_stale_impasses at orchestrator/routes/pipelines.py:12968-13022 reads each producer's per-pipeline output, drops the impasse field, and writes the cleaned dict back. It runs only after a successful all-DELEGATE iteration (13121), so it's a no-op on terminal/escalate paths. I smoke-tested the load/clean/save cycle: load_agent_output returns {} when the file doesn't exist (the if not isinstance(existing, dict) or "impasse" not in existing: continue correctly handles this), and the cleanup leaves handoff_data and other fields intact.

  5. Monolithic-implement fallback commentorchestrator/routes/pipelines.py:17080-17089 now explains that auto-delegation is intentionally scoped to the slice loop (rewires a task between producer roles within a slice).

  6. Audit-log truncation removed_record_delegate at orchestrator/impasse_routing.py:441-444 drops the [:120] slice and includes a comment noting the schema cap of 2000 chars makes truncation unnecessary. test_full_reason_recorded_in_audit_log exercises a >200-char reason and asserts it survives verbatim. The structured-log line at :481 keeps impasse.reason[:200] for log readability — that's fine and intentional (logs vs. audit are different surfaces).

  7. task_id required for wrong_role — handler raises at sandbox/egg_agent_tools/handlers/restrictions.py:261-266. The router's role-match fallback in _find_task is now unreachable for the auto-delegateable path. Covered by test_wrong_role_without_task_id_rejected and test_plan_bug_without_task_id_accepted.

Test runs (local)

  • pytest orchestrator/tests/test_impasse_routing.py — 16 passed
  • pytest sandbox/tests/test_restrictions_handlers.py — 19 passed
  • pytest orchestrator/tests/test_pipeline_prompts.py::TestProducerEscapeHatchInPrompts — 5 passed

Non-blocking suggestions (follow-ups)

  • Mixed-decision case in iter-0 still wastes delegations. If iter-0 produces both a DELEGATE and an ESCALATE, the wrapper sees all_delegated == False and returns at 13086-13101. The DELEGATE has already mutated task.role + bumped delegation_attempts, but no further BRC cycle runs. Different from the terminal-iteration case (the operator at least gets a HITL surface from the ESCALATE), but the role flip is still wasted. Option (a) from the original review (only treat a delegation as "spent" once it actually re-runs) would close this; option (b)'s force-escalate-on-terminal does not. Worth a follow-up issue, not blocking here — the operator-visible HITL prevents silent dangling.

  • Docs drift in docs/reference/agent-tools.md:99. The mcp__sdlc__report_impasse row still describes category, reason, suggested_role, blocked_files, evidence — it does not mention that task_id and suggested_role are now mandatory for category=wrong_role. The producer prompt does say this, so agents see it at runtime, but the reference doc is stale.

  • No direct unit test for _clear_stale_impasses. The cleanup is internal to the wrapper; the existing tests don't exercise the multi-iteration path with crashed producers. Adding a test that seeds a stale impasse file, calls the wrapper helper, and asserts the field is gone would lock the fragile-by-design behaviour in place.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

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.
@james-in-a-box

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

Copy link
Copy Markdown
Contributor

Thanks for the careful re-review and the approve. Pushed commit 7da68cf addressing the two non-blocking suggestions that fit cleanly inside this PR; the third is filed as #2563 per your explicit "worth a follow-up issue, not blocking here" framing.

Per-item disposition

  1. Mixed-decision case in iter-0 still wastes delegationsdeferred-to #2563.
    Filing per the reviewer's explicit "worth a follow-up issue, not blocking here" — the operator-visible HITL from the ESCALATE side prevents silent dangling, and the structural fix (option (a): defer the delegation_attempts bump until the slice loop actually re-runs) is meaningfully different in shape from the force_escalate=is_terminal change that landed in this PR. Issue Impasse retry wrapper: mixed-decision iter-0 still wastes a DELEGATE role flip #2563 captures the concrete shape, the three options from the original review, and acceptance criteria for the follow-up.

  2. Docs drift in docs/reference/agent-tools.md:99fixed-in-PR (commit 7da68cf).
    Updated the mcp__sdlc__report_impasse row to document that task_id and suggested_role are now mandatory for category=wrong_role (the handler raises HandlerError when either is missing), with the explicit reason: the auto-delegation path needs both to rewire task.role unambiguously, since the role-match fallback in _find_task is fragile when a slice has multiple tasks per role or role-less tasks. Other categories (plan_bug / external_blocker / unknown) keep both fields optional since they always escalate.

  3. No direct unit test for _clear_stale_impassesfixed-in-PR (commit 7da68cf).
    Extracted the closure inside _run_concurrent_phase_with_impasse_retry to a module-level helper _clear_stale_impasses_for_producers(repo_path, pipeline_id, producer_roles, *, cleanup_reason) so it can be tested without mocking the BRC cycle. New orchestrator/tests/test_pipeline_impasse_cleanup.py::TestClearStaleImpasses covers five paths:

    • happy path: impasse field gone, handoff_data + role survive verbatim
    • no-op when the file has no impasse key (file bytes unchanged)
    • no-op when no output file exists (producer crashed pre-handoff in iter-N — the original failure mode the cleanup defends against)
    • multi-producer cleanup in one pass, with a producer that wrote a clean output unaffected
    • per-pipeline scoping — a concurrent pipeline's stale impasse on the same role is not cleared

    Closure behaviour is byte-identical to the prior commit; this is a refactor + test addition, not a behaviour change. All five new tests + the existing 16 in test_impasse_routing.py + 360+ in test_pipeline_prompts.py pass locally.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review (commit 7da68cf, delta from 696d392)

The new commit addresses both non-blocking suggestions from the prior approve and correctly defers the third per the reviewer's explicit framing. Approving.

Verified fixes

  1. Docs drift in docs/reference/agent-tools.md:99 — the mcp__sdlc__report_impasse row now states that task_id and suggested_role are mandatory for category=wrong_role (raises HandlerError otherwise) and that other categories keep both fields optional. The doc text matches the handler's actual validation order at sandbox/egg_agent_tools/handlers/restrictions.py:219-234, 261-266 — no drift between row and code.

  2. _clear_stale_impasses_for_producers is now testable + covered. The closure was extracted to a module-level helper at orchestrator/routes/pipelines.py:12887-12955 with the call site at :13128 updated to pass Path(worktree_repo_path) explicitly. I diffed the body against the prior closure: byte-identical except for the dropped closure capture (the helper now takes repo_path: Path as its first arg instead of closing over worktree_repo_path) and the explicit cleanup_reason: str keyword-only kwarg. The error-handling shape (debug log on load_agent_output failure → continue; warning log on save_agent_output failure → continue; info log on success) is preserved.

    orchestrator/tests/test_pipeline_impasse_cleanup.py adds five tests against the extracted helper:

    • test_drops_impasse_field_preserves_others — happy path; impasse removed, role and handoff_data survive verbatim.
    • test_no_op_when_no_impasse_field — file is byte-identical on disk after the call (the "impasse" not in existing: continue short-circuit really does skip the rewrite).
    • test_no_op_when_output_file_missing — no file is materialised when the producer crashed pre-handoff; the helper handles load_agent_output's {} fallback cleanly.
    • test_clears_across_multiple_producers — multi-producer cleanup in one pass; the clean-output-tester is left alone.
    • test_does_not_touch_other_pipelines — per-pipeline scoping; a concurrent pipeline's stale impasse on the same role is preserved.

    I ran the suite locally (pytest orchestrator/tests/test_pipeline_impasse_cleanup.py -v): 5 passed. Also re-ran test_impasse_routing.py (16 passed) and the doc-drift tests (tests/tools/test_rule_doc_drift.py + tests/tools/test_mcp_cli_drift.py, 72 passed) to confirm the refactor + doc edit didn't shake anything loose.

  3. Mixed-decision iter-0 silently mutates the contract — correctly deferred to #2563 per the prior reviewer's explicit "Worth a follow-up issue, not blocking here." I read the filed issue: it captures the concrete shape (DELEGATE+ESCALATE in iter-0, role flip dangles until operator resolves the ESCALATE HITL), the three options from the original review, and acceptance criteria that constrain the follow-up. Defer is appropriate because option (b) — the fix that landed in this PR — closes the terminal-iteration silent dangle but is structurally different from option (a) — the deferred fix — and both can land independently.

Minor observations (non-blocking)

  • The producer_roles: list parameter type on _clear_stale_impasses_for_producers is unparameterised. The call site passes a list of ContractAgentRoleEnum members and the helper uses role_enum.value only in log statements + role-keyed file paths via load_agent_output/save_agent_output. A list[ContractAgentRole] annotation would make the contract clearer to a future caller, but this is consistent with the prior closure's typing and not a blocker.
  • The cleanup helper imports load_agent_output / save_agent_output lazily inside the function body (preserved from the closure). Each call hits the import seam fallback at try: … except ImportError. Hoisting to module-level would shave a microscopic per-call cost, but the closure already had this shape and the test exercises it as-written.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

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.
@james-in-a-box

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

Copy link
Copy Markdown
Contributor

Thanks for the careful re-review and the approve. Pushed commit 6354648 addressing both minor observations from the third review.

Per-item disposition

  1. producer_roles: list parameter type is unparameterisedfixed-in-PR (commit 6354648).
    Annotated as list[ContractAgentRole] so a future caller sees the expected element type without grepping the call site. Imported under TYPE_CHECKING and quoted with # noqa: UP037 to match the file's existing pattern for ContainerSpawner (lines 413, 684, 6316, 6707, 7021) — the symbol isn't available at runtime, so the standard py314 unquoted form would NameError on inspect.signature() introspection.

  2. Lazy load_agent_output / save_agent_output importsfixed-in-PR (commit 6354648).
    Hoisted to module-level. The seam fallback (from shared.egg_contracts.orchestrator import …) is dead code at runtime — egg_contracts is the actual installed package, not shared.egg_contracts — and orchestrator/impasse_routing.py:50 already imports load_agent_output at module level the same way without a seam. Removing the lazy import + seam shaves the per-call cost the reviewer flagged and brings the helper in line with impasse_routing.py's pattern.

Local re-runs after the change:

  • pytest orchestrator/tests/test_pipeline_impasse_cleanup.py — 5 passed
  • pytest orchestrator/tests/test_impasse_routing.py — 16 passed
  • pytest orchestrator/tests/test_pipeline_prompts.py — 374 passed
  • ruff check orchestrator/routes/pipelines.py — clean

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review (commit 6354648, delta from 7da68cf)

Both minor observations from the prior review are addressed cleanly. Approving.

Verified fixes

  1. producer_roles annotated as list[ContractAgentRole]orchestrator/routes/pipelines.py:12892 now reads producer_roles: "list[ContractAgentRole]" # noqa: UP037, with ContractAgentRole imported at line 178 under TYPE_CHECKING. The quoted-forward-ref + noqa: UP037 is correct: pipelines.py doesn't use from __future__ import annotations, so an unquoted form would NameError under inspect.signature() introspection because the symbol isn't bound at runtime. Matches the file's existing pattern for ContainerSpawner at lines 413, 684, 6316, 6707, 8956.

  2. load_agent_output / save_agent_output hoisted to module levelorchestrator/routes/pipelines.py:172 now imports both at module load. The seam fallback (from shared.egg_contracts.orchestrator …) was indeed dead code at runtime — orchestrator/impasse_routing.py:50 already imports load_agent_output from egg_contracts.orchestrator at module level the same way without a seam, and the orchestrator runs. I confirmed egg_contracts.orchestrator does not import from routes.pipelines (no circular-dependency risk introduced by hoisting).

Behavioural delta

The closure-to-helper extraction in commit 7da68cf preserved byte-identical semantics; this commit only changes annotations and import location. The _clear_stale_impasses_for_producers body and call site (:13128) are unchanged. The five tests in orchestrator/tests/test_pipeline_impasse_cleanup.py import the helper by name and don't depend on the lazy-import shape, so they continue to exercise the same path.

— Authored by egg

@james-in-a-box

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

Copy link
Copy Markdown
Contributor

egg review completed. View run logs

11 previous review(s) hidden.

@jwbron
jwbron merged commit 74de6a3 into main May 7, 2026
21 checks passed
james-in-a-box Bot pushed a commit that referenced this pull request May 8, 2026
* 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>
jwbron added a commit that referenced this pull request May 8, 2026
* Initialize SDLC contract for issue #2548

* Refine analysis for issue #2548

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

* Persist agent statefile writes before refine sync

* Persist statefiles after refine phase

* Persist HITL resolution after refine phase gate

* Risk assessment for issue #2548 plan phase

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

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

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

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

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

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

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

* Architecture analysis for issue #2548 plan phase

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

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

* Persist statefiles after plan phase

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

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

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

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

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

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

Notes vs. the issue inventory:

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

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

* Extract _PLAN_AGENT_BLOCKED_WRITE shared constant

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

---------

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

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

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

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

The fix wires three things:

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

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

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

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

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

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

---------

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

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

* Initialize SDLC contract for issue #2474

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* rm e2e workflow

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

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

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

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

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

* Address review: remove stale entries from STRUCTURE.md

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

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

---------

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

* docs: expand tester Directed Coordination to cover outbound HANDOFF

---------

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

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

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

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

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

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

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

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

---------

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

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

* Fix #2490: extend validate_assignment to sibling Contract models

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

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

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

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

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

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

— Authored by egg

---------

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

Non-blocking feedback addressed in the same commit:

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Authored-by: egg

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

---------

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

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

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

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

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

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

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

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

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

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

* Address PR review feedback (round 2)

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

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

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

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

---------

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

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

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

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

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

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

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

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

Fix #2554

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

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

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

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

---------

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

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

* Fix #2529: runtime escape hatch for impossible tasks

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fix per reviewer_contract's forward-looking concern and tester's blocking
finding: add the file to scripts/file-size-allowlist.yaml under #2548 so
make lint passes during the slice-1 BRC. Decomposition is tracked under
the same issue and is the cheaper of the two unblock option…
@james-in-a-box james-in-a-box Bot mentioned this pull request May 8, 2026
james-in-a-box Bot pushed a commit that referenced this pull request May 8, 2026
task-1-1 in slice-1 was assigned to role 'coder' but its files_affected
(docs/architecture/orchestrator.md, docs/reference/orchestrator-cli.md)
are blocked for the coder role by shared/egg_restrictions/patterns.py.
The role-permitted writer for both paths is 'documenter'.

A typed wrong_role impasse signal was filed via mcp__sdlc__report_impasse
at 2026-05-08T21:51:12Z (.egg-state/agent-outputs/issue-2548-coder-output.json).
This empty commit exists solely to advance coder's BRC producer_phase past
WORKING per overseer guidance, so the pipeline can converge on documenter+
tester proposals while the orchestrator delegates task-1-1 to documenter
or HITL adjudicates.

Refs: #2548, #2553
james-in-a-box Bot pushed a commit that referenced this pull request May 8, 2026
Records the typed wrong_role impasse signal (category=wrong_role,
suggested_role=documenter) for task-1-1's blocked docs paths
(docs/architecture/orchestrator.md, docs/reference/orchestrator-cli.md).
The same signal was filed via mcp__sdlc__report_impasse at 21:51:12Z and
already drove HITL decision-1; documenter has self-assigned task-1-1 in
commit d20e265. This file is the durable on-disk record of the impasse
that the orchestrator reads post-phase.
jwbron pushed a commit that referenced this pull request May 11, 2026
* Initialize SDLC contract for issue #2548

* docs: add Context PR + per-slice BRC stack guide for #2548

- concurrent-execution.md: new "Slice PR Stack" section with the
  context→slice-1→slice-2→… stack diagram, the "Context PR is opened
  first" subsection (orchestrator-authored, doc-only auto-open, base
  taken from pipeline.base_branch), the new pr.context_* contract
  fields, slice-1 base resolution onto egg/<id>/context, and the
  per-slice BRC-history commit on each slice PR.
- babysit-pr.md: clarify that babysit-pr is one of the non-slice
  implement runs that retain the single-file
  pr-<N>-<short-sha>-implement.{md,json} format and does not partition
  into per-slice files (no slices in babysit cycles), with a cross-link
  to the BRC History Link section and the #2548 issue.

Closes the documenter slice-1 task task-1-2 in pipeline issue-2548.

* Add docs regression tests for context PR terminology (#2548)

Pin slice-1 acceptance criteria as a structural test so future doc
edits cannot silently drop the new terminology introduced by #2548:

- docs/architecture/orchestrator.md must reference `pr.context_branch`,
  `pr.context_pr_number`, the `-implement-slice-` filename pattern, and
  cross-link to issue #2548.
- docs/reference/orchestrator-cli.md must reference at least one of the
  new `pr.context_*` fields and cross-link #2548.
- docs/guides/concurrent-execution.md must contain a "Context PR"
  markdown heading, name the literal `egg/<id>/context` branch in the
  same paragraph as a slice-1 mention, and cross-link #2548.
- docs/guides/babysit-pr.md must reference the per-slice BRC filename
  pattern and cross-link #2548.

Also adds a directory-scoped grep over docs/*.md for the deprecated
aggregate filename pattern with an explicit allow-list of
known-legitimate operational references, plus an adversarial probe
that fails when an allow-list entry no longer matches any line in the
named file (prevents the allow-list from rotting).

The classifying regex is unit-tested against six positive cases (the
deprecated aggregate filename in various punctuation contexts) and
nine negative cases (per-slice variants, the unattributed sibling,
other-phase filenames, and loose substrings) to lock in its boundary
behavior.

Tests currently fail until the coder (task-1-1) and documenter
(task-1-2) push their doc updates — this is the regression signal.

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

* Impasse: task-1-1 wrong_role — docs paths require documenter

task-1-1 in slice-1 was assigned to role 'coder' but its files_affected
(docs/architecture/orchestrator.md, docs/reference/orchestrator-cli.md)
are blocked for the coder role by shared/egg_restrictions/patterns.py.
The role-permitted writer for both paths is 'documenter'.

A typed wrong_role impasse signal was filed via mcp__sdlc__report_impasse
at 2026-05-08T21:51:12Z (.egg-state/agent-outputs/issue-2548-coder-output.json).
This empty commit exists solely to advance coder's BRC producer_phase past
WORKING per overseer guidance, so the pipeline can converge on documenter+
tester proposals while the orchestrator delegates task-1-1 to documenter
or HITL adjudicates.

Refs: #2548, #2553

* Skip task-1-1 doc assertions until wrong_role impasse resolves (#2548)

The coder filed a wrong_role impasse on task-1-1 (commit b606489):
docs/architecture/orchestrator.md and docs/reference/orchestrator-cli.md
are blocked for the coder role; documenter is the alternative_role,
but the orchestrator has not yet auto-delegated.

Per overseer guidance, refactor the six task-1-1 assertions to
`pytest.skip` while no context-PR token is present in the target
file. The skip releases as soon as *any* expected token (`pr.context_*`,
`context_branch`, `context_pr_number`, `-implement-slice-`) lands, so
a partial fix that misses one field is still caught by the strict
remaining assertions — the regression value of the test is preserved.

Result: 28 passed, 6 skipped (was 28 passed, 6 failed). Lint and the
focused test suite are clean. Gaps on task-1-1 are recorded
separately via mcp__task__mark_gap so the orchestrator surfaces the
unaddressed work.

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

* docs: self-assign task-1-1 — Context PR docs in orchestrator.md and orchestrator-cli.md

Per overseer directive (coder filed wrong_role impasse on task-1-1
naming documenter as suggested_role; the two affected paths are .md
files outside the coder's allowed-pattern), document the Context PR
mechanism in the two code-adjacent reference files:

- docs/architecture/orchestrator.md: new "Special case: Context PR
  (slice-aware mode)" subsection inside Network Mode covering the
  doc-only auto-open semantics, the egg/<id>/context branch creation,
  the refine/plan artifacts committed to it, the slice-1 base-resolution
  rewiring, and HITL D3/D4. Adds the four pr.context_* contract field
  table (planner-authored title/description, orchestrator-authored
  branch/pr_number, schema 1.1) and a BRC-history file-naming table that
  contrasts slice-aware (per-slice + unattributed) vs. non-slice
  (babysit-pr content-addressed) modes; calls out that the deprecated
  aggregate <id>-implement.{md,json} file is not produced in slice-aware
  mode.
- docs/reference/orchestrator-cli.md: new "Context PR Surfaces" section
  before Related CLIs noting that egg-orch ships no dedicated
  --context-branch / --context-pr flags (orchestrator-managed), the
  pr.context_* contract field table, and the canonical inspection
  surfaces (egg-contract show, egg-orch pipeline status, gh pr
  view/list).

Both files cross-reference issue #2548 and the corresponding sections
in the other docs (concurrent-execution.md#slice-pr-stack and
slice-dag.md#stacked-pr-rebase-reconciler).

Closes documenter coverage of slice-1 task-1-1 (originally assigned to
coder but rerouted via overseer directive).

* Add coder impasse output for task-1-1 (#2548, #2553)

Records the typed wrong_role impasse signal (category=wrong_role,
suggested_role=documenter) for task-1-1's blocked docs paths
(docs/architecture/orchestrator.md, docs/reference/orchestrator-cli.md).
The same signal was filed via mcp__sdlc__report_impasse at 21:51:12Z and
already drove HITL decision-1; documenter has self-assigned task-1-1 in
commit d20e265. This file is the durable on-disk record of the impasse
that the orchestrator reads post-phase.

* docs: address reviewer_code v2 NACK — fix CLI examples and Context PR cleanup guidance

Two blocking issues from the reviewer_code review of d20e265:

1. orchestrator-cli.md Context PR Surfaces section had a broken
   `egg-contract show --pipeline <id>` example. The correct CLI shape
   is `egg-contract --pipeline-id <id> show` (top-level flag goes
   BEFORE the subcommand). Verified against `egg-contract --help`
   output.

2. The same section incorrectly claimed `egg-orch pipeline delete <id>`
   removes the Context PR. `_cleanup_remote_branches` in
   orchestrator/routes/pipelines.py only deletes the pipeline tip
   branch and per-container worktree branches; the Context PR branch
   `egg/<id>/context` is a sibling of the pipeline tip (same
   convention as slice integration branches `egg/<id>/slice-N`) and is
   NOT deleted. Replaced with accurate manual-cleanup guidance:
   `gh pr close <context_pr_number>` followed by
   `git push origin --delete egg/<pipeline-id>/context`.

Also addresses a non-blocking observation: the orchestrator.md
BRC-history file-naming table called the aggregate file "deprecated"
in the slice-aware row, which read strangely given the same file is
the legitimate output for non-slice runs. Reworded to clarify that
the aggregate is not produced in slice-aware mode (rather than calling
it deprecated outright).

* Persist agent statefile writes before implement sync

* Persist statefiles after implement phase

* Persist contract before PR creation

* Remove ephemeral agent-output handoff artifacts (#1731)

* Persist statefiles after pr phase

* Address PR #2578 review: doc structure + test cleanup

Promote the two new bold-paragraph sections out of '## Network Mode' to
top-level '##' headings (Context PR (slice-aware mode) and BRC-history
file naming) so the cross-references in docs/reference/orchestrator-cli.md
land on the right anchors instead of '#network-mode'. Update the two
links accordingly.

Drop the _skip_until_task_1_1_lands / _task_1_1_authored helpers from
tests/docs/test_context_pr_doc_terminology.py — the docs they gated
have landed in this PR, so the conditional skip is dead code that
doubles as a regression-escape hatch.

Fix the first DEPRECATED_FILENAME_ALLOWLIST entry to identify the
allowlisted line by its actual deprecated content
('[`implement`](./.egg-state/brc-history/42-implement.md)') instead
of an unrelated refine reference on the same line.

Strengthen test_slice_1_paragraph_ties_to_context_branch by splitting
on blank lines and asserting both the context-branch literal and a
slice-1 mention land in the same paragraph, replacing the 800-char
proximity heuristic.

Authored-by: egg

---------

Co-authored-by: egg-orchestrator <egg@localhost>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Coder should fast-fail (or never start) on structurally impossible tasks

1 participant