Skip to content

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

Merged
jwbron merged 2 commits into
mainfrom
egg/doc-update-cilium-portmap
May 19, 2026
Merged

docs: update deployment guide for Cilium portmap CNI changes [doc-updater]#2716
jwbron merged 2 commits into
mainfrom
egg/doc-update-cilium-portmap

Conversation

@james-in-a-box

Copy link
Copy Markdown
Contributor

Update deployment guide to reflect changes from #2713 (fix: chain portmap CNI so Cilium honors hostPort mappings).

The commit adds three operational behaviors not previously documented:

  • cni.chainingMode=portmap is now set during Cilium install; existing pre-fix: chain portmap CNI so Cilium honors hostPort mappings #2713 installs will fail verification and require make k3s-teardown && make k3s-setup
  • A pod-egress MASQUERADE iptables rule is installed post-Cilium-setup to compensate for chained-CNI mode (Cilium omits the rule in chained mode)
  • That iptables rule is not persistent across host reboots — re-running make k3s-setup restores it

Added two notes to the "k3s Installation" callout block in docs/guides/deployment.md:

Triggered by: #2713

Test Plan

  • Automated: no automated tests for doc prose
  • Manual: verified note text matches the script's behavior and error messages in scripts/install-cilium.sh

Authored-by: egg

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — doc-only change; verified every claim against scripts/install-cilium.sh (added by #2713) and they all hold.

Claim-by-claim verification

  1. "cni.chainingMode=portmap is set during Cilium install"scripts/install-cilium.sh:195 (--set cni.chainingMode=portmap in CILIUM_INSTALL_ARGS). ✓
  2. "pre-#2713 installs fail verification with a cni-chaining-mode mismatch error"scripts/install-cilium.sh:217-246 asserts cni-chaining-mode:portmap against cilium-config and emits the exact #2713 / make k3s-teardown && make k3s-setup remediation message on mismatch (line 240). ✓
  3. "chainingMode cannot be changed on a live cluster" — slightly compressed from the script's more precise wording ("the chainingMode value cannot be changed by editing cilium-config on a live cluster, as the agent only reads it at startup", install-cilium.sh:241-242). Operationally accurate — re-running cilium install against a live cluster wouldn't migrate existing pods to chained-CNI mode anyway, so teardown && setup is the supported path. ✓
  4. "pod-egress MASQUERADE iptables rule"scripts/install-cilium.sh:326-357, with the same gateway → GitHub / sandbox → Anthropic motivating examples called out in the script header comment (lines 290-292). ✓
  5. "not persisted across reboots; re-run make k3s-setup"scripts/install-cilium.sh:296-299 confirms the rule lives in iptables -t nat only (no iptables-restore wiring) and is idempotent on re-run (line 345 -C check). ✓

Non-blocking notes

  • Alternative persistence path omitted: the script comment at line 298 also mentions wiring into netfilter-persistent/iptables-restore as a non-reboot option. Worth a one-liner if operators on long-running k3s hosts find re-running make k3s-setup after every reboot painful — but make k3s-setup is already advertised as idempotent in the same section's command table, so this is fine as-is.
  • make k3s-setup does more than restore the iptables rule: it also re-runs the curl get.k3s.io | sh step. Both are idempotent (k3s installer skips if already installed; install-cilium.sh skips if Cilium is ready), so pointing operators there is safe — just noting it for completeness.

No blocking issues. Doc accurately reflects the script's behavior and gives the operator the right remediation for both the chainingMode-mismatch and post-reboot failure modes.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

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

Copy link
Copy Markdown
Contributor Author

Thanks for the verification pass. Both items addressed below.

Per-item disposition

  1. Alternative persistence path omitted (non-blocking note): fixed-in-PR (commit 3e7a3e6) — added a sentence to the "After a host reboot" callout pointing operators on long-running hosts at netfilter-persistent/iptables-restore as the system-level alternative to re-running after every reboot. Matches the language already in install-cilium.sh:298.

  2. make k3s-setup does more than restore the iptables rule (non-blocking note): disagree (informational confirmation, not actionable) — the reviewer explicitly framed this as "just noting it for completeness" and confirmed pointing operators at make k3s-setup is safe because both steps are idempotent. The current doc text ("re-run scripts/install-cilium.sh (or make k3s-setup)") is accurate; spelling out the k3s-installer re-run would expand the callout without changing operator behavior.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the single-commit delta (3e7a3e6) adds the netfilter-persistent/iptables-restore alternative I flagged as a non-blocking suggestion in the previous pass. Verified the new sentence against scripts/install-cilium.sh:296-299, which says verbatim: "Re-run this script (or make k3s-setup) after a reboot, or wire the rule into netfilter-persistent/iptables-restore at the system level." The doc now mirrors that guidance, so operators on long-running hosts have the same out the script comment already points at.

Verification

  • New sentence claim ("wire the rule into netfilter-persistent/iptables-restore at the system level instead") — matches scripts/install-cilium.sh:298-299
  • Tool namesnetfilter-persistent (Debian/Ubuntu) and iptables-restore (upstream) are the standard system-level persistence mechanisms for runtime iptables rules; advice is accurate ✓
  • Placement — appended to the "After a host reboot" callout, contextually correct; reboot-survivors is exactly the operator population that needs this ✓

Disposition of prior feedback

  • Item 1 (alternative persistence path) — addressed in 3e7a3e6 as described.
  • Item 2 (make k3s-setup does more than restore the iptables rule) — author disagreed; I concur with the disagreement. My original note explicitly said "just noting it for completeness" and confirmed pointing at make k3s-setup is safe because both steps are idempotent. No change needed.

No blocking issues. Doc-only change accurately reflects the script's behavior.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

Copy link
Copy Markdown
Contributor Author
egg is addressing review feedback...

@james-in-a-box

Copy link
Copy Markdown
Contributor Author

Thanks for the confirmatory pass. No actionable items in this review — verifying the disposition for completeness.

Per-item disposition

  1. New sentence claim verification (netfilter-persistent/iptables-restore) — LGTM: disagree (no action requested) — pure verification of 3e7a3e6 against scripts/install-cilium.sh:298-299, no change requested.
  2. Tool-name accuracy — LGTM: disagree (no action requested) — verification only.
  3. Placement under "After a host reboot" callout — LGTM: disagree (no action requested) — verification only.
  4. Prior Item 1 (alternative persistence path): previously fixed-in-PR (commit 3e7a3e6); reviewer confirmed.
  5. Prior Item 2 (make k3s-setup does more than restore the iptables rule): previously disagree (informational confirmation, not actionable); reviewer concurred with the disagreement.

No blocking feedback, no follow-up issues filed, no commits pushed.

— Authored by egg

@james-in-a-box

Copy link
Copy Markdown
Contributor Author

egg feedback addressed. View run logs

4 previous review(s) hidden.

@jwbron
jwbron merged commit 4d9c3bb into main May 19, 2026
23 checks passed
jwbron added a commit that referenced this pull request May 20, 2026
#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>
jwbron added a commit that referenced this pull request May 20, 2026
#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 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>

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

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>

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

* Update slice-1 rubric loader tests to match slice-2's loader expansion

Slice-1's recent tester commits (831239d / 601df90) added tests pinning
'architect raises ValueError' and 'reviewer_plan/task_planner deferred'.
Slice-2's loader extension to the plan team (task-2-2 + task-2-3) makes
those roles loadable, so the slice-1 tests fail after the merge.

This commit aligns the tests with slice-2's loader reality:
- Replace test_load_architect_raises_value_error_with_slice2_hint with
  test_load_architect_rubric, mirroring the slice-1 success-path tests.
- Remove REVIEWER_PLAN and TASK_PLANNER from
  test_loader_still_rejects_unshipped_roles parameters; keep REVIEWER_CODE
  (slice-3, still deferred).
- Refresh module docstring to reflect architect-loads (vs architect-raises).

* 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'}).

* Address slice-2 review: phase plumbing, doc drift, defensive checks

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.

* Address slice-2 v2 review: NB1-NB4 (N9 fail-fast, flock pattern)

---------

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>
Co-authored-by: James Wiesebron <jameswiesebron@khanacademy.org>
jwbron added a commit that referenced this pull request May 20, 2026
* Initialize SDLC contract for issue #2717

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

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

* Persist agent statefile writes before refine sync

* Persist statefiles after refine phase

* Persist HITL resolution after refine phase gate

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

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

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

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

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

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

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

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

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

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

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

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

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

Blocking fixes:

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

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

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

Non-blocking fixes:

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

* Populate contract for 2717 (#2629)

* Persist statefiles after plan phase

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

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

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

* docs: fix substrate/claude_code listing per reviewer feedback

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

Addresses egg-reviewer CHANGES_REQUESTED on PR #2718.

---------

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

Refs #2717 (slice-1 coder).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Lint clean (ruff check + format).

Refs #2717 (slice-1 tester).

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

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

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

Blocking fixes in SKILL.md:

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

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

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

Non-blocking polish in SKILL.md:

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

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

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

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

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

* test_bridge_flattened_round_trip: fix subprocess PYTHONPATH

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

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

Refs #2717 (slice-1 tester).

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

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

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

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

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

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

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

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

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

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

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

Blockers (1-4):

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

Non-blocking (5-11):

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

Tests:

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

Authored-by: egg

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

Address review feedback on PR #2724:

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

* Address non-blocking review notes on PR #2724

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

---------

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

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

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

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

* docs: fix substrate/claude_code listing per reviewer feedback

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

Addresses egg-reviewer CHANGES_REQUESTED on PR #2718.

---------

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

Refs #2717 (slice-1 coder).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Lint clean (ruff check + format).

Refs #2717 (slice-1 tester).

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

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

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

Blocking fixes in SKILL.md:

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

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

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

Non-blocking polish in SKILL.md:

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

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

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

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

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

* test_bridge_flattened_round_trip: fix subprocess PYTHONPATH

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

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

Refs #2717 (slice-1 tester).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Blockers (1-4):

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

Non-blocking (5-11):

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

Tests:

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

Authored-by: egg

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

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

v4 makes `read_plan_reviewer_verdicts` accept BOTH schemas:

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

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

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

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

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

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

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

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

Address review feedback on PR #2724:

- Add bin/read_status.py and extend write_answer.py with --answer-…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant