[slice-2] Roll out Claude Code substrate to remaining roles + plan/... - #2726
Conversation
…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>
…/issue-2717-slice-1-documenter/work
…/issue-2717-slice-1-tester/work
…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>
…/issue-2717-slice-1-tester/work
…/issue-2717-slice-1-documenter/work
…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>
…/issue-2717-slice-1-tester/work
…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>
…/issue-2717-slice-1-tester/work
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>
…sue-2717-slice-2-documenter/work
…/issue-2717-slice-2-documenter/work
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
…/issue-2717-slice-2-tester/work
…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>
This comment has been minimized.
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'}).
This comment has been minimized.
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.
Conflict Resolution SummaryResolved merge conflicts with
Why the structural model over the enumerationThe two sides shipped genuinely different designs for the same fence:
The slice-1 model was chosen because:
VerificationAll affected tests pass locally:
Please review: The — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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_producerextension 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_proposeconcurrently without races. - Worktree allocation:
claude_code/worktree.pyuses an RLock; concurrentbundle.worktrees.createcalls are safe. - Heartbeat phase flip via
runner._current_phase: correct, prevents the stuck-phase-transition watchdog from false-positive-ing during plan stage. fail_closedheuristic: NACK every edge only when verdict file missing AND reviewer exit != 0; preserves the harness-faked synthetic-ACK path for tests._answer_continues_past_refinehandles the bare-string and{"selected": ...}dict forms; mirrors_answer_is_abort._LANDED_SLICESis correctly extended (not replaced) for slice-2; rubric loader's fence semantics for implement-team roles are preserved.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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.
Review-feedback dispositionThanks for the thorough review. All items addressed in-PR; per-item disposition below. Blocking
Non-blocking
Verification
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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:
- N9 fires →
matrix._entries[(reviewer_plan, architect)].state = NACKED, architect transitioned to WORKING (peer_consensus.py:542). - Downstream producers fan out (they receive
EGG_ARCHITECT_OUTPUT_PATHpointing at the missing file — that's a separate problem, see NB3). - Reviewer spawns.
_apply_reviewer_verdicts(_plan_phase.py:430-467) iteratesfor producer in plan_producers. The architect is inplan_producers, itsproducer_exit == 0, so neither of thecontinueguards fires.verdicts.get("architect") == None(no per-edge entry, since the harness or real-but-broken reviewer wrote no verdict file).fail_closedis False whenever the reviewer'sexit_code == 0.- Branch falls through to
_record_reviewer_ack(architect, …)→tracker.handle_ack(reviewer_plan, architect, …)→matrix.record_ack(…)overwritesentry.stateback to ACKED (approval_matrix.py:151). handle_confirmedthen succeeds because architect is fully ACKed; CONSENSUS_CONFIRMED fires; the HITL gate seesis_complete=Truewith 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_nackdirectly:synthetic_verdicts["architect"] = {"verdict": "NACK", "reason": "...", "artifact_references": [...]}, then merge it into the verdicts dict before_apply_reviewer_verdictsruns. The per-edge code path at_plan_phase.py:479-488will then NACK architect with the right reason and no optimistic-ACK clobbers it. - Or thread an
architect_nacked_by_n9: boolflag through_apply_reviewer_verdictsso the architect-iterationcontinues 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:
- Process A:
open(lock_path, "w")→ fd_A on inode X. Acquiresflockon inode X. - Process B:
open(lock_path, "w")→ fd_B on inode X (path still maps to X). - B blocks on
flock(fd_B). - A releases
flock(LOCK_UN infinally);withexits, closing fd_A. - B acquires
flockon inode X — B is now in the critical section. - 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. - Process C:
open(lock_path, "w")— path missing, so the OS creates a new inode Y. fd_C is on inode Y. - C's
flock(fd_C, LOCK_EX)succeeds immediately because no one holds a lock on inode Y. - 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_decisionphase plumbing): thephase: strkeyword-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 newtest_plan_gate_decision_persists_with_phase_plantest pins both invariants. - B2 (SKILL.md
approve_continue): the doc now saysapprove_continue(the string_maybe_fenceactually trips on). Therequest_changes/change_approachblurbs 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/abortdocumented. - N4
_SYNTHETIC_PLAN_COMMIT: deleted; the safety note is now folded intosynthetic_commit_for(role)'s docstring; no callers remain (grepconfirms). - N5 rubric
per_producerextension: 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_conditionremoval: dropped from both schemas inread_plan_reviewer_verdictsand from_record_reviewer_ack's signature. Plan-phase doesn't propagate it anywhere. - N10 sentinel teardown:
runner._teardown_sentinel()is called insiderun_plan_phaseafter the heartbeat-phase flip but before the architect spawn. The PreToolUse hook's fallback now resolves to "no role known" rather than stalerefinerduring the plan-producer fan-out.spawn_plan_reviewercorrectly rewrites the sentinel asreviewer_planfor the solo reviewer dispatch. - N11 test patch removal: the
patch.object(restrictions, "get_agent_role", ...)is dropped along with its unusedunittest.mock.patchimport; the test still pins the same real invariant (role="unknown_xyz"raisesHandlerError).
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Review-feedback dispositionThanks for the careful second pass. All four non-blockers addressed in-PR; per-item disposition below. Non-blocking
Test coverage
Verification
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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:
task_planner,risk_analyst,reviewer_planare never spawned (asserts via_spawned_roles(bundle) & forbidden_after_n9).- The plan-HITL decision offers
["retry", "abort"], not the success-pathapprove_continueoptions. tracker.evaluate()returnsis_complete=Falsewith"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.mdapprove_continuealignment): 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 clearreasonandartifact_references— the operator sees actionable diagnostics in the plan-HITL gate'sunresolved_nack_details. - Worktrees created for the architect on the N9 path are still cleaned up via the pipeline-level
_teardown_worktrees()in the generator'sfinallyblock (in_process.py:254-261). _record_producer_proposecorrectly short-circuits onexit_code != 0, so the architect's proposal is only registered when its spawn succeeded — thearchitect_handoff_brokencheck 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
This comment has been minimized.
This comment has been minimized.
#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.
* 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-…
There was a problem hiding this comment.
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 →8fb1b0ebad921f787a3534c222c6152c72d5f6c1a300b1f0(current head) tree →8fb1b0ebad921f787a3534c222c6152c72d5f6c1
The compare endpoint reports files_changed: 0 for 3a79c2a...a300b1f0. Two intervening commits, both zero-tree-delta against slice-2:
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.a300b1f0—Merge egg/issue-2717/work into slice-2 (squash-merge history reconcile). Per the commit message, this is a deliberate-s oursmerge 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
|
egg review completed. View run logs 11 previous review(s) hidden. |
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):
Closes the heredoc-HITL bridge gap via flattened
python3 <stage>.pyinvocations (cq-1 Option C, refine/planhalf). 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).stress test. Wires architect / task_planner / risk_analyst
producers and
reviewer_planthrough the in-processorchestrator.
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.
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.slowgating + 3-hour per-phase ceiling.
EGG_PIPELINE_MAX_AGENT_INVOCATIONScost cap default 200 (cq-6);
EggHarnessSpawner+egg-orch local-runCLI (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
AskUserQuestionwithout an MCPprovide_inputround-trip; the conformance matrix is green onboth substrate dimensions;
egg-orch local-runenables headlessruns; the four substrate protocols are stable.
This slice
Plan-phase substrate (3 producers + 1 reviewer)
Tasks:
_run_planstage to_InProcessOrchestrator.run()inorchestrator/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 existingconcurrent_executor.pyseam (line 569) so multi-prod..._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 raiseValueErrorfor implement-team roles.plugins/egg-sdlc/skills/egg-sdlc/agents/:architect.md,task_planner.md,risk_analyst.md,reviewer_plan.md. Mirrorrefiner.md's shape; body content mirrors what the k3s prompts inshared/prompts/produce for each role.test_inprocess_plan_brc.pyunderintegration_tests/regression/. Test bootsrun_pipeline_in_processagainst 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....egg-state/<pipeline_id>/r2-verdict.jsonfrom TASK-1-5). If R2 = fail, wire agent-side (sandbox-tool-handler) policy enforcement parallel to the PreToolUse hook: extendsandbox/egg_agent_tools/handlers/restrictions.py(which already exposes `check_f...SKILL.mdto 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
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).
python3 bin/run_pipeline.py issue-2717end-to-end andegg-orch local-run --issue 2718headlessly; both produce theexpected PR stack and metrics.
Manual Steps
Pre-merge (each slice): reviewer copies
orchestrator/substrate/claude_code/settings.template.jsonintotheir own
.claude/settings.jsonbefore exercising theconformance 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.