From a38bfbdabbaef1a9375e3f9b7a43ce2734135c81 Mon Sep 17 00:00:00 2001 From: James Wiesebron Date: Wed, 20 May 2026 13:55:35 -0700 Subject: [PATCH 1/3] Revert Claude Code substrate Abandons the Claude Code substrate initiative. Reverts the three commits that landed substrate code/docs on main: - 802f77d (#2715) walking-skeleton spike - 173f483 (#2718) substrate docs in index/structure docs - 82c4ba4 (#2731) substrate-swap rollout integration (slices 1-2) Removes orchestrator/substrate/, plugins/egg-sdlc/, the claude-code-substrate ADR, associated tests, and #2717/#2623 .egg-state pipeline artifacts. --- .../agent-outputs/2623-architect-output.json | 360 - .../2623-risk_analyst-output.json | 572 - .../agent-outputs/2717-architect-output.json | 389 - .../2717-risk_analyst-output.json | 728 -- .egg-state/brc-history/2623-plan.json | 1710 --- .egg-state/brc-history/2623-plan.md | 2286 ---- .egg-state/brc-history/2623-refine.json | 409 - .egg-state/brc-history/2623-refine.md | 494 - .../brc-history/2717-implement-slice-1.json | 7022 ----------- .../brc-history/2717-implement-slice-1.md | 8071 ------------- .../brc-history/2717-implement-slice-2.json | 8088 ------------- .../brc-history/2717-implement-slice-2.md | 10030 ---------------- .egg-state/brc-history/2717-plan.json | 1742 --- .egg-state/brc-history/2717-plan.md | 2187 ---- .egg-state/brc-history/2717-refine.json | 606 - .egg-state/brc-history/2717-refine.md | 694 -- .egg-state/contracts/issue-2623.json | 1435 --- .egg-state/contracts/issue-2717.json | 1754 --- .egg-state/drafts/2623-analysis.md | 326 - .egg-state/drafts/2623-plan.md | 1348 --- .egg-state/drafts/2717-analysis.md | 221 - .egg-state/drafts/2717-plan.md | 1557 --- docs/architecture/README.md | 1 - docs/architecture/claude-code-substrate.md | 297 - docs/development/STRUCTURE.md | 15 - docs/index.md | 1 - .../regression/_agent_tool_fake.py | 451 - integration_tests/regression/conftest.py | 60 +- .../test_bridge_flattened_round_trip.py | 468 - .../regression/test_inprocess_plan_brc.py | 999 -- .../regression/test_pretooluse_hook_nested.py | 357 - .../regression/test_substrate_smoke.py | 232 - orchestrator/concurrent_executor.py | 156 - orchestrator/substrate/__init__.py | 469 - orchestrator/substrate/_plan_phase.py | 818 -- .../substrate/claude_code/__init__.py | 28 - .../substrate/claude_code/hook_entry.py | 779 -- .../substrate/claude_code/message_bus.py | 53 - orchestrator/substrate/claude_code/policy.py | 188 - .../claude_code/settings.template.json | 15 - orchestrator/substrate/claude_code/spawner.py | 235 - .../substrate/claude_code/worktree.py | 290 - orchestrator/substrate/in_process.py | 1156 -- orchestrator/substrate/k3s_adapter.py | 186 - orchestrator/substrate/message_bus.py | 72 - orchestrator/substrate/policy.py | 64 - orchestrator/substrate/spawner.py | 115 - orchestrator/substrate/worktree.py | 49 - plugins/egg-sdlc/.claude-plugin/plugin.json | 28 - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md | 329 - .../skills/egg-sdlc/agents/architect.md | 87 - .../skills/egg-sdlc/agents/refiner.md | 118 - .../egg-sdlc/agents/reviewer_agent_design.md | 81 - .../skills/egg-sdlc/agents/reviewer_plan.md | 137 - .../skills/egg-sdlc/agents/reviewer_refine.md | 86 - .../skills/egg-sdlc/agents/risk_analyst.md | 99 - .../skills/egg-sdlc/agents/task_planner.md | 293 - .../egg-sdlc/skills/egg-sdlc/bin/preflight.py | 98 - .../skills/egg-sdlc/bin/read_status.py | 135 - .../skills/egg-sdlc/bin/run_pipeline.py | 607 - .../skills/egg-sdlc/bin/write_answer.py | 222 - shared/tests/test_claude_code_spawner.py | 249 - shared/tests/test_in_process_message_bus.py | 227 - shared/tests/test_k3s_spawner_adapter.py | 186 - shared/tests/test_local_worktree_manager.py | 195 - shared/tests/test_pretooluse_hook_policy.py | 639 - shared/tests/test_read_status.py | 185 - shared/tests/test_rubric_loader.py | 263 - shared/tests/test_run_pipeline_in_process.py | 212 - ...n_pipeline_in_process_sentinel_and_hitl.py | 414 - shared/tests/test_substrate_interfaces.py | 183 - shared/tests/test_write_answer.py | 280 - .../test_restrictions_validator.py | 323 - 73 files changed, 2 insertions(+), 65257 deletions(-) delete mode 100644 .egg-state/agent-outputs/2623-architect-output.json delete mode 100644 .egg-state/agent-outputs/2623-risk_analyst-output.json delete mode 100644 .egg-state/agent-outputs/2717-architect-output.json delete mode 100644 .egg-state/agent-outputs/2717-risk_analyst-output.json delete mode 100644 .egg-state/brc-history/2623-plan.json delete mode 100644 .egg-state/brc-history/2623-plan.md delete mode 100644 .egg-state/brc-history/2623-refine.json delete mode 100644 .egg-state/brc-history/2623-refine.md delete mode 100644 .egg-state/brc-history/2717-implement-slice-1.json delete mode 100644 .egg-state/brc-history/2717-implement-slice-1.md delete mode 100644 .egg-state/brc-history/2717-implement-slice-2.json delete mode 100644 .egg-state/brc-history/2717-implement-slice-2.md delete mode 100644 .egg-state/brc-history/2717-plan.json delete mode 100644 .egg-state/brc-history/2717-plan.md delete mode 100644 .egg-state/brc-history/2717-refine.json delete mode 100644 .egg-state/brc-history/2717-refine.md delete mode 100644 .egg-state/contracts/issue-2623.json delete mode 100644 .egg-state/contracts/issue-2717.json delete mode 100644 .egg-state/drafts/2623-analysis.md delete mode 100644 .egg-state/drafts/2623-plan.md delete mode 100644 .egg-state/drafts/2717-analysis.md delete mode 100644 .egg-state/drafts/2717-plan.md delete mode 100644 docs/architecture/claude-code-substrate.md delete mode 100644 integration_tests/regression/_agent_tool_fake.py delete mode 100644 integration_tests/regression/test_bridge_flattened_round_trip.py delete mode 100644 integration_tests/regression/test_inprocess_plan_brc.py delete mode 100644 integration_tests/regression/test_pretooluse_hook_nested.py delete mode 100644 integration_tests/regression/test_substrate_smoke.py delete mode 100644 orchestrator/substrate/__init__.py delete mode 100644 orchestrator/substrate/_plan_phase.py delete mode 100644 orchestrator/substrate/claude_code/__init__.py delete mode 100755 orchestrator/substrate/claude_code/hook_entry.py delete mode 100644 orchestrator/substrate/claude_code/message_bus.py delete mode 100644 orchestrator/substrate/claude_code/policy.py delete mode 100644 orchestrator/substrate/claude_code/settings.template.json delete mode 100644 orchestrator/substrate/claude_code/spawner.py delete mode 100644 orchestrator/substrate/claude_code/worktree.py delete mode 100644 orchestrator/substrate/in_process.py delete mode 100644 orchestrator/substrate/k3s_adapter.py delete mode 100644 orchestrator/substrate/message_bus.py delete mode 100644 orchestrator/substrate/policy.py delete mode 100644 orchestrator/substrate/spawner.py delete mode 100644 orchestrator/substrate/worktree.py delete mode 100644 plugins/egg-sdlc/.claude-plugin/plugin.json delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/SKILL.md delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md delete mode 100755 plugins/egg-sdlc/skills/egg-sdlc/bin/preflight.py delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py delete mode 100755 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py delete mode 100644 plugins/egg-sdlc/skills/egg-sdlc/bin/write_answer.py delete mode 100644 shared/tests/test_claude_code_spawner.py delete mode 100644 shared/tests/test_in_process_message_bus.py delete mode 100644 shared/tests/test_k3s_spawner_adapter.py delete mode 100644 shared/tests/test_local_worktree_manager.py delete mode 100644 shared/tests/test_pretooluse_hook_policy.py delete mode 100644 shared/tests/test_read_status.py delete mode 100644 shared/tests/test_rubric_loader.py delete mode 100644 shared/tests/test_run_pipeline_in_process.py delete mode 100644 shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py delete mode 100644 shared/tests/test_substrate_interfaces.py delete mode 100644 shared/tests/test_write_answer.py delete mode 100644 tests/sandbox/egg_agent_tools/test_restrictions_validator.py diff --git a/.egg-state/agent-outputs/2623-architect-output.json b/.egg-state/agent-outputs/2623-architect-output.json deleted file mode 100644 index d6bec396e3..0000000000 --- a/.egg-state/agent-outputs/2623-architect-output.json +++ /dev/null @@ -1,360 +0,0 @@ -{ - "issue": 2623, - "phase": "plan", - "role": "architect", - "schema_version": "1.0", - "title": "Substrate-swap walking skeleton: AgentSpawner abstraction + ClaudeCodeSpawner spike", - "summary": "Per the operator's resolution of cq-11 (Spike then plan), this plan is the walking-skeleton slice that gets ONE producer role (refiner) running through ONE phase (refine) end-to-end on the Claude Code substrate. It lands the minimum-viable AgentSpawner ABC and MessageBus ABC in egg_orchestrator, a first ClaudeCodeSpawner + InProcessMessageBus implementation, env-var substrate selection (EGG_SUBSTRATE=claude-code|k3s), a `sdlc` plugin entry point (Python-dep-declared, no shipped wheels — cq-8), a PreToolUseHookPolicy enforcer (cq-6) calling into the existing shared/egg_restrictions/patterns.py, a heredoc-style HITL generator surface (cq-7), and an integration-test parameter that runs the refine-phase happy-path under both substrates. The remaining roles (reviewer_refine, reviewer_agent_design), the remaining phases (plan/implement/pr), the full BRC concurrent dispatch, and k3s deprecation are explicitly deferred to follow-up issues once the spike proves the spawner shape.", - "recommended_approach": { - "approach": "Option A (parallel substrates, named interfaces, env-var selected), executed via cq-11's spike-first sequencing. The spike's job is to prove the AgentSpawner shape; the follow-up issues fill in the remaining roles, the second-phase BRC matrix in CI, and the operational surface (overseer thread, health checks, monitoring).", - "rationale": [ - "Operator resolved cq-1 to Option A (parallel substrates) and cq-11 to 'Spike then plan'. The plan must honor both: the abstractions land in this PR family with one concrete implementation each; the second implementations (Claude-Code-side multi-role concurrency, PR phase, etc.) are explicit follow-up scope.", - "AgentSpawner is the single largest abstraction gap in the codebase today. orchestrator/concurrent_executor.py:504 calls a SpawnFn callable that today is closured by orchestrator/kubernetes_spawner.py:1564 (create_concurrent_spawn_fn). The closure shape (role, branch, extra_env, command) is already substrate-agnostic — it just isn't named as an interface. Promoting it to an ABC is a refactor, not a rewrite.", - "MessageStore is already pluggable: orchestrator/message_store.py:531 (get_message_store) selects between in-memory MessageStore (line 200) and RedisMessageStore via EGG_MESSAGE_STORE_BACKEND. The in-process substrate reuses the in-memory MessageStore class verbatim; no new code path needed for the bus itself, only the substrate-selector wiring.", - "Policy enforcement is already factored: shared/egg_restrictions/patterns.py is a pure-Python module with no HTTP/k8s dependencies (gateway/agent_restrictions.py:58 partition_files_by_role wraps it, and sandbox/egg_agent_tools/handlers/restrictions.py:70 check_file_restriction calls it too — single source of truth). A PreToolUse hook script can import the same module without any new policy logic.", - "The contract layer (shared/egg_contracts/models.py:759 Contract, schemaVersion 1.1) is filesystem-native (.egg-state/contracts/.json) — survives the substrate transition unchanged.", - "build_system_prompt (shared/egg_harness/prompt.py:24) is already designed to assemble a system prompt from PromptSource = str | Callable[[], str] — the Claude-Code-substrate spawner just has to pass this assembled string into the subagent prompt. The depth-gap closure (#2622's structural cause) becomes a free side-effect of routing through the real spawner.", - "The integration_tests/regression/ suite is already substrate-portable in its mocking layout (KubernetesSpawner is constructor-injected with k8s_client=MockKubernetesClient in tests). Adding a pytest.mark.parametrize('substrate', [...]) is a CI-matrix change, not a test-rewrite — exactly what cq-3 selected.", - "Spike-first contains blast radius. The walking-skeleton slice touches ~6 source files (orchestrator interfaces + ClaudeCodeSpawner + skill bootstrap + one test) and keeps k3s users running unchanged. Failure modes are visible at one PR's blast radius, not at the substrate-swap's full surface." - ] - }, - "component_breakdown": [ - { - "component": "egg_orchestrator/spawner.py (NEW)", - "purpose": "AgentSpawner ABC + SpawnRequest/AgentResult dataclasses. The single named interface the orchestrator depends on for agent dispatch.", - "interface": { - "abc_name": "AgentSpawner", - "method_signature": "def spawn(self, request: SpawnRequest) -> AgentResult", - "spawn_request_fields": [ - "role: AgentRole", - "prompt: str (full assembled system+task prompt)", - "env: dict[str, str]", - "worktree_path: pathlib.Path", - "pipeline_id: str", - "slice_id: str | None", - "phase: PipelinePhase", - "command: list[str] | None (compat with today's kubernetes_spawner.spawn_agent_job command arg)" - ], - "agent_result_fields": [ - "exit_code: int", - "stdout: str", - "stderr: str", - "commit_sha: str | None (HEAD of agent's branch on completion)", - "agent_role: AgentRole", - "pipeline_id: str", - "duration_seconds: float" - ] - }, - "scope_for_spike": "ABC + dataclasses + one implementation (ClaudeCodeSpawner). KubernetesSpawner becomes the second implementation in the same PR via an adapter (existing create_concurrent_spawn_fn at orchestrator/kubernetes_spawner.py:1564 wraps the existing spawn_agent_job call to match the new AgentResult return shape).", - "decision_alignment": "cq-4 → synchronous spawn(role, prompt, env, worktree) -> AgentResult" - }, - { - "component": "egg_orchestrator/substrate.py (NEW)", - "purpose": "Substrate registry + env-var selector. Single import point that returns the right (AgentSpawner, MessageBus, PolicyEnforcer, WorktreeManager, HITLSurface) tuple based on EGG_SUBSTRATE.", - "interface": { - "function": "def get_substrate(substrate: str | None = None) -> Substrate", - "substrate_dataclass": "Substrate(spawner: AgentSpawner, bus: MessageBus, policy: PolicyEnforcer, worktrees: WorktreeManager, hitl: HITLSurface)", - "env_var": "EGG_SUBSTRATE (values: k3s, claude-code; default: k3s for back-compat)" - }, - "scope_for_spike": "Registry + two substrate factories: build_k3s_substrate(), build_claude_code_substrate(). The factory bodies stay thin — each constructs the relevant ABC subclass and wires them.", - "decision_alignment": "cq-1 → parallel substrates, env-var-selected" - }, - { - "component": "egg_orchestrator/spawners/claude_code_spawner.py (NEW)", - "purpose": "Concrete AgentSpawner implementation that dispatches to Claude Code's Agent tool from the parent session.", - "design": { - "call_shape": "Inside the parent Claude Code session, the spawner calls the Agent tool with subagent_type='general-purpose', the SpawnRequest.prompt verbatim, and isolation='worktree' for git isolation. The Agent call blocks until the subagent completes; the return contains stdout (which the spawner captures as AgentResult.stdout) and the exit status maps to AgentResult.exit_code.", - "worktree_lifecycle": "Per cq-5, the spawner does NOT use Claude Code's EnterWorktree primitive. Instead it pre-creates the worktree at SpawnRequest.worktree_path (mirroring today's WORKTREE_BASE_DIR layout, see gateway/worktree_manager.py:49 and orchestrator/kubernetes_spawner.py:65), checks the agent's branch out there, and passes that path to the Agent call. Teardown is filesystem-level after the AgentResult is returned.", - "commit_sha_capture": "After Agent returns, the spawner runs `git -C rev-parse HEAD` and records the SHA in AgentResult.commit_sha so the orchestrator can attach commit-bound ACKs (action_guards.validate_invariants INV-6 at orchestrator/action_guards.py:753 requires this).", - "non_claude_code_caller": "Per feedback-1 Q4 (operator: 'support non-Claude-Code callers as secondary goal'), the ClaudeCodeSpawner is constructor-injected with a `agent_tool_callable` so a test or CLI can pass a stub. A separate EggHarnessSpawner subprocess-based implementation is stub-only in this slice (not in scope); it lives as a TODO with a follow-up issue link." - }, - "scope_for_spike": "Single role (refiner) and single phase (refine). Multi-role concurrent dispatch (the BRC parallel-producer case) is in the spawner's surface but lands as a follow-up — for the spike, the orchestrator calls spawn() once per role serially. ThreadPoolExecutor-style parallelism at orchestrator/concurrent_executor.py:469 stays unchanged; the spike just stays in the N=1 case.", - "decision_alignment": "cq-4 (synchronous spawn), cq-5 (port WORKTREE_BASE_DIR), cq-11 (spike: single-role walking skeleton)" - }, - { - "component": "egg_orchestrator/buses/in_process_bus.py (NEW)", - "purpose": "In-process MessageBus implementation reusing the existing in-memory MessageStore.", - "design": { - "implementation": "Thin wrapper around orchestrator/message_store.py:200 (existing in-memory MessageStore class). The MessageBus ABC's surface is the six methods MessageStore already implements: add_message, get_messages, get_messages_with_meta, get_latest_id, get_status, clear (orchestrator/message_store.py:224, 253, 315, 478, 489, 505). Promoting MessageStore to a named ABC is a renaming refactor.", - "wait_loop_semantics": "Existing MessageStore.get_messages already supports the wait + wait_for_types blocking primitive (orchestrator/message_store.py:253). InProcessMessageBus inherits this for free. No new event-loop integration is needed for the spike — the BRC consensus-wait pattern (egg-orch message wait-loop) already works against an in-memory MessageStore.", - "thread_safety": "In-memory MessageStore is already thread-safe (existing tests confirm). The Claude-Code-substrate orchestrator runs in-process to the parent Claude Code session, so the bus is touched only by the orchestrator main thread + the ThreadPoolExecutor it owns — no IPC." - }, - "scope_for_spike": "Lift the existing in-memory MessageStore (no behavior changes) behind a named MessageBus ABC. Wire it into Substrate.bus.", - "decision_alignment": "Implicit in cq-1 (orchestrator-as-bus per refine analysis); reuses existing primitive" - }, - { - "component": "egg_orchestrator/policy/pretooluse_hook.py (NEW) + plugins/sdlc/hooks/pretooluse_policy.sh (NEW)", - "purpose": "PolicyEnforcer ABC + ClaudeCode-side concrete impl that calls a PreToolUse hook script. The hook script invokes shared/egg_restrictions/patterns.py and refuses Write/Edit/Bash on blocked paths.", - "design": { - "abc": "PolicyEnforcer with one method: validate(role: AgentRole, action: ToolAction, target: str) -> PolicyVerdict, where ToolAction is one of {WRITE, EDIT, BASH_GIT_PUSH, BASH_GH_PR_CREATE, ...} and PolicyVerdict is {ALLOW, DENY (reason)}.", - "k3s_impl": "Existing gateway/agent_restrictions.py:58 partition_files_by_role wrapping is already a PolicyEnforcer in everything but name. K3sPolicyEnforcer is a thin adapter — same logic, called from the gateway HTTP boundary as today.", - "claude_code_impl": "PreToolUseHookPolicy registers a PreToolUse hook in .claude/settings.json (skill-installed). The hook script (plugins/sdlc/hooks/pretooluse_policy.sh, or .py) reads the proposed tool call from stdin, extracts (role, action, target), calls into the same shared/egg_restrictions/patterns.py::AgentFilePattern.can_write() (shared/egg_restrictions/patterns.py:57), and exits 0 (allow) or 1 (deny) with a stderr message that surfaces in the Claude Code UI. Single source of truth preserved: the hook and the gateway both import the same patterns module.", - "scope_in_spike": "Hook script + .claude/settings.json registration template that the sdlc skill installs. The spike enforces Write/Edit only; Bash interception (git push, gh pr create) is in the same module but the spike doesn't exercise those paths (refine phase, refiner role writes .egg-state/drafts/ only)." - }, - "decision_alignment": "cq-6 → PreToolUse hooks in .claude/settings.json" - }, - { - "component": "egg_orchestrator/worktrees/local_worktree_manager.py (NEW)", - "purpose": "WorktreeManager ABC + filesystem-native implementation that ports gateway/worktree_manager.py to a substrate-agnostic shape.", - "design": { - "abc": "WorktreeManager with create_worktree(pipeline_id, role, slice_id=None, base_branch=...) -> Path, delete_worktree(path) -> None. Mirrors the existing gateway/worktree_manager.py public surface (create_worktrees / delete_worktrees, see gateway/worktree_manager.py:164+).", - "directory_layout": "/--// — same layout as today (orchestrator/kubernetes_spawner.py:65, gateway/worktree_manager.py:49). The default base is ~/.egg-worktrees on the user's filesystem (no k8s persistent volume).", - "concrete_impl": "LocalWorktreeManager uses git CLI (subprocess) to run `git worktree add -b ` and `git worktree remove `. No HTTP, no k8s API. The existing gateway code is mostly portable — extract into shared/.", - "credentials": "No credential injection in the worktree path — the user's local git config holds the GitHub token (or uses gh CLI auth)." - }, - "scope_for_spike": "ABC + LocalWorktreeManager class. Both substrates use it (the gateway-side k3s path eventually delegates here too, but for the spike the k3s substrate keeps its existing gateway worktree code unchanged).", - "decision_alignment": "cq-5 → port egg's WORKTREE_BASE_DIR model" - }, - { - "component": "egg_orchestrator/hitl/heredoc_surface.py (NEW)", - "purpose": "HITLSurface ABC + heredoc-style generator implementation that yields HITLDecision objects to the parent skill, which renders them via AskUserQuestion and feeds answers back.", - "design": { - "abc": "HITLSurface with pause_for_decision(question, options) -> HITLDecision. The orchestrator's add_decision call (orchestrator/models.py:1106) creates the decision; the HITLSurface implementation yields it.", - "heredoc_impl": "HeredocHITLSurface is a Python coroutine/generator. The skill's outer Python loop drives the orchestrator: each yielded HITLDecision is rendered by AskUserQuestion (the parent Claude Code session's tool, not an agent tool); the user answer is sent back into the coroutine via .send() and resolved via Pipeline.resolve_decision (orchestrator/models.py:1130).", - "k3s_impl": "K3sHITLSurface is the existing MCP provide_input path — the orchestrator's HTTP endpoint polls for resolution and agents call provide_input via the gateway (orchestrator/mcp_tools.py:2099 _handle_provide_input). No code change for k3s.", - "scope_in_spike": "The refiner role on a fresh issue usually emits ≥1 HITL decision (the substrate analysis already issued cq-1..cq-11 + feedback-1). The spike must exercise at least one round-trip through HeredocHITLSurface before the refiner completes." - }, - "decision_alignment": "cq-7 → Heredoc-style synchronous: orchestrator yields HITLDecision objects; skill renders via AskUserQuestion" - }, - { - "component": "plugins/sdlc/ (NEW marketplace plugin)", - "purpose": "Thin Claude Code skill entry point that boots the in-process orchestrator with EGG_SUBSTRATE=claude-code.", - "contents": { - ".claude-plugin/plugin.json": "Manifest with name='sdlc', description, version='0.1.0', and a pip dependency declaration ('egg>=X.Y.Z' — exact pin TBD by tester). Plugin metadata declares the pip dep per cq-8; install docs add a pre-flight `pip install egg` step.", - "SKILL.md": "Skill instructions: parses the user's input (issue number or PR), constructs a Pipeline object, calls orchestrator.run_phase() in a loop, renders HITLDecision yields via AskUserQuestion, surfaces final status.", - "commands/sdlc.md": "Slash-command entry: /sdlc launches the full pipeline; /sdlc-phase runs a single phase.", - "hooks/pretooluse_policy.sh": "PreToolUse hook script (see policy component above).", - "agents/": "Empty in the spike — the orchestrator uses build_system_prompt(sources) to assemble per-role prompts dynamically rather than per-role markdown files. (Existing plugins/refine-plan/skills/refine-plan/agents/refiner.md becomes a PromptSource the orchestrator loads, not a Claude Code subagent definition.)" - }, - "scope_for_spike": "Entry-point + slash command + PreToolUse hook. The skill imports egg_orchestrator and calls a new boot helper that returns a Substrate; passes that into Pipeline construction.", - "decision_alignment": "cq-8 → plugin.json declares pip dependencies (not vendored, not shipped as wheels)" - }, - { - "component": "integration_tests/regression/conftest.py (EXTEND)", - "purpose": "Add `substrate` parametrize fixture so the existing regression tests can run on both k3s and claude-code substrates.", - "design": { - "fixture": "@pytest.fixture(params=['k3s', 'claude-code']) def substrate(request): ... — returns a Substrate object the test uses to construct the orchestrator. Tests that are k3s-specific (e.g. test_live_pod_guard.py) are decorated with @pytest.mark.skipif(substrate == 'claude-code', reason='requires k8s primitive') at the function level.", - "scope": "Only one test gets parametrized in the spike (test_brc_single_cycle.py — the simplest refine-phase round-trip). Other tests remain k3s-only until follow-up issues parametrize them. CI runs both dimensions; matrix failures block merge.", - "spike_test_path": "test_brc_single_cycle.py — refiner + reviewer_refine, refine phase, no slice-DAG, no concurrent dispatch. The substrate=claude-code branch uses ClaudeCodeSpawner with a stub `agent_tool_callable` that returns a fixed AgentResult (so CI doesn't actually require Anthropic creds)." - }, - "decision_alignment": "cq-3 → extend integration_tests/regression/ with substrate parameter" - }, - { - "component": "docs/architecture/substrate-swap-adr.md (NEW)", - "purpose": "Architecture Decision Record documenting the substrate-swap interfaces, the spike scope, and the deferred work.", - "contents_outline": "ADR header (status: Accepted), Context (issue body summary + HITL resolutions table), Decision (Option A, spike-first), Consequences (k3s remains co-equal per cq-9; Python install footprint per cq-8; SendMessage gating documented as 'orchestrator-as-bus sidesteps for BRC correctness; mid-cycle revision is a quality booster only'), Alternatives Considered (Options B/C/D from refine analysis), Follow-up issues table (multi-role spawner, multi-phase parametrization, PreToolUse Bash interception, EggHarnessSpawner, conformance matrix expansion, k3s deprecation review)." - } - ], - "execution_context_and_primitive_audit": { - "preamble": "Per #2594, every runtime primitive cited here is classified on two orthogonal axes: (a) purpose — unit-test-only vs deployed-pod / production code; (b) execution context — in-sandbox-agent vs trusted-CI-runner vs parent-claude-code-session (new context introduced by this substrate). This is the surface most likely to attract reviewer NACKs.", - "primitives": [ - { - "primitive": "AgentSpawner ABC + SpawnRequest/AgentResult", - "evidence": "egg_orchestrator/spawner.py (NEW); replaces today's untyped SpawnFn = Callable[..., Any] at orchestrator/concurrent_executor.py:52", - "purpose": "production code (deployed-pod for k3s substrate; parent-claude-code-session for claude-code substrate)", - "execution_context": "k3s substrate: trusted-CI-runner constructs KubernetesSpawner via create_concurrent_spawn_fn (orchestrator/kubernetes_spawner.py:1564), in-sandbox-agent never sees it (agent runs in its own pod). claude-code substrate: parent-claude-code-session constructs ClaudeCodeSpawner; in-sandbox-agent is now 'spawned subagent', not a k8s pod." - }, - { - "primitive": "MessageBus ABC + InProcessMessageBus", - "evidence": "Promotion of in-memory MessageStore at orchestrator/message_store.py:200; the get_message_store factory at orchestrator/message_store.py:531 selects backend via EGG_MESSAGE_STORE_BACKEND env var", - "purpose": "production code (both substrates)", - "execution_context": "k3s substrate: deployed-pod RedisMessageStore at orchestrator/redis_message_store.py:107. claude-code substrate: parent-claude-code-session in-memory MessageStore. trusted-CI-runner tests already use the in-memory backend via EGG_MESSAGE_STORE_BACKEND=memory." - }, - { - "primitive": "PolicyEnforcer ABC + PreToolUseHookPolicy", - "evidence": "Hook script at plugins/sdlc/hooks/pretooluse_policy.sh (NEW); policy logic imported from shared/egg_restrictions/patterns.py:57 (AgentFilePattern.can_write); existing call site at sandbox/egg_agent_tools/handlers/restrictions.py:70 (check_file_restriction) and gateway/agent_restrictions.py:58 (partition_files_by_role) confirm pure-Python importability", - "purpose": "production code (gateway-side: deployed-pod; hook-side: parent-claude-code-session)", - "execution_context": "k3s substrate: deployed-pod gateway/gateway.py /api/v1/git/push (line 1097) calls check_phase_file_restrictions (gateway/phase_filter.py:1036). claude-code substrate: PreToolUse hook in .claude/settings.json runs in parent-claude-code-session before Write/Edit/Bash; same policy module imported. NOTE: hook executes in the user's local interpreter, not in a sandbox — the trust boundary moves from 'gateway sidecar in-cluster' to 'user's local Claude Code session'. The substrate-swap ADR must document this trust-context shift." - }, - { - "primitive": "WorktreeManager ABC + LocalWorktreeManager", - "evidence": "egg_orchestrator/worktrees/local_worktree_manager.py (NEW), porting gateway/worktree_manager.py:49 (WORKTREE_BASE_DIR) + gateway/worktree_manager.py:164 (worktree create/delete public surface). Directory layout: /--//", - "purpose": "production code (both substrates)", - "execution_context": "k3s substrate: deployed-pod gateway runs worktree CLI inside the gateway container, persisted on PV. claude-code substrate: parent-claude-code-session runs `git worktree add/remove` on the user's local filesystem at ~/.egg-worktrees/. trusted-CI-runner tests stub the WorktreeManager with a tempfile.TemporaryDirectory." - }, - { - "primitive": "HITLSurface ABC + HeredocHITLSurface", - "evidence": "egg_orchestrator/hitl/heredoc_surface.py (NEW). Existing pause point: orchestrator/models.py:1106 Pipeline.add_decision; resume: orchestrator/models.py:1130 Pipeline.resolve_decision; k3s-substrate resume path: orchestrator/mcp_tools.py:2099 _handle_provide_input", - "purpose": "production code (both substrates)", - "execution_context": "k3s substrate: deployed-pod orchestrator's HTTP endpoint polls for resolution; in-sandbox-agent calls provide_input MCP verb. claude-code substrate: parent-claude-code-session orchestrator yields HITLDecision to the skill loop; AskUserQuestion is the parent-session tool (not available to subagents); the user-typed reply goes back into the generator via .send(). The skill, not the subagent, holds the AskUserQuestion authority — this is the key architectural shift." - }, - { - "primitive": "Agent tool with isolation='worktree' (Claude Code native primitive)", - "evidence": "Claude Code subagent dispatch is the system tool the parent session uses; existing skill calls it without isolation today (plugins/refine-plan/skills/refine-plan/SKILL.md:66 'Agent call with subagent_type: \"general-purpose\"'). The spike adds isolation='worktree' for git isolation; the spawner pre-creates the worktree (cq-5) and passes the path through the subagent's prompt header so the subagent knows its CWD.", - "purpose": "production code (claude-code substrate only)", - "execution_context": "parent-claude-code-session only. NOTE: this primitive is gated on Claude Code itself, not on any developer-mode flag (per feedback-1 Q3, the substrate must work on stable Claude Code). Verify before merging that isolation='worktree' is in stable Claude Code's Agent tool surface, not a dev-mode preview." - }, - { - "primitive": "EGG_SUBSTRATE env var", - "evidence": "New env var; default 'k3s' for back-compat. Read at egg_orchestrator/substrate.py:get_substrate() (NEW). Existing precedent: EGG_HARNESS at shared/egg_agent/client.py:104, EGG_MESSAGE_STORE_BACKEND at orchestrator/message_store.py:552.", - "purpose": "production code (both substrates) + trusted-CI-runner (CI matrix dimension)", - "execution_context": "All three contexts. trusted-CI-runner pytest reads it via the substrate fixture (conftest.py change). parent-claude-code-session sets it to 'claude-code' at skill boot. deployed-pod orchestrator container defaults it to 'k3s'." - }, - { - "primitive": "shared/egg_restrictions/patterns.py::AgentFilePattern.can_write(file_path)", - "evidence": "shared/egg_restrictions/patterns.py:57; pure-Python module; imports json, logging, os, posixpath, collections.abc.Callable, dataclasses, egg_contracts.agent_roles. NO HTTP, k8s, or external services.", - "purpose": "production code; deployed-pod (k3s gateway), in-sandbox-agent (MCP check_file_restriction), and parent-claude-code-session (PreToolUse hook) all import it.", - "execution_context": "all three. Confirms single-source-of-truth across the substrate boundary." - }, - { - "primitive": "Contract schema (shared/egg_contracts/models.py:759, schemaVersion 1.1)", - "evidence": "Pydantic BaseModel at shared/egg_contracts/models.py:759; schemaVersion at line 762; top-level fields: issue (line 774), current_phase (line 785), slices (line 798), decisions (line 808), agent_executions (line 833)", - "purpose": "production code (both substrates use unchanged); test fixtures also construct Contract instances", - "execution_context": "all three contexts read/write to .egg-state/contracts/.json. No substrate coupling." - }, - { - "primitive": "validate_invariants(...) BRC invariants INV-1..INV-6", - "evidence": "orchestrator/action_guards.py:631; INV-1 (line 701, no-confirmed-with-unresolved-NACK), INV-2 (line 718, no-confirmed-with-stale-ACK), INV-3 (line 721, no-confirmed-with-unreviewed-changes), INV-4 (line 669, no-confirmed-with-zero-proposal-producer), INV-5 (line 652, is_fully_acked consistency), INV-6 (line 753, ack_commit_sha consistency)", - "purpose": "production code (both substrates use unchanged); validate_invariants is called by the orchestrator before CONSENSUS_CONFIRMED is emitted", - "execution_context": "in-process to whatever runs the orchestrator. INV-6 (line 753) requires the spawner to return commit_sha — that's why AgentResult.commit_sha is a first-class field above, not optional." - }, - { - "primitive": "ConcurrentPhaseExecutor (orchestrator/concurrent_executor.py:137) + ThreadPoolExecutor (line 469)", - "evidence": "Class constructor at orchestrator/concurrent_executor.py:137; ThreadPoolExecutor dispatch at line 469; SpawnFn type alias at line 52", - "purpose": "production code (both substrates use unchanged)", - "execution_context": "in-process to the orchestrator. Spike does NOT change concurrency model — just substitutes the SpawnFn closure to invoke ClaudeCodeSpawner instead of KubernetesSpawner." - }, - { - "primitive": "build_system_prompt(sources) (shared/egg_harness/prompt.py:24)", - "evidence": "Function signature `def build_system_prompt(sources: list[PromptSource]) -> str` at shared/egg_harness/prompt.py:24; PromptSource = str | Callable[[], str] at line 16. Joins with `\\n\\n---\\n\\n`.", - "purpose": "production code (both substrates depend on it for depth closure per #2622); also used in unit tests at shared/tests/test_egg_harness/test_prompt.py:9", - "execution_context": "parent-claude-code-session calls it before invoking the Agent tool (subagent receives the full assembled prompt in its prompt arg); deployed-pod calls it inside the orchestrator before issuing the agent job. Identical call shape — the depth gap closes structurally." - } - ] - }, - "scope_explicitly_in_spike": [ - "AgentSpawner ABC + ClaudeCodeSpawner (one role: refiner; one phase: refine)", - "MessageBus ABC promotion (no behavioral change; just naming)", - "PolicyEnforcer ABC + PreToolUseHookPolicy (Write/Edit only; Bash intercept is in the same module but spike doesn't exercise)", - "WorktreeManager ABC + LocalWorktreeManager (basic create/delete)", - "HITLSurface ABC + HeredocHITLSurface (round-trip exercised by cq-1..cq-11 style prompts)", - "Substrate registry + EGG_SUBSTRATE env var", - "plugins/sdlc/ marketplace skill entry point (slash command + hook + plugin.json)", - "One integration test parametrized: test_brc_single_cycle.py (refine phase only)", - "ADR documenting the design + deferred follow-ups" - ], - "scope_explicitly_deferred_to_followups": [ - "Multi-role concurrent dispatch on the claude-code substrate (parallel reviewers within a phase). Follow-up issue must address subagent ceiling per the issue body's 'concurrency limits' constraint and validate with a 4-role plan phase.", - "Plan/implement/pr phases on the claude-code substrate. Each becomes a follow-up: plan phase first (since cq-2 = all phases), then implement (the heaviest BRC surface — coder/tester/documenter producers + reviewer_code/reviewer_contract reviewers), then pr (gh CLI through the policy-enforced Bash path).", - "PreToolUse Bash interception (git push, gh pr create, gh execute). The hook script's policy module already covers these; the spike doesn't exercise the call paths because the refine phase writes only .egg-state/drafts/.", - "EggHarnessSpawner (non-Claude-Code subprocess-driven third implementation, per feedback-1 Q4). Constructor-shape compatibility is preserved in this spike; an actual implementation is its own slice.", - "Substrate-parametrize the remaining ~9 regression tests in integration_tests/regression/. cq-3's CI-matrix vision is fully realized only when all portable tests run on both substrates.", - "Latency / throughput budget characterization (feedback-1 Q2: operator deferred setting a budget until empirical data exists post-spike). Add to the spike's PR summary: measured refiner-phase wall-clock on the claude-code substrate vs an equivalent k3s run.", - "Telemetry / privacy review (feedback-1 Q6). Document checkpoint location (~/.egg-state//) in install docs; add an egg-state prune verb. Tracked in a separate doc PR.", - "Slice-DAG concurrency (sibling slices in a wave running in parallel). The spike uses N=1 slice; the parallel-slices invariant set lives at orchestrator/concurrent_executor.py:469 and ports unchanged but isn't exercised.", - "Overseer thread / in-process health checks. Egg's overseer pod (egg_overseer/) ports to an in-process thread for the claude-code substrate; punted from the spike to keep the skeleton walking.", - "k3s deprecation review (cq-9 = leave k3s indefinitely; no deprecation work in this issue family)" - ], - "risks_for_risk_analyst_to_pick_up": [ - "Trust-boundary shift: PreToolUse hooks run in the user's local Claude Code interpreter, not in a sandbox. A malicious agent that bypasses the prompt-time restrictions reaches Write/Bash directly — the hook is the only enforcement. The k3s gateway has defense-in-depth (NetworkPolicy + sidecar). The claude-code substrate relies on the hook's correctness and the user's Claude Code install integrity. Document mitigation in ADR.", - "Agent tool subagent ceiling. The spike runs N=1 so this doesn't bite, but the follow-up that adds parallel reviewer dispatch must measure whether the ThreadPoolExecutor pattern at orchestrator/concurrent_executor.py:469 oversubscribes Claude Code's subagent slot. Mitigation: cap max_concurrent per substrate at substrate-build time.", - "Subagent context window vs egg's max_turns=1000 (docs/guides/concurrent-execution.md:97). For the refiner role on a substantive issue (#2623 itself is a good example), the parent's analysis prompt + the issue body + the codebase reads can approach context limits. cq-10 (hybrid checkpoint+fork) covers the medium term but the spike has no checkpoint infrastructure on the claude-code substrate yet — if the refiner exhausts context, it crashes. Mitigation: spike's exit criteria include 'completes on at least one substantive issue without context exhaustion'.", - "isolation='worktree' availability: the design assumes stable Claude Code exposes this on the Agent tool. If it's developer-mode-only, the spike falls back to the spawner pre-creating the worktree and not using the Agent tool's isolation flag (effectively the design's behavior anyway because cq-5 chose the WORKTREE_BASE_DIR port). Mitigation: spike's smoke test should verify the Agent tool call shape on stable CC before declaring done.", - "EggContract schemaVersion 1.1 already in flight (shared/egg_contracts/models.py:762). Any schema bump landed in parallel issues during the spike's review cycle may require a rebase. Mitigation: rebase aggressively; treat schema-bump rebase as a coder-task in the slice.", - "RedisMessageStore migration: the k3s substrate's RedisMessageStore at orchestrator/redis_message_store.py:107 has subtle wait-loop and stream-ID semantics that the in-memory MessageStore matches in tests but may diverge in production (e.g. consumer-group semantics). The spike doesn't touch RedisMessageStore but the AgentSpawner refactor lands as one PR with the bus rename — risk that the refactor touches Redis paths inadvertently. Mitigation: keep the rename mechanical; do not refactor Redis logic in the same PR.", - "Plug-in installation footprint per cq-8: 'pip install egg' is a pre-flight step that the marketplace install cannot enforce. Users who skip it get cryptic ImportError messages at /sdlc invocation time. Mitigation: the skill's bootstrap script does an importlib.util.find_spec check and prints an actionable install instruction on miss." - ], - "tasks_for_task_planner_to_pick_up": [ - { - "suggested_task_id": "task-1", - "title": "Introduce AgentSpawner ABC + SpawnRequest/AgentResult dataclasses; refactor existing KubernetesSpawner to satisfy the interface (adapter at create_concurrent_spawn_fn)", - "files_likely": ["egg_orchestrator/spawner.py (NEW)", "orchestrator/concurrent_executor.py (edit: replace SpawnFn at line 52 with AgentSpawner)", "orchestrator/kubernetes_spawner.py (edit: create_concurrent_spawn_fn at line 1564 returns an AgentSpawner instance)"], - "acceptance_criteria": "All existing tests pass; orchestrator.spawn_fn becomes AgentSpawner.spawn; KubernetesSpawner test fixtures construct a real or stub AgentSpawner." - }, - { - "suggested_task_id": "task-2", - "title": "Promote orchestrator/message_store.py:200 MessageStore to a named MessageBus ABC; add a substrate registry that picks MessageStore vs RedisMessageStore via EGG_SUBSTRATE", - "files_likely": ["egg_orchestrator/substrate.py (NEW)", "orchestrator/message_store.py (edit: subclass relationship, keep behavior)", "orchestrator/redis_message_store.py (edit: declare it implements MessageBus)"] - }, - { - "suggested_task_id": "task-3", - "title": "Implement ClaudeCodeSpawner (orchestrator/spawners/claude_code_spawner.py); pre-creates worktree via LocalWorktreeManager, calls Agent tool, captures stdout + commit_sha", - "files_likely": ["egg_orchestrator/spawners/claude_code_spawner.py (NEW)", "tests for it in shared/tests/ or new module"] - }, - { - "suggested_task_id": "task-4", - "title": "Implement LocalWorktreeManager (port gateway/worktree_manager.py to substrate-agnostic surface)", - "files_likely": ["egg_orchestrator/worktrees/local_worktree_manager.py (NEW)", "gateway/worktree_manager.py (refactor: thin wrapper that delegates)"] - }, - { - "suggested_task_id": "task-5", - "title": "Implement PreToolUseHookPolicy; ship plugins/sdlc/hooks/pretooluse_policy.sh that imports shared/egg_restrictions/patterns.py and refuses Write/Edit on blocked paths", - "files_likely": ["egg_orchestrator/policy/pretooluse_hook.py (NEW)", "plugins/sdlc/hooks/pretooluse_policy.sh (NEW)", "plugins/sdlc/.claude/settings.json (NEW: registers the hook)"] - }, - { - "suggested_task_id": "task-6", - "title": "Implement HeredocHITLSurface (generator-based HITLSurface impl); wire to Pipeline.add_decision / resolve_decision", - "files_likely": ["egg_orchestrator/hitl/heredoc_surface.py (NEW)", "orchestrator/models.py (no edit; Pipeline already exposes the right primitives)"] - }, - { - "suggested_task_id": "task-7", - "title": "Create plugins/sdlc/ marketplace plugin entry point (plugin.json with pip dep, SKILL.md, commands/sdlc.md, hooks/)", - "files_likely": ["plugins/sdlc/.claude-plugin/plugin.json (NEW)", "plugins/sdlc/skills/sdlc/SKILL.md (NEW)", "plugins/sdlc/skills/sdlc/commands/sdlc.md (NEW)"] - }, - { - "suggested_task_id": "task-8", - "title": "Parametrize integration_tests/regression/test_brc_single_cycle.py with substrate fixture; CI matrix runs both substrates", - "files_likely": ["integration_tests/regression/conftest.py (edit: add substrate fixture)", "integration_tests/regression/test_brc_single_cycle.py (edit: accept substrate fixture)", "Makefile or CI config (edit: add substrate matrix dimension)"] - }, - { - "suggested_task_id": "task-9", - "title": "Write substrate-swap ADR at docs/architecture/substrate-swap-adr.md; document deferred work + trust-boundary shift + cq-1..cq-11 resolutions", - "files_likely": ["docs/architecture/substrate-swap-adr.md (NEW)"] - } - ], - "evidence_citations_index": { - "orchestrator/concurrent_executor.py:52": "SpawnFn type alias (becomes AgentSpawner.spawn)", - "orchestrator/concurrent_executor.py:137": "ConcurrentPhaseExecutor constructor", - "orchestrator/concurrent_executor.py:469": "ThreadPoolExecutor dispatch", - "orchestrator/concurrent_executor.py:504": "_spawn_agent (the call site to AgentSpawner.spawn after refactor)", - "orchestrator/kubernetes_spawner.py:65": "WORKTREE_BASE_DIR constant (one of two definitions)", - "orchestrator/kubernetes_spawner.py:249": "KubernetesSpawner class", - "orchestrator/kubernetes_spawner.py:411": "spawn_agent_job public method", - "orchestrator/kubernetes_spawner.py:1564": "create_concurrent_spawn_fn factory (becomes the k3s adapter)", - "orchestrator/message_store.py:200": "in-memory MessageStore class (becomes InProcessMessageBus)", - "orchestrator/message_store.py:224": "MessageStore.add_message", - "orchestrator/message_store.py:253": "MessageStore.get_messages (blocking wait-loop primitive)", - "orchestrator/message_store.py:315": "MessageStore.get_messages_with_meta", - "orchestrator/message_store.py:478": "MessageStore.get_latest_id", - "orchestrator/message_store.py:489": "MessageStore.get_status", - "orchestrator/message_store.py:505": "MessageStore.clear", - "orchestrator/message_store.py:531": "get_message_store factory (env-var dispatcher)", - "orchestrator/message_store.py:552": "EGG_MESSAGE_STORE_BACKEND env var read", - "orchestrator/redis_message_store.py:107": "RedisMessageStore class (becomes the k3s MessageBus impl)", - "orchestrator/models.py:1106": "Pipeline.add_decision (HITL pause point)", - "orchestrator/models.py:1130": "Pipeline.resolve_decision (HITL resume)", - "orchestrator/mcp_tools.py:2099": "_handle_provide_input (k3s-substrate HITL resume path)", - "orchestrator/action_guards.py:631": "validate_invariants function (BRC consensus correctness)", - "orchestrator/action_guards.py:652": "INV-5 is_fully_acked consistency", - "orchestrator/action_guards.py:669": "INV-4 no-confirmed-with-zero-proposal-producer", - "orchestrator/action_guards.py:701": "INV-1 no-confirmed-with-unresolved-NACK", - "orchestrator/action_guards.py:718": "INV-2 no-confirmed-with-stale-ACK", - "orchestrator/action_guards.py:721": "INV-3 no-confirmed-with-unreviewed-changes", - "orchestrator/action_guards.py:753": "INV-6 ack_commit_sha consistency (requires AgentResult.commit_sha)", - "gateway/gateway.py:1097": "/api/v1/git/push route (becomes K3sPolicyEnforcer entry)", - "gateway/phase_filter.py:1036": "check_phase_file_restrictions function", - "gateway/agent_restrictions.py:58": "partition_files_by_role (gateway-side wrapper of patterns.py)", - "gateway/worktree_manager.py:49": "WORKTREE_BASE_DIR (second definition)", - "gateway/worktree_manager.py:164": "WorktreeManager class (public surface to port)", - "gateway/anthropic_credentials.py:1": "Anthropic credential layer (k3s-substrate; not used in claude-code substrate)", - "shared/egg_restrictions/patterns.py:57": "AgentFilePattern.can_write — single source of truth for file-write policy", - "shared/egg_restrictions/checker.py:32": "check_agent_file_access entry point", - "shared/egg_harness/prompt.py:24": "build_system_prompt — depth-gap closure primitive", - "shared/egg_harness/prompt.py:16": "PromptSource = str | Callable[[], str]", - "shared/egg_contracts/models.py:759": "Contract Pydantic BaseModel", - "shared/egg_contracts/models.py:762": "schemaVersion 1.1", - "shared/egg_agent/client.py:104": "EGG_HARNESS env var (precedent for EGG_SUBSTRATE)", - "sandbox/egg_agent_tools/handlers/restrictions.py:70": "MCP check_file_restriction handler (proof patterns.py is sandbox-importable)", - "sandbox/egg_agent_tools/tools/sdlc.py:256": "MCP tool wrapper for check_file_restriction", - "plugins/refine-plan/.claude-plugin/plugin.json:1": "existing marketplace manifest precedent", - "plugins/refine-plan/skills/refine-plan/SKILL.md:66": "Agent tool call shape today (subagent_type='general-purpose')", - "docs/guides/concurrent-execution.md:97": "max_turns: 1000 target (context-window constraint)", - "docs/guides/harness-configuration.md": "harness modes documentation (claude-sdk / claude-code / egg)", - "docs/architecture/integration-test-trust-boundary.md": "trust-boundary doc (in-sandbox-agent / trusted-CI-runner / human-operator — the substrate swap adds parent-claude-code-session)" - }, - "questions_for_followup_iterations": [ - "Is `isolation='worktree'` on Claude Code's Agent tool surface available on stable CC, or developer-mode-only? Spike's smoke test must verify; if not stable, ClaudeCodeSpawner falls back to caller-managed worktrees with no flag (the design's cq-5 path handles this either way).", - "Does the operator want the EggHarnessSpawner (non-CC subprocess-driven third implementation per feedback-1 Q4) in the spike's PR family, or strictly a follow-up? Current plan: follow-up; the AgentSpawner ABC just preserves shape compatibility.", - "Should `EGG_SUBSTRATE` default to 'claude-code' once the spike merges, or stay 'k3s' until k3s deprecation review (cq-9 = leave k3s indefinitely)? Current plan: stay 'k3s' to preserve back-compat; users opt-in to claude-code via the /sdlc skill which sets the env var at boot." - ] -} diff --git a/.egg-state/agent-outputs/2623-risk_analyst-output.json b/.egg-state/agent-outputs/2623-risk_analyst-output.json deleted file mode 100644 index 53f8b37dc0..0000000000 --- a/.egg-state/agent-outputs/2623-risk_analyst-output.json +++ /dev/null @@ -1,572 +0,0 @@ -{ - "issue": 2623, - "phase": "plan", - "agent": "risk_analyst", - "title": "Risk Assessment: Run the full egg SDLC stack natively in Claude Code (substrate swap)", - "summary": "Technical risk assessment for swapping egg's k3s+Redis+Docker+gateway substrate for Claude-Code-native primitives behind AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager interfaces (Option A, HITL-resolved cq-1) under a spike-then-plan rollout (cq-11). Overall risk is HIGH because the substrate-swap (a) inverts the credential trust boundary — secrets move from a server-side gateway to the user's session; (b) replaces gateway-side pre-push enforcement with PreToolUse hooks whose role/context awareness is undocumented; (c) depends on runtime primitives whose stability / availability has not yet been verified end-to-end (Agent tool isolation: worktree, PreToolUse hook role-routing, concurrent subagent ceiling) — exactly the runtime-primitive / trust-boundary failure mode flagged in #2594. Recommendation: PROCEED with the spike, but treat the spike's exit criteria as *evidence-collection* on three named substrate primitives, not as 'feature works in happy path'. Mitigations below let the parent issue ship without exposing users to credential exfil or coordination drift; several individual risks need to be resurfaced as plan-phase reviewer criteria.", - - "overall_risk_level": "HIGH", - "recommendation": "PROCEED_WITH_MITIGATIONS", - "recommendation_rationale": "The substrate-swap intent is sound and the HITL resolutions (Option A parallel substrates, spike-then-plan slicing, plugin pip deps for packaging) are conservative. But several risks (R1, R2, R5, R6, R10, R11) are structural — they need to be answered DURING the spike, not after, or the walking skeleton will encode design choices we'll regret. The spike's exit criteria must include explicit evidence on three primitives: (a) PreToolUse hook role-routing, (b) Agent-tool subagent concurrency ceiling, (c) subprocess credential isolation. Without that, the second-phase rollout plan is built on assumptions.", - - "risks": [ - { - "id": "R1", - "title": "Credential trust boundary inversion: API key moves from gateway-process to user session", - "category": "security", - "severity": "HIGH", - "likelihood": "CERTAIN", - "impact": "In the k3s substrate the sandbox NEVER sees the real Anthropic API key — the gateway intercepts requests and injects credentials server-side (gateway/anthropic_credentials.py:115-138, sandbox sees only 'sk-ant-oat01-PROXY-INJECTED-...' placeholder per sandbox/entrypoint.py:733). In the Claude Code substrate the parent Claude session HOLDS the real key, and every subagent the orchestrator spawns inherits the session's credential context. A subagent compromised via prompt injection (untrusted issue body, malicious PR content, etc.) can now read or exfiltrate the key from environment / disk / network; in the k3s model the same compromise would only have access to the placeholder.", - "description": "The gateway model is a *capability* boundary: even a fully compromised agent process cannot extract API credentials because they're injected on the gateway hop. Moving in-process to a Claude Code session collapses that boundary. The current refine-plan skill is exposed to this same risk today but at a much smaller blast radius (it's a single-skill subagent spawning, not a 4-phase pipeline running dozens of agents against issue/PR content). Substrate swap dramatically expands the surface. Note: PreToolUse hooks (cq-6 selected) operate on *the agent's own tool calls*; they cannot prevent the agent from reading env vars or process memory.", - "affected_files": [ - "gateway/gateway.py", - "gateway/anthropic_credentials.py", - "sandbox/entrypoint.py", - "shared/egg_harness/ (claude-code mode binding to be built)" - ], - "mitigation": { - "strategy": "Three layers: (1) DOCUMENT the trust-context shift explicitly in the ADR (#2594-style; cite docs/architecture/integration-test-trust-boundary.md). Make 'in-parent-Claude-Code-session' a named, audited trust context. (2) SCOPE input untrust at the orchestrator boundary: issue bodies / PR diffs that flow into agent prompts must be tagged and never quoted verbatim into a tool-use template that could exfiltrate environment. (3) Recommend running the substrate ONLY against repos the user already trusts to commit on — the threat model is not 'agent from a randomly-encountered issue'; it's the user's own SDLC. Mark this clearly in the install docs. (4) Consider a follow-up issue for an opt-in 'credentialed-proxy' mode where the Claude Code substrate routes through a local gateway-equivalent for users running against untrusted issue streams.", - "effort": "MEDIUM", - "residual_risk": "MEDIUM — the threat model shift is real and structural; the only full mitigation is keeping the gateway in the loop, which contradicts the substrate-swap goal. Documenting the shift and scoping use to trusted repos is the realistic mitigation." - }, - "requires_human_review": true, - "review_reason": "Trust-boundary inversion materially changes egg's threat model. The operator should explicitly accept the new boundary (or push back on the substrate-swap framing) before the spike commits to it. This belongs in the ADR explicitly, not as an implementation detail." - }, - { - "id": "R2", - "title": "PreToolUse hook role-routing is an unverified runtime primitive (#2594 class)", - "category": "design", - "severity": "HIGH", - "likelihood": "HIGH", - "impact": "If PreToolUse hooks in Claude Code cannot distinguish 'which subagent / role is calling Write()' from the hook's process context, the policy enforcement layer cq-6 selected (PreToolUse hooks calling shared/egg_restrictions/patterns.py) doesn't actually work for the multi-role case. Without role context, the hook can only enforce global rules — no per-role allow/deny.", - "description": "RUNTIME-PRIMITIVE RISK (#2594). The plan assumes PreToolUse hooks can call into shared/egg_restrictions/patterns.py with role and pipeline context. But: (a) The current codebase has NO PreToolUse hook scaffolding (explore agent finding); (b) Claude Code's hook spec — to the best of our knowledge — is fired per-tool-call without explicit subagent identity passed in; (c) When the parent session spawns N subagents via Agent tool, those subagents share the parent's ~/.claude/settings.json hook config but the hook runs in *some* process — whether it can read EGG_AGENT_ROLE for the calling subagent vs. parent vs. orchestrator is unverified. The refine-phase analysis acknowledges this ('Whether they port cleanly is open' for context windows; 'role-aware hooks even possible' is unverified). The spike MUST surface evidence here or the second-wave implementation is building on sand.", - "affected_files": [ - ".claude/settings.json (skill-installed hooks)", - "shared/egg_restrictions/patterns.py", - "shared/egg_harness/ (claude-code hook scaffolding)" - ], - "mitigation": { - "strategy": "Make the spike's exit criteria EXPLICIT evidence on this primitive: produce a worked example where (a) parent session spawns 2 subagents with different role envs, (b) each subagent attempts a Write to a path the OTHER role would be allowed to write, (c) hook correctly distinguishes. If the spike CANNOT produce that evidence, fall back to cq-6 option 2 (MCP-tool-side validators) — which is structural enforcement at the egg_restrictions.can_write() callsite inside the MCP server, role passed by orchestrator metadata. That fallback is known to work because egg already ships check_file_restriction as an MCP tool today. The spike must report: 'hook role-routing works' OR 'fall back to MCP validators' BEFORE the abstraction interfaces freeze.", - "effort": "MEDIUM", - "residual_risk": "LOW if fallback is accepted; HIGH if plan commits to hooks without spike-time evidence. The architect's recommended approach should be conditional: 'PreToolUse if spike confirms role-routing; else MCP-validator-side enforcement.'" - }, - "requires_human_review": true, - "review_reason": "This is a runtime primitive that the plan currently treats as available. If it isn't, the policy layer needs a different shape. Reviewer should confirm the spike's exit-criteria language includes this evidence requirement." - }, - { - "id": "R3", - "title": "Agent-tool subagent concurrency ceiling vs. egg's ThreadPoolExecutor(max_workers=6)", - "category": "performance", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "Egg's concurrent_executor spawns up to 6 agents in parallel per phase (orchestrator/concurrent_executor.py:141). Claude Code's Agent tool has an undocumented practical ceiling on simultaneous subagents. If the ceiling is <6, the largest concurrent phase (plan: architect + task_planner + risk_analyst + reviewer_plan = 4 roles, fine) becomes substrate-dependent — implement phase with N coder + tester + reviewers for a slice-DAG of 5 slices could fan out to 15+ agents.", - "description": "RUNTIME-PRIMITIVE RISK (#2594). The cq-4 resolution is 'synchronous spawn(role, prompt, env, worktree) → AgentResult', which keeps the ThreadPoolExecutor model on the orchestrator side. Internally, ClaudeCodeSpawner calls Claude Code's Agent tool — which has its own concurrency model. We don't know the ceiling. Symptoms if exceeded: tool calls queue silently, timeouts, or — worst case — subagent process collisions over the parent's session state. This is exactly the failure class #2594 describes: a primitive (Agent-tool parallelism) that 'exists but in a different form than the plan assumes'.", - "affected_files": [ - "orchestrator/concurrent_executor.py", - "shared/egg_orchestrator/ (ClaudeCodeSpawner implementation)" - ], - "mitigation": { - "strategy": "Spike must measure: how many simultaneous Agent-tool subagent invocations succeed without degradation? Document the ceiling. If <6, add a configurable max_workers override per substrate (k3s keeps 6, claude-code drops to e.g. 3 with serial fallback). If the ceiling is hard (Agent tool refuses N+1), wrap the spawner in a semaphore. Reviewer_plan must require a measured number in the spike's exit deliverables, not 'assumed adequate'.", - "effort": "LOW", - "residual_risk": "LOW once measured. The risk is purely 'unknown unknown' today." - }, - "requires_human_review": false - }, - { - "id": "R4", - "title": "In-process orchestrator concurrency: reentrancy with HITL generator and main session", - "category": "design", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "cq-7 selected the 'heredoc-style synchronous: orchestrator surface is a generator that yields HITLDecision objects; skill's outer loop renders them with AskUserQuestion and feeds answers back' option. This makes the orchestrator a generator, but: (a) the orchestrator today runs as a Flask + waitress HTTP daemon (orchestrator/cli.py:83-150) with no in-process / library API; (b) BRC consensus involves background threads (PeerConsensusTracker, message poll loops, heartbeats); (c) AskUserQuestion blocks the parent session indefinitely. Putting this together inside a Claude Code session is a non-trivial reentrancy problem.", - "description": "The orchestrator's run loop is not currently designed to yield control to a parent session and resume. Background threads continue to run (heartbeats, message store polling) while the parent Claude awaits user input. If a heartbeat times out during a 30-minute HITL wait, the agent the heartbeat tracked may be marked failed. The generator-yield model assumes the orchestrator can pause cleanly — which it currently cannot. The 'in-process orchestrator' abstraction (no current in-process / embedded / local mode per refine analysis) needs to be built from scratch around this model.", - "affected_files": [ - "orchestrator/cli.py", - "orchestrator/api.py", - "orchestrator/concurrent_executor.py", - "shared/egg_orchestrator/ (new in-process entry point)" - ], - "mitigation": { - "strategy": "(1) The spike MUST exercise an HITL round-trip end-to-end (refine-phase decision pause → AskUserQuestion → resume → continue to plan phase). Don't accept the spike as complete without this. (2) Document the heartbeat / timeout interaction during HITL waits — either pause heartbeat timers during HITL, or extend timeouts dramatically. The refine analysis already calls out 'requires the orchestrator's run loop to be reentrant from the parent's perspective' — the plan should hold that line. (3) Consider that the parent-session reentrancy model also affects mid-phase progress: a long-running implement phase that pauses for HITL needs to keep the parent session alive in a way that doesn't burn context (every HITL pause re-emits the orchestrator's running state into the parent's context).", - "effort": "HIGH", - "residual_risk": "MEDIUM — the abstraction is buildable but requires careful design. Conservative path: support HITL only at phase boundaries in the first cut; mid-phase HITL is a follow-up." - }, - "requires_human_review": true, - "review_reason": "The generator/AskUserQuestion model is a novel control-flow pattern for the orchestrator. The plan should explicitly bound the spike to demonstrate this works end-to-end before the abstraction layer freezes." - }, - { - "id": "R5", - "title": "Worktree management: shared .git/ contention with concurrent agent commits", - "category": "performance", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "cq-5 selected 'port egg's WORKTREE_BASE_DIR model: agents run inside .egg-state// subdirectories on the user's filesystem; the spawner manages worktree creation/teardown explicitly.' Concurrent worktrees of one repo share the same .git directory. Git serializes index/refs operations via .git/index.lock — under heavy parallelism (6 agents committing to 6 branches simultaneously), lock contention can manifest as 'index.lock exists' errors. In the k3s substrate the gateway serializes pushes; in the local-filesystem substrate the gateway is replaced by PreToolUse hooks that don't serialize.", - "description": "Today's gateway is also a serialization point for git pushes — it processes one push at a time per repo. The Claude Code substrate replaces it with no equivalent. Worktrees themselves don't have contention (per-branch index), but operations that touch .git/refs (push, fetch, branch rename) do. The unpushed-commit-salvage regression test (test_unpushed_commit_salvage.py) and slice-DAG concurrency tests (test_brc_concurrency.py) would surface this if the in-process substrate exercises real git operations under load.", - "affected_files": [ - ".egg-state// (worktree layout)", - "shared/egg_orchestrator/ (WorktreeManager implementation)", - "integration_tests/regression/test_unpushed_commit_salvage.py" - ], - "mitigation": { - "strategy": "(1) WorktreeManager interface includes a push() method that serializes per-repo (semaphore or RLock). (2) Spike must include a 'two agents commit to two slices concurrently' integration test as an exit gate. (3) Conformance matrix's git-operation tests run on both substrates — divergence in 'index.lock' error rates is a substrate quality regression. (4) Document the per-repo concurrency cap in the install docs (e.g., 'concurrent slice-DAG width limited by your filesystem's lock throughput').", - "effort": "LOW", - "residual_risk": "LOW — git locking is well-understood; serializing pushes is a standard mitigation." - }, - "requires_human_review": false - }, - { - "id": "R6", - "title": "Conformance matrix shape: integration_tests/regression/ is more substrate-coupled than the analysis claims", - "category": "compatibility", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "cq-3 selected 'extend integration_tests/regression/ with a substrate parameter (CI matrix)'. Exploration confirms 12/14 regression tests are substrate-agnostic at the BRC level (use in-process PeerConsensusTracker), but they still implicitly depend on the gateway being present (e.g., file-restriction enforcement at push time, credential injection). The CI matrix needs to skip more than 'live-pod-guard' — it needs to skip every test that asserts a k3s-only behavior like 'gateway intercepts X' or 'sandbox env has injected token Y'.", - "description": "The conformance refactor is larger than the issue body assumes. Tests need to be partitioned into: (a) substrate-portable invariants (BRC mechanics, contract schema, message routing, HITL persistence); (b) k3s-only invariants (gateway proxy injection, kubectl-based pod-state assertions, Redis Streams cursor semantics); (c) claude-code-only invariants (PreToolUse hook firing, Agent-tool spawn metadata). Mis-categorizing a test as portable when it actually relies on k3s primitives produces a CI matrix that's silently broken for one dimension.", - "affected_files": [ - "integration_tests/regression/", - "integration_tests/regression/test_slice_spawn_env_threading.py", - "integration_tests/regression/test_message_bus_routing.py", - "integration_tests/regression/test_hitl_round_trip.py" - ], - "mitigation": { - "strategy": "(1) The plan should produce an EXPLICIT classification of each existing regression test as portable / k3s-only / claude-code-only BEFORE the matrix is wired. (2) Reviewer_plan should require this classification as a deliverable. (3) The 'conformance suite' deliverable should include a `pytest.mark.substrate('portable')` decorator strategy or equivalent — tests get tagged at the source. (4) The spike's deliverable should include the classification for at least the 14 regression tests + a stake-in-the-ground for the broader integration suite.", - "effort": "MEDIUM", - "residual_risk": "LOW — once the classification exists, the matrix is mechanical. The risk is shipping a 'substrate parameter' that doesn't actually skip the right tests." - }, - "requires_human_review": false - }, - { - "id": "R7", - "title": "Subagent context window vs egg's max_turns=1000 budget", - "category": "performance", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "Egg targets max_turns=1000 in concurrent-execution today (docs/guides/concurrent-execution.md:97). Claude Code subagents inherit a smaller context budget from the model's hard limit. cq-10 selected the hybrid 'checkpoint for cross-turn recovery, fork for sub-task delegation' answer, but neither primitive exists yet for Claude Code subagents — egg_container checkpointing was designed for sandbox-pod model. Deep refine of large issues (#2474, #1557 are 2k-line refine outputs) may exhaust subagent context before they finish.", - "description": "RUNTIME-PRIMITIVE RISK (#2594). The HITL chose 'hybrid' — checkpoint + fork. But: (a) the egg_container checkpoint code (decision-checkpoint, role-checkpoint) was built around sandbox FS persistence and orchestrator-side re-invocation. Claude Code subagents are NOT re-invocable mid-task — once they return, you spawn a new one with full prompt re-build. So 'checkpointing' here means 'orchestrator spawns the agent again with checkpoint summary in the prompt', which is qualitatively different from the k3s model. (b) Forking a subagent for sub-tasks is exactly the Agent-tool model, but it consumes from the same parent ceiling (R3).", - "affected_files": [ - "shared/egg_container/ (checkpoint primitives)", - "shared/egg_orchestrator/ (re-invocation logic)", - "shared/prompts/refiner.md (checkpoint awareness in the system prompt)" - ], - "mitigation": { - "strategy": "(1) The spike MUST exercise one deep refine (target: 2000-line analysis on a real issue from the conformance set) to measure context exhaustion likelihood. (2) Plan must document the difference between k3s-substrate checkpointing (sandbox restart) and claude-code-substrate checkpointing (new subagent with summary prompt). (3) Conservative path: accept smaller-than-1000 turn budget for the first cut (cq-10 option 2 is the safe fallback) and gate the hybrid approach on spike findings. (4) For deep-research tasks, the parent session itself can do additional reads/searches when the subagent hits its budget — this is a 'parent supplements child' model that doesn't require checkpoint primitives.", - "effort": "MEDIUM", - "residual_risk": "MEDIUM — context budget is fundamentally smaller in subagents; some refines will fit, some won't. The mitigation is acknowledging the gap, not closing it." - }, - "requires_human_review": true, - "review_reason": "The hybrid checkpoint+fork primitive doesn't exist yet. The plan should bound the spike's success criteria to either (a) demonstrate hybrid works on a worked example or (b) fall back to the smaller-budget mode for the walking skeleton." - }, - { - "id": "R8", - "title": "Plugin packaging: pip install egg drift / system-Python contamination", - "category": "compatibility", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "cq-8 selected 'plugin metadata declares pip dependencies: user runs pip install egg.' Egg's deps (waitress, redis, kubernetes, pydantic v2, etc.) are a heavy install tree. Users may have conflicting Python versions, conflicting package versions, or missing system libraries (e.g., libgit2 for pygit2). The plugin pre-flight check has to detect all of these.", - "description": "Egg currently uses uv-managed venv at .venv/. The skill-installed flow becomes 'user runs pip install egg into their system Python (or some venv they manage)' — which is different from how plugins typically install. The marketplace plugin shape (.claude-plugin/plugin.json, 22 lines today) doesn't currently scaffold a venv. Failure modes: (a) pip install fails on Windows due to native deps; (b) version conflict with user's existing site-packages; (c) Claude Code finds wrong python; (d) Apple Silicon vs x86 wheel mismatches.", - "affected_files": [ - ".claude-plugin/plugin.json", - "pyproject.toml", - "plugins/refine-plan/skills/refine-plan/SKILL.md" - ], - "mitigation": { - "strategy": "(1) Pre-flight check at skill load time: import egg; if ImportError, emit a clear install-failure message with the exact pip command and Python version. (2) Pin egg to a minimum Python version (3.11+, matching CI). (3) Document the recommended install pattern (`python3 -m venv ~/.egg/venv && ~/.egg/venv/bin/pip install egg`) rather than just 'pip install egg'. (4) Recommend the skill check for known-incompatible Python versions. (5) The HITL feedback Q3 already noted '~100 MB plugin size soft cap' — egg's wheel tree may approach that; consider trimming deps not needed for in-process mode (e.g., the kubernetes client wheel can be optional once EGG_SUBSTRATE=claude-code is the default).", - "effort": "MEDIUM", - "residual_risk": "LOW — pip install drift is a well-understood failure mode; clear errors and docs go a long way." - }, - "requires_human_review": false - }, - { - "id": "R9", - "title": "Anthropic API cost / rate-limit blast radius shifts to user's billing", - "category": "design", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "Today the gateway can rate-limit and per-pipeline cap Anthropic API usage server-side. In Claude Code substrate, every agent invocation bills directly to the user's Anthropic account / OAuth token. A buggy infinite-loop in a single phase (e.g., a refiner that keeps spawning sub-subagents for context exhaustion) could rack up significant cost before the user notices.", - "description": "Cost-control was implicit in the gateway architecture. The Claude Code substrate moves cost control to the user's session-level limits — which exist (per-session message caps) but are not pipeline-aware. A buggy slice-DAG that re-runs a phase 100 times due to a NACK loop costs the user 100× a normal run. The HITL Q2 feedback already noted 'no explicit latency budget; re-evaluate after the spike' — the same is true for cost.", - "affected_files": [ - "shared/egg_orchestrator/ (rate-limit hooks)", - "shared/prompts/role files (turn budget signaling, per #2622)" - ], - "mitigation": { - "strategy": "(1) Pipeline-level cost cap config: EGG_PIPELINE_MAX_AGENT_INVOCATIONS env var, hard-cap on total agent dispatches per pipeline. Default conservative (e.g., 30 — enough for a 5-slice DAG with reviews, less than runaway). (2) Per-phase cost reporting: at end of each phase, parent session emits 'phase X used N agent calls' so the user can see what's happening. (3) Re-propose limits already exist in BRC (consecutive_nack_cap); ensure they apply to the claude-code substrate too (this is one of the 'BRC invariants survives unchanged' claims that needs verification).", - "effort": "LOW", - "residual_risk": "LOW — adding a cost cap is mechanically simple once the orchestrator is in-process." - }, - "requires_human_review": false - }, - { - "id": "R10", - "title": "Spike-then-plan slicing risk: the walking skeleton encodes design decisions that bind the second wave", - "category": "design", - "severity": "HIGH", - "likelihood": "MEDIUM", - "impact": "cq-11 selected 'spike then plan: a single walking-skeleton slice that gets one role (refiner) through one substrate (claude-code) end-to-end.' The risk: the spike's design choices for the four interfaces (AgentSpawner, MessageBus, PolicyEnforcer, WorktreeManager) become de-facto frozen even though only ONE role and ONE substrate exercise them. The second-wave implementation discovers that the interface shape was wrong, but the spike has already shipped — making interface revisions a breaking change.", - "description": "Spike-then-plan is a sensible slicing strategy when the unknowns are 'do the primitives work', but it's a hazard when the unknowns are 'what's the right interface shape'. The four interfaces (cq-1 named them) need to satisfy BOTH substrates AND both substrates need to satisfy the full role roster (refiner + architect + risk_analyst + task_planner + reviewer + coder + tester + reviewer_code + reviewer_contract + ...). A single-role spike does not exercise the interface diversity that the second wave needs. Risk: the spike ships, the rollout starts, three slices in we discover AgentSpawner.spawn() needs an additional parameter (e.g., turn_budget for context-aware roles), and now we're versioning the interface.", - "affected_files": [ - "shared/egg_orchestrator/ (interface modules to be created)" - ], - "mitigation": { - "strategy": "(1) Spike's exit criteria MUST include 'design review of the four interfaces against the full role roster, not just refiner' — i.e., the spike's PR includes interface signatures that have been THOUGHT-EXPERIMENTED for the other roles, even if not implemented. Reviewer_plan should require this. (2) Mark the four interfaces as 'unstable, version 0.x' in the ADR until at least 3 roles run through them end-to-end. (3) The follow-up issue from the spike should include a checkpoint: 'before adding role N+2, freeze the interfaces' — and the plan should call this out. (4) Consider making the spike a two-role one (refiner + reviewer_refine) so the interface exercises the consensus path as well, not just the producer dispatch. This stays within 'walking skeleton' but doubles the interface coverage.", - "effort": "MEDIUM", - "residual_risk": "MEDIUM — interface design is genuinely hard to validate from a single use case. The realistic mitigation is acknowledging the interface is unstable and committing to revise it in the follow-up." - }, - "requires_human_review": true, - "review_reason": "The spike scope is a design-shape decision, not just an implementation detail. Reviewer should confirm whether 1 role or 2 roles is the better spike footprint, and whether the spike's exit gate includes interface thought-experiments for the absent roles." - }, - { - "id": "R11", - "title": "EnterWorktree / ExitWorktree primitive availability vs the chosen worktree model", - "category": "design", - "severity": "LOW", - "likelihood": "LOW", - "impact": "cq-5 chose option 2 (port egg's WORKTREE_BASE_DIR model, the spawner manages worktrees explicitly) — explicitly NOT using Claude Code's native EnterWorktree/ExitWorktree. This is the conservative choice and side-steps the worktree-primitive availability question. However, the analysis references Agent tool's 'isolation: \"worktree\"' as the Claude-Code-native primitive (refine-analysis line 16). If the spike implementation accidentally relies on Agent-tool worktree isolation rather than the chosen filesystem model, we get drift.", - "description": "RUNTIME-PRIMITIVE RISK (#2594) at lower severity because the HITL chose the conservative option. The Agent tool's `isolation: \"worktree\"` parameter is documented but not exercised by egg today. The plan must not silently re-introduce it. Conversely, if the spike finds the filesystem model has subtle bugs (concurrent worktree teardown races, residual .egg-state// directories after pipeline failure), the team may be tempted to switch to native worktree isolation — at which point the interface assumes a different ownership model.", - "affected_files": [ - "shared/egg_orchestrator/ (WorktreeManager)", - ".egg-state//" - ], - "mitigation": { - "strategy": "(1) Plan documents the worktree-ownership model explicitly: 'spawner owns creation and teardown; user's filesystem is the durable location; .egg-state// is the layout.' (2) Spike includes a cleanup test (failed pipeline leaves no orphan worktrees). (3) If the implementation drifts toward Agent-tool isolation, the change requires a new HITL decision (re-open cq-5).", - "effort": "LOW", - "residual_risk": "LOW — the chosen path is the conservative one." - }, - "requires_human_review": false - }, - { - "id": "R12", - "title": "BRC consensus mechanics in single-process model: heartbeat / liveness signals lose meaning", - "category": "design", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "Today BRC heartbeats from agents track 'sandbox pod alive' as a distinct signal from 'agent making progress'. In single-process Claude Code mode, the parent session IS the orchestrator, and the subagent IS the parent's tool call. Heartbeat checks become tautological — 'is the parent session alive' is trivially true. But 'is the subagent stuck' is no longer observable through liveness; it manifests as a long-running Agent tool call with no return.", - "description": "Concrete consequences: (a) the orchestrator's 'agent failed to heartbeat in 60s' invariant fires never (heartbeats are emitted from inside the parent process), so stuck-agent detection regresses. (b) Stuck-agent escalation today triggers overseer alerts and possible re-spawn; in single-process mode there's no equivalent. (c) The pipeline can deadlock if a subagent enters an infinite tool-call loop — the user notices via 'this is taking forever', not via a structured timeout. This is implicit in the issue body's 'in-process Python (orchestrator drives all coordination)' but the loss of liveness signals is unstated.", - "affected_files": [ - "orchestrator/agent_health.py", - "shared/egg_orchestrator/ (heartbeat replacement)", - "shared/egg_harness/ (per-agent timeout in claude-code mode)" - ], - "mitigation": { - "strategy": "(1) Replace heartbeat-based stuck-agent detection with per-agent wall-clock timeouts in the claude-code substrate. Default e.g. 10 minutes per agent invocation; configurable. (2) Document this regression in the ADR. (3) Conformance suite gains a 'stuck-agent terminates within timeout' test that exercises both substrates with a deliberately slow agent. (4) Parent session can implement watchdog using async timeouts on Agent-tool calls.", - "effort": "MEDIUM", - "residual_risk": "LOW — wall-clock timeouts are a coarser signal than heartbeats but adequate for the most common stuck-agent case." - }, - "requires_human_review": false - }, - { - "id": "R13", - "title": "Marketplace plugin distribution: egg package availability on PyPI", - "category": "compatibility", - "severity": "LOW", - "likelihood": "LOW", - "impact": "cq-8 declared 'pip install egg'. If 'egg' as a package name is taken on PyPI (it is — pypi.org/project/egg is a different project), users will get the wrong package. Even with a different name (e.g., 'egg-sdlc', 'jwbron-egg'), the plugin needs the package to actually be published.", - "description": "The egg repo today is installed from source (uv pip install -e . in CI). There's no published wheel. Switching to 'plugin declares pip dep' requires (a) reserving a PyPI name, (b) publishing wheels, (c) versioning aligned with releases, (d) the plugin.json pinning a known-compatible version range.", - "affected_files": [ - "pyproject.toml", - ".claude-plugin/plugin.json", - "scripts/release/" - ], - "mitigation": { - "strategy": "(1) Reserve the PyPI name early (low cost, blocks squatters). (2) Add a release-to-PyPI step in scripts/release/ as part of the substrate-swap rollout. (3) Plugin metadata pins the egg version (>=X.Y.Z,.md file with frontmatter (tool restrictions, model, allowed bash commands). Model (b) has structural enforcement of tool restrictions per role; model (a) does not. The substrate-swap should use (b) — but that means generating N agent definition files per role at skill install time, or vendoring them.", - "affected_files": [ - ".claude/agents/ (new directory)", - "shared/prompts/ (role markdown files)", - "plugins/refine-plan/skills/refine-plan/SKILL.md" - ], - "mitigation": { - "strategy": "(1) Plan picks model (b) explicitly: per-role agent definitions in .claude/agents/ shipped by the plugin. (2) Build a generator that takes shared/prompts/.md and produces .claude/agents/.md with the right frontmatter (model, allowed-tools, etc.). (3) Spike exercises at least one custom subagent_type (refiner) to verify the model works end-to-end. (4) If model (a) is kept for simplicity, document the trade-off: tool restrictions then rely entirely on PreToolUse hooks (R2) and prompt discipline.", - "effort": "MEDIUM", - "residual_risk": "LOW — both models work; the plan just needs to commit." - }, - "requires_human_review": false - }, - { - "id": "R16", - "title": "Trust-context downgrade in cross-substrate test failures", - "category": "compatibility", - "severity": "LOW", - "likelihood": "MEDIUM", - "impact": "When a conformance test fails on the claude-code substrate but passes on k3s (or vice versa), debugging is harder — you can't just 'kubectl logs' the failing pod. The trust-boundary doc (docs/architecture/integration-test-trust-boundary.md) distinguishes execution contexts; the new context (in-parent-Claude-Code-session) needs its own diagnostic surface.", - "description": "Concrete consequences: (a) test failure messages need to identify which substrate failed; (b) reproduction commands differ between substrates; (c) flaky-test debugging requires substrate-specific knowledge. This is a developer-experience risk more than a correctness risk.", - "affected_files": [ - "integration_tests/regression/conftest.py", - "docs/architecture/integration-test-trust-boundary.md" - ], - "mitigation": { - "strategy": "(1) Update integration-test-trust-boundary.md to add the new context as a first-class entry. (2) Test output includes substrate in the failure header. (3) Documented 'reproduce this failure locally' commands cover both substrates.", - "effort": "LOW", - "residual_risk": "LOW — diagnostic improvements are mechanical." - }, - "requires_human_review": false - } - ], - - "runtime_primitive_and_trust_boundary_risks": { - "context": "Per the risk_analyst phase task description and the failure mode catalogued in #2594 / #2474: plans that depend on classes, fixtures, routes, or env vars which don't exist in the form the plan assumes — or which exist in a different execution context — are a recurring high-impact failure mode. The substrate-swap framing concentrates these risks because it explicitly replaces multiple runtime substrates simultaneously.", - "primitives_to_verify_in_spike": [ - { - "primitive": "Claude Code Agent tool with isolation: \"worktree\"", - "assumption": "Plan assumes this is a stable, available isolation primitive that the substrate spawner can drive.", - "verification_needed": "Spike must exercise it at least once to confirm semantics (does it create a fresh worktree per Agent call? Is the worktree cleaned up on return? Does it interact with the chosen WORKTREE_BASE_DIR model?). Note cq-5's resolution explicitly DECLINES to use this — the plan must remain consistent.", - "risk_if_missing": "Worktree model becomes ambiguous; spawner implementation drifts." - }, - { - "primitive": "Claude Code PreToolUse hook with role/subagent context", - "assumption": "Plan (via cq-6) assumes hooks can read EGG_AGENT_ROLE for the calling subagent and gate writes per shared/egg_restrictions/patterns.py.", - "verification_needed": "Spike must produce a worked 2-role example (parent + 2 subagents with different EGG_AGENT_ROLE) and verify the hook sees the correct role for each subagent's tool call. See R2.", - "risk_if_missing": "Policy enforcement layer doesn't actually enforce per-role rules; fall back to MCP-validator-side enforcement (cq-6 option 2)." - }, - { - "primitive": "Claude Code Agent tool concurrency ceiling", - "assumption": "Plan assumes parallel subagent spawns scale to at least 6 (matching ThreadPoolExecutor max_workers).", - "verification_needed": "Spike measures the actual ceiling. See R3.", - "risk_if_missing": "Concurrent phases queue or fail silently; worst phases (implement+slice-DAG) are unreachable." - }, - { - "primitive": "Claude Code subagent context window vs egg's max_turns=1000", - "assumption": "Plan (via cq-10) assumes hybrid checkpoint+fork closes the gap.", - "verification_needed": "Spike must run one deep refine end-to-end and report whether the subagent's context budget held. See R7.", - "risk_if_missing": "Deep refines fail mid-task; fallback is the smaller-budget model (cq-10 option 2)." - }, - { - "primitive": "Custom subagent_type via .claude/agents/.md", - "assumption": "Plan implies role-specific tool restrictions via agent definitions (issue body line 35: 'per-skill agent definitions under agents/').", - "verification_needed": "Spike must demonstrate at least one custom subagent_type with tool restrictions taking effect (vs. the current general-purpose model in the refine-plan skill).", - "risk_if_missing": "Roles fall back to prompt-injected restrictions only (R15); structural enforcement weakens." - } - ], - "trust_boundary_shifts": [ - { - "boundary": "Anthropic API credential isolation", - "today": "Gateway holds credential server-side; sandbox sees placeholder.", - "after_swap": "Parent Claude session holds credential; subagents inherit access.", - "impact": "Credential exfiltration via prompt injection becomes feasible. See R1.", - "mitigation_summary": "Document the trust shift in the ADR; scope use to repos the user already trusts; consider opt-in proxy mode for untrusted-issue streams." - }, - { - "boundary": "File-write enforcement timing", - "today": "Gateway enforces shared/egg_restrictions/patterns.py at git push.", - "after_swap": "PreToolUse hooks (or MCP validators) enforce at each Write/Edit/Bash tool call.", - "impact": "Per-write enforcement is finer-grained but more error-prone. See R2.", - "mitigation_summary": "Make enforcement layer choice contingent on spike evidence; fall back to MCP-side enforcement if hooks lack role-routing." - }, - { - "boundary": "Cost / rate-limit control", - "today": "Gateway can rate-limit per-pipeline; user-side limits are not pipeline-aware.", - "after_swap": "Cost flows directly through user's API account / OAuth; no orchestrator-level cap by default.", - "impact": "Buggy NACK loops or context exhaustion fan-out can rack up user cost. See R9.", - "mitigation_summary": "Add EGG_PIPELINE_MAX_AGENT_INVOCATIONS env var; per-phase cost reporting." - }, - { - "boundary": "Agent liveness / stuck-agent detection", - "today": "Heartbeat signals from sandbox pods feed the orchestrator's health monitor.", - "after_swap": "Heartbeats are tautological (same process); stuck-agent detection requires wall-clock timeouts.", - "impact": "Coarser stuck-agent detection. See R12.", - "mitigation_summary": "Per-agent wall-clock timeouts in the claude-code substrate; document the regression." - }, - { - "boundary": "Push serialization", - "today": "Gateway serializes git pushes per-repo.", - "after_swap": "WorktreeManager must add its own serialization or accept .git/index.lock contention.", - "impact": "Concurrent pushes from sibling agents can race on the .git directory. See R5.", - "mitigation_summary": "WorktreeManager.push() per-repo semaphore." - } - ] - }, - - "areas_requiring_human_review": [ - { - "area": "Trust-boundary inversion (R1) — credential exposure in user's Claude session", - "reason": "The substrate-swap fundamentally moves Anthropic credentials from a server-side gateway (where the sandbox could never see them) into the user's parent Claude session (where subagents inherit access). This is a deliberate architectural choice with security implications — the operator must explicitly accept the new threat model and document it in the ADR, not have it appear as an implementation detail.", - "suggested_reviewer": "Security-aware engineer or operator (the human deciding to approve the substrate swap)" - }, - { - "area": "PreToolUse hook role-routing as an unverified primitive (R2) — fallback path", - "reason": "The plan's policy enforcement layer depends on a primitive whose role-awareness is not currently established. Reviewer should confirm the spike's exit criteria explicitly include 'hook role-routing works on 2-subagent example, else fall back to MCP-validator-side enforcement.'", - "suggested_reviewer": "reviewer_plan + a future Claude Code harness implementer" - }, - { - "area": "Generator-based HITL with in-process orchestrator reentrancy (R4)", - "reason": "The generator/yield/AskUserQuestion model is novel for egg's orchestrator (which has no in-process API today). The plan should explicitly bound the spike to demonstrate an HITL round-trip end-to-end, including how background threads (heartbeats, message poll) behave during the pause.", - "suggested_reviewer": "Architect or senior orchestrator engineer" - }, - { - "area": "Subagent context budget vs deep-refine workload (R7)", - "reason": "The hybrid checkpoint+fork primitive doesn't exist for Claude Code subagents yet. Reviewer should confirm the spike either demonstrates the hybrid works on a real deep-refine workload OR explicitly accepts the smaller-budget fallback for the walking skeleton.", - "suggested_reviewer": "Architect" - }, - { - "area": "Spike scope (R10) — single role vs role pair", - "reason": "A single-role spike does not exercise the interface diversity that the second wave needs. Reviewer should weigh whether the spike should cover 1 role (refiner) or 1 producer + 1 reviewer (refiner + reviewer_refine) — the latter exercises the consensus path and gives 2 interface-shape data points instead of 1.", - "suggested_reviewer": "Architect + reviewer_plan" - } - ], - - "rollback_plan": { - "strategy": "The substrate-swap is gated by EGG_SUBSTRATE env var (per cq-1 Option A). Rollback at any layer is via env var flip back to EGG_SUBSTRATE=k3s — no code revert needed. The k3s substrate code paths remain in main. If the spike PR introduces a bug in the abstraction interfaces themselves (e.g., wrong AgentSpawner signature that the k3s code now also has to satisfy), rollback is a git revert of the spike PR.", - "steps": [ - "1. Set EGG_SUBSTRATE=k3s in the user's environment (or remove the env var; k3s is the default).", - "2. Restart any in-flight orchestrator (k3s pod restart, or skill session re-start).", - "3. If the spike PR introduced an abstraction-layer regression on the k3s substrate (highly unlikely if interface design is conservative), revert the spike PR — k3s substrate code remains in main behind the new interfaces.", - "4. For users running pip install egg, downgrade with pip install 'egg.json) and BRC history are filesystem-native and substrate-portable; checkpoints are local files." - ], - "data_loss_risk": "NONE — both substrates use filesystem-native state (contracts, BRC history, agent outputs). No DB schema migration; no state rewriting.", - "downtime_risk": "LOW — env-var-driven substrate selection means downtime is whatever an orchestrator restart costs; no migration window. The Claude Code substrate is opt-in, so users who don't set EGG_SUBSTRATE see no change." - }, - - "implementation_recommendations": [ - { - "id": "REC1", - "priority": "HIGH", - "recommendation": "The spike's exit criteria must explicitly include evidence on five runtime primitives: (a) Agent tool isolation behavior, (b) PreToolUse hook role-routing, (c) subagent concurrency ceiling, (d) subagent context budget vs deep-refine, (e) custom subagent_type from .claude/agents/. Reviewer_plan should require these as deliverables, not as 'assumed working'.", - "rationale": "Five of the high-impact risks (R2, R3, R7, R10, R15) all reduce to 'did the spike exercise this primitive end-to-end?' Holding the spike to that bar prevents the second-wave from being built on assumptions." - }, - { - "id": "REC2", - "priority": "HIGH", - "recommendation": "ADR explicitly documents the trust-boundary shift (R1, R12, R5 push-serialization, R9 cost-control). The ADR is more than 'how the substrate works' — it includes 'what's NOT the same as before' and the operator's explicit acceptance of the threat model.", - "rationale": "The trust-boundary shift is the single highest-severity risk and is structural. Documenting it in code is necessary but not sufficient — the ADR is the artifact a future reviewer or auditor will reach for." - }, - { - "id": "REC3", - "priority": "HIGH", - "recommendation": "Plan deliverable includes an explicit conformance-test classification (portable / k3s-only / claude-code-only) for at least the 14 existing integration_tests/regression/ tests. Tests get pytest markers; the CI matrix skips the right dimension automatically.", - "rationale": "Without this, R6 manifests as a CI matrix that's silently broken for one substrate dimension." - }, - { - "id": "REC4", - "priority": "MEDIUM", - "recommendation": "Consider expanding the spike scope from 1 role to 2 (producer + reviewer). This exercises the consensus path (BRC ACK/NACK) end-to-end and gives 2 interface-shape data points. The cost is one extra agent invocation in the spike; the benefit is dramatically reducing R10's likelihood.", - "rationale": "A single-role spike doesn't exercise the orchestrator-as-bus model that the issue body specifically calls out as the BRC-mechanics workaround for SendMessage gating." - }, - { - "id": "MEDIUM", - "priority": "MEDIUM", - "id_override_note": "REC5", - "recommendation": "Add EGG_PIPELINE_MAX_AGENT_INVOCATIONS env var with a conservative default (e.g., 30) to cap runaway-cost scenarios. Per-phase agent-invocation reporting in the parent session so the user can see cost as it accrues.", - "rationale": "User-side cost control is a regression from the gateway model. A simple cap with a conservative default prevents the worst-case (R9) at near-zero implementation cost." - }, - { - "id": "REC6", - "priority": "MEDIUM", - "recommendation": "Reserve a PyPI package name early in the rollout (independent of the spike outcome) — cheapest available step toward fixing R13.", - "rationale": "PyPI namespace reservation costs nothing and removes a blocker from the eventual install-docs work." - }, - { - "id": "REC7", - "priority": "LOW", - "recommendation": "Mark all four abstraction interfaces as 'unstable, version 0.x' in the ADR until at least 3 roles have run through them end-to-end. Document explicit interface-stability criteria for the follow-up issue.", - "rationale": "Interface design is genuinely hard to validate from a single use case (R10). Signaling instability prevents downstream code from depending on shapes that will move." - } - ], - - "performance_assessment": { - "agent_dispatch_latency": "Unknown — Claude Code Agent tool dispatch latency is not yet measured for the in-parent-session case. Today's k3s spawn is dominated by pod-start time (~10-15s); Claude Code subagent spawn is sub-second but the first-message cost includes loading the agent definition + role prompt. The HITL feedback (Q2) explicitly noted no budget is set; re-evaluate after the spike.", - "throughput": "Reduced relative to k3s for parallel phases: ThreadPoolExecutor(max_workers=6) was sized for k3s pod parallelism. Subagent ceiling (R3) may force lower effective parallelism. Phases with <=4 roles (refine, plan) are unaffected; implement-phase slice-DAGs with 8+ agents are most exposed.", - "memory": "Single-process model means orchestrator memory + N subagent overhead in the parent session. For large slice-DAGs this can exceed several GB and may push the parent session into context-window pressure. Conservative concurrency caps mitigate.", - "disk": "Worktree per agent in .egg-state// — for 6 concurrent agents on a 100MB repo, ~600MB peak. Manageable on developer machines; flag in install docs.", - "note": "Performance is opportunity-cost more than absolute regression: the alternative (running k3s on the user's laptop) was the actual blocker the substrate-swap fixes. So 'slower than k3s in a datacenter' may still be 'faster than nothing' from the solo-developer perspective." - }, - - "security_assessment": { - "threat_model": "Three primary threats: (1) credential exfiltration via prompt injection — a malicious issue/PR/web-content quoted into an agent's prompt extracts the user's Anthropic key from environment or process state; (2) file-write boundary bypass via PreToolUse hook missing role context — a coder-role agent writes to docs/ which only doc-updater should write to; (3) git push of untrusted commits — gateway used to validate every push; now relies on PreToolUse hooks or MCP validators.", - "current_controls": [ - "Gateway-side credential injection (sandbox never sees real key)", - "Gateway file-restriction enforcement at push time (shared/egg_restrictions/patterns.py)", - "Gateway session tokens scoped per-agent-spawn", - "Read-only mounts of sensitive paths in sandbox container" - ], - "proposed_controls": [ - "Trust-context documentation (ADR) — operator explicitly accepts the new boundary", - "PreToolUse hooks OR MCP-validator-side enforcement at write time (cq-6 layered fallback per R2)", - "Scope-of-use guidance: substrate is for repos the user already trusts", - "Pipeline-level cost cap (EGG_PIPELINE_MAX_AGENT_INVOCATIONS)", - "Wall-clock per-agent timeouts as stuck-agent detection (R12)" - ], - "residual_risk": "MEDIUM — the credential boundary shift is structural and not closeable without abandoning the substrate-swap goal. The file-write enforcement depends on a primitive (PreToolUse hook role-routing) whose availability is unverified. Mitigation is doc + scope, not technical countermeasure. Operator review (R1) required.", - "explicit_non_goals": [ - "This issue does NOT remove the gateway or k3s substrate; both substrates remain (cq-1, cq-9 default).", - "This issue does NOT solve the SendMessage / Agent Teams gating (R14, #2612 follow-up).", - "This issue does NOT introduce a new trust context for malicious-issue defense beyond documentation." - ] - }, - - "compatibility_assessment": { - "breaking_changes": "None on the k3s substrate (k3s code paths preserved behind new interfaces). The Claude Code substrate is new and opt-in via EGG_SUBSTRATE=claude-code.", - "backward_compatibility": "Full for k3s users (no change unless they opt in). New for Claude Code users (no prior state to be compatible with).", - "forward_compatibility": "Good — abstraction interfaces are the substrate-extension point. Future substrates (e.g., GitHub Actions runner, hosted multi-tenant) would add a third implementation behind the same interfaces.", - "affected_systems": [ - "orchestrator/ (gains interface modules, new ClaudeCodeSpawner / InProcessMessageBus / PolicyEnforcer / WorktreeManager)", - "shared/egg_orchestrator/ (new in-process entry point and interface definitions)", - "shared/egg_harness/ (new claude-code mode binding to Claude Code Agent tool)", - "shared/egg_restrictions/patterns.py (becomes the source of truth for both gateway and PreToolUse hooks)", - ".claude/settings.json (PreToolUse hooks shipped by the plugin)", - ".claude/agents/ (custom subagent_type definitions per role)", - "integration_tests/regression/ (substrate-parameter CI matrix)", - "plugins/refine-plan/ (skill becomes a thin entry point to the in-process orchestrator)", - "docs/architecture/ (new ADR; integration-test-trust-boundary.md updated for new context)" - ], - "deprecation_paths": "k3s substrate stays indefinitely (cq-9). Refine-plan skill's current Markdown-only implementation is superseded but no rush — it can remain as a fallback or be deprecated when the in-process substrate ships green." - }, - - "open_questions_for_plan_phase": [ - { - "id": "Q1", - "question": "Should the spike scope be 1 role (refiner only) or 2 (refiner + reviewer_refine) to exercise the BRC consensus path? (See R10.)", - "default_recommendation": "Expand to 2 roles for interface validation. The cost is small; the coverage gain is significant." - }, - { - "id": "Q2", - "question": "Should the PreToolUse hook approach (cq-6 option 1) be conditional on spike evidence of role-routing, or accepted as the chosen path regardless? (See R2.)", - "default_recommendation": "Make it conditional. If the spike cannot demonstrate role-routing works for multi-subagent scenarios, the fallback is cq-6 option 2 (MCP-validator-side enforcement). The plan should explicitly document the fallback path." - }, - { - "id": "Q3", - "question": "What's the default value of EGG_PIPELINE_MAX_AGENT_INVOCATIONS (R9 cost cap)?", - "default_recommendation": "30 — covers a typical 5-slice DAG with refine + plan + implement-with-reviews. Easy to override for large slice-DAGs." - }, - { - "id": "Q4", - "question": "Should the parent session implement explicit watchdog timeouts on Agent-tool calls (R12 stuck-agent detection)?", - "default_recommendation": "Yes — 10 minutes default per agent invocation. The watchdog is a strictly-coarser version of the heartbeat regime today; lose-it-without-replacement is a real regression." - } - ], - - "alignment_with_issue_done_criteria": { - "criterion_1_conformance_passes_on_5_issues": "Risk: the 5 issues need to be a fixed curated set per HITL feedback Q1, and conformance test classification (R6) is a prerequisite. Confirm the spike's deliverables include at least the classification scaffolding even if the matrix doesn't run on all 5 yet.", - "criterion_2_ADR_with_abstraction_layers_merged": "Risk: ADR scope per REC2 must include the trust-boundary shift, not just 'how the substrate works'. The abstraction layers (4 interfaces) must be marked unstable (REC7) until validated by ≥3 roles.", - "criterion_3_children_closed_or_punted": "Risk: #2474 (conformance) and #2622 (depth gap) are structurally absorbed, but #2612 (SendMessage gating) stays open — this is fine, just confirm the plan doesn't accidentally try to fix #2612 in scope (R14)." - } -} diff --git a/.egg-state/agent-outputs/2717-architect-output.json b/.egg-state/agent-outputs/2717-architect-output.json deleted file mode 100644 index b7c3a70f49..0000000000 --- a/.egg-state/agent-outputs/2717-architect-output.json +++ /dev/null @@ -1,389 +0,0 @@ -{ - "issue": 2717, - "phase": "plan", - "role": "architect", - "pipeline_id": "issue-2717", - "summary": "Architecture analysis for the substrate-swap follow-up rollout (#2717). The refine phase settled the slice decomposition (cq-2 option 3: bridge-first then phase-sequential rollout with parallel hardening at the end), the HITL bridge approach (cq-1 option C: hybrid — flatten for refine/plan, daemon for implement), the harness re-host vs Agent-tool dispatch question (cq-3 option 4: decide empirically at implement-phase), the R15 model decision (cq-4 option 4: contingent on R2's empirical result), the R2 validation timing (cq-5 option 1: early-spike before phase rollout begins), the cost-cap default (cq-6 option 2: EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200), and the k3s adapter posture (cq-7 option 4: defer + drop v0.x unstable marker after ≥3 roles exercise the claude-code leg). This document maps those decisions onto concrete component changes, with file:line evidence for every runtime primitive cited.", - "scope_boundary": { - "in_scope": [ - "Closing the HITL bridge gap (cq-7 / reviewer-v1 blocker #6) end-to-end so the egg-sdlc skill can drive `run_pipeline_in_process(...)` across multiple AskUserQuestion round-trips", - "Wiring all 15 remaining role rubrics under plugins/egg-sdlc/skills/egg-sdlc/agents/ (architect, task_planner, risk_analyst, coder, tester, documenter, applier, reviewer_refine, reviewer_agent_design, reviewer_plan, reviewer_code, reviewer_code_holistic, reviewer_contract, reviewer_security, reviewer_concurrency)", - "Extending run_pipeline_in_process (orchestrator/substrate/in_process.py:84-149) with plan, implement, and pr phase branches; removing the cq-11 NotImplementedError scope-fence at orchestrator/substrate/in_process.py:820-826", - "Substrate-parametrized conformance matrix covering the 5 curated issues (#2714, #2474, #2261, #2705, #2718) under both EGG_SUBSTRATE values", - "EggHarnessSpawner with `egg-orch local-run --issue N` CLI subcommand (DoD #5; feedback Q3 = Option A full local-run)", - "Cost cap (REC5) at EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 default", - "R2 hook role-resolution empirical spike as the first deliverable in slice-1 (cq-5 option 1)", - "Drop the v0.x unstable marker on claude-code-side substrate modules once ≥3 roles run end-to-end (cq-7 option 4)", - "ADR refresh (docs/architecture/claude-code-substrate.md) to capture rollout deltas vs the spike's design" - ], - "out_of_scope": [ - "K3s-side adapter parity for MessageBus, PolicyEnforcer, WorktreeManager protocols (cq-7 option 4: defer entirely; RedisMessageStore / gateway / gateway-worktree-manager keep their direct call paths on k3s)", - "Marketplace publishing of the egg-sdlc plugin (cq-12 carryover; not a parent-close blocker)", - "Mid-cycle peer-to-peer messaging between subagents (issue body 'Out of scope'; still platform-gated on SendMessage / Agent Teams)", - "#2714 sync-regression-on-dirty-worktree (issue body 'Out of scope'; #2719 already merged a related rebase-autostash fix on the gateway leg)", - "#2622 structural causes #5 / #6 (tool-use budget signaling, minimum-breadth targets in role files) — stays in #2622's scope per feedback Q5 from #2623", - "Reviewer roster expansion (feedback Q5 in refine: existing roster covers all substrate dimensions; no new reviewer roles in this rollout)", - "Per-phase explicit budget tuning (feedback Q2: deferred until empirical data exists; the rollout produces that data, retune in a follow-up issue)", - "Migration of ClaudeCodeSpawner to a ClaudeCodeAgentToolSpawner that emits real Agent-tool envelopes (cq-3 option 4: stay on harness re-host for this rollout, decide empirically at implement-phase post-shipping)", - "Full R15 migration to per-role .claude/agents/.md frontmatter-restriction files (cq-4 option 4: contingent on R2's empirical result; only triggered if R2 fails)" - ] - }, - "current_state_map": { - "substrate_protocols": { - "spawner": "orchestrator/substrate/spawner.py:73 AgentSpawner Protocol — `spawn(role, prompt, env, worktree) -> AgentResult`. Today's two implementations: ClaudeCodeSpawner (orchestrator/substrate/claude_code/spawner.py:64) and K3sSpawnerAdapter (orchestrator/substrate/k3s_adapter.py:38).", - "bus": "orchestrator/substrate/message_bus.py:27 MessageBus Protocol. Today only one substrate satisfies it via the inheriting class: InProcessMessageBus (orchestrator/substrate/claude_code/message_bus.py:30) — k3s uses RedisMessageStore directly without going through the Protocol (cq-7 option 4 leaves this as-is for the rollout).", - "policy": "orchestrator/substrate/policy.py:25 PolicyEnforcer Protocol. Today's only implementation: PreToolUseHookPolicy (orchestrator/substrate/claude_code/policy.py:26). hook_entry.py at orchestrator/substrate/claude_code/hook_entry.py is the subprocess Claude Code invokes per tool-call.", - "worktrees": "orchestrator/substrate/worktree.py:24 WorktreeManager Protocol. Today's only implementation: LocalWorktreeManager (orchestrator/substrate/claude_code/worktree.py:59). K3s side still calls gateway/worktree_manager.py directly." - }, - "substrate_selector": "orchestrator/substrate/__init__.py:96 select_substrate(env) — reads EGG_SUBSTRATE, returns SubstrateBundle dataclass (orchestrator/substrate/__init__.py:77). v0.x markers at __init__.py:11, in_process.py:22, claude_code/spawner.py:46, spawner.py:15, message_bus.py:18, policy.py:17, worktree.py:13 — all are slated for removal in the hardening slice per cq-7 option 4.", - "rubric_loader": "orchestrator/substrate/__init__.py:232 _load_egg_sdlc_role_rubric(role) — reads plugins/egg-sdlc/skills/egg-sdlc/agents/.md, raises ValueError for any role except refiner today. Roster expansion is a pure additive-file change; the loader is already roster-agnostic.", - "spawn_seam_for_concurrent_executor": "orchestrator/concurrent_executor.py:504 _spawn_agent(role, prompt_text) — the dispatch site; at line 539 it reads EGG_SUBSTRATE and routes to _spawn_agent_via_substrate(role, prompt_text) at line 569 when claude-code is selected. K3s defaults retain the legacy KubernetesSpawner factory path.", - "in_process_generator": "orchestrator/substrate/in_process.py:84 run_pipeline_in_process(...) — yields HITLDecision objects. Yield sites: line 191 (preflight gate) and line 207 (refine artifact approval gate). The k3s leg fences at line 130-131; the plan/implement/pr scope-fence is the `_maybe_fence` method at line 820-826 which raises NotImplementedError on any 'approve_continue' answer.", - "phase_roles": "shared/egg_contracts/agent_roles.py:1172 _PHASE_ROLES + :1183 _PHASE_REVIEWERS. Confirmed roster: refine={REFINER}+{REVIEWER_REFINE,REVIEWER_AGENT_DESIGN}; plan={ARCHITECT,TASK_PLANNER,RISK_ANALYST}+{REVIEWER_PLAN}; apply={APPLIER}+{REVIEWER_CONTRACT}; implement={CODER,TESTER,DOCUMENTER}+{REVIEWER_CODE,REVIEWER_CODE_HOLISTIC,REVIEWER_CONTRACT,REVIEWER_SECURITY,REVIEWER_CONCURRENCY}. 15 new role rubrics needed plus the existing refiner.md.", - "monolithic_pipelines_route": "orchestrator/routes/pipelines.py — 23,354 lines today; #2261 slice-15 will decompose it into orchestrator/routes/pipelines/_run_loop/ with per-phase handlers (_run_refine.py, _run_plan.py, _run_implement.py, _run_pr.py). The rollout's in-process phase orchestrators in run_pipeline_in_process MUST NOT duplicate the BRC state-machine that pipelines.py already owns — they reuse ConcurrentPhaseExecutor (orchestrator/concurrent_executor.py:114) and PeerConsensusTracker (orchestrator/peer_consensus.py:69) the same way refine does today.", - "build_system_prompt_depth": "shared/egg_harness/prompt.py:24 build_system_prompt(sources) — joins fragments with `\\n\\n---\\n\\n`. The ClaudeCodeSpawner-side hook is orchestrator/substrate/claude_code/spawner.py:175 _build_system_prompt(role). Adding a new role rubric is structurally a no-op for prompt-build; the bottleneck is curating each rubric to match what the k3s side already injects via shared/prompts/ + agent_roles.py default prompts.", - "policy_single_source_of_truth": "shared/egg_restrictions/patterns.py:768 build_agent_patterns(repo, role) — the single source of truth shared by gateway/phase_filter.py:1061 (the trusted-CI-runner enforcement leg) and orchestrator/substrate/claude_code/hook_entry.py:99 (the in-sandbox-agent enforcement leg).", - "substrate_conformance_fixture": "integration_tests/regression/conftest.py:592-642 — `substrate` fixture parametrized over [k3s, claude-code]; skips claude-code dimension when EGG_AGENT_ROLE is set (in-sandbox-agent context) per the same conftest. Smoke test at integration_tests/regression/test_substrate_smoke.py — covers select_substrate shape, spawner round-trip type, in-process bus round-trip, and a single INV-3 stale-version test. Parametrize set for the wider conformance matrix is currently empty / per-test inline." - }, - "component_architecture": { - "slice_1_bridge_plus_r2_spike": { - "intent": "Close the HITL bridge gap (cq-1 option C: hybrid → ship Option B 'flatten' as MVP for refine and plan) AND ship the R2 hook-resolution empirical spike (cq-5 option 1: 2-subagent worked example as first slice). Bridge work is necessary for any phase rollout to be operator-driven; R2 validation gates the model-(a) vs model-(b) decision per cq-4. The two land together because both are slice-0 work and the R2 spike validates that the bridge surface (which dispatches a subagent via the harness) does not lose role identity.", - "deliverables": [ - { - "name": "Bridge driver — `bin/egg-sdlc-stage` script invoked from skill steps", - "purpose": "deployed-pod skill helper", - "execution_context": "in-sandbox-agent (parent Claude session, called from `Bash(python3 ...)` skill steps)", - "evidence": "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:97-102 documents the gap — Claude Code skills cannot drive a long-lived Python generator across AskUserQuestion round-trips because each `python3` invocation is a fresh process. The fix flattens run_pipeline_in_process's generator into a sequence of single-yield stage scripts that serialize state through .egg-state/contracts/.json (the same path the HTTP daemon uses per orchestrator/substrate/in_process.py:49-55).", - "shape": "Each invocation reads the prior HITL answer from a CLI flag (`--hitl-answer 'opt-1'` or `--preflight-answer '{...}'`), advances the in-process generator to the next yield point, serializes the HITLDecision to stdout as JSON for the skill to render via AskUserQuestion, persists in-flight contract state to .egg-state/contracts/.json, and exits. The skill's outer loop reads the JSON, invokes AskUserQuestion, captures the answer, and re-invokes the same stage script with the answer. Refine and plan phases are single-pass-per-phase so the stage script approach is a natural fit." - }, - { - "name": "Skill driver migration — SKILL.md updated to drive the stage script + multi-yield AskUserQuestion loop", - "purpose": "deployed-pod skill body", - "execution_context": "in-sandbox-agent (the parent Claude session reads SKILL.md and runs its Bash steps)", - "evidence": "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md currently documents an aspirational 7-step flow (SKILL.md:60-68); slice-1 reifies it. The driver replaces the `_K3S_FENCE_MESSAGE` early-exit logic in orchestrator/substrate/in_process.py:130-131 with a more graceful fallback so an operator without an installed plugin gets a useful diagnostic." - }, - { - "name": "R2 hook-resolution spike — 2-subagent worked example", - "purpose": "deployed-pod test fixture", - "execution_context": "trusted-CI-runner (pytest from outside the cluster) exercising in-sandbox-agent code paths", - "evidence": "orchestrator/substrate/claude_code/hook_entry.py reads EGG_AGENT_ROLE from os.environ (set at orchestrator/substrate/claude_code/spawner.py:124-128 by ClaudeCodeSpawner). The spike adds a test under shared/tests/test_hook_role_resolution.py that spawns one role (parent) and has it dispatch a second role (child) via Claude Code's Agent tool (subagent_type='general-purpose'), then asserts hook_entry.decide() sees the child's EGG_AGENT_ROLE for the child's Bash/Write tool calls and the parent's for the parent's. The shape is a true 2-subagent dispatch — if hooks fail this check the rollout switches to MCP-validator-side enforcement per cq-6 fallback BEFORE phase rollout begins.", - "fallback_branch": "If the spike shows hooks misattribute the nested call to the parent, slice-1 ALSO ships an MCP-validator-side enforcement path that calls build_agent_patterns(role) from inside whichever MCP tool the agent is calling (Write, Edit). This is heavier review surface but doesn't block downstream slices." - }, - { - "name": "Acceptance tests for bridge + R2", - "purpose": "test-only", - "execution_context": "trusted-CI-runner", - "evidence": "integration_tests/regression/test_substrate_bridge.py NEW. Drives a full refine-phase pipeline via the stage-script bridge and asserts the HITL answer flow round-trips correctly. Builds on the substrate fixture at integration_tests/regression/conftest.py:592-642." - } - ] - }, - "slice_2_plan_phase_substrate": { - "intent": "Wire the plan phase end-to-end on the claude-code substrate. Plan is the right first multi-role stress test for BRC mechanics: 3 producers (architect, task_planner, risk_analyst) + 1 reviewer (reviewer_plan) is enough concurrency to surface INV-3 / INV-5 / INV-6 regressions without the 8-subagent blast radius of implement.", - "deliverables": [ - { - "name": "Four new role rubrics — architect.md, task_planner.md, risk_analyst.md, reviewer_plan.md", - "purpose": "deployed-pod rubric files (concatenated into system prompt)", - "execution_context": "in-sandbox-agent (loaded by ClaudeCodeSpawner at orchestrator/substrate/claude_code/spawner.py:175)", - "evidence": "plugins/egg-sdlc/skills/egg-sdlc/agents/ today holds only refiner.md. _load_egg_sdlc_role_rubric at orchestrator/substrate/__init__.py:232-285 is already roster-agnostic; adding files extends the roster without code change. Each rubric mirrors the k3s side's per-role prompts so the role behaves identically across substrates — source is shared/prompts/ for k3s, the new agents/.md for claude-code; the rubrics are deliberately substrate-portable text." - }, - { - "name": "Plan-phase branch in run_pipeline_in_process", - "purpose": "deployed-pod orchestrator entry", - "execution_context": "in-sandbox-agent (run from skill driver) and trusted-CI-runner (run from local-run CLI)", - "evidence": "orchestrator/substrate/in_process.py:820-826 _maybe_fence currently raises NotImplementedError on 'approve_continue_to_plan'. The slice replaces the fence with a real plan-phase invocation that calls ConcurrentPhaseExecutor (orchestrator/concurrent_executor.py:114) to fan out the 3 producers concurrently, waits for PeerConsensusTracker (orchestrator/peer_consensus.py:69) to ratify CONSENSUS_CONFIRMED, then yields the plan HITL gate decision (analogous to the refine gate at line 207). This reuses the existing BRC state machine — no parallel implementation.", - "shape": "Add `_spawn_plan_producers(...)` (mirrors `_spawn_refiner` near line 203), `_build_plan_gate_decision(...)` (mirrors `_build_refine_gate_decision`), and extend the main loop to yield plan-gate after refine-gate when the operator selects 'approve_continue_to_plan'." - }, - { - "name": "Plan-phase BRC concurrency tests", - "purpose": "test-only", - "execution_context": "trusted-CI-runner", - "evidence": "integration_tests/regression/test_substrate_smoke.py:1-80 already covers a single INV-3 case via PeerConsensusTracker. Extend to: (a) parallel producer dispatch through ClaudeCodeSpawner (the synchronous spawn shape from cq-4 + ThreadPoolExecutor at the executor layer), (b) reviewer ACK aggregation in InProcessMessageBus, (c) open-NACK barrier at producer re-propose time (INV-5)." - } - ] - }, - "slice_3_implement_phase_substrate": { - "intent": "Wire the implement phase end-to-end. 8-role dispatch (3 producers + 5 reviewers) is the maximum concurrency surface in the SDLC. Also the empirical-data-collection point for cq-3 (harness re-host vs ClaudeCodeAgentToolSpawner) and the cq-1 (b)→(a) transition consideration for the bridge.", - "deliverables": [ - { - "name": "Eight new role rubrics — coder.md, tester.md, documenter.md, reviewer_code.md, reviewer_code_holistic.md, reviewer_contract.md, reviewer_security.md, reviewer_concurrency.md", - "purpose": "deployed-pod rubric files", - "execution_context": "in-sandbox-agent", - "evidence": "Same loader path as slice-2; this slice fills the rest of the producer-and-reviewer roster for the implement phase. Each rubric must encode the role's contract task assignments per shared/egg_contracts/agent_roles.py:954 AGENT_ROLES and the role's file-write boundary per shared/egg_restrictions/patterns.py:768 build_agent_patterns." - }, - { - "name": "Implement-phase branch in run_pipeline_in_process", - "purpose": "deployed-pod orchestrator entry", - "execution_context": "in-sandbox-agent and trusted-CI-runner", - "evidence": "Same shape as plan-phase branch in slice-2, with the 8-role dispatch. Critical concurrency invariants to preserve (validated via orchestrator/action_guards.py:631 validate_invariants): INV-3 stale-version rejection on producer re-propose (#2142), INV-5 open-NACK barrier (#2142), INV-6 commit-bound ACKs (gateway side; claude-code substrate gets commit_sha from ClaudeCodeSpawner at orchestrator/substrate/claude_code/spawner.py:158 _capture_head_sha).", - "shape": "Add `_spawn_implement_producers(...)`, `_build_implement_gate_decision(...)`. The implement phase is the natural cq-1 transition point from Option B (stage scripts) to Option A (daemon) for the bridge — multi-NACK BRC cycles across 8 subagents add up faster than the per-stage script overhead. The slice ships a `--daemon` flag on `egg-sdlc-stage` that switches the bridge from stage-per-call to a long-lived JSON-RPC daemon backing run_pipeline_in_process. Skill driver chooses daemon for implement, stage-script for refine/plan." - }, - { - "name": "Implement-phase concurrency stress test + cq-3 data collection", - "purpose": "test-only + telemetry artifact", - "execution_context": "trusted-CI-runner", - "evidence": "integration_tests/regression/test_substrate_implement.py NEW. Drives a full implement-phase pipeline on the claude-code substrate against one of the curated 5 issues. Collects timing (wall-clock per producer / per reviewer), invocation count, NACK cycle count, and context budget hit-rate (R7 risk). Writes to .egg-state/conformance-metrics/-.json. This is the data set for cq-3 option 4 ('decide empirically at implement-phase').", - "scope_boundary": "Stay on harness re-host per cq-3 default. If the data shows ClaudeCodeAgentToolSpawner migration is mandatory (e.g., context-budget regression > 50% on architect/refiner roles, R7 trip), file a follow-up issue rather than expanding this slice." - } - ] - }, - "slice_4_pr_phase_plus_conformance_plus_scope_fence_removal": { - "intent": "Wire the pr phase (mostly automation), land the 5-issue × 2-substrate conformance matrix, and remove the cq-11 scope-fence entirely (feedback Q6: operator can chain phases freely).", - "deliverables": [ - { - "name": "PR-phase branch in run_pipeline_in_process", - "purpose": "deployed-pod orchestrator entry", - "execution_context": "in-sandbox-agent (parent session) and trusted-CI-runner (local-run)", - "evidence": "_PHASE_ROLES has no producers for pr per shared/egg_contracts/agent_roles.py:1172 (it's automation in orchestrator/routes/pipelines.py for the HTTP daemon). The substrate version just needs to ferry the implement artifacts into the PR-creation routine and yield a final HITL gate for the operator to confirm the PR body. Reuses the post-#2685 context-PR + PR-phase plumbing in orchestrator/routes/pipelines.py (referenced by the audit-log entry #2694).", - "shape": "Add `_build_pr_gate_decision(...)` that yields the final approve-PR HITLDecision; on approval, call the existing PR-creation routine the HTTP daemon uses." - }, - { - "name": "Three new reviewer rubrics — reviewer_refine.md, reviewer_agent_design.md, applier.md", - "purpose": "deployed-pod rubric files", - "execution_context": "in-sandbox-agent", - "evidence": "These three round out the 15-rubric roster. reviewer_refine and reviewer_agent_design are exercised when refine phase runs on the claude-code substrate (today only the producer side, refiner.md, is wired). applier covers the epic-only apply phase per shared/egg_contracts/agent_roles.py:1180." - }, - { - "name": "Substrate-parametrized conformance matrix", - "purpose": "test-only", - "execution_context": "trusted-CI-runner", - "evidence": "integration_tests/regression/conftest.py:592-642 already has the substrate fixture. The slice adds a `@pytest.mark.parametrize` set covering the 5 curated issues (feedback Q1 confirmed: #2714, #2474, #2261, #2705, #2718). New file integration_tests/regression/test_conformance_matrix.py drives each issue under each substrate via the conftest fixture. Per feedback Q2: pytest.mark.slow as soft gate; 3-hour-per-phase ceiling as fail-the-CI-job safety fallback. No per-phase cost / latency budget yet (retune in a follow-up once empirical data exists from slice-3).", - "skip_semantics": "claude-code dimension stays gated on EGG_AGENT_ROLE-unset (in-sandbox-agent contexts skip per the existing conftest skip heuristic). CI matrix dimension `EGG_SUBSTRATE=claude-code` only runs on the trusted-CI-runner leg." - }, - { - "name": "Scope-fence removal", - "purpose": "deployed-pod orchestrator entry", - "execution_context": "in-sandbox-agent and trusted-CI-runner", - "evidence": "orchestrator/substrate/in_process.py:820-826 `_maybe_fence` is removed entirely (feedback Q6 option 1). Confirmation comes from the test in shared/tests/test_run_pipeline_in_process.py that asserts the fence fires — flip to assert it does NOT fire and chain proceeds. No replacement --single-phase flag (feedback Q6: redundant with the existing start_phase + hitl_gates: false config flags)." - } - ] - }, - "slice_5_hardening_pass": { - "intent": "Parallel-shippable hardening work: EggHarnessSpawner with full local-run, cost cap, R15 migration (if R2 forced it in slice-1), drop v0.x markers, and ADR refresh. All siblings within slice-5 can dispatch concurrently because they touch disjoint surfaces.", - "deliverables": [ - { - "name": "EggHarnessSpawner — third AgentSpawner implementation + `egg-orch local-run --issue N` CLI", - "purpose": "deployed-pod production code", - "execution_context": "trusted-CI-runner (operator's local dev machine; runs outside any Claude Code session)", - "evidence": "Today's spawners: orchestrator/substrate/claude_code/spawner.py:64 ClaudeCodeSpawner and orchestrator/substrate/k3s_adapter.py:38 K3sSpawnerAdapter. The new orchestrator/substrate/egg_harness/spawner.py:EggHarnessSpawner drives egg_harness.client.run_agent directly without a parent Claude Code session — equivalent to today's k3s-inside-the-pod model but in a local subprocess. CLI entry at orchestrator/cli.py adds `local-run --issue N` (next to the existing `serve`, `status`, etc. subcommands). DoD #5 + feedback Q3 option A: full local-run end-to-end including HITL via stdin/stdout JSON prompts (operator answers HITL by typing JSON at a stdin prompt).", - "shape": "Third concrete implementation of the AgentSpawner Protocol (orchestrator/substrate/spawner.py:73). Selectable via EGG_SUBSTRATE=egg-harness in select_substrate at orchestrator/substrate/__init__.py:96-202. CLI handler at orchestrator/cli.py invokes run_pipeline_in_process with the egg-harness bundle and renders HITLDecision yields to stdin prompts." - }, - { - "name": "Cost cap — EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200", - "purpose": "deployed-pod production safeguard", - "execution_context": "in-sandbox-agent (where the parent's API credentials are charged) and trusted-CI-runner (where CI minutes are charged)", - "evidence": "Env var EGG_PIPELINE_MAX_AGENT_INVOCATIONS does not exist anywhere in the codebase today (verified by grep). Add a per-pipeline counter in orchestrator/concurrent_executor.py:_spawn_agent at line 504 (the canonical dispatch point). When the counter exceeds the cap, raise a PipelineCostCapExceeded exception that propagates through ConcurrentPhaseExecutor and halts the pipeline cleanly. Default 200 per cq-6 option 2 — enough headroom for a 3-slice slice-DAG with concurrent producers/reviewers plus a couple of NACK cycles." - }, - { - "name": "R15 model-(b) migration — only if slice-1's R2 spike forced it", - "purpose": "deployed-pod production code (conditional)", - "execution_context": "in-sandbox-agent", - "evidence": "cq-4 option 4 makes this contingent on the R2 spike's outcome. If hooks reliably resolve nested-subagent EGG_AGENT_ROLE, stay on model (a) ad-hoc prompts indefinitely. If not, this sub-slice converts every plugins/egg-sdlc/skills/egg-sdlc/agents/.md into a structurally restricting .claude/agents/.md frontmatter file (per Claude Code's subagent definition convention). Loader at orchestrator/substrate/__init__.py:232 switches from reading agents/.md as raw markdown to reading .claude/agents/.md with frontmatter (tools, allowed-tools, model)." - }, - { - "name": "Drop v0.x unstable markers on claude-code-side substrate modules", - "purpose": "deployed-pod docstring updates", - "execution_context": "deployed-pod (the comments live in production code that runs in-sandbox-agent or trusted-CI-runner depending on consumer)", - "evidence": "cq-7 option 4: 'drop the v0.x unstable marker after ≥3 roles exercise the claude-code side'. Markers live at orchestrator/substrate/__init__.py:11, orchestrator/substrate/in_process.py:22, orchestrator/substrate/claude_code/spawner.py:46, orchestrator/substrate/spawner.py:15, orchestrator/substrate/message_bus.py:18, orchestrator/substrate/policy.py:17, orchestrator/substrate/worktree.py:13. After slice-3 lands, the 8-role implement phase exercises every Protocol method on the claude-code leg; the markers come off. Slice-5 also documents in the ADR that the k3s leg interfaces remain v0.x unstable (cq-7 option 4 second clause)." - }, - { - "name": "ADR refresh — docs/architecture/claude-code-substrate.md", - "purpose": "documentation (no runtime behavior)", - "execution_context": "n/a — docs are read by humans / IDE-assisted agents", - "evidence": "docs/architecture/claude-code-substrate.md sections: 'Open work', 'Follow-up draft', 'Rollout deltas'. Update each to reflect what shipped vs what's deferred, capture cq-3 empirical decision outcome, document the bridge architecture (stage-script for refine/plan, daemon for implement), document the cost cap default and tuning guidance, document the drop of the v0.x marker on the claude-code leg only. Issue body DoD #6 calls this out explicitly." - } - ] - } - }, - "runtime_primitives_with_evidence": [ - { - "primitive": "run_pipeline_in_process", - "evidence": "orchestrator/substrate/in_process.py:84-149", - "purpose": "deployed-pod orchestrator entry (production code)", - "execution_context": "both — invoked by in-sandbox-agent from the egg-sdlc skill, and by trusted-CI-runner from the egg-orch local-run CLI" - }, - { - "primitive": "select_substrate(env)", - "evidence": "orchestrator/substrate/__init__.py:96-202", - "purpose": "deployed-pod factory function", - "execution_context": "both — used by concurrent_executor (in-sandbox-agent under claude-code substrate, deployed-pod gateway under k3s) and by tests (trusted-CI-runner)" - }, - { - "primitive": "_load_egg_sdlc_role_rubric(role)", - "evidence": "orchestrator/substrate/__init__.py:232-285", - "purpose": "deployed-pod rubric loader", - "execution_context": "in-sandbox-agent — wired into ClaudeCodeSpawner constructor; trusted-CI-runner invokes the same path under tests" - }, - { - "primitive": "ClaudeCodeSpawner.spawn", - "evidence": "orchestrator/substrate/claude_code/spawner.py:64-236; _capture_head_sha at :158; env injection at :124-128", - "purpose": "deployed-pod production spawner", - "execution_context": "in-sandbox-agent (calls egg_harness.run_agent in the parent's Python process)" - }, - { - "primitive": "InProcessMessageBus", - "evidence": "orchestrator/substrate/claude_code/message_bus.py:30-53; subclasses orchestrator/message_store.py:200 MessageStore", - "purpose": "deployed-pod production message bus", - "execution_context": "in-sandbox-agent (in-memory, lives in the parent's Python process)" - }, - { - "primitive": "PreToolUseHookPolicy + hook_entry.decide", - "evidence": "orchestrator/substrate/claude_code/policy.py:26-188; orchestrator/substrate/claude_code/hook_entry.py reads EGG_AGENT_ROLE", - "purpose": "deployed-pod production policy enforcer (the hook runs in-process as a subprocess Claude Code invokes for every tool call)", - "execution_context": "in-sandbox-agent — hook subprocesses run inside the parent Claude Code session's environment" - }, - { - "primitive": "LocalWorktreeManager", - "evidence": "orchestrator/substrate/claude_code/worktree.py:59-291", - "purpose": "deployed-pod production worktree manager", - "execution_context": "in-sandbox-agent (filesystem operations under EGG_WORKTREE_BASE)" - }, - { - "primitive": "build_agent_patterns(repo, role)", - "evidence": "shared/egg_restrictions/patterns.py:768", - "purpose": "deployed-pod single source of truth for file-write boundaries", - "execution_context": "both — gateway/phase_filter.py:1061 calls it in the trusted-CI-runner-vs-deployed-pod boundary, hook_entry.py calls it in-sandbox-agent" - }, - { - "primitive": "validate_invariants() / INV-3 / INV-5 / INV-6", - "evidence": "orchestrator/action_guards.py:631-757", - "purpose": "deployed-pod BRC concurrency guard", - "execution_context": "both — runs in any consumer of PeerConsensusTracker (orchestrator/peer_consensus.py:69)" - }, - { - "primitive": "_PHASE_ROLES / _PHASE_REVIEWERS", - "evidence": "shared/egg_contracts/agent_roles.py:1172, :1183", - "purpose": "deployed-pod roster definition (single source of truth for phase composition)", - "execution_context": "both — same import path on k3s and claude-code" - }, - { - "primitive": "_spawn_agent + _spawn_agent_via_substrate", - "evidence": "orchestrator/concurrent_executor.py:504-603", - "purpose": "deployed-pod dispatch seam (reads EGG_SUBSTRATE, picks bundle, calls spawner)", - "execution_context": "both — ConcurrentPhaseExecutor runs under in-sandbox-agent (claude-code) or in the gateway pod (k3s)" - }, - { - "primitive": "EGG_AGENT_ROLE env var", - "evidence": "set at orchestrator/substrate/claude_code/spawner.py:124-128; read at orchestrator/substrate/claude_code/hook_entry.py for role resolution", - "purpose": "deployed-pod production env var", - "execution_context": "in-sandbox-agent — must be set on every spawned subagent's environment" - }, - { - "primitive": "EGG_SUBSTRATE env var", - "evidence": "read at orchestrator/substrate/__init__.py:96-202 (select_substrate) and orchestrator/concurrent_executor.py:539", - "purpose": "deployed-pod production env var", - "execution_context": "both — operator sets it once per pipeline run; both substrates honor it identically" - }, - { - "primitive": "EGG_WORKTREE_BASE env var", - "evidence": "read at orchestrator/substrate/claude_code/worktree.py:LocalWorktreeManager; default ~/.egg-worktrees///", - "purpose": "deployed-pod production env var", - "execution_context": "in-sandbox-agent (parent session) — typical override in dev is ./.egg-state/" - }, - { - "primitive": "EGG_PIPELINE_MAX_AGENT_INVOCATIONS env var (NEW in slice-5)", - "evidence": "does NOT exist today; verified by grep across orchestrator/ shared/. Will be introduced at orchestrator/concurrent_executor.py:_spawn_agent", - "purpose": "deployed-pod production safeguard env var", - "execution_context": "both — counter is per-pipeline, surfaced via PipelineCostCapExceeded exception", - "default": "200 per cq-6 option 2" - }, - { - "primitive": "egg-orch local-run --issue N CLI subcommand (NEW in slice-5)", - "evidence": "does NOT exist today; orchestrator/cli.py has serve / status / etc but not local-run", - "purpose": "deployed-pod operator-facing CLI", - "execution_context": "trusted-CI-runner (operator's local machine; outside any Claude Code session). DoD #5 requirement; feedback Q3 option A scope." - }, - { - "primitive": "substrate test fixture", - "evidence": "integration_tests/regression/conftest.py:592-642 — parametrized over [k3s, claude-code]; skips claude-code when EGG_AGENT_ROLE is set", - "purpose": "test-only fixture", - "execution_context": "trusted-CI-runner (pytest-driven); the skip heuristic prevents in-sandbox-agent test runs from exercising the claude-code dimension twice" - }, - { - "primitive": "substrate smoke test", - "evidence": "integration_tests/regression/test_substrate_smoke.py — covers select_substrate shape, spawner round-trip type, in-process bus round-trip, single INV-3 case", - "purpose": "test-only", - "execution_context": "trusted-CI-runner" - }, - { - "primitive": "Substrate conformance matrix (NEW in slice-4)", - "evidence": "integration_tests/regression/test_conformance_matrix.py NEW; parametrize set is empty today, populated with [#2714, #2474, #2261, #2705, #2718] in slice-4 per feedback Q1", - "purpose": "test-only", - "execution_context": "trusted-CI-runner — pytest.mark.slow + 3-hour-per-phase ceiling per feedback Q2" - } - ], - "risks_anticipated": [ - { - "id": "R2-hook-multi-subagent", - "summary": "PreToolUse hook may not resolve EGG_AGENT_ROLE correctly for nested Agent-tool subagents (the parent's role leaks into the child's hook context). Today's single-role refiner case verifies it for one subagent only.", - "mitigation": "Slice-1 ships the 2-subagent worked-example spike BEFORE phase rollout begins (cq-5 option 1). If hooks fail this check, slice-1 also ships MCP-validator-side enforcement (cq-6 fallback). The decision to migrate to model (b) per-role .claude/agents/.md depends on this outcome (cq-4 option 4)." - }, - { - "id": "R7-context-budget", - "summary": "Architect / refiner / risk_analyst roles do deeper research than the single-role spike validated; they may hit Claude Code's subagent context limit in real pipelines.", - "mitigation": "Slice-3 collects context-budget metrics into .egg-state/conformance-metrics/-.json. If the data shows context-budget regression > 50% on architect / refiner, file a follow-up issue for the cq-10 fork-based delegation work (deferred from the spike per ADR open-work section)." - }, - { - "id": "BRC-concurrency-on-new-substrate", - "summary": "INV-3 / INV-5 / INV-6 are validated only for the single-role refiner case today. Plan-phase concurrency (3 producers + 1 reviewer) is the first multi-role BRC test; implement is the maximum concurrency surface (8 roles).", - "mitigation": "Slice-2 ships plan-phase BRC concurrency tests covering INV-3 stale-version, INV-5 open-NACK barrier, INV-6 commit-bound ACK on the new substrate before slice-3's 8-role surface. Re-use existing PeerConsensusTracker via InProcessMessageBus (no parallel implementation)." - }, - { - "id": "cost-runaway", - "summary": "At 11-15 subagents per pipeline on the parent's direct-billing API credentials, NACK ping-pong can rack up real money fast before the operator notices.", - "mitigation": "Slice-5 ships the EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 cap (cq-6 option 2). The cap is 200 — enough for legitimate 3-slice slice-DAG runs but trips before a runaway loop multiplies the spend by 10x. Operator can raise it explicitly when expected; opt-out by setting to a large value." - }, - { - "id": "decomposition-conflict-with-2261", - "summary": "#2261 slice-15 will decompose orchestrator/routes/pipelines.py (23,354 lines today) into orchestrator/routes/pipelines/_run_loop/ with per-phase handlers. The rollout's plan/implement/pr extensions in run_pipeline_in_process touch the same surface conceptually.", - "mitigation": "run_pipeline_in_process does NOT modify orchestrator/routes/pipelines.py — it reuses ConcurrentPhaseExecutor + PeerConsensusTracker from outside. The two efforts are decoupled at the file level. If #2261 slice-15 lands during this rollout, run_pipeline_in_process callers continue to import from the (decomposed) pipelines barrel without behavioral change." - }, - { - "id": "scope-fence-removal-blast-radius", - "summary": "Removing the cq-11 NotImplementedError scope-fence at in_process.py:820-826 widens the running surface from 1 phase to 4 phases at once. A regression in any phase blocks the whole rollout.", - "mitigation": "Scope-fence removal is in slice-4 — after slice-2 (plan) and slice-3 (implement) have each separately shipped and proven. The fence comes off only after each phase has its own BRC concurrency tests green. Slice-4 also re-flips the unit test in shared/tests/test_run_pipeline_in_process.py from 'assert fence raises' to 'assert chain proceeds' so the regression case is caught." - } - ], - "key_files_to_touch": [ - "orchestrator/substrate/in_process.py (plan/implement/pr branches; remove fence at :820-826)", - "orchestrator/substrate/__init__.py (drop v0.x markers at :11, :96-202 stays roster-agnostic)", - "orchestrator/substrate/claude_code/spawner.py (drop v0.x marker at :46; no functional change unless cq-3 outcome forces ClaudeCodeAgentToolSpawner)", - "orchestrator/substrate/claude_code/message_bus.py / policy.py / worktree.py / spawner.py / message_bus.py / policy.py / worktree.py (drop v0.x markers)", - "orchestrator/substrate/egg_harness/spawner.py (NEW EggHarnessSpawner)", - "orchestrator/concurrent_executor.py (cost cap counter; add EGG_PIPELINE_MAX_AGENT_INVOCATIONS handling in _spawn_agent / _spawn_agent_via_substrate seam)", - "orchestrator/cli.py (NEW local-run subcommand)", - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md / task_planner.md / risk_analyst.md / coder.md / tester.md / documenter.md / applier.md / reviewer_refine.md / reviewer_agent_design.md / reviewer_plan.md / reviewer_code.md / reviewer_code_holistic.md / reviewer_contract.md / reviewer_security.md / reviewer_concurrency.md (15 NEW rubrics)", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md (drive-loop migrated to real stage-script bridge; deferred-bridge callout removed at :97-102)", - "bin/egg-sdlc-stage (NEW bridge driver — stage-script for refine/plan, daemon mode for implement)", - "shared/tests/test_hook_role_resolution.py (NEW 2-subagent worked example for R2 spike)", - "shared/tests/test_run_pipeline_in_process.py (flip fence-removal assertion in slice-4)", - "integration_tests/regression/test_substrate_bridge.py (NEW HITL round-trip test)", - "integration_tests/regression/test_substrate_plan.py (NEW plan-phase BRC concurrency test)", - "integration_tests/regression/test_substrate_implement.py (NEW implement-phase concurrency stress + cq-3 metrics)", - "integration_tests/regression/test_conformance_matrix.py (NEW 5×2 matrix)", - "integration_tests/regression/conftest.py (parametrize set populated with the 5 curated issues at :592-642)", - "docs/architecture/claude-code-substrate.md (ADR refresh — DoD #6)" - ], - "acceptance_signals": [ - "All 15 new role rubric files exist under plugins/egg-sdlc/skills/egg-sdlc/agents/ and load via _load_egg_sdlc_role_rubric without ValueError.", - "run_pipeline_in_process(...) drives refine → plan → implement → pr end-to-end on EGG_SUBSTRATE=claude-code against at least one of the 5 curated conformance issues.", - "Operator can drive a full pipeline including all HITL gates through the egg-sdlc skill (no MCP provide_input round-trips required for the claude-code dimension).", - "egg-orch local-run --issue N produces the same artifact path as run_pipeline_in_process driven from the skill.", - "Conformance matrix integration_tests/regression/test_conformance_matrix.py is green for {#2714, #2474, #2261, #2705, #2718} × {k3s, claude-code} in CI.", - "EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 trips correctly when exceeded (verified by a unit test that fakes invocation counter overflow).", - "v0.x unstable markers removed from orchestrator/substrate/__init__.py, in_process.py, claude_code/spawner.py, spawner.py, message_bus.py, policy.py, worktree.py (claude-code leg). K3s leg keeps its markers per cq-7 option 4.", - "docs/architecture/claude-code-substrate.md updated to document the bridge architecture, the cost cap default, the dropped markers, and any cq-3 / cq-4 empirical outcomes." - ], - "open_questions_for_task_planner_and_risk_analyst": [ - "task_planner: How granular should the 15 role rubrics be? Each one is a fresh task or can the rubrics share a templated structure with role-specific variances?", - "task_planner: Is slice-1's R2 spike a single task or two (one for the spike, one for the conditional MCP-validator fallback)?", - "task_planner: How is slice-3's empirical data collection (cq-3 decision input) scoped — task with explicit metrics target, or a deliverable artifact (.egg-state/conformance-metrics/) without a pass/fail?", - "risk_analyst: What's the rollback story if slice-3's implement-phase substrate ships with a context-budget regression > 50% on one of the deeper-research roles? Does the slice ship with a feature flag to disable claude-code for specific roles?", - "risk_analyst: At 15-subagent scale, the parent session's API credentials are exposed to all subagents (feedback Q4 from refine: no additional mitigations for v1). Should slice-1 add a startup banner that confirms this trust posture before the first subagent dispatches?", - "risk_analyst: How do we monitor for runaway-NACK loops between EGG_PIPELINE_MAX_AGENT_INVOCATIONS triggers? The cap is a hard kill; we may want a softer 'review burden warning' at 100 invocations." - ], - "rollout_dependencies": [ - "#2261 slice-15 (decomposition of orchestrator/routes/pipelines.py) — independent but adjacent. Coordinate landing order; if slice-15 lands first, this rollout imports from the decomposed barrel; if this rollout lands first, slice-15's _run_loop/_run_plan.py / _run_implement.py / _run_pr.py will need to consider the in-process variant lives under orchestrator/substrate/in_process.py rather than orchestrator/routes/pipelines.py.", - "#2715 (predecessor spike) — already merged; this rollout extends rather than replaces it.", - "#2718 (claude-code substrate docs) — already merged into docs/index.md and docs/structure.md; the ADR refresh in slice-5 extends docs/architecture/claude-code-substrate.md from there." - ] -} diff --git a/.egg-state/agent-outputs/2717-risk_analyst-output.json b/.egg-state/agent-outputs/2717-risk_analyst-output.json deleted file mode 100644 index 4ccfb9ed7f..0000000000 --- a/.egg-state/agent-outputs/2717-risk_analyst-output.json +++ /dev/null @@ -1,728 +0,0 @@ -{ - "issue": 2717, - "phase": "plan", - "agent": "risk_analyst", - "schema_version": "1.0", - "title": "Risk Assessment: Substrate-swap follow-up rollout — wire remaining roles + plan/implement/pr phases on Claude Code substrate", - "summary": "Technical risk assessment for the post-spike rollout that (a) closes the HITL bridge gap (refine analysis line 36; reviewer v1 blocker #6), (b) adds 15 missing role rubrics, (c) lights up plan/implement/pr on the claude-code substrate, (d) ships the 5-issue conformance matrix in CI, (e) adds EggHarnessSpawner as the third spawner implementation (DoD #5; feedback Q3 = Option A full local-run), (f) lands a hardening pass for R2 validation / R15 migration / cost cap / k3s adapter promotion. Per the operator-resolved HITL set (cq-1..cq-7 + feedback Q1..Q6), this rollout proceeds via Option C (phase-sequential then parallel hardening) with bridge Option C-hybrid (flatten stage scripts for refine/plan, daemon for implement). Overall risk is HIGH — though notably structural rather than security-existential — because (a) the rollout's bridge implementation introduces TWO different bridge architectures with different failure modes (cq-1), (b) the R2 PreToolUse hook role-routing primitive is still unverified for multi-subagent dispatch (cq-5 only schedules the validation in slice-0), (c) 15 role rubrics must be authored AND each must structurally close the depth gap (#2622), (d) the implement-phase dispatches 8 concurrent subagents from a single parent session — the harness re-host path (cq-3 deferred substrate migration) puts that load on the parent's process, and (e) operator declined additional credential-scaling mitigations (feedback Q4) so trust-boundary blast radius scales linearly with subagent count. Recommendation: PROCEED_WITH_MITIGATIONS — the slice-0 R2 validation (cq-5) is the right hard checkpoint; every subsequent slice gates on its result.", - - "overall_risk_level": "HIGH", - "recommendation": "PROCEED_WITH_MITIGATIONS", - "recommendation_rationale": "The rollout's HITL resolutions are conservative in aggregate: spike-then-plan slicing inherited (cq-11), early R2 validation as slice-0 (cq-5), parallel substrates preserved (cq-1), k3s adapter parity deferred to avoid scope bloat (cq-7), cost cap at 200 (cq-6 = 4x the ADR's conservative number to accommodate multi-slice DAGs). However, several risks (R17, R18, R19, R20, R29) are structural to the rollout shape and not closeable by mitigation alone: they need explicit acceptance from reviewer_plan as PART of the slice plan, not as residual notes. The slice-0 R2 validation must include a binary go/no-go decision: hook role-routing works (continue to model (a) ad-hoc prompts per cq-4), or fall back to MCP-validator-side structural enforcement (cq-6 fallback path AND triggers cq-4 model-(b) migration). Without that explicit gate, the rollout will encode whichever default the slice-0 implementer assumes, and the second wave will inherit it.", - - "risks": [ - { - "id": "R17", - "title": "HITL bridge gap — Option (c) hybrid introduces two parallel bridge architectures", - "category": "design", - "severity": "HIGH", - "likelihood": "CERTAIN", - "impact": "cq-1 resolved to Option C hybrid: ship Option (b) (flatten generator into single-yield `python3 .py` invocations; decisions thread through .egg-state/contracts/.json) for refine and plan; design Option (a) (long-lived Python REPL/daemon the skill talks to via JSON-RPC envelopes) for implement. This is structurally TWO different bridge architectures with different failure modes, observability surfaces, and crash-recovery semantics. A bug fixed in one bridge does not necessarily fix the other; agent behavior under HITL pause differs by phase.", - "description": "RUNTIME-PRIMITIVE RISK (#2594). The current `run_pipeline_in_process(...)` generator (orchestrator/substrate/in_process.py:84) is engineered for Option (a)'s persistent-process model — three daemon threads (heartbeat, BRC re-review, bus tick) keep running while paused at yield boundaries. Option (b) discards this property: each `python3 .py` invocation is a fresh process, so the daemon threads die at exit, and the bridge between stages re-builds them on each invocation. Hidden assumption: every reachable HITL pause point during refine/plan can serialize its state into .egg-state/contracts/.json AND resume cleanly from a fresh process. The plan phase emits multiple HITL decisions per phase (3 producers can each register decisions concurrently; cq-1..cq-7 in #2717 refine is a worked example of 7 decisions per phase) — Option (b) must aggregate cross-producer decision state. Option (a) for implement requires JSON-RPC envelope design, port allocation, daemon crash recovery, and pre-flight detection of a stale daemon — none of which exists today. The walking-skeleton spike (#2715) tested neither bridge variant end-to-end (SKILL.md:97-102 explicitly flags the gap).", - "affected_files": [ - "orchestrator/substrate/in_process.py", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "plugins/egg-sdlc/skills/egg-sdlc/commands/ (NEW bridge driver)", - ".egg-state/contracts/.json (state ferrying contract)" - ], - "mitigation": { - "strategy": "(1) The slice-0 plan must include both bridge implementations as parallel work items, not a 'do Option (b) first, then design Option (a)' sequence — Option (a)'s daemon design influences Option (b)'s state-serialization contract (e.g., the daemon's state should also be .egg-state/contracts/.json so the two bridges can swap). (2) The bridge MUST be exercised end-to-end on a real issue (one HITL pause + resume + answer) BEFORE the plan-phase rollout commits to Option (b). (3) Per-phase HITL contract: define which phases use which bridge in the ADR, with explicit operator acceptance. (4) Cross-bridge compatibility test: an issue paused at refine via Option (b) then resumed under Option (a) at implement must produce identical state — that's a property the conformance matrix must check. (5) Reviewer_plan should require: bridge driver scripts (or daemon manifest), HITL state contract, and end-to-end test all ship in slice-0 alongside R2 validation.", - "effort": "HIGH", - "residual_risk": "MEDIUM — two bridge architectures fundamentally doubles the surface; mitigation is making the surface explicit and exercised, not removing it. If Option (b)'s state-serialization proves too brittle for plan-phase multi-decision workloads, the fallback is moving plan to Option (a) early, which is itself a slice-1 risk." - }, - "requires_human_review": true, - "review_reason": "The dual-bridge architecture is a structural HITL resolution; reviewer_plan should confirm that the rollout treats Option (a) and Option (b) as parallel deliverables — not 'Option (b) now, Option (a) when we get to implement' — because Option (a)'s design constrains Option (b)'s contract shape. This is the highest-impact rollout-shape decision." - }, - { - "id": "R18", - "title": "15 role rubrics must be authored AND structurally close the depth gap (#2622)", - "category": "design", - "severity": "HIGH", - "likelihood": "HIGH", - "impact": "Today `_load_egg_sdlc_role_rubric` (orchestrator/substrate/__init__.py:232) raises ValueError for any role except `refiner`. The rollout must ship 15 role rubric markdown files: architect, task_planner, risk_analyst, coder, tester, documenter, applier, reviewer_refine, reviewer_agent_design, reviewer_plan, reviewer_code, reviewer_code_holistic, reviewer_contract, reviewer_security, reviewer_concurrency. Each rubric must reach the agent via `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) — that's the structural depth-gap fix from #2622, which the spike ONLY validated for the refiner case.", - "description": "Two compounding sub-risks: (a) AUTHORING — each role rubric is a non-trivial content artifact (refiner.md is 119 lines; reviewer roles are typically longer because they encode review rubrics). Authoring 15 of them is a large content burden and quality varies; a thin rubric for `reviewer_security` (e.g.) means security review fidelity regresses on the claude-code substrate vs k3s. (b) STRUCTURAL DEPTH CLOSURE — #2622's fix was that rubric content flows through `build_system_prompt(sources)` so the calling subagent receives the depth content. The rollout must verify this property holds for EACH role under the harness re-host path (cq-3 deferred to implement-phase empirical decision); if any role's rubric is loaded but not threaded into the subagent's prompt, the depth gap silently re-opens for that role and reviewers will miss substantive issues.", - "affected_files": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/.md (15 NEW files)", - "orchestrator/substrate/__init__.py (_load_egg_sdlc_role_rubric)", - "shared/egg_harness/prompt.py (build_system_prompt depth invariant)", - "shared/prompts/.md (potential source material if rubrics adapt existing prompts)" - ], - "mitigation": { - "strategy": "(1) Each phase-rollout slice MUST include both rubric authorship AND a structural test that verifies `build_system_prompt(sources)` includes the rubric content when the spawner dispatches that role — the test runs against EACH role added in that slice. (2) Reviewer_plan reviews rubrics for completeness using a checklist: matches role's k3s prompt depth (cross-check against shared/prompts/.md), covers BRC consensus protocol, includes file-restriction guidance, includes phase-boundary expectations. (3) Per-role acceptance criteria: spawn that role on a real issue and inspect the captured prompt — depth gap closure is verifiable by reading the assembled system prompt. (4) Reviewer-role rubrics (reviewer_plan, reviewer_code, etc.) are particularly high-fidelity because they encode review criteria; treat them as 'reviewer rubric authorship' tasks distinct from producer rubric authorship — they may need separate reviewer (reviewer_agent_design) attention. (5) An automated check: a test that asserts every role enumerated in `shared/egg_contracts/agent_roles.py:_PHASE_ROLES` has a corresponding rubric file AND that loading it succeeds.", - "effort": "HIGH", - "residual_risk": "MEDIUM — rubric quality is content-bound; mitigation reduces structural issues but doesn't guarantee content fidelity. The conformance matrix (5 issues × 2 substrates) is the empirical backstop — if a rubric is thin, the matrix run on the claude-code substrate will surface symptoms (NACK loops, missing checks, fast-passing reviews)." - }, - "requires_human_review": true, - "review_reason": "Rubric authorship is content-quality work that pure structural validation can't catch. Reviewer_plan should explicitly accept the rubric scope per slice and confirm the structural-depth test plan." - }, - { - "id": "R19", - "title": "Implement-phase 8-subagent concurrent dispatch under harness re-host strains parent session", - "category": "performance", - "severity": "HIGH", - "likelihood": "HIGH", - "impact": "cq-3 resolved to 'decide empirically at implement-phase' — meaning ClaudeCodeSpawner stays on the harness re-host path (orchestrator/substrate/claude_code/spawner.py:14-26 re-hosts `egg_harness.run_agent` inside the parent Claude Code session's Python process). For the implement phase's 3 producers (coder/tester/documenter) + 5 reviewers (reviewer_code/reviewer_code_holistic/reviewer_contract/reviewer_security/reviewer_concurrency), that's 8 concurrent harness loops running inside the parent session. Each harness loop is a long-running Anthropic SDK conversation with its own prompt cache, tool-use roundtrips, and context budget. Stacking 8 in one process risks: parent context window saturation, single-process GIL contention on the SDK message dispatch, memory pressure from 8 simultaneous conversation histories.", - "description": "RUNTIME-PRIMITIVE RISK (#2594). The 'harness re-host' name is more benign-sounding than what it does: every harness loop the spawner runs has its own SDK client, history, tool calls, and (importantly) eats context from the parent Claude session. The k3s substrate didn't have this constraint because each agent ran in its own sandbox container. cq-3 deferred the migration to ClaudeCodeAgentToolSpawner (true Agent-tool dispatch with `subagent_type='general-purpose'`) until after empirical data exists — but the empirical data is the rollout itself. Plan phase (3 producers + 1 reviewer = 4 harness loops) is the first stress test; implement phase (8 loops) is double. Symptoms if exceeded: parent session OOM, SDK message throttling, agents reaching context limits mid-task with no checkpoint primitive (cq-10's hybrid is half-implemented in the spike), pipeline-wide deadlock when one agent's harness loop blocks the parent's main thread.", - "affected_files": [ - "orchestrator/substrate/claude_code/spawner.py", - "orchestrator/concurrent_executor.py (ThreadPoolExecutor sizing)", - "shared/egg_harness/run.py (harness loop entrypoint)" - ], - "mitigation": { - "strategy": "(1) Plan-phase slice (3 producers + 1 reviewer) MUST surface the empirical numbers: measured parent memory delta per harness loop, measured wall-clock per loop, measured parent context-window consumption. These numbers are the input to the cq-3 deferred decision. (2) Per-substrate `max_workers` cap: claude-code substrate defaults to a conservative number (e.g., 3 with serial overflow) until the plan-phase numbers say otherwise; k3s keeps 6. (3) The implement-phase slice MUST NOT ship until plan-phase numbers project safely to 8 concurrent loops, OR the rollout migrates to ClaudeCodeAgentToolSpawner mid-rollout. (4) Reviewer_plan requires a `concurrency-budget` deliverable in the plan-phase slice's acceptance criteria. (5) Slice-1 (plan) should include a stress test: dispatch 8 hello-world subagents concurrently under harness re-host and report parent metrics. (6) Document an explicit fallback: if 8-way harness re-host is infeasible, the implement-phase slice migrates to ClaudeCodeAgentToolSpawner (R15 option 1 effectively becomes 'migrate now during implement') instead of staying on the deferred path.", - "effort": "MEDIUM", - "residual_risk": "MEDIUM — the empirical answer is unknown until plan-phase numbers exist. Mitigation makes the empirical decision a gate, not a hope." - }, - "requires_human_review": true, - "review_reason": "The 8-subagent harness re-host load is the load-bearing performance claim of the rollout. Reviewer_plan should require explicit measurement in the plan-phase slice before approving the implement-phase shape." - }, - { - "id": "R20", - "title": "Reviewer roster has no substrate-specific reviewer (feedback Q5 declined expansion)", - "category": "design", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "feedback Q5 resolved to 'no new reviewer roles': existing reviewers (reviewer_code, reviewer_concurrency, reviewer_security, reviewer_agent_design) cover the substrate-rollout work. The operator's rationale is sound — adding reviewer_substrate would bloat the implement-phase fan-out to 9 reviewers. The risk: substrate-specific failure modes (subagent context overflow, hook firing semantics, parent-session reentrancy, bridge-state-file staleness) may not map cleanly to any existing reviewer's rubric, so a substrate-specific bug ships without being caught by any reviewer.", - "description": "Existing reviewer rubrics encode well-understood review dimensions: reviewer_code reviews code quality and adherence to conventions; reviewer_concurrency reviews thread-safety and BRC correctness; reviewer_security reviews credential boundaries and policy enforcement; reviewer_agent_design reviews prompt and agent-behavior design. None of these explicitly cover, e.g., 'does the rubric file actually flow through build_system_prompt to the subagent's context'? or 'does the bridge state file deserialize correctly under a fresh process'? The reviewer's rubric updates that this rollout requires (per R18) need to extend EACH existing reviewer's rubric with substrate-specific review criteria; otherwise the substrate-specific bug surface is invisible to review.", - "affected_files": [ - "shared/prompts/reviewer_concurrency.md", - "shared/prompts/reviewer_security.md", - "shared/prompts/reviewer_agent_design.md", - "shared/prompts/reviewer_code.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_*.md (new rubrics for rollout)" - ], - "mitigation": { - "strategy": "(1) Each existing reviewer's rubric receives a 'substrate-aware review criteria' appendix as part of this rollout. The appendix names the substrate-specific failure modes that reviewer should catch (e.g., reviewer_concurrency: 'bridge state-file race; parent-session reentrancy; bus thread shutdown discipline'; reviewer_security: 'hook role-routing; PreToolUse hook bypass paths'; reviewer_agent_design: 'rubric flows through build_system_prompt; per-role context-budget'). (2) Reviewer_plan slice deliverable: explicit mapping table — 'substrate-specific risk R-N reviewed by reviewer-X'. (3) When a substrate-specific issue surfaces during the rollout that doesn't map to an existing reviewer, file a follow-up issue (per feedback Q5's escape hatch). (4) The rollout's pr-phase slice includes a final 'substrate-coverage audit' pass.", - "effort": "MEDIUM", - "residual_risk": "LOW — extending existing rubrics is mechanical; the risk reduces to a content-quality issue." - }, - "requires_human_review": false - }, - { - "id": "R21", - "title": "5-issue conformance set includes issues whose state is no longer reproducible", - "category": "compatibility", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "feedback Q1 fixed the conformance set: #2714 (bug), #2474 (feature/regression suite), #2261 (refactor), #2705 (infra), #2718 (doc). Note #2714 was already closed when feedback was submitted (operator flagged this explicitly). #2261 is a multi-slice decomposition in flight (orchestrator/CLAUDE.md slice-15 placeholder), so its end state shifts as slices land. #2705 is a Cilium/k3s setup change that requires a real k3s cluster to reproduce. Running these 5 issues through a full SDLC pipeline in CI under BOTH substrates means CI must (a) replay an issue against current main, (b) tolerate baseline changes between issue-filed and now, (c) not require external infrastructure (k3s cluster).", - "description": "Concrete issues: (a) #2714 is closed — does the conformance run target the issue's body verbatim, or does it require a snapshot of the repo at the time of filing? If verbatim, the rollout may already be a rebase ahead (autostash fallback merged ff1c4d8). (b) #2705 reproduces a Cilium portmap CNI scenario that requires a real k3s cluster — running this issue under EGG_SUBSTRATE=claude-code is meaningful (it's the substrate decision) but the issue's content is k3s-flavored. (c) #2261 is mid-decomposition — the slice plan changes as slices land. (d) #2718 is a doc change — minimal SDLC surface. (e) #2474 is a feature/regression suite — high SDLC surface but historically expensive (the issue itself was a multi-cycle BRC). The Q2 budget ('3-hour-per-phase ceiling') projects: a single conformance run might be 4 phases × 5 issues × 2 substrates × up to 3h/phase = 120h CI wall-clock in the worst case. That's prohibitive for PR-blocking CI; the matrix must run nightly or weekly, not per-PR.", - "affected_files": [ - "integration_tests/regression/test_substrate_smoke.py", - "integration_tests/regression/conftest.py", - ".github/workflows/ (CI matrix configuration)", - ".egg-state/conformance// (per-issue reproducible fixtures)" - ], - "mitigation": { - "strategy": "(1) Conformance runs target SNAPSHOTS of the repo at the time of each issue's filing, not main. Each conformance fixture pins a base commit; the rollout includes the work to capture those snapshots. (2) Per-issue substrate-eligibility: #2705 may be marked 'k3s-only' if the issue's content requires a real cluster (running claude-code substrate on it is still meaningful — it's testing the substrate's ability to drive SDLC on a k3s-flavored issue, not the k3s reality itself). (3) CI matrix runs nightly with a fast-feedback smoke-only PR check; the full matrix is gated on the smoke check. (4) Per-phase ceiling enforced as a CI job timeout (3h fails the job; tester can request bump via HITL). (5) The rollout includes a 'matrix-runner' utility script in scripts/ that operators can run locally against a single issue to debug failures. (6) Replace #2714 if the closed-issue reproduction proves brittle — feedback Q1 explicitly said 'current state (open/closed/merged) is not relevant' but if reproduction fails, swap it for a still-open bug fix issue.", - "effort": "HIGH", - "residual_risk": "MEDIUM — conformance reproducibility is fundamentally hard at this scale; mitigation is making the matrix sustainable (snapshots, nightly cadence) rather than fast." - }, - "requires_human_review": true, - "review_reason": "The conformance-matrix shape (snapshots vs main, nightly vs per-PR, infra requirements) is a CI architectural decision. Reviewer_plan should sign off on the matrix-runner's shape before the conformance slice ships." - }, - { - "id": "R22", - "title": "Rollout conflicts with #2261 slice-15 decomposition of orchestrator/routes/pipelines.py", - "category": "compatibility", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "#2261 slice-15 is decomposing orchestrator/routes/pipelines.py (~16,400 lines) into a sub-package — orchestrator/CLAUDE.md lists pre-allocated submodule clusters including `_run_loop/` (_run_pipeline, PHASE_HANDLERS, _run_refine.py, _run_plan.py, _run_implement.py, _run_pr.py). The rollout's plan/implement/pr work in run_pipeline_in_process(...) touches the same code paths via the per-phase handlers. If slice-15 lands during this rollout, every in-flight slice rebases onto the new module structure; if the rollout lands first, slice-15 rebases through the rollout's changes.", - "description": "Both efforts touch the same per-phase handler surface. The rollout extends run_pipeline_in_process(...) (currently refine-only) to plan/implement/pr; slice-15 decomposes the analogous per-phase handlers under orchestrator/routes/pipelines/_run_loop/. Without coordination: (a) merge conflicts at every phase-handler change; (b) interface drift between the in-process handler and the HTTP-daemon handler; (c) test fixture duplication. The k3s-substrate handlers (HTTP daemon) and claude-code-substrate handlers (in-process generator) ideally share core phase logic — that's the architectural intent — but if they diverge during the rollout, the substrate-portable claim breaks.", - "affected_files": [ - "orchestrator/routes/pipelines.py", - "orchestrator/routes/pipelines/_run_loop/* (slice-15 target)", - "orchestrator/substrate/in_process.py", - "orchestrator/concurrent_executor.py" - ], - "mitigation": { - "strategy": "(1) The rollout's plan-phase slice coordinates with #2261 slice-15: either slice-15 lands first (preferred — it's a refactor, low semantic change), or this rollout commits to NOT decomposing _run_plan.py / _run_implement.py / _run_pr.py during the rollout and lets slice-15 do it later. (2) The phase handlers in run_pipeline_in_process(...) call into the HTTP daemon's phase logic (shared internal API) rather than reimplementing it. (3) Document a 'per-phase handler shape' invariant in docs/architecture/claude-code-substrate.md: both substrates' handlers share the same function signatures and the same _criteria/_decisions/_readers helpers. (4) Reviewer_plan should require an explicit dependency declaration: 'this rollout depends on/conflicts with #2261 slice-15; here's the coordination plan.' (5) A small upfront slice 0.5 could extract the per-phase handler shape into shared/orchestrator/phase_handlers.py so both #2261 and this rollout consume the shared module.", - "effort": "MEDIUM", - "residual_risk": "LOW — refactor-versus-feature merge conflicts are well-understood; mitigation is sequencing rather than redesign." - }, - "requires_human_review": false - }, - { - "id": "R23", - "title": "Cost cap of 200 (cq-6) leaves room for $1000+ runaway scenarios on direct-billing", - "category": "security", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "cq-6 selected EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 as the default. At Anthropic's per-call pricing for substantial subagent invocations (Sonnet 4.x-tier with deep tool use), 200 invocations is materially expensive — a NACK ping-pong on the implement phase could rack up real money before tripping. The ADR's original conservative number was 50; the operator's 200 accommodates 3-slice DAGs with concurrent producers/reviewers plus a couple of NACK cycles, but the cost ceiling at 200 is roughly $50-$200+ per pipeline depending on token volumes — material for solo developers.", - "description": "Two compounding factors: (a) the cap is invocation-count-based, not token-count-based — a single invocation that exhausts its context budget can cost more than several short ones (deep refines on substantive issues can run 100K+ tokens per invocation); (b) per-phase reporting (recommended in 2623 risk assessment REC5 part 2) was not explicitly resolved in 2717 HITL set — the cap exists but visibility-during-pipeline does not. Concrete failure mode: a reviewer NACKs 3 producers, each re-proposes, reviewer NACKs again, this cycles 5+ times — that's 3 × 5 × 2 = 30+ invocations just on the NACK loop, plus the producers' baseline implement-phase invocations (3 producers × several harness turns each).", - "affected_files": [ - "orchestrator/substrate/in_process.py (cost-cap enforcement)", - "orchestrator/concurrent_executor.py (invocation accounting)", - "shared/egg_harness/run.py (per-agent invocation counting)", - "docs/architecture/claude-code-substrate.md (operator guidance)" - ], - "mitigation": { - "strategy": "(1) The cost cap implementation MUST emit per-phase invocation reports during the pipeline so the operator sees cost as it accrues (REC5 part 2 from #2623 assessment — still applicable). (2) Add a secondary token-based ceiling (EGG_PIPELINE_MAX_AGENT_TOKENS, e.g., 5M tokens) as a complementary cap that catches deep-research scenarios where invocation count is low but token volume is high. (3) Document cost-conscious operator guidance in install docs: 'each agent invocation may cost $0.X-$X depending on tokens; for $0-budget exploration, set EGG_PIPELINE_MAX_AGENT_INVOCATIONS=20'. (4) Hardening-pass slice includes per-phase cost reporting (orchestrator emits structured progress events with token/cost deltas). (5) When the cap trips, the orchestrator emits an OVERSEER_ALERT and halts the pipeline cleanly (no zombie subagents). (6) Conformance matrix runs include cost telemetry that surfaces per-substrate cost deltas — informs the operator's next round of Q2 budget setting.", - "effort": "LOW", - "residual_risk": "LOW once visibility ships; the cap value can be tuned post-rollout based on conformance-matrix data." - }, - "requires_human_review": false - }, - { - "id": "R24", - "title": "Hardening-pass parallel fan-out (5 sibling slices) exceeds any prior slice-DAG width", - "category": "performance", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "cq-2 selected Option 3: 'hardening pass: EggHarnessSpawner + cost cap + R15 + R2 + k3s adapter, all parallel'. Five sibling slices in one wave is larger than any prior pipeline run in the codebase. The slice scheduler at orchestrator/concurrent_executor.py:469 (ThreadPoolExecutor(max_workers=6)) accommodates the dispatch, but BRC's open-NACK barrier (issue #2142) was designed at lower fan-out and may exhibit different latency characteristics with 5 concurrent producer/reviewer pairs. Each hardening slice has its own producers + reviewers; if 5 slices fan out to ~25 concurrent subagents, the R19 parent-session-strain risk compounds.", - "description": "Two coupling factors: (a) BRC consensus mechanics at 5-slice width — peer message routing, ACK aggregation, and the open-NACK barrier all scale linearly with concurrent producers. The current peer_consensus.py code is well-tested at width 2-3; width 5 is unexplored. (b) The harness re-host parent-session strain (R19) multiplies: each hardening slice spawns its own concurrent agents on top of the rollout's already-stressed parent session. A single parent session running the hardening pass under harness re-host may be load-equivalent to 30+ concurrent subagents.", - "affected_files": [ - "orchestrator/concurrent_executor.py", - "orchestrator/peer_consensus.py (BRC mechanics at width-5)", - "orchestrator/substrate/claude_code/spawner.py" - ], - "mitigation": { - "strategy": "(1) The hardening slice plan should EXPLICITLY measure slice-scheduler throughput at width-5 — emit a structured progress report at hardening-wave start: 'fan-out width = 5; total active subagents = N; estimated wall-clock = X'. (2) If parent-session strain is a known constraint (R19), serialize the hardening wave instead: hardening-wave-A (EggHarnessSpawner + R15 migration), then hardening-wave-B (cost cap + R2 + k3s adapter). The HITL resolution can be re-litigated for cq-2 if early measurements say width-5 is infeasible. (3) BRC test coverage: add an integration test that exercises 5-slice fan-out specifically (in addition to the existing 2-3 slice tests). (4) Per-slice resource budgets enforced by the scheduler — if total active subagents > N, queue subsequent slices. (5) Reviewer_concurrency should require this as part of the hardening-wave slice acceptance.", - "effort": "MEDIUM", - "residual_risk": "LOW — sequencing fallback exists; the worst case is hardening-wave running serially as two waves of 2-3." - }, - "requires_human_review": false - }, - { - "id": "R25", - "title": "Walking-skeleton scope-fence removed entirely (feedback Q6) — partial-rollout fragility", - "category": "compatibility", - "severity": "LOW", - "likelihood": "HIGH", - "impact": "feedback Q6 resolved to 'remove scope-fence entirely once plan/implement/pr land'. The fence today (orchestrator/substrate/in_process.py: NotImplementedError for non-refine phases) is a spike-scope guard. Removing it AFTER all phases land is the right call; the operator's wording 'once plan/implement/pr land' is precise. Risk: during the rollout, as slices land incrementally (e.g., plan slice merges before implement slice), the fence behavior may degrade in ambiguous ways — e.g., a partial-rollout state where plan is implemented but implement is not, and the fence message becomes stale or misleading.", - "description": "Concrete failure modes during rollout: (a) plan slice merges → fence message for 'implement' is still NotImplementedError but the SKILL.md operator-facing text may say 'plan ships'; (b) operators who run `bin/egg` against issue X during the rollout may find pipeline halts mid-rollout with a confusing error; (c) the marker substring (_K3S_FENCE_MESSAGE) is used by tests to structurally detect the k3s-leg fence — if those tests are updated mid-rollout to remove the fence, they may pass on a partial rollout that's actually broken. Risk is low because the rollout is the operator's own work and partial-rollout periods are short, but ambiguity in error messages compounds debugging time.", - "affected_files": [ - "orchestrator/substrate/in_process.py", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "shared/tests/test_run_pipeline_in_process.py" - ], - "mitigation": { - "strategy": "(1) Each phase-rollout slice updates the fence message to reflect the new partial-rollout state: 'plan works; implement still in flight (#2717)'. (2) Reviewer_plan checklist item: 'this slice updates the fence message AND the SKILL.md status table'. (3) Pipeline status reports at each phase transition include 'this substrate supports refine, plan; implement is in development'. (4) The pr-phase + conformance slice removes the fence as part of its acceptance — and that slice can't pass until all four phases work, by design.", - "effort": "LOW", - "residual_risk": "NEGLIGIBLE — UX papercuts in transient rollout states." - }, - "requires_human_review": false - }, - { - "id": "R26", - "title": "EggHarnessSpawner full local-run (feedback Q3 Option A) demands a third spawner against an immature protocol", - "category": "design", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "feedback Q3 resolved to 'Option A — Full local-run end-to-end': `egg-orch local-run --issue 1234` must drive a pipeline headlessly through the EggHarnessSpawner. This is the third AgentSpawner implementation after ClaudeCodeSpawner (in-parent-session) and K3sSpawnerAdapter (legacy shim). The operator's rationale is sound: cq-7's stability marker explicitly requires ≥3 roles exercise the protocols before drop, and EggHarnessSpawner provides the third implementation. Risk: the AgentSpawner protocol surface was designed against 2 implementations; adding a third (subprocess-based, no parent session, HITL via stdin/stdout JSON) may reveal protocol-design gaps that the first two papered over.", - "description": "Specific protocol-design gaps to watch: (a) HITL flow — ClaudeCodeSpawner has parent-session AskUserQuestion; K3sSpawnerAdapter has MCP provide_input; EggHarnessSpawner needs stdin/stdout JSON prompts to a local CLI. The protocol must accommodate all three. (b) commit_sha capture — ClaudeCodeSpawner runs `git -C rev-parse HEAD` in-process; K3sSpawnerAdapter reads from pod stdout; EggHarnessSpawner runs the harness in a subprocess and must capture the subprocess's reported SHA. (c) Concurrency — ClaudeCodeSpawner runs in the parent session; K3sSpawnerAdapter dispatches to k8s pods; EggHarnessSpawner spawns subprocesses with their own credentials. Each has different process-lifetime semantics. (d) Per-agent env injection — each spawner needs to thread EGG_AGENT_ROLE / EGG_PIPELINE_ID correctly; the protocol must specify which env vars are required vs optional.", - "affected_files": [ - "orchestrator/substrate/spawner.py (protocol)", - "orchestrator/substrate/egg_harness/spawner.py (NEW — EggHarnessSpawner)", - "orchestrator/cli.py (egg-orch local-run command)", - "shared/egg_harness/run.py" - ], - "mitigation": { - "strategy": "(1) EggHarnessSpawner slice (one of the hardening-pass slices) MUST start with a protocol-review pass: enumerate every assumption the AgentSpawner protocol made against 2 implementations and confirm the third honors them. Any gap surfaces as a protocol-change task, not an implementation hack. (2) Per-spawner end-to-end test that runs the same issue through each spawner and asserts identical contract / artifact outputs. (3) HITL bridge for EggHarnessSpawner: structured stdin/stdout JSON-RPC (matches Option (a) bridge for ClaudeCodeSpawner — same shape, different transport). (4) Reviewer_agent_design reviews the EggHarnessSpawner alongside the AgentSpawner protocol — not as a 'new spawner' task but as a 'protocol-stability validation' task. (5) The protocol stability marker drop (cq-7) happens only AFTER all three spawners satisfy the same contract test suite.", - "effort": "MEDIUM", - "residual_risk": "LOW — third-implementation discovery is well-understood; mitigation surfaces design gaps early." - }, - "requires_human_review": false - }, - { - "id": "R27", - "title": "MCP-validator fallback (cq-6 option 2) has structural enforcement gaps", - "category": "security", - "severity": "HIGH", - "likelihood": "MEDIUM", - "impact": "cq-5 schedules R2 PreToolUse hook role-routing validation as slice-0. If validation fails, the rollout falls back to MCP-validator-side enforcement (cq-6 option 2). Today's MCP server (sandbox/egg_agent_tools/handlers/restrictions.py:70 check_file_restriction) is a tool the agent voluntarily calls — agents that DON'T call it bypass the policy. Without a structural enforcement mechanism, the MCP-validator fallback is advisory, not load-bearing.", - "description": "RUNTIME-PRIMITIVE RISK + TRUST-BOUNDARY RISK (#2594, #2474). The k3s substrate's gateway enforces patterns.py at git push time — agents can't bypass it because all writes funnel through the gateway. The claude-code substrate's PreToolUse hooks enforce at Write/Edit time — agents can't bypass them because the hook fires before the tool returns. The MCP-validator fallback relies on agent cooperation — agents that don't invoke check_file_restriction simply write whatever they want. If R2 fails and the rollout commits to MCP-validator, the substrate loses structural enforcement. Compounding: the spike's R1 acceptance was qualitative for trusted-repo SDLC; the multi-subagent rollout (15 subagents per pipeline) expands the blast radius without expanding the enforcement model — feedback Q4 explicitly declined additional mitigations.", - "affected_files": [ - "sandbox/egg_agent_tools/handlers/restrictions.py", - "orchestrator/substrate/claude_code/policy.py", - "shared/egg_restrictions/patterns.py", - "plugins/egg-sdlc/skills/egg-sdlc/hooks/ (PreToolUse hook)" - ], - "mitigation": { - "strategy": "(1) slice-0 R2 validation MUST report a binary outcome (hook role-routing works | fallback required). If fallback required: triage immediately as a HITL — the rollout's enforcement model is a security architecture decision, not an implementation detail. (2) MCP-validator fallback path includes structural enforcement: PreToolUse hook STILL fires (for Bash and global-deny rules), and an additional mechanism — e.g., a write-tracker hook that records all Writes, and a post-tool-call validator that retroactively flags bypasses — restores enforceability. (3) Each role's rubric MANDATES calling check_file_restriction before any Write — this is a behavioral mitigation but combined with the hook-emitted advisories, it's defense in depth. (4) The fallback path is documented as 'requires R15 model (b) migration' — per-role .claude/agents/.md frontmatter constrains tool restrictions structurally even without role-routing hooks. (5) Reviewer_security MUST review the slice-0 outcome before approving any subsequent slice; if fallback is taken, reviewer_security signs off on the structural enforcement plan.", - "effort": "HIGH (if fallback is taken)", - "residual_risk": "MEDIUM — fallback enforcement is structurally weaker than gateway-side; mitigation reduces but doesn't eliminate the gap." - }, - "requires_human_review": true, - "review_reason": "Falling back to MCP-validator enforcement is a security architecture decision. Reviewer_security and the operator should explicitly accept the residual gap before subsequent slices proceed." - }, - { - "id": "R28", - "title": "Phase-sequential rollout (cq-2 Option 3) discovers BRC issues late and forces multi-slice rework", - "category": "design", - "severity": "MEDIUM", - "likelihood": "LOW", - "impact": "cq-2 Option 3 sequences slices: bridge → plan → implement → (pr + conformance) → hardening. The rationale is sound (smaller blast radius than parallel; faster than full serial), but the trade-off is that BRC issues discovered in implement-phase may require rework in plan-phase. Specifically: if implement-phase reveals that the orchestrator-as-bus model (InProcessMessageBus) drops messages under high concurrency (8 producers/reviewers), the fix may require InProcessMessageBus changes that ALSO affect plan-phase (4 producers/reviewers). Re-running the plan-phase slice is expensive (rubric rework, integration-test re-runs).", - "description": "Specific late-discovery scenarios: (a) InProcessMessageBus delivery semantics drift under 8-way concurrent dispatch — plan phase ships green at 4-way, implement reveals at 8-way; (b) HITL bridge Option (b) state-file aggregation drift — plan phase has multiple concurrent HITL emissions (cq-1..cq-7 in #2717 itself was 7 decisions across producers), implement phase has 8 producers + 5 reviewers all potentially emitting HITL — the Option (b) state file may not aggregate cleanly across producers; (c) per-role rubric depth gaps surface only when the role runs end-to-end on a real issue, and implement-phase roles (coder, tester, documenter) have the most demanding rubrics. Probability is LOW because the spike + plan slice provide multiple checkpoints; impact if it occurs is MEDIUM-HIGH because rework crosses slice boundaries.", - "affected_files": [ - "orchestrator/substrate/claude_code/message_bus.py", - "orchestrator/substrate/in_process.py", - "shared/egg_orchestrator/ (BRC mechanics)" - ], - "mitigation": { - "strategy": "(1) Plan-phase slice acceptance criteria includes a stress test: dispatch 8 hello-world agents through InProcessMessageBus and verify ACK aggregation, message delivery, and barrier semantics. The stress test is the canary for implement-phase. (2) HITL bridge state-file contract is defined and tested independently of any specific phase — the contract handles arbitrary concurrent producers. (3) Each phase-rollout slice ships its own integration test that exercises the phase's concurrency level (4 for plan, 8 for implement, 0 for pr). (4) Reviewer_concurrency reviews the BRC mechanics specifically — not as a 'plan phase works' check but as a 'BRC scales to 8' check during the plan-phase slice. (5) If rework is required mid-rollout, the slice-DAG allows revisiting a completed slice — that's a normal pipeline operation, not a crisis.", - "effort": "MEDIUM", - "residual_risk": "LOW — multi-checkpoint mitigation reduces late-discovery probability substantially." - }, - "requires_human_review": false - }, - { - "id": "R29", - "title": "Subagent context budget under harness re-host: implement-phase deep work exceeds parent-session window", - "category": "performance", - "severity": "HIGH", - "likelihood": "MEDIUM", - "impact": "Implement-phase agents (coder, tester, documenter) do deep code work: reading O(20-50) source files, writing 100s-1000s of lines, running tests, iterating. Each harness loop's conversation history grows large. Under harness re-host (cq-3), every active harness loop's history sits in the parent Claude session's context. cq-10 selected the 'hybrid checkpoint + fork' strategy — checkpoint is ported (orchestrator/substrate/claude_code/ checkpoint primitives), forking is deferred. Without fork-based delegation, deep implement-phase work may exhaust the parent session's context window before the phase completes.", - "description": "RUNTIME-PRIMITIVE RISK (#2594). The parent Claude session has a finite context window. Each active harness loop's accumulated conversation eats from that budget. With 8 concurrent implement-phase loops, the parent may be holding 8 × O(50K) = 400K+ tokens of subagent-conversation history. Adding checkpoints reduces this (subagents can resume from a summary), but only if checkpoints are USED — and the rubrics + harness need to know when to checkpoint. The refine-phase analysis explicitly flagged this in R7 for the spike's single-role case; the implement-phase 8-role case is roughly 8× the risk. Symptoms if exceeded: parent session reports 'context limit reached' mid-pipeline, subagents fail with cryptic errors, pipeline deadlocks awaiting subagent completion that's actually crashed.", - "affected_files": [ - "orchestrator/substrate/claude_code/spawner.py", - "shared/egg_harness/run.py", - "plugins/egg-sdlc/skills/egg-sdlc/agents/coder.md (rubric must teach checkpoint discipline)", - "plugins/egg-sdlc/skills/egg-sdlc/agents/tester.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/documenter.md" - ], - "mitigation": { - "strategy": "(1) Implement-phase slice MUST include a context-budget stress test: run coder/tester/documenter agents on a real-world issue and measure peak parent-session context usage. (2) Producer rubrics for implement-phase roles include checkpoint-discipline guidance — agents emit checkpoints at well-known phase boundaries (after reading files; before writing major sections). (3) Spawner-side: enforce per-harness-loop context budgets (terminate the harness if its conversation exceeds N tokens; resume from checkpoint summary). (4) Hardening-pass slice for fork-based delegation is moved UP if context-budget pressure manifests during plan-phase rollout — fork delegation is the structural fix and cq-10's intended end state. (5) Conservative fallback: implement-phase initially runs with max_workers=3 (serial of 8 reviewers becomes 3-3-2 batches) until context-budget measurements support raising it. (6) Reviewer_concurrency reviews the implement-phase concurrency cap as part of slice acceptance.", - "effort": "HIGH", - "residual_risk": "MEDIUM — context budget is fundamentally finite; mitigation reduces consumption but cannot eliminate the constraint. The realistic path is accepting smaller-than-k3s effective concurrency on the claude-code substrate until the fork primitive lands." - }, - "requires_human_review": true, - "review_reason": "The 8-subagent implement-phase context budget is the second-highest-impact rollout risk after R17 (bridge gap). Reviewer_concurrency + architect should review the measurement plan and the fallback path before the implement-phase slice ships." - }, - { - "id": "R30", - "title": "Plan-phase concurrent HITL emission stresses Option (b) bridge state aggregation", - "category": "design", - "severity": "MEDIUM", - "likelihood": "HIGH", - "impact": "Plan phase has 3 concurrent producers (architect, task_planner, risk_analyst) each able to emit HITL decisions to the contract. The #2717 refine itself emitted cq-1..cq-7 (7 decisions) — implying plan-phase may emit similar volume per pipeline. Under Option (b) bridge (flatten stage scripts; decisions thread through .egg-state/contracts/.json), multiple producers writing decisions concurrently to the same contract file risks: write contention (lost updates), inconsistent ordering across stage-script invocations, stale-read scenarios where one producer's stage script doesn't see another's decision yet.", - "description": "RUNTIME-PRIMITIVE RISK (#2594). The current contract-store implementation handles concurrent writes via file locks and atomic-replace patterns — that works for the HTTP daemon where one process owns the contract. The Option (b) bridge has multiple `python3 .py` processes potentially writing concurrently. Race conditions: (a) producer A reads contract, decides to add cq-N, writes — meanwhile producer B reads pre-A contract, decides to add cq-N+1 against pre-A state, writes — A's decision is overwritten or B's decision is duplicated; (b) HITL aggregation: skill loop renders cq-1, gets answer, writes answer back — meanwhile producer C is mid-stage and reads stale answer; (c) BRC verdicts in flight while HITL is paused: timing edges where verdict aggregation lags HITL resolution.", - "affected_files": [ - "orchestrator/substrate/in_process.py", - "shared/egg_contracts/contract_store.py (concurrent-write semantics)", - "plugins/egg-sdlc/skills/egg-sdlc/commands/ (bridge driver — sequencing logic)" - ], - "mitigation": { - "strategy": "(1) Option (b) bridge contract: stage scripts hold a contract-lock (filesystem advisory lock or contract-store CAS) for the duration of any read-modify-write sequence. (2) HITL decisions emit through a queue rather than direct contract writes — the bridge driver dequeues and serializes them. (3) Conformance test: 3 concurrent producers + 7 HITL decisions per producer = 21 decisions racing — verify they all land, no duplicates, no losses. (4) If the lock contention proves prohibitive, fall back to plan-phase serial dispatch (producers run sequentially) until Option (a) daemon ships — that's a known performance regression but a known-safe correctness path. (5) Reviewer_concurrency reviews the bridge-state contract specifically.", - "effort": "MEDIUM", - "residual_risk": "LOW — contract-store concurrency patterns are well-understood; mitigation surfaces existing primitives at the bridge layer." - }, - "requires_human_review": false - }, - { - "id": "R31", - "title": "Trust-context shift at 15-subagent scale (R1 scaled) — feedback Q4 explicitly declined additional mitigations", - "category": "security", - "severity": "MEDIUM", - "likelihood": "CERTAIN", - "impact": "feedback Q4 resolved 'no additional mitigations for v1' — the operator accepted that scaling from 1 subagent (spike) to 15 subagents (full rollout) does NOT change the trust model (same operator, same machine, same trusted repo). This is a deliberate operator-accepted risk. The credential-exposure surface still scales linearly with subagent count: 15× more agent processes that COULD exfiltrate the parent session's Anthropic credential under a prompt-injection scenario. The risk is acknowledged-and-accepted, but the rollout's ADR refresh must document the scaling delta explicitly so future operators inherit the same understanding.", - "description": "The R1 acceptance from #2623 plan phase was qualitative: 'trusted-repo SDLC on operator's own machine'. That framing scales to 15 subagents because the trust model is unchanged. However: (a) the per-pipeline blast radius if a single subagent IS compromised expands (more downstream agents inherit the compromised state); (b) the prompt-injection surface is now 15× the prompts (more chances for malicious content to land in some agent's context); (c) the operator who later opts INTO multi-tenant or CI-runner-managed usage inherits the v1 trust model with no warning. Operator declined the opt-in EGG_REQUIRE_CREDENTIAL_PROXY flag explicitly — that's a deliberate v1 scope decision.", - "affected_files": [ - "docs/architecture/claude-code-substrate.md (ADR refresh — DoD #6)", - "docs/architecture/integration-test-trust-boundary.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md (operator-facing scope notice)" - ], - "mitigation": { - "strategy": "(1) ADR refresh explicitly states: the v1 trust model is 'trusted-repo SDLC on operator's machine; same credential exposure model as the spike'. Future-proofing: explicit list of usage patterns that REQUIRE re-litigation (multi-tenant, CI-runner-managed, public-issue-stream-defended). (2) SKILL.md adds a scope-of-use notice in install docs: 'use only against repos you trust to commit on'. (3) Conformance matrix issue selection (feedback Q1's 5 issues) explicitly stays on this trust model — they're all egg-internal issues. (4) Per-pipeline cost reporting (R23 mitigation) is also a 'something weird happening' detection signal — anomalous cost may indicate compromised agent loop. (5) Document a follow-up issue for opt-in credential proxy mode (operator's escape hatch if v1 model proves insufficient).", - "effort": "LOW", - "residual_risk": "MEDIUM-LOW — operator-accepted v1 risk; mitigation is documentation and detection, not closure." - }, - "requires_human_review": true, - "review_reason": "The 15-subagent trust-context scaling is operator-accepted but the acceptance text must surface in the ADR refresh. Reviewer_security should confirm the ADR refresh captures the scaling delta explicitly, not as an implementation detail." - }, - { - "id": "R32", - "title": "Interface stability marker (cq-7 drop after ≥3 roles exercise) timing collision with rollout completion", - "category": "compatibility", - "severity": "LOW", - "likelihood": "HIGH", - "impact": "cq-7 resolved to 'Defer entirely AND drop the v0.x unstable marker after ≥3 roles exercise the claude-code side'. The 3-role threshold is hit DURING the rollout (refiner already done; plan phase adds 3 producers → 4 roles exercised; implement adds 3 more → 7 roles). The marker drop is triggered before the rollout completes. Risk: a downstream consumer (test fixture, external tool, second-party plugin) starts depending on the protocol shape after the marker drops, but the protocol surface MAY still change during the hardening pass (R26 EggHarnessSpawner discovery, k3s adapter promotion deferred per cq-7).", - "description": "The cq-7 wording is precise: 'accept the interfaces are stable on the claude-code leg even if the k3s leg hasn't promoted yet'. That's a graceful precision. But hardening-pass slices may surface protocol-design gaps that DO require interface changes (R26 specifically warns about this). If the marker drops mid-rollout and then a hardening-pass slice changes the protocol, downstream consumers break with no warning. The precision needed is: drop the marker AFTER the hardening pass, OR drop the marker after 3 roles AND retain a 'subject to change during hardening' sub-note.", - "affected_files": [ - "orchestrator/substrate/__init__.py (INTERFACE STABILITY: v0.x unstable.)", - "orchestrator/substrate/spawner.py", - "orchestrator/substrate/message_bus.py", - "orchestrator/substrate/policy.py", - "orchestrator/substrate/worktree.py", - "docs/architecture/claude-code-substrate.md" - ], - "mitigation": { - "strategy": "(1) The marker drop is the FINAL deliverable of the rollout — moved from 'after 3 roles' to 'after pr-phase + conformance + hardening completes'. The intent of cq-7 (≥3 roles exercise is enough) is honored because by the time hardening completes, ≥7 roles will have exercised the protocols. (2) Document a 'subject to revision during the rollout's hardening pass' note in the docstring AND ADR while the rollout is in flight. (3) Reviewer_agent_design owns the marker-drop decision — they sign off when the rollout closes. (4) An automated CI check: the marker may not be dropped while open follow-up issues exist on protocol surface.", - "effort": "LOW", - "residual_risk": "NEGLIGIBLE — sequencing fix." - }, - "requires_human_review": false - }, - { - "id": "R33", - "title": "Apply-phase (epic-only) producer role 'applier' is dispatched but UNREFERENCED in DoD", - "category": "compatibility", - "severity": "LOW", - "likelihood": "MEDIUM", - "impact": "The refine analysis Phase Roles table (line 53) includes the apply phase: producer APPLIER, reviewer REVIEWER_CONTRACT. The issue body's DoD #1 says 'every agent role used by the SDLC pipeline (full producer + reviewer lists named above) runs end-to-end on the Claude Code substrate' — but the issue body's role enumeration does NOT include APPLIER (only refine + plan + implement + pr phases enumerated, no apply). The apply phase is epic-only and may be silently skipped, but the rubric authorship list (R18) is ambiguous: is applier in or out?", - "description": "If applier is out: the rollout closes with apply-phase still requiring k3s substrate, contradicting the 'all phases, all roles' claim for epic-driven pipelines. If applier is in: 16 rubrics, not 15 (adds applier.md), and the rollout needs an apply-phase end-to-end test on an epic issue — none of the conformance 5 issues are epics. Risk is LOW (apply phase is epic-only; few users hit it; deferral to a separate issue is reasonable) but the ambiguity surfaces during slice planning and may cause wasted work or downstream confusion.", - "affected_files": [ - "shared/egg_contracts/agent_roles.py (_PHASE_ROLES includes apply)", - "plugins/egg-sdlc/skills/egg-sdlc/agents/applier.md (potential 16th rubric)" - ], - "mitigation": { - "strategy": "(1) Plan-phase HITL: ask the operator whether apply-phase is in or out of this rollout's DoD. If in: add applier rubric to the rubric-authorship list and an epic conformance fixture. If out: explicitly document the deferral in the ADR ('apply phase remains k3s-only; epic-driven pipelines require EGG_SUBSTRATE=k3s'). (2) Task planner's task list should not include applier rubric work unless HITL confirms. (3) If deferred, a follow-up issue tracks apply-phase parity.", - "effort": "LOW", - "residual_risk": "NEGLIGIBLE — scope ambiguity, not technical risk." - }, - "requires_human_review": true, - "review_reason": "DoD ambiguity should be resolved before slice plans commit to either including or excluding applier." - }, - { - "id": "R34", - "title": "Bridge driver creation under role file boundaries (#2474-class risk)", - "category": "compatibility", - "severity": "MEDIUM", - "likelihood": "MEDIUM", - "impact": "The HITL bridge gap closure (R17) requires NEW files: a bridge driver script (Option (b) stage scripts or Option (a) daemon manifest) plus the bridge driver's JSON-RPC envelope spec. These files land somewhere — likely `plugins/egg-sdlc/skills/egg-sdlc/commands/` or `bin/` or `scripts/`. File-write boundaries for the coder role (or whoever ships the bridge) must permit those paths. If the bridge driver script is treated as 'production code' it falls under `src/` or similar restricted paths; if treated as 'plugin content' it may need to be in `plugins/`; if it's a CLI in `bin/` it has its own restrictions. The role responsible for shipping the bridge needs file-write permission at the target path.", - "description": "RUNTIME-PRIMITIVE RISK + #2474-class risk. The spike's reviewer cycle surfaced this class of risk (the spike's structural-cause #5/#6 are about role file boundaries and tool-use budgets). The bridge driver is a new artifact that doesn't fit any existing pattern. If it's a python module loaded by the skill, it goes under `plugins/egg-sdlc/skills/egg-sdlc/`; if it's a CLI executable, it might go in `bin/`. The coder role's restrictions in shared/egg_restrictions/patterns.py determine where the coder can ship it. Symptoms if mismatched: coder slices NACK at file-restriction gateway during push, requires HITL re-routing (apply-phase pattern), wastes a NACK cycle.", - "affected_files": [ - "shared/egg_restrictions/patterns.py (role file boundaries)", - "plugins/egg-sdlc/skills/egg-sdlc/commands/ (likely bridge driver location)", - "bin/ (potential CLI entry)", - "scripts/ (potential helper utilities)" - ], - "mitigation": { - "strategy": "(1) slice-1 plan-phase work explicitly identifies the bridge driver file paths and verifies the implementing role has write permission. (2) If mismatch: add the path to shared/egg_restrictions/patterns.py for the implementing role BEFORE the slice starts. (3) Reviewer_contract reviews the file-boundary check as part of slice acceptance. (4) Task planner emits per-task target paths so this surfaces at task-planning time, not at push time.", - "effort": "LOW", - "residual_risk": "LOW — well-understood pattern; mitigation is upfront path verification." - }, - "requires_human_review": false - } - ], - - "runtime_primitive_and_trust_boundary_risks": { - "context": "Per #2594 and #2474: plans that depend on classes, fixtures, routes, or env vars which don't exist in the form the plan assumes — or which exist only in a different execution context — are a recurring high-impact failure mode. The substrate-swap rollout concentrates these risks because it depends on Claude Code primitives (PreToolUse hooks, Agent tool dispatch, AskUserQuestion call shape) AND introduces new primitives (HITL bridge daemon vs stage scripts, in-process orchestrator, per-substrate cost cap) the codebase has never exercised. The audit below enumerates every primitive the plan-phase rollout assumes and the verification path for each.", - "primitives_to_verify_in_rollout": [ - { - "primitive": "PreToolUse hook role-routing for nested / multi-subagent dispatch", - "assumption": "cq-5 schedules slice-0 validation; the rollout assumes hooks correctly identify the calling subagent's EGG_AGENT_ROLE across 2+ concurrent subagents.", - "verification_needed": "slice-0 2-subagent worked example (operator-defined acceptance per cq-5 resolution). Binary go/no-go output: hooks work → continue; else → cq-6 option 2 fallback + R15 model (b) migration triggered.", - "risk_if_missing": "Policy enforcement collapses to advisory; structural enforcement requires fallback to MCP-validator + agent-definition tool restrictions (R27)." - }, - { - "primitive": "HITL bridge — Option (b) stage script with .egg-state/contracts/.json serialization", - "assumption": "cq-1 Option C-hybrid assumes stage scripts can serialize ALL HITL state through the contract file and resume cleanly from a fresh process for refine and plan phases.", - "verification_needed": "slice-1 end-to-end test: refine + plan phases with at least 7 HITL decisions across 3 producers, paused/resumed across 4 separate `python3 .py` invocations. Verify no state loss.", - "risk_if_missing": "Plan-phase HITL aggregation drops decisions or duplicates them; rollout falls back to Option (a) daemon earlier than planned (R17, R30)." - }, - { - "primitive": "HITL bridge — Option (a) long-lived daemon with JSON-RPC envelopes", - "assumption": "cq-1 Option C-hybrid assumes a long-lived Python REPL/daemon can be designed for implement phase; current codebase has NO daemon lifecycle primitive (no port allocation, crash recovery, stale-detection).", - "verification_needed": "Implement-phase slice ships full daemon: lifecycle (start/stop/restart), port allocation (avoid collisions), state recovery (resume after parent skill restart), pre-flight check (detect stale daemon).", - "risk_if_missing": "Implement phase falls back to Option (b) at higher concurrency, which R30 already identifies as state-aggregation-risky." - }, - { - "primitive": "ClaudeCodeSpawner harness re-host under 8-way concurrent dispatch", - "assumption": "cq-3 defers the ClaudeCodeAgentToolSpawner migration; rollout assumes the parent session's process can host 8 concurrent egg_harness loops without context / memory / SDK-rate exhaustion.", - "verification_needed": "plan-phase slice measures parent-session memory + context + SDK call latency under 4 concurrent harness loops; implement-phase slice measures under 8. Empirical numbers feed the cq-3 deferred decision (stay on harness re-host vs migrate to AgentToolSpawner).", - "risk_if_missing": "Implement phase deadlocks, OOMs, or context-exhausts the parent (R19, R29)." - }, - { - "primitive": "build_system_prompt(sources) depth-closure for 15 new role rubrics", - "assumption": "The spike validated depth closure for refiner only; rollout assumes every new rubric reaches `build_system_prompt(sources)` and flows into the subagent's prompt.", - "verification_needed": "Per-role structural test: spawn role X on a real issue, capture the assembled system prompt via spawner introspection, assert rubric content is present (#2622 depth-gap structural fix).", - "risk_if_missing": "Depth gap re-opens for affected roles; reviewers miss substantive issues; reviewer-rubrics in particular regress vs k3s baseline (R18)." - }, - { - "primitive": "Cost cap (EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200) — count-based vs token-based ceiling", - "assumption": "cq-6 sets 200 as the per-pipeline invocation count cap; rollout assumes 200 is high enough for legitimate work and low enough to catch runaway loops.", - "verification_needed": "Hardening-pass slice exercises a deliberate NACK loop and verifies the cap trips cleanly; conformance matrix runs report per-issue invocation counts to validate 200 covers the 5-issue range.", - "risk_if_missing": "Cap is too generous (R23 cost overruns) or too tight (legitimate large pipelines trip prematurely)." - }, - { - "primitive": "Per-substrate ThreadPoolExecutor max_workers cap", - "assumption": "Today's max_workers=6 was sized for k3s pod parallelism; rollout assumes the same cap or a per-substrate override.", - "verification_needed": "Plan-phase slice introduces per-substrate cap configuration AND default values (k3s=6, claude-code=3 conservatively). Implement-phase slice validates the claude-code default against 8-way harness re-host stress.", - "risk_if_missing": "Plan phase ships with 6-way default; implement phase oversaturates parent session (R19)." - }, - { - "primitive": "5-issue conformance matrix repository state at issue-filing time", - "assumption": "feedback Q1 fixed 5 specific issues; rollout assumes each issue is reproducible from its issue-body content at some chosen repo state.", - "verification_needed": "Conformance slice captures per-issue base-commit snapshots, fixture files (.egg-state/conformance//manifest.json or similar), and a matrix-runner script.", - "risk_if_missing": "Matrix flakes as main moves forward; rollout's 'green on claude-code' claim becomes a moving target (R21)." - }, - { - "primitive": "AgentSpawner protocol satisfies 3rd implementation (EggHarnessSpawner)", - "assumption": "feedback Q3 Option A requires EggHarnessSpawner full local-run; rollout assumes the existing protocol surface accommodates a subprocess-based implementation without changes.", - "verification_needed": "EggHarnessSpawner slice in the hardening pass starts with a protocol-review pass; any protocol gap surfaces as a change task BEFORE the spawner is implemented.", - "risk_if_missing": "Protocol surface revisions during the hardening pass invalidate the interface-stability marker drop (R26, R32)." - }, - { - "primitive": "Reviewer rubrics' substrate-awareness extension", - "assumption": "feedback Q5 declined new reviewer roles; rollout assumes existing reviewer rubrics can be extended in-place to cover substrate-specific failure modes.", - "verification_needed": "Each existing reviewer rubric receives a 'substrate-aware review criteria' appendix; reviewer_plan slice deliverable includes the mapping table 'substrate-specific risk → reviewer who catches it'.", - "risk_if_missing": "Substrate-specific bug surface is invisible to review; bug ships unreviewed (R20)." - } - ], - "trust_boundary_shifts": [ - { - "boundary": "Anthropic API credential isolation at multi-subagent scale (15 subagents per pipeline)", - "today_post_spike": "Spike validated R1 acceptance qualitatively for 1 subagent against trusted-repo SDLC.", - "after_rollout": "15 subagents per non-epic pipeline (refine reviewers + plan producers/reviewer + implement producers/reviewers). All inherit parent session's credential context.", - "impact": "Credential exfiltration blast radius scales linearly with subagent count. feedback Q4 explicitly accepted no additional mitigations; v1 trust model documented.", - "mitigation_summary": "ADR refresh documents the scaling delta explicitly; SKILL.md scope-of-use notice in install docs; follow-up issue tracks opt-in EGG_REQUIRE_CREDENTIAL_PROXY for future use cases (R31)." - }, - { - "boundary": "File-write enforcement timing under multi-role dispatch", - "today_post_spike": "Spike validated PreToolUse hook for single-role refiner only.", - "after_rollout": "15 roles + nested-dispatch; hook must distinguish caller role for EACH subagent's tool call.", - "impact": "If R2 validation fails, fallback to MCP-validator-side is advisory (R27 — structural enforcement gap). Defense in depth via cq-4 R15 model (b) migration becomes load-bearing.", - "mitigation_summary": "slice-0 R2 validation is the gate; binary outcome triggers cq-6 option 2 fallback path with structural compensation (R15 model (b))." - }, - { - "boundary": "Cost control at multi-slice DAG scale", - "today_post_spike": "Spike has no cost cap.", - "after_rollout": "EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 (cq-6); per-phase reporting recommended but not separately resolved.", - "impact": "200-invocation cap may still cost $50-200+ per runaway pipeline (R23). User-side visibility during pipeline is essential for early intervention.", - "mitigation_summary": "Hardening-pass slice adds per-phase invocation reporting AND a secondary token ceiling. Conformance matrix surfaces per-substrate cost deltas." - }, - { - "boundary": "Parent-session context-window pressure", - "today_post_spike": "Spike's single role does not pressure parent context.", - "after_rollout": "Up to 8 concurrent harness loops during implement; checkpoint primitive ported but fork primitive deferred (cq-10 hybrid is half-implemented).", - "impact": "Parent session may exhaust context before implement phase completes (R29).", - "mitigation_summary": "Per-substrate max_workers cap; harness-loop context budgets; checkpoint-discipline guidance in implement-phase rubrics; fork primitive moved up if pressure manifests during plan phase." - }, - { - "boundary": "Bridge state-file integrity at concurrent-producer scale", - "today_post_spike": "Spike has no bridge — gap is acknowledged in SKILL.md.", - "after_rollout": "Option (b) stage scripts (refine/plan) thread state through .egg-state/contracts/.json with multiple concurrent producers writing HITL decisions.", - "impact": "Concurrent contract writes can lose decisions or produce inconsistent state (R30).", - "mitigation_summary": "Contract-lock during read-modify-write; HITL queue + serialized rendering by the bridge driver; conformance test for 21-decision concurrent emission." - }, - { - "boundary": "Per-role file-write boundaries for new bridge artifacts", - "today_post_spike": "Spike's new files are in well-known plugin paths (plugins/egg-sdlc/skills/egg-sdlc/).", - "after_rollout": "Bridge driver scripts (Option (b)) and/or daemon entry point (Option (a)) need a target path — coder role must have write permission there.", - "impact": "Coder slices NACK at gateway during push if path not permitted (R34, #2474-class).", - "mitigation_summary": "slice-1 explicitly identifies bridge driver paths; shared/egg_restrictions/patterns.py is updated upfront if needed; reviewer_contract validates." - } - ] - }, - - "areas_requiring_human_review": [ - { - "area": "HITL bridge dual-architecture (R17) — Option (a) and Option (b) as parallel deliverables", - "reason": "cq-1 Option C-hybrid commits to TWO bridge architectures. Reviewer_plan should confirm the slice plan treats them as parallel work items (Option (a)'s design constrains Option (b)'s contract) — not sequential ('Option (b) now, Option (a) later'). The bridge gap is the rollout's largest single technical risk.", - "suggested_reviewer": "reviewer_plan + the operator (HITL re-litigation possible if Option (b) proves brittle)" - }, - { - "area": "R2 PreToolUse hook role-routing validation — slice-0 binary go/no-go (R27)", - "reason": "slice-0 must produce a binary outcome (hooks work | fallback). Reviewer_security must review the outcome before any subsequent slice proceeds. If fallback: the rollout's enforcement model becomes load-bearing on cq-4 R15 model (b) migration, which is a substantive architectural change.", - "suggested_reviewer": "reviewer_security + reviewer_plan" - }, - { - "area": "Parent-session context budget under 8-way harness re-host (R19, R29)", - "reason": "The harness re-host path (cq-3 deferred) puts 8 concurrent harness loops in the parent session for implement-phase. Plan-phase slice must measure 4-way numbers; implement-phase slice must not ship until those numbers project safely. Reviewer_concurrency + architect should review the measurement plan and the fallback (early migration to ClaudeCodeAgentToolSpawner).", - "suggested_reviewer": "reviewer_concurrency + architect" - }, - { - "area": "Conformance matrix shape (R21) — snapshots vs main, nightly vs per-PR, infra requirements", - "reason": "The 5-issue conformance matrix is a CI architectural decision (R21). Reviewer_plan should sign off on the matrix-runner's shape before the conformance slice ships — particularly whether issues run against current main (matrix becomes flaky) or against pinned base commits (matrix is stable but more setup-intensive).", - "suggested_reviewer": "reviewer_plan" - }, - { - "area": "Apply-phase rubric and DoD scope (R33)", - "reason": "The issue body's DoD says 'every agent role' but does not name applier. Plan-phase HITL should ask the operator whether apply-phase is in or out of this rollout. The answer changes the rubric count (15 vs 16) and the conformance-matrix issue selection (need an epic fixture or not).", - "suggested_reviewer": "operator (HITL) + reviewer_plan" - }, - { - "area": "Trust-boundary scaling delta at 15-subagent scale (R31)", - "reason": "feedback Q4 operator-accepted the scaling delta; reviewer_security should confirm the ADR refresh captures it explicitly. The acceptance must be visible to future operators, not buried in implementation details.", - "suggested_reviewer": "reviewer_security" - } - ], - - "rollback_plan": { - "strategy": "The rollout's slice-DAG and EGG_SUBSTRATE env var preserve rollback semantics at every layer. The k3s substrate code paths remain in main throughout. Each slice can be reverted individually because slices are PR-shaped and gated through BRC. The most consequential per-slice rollback is the bridge slice (slice-1) — if the dual-bridge architecture proves unworkable, the rollout reverts to spike state and reconsiders cq-1.", - "steps": [ - "Per-slice rollback (intra-rollout): revert the slice's PR; the parent issue's slice-DAG re-runs the failed slice with new evidence. No data migration; .egg-state/contracts/.json is filesystem-native.", - "Full-rollout rollback (post-merge): set EGG_SUBSTRATE=k3s in the operator's env (or unset; k3s remains the default). The walking-skeleton spike's refiner-on-claude-code path still works (it's intact), but plan/implement/pr default to k3s. Conformance matrix can be re-pointed at the spike-only snapshot if needed.", - "Bridge-rollback (Option (a) failure): if the daemon proves unworkable, fall back to Option (b) for implement phase too. This is a performance regression (loses heartbeat-during-HITL for implement) but preserves correctness.", - "Bridge-rollback (Option (b) failure): if the stage scripts prove unworkable, fall back to Option (a) daemon for refine/plan too. This delays the rollout but preserves correctness.", - "R2-fallback (hook role-routing fails): roll forward into cq-6 option 2 (MCP-validator) + cq-4 model (b) migration. This is NOT a rollback — it's the planned fallback path. Reviewer_security + operator must explicitly accept the residual structural-enforcement gap (R27).", - "Cost-cap rollback: trivially configurable via EGG_PIPELINE_MAX_AGENT_INVOCATIONS env var; no code change needed.", - "k3s adapter parity rollback: cq-7 deferred this entirely; no rollback needed because nothing changes on the k3s side." - ], - "data_loss_risk": "NONE — all substrate state (contracts, BRC history, agent outputs, checkpoints) is filesystem-native and substrate-portable. No DB schema migration; no state rewriting.", - "downtime_risk": "LOW — env-var-driven substrate selection means downtime is whatever a session-restart costs; no migration window. The claude-code substrate is opt-in via the egg-sdlc plugin; users who don't install or set EGG_SUBSTRATE=claude-code see no change." - }, - - "implementation_recommendations": [ - { - "id": "REC1", - "priority": "CRITICAL", - "recommendation": "slice-0 (R2 validation) must produce a BINARY go/no-go outcome documented in the rollout's ADR before slice-1 (plan phase) starts. The outcome dictates cq-4 R15 model decision: if hooks work → stay on model (a); if fallback → migrate to model (b) as a load-bearing structural fix.", - "rationale": "cq-5's resolution to early-spike R2 is conditional on the validation actually being performed and acted on. Without a binary outcome, the second-wave rollout encodes whichever default the slice-0 implementer assumes — and subsequent slices inherit it without review." - }, - { - "id": "REC2", - "priority": "CRITICAL", - "recommendation": "HITL bridge implementation (slice-1) MUST ship BOTH Option (a) daemon design AND Option (b) stage scripts in the same slice — not sequentially. Option (a)'s envelope spec determines Option (b)'s state-serialization contract; designing them independently invites divergence. End-to-end test exercises both architectures.", - "rationale": "cq-1 Option C-hybrid commits to two bridges; treating them as parallel work prevents the rollout from shipping a Option (b) that doesn't accommodate the eventual Option (a) (or vice versa). R17 is the rollout's largest technical risk; this mitigation reduces residual risk substantially." - }, - { - "id": "REC3", - "priority": "HIGH", - "recommendation": "Per-role rubric authorship is gated by a structural-depth test: spawn role X on a real issue, capture the assembled prompt, assert rubric content is present. Each phase-rollout slice ships rubrics + structural tests together — not rubrics alone.", - "rationale": "R18's depth-gap risk only closes when the rubric content actually flows through build_system_prompt(sources). The spike validated this for refiner; the rollout must validate for each new role." - }, - { - "id": "HIGH", - "priority": "HIGH", - "id_override_note": "REC4", - "recommendation": "Plan-phase slice MUST measure parent-session memory + context + SDK latency under 4-way concurrent harness re-host AND project to 8-way. If 8-way projection is unsafe, the implement-phase slice migrates to ClaudeCodeAgentToolSpawner (mid-rollout R15 model migration) — that's a known fallback per cq-3.", - "rationale": "R19 + R29 are the rollout's highest-impact performance risks. The empirical answer is the cq-3 deferred-decision input; without an explicit gate, implement phase ships on assumptions." - }, - { - "id": "REC5", - "priority": "HIGH", - "recommendation": "Each existing reviewer rubric (reviewer_code, reviewer_concurrency, reviewer_security, reviewer_agent_design, reviewer_contract, reviewer_code_holistic) receives a 'substrate-aware review criteria' appendix as part of this rollout. The plan-phase slice produces the appendix template; subsequent slices apply it.", - "rationale": "feedback Q5 declined new reviewer roles; the trade-off is that existing reviewers' rubrics must be extended to catch substrate-specific failure modes (R20). Without this, substrate-specific bugs ship unreviewed." - }, - { - "id": "REC6", - "priority": "MEDIUM", - "recommendation": "Conformance matrix runs against PINNED BASE COMMITS captured per issue — not against current main. The conformance slice includes (a) per-issue manifests at .egg-state/conformance//, (b) a matrix-runner script in scripts/, (c) nightly CI cadence with a PR-blocking smoke subset.", - "rationale": "R21 reproducibility risk; the matrix only proves what it pins. Running against moving main produces a flaky matrix that erodes operator trust." - }, - { - "id": "MEDIUM", - "priority": "MEDIUM", - "id_override_note": "REC7", - "recommendation": "Cost cap (EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200) implementation MUST include per-phase invocation reporting in real time (orchestrator emits structured progress events with token + invocation deltas at each phase transition). A secondary token-based ceiling (EGG_PIPELINE_MAX_AGENT_TOKENS, e.g., 5M) complements the count cap.", - "rationale": "R23 — 200 invocations may cost $50-$200+; visibility during pipeline is essential for the operator to intervene before the cap trips." - }, - { - "id": "REC8", - "priority": "MEDIUM", - "recommendation": "Slice plan explicitly identifies #2261 slice-15 dependency: either slice-15 lands first, or this rollout commits to NOT decomposing the _run_*.py phase handlers (lets slice-15 do it later). The plan-phase slice produces an explicit coordination plan.", - "rationale": "R22 — concurrent decomposition of orchestrator/routes/pipelines.py creates merge conflicts AND interface-drift risk between substrates." - }, - { - "id": "REC9", - "priority": "MEDIUM", - "recommendation": "Apply-phase scope HITL: plan-phase slice asks the operator whether applier role is in or out of this rollout's DoD. Documented in the ADR either way.", - "rationale": "R33 — DoD ambiguity must resolve before task planning commits to 15 vs 16 rubrics." - }, - { - "id": "REC10", - "priority": "MEDIUM", - "recommendation": "EggHarnessSpawner slice (in hardening pass) starts with a protocol-review pass before implementation. The pass surfaces any protocol gaps as change tasks; the spawner is implemented against the revised protocol if needed.", - "rationale": "R26 — third-implementation discovery is a known design pattern; pre-implementation protocol review prevents post-merge protocol revisions that would invalidate the cq-7 stability marker drop (R32)." - }, - { - "id": "REC11", - "priority": "LOW", - "recommendation": "Interface-stability marker drop (cq-7) is moved to the FINAL deliverable of the rollout (after pr + conformance + hardening). Until then, a 'subject to revision during hardening pass' note appears in the docstring + ADR.", - "rationale": "R32 — the marker should drop AFTER the rollout's last protocol-shape change, not in the middle of the rollout." - } - ], - - "performance_assessment": { - "agent_dispatch_latency": "Spike's harness re-host has sub-second dispatch overhead (no k8s pod start). Implement-phase 8-way dispatch may see queueing delays if parent session GIL contention manifests; plan-phase 4-way is the projection input. Conformance matrix surfaces per-substrate latency deltas.", - "throughput": "Reduced relative to k3s during the rollout if parent-session strain forces a conservative max_workers cap (e.g., claude-code=3 vs k3s=6). Recoverable post-rollout if R15 model (b) migration + ClaudeCodeAgentToolSpawner ship (would lift parent-session bottleneck). Phases with ≤4 roles unaffected.", - "memory": "Parent session memory grows linearly with active harness loops. 4-way plan phase: O(2-4 GB) projected; 8-way implement: O(4-8 GB) — exceeds typical developer-laptop budget under harness re-host without checkpointing. Mitigation: per-loop context budgets, checkpoint discipline, per-substrate max_workers cap.", - "disk": "Worktrees: ~/.egg-worktrees/// — for 8 concurrent agents on a 100MB repo, ~800MB peak. Manageable on developer machines; flagged in install docs. State files (.egg-state/) grow with HITL bridge state-aggregation under Option (b); cleanup utility in scripts/.", - "context_window_pressure": "The load-bearing constraint. R29 elevates this risk: without fork-based delegation (cq-10 hybrid half-implemented), 8 concurrent implement-phase harness loops may exhaust parent session before phase completes. Mitigation: checkpoint discipline, per-loop budgets, conservative max_workers, fork-primitive ship-up if pressure manifests during plan phase.", - "note": "Performance characterization is the rollout's most important empirical deliverable. feedback Q2 deferred budget setting until empirical numbers exist — the plan-phase slice produces those numbers." - }, - - "security_assessment": { - "threat_model": "Inherits the spike's threat model (R1) at 15x subagent scale. Three primary threats: (1) credential exfiltration via prompt injection — 15× more agent contexts means 15× more opportunities for malicious issue/PR content to land in a tool-use template; (2) file-write boundary bypass — if R2 hook role-routing fails and MCP-validator fallback is taken, structural enforcement collapses to advisory unless cq-4 R15 model (b) ships as compensation; (3) per-pipeline cost runaway on user's direct-billing — 200-invocation cap may still cost $50-$200+ if NACK loops + deep tokens; without real-time visibility, intervention is reactive.", - "current_controls_post_spike": [ - "Spike's R1 acceptance for trusted-repo SDLC at single-subagent scale (qualitative)", - "PreToolUse hook for refiner role file-writes (single-role validated)", - "shared/egg_restrictions/patterns.py single-source-of-truth maintained", - "Walking-skeleton scope-fence on plan/implement/pr (NotImplementedError until rollout closes the gap)" - ], - "proposed_controls_rollout": [ - "ADR refresh documents 15-subagent trust-context scaling (R31; feedback Q4 acceptance must be explicit)", - "slice-0 R2 validation produces binary go/no-go outcome (REC1)", - "MCP-validator + R15 model (b) fallback if hooks fail role-routing (R27, cq-6 option 2)", - "Cost cap with per-phase visibility (R23, REC7)", - "SKILL.md scope-of-use notice in install docs", - "Pipeline-end cost reporting (operator-visible spend)", - "Conformance matrix runs include cost telemetry as anomaly-detection signal" - ], - "residual_risk": "MEDIUM — operator-accepted v1 trust model is unchanged from spike; mitigation is documentation and detection, not closure. The structural enforcement gap (R27) under MCP-validator fallback is the highest residual security risk if R2 validation fails. Operator + reviewer_security review required.", - "explicit_non_goals_inherited_from_2623": [ - "Does NOT remove the gateway or k3s substrate (cq-9; both substrates remain).", - "Does NOT solve SendMessage / Agent Teams gating (#2612 stays open).", - "Does NOT introduce a new trust context for malicious-issue defense beyond documentation and scope-of-use guidance." - ], - "explicit_non_goals_this_rollout": [ - "Does NOT migrate ClaudeCodeSpawner to ClaudeCodeAgentToolSpawner (cq-3 defers to post-implement empirical decision; mid-rollout migration is a documented fallback if R19/R29 pressure manifests).", - "Does NOT ship fork-based delegation (cq-10 hybrid is half-implemented; checkpointing only).", - "Does NOT add new reviewer roles (feedback Q5).", - "Does NOT add credential-proxy mitigations at 15-subagent scale (feedback Q4)." - ] - }, - - "compatibility_assessment": { - "breaking_changes": "None on the k3s substrate (cq-7 explicitly defers k3s adapter parity). The claude-code substrate's interfaces are v0.x unstable through the rollout's hardening pass (R32); the marker drops only at rollout close.", - "backward_compatibility": "Full for k3s users (no change). New for claude-code users (no prior state to be compatible with beyond the spike's refiner-only path).", - "forward_compatibility": "Good — abstraction interfaces remain the substrate-extension point. EggHarnessSpawner is the third implementation that exercises the protocol; cq-7 drops the unstable marker after ≥3 roles validate.", - "affected_systems": [ - "orchestrator/substrate/ (15 new role rubrics; bridge driver; expanded run_pipeline_in_process)", - "plugins/egg-sdlc/ (skill expansion: 15 agent rubrics; bridge entry; hooks)", - "shared/egg_restrictions/patterns.py (path additions for bridge artifacts if R34 surfaces)", - "shared/egg_contracts/contract_store.py (concurrent-write semantics under Option (b) bridge)", - "integration_tests/regression/ (substrate-parametrize matrix; 5-issue conformance fixtures)", - ".github/workflows/ (matrix dimension; nightly cadence)", - "docs/architecture/claude-code-substrate.md (ADR refresh per DoD #6)", - "orchestrator/cli.py (egg-orch local-run for EggHarnessSpawner per feedback Q3)" - ], - "deprecation_paths": "k3s substrate stays indefinitely (cq-9). Walking-skeleton scope-fence removed at rollout close (feedback Q6). v0.x interface marker drops at rollout close (cq-7 + R32 sequencing fix). #2261 slice-15 coordination required (R22).", - "issue_dependencies": [ - "#2261 slice-15 — orchestrator/routes/pipelines.py decomposition (R22 conflict surface)", - "#2622 — depth gap structural fix (every new rubric closes for its role; R18)", - "#2474 — runtime-primitive / fixture mismatch class (R27, R30, R34)", - "#2594 — runtime-primitive risks framing (this assessment's organizing principle)", - "#2612 — SendMessage / Agent Teams gating (explicit non-goal; stays open)", - "#2714 — autostash rebase (already merged ff1c4d8; conformance set fixture R21)" - ] - }, - - "open_questions_for_implement_phase": [ - { - "id": "Q1", - "question": "If R2 PreToolUse hook role-routing validation fails in slice-0, should the rollout fall back to MCP-validator-side (cq-6 option 2) AND simultaneously migrate to R15 model (b) per-role .claude/agents/.md definitions, or first land MCP-validator-only and migrate model (b) in a separate slice? The former is one larger slice; the latter is two smaller slices with a brief window of structurally-weaker enforcement between them.", - "default_recommendation": "Combine into one slice. The window of weaker enforcement is real (R27), and splitting the fallback into two PRs creates a documented vulnerability window." - }, - { - "id": "Q2", - "question": "If plan-phase parent-session strain measurement projects unsafely to 8-way implement (R19), should the rollout migrate to ClaudeCodeAgentToolSpawner mid-rollout (cq-3 deferred decision flips early), or cap implement-phase at 3-way serial dispatch and ship the migration as a follow-up?", - "default_recommendation": "Migrate mid-rollout. Capping at 3-way effectively turns implement phase into a serialized k3s-equivalent with no concurrency benefit; the cq-3 deferred decision was always 'decide when empirical numbers exist', and the plan-phase numbers ARE the empirical input." - }, - { - "id": "Q3", - "question": "Is apply-phase (epic-only producer 'applier' + reviewer_contract) in or out of this rollout's DoD?", - "default_recommendation": "Defer to a follow-up; epic-driven pipelines remain k3s-only for v1. Document explicitly in the ADR. Adding applier expands rubric count to 16 and demands an epic conformance fixture, neither of which is in the issue body's enumerated scope." - }, - { - "id": "Q4", - "question": "Should the conformance matrix run against pinned base commits (R21 stability) or against current main (recency)? If pinned, who maintains the snapshots?", - "default_recommendation": "Pinned. Operator-maintained snapshots; the conformance slice ships a snapshot-capture utility. Recency-vs-stability trade-off favors stability for CI gates; per-PR runs can opt into a 'rebase against main' variant for spot-checking." - }, - { - "id": "Q5", - "question": "Should the bridge state-file (Option (b) contract serialization under R30) use the existing contract_store CAS pattern, or introduce a separate bridge-state file (.egg-state/bridge/.json) to isolate concurrent writes?", - "default_recommendation": "Separate bridge-state file. The contract file is already concurrent-write-prone via BRC verdicts and agent_executions; layering bridge state on top compounds the surface. A separate file with its own lock isolates the bridge's failure mode." - } - ], - - "alignment_with_issue_done_criteria": { - "criterion_1_every_agent_role_runs_on_claude_code": "Risk: 15 rubrics must close depth gap structurally (R18) AND apply-phase scope is ambiguous (R33). Mitigation: per-role structural-depth test in each slice; apply-phase HITL resolves at plan-phase.", - "criterion_2_all_four_phases_work_end_to_end": "Risk: bridge dual-architecture (R17) and parent-session strain (R19, R29) are the load-bearing risks; if either fails, phases ship behind a fence (R25). Mitigation: slice-0 R2 + slice-1 bridge end-to-end exercise; plan-phase strain measurement gates implement.", - "criterion_3_conformance_matrix_green_on_claude_code": "Risk: 5-issue selection reproducibility (R21); CI runtime exceeds practical PR budgets (Q2 3h ceiling); pinned-snapshot maintenance burden. Mitigation: pinned base commits, nightly cadence with PR-smoke gate, per-issue matrix-runner script.", - "criterion_4_HITL_via_AskUserQuestion_no_MCP_provide_input": "Risk: bridge gap (R17, R30) is the entire load-bearing surface of this criterion. Mitigation: dual-bridge architecture shipped as parallel deliverables in slice-1.", - "criterion_5_EggHarnessSpawner_local_run": "Risk: third-implementation protocol gaps (R26); interface stability collision (R32). Mitigation: hardening-slice protocol-review pass before implementation; marker drop moved to rollout close.", - "criterion_6_ADR_refresh": "Risk: ADR must capture (a) 15-subagent trust-scaling delta (R31), (b) bridge dual-architecture rationale, (c) R2 validation outcome, (d) parent-session strain measurements, (e) deferred items now-resolved. Mitigation: ADR refresh as final pr-phase deliverable; reviewer_security signs off on trust-context section." - } -} diff --git a/.egg-state/brc-history/2623-plan.json b/.egg-state/brc-history/2623-plan.json deleted file mode 100644 index 079ab41e8a..0000000000 --- a/.egg-state/brc-history/2623-plan.json +++ /dev/null @@ -1,1710 +0,0 @@ -[ - { - "id": "83df3abd-6c2b-41", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-18T23:56:40.261112+00:00", - "phase": "plan" - }, - { - "id": "409ba48d-6d2a-4e", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-18T23:57:40.305969+00:00", - "phase": "plan" - }, - { - "id": "85b88138-33b8-4b", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-18T23:58:40.373348+00:00", - "phase": "plan" - }, - { - "id": "913e6add-c0da-40", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-18T23:59:40.430236+00:00", - "phase": "plan" - }, - { - "id": "dbf7b0a0-5ca6-46", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-19T00:00:40.615733+00:00", - "phase": "plan" - }, - { - "id": "c78e3b08-af81-46", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-19T00:01:40.670099+00:00", - "phase": "plan" - }, - { - "id": "90225d5c-a792-4b", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-19T00:02:40.726128+00:00", - "phase": "plan" - }, - { - "id": "77a3726d-b06b-48", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-19T00:03:57.817385+00:00", - "phase": "plan" - }, - { - "id": "c5746a0a-f902-4f", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T23:56:28.048573+00:00" - }, - "timestamp": "2026-05-19T00:05:20.248650+00:00", - "phase": "plan" - }, - { - "id": "d50d0086-19e4-43", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from task_planner", - "body": "Plan: walking-skeleton Claude Code substrate spike (one slice, eleven tasks). Single slice per cq-11 spike-then-plan. Lands four substrate interfaces (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager), claude-code implementations of each (ClaudeCodeSpawner, InProcessMessageBus, PreToolUseHookPolicy, LocalWorktreeManager), in-process orchestrator boot generator yielding HITLDecision per cq-7, egg-sdlc plugin (TASK-1-7 coder plugin.json + TASK-1-11 documenter SKILL.md/agents) per cq-8, one parametrized regression test in conftest.py + test_brc_single_cycle.py per cq-3, ADR at docs/architecture/claude-code-substrate.md, and reviewer-pasted follow-up issue draft. Existing primitives cited with file:line: KubernetesSpawner orchestrator/kubernetes_spawner.py:249, MessageStore orchestrator/message_store.py:200, ConcurrentPhaseExecutor orchestrator/concurrent_executor.py:114, _spawn_agent orchestrator/concurrent_executor.py:504, HITLDecision orchestrator/models.py:300, build_system_prompt shared/egg_harness/prompt.py:24, build_agent_patterns shared/egg_restrictions/patterns.py:768, WORKTREE_BASE_DIR gateway/worktree_manager.py:49. All eleven cq decisions and six feedback items applied; tasks split by role (coder/tester/documenter); no .github/ writes; tests under integration_tests/regression and shared/tests (existing tester-allowed directories).", - "metadata": { - "payload": { - "summary": "Plan: walking-skeleton Claude Code substrate spike (one slice, eleven tasks). Single slice per cq-11 spike-then-plan. Lands four substrate interfaces (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager), claude-code implementations of each (ClaudeCodeSpawner, InProcessMessageBus, PreToolUseHookPolicy, LocalWorktreeManager), in-process orchestrator boot generator yielding HITLDecision per cq-7, egg-sdlc plugin (TASK-1-7 coder plugin.json + TASK-1-11 documenter SKILL.md/agents) per cq-8, one parametrized regression test in conftest.py + test_brc_single_cycle.py per cq-3, ADR at docs/architecture/claude-code-substrate.md, and reviewer-pasted follow-up issue draft. Existing primitives cited with file:line: KubernetesSpawner orchestrator/kubernetes_spawner.py:249, MessageStore orchestrator/message_store.py:200, ConcurrentPhaseExecutor orchestrator/concurrent_executor.py:114, _spawn_agent orchestrator/concurrent_executor.py:504, HITLDecision orchestrator/models.py:300, build_system_prompt shared/egg_harness/prompt.py:24, build_agent_patterns shared/egg_restrictions/patterns.py:768, WORKTREE_BASE_DIR gateway/worktree_manager.py:49. All eleven cq decisions and six feedback items applied; tasks split by role (coder/tester/documenter); no .github/ writes; tests under integration_tests/regression and shared/tests (existing tester-allowed directories).", - "attestation": {}, - "artifacts": [ - ".egg-state/drafts/2623-plan.md" - ], - "risk_considered": "Single-slice plan over the 1k LOC advisory ceiling (~1.8-2.5k LOC) \u2014 explicitly justified by cq-11 spike-then-plan choice; splitting would re-litigate cq-11. k3s adapter deferred as NotImplementedError TODO marker is a deliberate scope boundary, not silent drift \u2014 surfaced both in tests (TASK-1-8) and the ADR open-work appendix (TASK-1-9, TASK-1-10).", - "commit_sha": "51d4a24bc", - "files_changed": [ - ".egg-state/drafts/2623-plan.md" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 1, - "commit_sha": "51d4a24bc" - }, - "timestamp": "2026-05-19T00:05:21.308202+00:00", - "phase": "plan" - }, - { - "id": "465276a7-10b6-45", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:05:21.450617+00:00", - "phase": "plan" - }, - { - "id": "7572bf86-4bbc-4d", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:05:49.617277+00:00", - "phase": "plan" - }, - { - "id": "fc985eee-f5a1-4c", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from architect", - "body": "Drafted the plan-phase architect output for #2623 (substrate-swap walking skeleton). The analysis honors all 11 HITL resolutions and the operator's spike-then-plan sequencing (cq-11): it proposes an AgentSpawner ABC + ClaudeCodeSpawner concrete impl as the minimum viable abstraction that proves the substrate boundary on one role (refiner) and one phase (refine) end-to-end, with MessageBus / PolicyEnforcer / WorktreeManager / HITLSurface ABCs accompanying it so the interfaces land together. Substrate selection is an EGG_SUBSTRATE env var (default 'k3s' for back-compat) read by a new egg_orchestrator/substrate.py registry. Concrete decisions: ClaudeCodeSpawner uses Claude Code's Agent tool with synchronous call-and-wait (cq-4) and a pre-created LocalWorktreeManager-owned worktree (cq-5, not Claude Code's EnterWorktree); PreToolUseHookPolicy lives at plugins/sdlc/hooks/pretooluse_policy.sh and imports shared/egg_restrictions/patterns.py for single-source-of-truth (cq-6); HeredocHITLSurface yields HITLDecision via a Python generator that the skill renders through AskUserQuestion (cq-7); plugins/sdlc/.claude-plugin/plugin.json declares a pip dep on egg (cq-8). Conformance is verified by parametrizing integration_tests/regression/test_brc_single_cycle.py with a substrate fixture (cq-3); the remaining regression tests are deferred to follow-ups. The analysis surfaces 48 file:line citations covering every cited primitive (concurrent_executor, kubernetes_spawner, message_store/redis_message_store, action_guards INV-1..INV-6, models.Contract schemaVersion 1.1, patterns.AgentFilePattern.can_write, build_system_prompt, MCP check_file_restriction handler, refine-plan plugin manifest precedent) and classifies each on the #2594 execution-context axes (deployed-pod vs trusted-CI-runner vs the newly introduced parent-claude-code-session context). It hands 9 candidate tasks to task_planner (each scoped to \u22641 logical change), a 7-item risk list to risk_analyst (trust-boundary shift, subagent ceiling, context window, isolation='worktree' availability, schemaVersion rebase, RedisMessageStore drift, pip-install pre-flight), and an explicit deferred-scope list (multi-role concurrency, plan/implement/pr phases, PreToolUse Bash interception, EggHarnessSpawner, full regression matrix, k3s deprecation, overseer thread, slice-DAG concurrency, telemetry review). Satisfies the plan-phase architect contract task by providing the architecture analysis + component breakdown that the task_planner and risk_analyst can build on without re-litigating substrate-swap framing or HITL resolutions.", - "metadata": { - "payload": { - "summary": "Drafted the plan-phase architect output for #2623 (substrate-swap walking skeleton). The analysis honors all 11 HITL resolutions and the operator's spike-then-plan sequencing (cq-11): it proposes an AgentSpawner ABC + ClaudeCodeSpawner concrete impl as the minimum viable abstraction that proves the substrate boundary on one role (refiner) and one phase (refine) end-to-end, with MessageBus / PolicyEnforcer / WorktreeManager / HITLSurface ABCs accompanying it so the interfaces land together. Substrate selection is an EGG_SUBSTRATE env var (default 'k3s' for back-compat) read by a new egg_orchestrator/substrate.py registry. Concrete decisions: ClaudeCodeSpawner uses Claude Code's Agent tool with synchronous call-and-wait (cq-4) and a pre-created LocalWorktreeManager-owned worktree (cq-5, not Claude Code's EnterWorktree); PreToolUseHookPolicy lives at plugins/sdlc/hooks/pretooluse_policy.sh and imports shared/egg_restrictions/patterns.py for single-source-of-truth (cq-6); HeredocHITLSurface yields HITLDecision via a Python generator that the skill renders through AskUserQuestion (cq-7); plugins/sdlc/.claude-plugin/plugin.json declares a pip dep on egg (cq-8). Conformance is verified by parametrizing integration_tests/regression/test_brc_single_cycle.py with a substrate fixture (cq-3); the remaining regression tests are deferred to follow-ups. The analysis surfaces 48 file:line citations covering every cited primitive (concurrent_executor, kubernetes_spawner, message_store/redis_message_store, action_guards INV-1..INV-6, models.Contract schemaVersion 1.1, patterns.AgentFilePattern.can_write, build_system_prompt, MCP check_file_restriction handler, refine-plan plugin manifest precedent) and classifies each on the #2594 execution-context axes (deployed-pod vs trusted-CI-runner vs the newly introduced parent-claude-code-session context). It hands 9 candidate tasks to task_planner (each scoped to \u22641 logical change), a 7-item risk list to risk_analyst (trust-boundary shift, subagent ceiling, context window, isolation='worktree' availability, schemaVersion rebase, RedisMessageStore drift, pip-install pre-flight), and an explicit deferred-scope list (multi-role concurrency, plan/implement/pr phases, PreToolUse Bash interception, EggHarnessSpawner, full regression matrix, k3s deprecation, overseer thread, slice-DAG concurrency, telemetry review). Satisfies the plan-phase architect contract task by providing the architecture analysis + component breakdown that the task_planner and risk_analyst can build on without re-litigating substrate-swap framing or HITL resolutions.", - "attestation": {}, - "artifacts": [ - ".egg-state/agent-outputs/2623-architect-output.json" - ], - "risk_considered": "Risks for risk_analyst to formally rate: (1) trust-boundary shift \u2014 PreToolUse hooks run in the user's local Claude Code interpreter, not in a sandbox; the k3s gateway has defense-in-depth (NetworkPolicy + sidecar) which the claude-code substrate trades for hook-correctness + user-install integrity; (2) Agent tool subagent ceiling \u2014 the spike runs N=1 so doesn't bite, but the follow-up parallel-reviewer dispatch may oversubscribe; (3) subagent context window vs egg's max_turns=1000 \u2014 for a substantive issue like #2623 itself, the refiner can approach the limit on the claude-code substrate before cq-10's checkpoint+fork infrastructure lands; (4) isolation='worktree' availability \u2014 design assumes stable CC exposes this; if developer-mode-only, the spawner falls back to caller-managed worktrees (the design's cq-5 path already does this); (5) Contract schemaVersion 1.1 rebases \u2014 any parallel schema bump during the spike's review cycle requires a mechanical rebase; (6) RedisMessageStore drift \u2014 the spike does NOT touch Redis paths, but the MessageBus rename PR could inadvertently regress consumer-group semantics; mitigation is to keep the rename mechanical; (7) pip-install pre-flight \u2014 marketplace install cannot enforce 'pip install egg', so the skill's bootstrap must do an importlib.util.find_spec check and print actionable instructions on miss. Mitigations are documented inline next to each risk in the JSON output; the spike's exit criteria include 'completes on at least one substantive issue without context exhaustion' to validate (3) empirically.", - "commit_sha": "fb9192341b2c78dfd0ac0dad7c5d444733cacf97", - "files_changed": [ - ".egg-state/agent-outputs/2623-architect-output.json" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 1, - "commit_sha": "fb9192341b2c78dfd0ac0dad7c5d444733cacf97" - }, - "timestamp": "2026-05-19T00:06:18.713778+00:00", - "phase": "plan" - }, - { - "id": "3d0318a3-61c2-40", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from risk_analyst", - "body": "Risk assessment for substrate-swap (k3s -> claude-code-native) identifying 16 risks across security, design, performance, and compatibility. Overall risk HIGH driven by (a) credential trust-boundary inversion \u2014 Anthropic API key moves from server-side gateway to user's parent Claude session, exposing it to prompt-injection from subagents (R1); (b) PreToolUse-hook role-routing is an unverified runtime primitive \u2014 the chosen policy enforcement layer (cq-6 option 1) assumes hooks can read EGG_AGENT_ROLE for the calling subagent, which is not documented behavior (R2, #2594 class); (c) spike-then-plan slicing risks freezing the four abstraction interfaces (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager) from a single-role exercise (R10). Five runtime primitives flagged for explicit spike-time verification (Agent worktree isolation, hook role-routing, subagent concurrency ceiling, subagent context budget vs max_turns=1000, custom subagent_type via .claude/agents/). Five trust-boundary shifts documented (credentials, file-write timing, cost/rate-limit, agent liveness, push serialization). Recommendation: PROCEED_WITH_MITIGATIONS with five high-priority requirements \u2014 (1) spike's exit criteria must surface evidence on the five primitives; (2) ADR must explicitly accept the trust-shift, not bury it as implementation detail; (3) plan must classify the 14 regression tests as portable/k3s-only/claude-code-only; (4) consider expanding spike scope from 1 role to 2 (producer + reviewer) to exercise BRC consensus path; (5) EGG_PIPELINE_MAX_AGENT_INVOCATIONS cap for cost-control regression. Five risks (R1, R2, R4, R7, R10) flagged for explicit human review by reviewer_plan and the operator.", - "metadata": { - "payload": { - "summary": "Risk assessment for substrate-swap (k3s -> claude-code-native) identifying 16 risks across security, design, performance, and compatibility. Overall risk HIGH driven by (a) credential trust-boundary inversion \u2014 Anthropic API key moves from server-side gateway to user's parent Claude session, exposing it to prompt-injection from subagents (R1); (b) PreToolUse-hook role-routing is an unverified runtime primitive \u2014 the chosen policy enforcement layer (cq-6 option 1) assumes hooks can read EGG_AGENT_ROLE for the calling subagent, which is not documented behavior (R2, #2594 class); (c) spike-then-plan slicing risks freezing the four abstraction interfaces (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager) from a single-role exercise (R10). Five runtime primitives flagged for explicit spike-time verification (Agent worktree isolation, hook role-routing, subagent concurrency ceiling, subagent context budget vs max_turns=1000, custom subagent_type via .claude/agents/). Five trust-boundary shifts documented (credentials, file-write timing, cost/rate-limit, agent liveness, push serialization). Recommendation: PROCEED_WITH_MITIGATIONS with five high-priority requirements \u2014 (1) spike's exit criteria must surface evidence on the five primitives; (2) ADR must explicitly accept the trust-shift, not bury it as implementation detail; (3) plan must classify the 14 regression tests as portable/k3s-only/claude-code-only; (4) consider expanding spike scope from 1 role to 2 (producer + reviewer) to exercise BRC consensus path; (5) EGG_PIPELINE_MAX_AGENT_INVOCATIONS cap for cost-control regression. Five risks (R1, R2, R4, R7, R10) flagged for explicit human review by reviewer_plan and the operator.", - "attestation": {}, - "artifacts": [ - ".egg-state/agent-outputs/2623-risk_analyst-output.json" - ], - "risk_considered": "16 risks enumerated covering: (R1) credential trust-boundary inversion HIGH; (R2) PreToolUse hook role-routing unverified primitive HIGH; (R3) Agent-tool concurrency ceiling MEDIUM; (R4) in-process orchestrator reentrancy with HITL generator MEDIUM; (R5) shared .git/ contention MEDIUM; (R6) integration_tests/regression/ more substrate-coupled than analysis claims MEDIUM; (R7) subagent context budget vs max_turns=1000 MEDIUM; (R8) plugin pip-install drift MEDIUM; (R9) Anthropic API cost / rate-limit blast radius shifts to user MEDIUM; (R10) spike-then-plan interface-freeze HIGH; (R11) EnterWorktree primitive drift LOW; (R12) BRC heartbeat / stuck-agent detection regression MEDIUM; (R13) PyPI package distribution LOW; (R14) #2612 SendMessage gating stays open LOW; (R15) custom subagent_type vs prompt-injected roles LOW; (R16) cross-substrate test-failure diagnostic shift LOW. Five trust-boundary shifts and five runtime-primitive risks called out per #2594.", - "commit_sha": "363e8d6d3", - "files_changed": [ - ".egg-state/agent-outputs/2623-risk_analyst-output.json" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 1, - "commit_sha": "363e8d6d3" - }, - "timestamp": "2026-05-19T00:06:21.502298+00:00", - "phase": "plan" - }, - { - "id": "6dd06618-fa9d-4f", - "pipeline_id": "issue-2623", - "from_role": "orchestrator", - "to_role": "risk_analyst", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1 - }, - "timestamp": "2026-05-19T00:06:21.502453+00:00", - "phase": "plan" - }, - { - "id": "dcb14343-fc68-46", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:06:30.929609+00:00" - }, - "timestamp": "2026-05-19T00:06:47.536100+00:00", - "phase": "plan" - }, - { - "id": "33902797-5f97-4b", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:06:49.692565+00:00", - "phase": "plan" - }, - { - "id": "2756c5ac-4d01-4e", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:07:23.523495+00:00" - }, - "timestamp": "2026-05-19T00:07:45.674895+00:00", - "phase": "plan" - }, - { - "id": "59563725-6243-4b", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:06:30.929609+00:00" - }, - "timestamp": "2026-05-19T00:07:45.974014+00:00", - "phase": "plan" - }, - { - "id": "3b161a34-7ab4-49", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:08:13.717617+00:00", - "phase": "plan" - }, - { - "id": "b7a696b4-b3e1-4b", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:07:23.523495+00:00" - }, - "timestamp": "2026-05-19T00:08:40.415457+00:00", - "phase": "plan" - }, - { - "id": "11b509ae-1582-46", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:06:30.929609+00:00" - }, - "timestamp": "2026-05-19T00:08:46.037543+00:00", - "phase": "plan" - }, - { - "id": "32c2e17f-241d-40", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:09:04.805813+00:00", - "phase": "plan" - }, - { - "id": "726f49dc-6595-43", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:07:23.523495+00:00" - }, - "timestamp": "2026-05-19T00:09:40.502715+00:00", - "phase": "plan" - }, - { - "id": "7a47dd4c-e4d0-4f", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:06:30.929609+00:00" - }, - "timestamp": "2026-05-19T00:09:46.107834+00:00", - "phase": "plan" - }, - { - "id": "ac8a1088-0764-45", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:10:04.891585+00:00", - "phase": "plan" - }, - { - "id": "d0d68d88-f50b-46", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:07:23.523495+00:00" - }, - "timestamp": "2026-05-19T00:10:40.581221+00:00", - "phase": "plan" - }, - { - "id": "ea44faba-af20-4f", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:06:30.929609+00:00" - }, - "timestamp": "2026-05-19T00:10:46.219275+00:00", - "phase": "plan" - }, - { - "id": "50ce881e-ac97-43", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:11:04.966794+00:00", - "phase": "plan" - }, - { - "id": "e42e28cd-ee19-43", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:07:23.523495+00:00" - }, - "timestamp": "2026-05-19T00:11:40.659338+00:00", - "phase": "plan" - }, - { - "id": "3cd1ffc8-c95f-44", - "pipeline_id": "issue-2623", - "from_role": "orchestrator", - "to_role": "risk_analyst", - "message_type": "OVERSEER_ALERT", - "subject": "BRC confirmation timeout \u2014 call mcp__brc__confirm", - "body": "You are PROPOSED and fully ACKed but have not confirmed in 301s. Call `mcp__brc__confirm` now. If it returns `status='pending_acks'`, read `message` for the guard reason and wait on the prerequisite events instead: `CONSENSUS_PROPOSE` if a producer hasn't proposed (`zero_proposal_producers`), `CONSENSUS_ACK` / `CONSENSUS_RE_REVIEW` if a reviewer's ACK is stale or unresolved. Then retry confirm.", - "metadata": { - "alert_type": "brc_confirmation_timeout", - "elapsed_seconds": 301, - "source": "health_monitor" - }, - "timestamp": "2026-05-19T00:11:42.829964+00:00", - "phase": "plan" - }, - { - "id": "2967aedb-ad19-4c", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:11:44.939853+00:00", - "phase": "plan" - }, - { - "id": "18fd4de7-8b5e-4e", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by risk_analyst", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T00:11:48.938899+00:00", - "phase": "plan" - }, - { - "id": "3254ac30-eeae-45", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:11:57.004124+00:00" - }, - "timestamp": "2026-05-19T00:11:57.056667+00:00", - "phase": "plan" - }, - { - "id": "0b320675-85d3-41", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:12:05.112105+00:00", - "phase": "plan" - }, - { - "id": "c9e7b1c4-a4f3-43", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:07:23.523495+00:00" - }, - "timestamp": "2026-05-19T00:12:40.781667+00:00", - "phase": "plan" - }, - { - "id": "19b0b3f0-ad18-41", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:11:57.004124+00:00" - }, - "timestamp": "2026-05-19T00:12:57.125411+00:00", - "phase": "plan" - }, - { - "id": "7fa237c4-c766-4f", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:05:35.038235+00:00" - }, - "timestamp": "2026-05-19T00:13:05.222574+00:00", - "phase": "plan" - }, - { - "id": "2dca04aa-c3bc-40", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:07:23.523495+00:00" - }, - "timestamp": "2026-05-19T00:13:40.871108+00:00", - "phase": "plan" - }, - { - "id": "09645a63-a19a-49", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "task_planner", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_plan for task_planner", - "body": "\n### Blocking\n\n1. **`.egg-state/drafts/2623-plan.md` \u00a7\"Trust-boundary scope\" + Primitives table \u2014 non-existent fixture cited (\u00a79 primitive-existence audit failure).** The plan cites `local_pipeline/conftest.py:261 gateway_url fixture` as load-bearing evidence that the parametrized substrate test is \"already kubectl-gated through `local_pipeline_stack` (via the parent `integration_tests/conftest.py:78` `EggStack` dataclass + the `local_pipeline/conftest.py:261` `gateway_url` fixture)\". Grep verdict: `ls integration_tests/local_pipeline/conftest.py` \u2192 \"No such file or directory\". The directory was deleted in commit `f7803637d1 test: delete deprecated local_pipeline + squid tests; file follow-up issues`. The trust-boundary doc on this branch is stale, but the plan cannot ride on a deleted file. **Fix:** drop the `local_pipeline/conftest.py:261` reference; rewrite the Trust-Boundary section against the actual fixture layout: `EggStack` at `integration_tests/conftest.py:71`, `orchestrator_url` fixture at `integration_tests/conftest.py:357`, `egg_stack` fixture at `integration_tests/conftest.py:340`.\n\n2. **`.egg-state/drafts/2623-plan.md` Trust-Boundary claim \"test_brc_single_cycle.py is already kubectl-gated through local_pipeline_stack\" \u2014 factually wrong.** Read `integration_tests/regression/test_brc_single_cycle.py`: it uses `single_reviewer_graph` (regression/conftest.py:564), `event_capture` (regression/conftest.py:519), and `filter_events` (regression/conftest.py:552), then drives the orchestrator's BRC Python API directly via `make_tracker` (`integration_tests/regression/_helpers.py:156`) and `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`). The test does not touch kubectl, does not depend on `egg_stack`/`local_pipeline_stack`, and never instantiates a spawner or message bus. Both `substrate=\"k3s\"` and `substrate=\"claude-code\"` parameters would run identical Python code \u2014 the test is not a substrate-distinguishing artifact. **Fix:** pick a test (existing or new) that actually exercises `AgentSpawner.spawn(...)` and/or `MessageBus.add_message/get_messages` so the two substrate parameters can produce divergent execution paths. Without that, TASK-1-8's \"matrix proof\" is vacuous.\n\n3. **Internal contradiction between TASK-1-1 and TASK-1-2 about the k3s adapter \u2014 plan is incoherent on whether k3s still works.** TASK-1-1 acceptance #3 (lines 222-226): \"`select_substrate({})` \u2026 returns the bundle pointing at the k3s implementations *but* the k3s adapters raise `NotImplementedError` from the protocol seam with a clear message naming the follow-up issue \u2014 this is the explicit TODO marker\". TASK-1-2 description (lines 257-266): \"the k3s code path keeps working via the (deferred) k3s adapter raising `NotImplementedError`; this is acceptable because the spike's acceptance is claude-code-substrate-only, **and the regression matrix keeps `EGG_SUBSTRATE=k3s` selecting the existing in-tree `KubernetesSpawner`**\" and TASK-1-2 acceptance #4 (lines 280-283): \"`orchestrator/concurrent_executor.py:504` is patched to dispatch through `select_substrate(...)`; the existing k3s path continues to work unchanged **because the k3s implementation of the protocol is a thin shim around the existing `KubernetesSpawner.spawn_agent_job`**\". The two are mutually exclusive \u2014 either the k3s adapter is a working shim around `spawn_agent_job` (TASK-1-2 description) or it raises `NotImplementedError` (TASK-1-1). If TASK-1-2 patches `concurrent_executor.py:504` (the only call site to `spawn_fn`) to dispatch through a `select_substrate(...)` bundle whose k3s leg raises `NotImplementedError`, every k3s deployment is broken on this PR. **Fix:** pick one model and apply consistently. The architect's tasks_for_task_planner_to_pick_up #1 prescribes the working-shim approach: refactor the existing `create_concurrent_spawn_fn` at `orchestrator/kubernetes_spawner.py:1564` to return an `AgentSpawner`-conforming object \u2014 that path keeps k3s green. If you instead want the `NotImplementedError` k3s leg, do not also rewrite `concurrent_executor.py:504`; leave the k3s path on the direct import for this spike.\n\n4. **AgentResult missing `commit_sha` field \u2192 BRC INV-6 violation, hard correctness regression.** TASK-1-2 acceptance #2 defines `AgentResult` with `stdout, exit_code, duration_seconds, worktree` \u2014 no `commit_sha`. `orchestrator/action_guards.py::validate_invariants` enforces INV-6 (ack_commit_sha consistency) \u2014 the orchestrator must know each agent's resulting commit SHA so reviewers can attach commit-bound ACKs and the consensus tracker can detect commit drift between proposal and ack. The architect's spawner-component design called this out explicitly: \"the spawner runs `git -C rev-parse HEAD` and records the SHA in AgentResult.commit_sha so the orchestrator can attach commit-bound ACKs (action_guards.validate_invariants INV-6 at orchestrator/action_guards.py:753 requires this)\" (architect output, component \"egg_orchestrator/spawners/claude_code_spawner.py\", design.commit_sha_capture). Dropping commit_sha makes the spawner unable to satisfy the existing BRC invariant. **Fix:** add `commit_sha: str | None` to the `AgentResult` dataclass shape in TASK-1-1 and require capture-via-`git rev-parse HEAD` in TASK-1-2's acceptance criteria.\n\n5. **TASK-1-5 cites a non-existent path-escape primitive (\u00a79 primitive-existence audit failure).** Plan line 380-382: \"Safety: `tear_down` resolves the target through `path.resolve().is_relative_to(base.resolve())` so a malicious pipeline ID can't escape the base \u2014 **same defense as `gateway/gateway.py:7903`**.\" Grep verdict: `grep -n 'is_relative_to\\|path.resolve()' gateway/gateway.py` returns zero hits. The defense pattern actually lives at `gateway/worktree_manager.py:1711` (call site) and `gateway/phase_api.py:96-108` (an unrelated phase-api path-resolve). **Fix:** cite `gateway/worktree_manager.py:1711` as the reference implementation. Mis-citing primitives in acceptance criteria pushes the coder into the same hunt I just did \u2014 and risks them inventing a different defense.\n\n6. **TASK-1-3 acceptance #2 cites BRC unit tests in the wrong directory (\u00a79 primitive-existence audit failure).** Plan line 308-311: \"Existing BRC unit tests under **`shared/tests/test_brc_*`** (or wherever they live) pass when run against `InProcessMessageBus`\". Grep verdict: `ls shared/tests/` returns `test_agent_roles_has_contract.py / test_cross_process_lock.py / test_egg_harness / test_egg_health.py / test_egg_restrictions.py / test_egg_restrictions_hints.py / test_github_block_alignment.py / test_impasse_schema.py / test_overseer_advisor.py` \u2014 zero `test_brc_*` files. The BRC unit tests are at `orchestrator/tests/test_brc_*` (15+ files: `test_brc_confirmation_nudge.py`, `test_brc_content_validation.py`, `test_brc_history.py`, `test_brc_nack_iteration.py`, `test_brc_open_nacks_barrier.py`, `test_brc_phase_propagation.py`, plus `test_auto_ack_pure_producers.py` which exercises `PeerConsensusTracker` directly). The \"or wherever they live\" hedge is not adequate \u2014 the tester role needs a precise path-set to parametrize. **Fix:** point TASK-1-3 acceptance #2 at the actual BRC test directory (`orchestrator/tests/test_brc_*.py`) and confirm the tester role is allowed to write there. If not, the cross-substrate BRC-invariant test belongs in `shared/tests/test_in_process_message_bus.py` (which TASK-1-8 already creates) \u2014 say so explicitly instead of vaguely deferring.\n\n7. **TASK-1-6 ignores the orchestrator-reentrancy problem flagged by risk_analyst R4.** TASK-1-6 (lines 387-419) says \"Build `run_pipeline_in_process(issue, role, ...)`\" and \"instantiates `select_substrate(os.environ)`, runs the refine phase with refiner-only\". The orchestrator today is a Flask/waitress HTTP daemon (`orchestrator/cli.py:83-150 cmd_serve` \u2014 verified). It has no in-process API. `ConcurrentPhaseExecutor` (line 114) uses a `ThreadPoolExecutor` for background dispatch; `PeerConsensusTracker` holds its own lock and runs message poll loops. Wrapping all of this in a Python generator that yields `HITLDecision` and resumes via `.send()` is a non-trivial refactor \u2014 the risk_analyst R4 marks this `requires_human_review: true` with `effort: HIGH`. The plan offers neither a refactor task nor a HITL gate, just an acceptance bullet (\"the generator runs the refiner through to completion\"). **Fix:** either (a) split TASK-1-6 into a \"build in-process orchestrator entry point\" task with its own acceptance criteria covering heartbeat-during-HITL, background-thread lifetime, and contract-state synchronization; or (b) register a HITL decision asking the operator to bound the generator/AskUserQuestion model before this slice merges. Calling it a one-line acceptance bullet is hiding hours of refactor work.\n\n8. **No risk-mitigation tasks for the five risk_analyst HIGH-severity / human-review-required items (R1, R2, R4, R7, R10).** The risk_analyst's proposal flagged five risks requiring human review: R1 credential trust-boundary inversion (security, HIGH/CERTAIN), R2 PreToolUse hook role-routing unverified (design, HIGH/HIGH), R4 in-process orchestrator reentrancy (covered above), R7 subagent context budget vs deep-refine (performance, MEDIUM/HIGH), R10 single-role spike freezing interfaces (design, HIGH/MEDIUM). The plan acknowledges none of these as task-level concerns, decisions, or HITL gates. R1 in particular \u2014 moving the Anthropic credential from a gateway-isolated process into the user's parent session \u2014 is a structural threat-model change that the ADR must document and the operator must explicitly accept (per risk_analyst recommendation_rationale and REC2). The plan's TASK-1-9 ADR acceptance does NOT call this out \u2014 bullets only cover the 11 cq decisions, 6 feedback items, and primitives table. **Fix:** add explicit acceptance criteria in TASK-1-9 for each of R1, R2, R7, R10 (\"ADR's 'Trust-context shift' subsection names credential exposure\"; \"ADR documents PreToolUse hook role-routing fallback path to MCP-validator-side enforcement (cq-6 opt-2) if spike evidence is negative\"; \"ADR documents subagent context budget regression and the smaller-budget fallback per cq-10 opt-2\"; \"interfaces marked unstable / v0.x until \u22653 roles exercise them\"). Without this, the spike encodes design decisions that the second wave will need to revisit, expensively.\n\n9. **TASK-1-8 acceptance #2 \u2014 kubectl gating claim does not match the chosen test.** \"the parametrized regression test passes under both substrates locally (kubectl available for k3s; no kubectl needed for claude-code)\". The chosen test (`test_brc_single_cycle.py`) does not require kubectl for any parameter \u2014 see blocking issue #2. **Fix:** if you keep this test as the parametrize target, drop the \"kubectl available for k3s\" half because both parameters run pure-Python. Better: replace the chosen test as in blocking issue #2.\n\n10. **PyPI package name `egg-shared` is invented (TASK-1-7 and TASK-1-11).** Plan line 432 and line 569: \"lists `egg-shared` (or the canonical pip name) as a required dep\". There is no PyPI package called `egg-shared`. The risk_analyst R13 flagged this: the `egg` name is already taken on PyPI, and no published wheel exists for this repo. Picking `egg-shared` without operator authority pre-empts a packaging decision the project hasn't made. **Fix:** either register a HITL decision asking the operator to pick the canonical pip name (and what registry \u2014 PyPI vs git+https), or accept the dep declaration as a TODO placeholder string with a comment, not as a real install instruction. The current language (\"or the canonical pip name\") will produce a coder asking the same question downstream.\n\n11. **Primitive line numbers off \u2014 minor but a pattern.** Plan line 124: \"`integration_tests/conftest.py:78` `EggStack` dataclass\". Grep: `class EggStack` is at `integration_tests/conftest.py:71`, not :78. (line 78 is the `gateway_url: str` field inside the dataclass.) Plan line 121-123 lists `class HITLDecision` at `orchestrator/models.py:300` \u2014 that one's correct. **Fix:** scan the Primitives table for off-by-N line numbers; cite the class definition line, not a field-in-class line.\n\n### Non-blocking\n\n- **Slice-sizing advisory (#2137 size advisory; never NACK):** the plan's own LOC estimate is 1,800\u20132,500 LOC, over the 1k soft target. Per HITL decision-6 opt-2 I will not NACK on size; this is informational only. The plan acknowledges this and ties it to cq-11's spike-shape choice \u2014 that justification holds, but the next reviewer of the implement-phase BRC cycle should know the slice will be expensive to NACK.\n- **Consider expanding spike to 2 roles (risk_analyst REC4).** A single-role spike does not exercise the BRC consensus path (PROPOSE \u2192 ACK), which is the actual structural concern the orchestrator-as-bus model needs to prove. A `refiner + reviewer_refine` pair stays within cq-11's \"walking skeleton\" framing but exercises producer dispatch AND review verdict round-trip. Operator decision; not a blocker.\n- **Subagent type model (R15).** The plan implicitly assumes `subagent_type='general-purpose'` (matching the existing refine-plan skill) but does not commit. Custom `subagent_type` via `.claude/agents/.md` gives structural tool restrictions per role; the existing model relies on prompt discipline + PreToolUse hooks. Picking one and documenting in the ADR would tighten the design.\n- **Cost cap (risk_analyst REC5).** Consider adding an `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` env var with conservative default. Without it, a buggy NACK loop bills directly to the user's Anthropic account \u2014 a real regression from the gateway model.\n- **TASK-1-2 list of files modified should explicitly include `orchestrator/concurrent_executor.py`** (it does \u2014 line 288, good) \u2014 but the acceptance criteria say \"verified by an `assert isinstance(...)` in a unit test\"; consider also a smoke test that `_spawn_agent` dispatches through the bundle, otherwise the contradiction in blocking #3 could land silently.\n- **TASK-1-1 Files list does not include `orchestrator/substrate/k3s_adapter.py`** \u2014 but TASK-1-2 introduces it. If the adapter is the k3s leg of `select_substrate({})`, it logically belongs to TASK-1-1's deliverable surface. Re-organize so the protocol module and the k3s adapter shim ship together, or document that TASK-1-1's k3s leg is a stub and TASK-1-2 replaces it with the shim.\n- **Plan's \"Manual verification\" step 3 (\"HITL surfaces through AskUserQuestion\")** requires a working in-process generator with HITL round-trip \u2014 circular with blocking #7. Resolve that first.\n", - "metadata": { - "payload": { - "reason": "\n### Blocking\n\n1. **`.egg-state/drafts/2623-plan.md` \u00a7\"Trust-boundary scope\" + Primitives table \u2014 non-existent fixture cited (\u00a79 primitive-existence audit failure).** The plan cites `local_pipeline/conftest.py:261 gateway_url fixture` as load-bearing evidence that the parametrized substrate test is \"already kubectl-gated through `local_pipeline_stack` (via the parent `integration_tests/conftest.py:78` `EggStack` dataclass + the `local_pipeline/conftest.py:261` `gateway_url` fixture)\". Grep verdict: `ls integration_tests/local_pipeline/conftest.py` \u2192 \"No such file or directory\". The directory was deleted in commit `f7803637d1 test: delete deprecated local_pipeline + squid tests; file follow-up issues`. The trust-boundary doc on this branch is stale, but the plan cannot ride on a deleted file. **Fix:** drop the `local_pipeline/conftest.py:261` reference; rewrite the Trust-Boundary section against the actual fixture layout: `EggStack` at `integration_tests/conftest.py:71`, `orchestrator_url` fixture at `integration_tests/conftest.py:357`, `egg_stack` fixture at `integration_tests/conftest.py:340`.\n\n2. **`.egg-state/drafts/2623-plan.md` Trust-Boundary claim \"test_brc_single_cycle.py is already kubectl-gated through local_pipeline_stack\" \u2014 factually wrong.** Read `integration_tests/regression/test_brc_single_cycle.py`: it uses `single_reviewer_graph` (regression/conftest.py:564), `event_capture` (regression/conftest.py:519), and `filter_events` (regression/conftest.py:552), then drives the orchestrator's BRC Python API directly via `make_tracker` (`integration_tests/regression/_helpers.py:156`) and `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`). The test does not touch kubectl, does not depend on `egg_stack`/`local_pipeline_stack`, and never instantiates a spawner or message bus. Both `substrate=\"k3s\"` and `substrate=\"claude-code\"` parameters would run identical Python code \u2014 the test is not a substrate-distinguishing artifact. **Fix:** pick a test (existing or new) that actually exercises `AgentSpawner.spawn(...)` and/or `MessageBus.add_message/get_messages` so the two substrate parameters can produce divergent execution paths. Without that, TASK-1-8's \"matrix proof\" is vacuous.\n\n3. **Internal contradiction between TASK-1-1 and TASK-1-2 about the k3s adapter \u2014 plan is incoherent on whether k3s still works.** TASK-1-1 acceptance #3 (lines 222-226): \"`select_substrate({})` \u2026 returns the bundle pointing at the k3s implementations *but* the k3s adapters raise `NotImplementedError` from the protocol seam with a clear message naming the follow-up issue \u2014 this is the explicit TODO marker\". TASK-1-2 description (lines 257-266): \"the k3s code path keeps working via the (deferred) k3s adapter raising `NotImplementedError`; this is acceptable because the spike's acceptance is claude-code-substrate-only, **and the regression matrix keeps `EGG_SUBSTRATE=k3s` selecting the existing in-tree `KubernetesSpawner`**\" and TASK-1-2 acceptance #4 (lines 280-283): \"`orchestrator/concurrent_executor.py:504` is patched to dispatch through `select_substrate(...)`; the existing k3s path continues to work unchanged **because the k3s implementation of the protocol is a thin shim around the existing `KubernetesSpawner.spawn_agent_job`**\". The two are mutually exclusive \u2014 either the k3s adapter is a working shim around `spawn_agent_job` (TASK-1-2 description) or it raises `NotImplementedError` (TASK-1-1). If TASK-1-2 patches `concurrent_executor.py:504` (the only call site to `spawn_fn`) to dispatch through a `select_substrate(...)` bundle whose k3s leg raises `NotImplementedError`, every k3s deployment is broken on this PR. **Fix:** pick one model and apply consistently. The architect's tasks_for_task_planner_to_pick_up #1 prescribes the working-shim approach: refactor the existing `create_concurrent_spawn_fn` at `orchestrator/kubernetes_spawner.py:1564` to return an `AgentSpawner`-conforming object \u2014 that path keeps k3s green. If you instead want the `NotImplementedError` k3s leg, do not also rewrite `concurrent_executor.py:504`; leave the k3s path on the direct import for this spike.\n\n4. **AgentResult missing `commit_sha` field \u2192 BRC INV-6 violation, hard correctness regression.** TASK-1-2 acceptance #2 defines `AgentResult` with `stdout, exit_code, duration_seconds, worktree` \u2014 no `commit_sha`. `orchestrator/action_guards.py::validate_invariants` enforces INV-6 (ack_commit_sha consistency) \u2014 the orchestrator must know each agent's resulting commit SHA so reviewers can attach commit-bound ACKs and the consensus tracker can detect commit drift between proposal and ack. The architect's spawner-component design called this out explicitly: \"the spawner runs `git -C rev-parse HEAD` and records the SHA in AgentResult.commit_sha so the orchestrator can attach commit-bound ACKs (action_guards.validate_invariants INV-6 at orchestrator/action_guards.py:753 requires this)\" (architect output, component \"egg_orchestrator/spawners/claude_code_spawner.py\", design.commit_sha_capture). Dropping commit_sha makes the spawner unable to satisfy the existing BRC invariant. **Fix:** add `commit_sha: str | None` to the `AgentResult` dataclass shape in TASK-1-1 and require capture-via-`git rev-parse HEAD` in TASK-1-2's acceptance criteria.\n\n5. **TASK-1-5 cites a non-existent path-escape primitive (\u00a79 primitive-existence audit failure).** Plan line 380-382: \"Safety: `tear_down` resolves the target through `path.resolve().is_relative_to(base.resolve())` so a malicious pipeline ID can't escape the base \u2014 **same defense as `gateway/gateway.py:7903`**.\" Grep verdict: `grep -n 'is_relative_to\\|path.resolve()' gateway/gateway.py` returns zero hits. The defense pattern actually lives at `gateway/worktree_manager.py:1711` (call site) and `gateway/phase_api.py:96-108` (an unrelated phase-api path-resolve). **Fix:** cite `gateway/worktree_manager.py:1711` as the reference implementation. Mis-citing primitives in acceptance criteria pushes the coder into the same hunt I just did \u2014 and risks them inventing a different defense.\n\n6. **TASK-1-3 acceptance #2 cites BRC unit tests in the wrong directory (\u00a79 primitive-existence audit failure).** Plan line 308-311: \"Existing BRC unit tests under **`shared/tests/test_brc_*`** (or wherever they live) pass when run against `InProcessMessageBus`\". Grep verdict: `ls shared/tests/` returns `test_agent_roles_has_contract.py / test_cross_process_lock.py / test_egg_harness / test_egg_health.py / test_egg_restrictions.py / test_egg_restrictions_hints.py / test_github_block_alignment.py / test_impasse_schema.py / test_overseer_advisor.py` \u2014 zero `test_brc_*` files. The BRC unit tests are at `orchestrator/tests/test_brc_*` (15+ files: `test_brc_confirmation_nudge.py`, `test_brc_content_validation.py`, `test_brc_history.py`, `test_brc_nack_iteration.py`, `test_brc_open_nacks_barrier.py`, `test_brc_phase_propagation.py`, plus `test_auto_ack_pure_producers.py` which exercises `PeerConsensusTracker` directly). The \"or wherever they live\" hedge is not adequate \u2014 the tester role needs a precise path-set to parametrize. **Fix:** point TASK-1-3 acceptance #2 at the actual BRC test directory (`orchestrator/tests/test_brc_*.py`) and confirm the tester role is allowed to write there. If not, the cross-substrate BRC-invariant test belongs in `shared/tests/test_in_process_message_bus.py` (which TASK-1-8 already creates) \u2014 say so explicitly instead of vaguely deferring.\n\n7. **TASK-1-6 ignores the orchestrator-reentrancy problem flagged by risk_analyst R4.** TASK-1-6 (lines 387-419) says \"Build `run_pipeline_in_process(issue, role, ...)`\" and \"instantiates `select_substrate(os.environ)`, runs the refine phase with refiner-only\". The orchestrator today is a Flask/waitress HTTP daemon (`orchestrator/cli.py:83-150 cmd_serve` \u2014 verified). It has no in-process API. `ConcurrentPhaseExecutor` (line 114) uses a `ThreadPoolExecutor` for background dispatch; `PeerConsensusTracker` holds its own lock and runs message poll loops. Wrapping all of this in a Python generator that yields `HITLDecision` and resumes via `.send()` is a non-trivial refactor \u2014 the risk_analyst R4 marks this `requires_human_review: true` with `effort: HIGH`. The plan offers neither a refactor task nor a HITL gate, just an acceptance bullet (\"the generator runs the refiner through to completion\"). **Fix:** either (a) split TASK-1-6 into a \"build in-process orchestrator entry point\" task with its own acceptance criteria covering heartbeat-during-HITL, background-thread lifetime, and contract-state synchronization; or (b) register a HITL decision asking the operator to bound the generator/AskUserQuestion model before this slice merges. Calling it a one-line acceptance bullet is hiding hours of refactor work.\n\n8. **No risk-mitigation tasks for the five risk_analyst HIGH-severity / human-review-required items (R1, R2, R4, R7, R10).** The risk_analyst's proposal flagged five risks requiring human review: R1 credential trust-boundary inversion (security, HIGH/CERTAIN), R2 PreToolUse hook role-routing unverified (design, HIGH/HIGH), R4 in-process orchestrator reentrancy (covered above), R7 subagent context budget vs deep-refine (performance, MEDIUM/HIGH), R10 single-role spike freezing interfaces (design, HIGH/MEDIUM). The plan acknowledges none of these as task-level concerns, decisions, or HITL gates. R1 in particular \u2014 moving the Anthropic credential from a gateway-isolated process into the user's parent session \u2014 is a structural threat-model change that the ADR must document and the operator must explicitly accept (per risk_analyst recommendation_rationale and REC2). The plan's TASK-1-9 ADR acceptance does NOT call this out \u2014 bullets only cover the 11 cq decisions, 6 feedback items, and primitives table. **Fix:** add explicit acceptance criteria in TASK-1-9 for each of R1, R2, R7, R10 (\"ADR's 'Trust-context shift' subsection names credential exposure\"; \"ADR documents PreToolUse hook role-routing fallback path to MCP-validator-side enforcement (cq-6 opt-2) if spike evidence is negative\"; \"ADR documents subagent context budget regression and the smaller-budget fallback per cq-10 opt-2\"; \"interfaces marked unstable / v0.x until \u22653 roles exercise them\"). Without this, the spike encodes design decisions that the second wave will need to revisit, expensively.\n\n9. **TASK-1-8 acceptance #2 \u2014 kubectl gating claim does not match the chosen test.** \"the parametrized regression test passes under both substrates locally (kubectl available for k3s; no kubectl needed for claude-code)\". The chosen test (`test_brc_single_cycle.py`) does not require kubectl for any parameter \u2014 see blocking issue #2. **Fix:** if you keep this test as the parametrize target, drop the \"kubectl available for k3s\" half because both parameters run pure-Python. Better: replace the chosen test as in blocking issue #2.\n\n10. **PyPI package name `egg-shared` is invented (TASK-1-7 and TASK-1-11).** Plan line 432 and line 569: \"lists `egg-shared` (or the canonical pip name) as a required dep\". There is no PyPI package called `egg-shared`. The risk_analyst R13 flagged this: the `egg` name is already taken on PyPI, and no published wheel exists for this repo. Picking `egg-shared` without operator authority pre-empts a packaging decision the project hasn't made. **Fix:** either register a HITL decision asking the operator to pick the canonical pip name (and what registry \u2014 PyPI vs git+https), or accept the dep declaration as a TODO placeholder string with a comment, not as a real install instruction. The current language (\"or the canonical pip name\") will produce a coder asking the same question downstream.\n\n11. **Primitive line numbers off \u2014 minor but a pattern.** Plan line 124: \"`integration_tests/conftest.py:78` `EggStack` dataclass\". Grep: `class EggStack` is at `integration_tests/conftest.py:71`, not :78. (line 78 is the `gateway_url: str` field inside the dataclass.) Plan line 121-123 lists `class HITLDecision` at `orchestrator/models.py:300` \u2014 that one's correct. **Fix:** scan the Primitives table for off-by-N line numbers; cite the class definition line, not a field-in-class line.\n\n### Non-blocking\n\n- **Slice-sizing advisory (#2137 size advisory; never NACK):** the plan's own LOC estimate is 1,800\u20132,500 LOC, over the 1k soft target. Per HITL decision-6 opt-2 I will not NACK on size; this is informational only. The plan acknowledges this and ties it to cq-11's spike-shape choice \u2014 that justification holds, but the next reviewer of the implement-phase BRC cycle should know the slice will be expensive to NACK.\n- **Consider expanding spike to 2 roles (risk_analyst REC4).** A single-role spike does not exercise the BRC consensus path (PROPOSE \u2192 ACK), which is the actual structural concern the orchestrator-as-bus model needs to prove. A `refiner + reviewer_refine` pair stays within cq-11's \"walking skeleton\" framing but exercises producer dispatch AND review verdict round-trip. Operator decision; not a blocker.\n- **Subagent type model (R15).** The plan implicitly assumes `subagent_type='general-purpose'` (matching the existing refine-plan skill) but does not commit. Custom `subagent_type` via `.claude/agents/.md` gives structural tool restrictions per role; the existing model relies on prompt discipline + PreToolUse hooks. Picking one and documenting in the ADR would tighten the design.\n- **Cost cap (risk_analyst REC5).** Consider adding an `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` env var with conservative default. Without it, a buggy NACK loop bills directly to the user's Anthropic account \u2014 a real regression from the gateway model.\n- **TASK-1-2 list of files modified should explicitly include `orchestrator/concurrent_executor.py`** (it does \u2014 line 288, good) \u2014 but the acceptance criteria say \"verified by an `assert isinstance(...)` in a unit test\"; consider also a smoke test that `_spawn_agent` dispatches through the bundle, otherwise the contradiction in blocking #3 could land silently.\n- **TASK-1-1 Files list does not include `orchestrator/substrate/k3s_adapter.py`** \u2014 but TASK-1-2 introduces it. If the adapter is the k3s leg of `select_substrate({})`, it logically belongs to TASK-1-1's deliverable surface. Re-organize so the protocol module and the k3s adapter shim ship together, or document that TASK-1-1's k3s leg is a stub and TASK-1-2 replaces it with the shim.\n- **Plan's \"Manual verification\" step 3 (\"HITL surfaces through AskUserQuestion\")** requires a working in-process generator with HITL round-trip \u2014 circular with blocking #7. Resolve that first.\n", - "artifact_references": [ - ".egg-state/drafts/2623-plan.md", - ".egg-state/drafts/2623-analysis.md", - "orchestrator/concurrent_executor.py", - "orchestrator/kubernetes_spawner.py", - "orchestrator/message_store.py", - "orchestrator/models.py", - "orchestrator/peer_consensus.py", - "orchestrator/tests/test_auto_ack_pure_producers.py", - "shared/egg_harness/prompt.py", - "shared/egg_restrictions/patterns.py", - "gateway/worktree_manager.py", - "gateway/phase_filter.py", - "gateway/gateway.py", - "integration_tests/regression/conftest.py", - "integration_tests/regression/test_brc_single_cycle.py", - "integration_tests/conftest.py", - "docs/architecture/integration-test-trust-boundary.md" - ], - "nack_version": 1 - }, - "reason": "\n### Blocking\n\n1. **`.egg-state/drafts/2623-plan.md` \u00a7\"Trust-boundary scope\" + Primitives table \u2014 non-existent fixture cited (\u00a79 primitive-existence audit failure).** The plan cites `local_pipeline/conftest.py:261 gateway_url fixture` as load-bearing evidence that the parametrized substrate test is \"already kubectl-gated through `local_pipeline_stack` (via the parent `integration_tests/conftest.py:78` `EggStack` dataclass + the `local_pipeline/conftest.py:261` `gateway_url` fixture)\". Grep verdict: `ls integration_tests/local_pipeline/conftest.py` \u2192 \"No such file or directory\". The directory was deleted in commit `f7803637d1 test: delete deprecated local_pipeline + squid tests; file follow-up issues`. The trust-boundary doc on this branch is stale, but the plan cannot ride on a deleted file. **Fix:** drop the `local_pipeline/conftest.py:261` reference; rewrite the Trust-Boundary section against the actual fixture layout: `EggStack` at `integration_tests/conftest.py:71`, `orchestrator_url` fixture at `integration_tests/conftest.py:357`, `egg_stack` fixture at `integration_tests/conftest.py:340`.\n\n2. **`.egg-state/drafts/2623-plan.md` Trust-Boundary claim \"test_brc_single_cycle.py is already kubectl-gated through local_pipeline_stack\" \u2014 factually wrong.** Read `integration_tests/regression/test_brc_single_cycle.py`: it uses `single_reviewer_graph` (regression/conftest.py:564), `event_capture` (regression/conftest.py:519), and `filter_events` (regression/conftest.py:552), then drives the orchestrator's BRC Python API directly via `make_tracker` (`integration_tests/regression/_helpers.py:156`) and `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`). The test does not touch kubectl, does not depend on `egg_stack`/`local_pipeline_stack`, and never instantiates a spawner or message bus. Both `substrate=\"k3s\"` and `substrate=\"claude-code\"` parameters would run identical Python code \u2014 the test is not a substrate-distinguishing artifact. **Fix:** pick a test (existing or new) that actually exercises `AgentSpawner.spawn(...)` and/or `MessageBus.add_message/get_messages` so the two substrate parameters can produce divergent execution paths. Without that, TASK-1-8's \"matrix proof\" is vacuous.\n\n3. **Internal contradiction between TASK-1-1 and TASK-1-2 about the k3s adapter \u2014 plan is incoherent on whether k3s still works.** TASK-1-1 acceptance #3 (lines 222-226): \"`select_substrate({})` \u2026 returns the bundle pointing at the k3s implementations *but* the k3s adapters raise `NotImplementedError` from the protocol seam with a clear message naming the follow-up issue \u2014 this is the explicit TODO marker\". TASK-1-2 description (lines 257-266): \"the k3s code path keeps working via the (deferred) k3s adapter raising `NotImplementedError`; this is acceptable because the spike's acceptance is claude-code-substrate-only, **and the regression matrix keeps `EGG_SUBSTRATE=k3s` selecting the existing in-tree `KubernetesSpawner`**\" and TASK-1-2 acceptance #4 (lines 280-283): \"`orchestrator/concurrent_executor.py:504` is patched to dispatch through `select_substrate(...)`; the existing k3s path continues to work unchanged **because the k3s implementation of the protocol is a thin shim around the existing `KubernetesSpawner.spawn_agent_job`**\". The two are mutually exclusive \u2014 either the k3s adapter is a working shim around `spawn_agent_job` (TASK-1-2 description) or it raises `NotImplementedError` (TASK-1-1). If TASK-1-2 patches `concurrent_executor.py:504` (the only call site to `spawn_fn`) to dispatch through a `select_substrate(...)` bundle whose k3s leg raises `NotImplementedError`, every k3s deployment is broken on this PR. **Fix:** pick one model and apply consistently. The architect's tasks_for_task_planner_to_pick_up #1 prescribes the working-shim approach: refactor the existing `create_concurrent_spawn_fn` at `orchestrator/kubernetes_spawner.py:1564` to return an `AgentSpawner`-conforming object \u2014 that path keeps k3s green. If you instead want the `NotImplementedError` k3s leg, do not also rewrite `concurrent_executor.py:504`; leave the k3s path on the direct import for this spike.\n\n4. **AgentResult missing `commit_sha` field \u2192 BRC INV-6 violation, hard correctness regression.** TASK-1-2 acceptance #2 defines `AgentResult` with `stdout, exit_code, duration_seconds, worktree` \u2014 no `commit_sha`. `orchestrator/action_guards.py::validate_invariants` enforces INV-6 (ack_commit_sha consistency) \u2014 the orchestrator must know each agent's resulting commit SHA so reviewers can attach commit-bound ACKs and the consensus tracker can detect commit drift between proposal and ack. The architect's spawner-component design called this out explicitly: \"the spawner runs `git -C rev-parse HEAD` and records the SHA in AgentResult.commit_sha so the orchestrator can attach commit-bound ACKs (action_guards.validate_invariants INV-6 at orchestrator/action_guards.py:753 requires this)\" (architect output, component \"egg_orchestrator/spawners/claude_code_spawner.py\", design.commit_sha_capture). Dropping commit_sha makes the spawner unable to satisfy the existing BRC invariant. **Fix:** add `commit_sha: str | None` to the `AgentResult` dataclass shape in TASK-1-1 and require capture-via-`git rev-parse HEAD` in TASK-1-2's acceptance criteria.\n\n5. **TASK-1-5 cites a non-existent path-escape primitive (\u00a79 primitive-existence audit failure).** Plan line 380-382: \"Safety: `tear_down` resolves the target through `path.resolve().is_relative_to(base.resolve())` so a malicious pipeline ID can't escape the base \u2014 **same defense as `gateway/gateway.py:7903`**.\" Grep verdict: `grep -n 'is_relative_to\\|path.resolve()' gateway/gateway.py` returns zero hits. The defense pattern actually lives at `gateway/worktree_manager.py:1711` (call site) and `gateway/phase_api.py:96-108` (an unrelated phase-api path-resolve). **Fix:** cite `gateway/worktree_manager.py:1711` as the reference implementation. Mis-citing primitives in acceptance criteria pushes the coder into the same hunt I just did \u2014 and risks them inventing a different defense.\n\n6. **TASK-1-3 acceptance #2 cites BRC unit tests in the wrong directory (\u00a79 primitive-existence audit failure).** Plan line 308-311: \"Existing BRC unit tests under **`shared/tests/test_brc_*`** (or wherever they live) pass when run against `InProcessMessageBus`\". Grep verdict: `ls shared/tests/` returns `test_agent_roles_has_contract.py / test_cross_process_lock.py / test_egg_harness / test_egg_health.py / test_egg_restrictions.py / test_egg_restrictions_hints.py / test_github_block_alignment.py / test_impasse_schema.py / test_overseer_advisor.py` \u2014 zero `test_brc_*` files. The BRC unit tests are at `orchestrator/tests/test_brc_*` (15+ files: `test_brc_confirmation_nudge.py`, `test_brc_content_validation.py`, `test_brc_history.py`, `test_brc_nack_iteration.py`, `test_brc_open_nacks_barrier.py`, `test_brc_phase_propagation.py`, plus `test_auto_ack_pure_producers.py` which exercises `PeerConsensusTracker` directly). The \"or wherever they live\" hedge is not adequate \u2014 the tester role needs a precise path-set to parametrize. **Fix:** point TASK-1-3 acceptance #2 at the actual BRC test directory (`orchestrator/tests/test_brc_*.py`) and confirm the tester role is allowed to write there. If not, the cross-substrate BRC-invariant test belongs in `shared/tests/test_in_process_message_bus.py` (which TASK-1-8 already creates) \u2014 say so explicitly instead of vaguely deferring.\n\n7. **TASK-1-6 ignores the orchestrator-reentrancy problem flagged by risk_analyst R4.** TASK-1-6 (lines 387-419) says \"Build `run_pipeline_in_process(issue, role, ...)`\" and \"instantiates `select_substrate(os.environ)`, runs the refine phase with refiner-only\". The orchestrator today is a Flask/waitress HTTP daemon (`orchestrator/cli.py:83-150 cmd_serve` \u2014 verified). It has no in-process API. `ConcurrentPhaseExecutor` (line 114) uses a `ThreadPoolExecutor` for background dispatch; `PeerConsensusTracker` holds its own lock and runs message poll loops. Wrapping all of this in a Python generator that yields `HITLDecision` and resumes via `.send()` is a non-trivial refactor \u2014 the risk_analyst R4 marks this `requires_human_review: true` with `effort: HIGH`. The plan offers neither a refactor task nor a HITL gate, just an acceptance bullet (\"the generator runs the refiner through to completion\"). **Fix:** either (a) split TASK-1-6 into a \"build in-process orchestrator entry point\" task with its own acceptance criteria covering heartbeat-during-HITL, background-thread lifetime, and contract-state synchronization; or (b) register a HITL decision asking the operator to bound the generator/AskUserQuestion model before this slice merges. Calling it a one-line acceptance bullet is hiding hours of refactor work.\n\n8. **No risk-mitigation tasks for the five risk_analyst HIGH-severity / human-review-required items (R1, R2, R4, R7, R10).** The risk_analyst's proposal flagged five risks requiring human review: R1 credential trust-boundary inversion (security, HIGH/CERTAIN), R2 PreToolUse hook role-routing unverified (design, HIGH/HIGH), R4 in-process orchestrator reentrancy (covered above), R7 subagent context budget vs deep-refine (performance, MEDIUM/HIGH), R10 single-role spike freezing interfaces (design, HIGH/MEDIUM). The plan acknowledges none of these as task-level concerns, decisions, or HITL gates. R1 in particular \u2014 moving the Anthropic credential from a gateway-isolated process into the user's parent session \u2014 is a structural threat-model change that the ADR must document and the operator must explicitly accept (per risk_analyst recommendation_rationale and REC2). The plan's TASK-1-9 ADR acceptance does NOT call this out \u2014 bullets only cover the 11 cq decisions, 6 feedback items, and primitives table. **Fix:** add explicit acceptance criteria in TASK-1-9 for each of R1, R2, R7, R10 (\"ADR's 'Trust-context shift' subsection names credential exposure\"; \"ADR documents PreToolUse hook role-routing fallback path to MCP-validator-side enforcement (cq-6 opt-2) if spike evidence is negative\"; \"ADR documents subagent context budget regression and the smaller-budget fallback per cq-10 opt-2\"; \"interfaces marked unstable / v0.x until \u22653 roles exercise them\"). Without this, the spike encodes design decisions that the second wave will need to revisit, expensively.\n\n9. **TASK-1-8 acceptance #2 \u2014 kubectl gating claim does not match the chosen test.** \"the parametrized regression test passes under both substrates locally (kubectl available for k3s; no kubectl needed for claude-code)\". The chosen test (`test_brc_single_cycle.py`) does not require kubectl for any parameter \u2014 see blocking issue #2. **Fix:** if you keep this test as the parametrize target, drop the \"kubectl available for k3s\" half because both parameters run pure-Python. Better: replace the chosen test as in blocking issue #2.\n\n10. **PyPI package name `egg-shared` is invented (TASK-1-7 and TASK-1-11).** Plan line 432 and line 569: \"lists `egg-shared` (or the canonical pip name) as a required dep\". There is no PyPI package called `egg-shared`. The risk_analyst R13 flagged this: the `egg` name is already taken on PyPI, and no published wheel exists for this repo. Picking `egg-shared` without operator authority pre-empts a packaging decision the project hasn't made. **Fix:** either register a HITL decision asking the operator to pick the canonical pip name (and what registry \u2014 PyPI vs git+https), or accept the dep declaration as a TODO placeholder string with a comment, not as a real install instruction. The current language (\"or the canonical pip name\") will produce a coder asking the same question downstream.\n\n11. **Primitive line numbers off \u2014 minor but a pattern.** Plan line 124: \"`integration_tests/conftest.py:78` `EggStack` dataclass\". Grep: `class EggStack` is at `integration_tests/conftest.py:71`, not :78. (line 78 is the `gateway_url: str` field inside the dataclass.) Plan line 121-123 lists `class HITLDecision` at `orchestrator/models.py:300` \u2014 that one's correct. **Fix:** scan the Primitives table for off-by-N line numbers; cite the class definition line, not a field-in-class line.\n\n### Non-blocking\n\n- **Slice-sizing advisory (#2137 size advisory; never NACK):** the plan's own LOC estimate is 1,800\u20132,500 LOC, over the 1k soft target. Per HITL decision-6 opt-2 I will not NACK on size; this is informational only. The plan acknowledges this and ties it to cq-11's spike-shape choice \u2014 that justification holds, but the next reviewer of the implement-phase BRC cycle should know the slice will be expensive to NACK.\n- **Consider expanding spike to 2 roles (risk_analyst REC4).** A single-role spike does not exercise the BRC consensus path (PROPOSE \u2192 ACK), which is the actual structural concern the orchestrator-as-bus model needs to prove. A `refiner + reviewer_refine` pair stays within cq-11's \"walking skeleton\" framing but exercises producer dispatch AND review verdict round-trip. Operator decision; not a blocker.\n- **Subagent type model (R15).** The plan implicitly assumes `subagent_type='general-purpose'` (matching the existing refine-plan skill) but does not commit. Custom `subagent_type` via `.claude/agents/.md` gives structural tool restrictions per role; the existing model relies on prompt discipline + PreToolUse hooks. Picking one and documenting in the ADR would tighten the design.\n- **Cost cap (risk_analyst REC5).** Consider adding an `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` env var with conservative default. Without it, a buggy NACK loop bills directly to the user's Anthropic account \u2014 a real regression from the gateway model.\n- **TASK-1-2 list of files modified should explicitly include `orchestrator/concurrent_executor.py`** (it does \u2014 line 288, good) \u2014 but the acceptance criteria say \"verified by an `assert isinstance(...)` in a unit test\"; consider also a smoke test that `_spawn_agent` dispatches through the bundle, otherwise the contradiction in blocking #3 could land silently.\n- **TASK-1-1 Files list does not include `orchestrator/substrate/k3s_adapter.py`** \u2014 but TASK-1-2 introduces it. If the adapter is the k3s leg of `select_substrate({})`, it logically belongs to TASK-1-1's deliverable surface. Re-organize so the protocol module and the k3s adapter shim ship together, or document that TASK-1-1's k3s leg is a stub and TASK-1-2 replaces it with the shim.\n- **Plan's \"Manual verification\" step 3 (\"HITL surfaces through AskUserQuestion\")** requires a working in-process generator with HITL round-trip \u2014 circular with blocking #7. Resolve that first.\n", - "revision_count": 1 - }, - "timestamp": "2026-05-19T00:13:51.381971+00:00", - "phase": "plan" - }, - { - "id": "412c2595-b714-49", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:13:51.496249+00:00", - "phase": "plan" - }, - { - "id": "2f9c97d9-94e3-43", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "architect", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_plan for architect", - "body": "\nReviewed the architect's substrate-swap walking-skeleton analysis at `.egg-state/agent-outputs/2623-architect-output.json` (360 lines, 9 components, 12 primitives audited, 9 candidate tasks for task_planner, 7 risks delegated to risk_analyst, 48-entry evidence_citations_index). Spot-checked every primitive citation against the actual codebase:\n\n- `orchestrator/concurrent_executor.py:52` SpawnFn type alias \u2014 confirmed (line 52, `SpawnFn = Callable[..., Any]`).\n- `orchestrator/concurrent_executor.py:114` ConcurrentPhaseExecutor class def \u2014 confirmed (the architect's :137 cites the constructor; both fit).\n- `orchestrator/concurrent_executor.py:469` ThreadPoolExecutor dispatch \u2014 confirmed (`with ThreadPoolExecutor(max_workers=self.max_concurrent) as pool:`).\n- `orchestrator/concurrent_executor.py:504 _spawn_agent` \u2014 confirmed.\n- `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn` \u2014 confirmed.\n- `orchestrator/message_store.py:200 in-memory MessageStore`, :531 `get_message_store`, :552 `EGG_MESSAGE_STORE_BACKEND` \u2014 all confirmed.\n- `orchestrator/redis_message_store.py:107 RedisMessageStore` \u2014 confirmed.\n- `orchestrator/models.py:1106 add_decision` / `:1130 resolve_decision` \u2014 confirmed.\n- `gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` \u2014 confirmed. Architect calls this out as \"second definition\" (parallel to `orchestrator/kubernetes_spawner.py:65`); both exist.\n- `gateway/agent_restrictions.py:58 partition_files_by_role` \u2014 confirmed.\n- `gateway/phase_filter.py:1036 check_phase_file_restrictions`, `:1061 check_agent_restrictions` \u2014 both confirmed.\n- `shared/egg_restrictions/patterns.py:57 AgentFilePattern.can_write`, `:768 build_agent_patterns` \u2014 confirmed. Note: `AgentFilePattern` class is defined at line 44; the architect's `:57` refers to the `can_write` method within it. Both are accurate.\n- `shared/egg_harness/prompt.py:24 build_system_prompt` \u2014 confirmed.\n\nThe component-by-component breakdown correctly slots each interface into the cq-N decisions (cq-1 \u2192 substrate registry; cq-4 \u2192 synchronous spawn; cq-5 \u2192 port WORKTREE_BASE_DIR; cq-6 \u2192 PreToolUse hook; cq-7 \u2192 heredoc HITL; cq-8 \u2192 plugin.json pip dep; cq-11 \u2192 spike scope). The `execution_context_and_primitive_audit` section correctly handles the new `parent-claude-code-session` context as a first-class entry alongside `in-sandbox-agent` / `trusted-CI-runner` / `human-operator`. The `scope_explicitly_in_spike` vs `scope_explicitly_deferred_to_followups` split is exactly the bookkeeping cq-11 needs.\n\nThe architect correctly identified the spawner's `commit_sha` requirement against INV-6 (`orchestrator/action_guards.py:753`) \u2014 a structural detail the task_planner subsequently dropped from `AgentResult`, which I'm NACKing the task_planner for (my blocking #4 there). The architect's design here is sound.\n\nForest-violation check: this proposal does not modify the contract's slice DAG (architect output is analysis, not contract surgery). No forest violation.\n\n### Non-blocking\n\n- **Component \"egg_orchestrator/policy/pretooluse_hook.py\" assumes the hook can resolve calling-subagent role at hook-execution time.** The risk_analyst's R2 marks this as an unverified runtime primitive. Recommend the architect's follow-up note (or the ADR) explicitly state the fallback path: if PreToolUse hooks cannot distinguish the calling subagent's role at runtime, the policy enforcer falls back to MCP-validator-side enforcement (cq-6 option 2). Otherwise the spike commits to a primitive that may not exist in the form assumed.\n- **Component \"plugins/sdlc/\" agents/ design (line 127)** says \"Empty in the spike \u2014 the orchestrator uses build_system_prompt(sources) to assemble per-role prompts dynamically rather than per-role markdown files.\" This is a deliberate choice (model-(a) prompt-injected, per risk_analyst R15). The trade-off vs. custom `subagent_type` (model-(b) structural tool restrictions) is real and should be a named decision in the ADR rather than implicit in the file layout.\n- **Recommended approach line 17 \u2014 \"regression suite is already substrate-portable in its mocking layout\"** somewhat overstates the case. The risk_analyst R6 flags that 12/14 regression tests are BRC-tracker-level (substrate-agnostic) but several still implicitly depend on gateway primitives or k3s metadata. The architect's \"adding `pytest.mark.parametrize('substrate', [...])` is a CI-matrix change, not a test-rewrite\" is the optimistic framing; the realistic framing per R6 REC3 is \"classify each test as portable / k3s-only / claude-code-only first.\" Not blocking the architect since this lands as task_planner / tester work, but worth aligning in any rewrite.\n- **`evidence_citations_index` is excellent (48 entries, file:line each) \u2014 this format should be cargo-culted to future architect outputs.**\n- **`questions_for_followup_iterations` (3 items)** are well-scoped; none rise to plan-phase HITL gates. Operator may want to register Q3 (`EGG_SUBSTRATE` default) as a contract decision once the spike merges and before the follow-up issue starts.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/agent-outputs/2623-architect-output.json", - "orchestrator/concurrent_executor.py", - "orchestrator/kubernetes_spawner.py", - "orchestrator/message_store.py", - "orchestrator/models.py", - "orchestrator/action_guards.py", - "shared/egg_harness/prompt.py", - "shared/egg_restrictions/patterns.py", - "gateway/worktree_manager.py", - "gateway/phase_filter.py", - "gateway/agent_restrictions.py" - ], - "reason": "\nReviewed the architect's substrate-swap walking-skeleton analysis at `.egg-state/agent-outputs/2623-architect-output.json` (360 lines, 9 components, 12 primitives audited, 9 candidate tasks for task_planner, 7 risks delegated to risk_analyst, 48-entry evidence_citations_index). Spot-checked every primitive citation against the actual codebase:\n\n- `orchestrator/concurrent_executor.py:52` SpawnFn type alias \u2014 confirmed (line 52, `SpawnFn = Callable[..., Any]`).\n- `orchestrator/concurrent_executor.py:114` ConcurrentPhaseExecutor class def \u2014 confirmed (the architect's :137 cites the constructor; both fit).\n- `orchestrator/concurrent_executor.py:469` ThreadPoolExecutor dispatch \u2014 confirmed (`with ThreadPoolExecutor(max_workers=self.max_concurrent) as pool:`).\n- `orchestrator/concurrent_executor.py:504 _spawn_agent` \u2014 confirmed.\n- `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn` \u2014 confirmed.\n- `orchestrator/message_store.py:200 in-memory MessageStore`, :531 `get_message_store`, :552 `EGG_MESSAGE_STORE_BACKEND` \u2014 all confirmed.\n- `orchestrator/redis_message_store.py:107 RedisMessageStore` \u2014 confirmed.\n- `orchestrator/models.py:1106 add_decision` / `:1130 resolve_decision` \u2014 confirmed.\n- `gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` \u2014 confirmed. Architect calls this out as \"second definition\" (parallel to `orchestrator/kubernetes_spawner.py:65`); both exist.\n- `gateway/agent_restrictions.py:58 partition_files_by_role` \u2014 confirmed.\n- `gateway/phase_filter.py:1036 check_phase_file_restrictions`, `:1061 check_agent_restrictions` \u2014 both confirmed.\n- `shared/egg_restrictions/patterns.py:57 AgentFilePattern.can_write`, `:768 build_agent_patterns` \u2014 confirmed. Note: `AgentFilePattern` class is defined at line 44; the architect's `:57` refers to the `can_write` method within it. Both are accurate.\n- `shared/egg_harness/prompt.py:24 build_system_prompt` \u2014 confirmed.\n\nThe component-by-component breakdown correctly slots each interface into the cq-N decisions (cq-1 \u2192 substrate registry; cq-4 \u2192 synchronous spawn; cq-5 \u2192 port WORKTREE_BASE_DIR; cq-6 \u2192 PreToolUse hook; cq-7 \u2192 heredoc HITL; cq-8 \u2192 plugin.json pip dep; cq-11 \u2192 spike scope). The `execution_context_and_primitive_audit` section correctly handles the new `parent-claude-code-session` context as a first-class entry alongside `in-sandbox-agent` / `trusted-CI-runner` / `human-operator`. The `scope_explicitly_in_spike` vs `scope_explicitly_deferred_to_followups` split is exactly the bookkeeping cq-11 needs.\n\nThe architect correctly identified the spawner's `commit_sha` requirement against INV-6 (`orchestrator/action_guards.py:753`) \u2014 a structural detail the task_planner subsequently dropped from `AgentResult`, which I'm NACKing the task_planner for (my blocking #4 there). The architect's design here is sound.\n\nForest-violation check: this proposal does not modify the contract's slice DAG (architect output is analysis, not contract surgery). No forest violation.\n\n### Non-blocking\n\n- **Component \"egg_orchestrator/policy/pretooluse_hook.py\" assumes the hook can resolve calling-subagent role at hook-execution time.** The risk_analyst's R2 marks this as an unverified runtime primitive. Recommend the architect's follow-up note (or the ADR) explicitly state the fallback path: if PreToolUse hooks cannot distinguish the calling subagent's role at runtime, the policy enforcer falls back to MCP-validator-side enforcement (cq-6 option 2). Otherwise the spike commits to a primitive that may not exist in the form assumed.\n- **Component \"plugins/sdlc/\" agents/ design (line 127)** says \"Empty in the spike \u2014 the orchestrator uses build_system_prompt(sources) to assemble per-role prompts dynamically rather than per-role markdown files.\" This is a deliberate choice (model-(a) prompt-injected, per risk_analyst R15). The trade-off vs. custom `subagent_type` (model-(b) structural tool restrictions) is real and should be a named decision in the ADR rather than implicit in the file layout.\n- **Recommended approach line 17 \u2014 \"regression suite is already substrate-portable in its mocking layout\"** somewhat overstates the case. The risk_analyst R6 flags that 12/14 regression tests are BRC-tracker-level (substrate-agnostic) but several still implicitly depend on gateway primitives or k3s metadata. The architect's \"adding `pytest.mark.parametrize('substrate', [...])` is a CI-matrix change, not a test-rewrite\" is the optimistic framing; the realistic framing per R6 REC3 is \"classify each test as portable / k3s-only / claude-code-only first.\" Not blocking the architect since this lands as task_planner / tester work, but worth aligning in any rewrite.\n- **`evidence_citations_index` is excellent (48 entries, file:line each) \u2014 this format should be cargo-culted to future architect outputs.**\n- **`questions_for_followup_iterations` (3 items)** are well-scoped; none rise to plan-phase HITL gates. Operator may want to register Q3 (`EGG_SUBSTRATE` default) as a contract decision once the spike merges and before the follow-up issue starts.\n", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-19T00:13:51.538236+00:00", - "phase": "plan" - }, - { - "id": "8d9c3460-2cea-4d", - "pipeline_id": "issue-2623", - "from_role": "orchestrator", - "to_role": "architect", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1 - }, - "timestamp": "2026-05-19T00:13:51.538296+00:00", - "phase": "plan" - }, - { - "id": "f0547a66-c9e1-46", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:13:51.592960+00:00", - "phase": "plan" - }, - { - "id": "3de219a8-9c20-49", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "risk_analyst", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_plan for risk_analyst", - "body": "\nReviewed the risk_analyst's 572-line output covering 16 risks across security/design/performance/compatibility, 5 runtime primitives flagged for spike-time verification, 5 trust-boundary shifts named with mitigations, 7 implementation recommendations, 4 open questions for plan phase. Cross-referenced against the issue body's constraints (`SendMessage` gating, subagent context windows, HITL-through-parent-session, concurrency ceiling, install footprint) and the architect's parallel proposal.\n\nThe structural risks are correctly identified and graded:\n\n- **R1 (credential trust-boundary inversion, HIGH/CERTAIN)** \u2014 confirmed by reading `gateway/anthropic_credentials.py` plus the analysis's section \"Gateway is an HTTP sidecar with portable policy logic\" (line 53-57). The gateway's `anthropic_credentials.py` does in fact inject server-side; the sandbox sees a placeholder. Moving the orchestrator in-process to a Claude Code session moves the credential into the parent's process, and any subagent inherits access. This is a real structural threat-model change that the issue body's \"credentials are inherited from the user's session \u2014 no injection needed\" framing understates. R1's mitigation strategy (document trust-context shift; scope use to trusted repos; consider opt-in proxy fallback) is the right shape \u2014 risk_analyst correctly flags this for human review (operator must explicitly accept the new boundary).\n\n- **R2 (PreToolUse hook role-routing unverified, HIGH/HIGH)** \u2014 exactly the #2594 failure class. Plan cannot rely on a primitive whose role-awareness is unverified. The fallback path (cq-6 option 2 MCP-validator-side enforcement) is real \u2014 `sandbox/egg_agent_tools/handlers/restrictions.py:70 check_file_restriction` already wraps `shared/egg_restrictions/patterns.py` and runs in-sandbox today. The recommendation that the spike's exit criteria require a 2-subagent worked example before the abstraction freezes is structurally correct.\n\n- **R4 (in-process orchestrator reentrancy, MEDIUM/HIGH)** \u2014 confirmed by reading `orchestrator/concurrent_executor.py`: `ConcurrentPhaseExecutor` (line 114) holds a `ThreadPoolExecutor` (line 469); `PeerConsensusTracker` runs its own lock; heartbeat / message-poll loops run as background threads. The generator-yields-HITLDecision model demands that the orchestrator pause cleanly while preserving these threads. The risk_analyst's mitigation (\"spike must exercise HITL round-trip end-to-end\"; \"pause heartbeat timers during HITL or extend dramatically\") is the right bound to put on the spike.\n\n- **R7 (subagent context budget vs deep-refine, MEDIUM/HIGH)** \u2014 correctly distinguishes k3s-substrate checkpoint primitives (sandbox restart with full prompt rebuild) from Claude Code subagent re-spawn (orchestrator must re-summarize and re-feed). The honest assessment (\"some refines will fit, some won't; the mitigation is acknowledging the gap\") is more useful than a false promise.\n\n- **R10 (single-role spike freezing interfaces, HIGH/MEDIUM)** \u2014 the strongest structural critique of cq-11 itself. A single-role spike doesn't exercise the orchestrator-as-bus model (BRC's PROPOSE\u2192ACK round-trip). The recommendation to expand to 2 roles (refiner + reviewer_refine) is a defensible counter-proposal to cq-11 that the plan reviewer should surface; risk_analyst correctly notes it requires human review.\n\nThe five trust-boundary shifts (credential isolation, file-write enforcement timing, cost/rate-limit control, agent liveness, push serialization) are the right list and each has a concrete mitigation. The `rollback_plan` is realistic (env-var flip back to `EGG_SUBSTRATE=k3s`, no data migration needed because contracts are filesystem-native).\n\nThe 7 `implementation_recommendations` (REC1\u2013REC7) and 4 `open_questions_for_plan_phase` correctly target structural items that should land in the plan's tasks or HITL gates. None of these recommendations exceed risk_analyst's scope (they're properly framed as \"the planner should\u2026\" not \"I will\u2026\").\n\nThe output is well-structured for downstream consumption: every risk has `id`, `category`, `severity`, `likelihood`, `impact`, `description`, `affected_files`, `mitigation` with `strategy`/`effort`/`residual_risk`, and `requires_human_review` flag. This is the schema the plan reviewer needs to cross-reference against the planner's task list.\n\n### Non-blocking\n\n- **R8 (pip install drift) recommendation #5 has a JSON shape glitch (line 475-477)** \u2014 the entry uses `\"id\": \"MEDIUM\"` and then a comment field `\"id_override_note\": \"REC5\"`. This is a serialization mistake; the actual `id` should be `\"REC5\"`. Fix in any re-propose. Not blocking \u2014 content is clear, just self-consistent IDs would help downstream consumers.\n- **R3 (subagent concurrency ceiling) \"MEDIUM/MEDIUM\"** could be argued up to MEDIUM/HIGH given egg's `ThreadPoolExecutor(max_workers=6)` is hard-coded for k3s and the implement-phase slice-DAG fans out to 8+ agents per the issue body. Not blocking \u2014 the mitigation (per-substrate `max_workers` override + semaphore) is sound at either grading.\n- **REC4 (expand spike to 2 roles) directly contradicts cq-11's HITL resolution** (\"Spike then plan: a single 'walking skeleton' slice that gets one role (refiner) running through one substrate (claude-code) end-to-end\"). The risk_analyst correctly flags this as a recommendation, not a verdict. Plan reviewer's call on whether to surface as a re-open of cq-11 to the operator or accept the single-role spike as-is. I lean toward surfacing \u2014 the consensus path is the actually-novel surface \u2014 but cq-11 is a resolved HITL and the planner cannot unilaterally reopen.\n- **R14 (SendMessage / Agent Teams gating)** is correctly bounded as known-known with the right mitigation (don't let scope creep absorb #2612). No action.\n- **`alignment_with_issue_done_criteria`** is a nice closing section; consider including this shape in future risk_analyst outputs as a contract-vs-risk cross-check.\n- **`performance_assessment.note`** (\"'slower than k3s in a datacenter' may still be 'faster than nothing' from the solo-developer perspective\") is the kind of framing the operator needs and was missing from the refine analysis. Good to surface.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/agent-outputs/2623-risk_analyst-output.json", - ".egg-state/drafts/2623-analysis.md", - "docs/architecture/integration-test-trust-boundary.md", - "orchestrator/concurrent_executor.py", - "gateway/anthropic_credentials.py", - "shared/egg_restrictions/patterns.py", - "shared/egg_harness/prompt.py" - ], - "reason": "\nReviewed the risk_analyst's 572-line output covering 16 risks across security/design/performance/compatibility, 5 runtime primitives flagged for spike-time verification, 5 trust-boundary shifts named with mitigations, 7 implementation recommendations, 4 open questions for plan phase. Cross-referenced against the issue body's constraints (`SendMessage` gating, subagent context windows, HITL-through-parent-session, concurrency ceiling, install footprint) and the architect's parallel proposal.\n\nThe structural risks are correctly identified and graded:\n\n- **R1 (credential trust-boundary inversion, HIGH/CERTAIN)** \u2014 confirmed by reading `gateway/anthropic_credentials.py` plus the analysis's section \"Gateway is an HTTP sidecar with portable policy logic\" (line 53-57). The gateway's `anthropic_credentials.py` does in fact inject server-side; the sandbox sees a placeholder. Moving the orchestrator in-process to a Claude Code session moves the credential into the parent's process, and any subagent inherits access. This is a real structural threat-model change that the issue body's \"credentials are inherited from the user's session \u2014 no injection needed\" framing understates. R1's mitigation strategy (document trust-context shift; scope use to trusted repos; consider opt-in proxy fallback) is the right shape \u2014 risk_analyst correctly flags this for human review (operator must explicitly accept the new boundary).\n\n- **R2 (PreToolUse hook role-routing unverified, HIGH/HIGH)** \u2014 exactly the #2594 failure class. Plan cannot rely on a primitive whose role-awareness is unverified. The fallback path (cq-6 option 2 MCP-validator-side enforcement) is real \u2014 `sandbox/egg_agent_tools/handlers/restrictions.py:70 check_file_restriction` already wraps `shared/egg_restrictions/patterns.py` and runs in-sandbox today. The recommendation that the spike's exit criteria require a 2-subagent worked example before the abstraction freezes is structurally correct.\n\n- **R4 (in-process orchestrator reentrancy, MEDIUM/HIGH)** \u2014 confirmed by reading `orchestrator/concurrent_executor.py`: `ConcurrentPhaseExecutor` (line 114) holds a `ThreadPoolExecutor` (line 469); `PeerConsensusTracker` runs its own lock; heartbeat / message-poll loops run as background threads. The generator-yields-HITLDecision model demands that the orchestrator pause cleanly while preserving these threads. The risk_analyst's mitigation (\"spike must exercise HITL round-trip end-to-end\"; \"pause heartbeat timers during HITL or extend dramatically\") is the right bound to put on the spike.\n\n- **R7 (subagent context budget vs deep-refine, MEDIUM/HIGH)** \u2014 correctly distinguishes k3s-substrate checkpoint primitives (sandbox restart with full prompt rebuild) from Claude Code subagent re-spawn (orchestrator must re-summarize and re-feed). The honest assessment (\"some refines will fit, some won't; the mitigation is acknowledging the gap\") is more useful than a false promise.\n\n- **R10 (single-role spike freezing interfaces, HIGH/MEDIUM)** \u2014 the strongest structural critique of cq-11 itself. A single-role spike doesn't exercise the orchestrator-as-bus model (BRC's PROPOSE\u2192ACK round-trip). The recommendation to expand to 2 roles (refiner + reviewer_refine) is a defensible counter-proposal to cq-11 that the plan reviewer should surface; risk_analyst correctly notes it requires human review.\n\nThe five trust-boundary shifts (credential isolation, file-write enforcement timing, cost/rate-limit control, agent liveness, push serialization) are the right list and each has a concrete mitigation. The `rollback_plan` is realistic (env-var flip back to `EGG_SUBSTRATE=k3s`, no data migration needed because contracts are filesystem-native).\n\nThe 7 `implementation_recommendations` (REC1\u2013REC7) and 4 `open_questions_for_plan_phase` correctly target structural items that should land in the plan's tasks or HITL gates. None of these recommendations exceed risk_analyst's scope (they're properly framed as \"the planner should\u2026\" not \"I will\u2026\").\n\nThe output is well-structured for downstream consumption: every risk has `id`, `category`, `severity`, `likelihood`, `impact`, `description`, `affected_files`, `mitigation` with `strategy`/`effort`/`residual_risk`, and `requires_human_review` flag. This is the schema the plan reviewer needs to cross-reference against the planner's task list.\n\n### Non-blocking\n\n- **R8 (pip install drift) recommendation #5 has a JSON shape glitch (line 475-477)** \u2014 the entry uses `\"id\": \"MEDIUM\"` and then a comment field `\"id_override_note\": \"REC5\"`. This is a serialization mistake; the actual `id` should be `\"REC5\"`. Fix in any re-propose. Not blocking \u2014 content is clear, just self-consistent IDs would help downstream consumers.\n- **R3 (subagent concurrency ceiling) \"MEDIUM/MEDIUM\"** could be argued up to MEDIUM/HIGH given egg's `ThreadPoolExecutor(max_workers=6)` is hard-coded for k3s and the implement-phase slice-DAG fans out to 8+ agents per the issue body. Not blocking \u2014 the mitigation (per-substrate `max_workers` override + semaphore) is sound at either grading.\n- **REC4 (expand spike to 2 roles) directly contradicts cq-11's HITL resolution** (\"Spike then plan: a single 'walking skeleton' slice that gets one role (refiner) running through one substrate (claude-code) end-to-end\"). The risk_analyst correctly flags this as a recommendation, not a verdict. Plan reviewer's call on whether to surface as a re-open of cq-11 to the operator or accept the single-role spike as-is. I lean toward surfacing \u2014 the consensus path is the actually-novel surface \u2014 but cq-11 is a resolved HITL and the planner cannot unilaterally reopen.\n- **R14 (SendMessage / Agent Teams gating)** is correctly bounded as known-known with the right mitigation (don't let scope creep absorb #2612). No action.\n- **`alignment_with_issue_done_criteria`** is a nice closing section; consider including this shape in future risk_analyst outputs as a contract-vs-risk cross-check.\n- **`performance_assessment.note`** (\"'slower than k3s in a datacenter' may still be 'faster than nothing' from the solo-developer perspective\") is the kind of framing the operator needs and was missing from the refine analysis. Good to surface.\n", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-19T00:13:51.638888+00:00", - "phase": "plan" - }, - { - "id": "9f421a3c-3419-4f", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by architect", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T00:13:56.522158+00:00", - "phase": "plan" - }, - { - "id": "51264ca5-4325-4b", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:13:56.601835+00:00", - "phase": "plan" - }, - { - "id": "3db1f8bb-6451-47", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:14:03.990459+00:00", - "phase": "plan" - }, - { - "id": "73e96e13-e91f-48", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:14:04.358899+00:00", - "phase": "plan" - }, - { - "id": "e14d3f73-2cc0-44", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:14:07.203422+00:00", - "phase": "plan" - }, - { - "id": "fb650e95-0846-4f", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:15:04.071564+00:00", - "phase": "plan" - }, - { - "id": "7009b36f-f1e1-4e", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:15:04.443138+00:00", - "phase": "plan" - }, - { - "id": "81ebd72b-e93b-45", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:15:07.250372+00:00", - "phase": "plan" - }, - { - "id": "c3666cc3-bc0c-43", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:16:04.150571+00:00", - "phase": "plan" - }, - { - "id": "d22d958e-5f87-4b", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:16:04.529624+00:00", - "phase": "plan" - }, - { - "id": "9d9f32d3-e124-43", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:16:07.335968+00:00", - "phase": "plan" - }, - { - "id": "95e2614b-01b1-44", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:17:04.356657+00:00", - "phase": "plan" - }, - { - "id": "c2dcbb37-7bc6-4b", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:17:04.610202+00:00", - "phase": "plan" - }, - { - "id": "07825e67-10ab-45", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:17:07.420571+00:00", - "phase": "plan" - }, - { - "id": "c9c7e3ee-d51e-46", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:18:04.416864+00:00", - "phase": "plan" - }, - { - "id": "6d18c834-0500-4a", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:18:04.699888+00:00", - "phase": "plan" - }, - { - "id": "047d3fb2-68fc-45", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:18:07.480223+00:00", - "phase": "plan" - }, - { - "id": "1822671a-bfee-46", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:19:04.549055+00:00", - "phase": "plan" - }, - { - "id": "a0945205-6558-4b", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:19:04.768185+00:00", - "phase": "plan" - }, - { - "id": "62a85bc7-e8eb-4e", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:19:07.623529+00:00", - "phase": "plan" - }, - { - "id": "5c582760-c53b-41", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:20:04.684898+00:00", - "phase": "plan" - }, - { - "id": "02e7d738-9edf-45", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:20:04.838836+00:00", - "phase": "plan" - }, - { - "id": "e2535f66-02f5-43", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:20:07.707127+00:00", - "phase": "plan" - }, - { - "id": "fb326fad-d6b8-40", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:21:04.787549+00:00", - "phase": "plan" - }, - { - "id": "4463d38c-9405-4f", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:21:04.940902+00:00", - "phase": "plan" - }, - { - "id": "f6a0c4b5-25a7-4e", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:21:07.830091+00:00", - "phase": "plan" - }, - { - "id": "4869401e-931a-4c", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:22:04.860747+00:00", - "phase": "plan" - }, - { - "id": "b4bf027a-cea8-4c", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:22:05.031888+00:00", - "phase": "plan" - }, - { - "id": "9f0cd47f-04e6-46", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:22:07.884881+00:00", - "phase": "plan" - }, - { - "id": "58a29a77-44bd-40", - "pipeline_id": "issue-2623", - "from_role": "overseer", - "to_role": "task_planner", - "message_type": "STATUS", - "subject": "NUDGE: BRC CONSENSUS_CONFIRMED required", - "body": "task_planner: You are in producer_phase=WORKING after receiving your ACK at 00:13:51 UTC. You have been in WORKING state for 400+ seconds without sending CONSENSUS_CONFIRMED. Architect confirmed at 00:13:56 (5s), risk_analyst confirmed at 00:11:48 (4s). reviewer_plan is blocked in REVIEWING state waiting for your confirmation. Please send CONSENSUS_CONFIRMED for your proposal immediately to unblock BRC.", - "metadata": {}, - "timestamp": "2026-05-19T00:22:09.868979+00:00", - "phase": "plan" - }, - { - "id": "5c4c4e2e-ccbf-4c", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:03.952398+00:00" - }, - "timestamp": "2026-05-19T00:23:09.211656+00:00", - "phase": "plan" - }, - { - "id": "35612627-8aab-4d", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:23:09.214842+00:00", - "phase": "plan" - }, - { - "id": "7a3a4051-bcb0-40", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:23:09.223934+00:00", - "phase": "plan" - }, - { - "id": "29c8900e-0336-47", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from task_planner", - "body": "Re-propose addressing all 11 reviewer_plan blockers: (1) Trust-Boundary rewritten to cite integration_tests/conftest.py:71/340/357 (dropped deleted local_pipeline reference); (2) NEW substrate-distinguishing test test_substrate_smoke.py exercises select_substrate().spawner.spawn() and .bus directly under both substrate params; (3) k3s adapter contradiction resolved \u2014 K3sSpawnerAdapter is WORKING (wraps orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn); NotImplementedError only in TASK-1-6's run_pipeline_in_process k3s leg (cq-11 scope-fence); (4) AgentResult.commit_sha added for INV-6 (orchestrator/action_guards.py:631 body :757), captured via git rev-parse HEAD in both spawners; (5) TASK-1-5 cites gateway/worktree_manager.py:1711 is_relative_to defense; (6) TASK-1-3 oracle now orchestrator/tests/test_brc_*.py with named scenarios from test_brc_open_nacks_barrier.py and test_brc_content_validation.py; (7) TASK-1-6 expanded with R4 refactor acceptance: heartbeat-during-HITL, background-thread lifetime on GeneratorExit, contract-state sync; (8) TASK-1-9 ADR covers R1 trust-context shift, R2 hook fallback, R7 context budget, R10 interface stability marker, REC5 cost cap, R15 subagent_type choice; (9) both substrate dimensions pure-Python (kubectl claim corrected); (10) cq-12 HITL registered for pip name selection \u2014 TASK-1-7 carries operator's selection verbatim; (11) EggStack line :78 \u2192 :71. Non-blocking items addressed inline.", - "metadata": { - "payload": { - "summary": "Re-propose addressing all 11 reviewer_plan blockers: (1) Trust-Boundary rewritten to cite integration_tests/conftest.py:71/340/357 (dropped deleted local_pipeline reference); (2) NEW substrate-distinguishing test test_substrate_smoke.py exercises select_substrate().spawner.spawn() and .bus directly under both substrate params; (3) k3s adapter contradiction resolved \u2014 K3sSpawnerAdapter is WORKING (wraps orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn); NotImplementedError only in TASK-1-6's run_pipeline_in_process k3s leg (cq-11 scope-fence); (4) AgentResult.commit_sha added for INV-6 (orchestrator/action_guards.py:631 body :757), captured via git rev-parse HEAD in both spawners; (5) TASK-1-5 cites gateway/worktree_manager.py:1711 is_relative_to defense; (6) TASK-1-3 oracle now orchestrator/tests/test_brc_*.py with named scenarios from test_brc_open_nacks_barrier.py and test_brc_content_validation.py; (7) TASK-1-6 expanded with R4 refactor acceptance: heartbeat-during-HITL, background-thread lifetime on GeneratorExit, contract-state sync; (8) TASK-1-9 ADR covers R1 trust-context shift, R2 hook fallback, R7 context budget, R10 interface stability marker, REC5 cost cap, R15 subagent_type choice; (9) both substrate dimensions pure-Python (kubectl claim corrected); (10) cq-12 HITL registered for pip name selection \u2014 TASK-1-7 carries operator's selection verbatim; (11) EggStack line :78 \u2192 :71. Non-blocking items addressed inline.", - "attestation": {}, - "artifacts": [ - ".egg-state/drafts/2623-plan.md" - ], - "risk_considered": "Single-slice plan over 1k LOC advisory ceiling is unchanged (cq-11 spike-then-plan). New cq-12 HITL adds one pending decision the operator must resolve for TASK-1-7 to ship; documented in plan body. TASK-1-6 is now an explicit R4 refactor task with three acceptance bullets (heartbeat, lifetime, state sync) \u2014 the most expensive task in the slice but the cost is now visible to the coder rather than hidden.", - "commit_sha": "f760fba16", - "files_changed": [ - ".egg-state/drafts/2623-plan.md" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 2, - "commit_sha": "f760fba16" - }, - "timestamp": "2026-05-19T00:23:09.707117+00:00", - "phase": "plan" - }, - { - "id": "3fa8dbed-b2a3-46", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:23:09.753943+00:00", - "phase": "plan" - }, - { - "id": "2138136f-0010-4b", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:23:15.055576+00:00" - }, - "timestamp": "2026-05-19T00:23:34.746093+00:00", - "phase": "plan" - }, - { - "id": "6d3a1592-7dd9-43", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:23:34.838518+00:00", - "phase": "plan" - }, - { - "id": "ebd6cbd3-457d-4d", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:23:49.864906+00:00" - }, - "timestamp": "2026-05-19T00:23:49.926493+00:00", - "phase": "plan" - }, - { - "id": "6660f40d-4cb4-44", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:24:09.311965+00:00", - "phase": "plan" - }, - { - "id": "3f548e9e-11da-4d", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:24:09.320102+00:00", - "phase": "plan" - }, - { - "id": "3e32ef0a-80b8-4f", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:23:49.864906+00:00" - }, - "timestamp": "2026-05-19T00:24:50.035429+00:00", - "phase": "plan" - }, - { - "id": "96300726-3b2c-4b", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:25:09.400792+00:00", - "phase": "plan" - }, - { - "id": "04d14f8e-1bce-40", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:25:09.410305+00:00", - "phase": "plan" - }, - { - "id": "8e668321-1dc8-4b", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:23:49.864906+00:00" - }, - "timestamp": "2026-05-19T00:25:50.122517+00:00", - "phase": "plan" - }, - { - "id": "eba47701-2da3-4e", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "task_planner", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_plan for task_planner", - "body": "\nRe-reviewed `.egg-state/drafts/2623-plan.md` (v2, commit `f760fba16`, 1,348 lines, +525/-219 since v1). Walked every blocker from the v1 NACK against the re-propose:\n\n1. **Trust-Boundary scope** \u2014 rewritten. v2 cites `EggStack` at `integration_tests/conftest.py:71` (verified \u2014 `grep -n 'class EggStack' integration_tests/conftest.py` \u2192 `71:class EggStack(GatewayClientMixin):`), `egg_stack` fixture at `:340`, `orchestrator_url` fixture at `:357` (both verified). Drops the deleted `local_pipeline/` reference and explicitly names `f7803637d1` as the deletion commit. Implications block clearly states \"Both parameters run **pure-Python in-process**. No kubectl gate is needed (or claimed) for either parameter.\" Cleanly addresses v1 blockers #1, #9, #11.\n\n2. **Substrate-distinguishing test** \u2014 replaced. v2 introduces a NEW `integration_tests/regression/test_substrate_smoke.py` that drives `select_substrate(...).spawner.spawn(...)` and `.bus.add_message/get_messages` directly. TASK-1-8 acceptance #2 (lines 1220-1234): k3s dimension routes through `K3sSpawnerAdapter` (with the underlying `create_concurrent_spawn_fn` mocked); claude-code dimension routes through `ClaudeCodeSpawner` + `InProcessMessageBus`. Both produce divergent execution paths and the test asserts `AgentResult.commit_sha` populated and INV-3 stale-version rejection round-trip. v1 blocker #2 cleanly addressed.\n\n3. **k3s adapter contradiction** \u2014 resolved. TASK-1-1 acceptance #3 (lines 276-280): `select_substrate({})` defaults to \"k3s\" and returns a **working** `K3sSpawnerAdapter` wrapping `create_concurrent_spawn_fn` (`orchestrator/kubernetes_spawner.py:1564` \u2014 verified by grep). TASK-1-2 description lines 320-326: \"Both substrate legs are working under this patch: the k3s leg returns the `K3sSpawnerAdapter` shim from TASK-1-1 \u2026 the claude-code leg returns this task's `ClaudeCodeSpawner`. The patch is a re-wiring, not a behavior change \u2014 k3s deployments stay green.\" The only `NotImplementedError` raised in this slice is now TASK-1-6's `run_pipeline_in_process` k3s leg \u2014 the cq-11 scope-fence is on the in-process *boot path*, not on the spawner protocol. Internally consistent. v1 blocker #3 cleanly addressed.\n\n4. **AgentResult.commit_sha for INV-6** \u2014 added. TASK-1-1 acceptance #2 (lines 272-275): \"`AgentResult` includes `commit_sha: str | None`\". TASK-1-2 acceptance #2 (lines 333-337): \"captured via `git -C rev-parse HEAD`\"; \"A unit test asserts `commit_sha` is a 40-char hex string after a spawn that produced a commit.\" TASK-1-1 adapter and TASK-1-2 ClaudeCodeSpawner both capture commit_sha (lines 261-263 and 311-315 respectively). INV-6 (`orchestrator/action_guards.py:631`, body at `:757` \u2014 verified line numbers) is preserved across both substrates. v1 blocker #4 cleanly addressed.\n\n5. **TASK-1-5 path-escape primitive** \u2014 corrected. Plan now cites `gateway/worktree_manager.py:1711` (call site within `_remove_worktree`) and `:1700` (matching `base.resolve()`) \u2014 verified by grep (`gateway/worktree_manager.py:1693` docstring; `:1711` is `if not resolved.is_relative_to(base_resolved):`). The bogus `gateway/gateway.py:7903` reference is gone. v1 blocker #5 cleanly addressed.\n\n6. **TASK-1-3 BRC oracle directory** \u2014 corrected. TASK-1-3 description (lines 1046-1051) and acceptance (lines 1052-1059) point at `orchestrator/tests/test_brc_*.py` with named scenarios from `test_brc_open_nacks_barrier.py` and `test_brc_content_validation.py` (both verified to exist). The bogus `shared/tests/test_brc_*` reference is gone. Tester role's behavioral test lands at `shared/tests/test_in_process_message_bus.py` (TASK-1-8 file list line 1252) \u2014 tester-allowed path. v1 blocker #6 cleanly addressed.\n\n7. **TASK-1-6 R4 acceptance criteria** \u2014 added. TASK-1-6 description (lines 1117-1131) explicitly calls out the refactor scope: \"This is the slice's most expensive task \u2014 risk_analyst R4 flagged the orchestrator-reentrancy refactor as `requires_human_review` (effort HIGH).\" Acceptance now has three R4-specific bullets: **heartbeat-during-HITL** (\"background heartbeat poll, BRC re-review, and message-bus tick threads remain alive while the generator is paused at a yield boundary\"), **background-thread lifetime** (\"the generator returns cleanly on both normal completion AND on `GeneratorExit`\"), **contract-state synchronization** (\"the in-process orchestrator writes to the same `.egg-state/contracts/.json` filesystem path the HTTP daemon uses\"). Each has a concrete verification path. v1 blocker #7 cleanly addressed.\n\n8. **Risk-mitigation acceptance in TASK-1-9 ADR** \u2014 added. ADR acceptance (lines 1269-1291) now has dedicated subsections for R1 (\"Trust-context shift\" \u2014 credential exposure), R2 (\"PreToolUse hook fallback\" \u2014 primary + MCP-validator-side fallback), R7 (\"Subagent context budget regression\"), R10 (\"Interface stability marker\" \u2014 `# v0.x unstable` until \u22653 roles), REC5 (\"Cost cap\" \u2014 `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`), R15 (\"Subagent type model\" \u2014 `general-purpose` choice + trade-off). Each is required as an explicit acceptance bullet, not buried in narrative. v1 blocker #8 cleanly addressed.\n\n9. **TASK-1-8 kubectl claim** \u2014 corrected per #1 above.\n\n10. **cq-12 HITL for pip name** \u2014 registered. `mcp__sdlc__show_contract` confirms a `cq-12` decision in the contract with three substantive options (PyPI `egg-shared` publish; `git+https://github.com/jwbron/egg.git#subdirectory=shared`; \"Defer to follow-up TODO placeholder\") plus \"Other\". TASK-1-7 description (lines 1170-1180) and acceptance (lines 1182-1193) carry the operator's selection verbatim with TODO-placeholder fallback. v1 blocker #10 cleanly addressed.\n\n11. **EggStack line number** \u2014 corrected to `:71` per #1.\n\nPrimitive-audit grep verdict on the v2 Primitives table (lines 109-139): all newly added rows cite real file:line locations \u2014 `validate_invariants` at `:631`/body at `:757` (verified \u2014 `def validate_invariants` is at `orchestrator/action_guards.py:631`); `create_concurrent_spawn_fn` at `:1564`; `make_tracker` at `integration_tests/regression/_helpers.py:156`; `PeerConsensusTracker` at `orchestrator/peer_consensus.py:69`; existing regression fixtures `single_reviewer_graph`/`event_capture`/`filter_events` at `:564`/`:519`/`:552` (verified). BRC test directory `orchestrator/tests/test_brc_*.py` \u2014 verified the directory contains 15+ matching files. The Primitives table is now grep-clean.\n\nSlice-DAG and forest checks: single root slice (slice id 1), no `serialized_chain_order` required, no forest violation. Plan ingestion would accept this.\n\n### Non-blocking\n\nThe structural plan is sound and I will not re-NACK on these. Each is a mechanical cleanup; please pick up before opening the PR so the auto-rendered PR body matches the actual code.\n\n1. **`yaml-tasks \u2192 pr.description.Changes[1]` still says \"The k3s adapter is a deferred TODO that raises `NotImplementedError` (cq-11 explicit boundary)\" (line 866-867).** This directly contradicts the v2 task definitions: TASK-1-1 acceptance #3 ships a *working* `K3sSpawnerAdapter`. The yaml PR description is what becomes the actual GitHub PR body when the implement-phase coder runs `gh pr create` \u2014 future readers of the PR will be misled. Replace with: \"The k3s leg uses a working `K3sSpawnerAdapter` shim wrapping `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn`; the `NotImplementedError` scope-fence applies only to TASK-1-6's `run_pipeline_in_process` boot path (cq-11).\"\n\n2. **`yaml-tasks \u2192 pr.test_plan` still references the wrong test file (line 907):** \"`test_brc_single_cycle.py` passes under both `substrate` parameters\". The new substrate-distinguishing test is `test_substrate_smoke.py` (TASK-1-8 acceptance #2, file at line 1248). Replace `test_brc_single_cycle.py` with `test_substrate_smoke.py` in the yaml test_plan.\n\n3. **Plan \u00a7\"Test strategy\" \u2192 \"Behavioral parity\" bullet (lines 778-781)** has the same stale reference: \"the existing `test_brc_single_cycle.py` runs under both `substrate` parameters; the claude-code parameter exercises `InProcessMessageBus` + `ClaudeCodeSpawner`. Pass on both = behavioral parity proof.\" Replace with the new test_substrate_smoke.py phrasing.\n\n4. **Primitives \"New primitives\" table row \"test_brc_single_cycle_claude_code\" at `integration_tests/regression/test_brc_single_cycle.py (existing \u2014 extended)\" (line 161)** is stale. Remove this row entirely; the actual artifact is the new `test_substrate_smoke.py` row that needs to be added in its place (`integration_tests/regression/test_substrate_smoke.py` NEW, created by TASK-1-8 tester).\n\n5. **`yaml-tasks \u2192 pr.description.Impact` and \"Manual verification (reviewer)\" step 1 and `yaml.test_plan` still hardcode `pip install egg-shared`** (lines 798, 893, 909). The actual install string is whatever cq-12 resolves to. Replace with conditional phrasing: \"`pip install ` (currently TBD pending operator selection \u2014 see `plugins/egg-sdlc/.claude-plugin/plugin.json` after merge)\" or leave the placeholder explicitly bracketed (``) so a coder picks it up at implement time.\n\n6. **The cq-12 decision is currently unresolved.** The plan's TASK-1-7 handles this gracefully (TODO placeholder if cq-12 defers), but if the operator wants to resolve cq-12 *before* implement-phase starts, that gives the coder a deterministic string to ship. Not a plan-phase blocker; just calling it out for the orchestrator's HITL gate scheduling.\n\n7. **Slice-sizing advisory (#2137 size advisory; never NACK):** v2's estimated 1,800\u20132,500 LOC stands (no change). Per HITL decision-6 opt-2 I do NOT NACK on size; this is advisory only and the spike-then-plan framing (cq-11) justifies the size. The next reviewer of the implement-phase BRC cycle should know the slice will be expensive to NACK and the producer should land in tight, well-scoped commits per task.\n\n8. **R5 (git lock contention on concurrent push) and R12 (heartbeat tautology / wall-clock timeouts) from the risk_analyst** are still not surfaced in any acceptance criteria. The spike's single-role / single-phase scope makes these unobservable (no concurrent producer dispatch in the refine phase), so this is acceptable scope-fencing \u2014 but worth listing in TASK-1-9's \"Open work\" appendix so the follow-up issue inherits them. Currently the Open work list (line 1285-1291) names plan/implement/pr, BRC concurrency, full conformance, k3s adapter, EggHarnessSpawner, prune verb, fork delegation, cost cap, subagent_type migration \u2014 but not R5's push-serialization or R12's wall-clock-timeout watchdog. Consider adding these two items.\n\n9. **Risk_analyst's REC4 (expand spike to 2 roles)** is NOT picked up \u2014 the plan stays at refiner-only, consistent with cq-11. This is the right call per the resolved HITL; the operator can revisit by re-opening cq-11 if the spike's interface lessons end up too thin from one role's perspective. Calling it out so the producer / operator know the path explicitly.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/drafts/2623-plan.md", - ".egg-state/contracts/issue-2623.json", - "orchestrator/concurrent_executor.py", - "orchestrator/kubernetes_spawner.py", - "orchestrator/peer_consensus.py", - "orchestrator/action_guards.py", - "integration_tests/conftest.py", - "integration_tests/regression/conftest.py", - "integration_tests/regression/test_brc_single_cycle.py", - "gateway/worktree_manager.py", - "shared/egg_restrictions/patterns.py" - ], - "reason": "\nRe-reviewed `.egg-state/drafts/2623-plan.md` (v2, commit `f760fba16`, 1,348 lines, +525/-219 since v1). Walked every blocker from the v1 NACK against the re-propose:\n\n1. **Trust-Boundary scope** \u2014 rewritten. v2 cites `EggStack` at `integration_tests/conftest.py:71` (verified \u2014 `grep -n 'class EggStack' integration_tests/conftest.py` \u2192 `71:class EggStack(GatewayClientMixin):`), `egg_stack` fixture at `:340`, `orchestrator_url` fixture at `:357` (both verified). Drops the deleted `local_pipeline/` reference and explicitly names `f7803637d1` as the deletion commit. Implications block clearly states \"Both parameters run **pure-Python in-process**. No kubectl gate is needed (or claimed) for either parameter.\" Cleanly addresses v1 blockers #1, #9, #11.\n\n2. **Substrate-distinguishing test** \u2014 replaced. v2 introduces a NEW `integration_tests/regression/test_substrate_smoke.py` that drives `select_substrate(...).spawner.spawn(...)` and `.bus.add_message/get_messages` directly. TASK-1-8 acceptance #2 (lines 1220-1234): k3s dimension routes through `K3sSpawnerAdapter` (with the underlying `create_concurrent_spawn_fn` mocked); claude-code dimension routes through `ClaudeCodeSpawner` + `InProcessMessageBus`. Both produce divergent execution paths and the test asserts `AgentResult.commit_sha` populated and INV-3 stale-version rejection round-trip. v1 blocker #2 cleanly addressed.\n\n3. **k3s adapter contradiction** \u2014 resolved. TASK-1-1 acceptance #3 (lines 276-280): `select_substrate({})` defaults to \"k3s\" and returns a **working** `K3sSpawnerAdapter` wrapping `create_concurrent_spawn_fn` (`orchestrator/kubernetes_spawner.py:1564` \u2014 verified by grep). TASK-1-2 description lines 320-326: \"Both substrate legs are working under this patch: the k3s leg returns the `K3sSpawnerAdapter` shim from TASK-1-1 \u2026 the claude-code leg returns this task's `ClaudeCodeSpawner`. The patch is a re-wiring, not a behavior change \u2014 k3s deployments stay green.\" The only `NotImplementedError` raised in this slice is now TASK-1-6's `run_pipeline_in_process` k3s leg \u2014 the cq-11 scope-fence is on the in-process *boot path*, not on the spawner protocol. Internally consistent. v1 blocker #3 cleanly addressed.\n\n4. **AgentResult.commit_sha for INV-6** \u2014 added. TASK-1-1 acceptance #2 (lines 272-275): \"`AgentResult` includes `commit_sha: str | None`\". TASK-1-2 acceptance #2 (lines 333-337): \"captured via `git -C rev-parse HEAD`\"; \"A unit test asserts `commit_sha` is a 40-char hex string after a spawn that produced a commit.\" TASK-1-1 adapter and TASK-1-2 ClaudeCodeSpawner both capture commit_sha (lines 261-263 and 311-315 respectively). INV-6 (`orchestrator/action_guards.py:631`, body at `:757` \u2014 verified line numbers) is preserved across both substrates. v1 blocker #4 cleanly addressed.\n\n5. **TASK-1-5 path-escape primitive** \u2014 corrected. Plan now cites `gateway/worktree_manager.py:1711` (call site within `_remove_worktree`) and `:1700` (matching `base.resolve()`) \u2014 verified by grep (`gateway/worktree_manager.py:1693` docstring; `:1711` is `if not resolved.is_relative_to(base_resolved):`). The bogus `gateway/gateway.py:7903` reference is gone. v1 blocker #5 cleanly addressed.\n\n6. **TASK-1-3 BRC oracle directory** \u2014 corrected. TASK-1-3 description (lines 1046-1051) and acceptance (lines 1052-1059) point at `orchestrator/tests/test_brc_*.py` with named scenarios from `test_brc_open_nacks_barrier.py` and `test_brc_content_validation.py` (both verified to exist). The bogus `shared/tests/test_brc_*` reference is gone. Tester role's behavioral test lands at `shared/tests/test_in_process_message_bus.py` (TASK-1-8 file list line 1252) \u2014 tester-allowed path. v1 blocker #6 cleanly addressed.\n\n7. **TASK-1-6 R4 acceptance criteria** \u2014 added. TASK-1-6 description (lines 1117-1131) explicitly calls out the refactor scope: \"This is the slice's most expensive task \u2014 risk_analyst R4 flagged the orchestrator-reentrancy refactor as `requires_human_review` (effort HIGH).\" Acceptance now has three R4-specific bullets: **heartbeat-during-HITL** (\"background heartbeat poll, BRC re-review, and message-bus tick threads remain alive while the generator is paused at a yield boundary\"), **background-thread lifetime** (\"the generator returns cleanly on both normal completion AND on `GeneratorExit`\"), **contract-state synchronization** (\"the in-process orchestrator writes to the same `.egg-state/contracts/.json` filesystem path the HTTP daemon uses\"). Each has a concrete verification path. v1 blocker #7 cleanly addressed.\n\n8. **Risk-mitigation acceptance in TASK-1-9 ADR** \u2014 added. ADR acceptance (lines 1269-1291) now has dedicated subsections for R1 (\"Trust-context shift\" \u2014 credential exposure), R2 (\"PreToolUse hook fallback\" \u2014 primary + MCP-validator-side fallback), R7 (\"Subagent context budget regression\"), R10 (\"Interface stability marker\" \u2014 `# v0.x unstable` until \u22653 roles), REC5 (\"Cost cap\" \u2014 `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`), R15 (\"Subagent type model\" \u2014 `general-purpose` choice + trade-off). Each is required as an explicit acceptance bullet, not buried in narrative. v1 blocker #8 cleanly addressed.\n\n9. **TASK-1-8 kubectl claim** \u2014 corrected per #1 above.\n\n10. **cq-12 HITL for pip name** \u2014 registered. `mcp__sdlc__show_contract` confirms a `cq-12` decision in the contract with three substantive options (PyPI `egg-shared` publish; `git+https://github.com/jwbron/egg.git#subdirectory=shared`; \"Defer to follow-up TODO placeholder\") plus \"Other\". TASK-1-7 description (lines 1170-1180) and acceptance (lines 1182-1193) carry the operator's selection verbatim with TODO-placeholder fallback. v1 blocker #10 cleanly addressed.\n\n11. **EggStack line number** \u2014 corrected to `:71` per #1.\n\nPrimitive-audit grep verdict on the v2 Primitives table (lines 109-139): all newly added rows cite real file:line locations \u2014 `validate_invariants` at `:631`/body at `:757` (verified \u2014 `def validate_invariants` is at `orchestrator/action_guards.py:631`); `create_concurrent_spawn_fn` at `:1564`; `make_tracker` at `integration_tests/regression/_helpers.py:156`; `PeerConsensusTracker` at `orchestrator/peer_consensus.py:69`; existing regression fixtures `single_reviewer_graph`/`event_capture`/`filter_events` at `:564`/`:519`/`:552` (verified). BRC test directory `orchestrator/tests/test_brc_*.py` \u2014 verified the directory contains 15+ matching files. The Primitives table is now grep-clean.\n\nSlice-DAG and forest checks: single root slice (slice id 1), no `serialized_chain_order` required, no forest violation. Plan ingestion would accept this.\n\n### Non-blocking\n\nThe structural plan is sound and I will not re-NACK on these. Each is a mechanical cleanup; please pick up before opening the PR so the auto-rendered PR body matches the actual code.\n\n1. **`yaml-tasks \u2192 pr.description.Changes[1]` still says \"The k3s adapter is a deferred TODO that raises `NotImplementedError` (cq-11 explicit boundary)\" (line 866-867).** This directly contradicts the v2 task definitions: TASK-1-1 acceptance #3 ships a *working* `K3sSpawnerAdapter`. The yaml PR description is what becomes the actual GitHub PR body when the implement-phase coder runs `gh pr create` \u2014 future readers of the PR will be misled. Replace with: \"The k3s leg uses a working `K3sSpawnerAdapter` shim wrapping `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn`; the `NotImplementedError` scope-fence applies only to TASK-1-6's `run_pipeline_in_process` boot path (cq-11).\"\n\n2. **`yaml-tasks \u2192 pr.test_plan` still references the wrong test file (line 907):** \"`test_brc_single_cycle.py` passes under both `substrate` parameters\". The new substrate-distinguishing test is `test_substrate_smoke.py` (TASK-1-8 acceptance #2, file at line 1248). Replace `test_brc_single_cycle.py` with `test_substrate_smoke.py` in the yaml test_plan.\n\n3. **Plan \u00a7\"Test strategy\" \u2192 \"Behavioral parity\" bullet (lines 778-781)** has the same stale reference: \"the existing `test_brc_single_cycle.py` runs under both `substrate` parameters; the claude-code parameter exercises `InProcessMessageBus` + `ClaudeCodeSpawner`. Pass on both = behavioral parity proof.\" Replace with the new test_substrate_smoke.py phrasing.\n\n4. **Primitives \"New primitives\" table row \"test_brc_single_cycle_claude_code\" at `integration_tests/regression/test_brc_single_cycle.py (existing \u2014 extended)\" (line 161)** is stale. Remove this row entirely; the actual artifact is the new `test_substrate_smoke.py` row that needs to be added in its place (`integration_tests/regression/test_substrate_smoke.py` NEW, created by TASK-1-8 tester).\n\n5. **`yaml-tasks \u2192 pr.description.Impact` and \"Manual verification (reviewer)\" step 1 and `yaml.test_plan` still hardcode `pip install egg-shared`** (lines 798, 893, 909). The actual install string is whatever cq-12 resolves to. Replace with conditional phrasing: \"`pip install ` (currently TBD pending operator selection \u2014 see `plugins/egg-sdlc/.claude-plugin/plugin.json` after merge)\" or leave the placeholder explicitly bracketed (``) so a coder picks it up at implement time.\n\n6. **The cq-12 decision is currently unresolved.** The plan's TASK-1-7 handles this gracefully (TODO placeholder if cq-12 defers), but if the operator wants to resolve cq-12 *before* implement-phase starts, that gives the coder a deterministic string to ship. Not a plan-phase blocker; just calling it out for the orchestrator's HITL gate scheduling.\n\n7. **Slice-sizing advisory (#2137 size advisory; never NACK):** v2's estimated 1,800\u20132,500 LOC stands (no change). Per HITL decision-6 opt-2 I do NOT NACK on size; this is advisory only and the spike-then-plan framing (cq-11) justifies the size. The next reviewer of the implement-phase BRC cycle should know the slice will be expensive to NACK and the producer should land in tight, well-scoped commits per task.\n\n8. **R5 (git lock contention on concurrent push) and R12 (heartbeat tautology / wall-clock timeouts) from the risk_analyst** are still not surfaced in any acceptance criteria. The spike's single-role / single-phase scope makes these unobservable (no concurrent producer dispatch in the refine phase), so this is acceptable scope-fencing \u2014 but worth listing in TASK-1-9's \"Open work\" appendix so the follow-up issue inherits them. Currently the Open work list (line 1285-1291) names plan/implement/pr, BRC concurrency, full conformance, k3s adapter, EggHarnessSpawner, prune verb, fork delegation, cost cap, subagent_type migration \u2014 but not R5's push-serialization or R12's wall-clock-timeout watchdog. Consider adding these two items.\n\n9. **Risk_analyst's REC4 (expand spike to 2 roles)** is NOT picked up \u2014 the plan stays at refiner-only, consistent with cq-11. This is the right call per the resolved HITL; the operator can revisit by re-opening cq-11 if the spike's interface lessons end up too thin from one role's perspective. Calling it out so the producer / operator know the path explicitly.\n", - "ack_version": 2 - }, - "version": 2 - }, - "timestamp": "2026-05-19T00:26:06.834512+00:00", - "phase": "plan" - }, - { - "id": "d639bbf7-f495-47", - "pipeline_id": "issue-2623", - "from_role": "orchestrator", - "to_role": "task_planner", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 2) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 2 - }, - "timestamp": "2026-05-19T00:26:06.834649+00:00", - "phase": "plan" - }, - { - "id": "c0377c7d-355a-4d", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:26:06.880944+00:00", - "phase": "plan" - }, - { - "id": "0320d5e9-a8c8-45", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:04.318709+00:00" - }, - "timestamp": "2026-05-19T00:26:09.485522+00:00", - "phase": "plan" - }, - { - "id": "78c87376-9b21-49", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:14:07.149250+00:00" - }, - "timestamp": "2026-05-19T00:26:09.496815+00:00", - "phase": "plan" - }, - { - "id": "9abb692e-e971-4b", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_plan", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T00:26:19.633389+00:00", - "phase": "plan" - }, - { - "id": "1b39451e-005a-4d", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:26:19.776486+00:00", - "phase": "plan" - }, - { - "id": "bac486c3-f75c-45", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:26:19.784185+00:00", - "phase": "plan" - }, - { - "id": "696eb922-af87-49", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:26:29.251699+00:00" - }, - "timestamp": "2026-05-19T00:26:29.295476+00:00", - "phase": "plan" - }, - { - "id": "cb9ddd3a-739e-44", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:26:29.959878+00:00" - }, - "timestamp": "2026-05-19T00:26:29.992336+00:00", - "phase": "plan" - }, - { - "id": "900672ca-b373-45", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:26:30.119269+00:00", - "phase": "plan" - }, - { - "id": "b3451bb7-e71d-4d", - "pipeline_id": "issue-2623", - "from_role": "task_planner", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by task_planner", - "body": "", - "metadata": { - "consensus_reached": true - }, - "timestamp": "2026-05-19T00:26:32.099788+00:00", - "phase": "plan" - }, - { - "id": "f03ebb75-f0f3-43", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:26:32.178874+00:00", - "phase": "plan" - }, - { - "id": "de289b86-8d47-49", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:26:33.858056+00:00" - }, - "timestamp": "2026-05-19T00:26:33.910057+00:00", - "phase": "plan" - }, - { - "id": "13696345-3ab7-4b", - "pipeline_id": "issue-2623", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:26:34.026522+00:00", - "phase": "plan" - }, - { - "id": "0a1c07b9-0250-46", - "pipeline_id": "issue-2623", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:26:48.496740+00:00" - }, - "timestamp": "2026-05-19T00:27:02.508228+00:00", - "phase": "plan" - }, - { - "id": "87b9ce76-749d-41", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T00:26:39.211574+00:00" - }, - "timestamp": "2026-05-19T00:27:02.514240+00:00", - "phase": "plan" - }, - { - "id": "6b67ccd2-d8e5-45", - "pipeline_id": "issue-2623", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T00:27:02.589929+00:00", - "phase": "plan" - } -] \ No newline at end of file diff --git a/.egg-state/brc-history/2623-plan.md b/.egg-state/brc-history/2623-plan.md deleted file mode 100644 index 033bd3c3fc..0000000000 --- a/.egg-state/brc-history/2623-plan.md +++ /dev/null @@ -1,2286 +0,0 @@ -# BRC Consensus History — plan phase - -Generated: 2026-05-19T00:27:02Z -Pipeline: issue-2623 - -### [2026-05-18T23:56:40Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 83df3abd-6c2b-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-18T23:57:40Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 409ba48d-6d2a-4e -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-18T23:58:40Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 85b88138-33b8-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-18T23:59:40Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 913e6add-c0da-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-19T00:00:40Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: dbf7b0a0-5ca6-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-19T00:01:40Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c78e3b08-af81-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-19T00:02:40Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 90225d5c-a792-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-19T00:03:57Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 77a3726d-b06b-48 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-19T00:05:20Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c5746a0a-f902-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T23:56:28.048573+00:00' -```` - -### [2026-05-19T00:05:21Z] task_planner (CONSENSUS_PROPOSE): Proposal from task_planner - -Plan: walking-skeleton Claude Code substrate spike (one slice, eleven tasks). Single slice per cq-11 spike-then-plan. Lands four substrate interfaces (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager), claude-code implementations of each (ClaudeCodeSpawner, InProcessMessageBus, PreToolUseHookPolicy, LocalWorktreeManager), in-process orchestrator boot generator yielding HITLDecision per cq-7, egg-sdlc plugin (TASK-1-7 coder plugin.json + TASK-1-11 documenter SKILL.md/agents) per cq-8, one parametrized regression test in conftest.py + test_brc_single_cycle.py per cq-3, ADR at docs/architecture/claude-code-substrate.md, and reviewer-pasted follow-up issue draft. Existing primitives cited with file:line: KubernetesSpawner orchestrator/kubernetes_spawner.py:249, MessageStore orchestrator/message_store.py:200, ConcurrentPhaseExecutor orchestrator/concurrent_executor.py:114, _spawn_agent orchestrator/concurrent_executor.py:504, HITLDecision orchestrator/models.py:300, build_system_prompt shared/egg_harness/prompt.py:24, build_agent_patterns shared/egg_restrictions/patterns.py:768, WORKTREE_BASE_DIR gateway/worktree_manager.py:49. All eleven cq decisions and six feedback items applied; tasks split by role (coder/tester/documenter); no .github/ writes; tests under integration_tests/regression and shared/tests (existing tester-allowed directories). - -````yaml -id: d50d0086-19e4-43 -phase: plan -metadata: - payload: - summary: 'Plan: walking-skeleton Claude Code substrate spike (one slice, eleven - tasks). Single slice per cq-11 spike-then-plan. Lands four substrate interfaces - (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager), claude-code implementations - of each (ClaudeCodeSpawner, InProcessMessageBus, PreToolUseHookPolicy, LocalWorktreeManager), - in-process orchestrator boot generator yielding HITLDecision per cq-7, egg-sdlc - plugin (TASK-1-7 coder plugin.json + TASK-1-11 documenter SKILL.md/agents) per - cq-8, one parametrized regression test in conftest.py + test_brc_single_cycle.py - per cq-3, ADR at docs/architecture/claude-code-substrate.md, and reviewer-pasted - follow-up issue draft. Existing primitives cited with file:line: KubernetesSpawner - orchestrator/kubernetes_spawner.py:249, MessageStore orchestrator/message_store.py:200, - ConcurrentPhaseExecutor orchestrator/concurrent_executor.py:114, _spawn_agent - orchestrator/concurrent_executor.py:504, HITLDecision orchestrator/models.py:300, - build_system_prompt shared/egg_harness/prompt.py:24, build_agent_patterns shared/egg_restrictions/patterns.py:768, - WORKTREE_BASE_DIR gateway/worktree_manager.py:49. All eleven cq decisions and - six feedback items applied; tasks split by role (coder/tester/documenter); no - .github/ writes; tests under integration_tests/regression and shared/tests (existing - tester-allowed directories).' - attestation: {} - artifacts: - - .egg-state/drafts/2623-plan.md - risk_considered: "Single-slice plan over the 1k LOC advisory ceiling (~1.8-2.5k\ - \ LOC) \u2014 explicitly justified by cq-11 spike-then-plan choice; splitting\ - \ would re-litigate cq-11. k3s adapter deferred as NotImplementedError TODO\ - \ marker is a deliberate scope boundary, not silent drift \u2014 surfaced both\ - \ in tests (TASK-1-8) and the ADR open-work appendix (TASK-1-9, TASK-1-10)." - commit_sha: 51d4a24bc - files_changed: - - .egg-state/drafts/2623-plan.md - tests_run: [] - tasks_satisfied: [] - version: 1 - commit_sha: 51d4a24bc -```` - -### [2026-05-19T00:05:21Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 465276a7-10b6-45 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:05:49Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7572bf86-4bbc-4d -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:06:18Z] architect (CONSENSUS_PROPOSE): Proposal from architect - -Drafted the plan-phase architect output for #2623 (substrate-swap walking skeleton). The analysis honors all 11 HITL resolutions and the operator's spike-then-plan sequencing (cq-11): it proposes an AgentSpawner ABC + ClaudeCodeSpawner concrete impl as the minimum viable abstraction that proves the substrate boundary on one role (refiner) and one phase (refine) end-to-end, with MessageBus / PolicyEnforcer / WorktreeManager / HITLSurface ABCs accompanying it so the interfaces land together. Substrate selection is an EGG_SUBSTRATE env var (default 'k3s' for back-compat) read by a new egg_orchestrator/substrate.py registry. Concrete decisions: ClaudeCodeSpawner uses Claude Code's Agent tool with synchronous call-and-wait (cq-4) and a pre-created LocalWorktreeManager-owned worktree (cq-5, not Claude Code's EnterWorktree); PreToolUseHookPolicy lives at plugins/sdlc/hooks/pretooluse_policy.sh and imports shared/egg_restrictions/patterns.py for single-source-of-truth (cq-6); HeredocHITLSurface yields HITLDecision via a Python generator that the skill renders through AskUserQuestion (cq-7); plugins/sdlc/.claude-plugin/plugin.json declares a pip dep on egg (cq-8). Conformance is verified by parametrizing integration_tests/regression/test_brc_single_cycle.py with a substrate fixture (cq-3); the remaining regression tests are deferred to follow-ups. The analysis surfaces 48 file:line citations covering every cited primitive (concurrent_executor, kubernetes_spawner, message_store/redis_message_store, action_guards INV-1..INV-6, models.Contract schemaVersion 1.1, patterns.AgentFilePattern.can_write, build_system_prompt, MCP check_file_restriction handler, refine-plan plugin manifest precedent) and classifies each on the #2594 execution-context axes (deployed-pod vs trusted-CI-runner vs the newly introduced parent-claude-code-session context). It hands 9 candidate tasks to task_planner (each scoped to ≤1 logical change), a 7-item risk list to risk_analyst (trust-boundary shift, subagent ceiling, context window, isolation='worktree' availability, schemaVersion rebase, RedisMessageStore drift, pip-install pre-flight), and an explicit deferred-scope list (multi-role concurrency, plan/implement/pr phases, PreToolUse Bash interception, EggHarnessSpawner, full regression matrix, k3s deprecation, overseer thread, slice-DAG concurrency, telemetry review). Satisfies the plan-phase architect contract task by providing the architecture analysis + component breakdown that the task_planner and risk_analyst can build on without re-litigating substrate-swap framing or HITL resolutions. - -````yaml -id: fc985eee-f5a1-4c -phase: plan -metadata: - payload: - summary: "Drafted the plan-phase architect output for #2623 (substrate-swap walking\ - \ skeleton). The analysis honors all 11 HITL resolutions and the operator's\ - \ spike-then-plan sequencing (cq-11): it proposes an AgentSpawner ABC + ClaudeCodeSpawner\ - \ concrete impl as the minimum viable abstraction that proves the substrate\ - \ boundary on one role (refiner) and one phase (refine) end-to-end, with MessageBus\ - \ / PolicyEnforcer / WorktreeManager / HITLSurface ABCs accompanying it so the\ - \ interfaces land together. Substrate selection is an EGG_SUBSTRATE env var\ - \ (default 'k3s' for back-compat) read by a new egg_orchestrator/substrate.py\ - \ registry. Concrete decisions: ClaudeCodeSpawner uses Claude Code's Agent tool\ - \ with synchronous call-and-wait (cq-4) and a pre-created LocalWorktreeManager-owned\ - \ worktree (cq-5, not Claude Code's EnterWorktree); PreToolUseHookPolicy lives\ - \ at plugins/sdlc/hooks/pretooluse_policy.sh and imports shared/egg_restrictions/patterns.py\ - \ for single-source-of-truth (cq-6); HeredocHITLSurface yields HITLDecision\ - \ via a Python generator that the skill renders through AskUserQuestion (cq-7);\ - \ plugins/sdlc/.claude-plugin/plugin.json declares a pip dep on egg (cq-8).\ - \ Conformance is verified by parametrizing integration_tests/regression/test_brc_single_cycle.py\ - \ with a substrate fixture (cq-3); the remaining regression tests are deferred\ - \ to follow-ups. The analysis surfaces 48 file:line citations covering every\ - \ cited primitive (concurrent_executor, kubernetes_spawner, message_store/redis_message_store,\ - \ action_guards INV-1..INV-6, models.Contract schemaVersion 1.1, patterns.AgentFilePattern.can_write,\ - \ build_system_prompt, MCP check_file_restriction handler, refine-plan plugin\ - \ manifest precedent) and classifies each on the #2594 execution-context axes\ - \ (deployed-pod vs trusted-CI-runner vs the newly introduced parent-claude-code-session\ - \ context). It hands 9 candidate tasks to task_planner (each scoped to \u2264\ - 1 logical change), a 7-item risk list to risk_analyst (trust-boundary shift,\ - \ subagent ceiling, context window, isolation='worktree' availability, schemaVersion\ - \ rebase, RedisMessageStore drift, pip-install pre-flight), and an explicit\ - \ deferred-scope list (multi-role concurrency, plan/implement/pr phases, PreToolUse\ - \ Bash interception, EggHarnessSpawner, full regression matrix, k3s deprecation,\ - \ overseer thread, slice-DAG concurrency, telemetry review). Satisfies the plan-phase\ - \ architect contract task by providing the architecture analysis + component\ - \ breakdown that the task_planner and risk_analyst can build on without re-litigating\ - \ substrate-swap framing or HITL resolutions." - attestation: {} - artifacts: - - .egg-state/agent-outputs/2623-architect-output.json - risk_considered: "Risks for risk_analyst to formally rate: (1) trust-boundary\ - \ shift \u2014 PreToolUse hooks run in the user's local Claude Code interpreter,\ - \ not in a sandbox; the k3s gateway has defense-in-depth (NetworkPolicy + sidecar)\ - \ which the claude-code substrate trades for hook-correctness + user-install\ - \ integrity; (2) Agent tool subagent ceiling \u2014 the spike runs N=1 so doesn't\ - \ bite, but the follow-up parallel-reviewer dispatch may oversubscribe; (3)\ - \ subagent context window vs egg's max_turns=1000 \u2014 for a substantive issue\ - \ like #2623 itself, the refiner can approach the limit on the claude-code substrate\ - \ before cq-10's checkpoint+fork infrastructure lands; (4) isolation='worktree'\ - \ availability \u2014 design assumes stable CC exposes this; if developer-mode-only,\ - \ the spawner falls back to caller-managed worktrees (the design's cq-5 path\ - \ already does this); (5) Contract schemaVersion 1.1 rebases \u2014 any parallel\ - \ schema bump during the spike's review cycle requires a mechanical rebase;\ - \ (6) RedisMessageStore drift \u2014 the spike does NOT touch Redis paths, but\ - \ the MessageBus rename PR could inadvertently regress consumer-group semantics;\ - \ mitigation is to keep the rename mechanical; (7) pip-install pre-flight \u2014\ - \ marketplace install cannot enforce 'pip install egg', so the skill's bootstrap\ - \ must do an importlib.util.find_spec check and print actionable instructions\ - \ on miss. Mitigations are documented inline next to each risk in the JSON output;\ - \ the spike's exit criteria include 'completes on at least one substantive issue\ - \ without context exhaustion' to validate (3) empirically." - commit_sha: fb9192341b2c78dfd0ac0dad7c5d444733cacf97 - files_changed: - - .egg-state/agent-outputs/2623-architect-output.json - tests_run: [] - tasks_satisfied: [] - version: 1 - commit_sha: fb9192341b2c78dfd0ac0dad7c5d444733cacf97 -```` - -### [2026-05-19T00:06:21Z] risk_analyst (CONSENSUS_PROPOSE): Proposal from risk_analyst - -Risk assessment for substrate-swap (k3s -> claude-code-native) identifying 16 risks across security, design, performance, and compatibility. Overall risk HIGH driven by (a) credential trust-boundary inversion — Anthropic API key moves from server-side gateway to user's parent Claude session, exposing it to prompt-injection from subagents (R1); (b) PreToolUse-hook role-routing is an unverified runtime primitive — the chosen policy enforcement layer (cq-6 option 1) assumes hooks can read EGG_AGENT_ROLE for the calling subagent, which is not documented behavior (R2, #2594 class); (c) spike-then-plan slicing risks freezing the four abstraction interfaces (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager) from a single-role exercise (R10). Five runtime primitives flagged for explicit spike-time verification (Agent worktree isolation, hook role-routing, subagent concurrency ceiling, subagent context budget vs max_turns=1000, custom subagent_type via .claude/agents/). Five trust-boundary shifts documented (credentials, file-write timing, cost/rate-limit, agent liveness, push serialization). Recommendation: PROCEED_WITH_MITIGATIONS with five high-priority requirements — (1) spike's exit criteria must surface evidence on the five primitives; (2) ADR must explicitly accept the trust-shift, not bury it as implementation detail; (3) plan must classify the 14 regression tests as portable/k3s-only/claude-code-only; (4) consider expanding spike scope from 1 role to 2 (producer + reviewer) to exercise BRC consensus path; (5) EGG_PIPELINE_MAX_AGENT_INVOCATIONS cap for cost-control regression. Five risks (R1, R2, R4, R7, R10) flagged for explicit human review by reviewer_plan and the operator. - -````yaml -id: 3d0318a3-61c2-40 -phase: plan -metadata: - payload: - summary: "Risk assessment for substrate-swap (k3s -> claude-code-native) identifying\ - \ 16 risks across security, design, performance, and compatibility. Overall\ - \ risk HIGH driven by (a) credential trust-boundary inversion \u2014 Anthropic\ - \ API key moves from server-side gateway to user's parent Claude session, exposing\ - \ it to prompt-injection from subagents (R1); (b) PreToolUse-hook role-routing\ - \ is an unverified runtime primitive \u2014 the chosen policy enforcement layer\ - \ (cq-6 option 1) assumes hooks can read EGG_AGENT_ROLE for the calling subagent,\ - \ which is not documented behavior (R2, #2594 class); (c) spike-then-plan slicing\ - \ risks freezing the four abstraction interfaces (AgentSpawner/MessageBus/PolicyEnforcer/WorktreeManager)\ - \ from a single-role exercise (R10). Five runtime primitives flagged for explicit\ - \ spike-time verification (Agent worktree isolation, hook role-routing, subagent\ - \ concurrency ceiling, subagent context budget vs max_turns=1000, custom subagent_type\ - \ via .claude/agents/). Five trust-boundary shifts documented (credentials,\ - \ file-write timing, cost/rate-limit, agent liveness, push serialization). Recommendation:\ - \ PROCEED_WITH_MITIGATIONS with five high-priority requirements \u2014 (1) spike's\ - \ exit criteria must surface evidence on the five primitives; (2) ADR must explicitly\ - \ accept the trust-shift, not bury it as implementation detail; (3) plan must\ - \ classify the 14 regression tests as portable/k3s-only/claude-code-only; (4)\ - \ consider expanding spike scope from 1 role to 2 (producer + reviewer) to exercise\ - \ BRC consensus path; (5) EGG_PIPELINE_MAX_AGENT_INVOCATIONS cap for cost-control\ - \ regression. Five risks (R1, R2, R4, R7, R10) flagged for explicit human review\ - \ by reviewer_plan and the operator." - attestation: {} - artifacts: - - .egg-state/agent-outputs/2623-risk_analyst-output.json - risk_considered: '16 risks enumerated covering: (R1) credential trust-boundary - inversion HIGH; (R2) PreToolUse hook role-routing unverified primitive HIGH; - (R3) Agent-tool concurrency ceiling MEDIUM; (R4) in-process orchestrator reentrancy - with HITL generator MEDIUM; (R5) shared .git/ contention MEDIUM; (R6) integration_tests/regression/ - more substrate-coupled than analysis claims MEDIUM; (R7) subagent context budget - vs max_turns=1000 MEDIUM; (R8) plugin pip-install drift MEDIUM; (R9) Anthropic - API cost / rate-limit blast radius shifts to user MEDIUM; (R10) spike-then-plan - interface-freeze HIGH; (R11) EnterWorktree primitive drift LOW; (R12) BRC heartbeat - / stuck-agent detection regression MEDIUM; (R13) PyPI package distribution LOW; - (R14) #2612 SendMessage gating stays open LOW; (R15) custom subagent_type vs - prompt-injected roles LOW; (R16) cross-substrate test-failure diagnostic shift - LOW. Five trust-boundary shifts and five runtime-primitive risks called out - per #2594.' - commit_sha: 363e8d6d3 - files_changed: - - .egg-state/agent-outputs/2623-risk_analyst-output.json - tests_run: [] - tasks_satisfied: [] - version: 1 - commit_sha: 363e8d6d3 -```` - -### [2026-05-19T00:06:21Z] orchestrator → risk_analyst (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: 6dd06618-fa9d-4f -phase: plan -metadata: - ready_to_confirm: true - version: 1 -```` - -### [2026-05-19T00:06:47Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: dcb14343-fc68-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:06:30.929609+00:00' -```` - -### [2026-05-19T00:06:49Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 33902797-5f97-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:07:45Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 2756c5ac-4d01-4e -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:07:23.523495+00:00' -```` - -### [2026-05-19T00:07:45Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 59563725-6243-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:06:30.929609+00:00' -```` - -### [2026-05-19T00:08:13Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3b161a34-7ab4-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:08:40Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b7a696b4-b3e1-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:07:23.523495+00:00' -```` - -### [2026-05-19T00:08:46Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 11b509ae-1582-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:06:30.929609+00:00' -```` - -### [2026-05-19T00:09:04Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 32c2e17f-241d-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:09:40Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 726f49dc-6595-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:07:23.523495+00:00' -```` - -### [2026-05-19T00:09:46Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7a47dd4c-e4d0-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:06:30.929609+00:00' -```` - -### [2026-05-19T00:10:04Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ac8a1088-0764-45 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:10:40Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d0d68d88-f50b-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:07:23.523495+00:00' -```` - -### [2026-05-19T00:10:46Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ea44faba-af20-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:06:30.929609+00:00' -```` - -### [2026-05-19T00:11:04Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 50ce881e-ac97-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:11:40Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e42e28cd-ee19-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:07:23.523495+00:00' -```` - -### [2026-05-19T00:11:42Z] orchestrator → risk_analyst (OVERSEER_ALERT): BRC confirmation timeout — call mcp__brc__confirm - -You are PROPOSED and fully ACKed but have not confirmed in 301s. Call `mcp__brc__confirm` now. If it returns `status='pending_acks'`, read `message` for the guard reason and wait on the prerequisite events instead: `CONSENSUS_PROPOSE` if a producer hasn't proposed (`zero_proposal_producers`), `CONSENSUS_ACK` / `CONSENSUS_RE_REVIEW` if a reviewer's ACK is stale or unresolved. Then retry confirm. - -````yaml -id: 3cd1ffc8-c95f-44 -phase: plan -metadata: - alert_type: brc_confirmation_timeout - elapsed_seconds: 301 - source: health_monitor -```` - -### [2026-05-19T00:11:44Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 2967aedb-ad19-4c -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:11:48Z] risk_analyst (CONSENSUS_CONFIRMED): Confirmed by risk_analyst - -````yaml -id: 18fd4de7-8b5e-4e -phase: plan -metadata: - consensus_reached: false -```` - -### [2026-05-19T00:11:57Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 3254ac30-eeae-45 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:11:57.004124+00:00' -```` - -### [2026-05-19T00:12:05Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 0b320675-85d3-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:12:40Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c9e7b1c4-a4f3-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:07:23.523495+00:00' -```` - -### [2026-05-19T00:12:57Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 19b0b3f0-ad18-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:11:57.004124+00:00' -```` - -### [2026-05-19T00:13:05Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7fa237c4-c766-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:05:35.038235+00:00' -```` - -### [2026-05-19T00:13:40Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 2dca04aa-c3bc-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:07:23.523495+00:00' -```` - -### [2026-05-19T00:13:51Z] reviewer_plan → task_planner (CONSENSUS_NACK): NACK from reviewer_plan for task_planner - - -### Blocking - -1. **`.egg-state/drafts/2623-plan.md` §"Trust-boundary scope" + Primitives table — non-existent fixture cited (§9 primitive-existence audit failure).** The plan cites `local_pipeline/conftest.py:261 gateway_url fixture` as load-bearing evidence that the parametrized substrate test is "already kubectl-gated through `local_pipeline_stack` (via the parent `integration_tests/conftest.py:78` `EggStack` dataclass + the `local_pipeline/conftest.py:261` `gateway_url` fixture)". Grep verdict: `ls integration_tests/local_pipeline/conftest.py` → "No such file or directory". The directory was deleted in commit `f7803637d1 test: delete deprecated local_pipeline + squid tests; file follow-up issues`. The trust-boundary doc on this branch is stale, but the plan cannot ride on a deleted file. **Fix:** drop the `local_pipeline/conftest.py:261` reference; rewrite the Trust-Boundary section against the actual fixture layout: `EggStack` at `integration_tests/conftest.py:71`, `orchestrator_url` fixture at `integration_tests/conftest.py:357`, `egg_stack` fixture at `integration_tests/conftest.py:340`. - -2. **`.egg-state/drafts/2623-plan.md` Trust-Boundary claim "test_brc_single_cycle.py is already kubectl-gated through local_pipeline_stack" — factually wrong.** Read `integration_tests/regression/test_brc_single_cycle.py`: it uses `single_reviewer_graph` (regression/conftest.py:564), `event_capture` (regression/conftest.py:519), and `filter_events` (regression/conftest.py:552), then drives the orchestrator's BRC Python API directly via `make_tracker` (`integration_tests/regression/_helpers.py:156`) and `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`). The test does not touch kubectl, does not depend on `egg_stack`/`local_pipeline_stack`, and never instantiates a spawner or message bus. Both `substrate="k3s"` and `substrate="claude-code"` parameters would run identical Python code — the test is not a substrate-distinguishing artifact. **Fix:** pick a test (existing or new) that actually exercises `AgentSpawner.spawn(...)` and/or `MessageBus.add_message/get_messages` so the two substrate parameters can produce divergent execution paths. Without that, TASK-1-8's "matrix proof" is vacuous. - -3. **Internal contradiction between TASK-1-1 and TASK-1-2 about the k3s adapter — plan is incoherent on whether k3s still works.** TASK-1-1 acceptance #3 (lines 222-226): "`select_substrate({})` … returns the bundle pointing at the k3s implementations *but* the k3s adapters raise `NotImplementedError` from the protocol seam with a clear message naming the follow-up issue — this is the explicit TODO marker". TASK-1-2 description (lines 257-266): "the k3s code path keeps working via the (deferred) k3s adapter raising `NotImplementedError`; this is acceptable because the spike's acceptance is claude-code-substrate-only, **and the regression matrix keeps `EGG_SUBSTRATE=k3s` selecting the existing in-tree `KubernetesSpawner`**" and TASK-1-2 acceptance #4 (lines 280-283): "`orchestrator/concurrent_executor.py:504` is patched to dispatch through `select_substrate(...)`; the existing k3s path continues to work unchanged **because the k3s implementation of the protocol is a thin shim around the existing `KubernetesSpawner.spawn_agent_job`**". The two are mutually exclusive — either the k3s adapter is a working shim around `spawn_agent_job` (TASK-1-2 description) or it raises `NotImplementedError` (TASK-1-1). If TASK-1-2 patches `concurrent_executor.py:504` (the only call site to `spawn_fn`) to dispatch through a `select_substrate(...)` bundle whose k3s leg raises `NotImplementedError`, every k3s deployment is broken on this PR. **Fix:** pick one model and apply consistently. The architect's tasks_for_task_planner_to_pick_up #1 prescribes the working-shim approach: refactor the existing `create_concurrent_spawn_fn` at `orchestrator/kubernetes_spawner.py:1564` to return an `AgentSpawner`-conforming object — that path keeps k3s green. If you instead want the `NotImplementedError` k3s leg, do not also rewrite `concurrent_executor.py:504`; leave the k3s path on the direct import for this spike. - -4. **AgentResult missing `commit_sha` field → BRC INV-6 violation, hard correctness regression.** TASK-1-2 acceptance #2 defines `AgentResult` with `stdout, exit_code, duration_seconds, worktree` — no `commit_sha`. `orchestrator/action_guards.py::validate_invariants` enforces INV-6 (ack_commit_sha consistency) — the orchestrator must know each agent's resulting commit SHA so reviewers can attach commit-bound ACKs and the consensus tracker can detect commit drift between proposal and ack. The architect's spawner-component design called this out explicitly: "the spawner runs `git -C rev-parse HEAD` and records the SHA in AgentResult.commit_sha so the orchestrator can attach commit-bound ACKs (action_guards.validate_invariants INV-6 at orchestrator/action_guards.py:753 requires this)" (architect output, component "egg_orchestrator/spawners/claude_code_spawner.py", design.commit_sha_capture). Dropping commit_sha makes the spawner unable to satisfy the existing BRC invariant. **Fix:** add `commit_sha: str | None` to the `AgentResult` dataclass shape in TASK-1-1 and require capture-via-`git rev-parse HEAD` in TASK-1-2's acceptance criteria. - -5. **TASK-1-5 cites a non-existent path-escape primitive (§9 primitive-existence audit failure).** Plan line 380-382: "Safety: `tear_down` resolves the target through `path.resolve().is_relative_to(base.resolve())` so a malicious pipeline ID can't escape the base — **same defense as `gateway/gateway.py:7903`**." Grep verdict: `grep -n 'is_relative_to\|path.resolve()' gateway/gateway.py` returns zero hits. The defense pattern actually lives at `gateway/worktree_manager.py:1711` (call site) and `gateway/phase_api.py:96-108` (an unrelated phase-api path-resolve). **Fix:** cite `gateway/worktree_manager.py:1711` as the reference implementation. Mis-citing primitives in acceptance criteria pushes the coder into the same hunt I just did — and risks them inventing a different defense. - -6. **TASK-1-3 acceptance #2 cites BRC unit tests in the wrong directory (§9 primitive-existence audit failure).** Plan line 308-311: "Existing BRC unit tests under **`shared/tests/test_brc_*`** (or wherever they live) pass when run against `InProcessMessageBus`". Grep verdict: `ls shared/tests/` returns `test_agent_roles_has_contract.py / test_cross_process_lock.py / test_egg_harness / test_egg_health.py / test_egg_restrictions.py / test_egg_restrictions_hints.py / test_github_block_alignment.py / test_impasse_schema.py / test_overseer_advisor.py` — zero `test_brc_*` files. The BRC unit tests are at `orchestrator/tests/test_brc_*` (15+ files: `test_brc_confirmation_nudge.py`, `test_brc_content_validation.py`, `test_brc_history.py`, `test_brc_nack_iteration.py`, `test_brc_open_nacks_barrier.py`, `test_brc_phase_propagation.py`, plus `test_auto_ack_pure_producers.py` which exercises `PeerConsensusTracker` directly). The "or wherever they live" hedge is not adequate — the tester role needs a precise path-set to parametrize. **Fix:** point TASK-1-3 acceptance #2 at the actual BRC test directory (`orchestrator/tests/test_brc_*.py`) and confirm the tester role is allowed to write there. If not, the cross-substrate BRC-invariant test belongs in `shared/tests/test_in_process_message_bus.py` (which TASK-1-8 already creates) — say so explicitly instead of vaguely deferring. - -7. **TASK-1-6 ignores the orchestrator-reentrancy problem flagged by risk_analyst R4.** TASK-1-6 (lines 387-419) says "Build `run_pipeline_in_process(issue, role, ...)`" and "instantiates `select_substrate(os.environ)`, runs the refine phase with refiner-only". The orchestrator today is a Flask/waitress HTTP daemon (`orchestrator/cli.py:83-150 cmd_serve` — verified). It has no in-process API. `ConcurrentPhaseExecutor` (line 114) uses a `ThreadPoolExecutor` for background dispatch; `PeerConsensusTracker` holds its own lock and runs message poll loops. Wrapping all of this in a Python generator that yields `HITLDecision` and resumes via `.send()` is a non-trivial refactor — the risk_analyst R4 marks this `requires_human_review: true` with `effort: HIGH`. The plan offers neither a refactor task nor a HITL gate, just an acceptance bullet ("the generator runs the refiner through to completion"). **Fix:** either (a) split TASK-1-6 into a "build in-process orchestrator entry point" task with its own acceptance criteria covering heartbeat-during-HITL, background-thread lifetime, and contract-state synchronization; or (b) register a HITL decision asking the operator to bound the generator/AskUserQuestion model before this slice merges. Calling it a one-line acceptance bullet is hiding hours of refactor work. - -8. **No risk-mitigation tasks for the five risk_analyst HIGH-severity / human-review-required items (R1, R2, R4, R7, R10).** The risk_analyst's proposal flagged five risks requiring human review: R1 credential trust-boundary inversion (security, HIGH/CERTAIN), R2 PreToolUse hook role-routing unverified (design, HIGH/HIGH), R4 in-process orchestrator reentrancy (covered above), R7 subagent context budget vs deep-refine (performance, MEDIUM/HIGH), R10 single-role spike freezing interfaces (design, HIGH/MEDIUM). The plan acknowledges none of these as task-level concerns, decisions, or HITL gates. R1 in particular — moving the Anthropic credential from a gateway-isolated process into the user's parent session — is a structural threat-model change that the ADR must document and the operator must explicitly accept (per risk_analyst recommendation_rationale and REC2). The plan's TASK-1-9 ADR acceptance does NOT call this out — bullets only cover the 11 cq decisions, 6 feedback items, and primitives table. **Fix:** add explicit acceptance criteria in TASK-1-9 for each of R1, R2, R7, R10 ("ADR's 'Trust-context shift' subsection names credential exposure"; "ADR documents PreToolUse hook role-routing fallback path to MCP-validator-side enforcement (cq-6 opt-2) if spike evidence is negative"; "ADR documents subagent context budget regression and the smaller-budget fallback per cq-10 opt-2"; "interfaces marked unstable / v0.x until ≥3 roles exercise them"). Without this, the spike encodes design decisions that the second wave will need to revisit, expensively. - -9. **TASK-1-8 acceptance #2 — kubectl gating claim does not match the chosen test.** "the parametrized regression test passes under both substrates locally (kubectl available for k3s; no kubectl needed for claude-code)". The chosen test (`test_brc_single_cycle.py`) does not require kubectl for any parameter — see blocking issue #2. **Fix:** if you keep this test as the parametrize target, drop the "kubectl available for k3s" half because both parameters run pure-Python. Better: replace the chosen test as in blocking issue #2. - -10. **PyPI package name `egg-shared` is invented (TASK-1-7 and TASK-1-11).** Plan line 432 and line 569: "lists `egg-shared` (or the canonical pip name) as a required dep". There is no PyPI package called `egg-shared`. The risk_analyst R13 flagged this: the `egg` name is already taken on PyPI, and no published wheel exists for this repo. Picking `egg-shared` without operator authority pre-empts a packaging decision the project hasn't made. **Fix:** either register a HITL decision asking the operator to pick the canonical pip name (and what registry — PyPI vs git+https), or accept the dep declaration as a TODO placeholder string with a comment, not as a real install instruction. The current language ("or the canonical pip name") will produce a coder asking the same question downstream. - -11. **Primitive line numbers off — minor but a pattern.** Plan line 124: "`integration_tests/conftest.py:78` `EggStack` dataclass". Grep: `class EggStack` is at `integration_tests/conftest.py:71`, not :78. (line 78 is the `gateway_url: str` field inside the dataclass.) Plan line 121-123 lists `class HITLDecision` at `orchestrator/models.py:300` — that one's correct. **Fix:** scan the Primitives table for off-by-N line numbers; cite the class definition line, not a field-in-class line. - -### Non-blocking - -- **Slice-sizing advisory (#2137 size advisory; never NACK):** the plan's own LOC estimate is 1,800–2,500 LOC, over the 1k soft target. Per HITL decision-6 opt-2 I will not NACK on size; this is informational only. The plan acknowledges this and ties it to cq-11's spike-shape choice — that justification holds, but the next reviewer of the implement-phase BRC cycle should know the slice will be expensive to NACK. -- **Consider expanding spike to 2 roles (risk_analyst REC4).** A single-role spike does not exercise the BRC consensus path (PROPOSE → ACK), which is the actual structural concern the orchestrator-as-bus model needs to prove. A `refiner + reviewer_refine` pair stays within cq-11's "walking skeleton" framing but exercises producer dispatch AND review verdict round-trip. Operator decision; not a blocker. -- **Subagent type model (R15).** The plan implicitly assumes `subagent_type='general-purpose'` (matching the existing refine-plan skill) but does not commit. Custom `subagent_type` via `.claude/agents/.md` gives structural tool restrictions per role; the existing model relies on prompt discipline + PreToolUse hooks. Picking one and documenting in the ADR would tighten the design. -- **Cost cap (risk_analyst REC5).** Consider adding an `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` env var with conservative default. Without it, a buggy NACK loop bills directly to the user's Anthropic account — a real regression from the gateway model. -- **TASK-1-2 list of files modified should explicitly include `orchestrator/concurrent_executor.py`** (it does — line 288, good) — but the acceptance criteria say "verified by an `assert isinstance(...)` in a unit test"; consider also a smoke test that `_spawn_agent` dispatches through the bundle, otherwise the contradiction in blocking #3 could land silently. -- **TASK-1-1 Files list does not include `orchestrator/substrate/k3s_adapter.py`** — but TASK-1-2 introduces it. If the adapter is the k3s leg of `select_substrate({})`, it logically belongs to TASK-1-1's deliverable surface. Re-organize so the protocol module and the k3s adapter shim ship together, or document that TASK-1-1's k3s leg is a stub and TASK-1-2 replaces it with the shim. -- **Plan's "Manual verification" step 3 ("HITL surfaces through AskUserQuestion")** requires a working in-process generator with HITL round-trip — circular with blocking #7. Resolve that first. - - -````yaml -id: 09645a63-a19a-49 -phase: plan -metadata: - payload: - reason: "\n### Blocking\n\n1. **`.egg-state/drafts/2623-plan.md` \xA7\"Trust-boundary\ - \ scope\" + Primitives table \u2014 non-existent fixture cited (\xA79 primitive-existence\ - \ audit failure).** The plan cites `local_pipeline/conftest.py:261 gateway_url\ - \ fixture` as load-bearing evidence that the parametrized substrate test is\ - \ \"already kubectl-gated through `local_pipeline_stack` (via the parent `integration_tests/conftest.py:78`\ - \ `EggStack` dataclass + the `local_pipeline/conftest.py:261` `gateway_url`\ - \ fixture)\". Grep verdict: `ls integration_tests/local_pipeline/conftest.py`\ - \ \u2192 \"No such file or directory\". The directory was deleted in commit\ - \ `f7803637d1 test: delete deprecated local_pipeline + squid tests; file follow-up\ - \ issues`. The trust-boundary doc on this branch is stale, but the plan cannot\ - \ ride on a deleted file. **Fix:** drop the `local_pipeline/conftest.py:261`\ - \ reference; rewrite the Trust-Boundary section against the actual fixture layout:\ - \ `EggStack` at `integration_tests/conftest.py:71`, `orchestrator_url` fixture\ - \ at `integration_tests/conftest.py:357`, `egg_stack` fixture at `integration_tests/conftest.py:340`.\n\ - \n2. **`.egg-state/drafts/2623-plan.md` Trust-Boundary claim \"test_brc_single_cycle.py\ - \ is already kubectl-gated through local_pipeline_stack\" \u2014 factually wrong.**\ - \ Read `integration_tests/regression/test_brc_single_cycle.py`: it uses `single_reviewer_graph`\ - \ (regression/conftest.py:564), `event_capture` (regression/conftest.py:519),\ - \ and `filter_events` (regression/conftest.py:552), then drives the orchestrator's\ - \ BRC Python API directly via `make_tracker` (`integration_tests/regression/_helpers.py:156`)\ - \ and `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`). The test\ - \ does not touch kubectl, does not depend on `egg_stack`/`local_pipeline_stack`,\ - \ and never instantiates a spawner or message bus. Both `substrate=\"k3s\"`\ - \ and `substrate=\"claude-code\"` parameters would run identical Python code\ - \ \u2014 the test is not a substrate-distinguishing artifact. **Fix:** pick\ - \ a test (existing or new) that actually exercises `AgentSpawner.spawn(...)`\ - \ and/or `MessageBus.add_message/get_messages` so the two substrate parameters\ - \ can produce divergent execution paths. Without that, TASK-1-8's \"matrix proof\"\ - \ is vacuous.\n\n3. **Internal contradiction between TASK-1-1 and TASK-1-2 about\ - \ the k3s adapter \u2014 plan is incoherent on whether k3s still works.** TASK-1-1\ - \ acceptance #3 (lines 222-226): \"`select_substrate({})` \u2026 returns the\ - \ bundle pointing at the k3s implementations *but* the k3s adapters raise `NotImplementedError`\ - \ from the protocol seam with a clear message naming the follow-up issue \u2014\ - \ this is the explicit TODO marker\". TASK-1-2 description (lines 257-266):\ - \ \"the k3s code path keeps working via the (deferred) k3s adapter raising `NotImplementedError`;\ - \ this is acceptable because the spike's acceptance is claude-code-substrate-only,\ - \ **and the regression matrix keeps `EGG_SUBSTRATE=k3s` selecting the existing\ - \ in-tree `KubernetesSpawner`**\" and TASK-1-2 acceptance #4 (lines 280-283):\ - \ \"`orchestrator/concurrent_executor.py:504` is patched to dispatch through\ - \ `select_substrate(...)`; the existing k3s path continues to work unchanged\ - \ **because the k3s implementation of the protocol is a thin shim around the\ - \ existing `KubernetesSpawner.spawn_agent_job`**\". The two are mutually exclusive\ - \ \u2014 either the k3s adapter is a working shim around `spawn_agent_job` (TASK-1-2\ - \ description) or it raises `NotImplementedError` (TASK-1-1). If TASK-1-2 patches\ - \ `concurrent_executor.py:504` (the only call site to `spawn_fn`) to dispatch\ - \ through a `select_substrate(...)` bundle whose k3s leg raises `NotImplementedError`,\ - \ every k3s deployment is broken on this PR. **Fix:** pick one model and apply\ - \ consistently. The architect's tasks_for_task_planner_to_pick_up #1 prescribes\ - \ the working-shim approach: refactor the existing `create_concurrent_spawn_fn`\ - \ at `orchestrator/kubernetes_spawner.py:1564` to return an `AgentSpawner`-conforming\ - \ object \u2014 that path keeps k3s green. If you instead want the `NotImplementedError`\ - \ k3s leg, do not also rewrite `concurrent_executor.py:504`; leave the k3s path\ - \ on the direct import for this spike.\n\n4. **AgentResult missing `commit_sha`\ - \ field \u2192 BRC INV-6 violation, hard correctness regression.** TASK-1-2\ - \ acceptance #2 defines `AgentResult` with `stdout, exit_code, duration_seconds,\ - \ worktree` \u2014 no `commit_sha`. `orchestrator/action_guards.py::validate_invariants`\ - \ enforces INV-6 (ack_commit_sha consistency) \u2014 the orchestrator must know\ - \ each agent's resulting commit SHA so reviewers can attach commit-bound ACKs\ - \ and the consensus tracker can detect commit drift between proposal and ack.\ - \ The architect's spawner-component design called this out explicitly: \"the\ - \ spawner runs `git -C rev-parse HEAD` and records the SHA in\ - \ AgentResult.commit_sha so the orchestrator can attach commit-bound ACKs (action_guards.validate_invariants\ - \ INV-6 at orchestrator/action_guards.py:753 requires this)\" (architect output,\ - \ component \"egg_orchestrator/spawners/claude_code_spawner.py\", design.commit_sha_capture).\ - \ Dropping commit_sha makes the spawner unable to satisfy the existing BRC invariant.\ - \ **Fix:** add `commit_sha: str | None` to the `AgentResult` dataclass shape\ - \ in TASK-1-1 and require capture-via-`git rev-parse HEAD` in TASK-1-2's acceptance\ - \ criteria.\n\n5. **TASK-1-5 cites a non-existent path-escape primitive (\xA7\ - 9 primitive-existence audit failure).** Plan line 380-382: \"Safety: `tear_down`\ - \ resolves the target through `path.resolve().is_relative_to(base.resolve())`\ - \ so a malicious pipeline ID can't escape the base \u2014 **same defense as\ - \ `gateway/gateway.py:7903`**.\" Grep verdict: `grep -n 'is_relative_to\\|path.resolve()'\ - \ gateway/gateway.py` returns zero hits. The defense pattern actually lives\ - \ at `gateway/worktree_manager.py:1711` (call site) and `gateway/phase_api.py:96-108`\ - \ (an unrelated phase-api path-resolve). **Fix:** cite `gateway/worktree_manager.py:1711`\ - \ as the reference implementation. Mis-citing primitives in acceptance criteria\ - \ pushes the coder into the same hunt I just did \u2014 and risks them inventing\ - \ a different defense.\n\n6. **TASK-1-3 acceptance #2 cites BRC unit tests in\ - \ the wrong directory (\xA79 primitive-existence audit failure).** Plan line\ - \ 308-311: \"Existing BRC unit tests under **`shared/tests/test_brc_*`** (or\ - \ wherever they live) pass when run against `InProcessMessageBus`\". Grep verdict:\ - \ `ls shared/tests/` returns `test_agent_roles_has_contract.py / test_cross_process_lock.py\ - \ / test_egg_harness / test_egg_health.py / test_egg_restrictions.py / test_egg_restrictions_hints.py\ - \ / test_github_block_alignment.py / test_impasse_schema.py / test_overseer_advisor.py`\ - \ \u2014 zero `test_brc_*` files. The BRC unit tests are at `orchestrator/tests/test_brc_*`\ - \ (15+ files: `test_brc_confirmation_nudge.py`, `test_brc_content_validation.py`,\ - \ `test_brc_history.py`, `test_brc_nack_iteration.py`, `test_brc_open_nacks_barrier.py`,\ - \ `test_brc_phase_propagation.py`, plus `test_auto_ack_pure_producers.py` which\ - \ exercises `PeerConsensusTracker` directly). The \"or wherever they live\"\ - \ hedge is not adequate \u2014 the tester role needs a precise path-set to parametrize.\ - \ **Fix:** point TASK-1-3 acceptance #2 at the actual BRC test directory (`orchestrator/tests/test_brc_*.py`)\ - \ and confirm the tester role is allowed to write there. If not, the cross-substrate\ - \ BRC-invariant test belongs in `shared/tests/test_in_process_message_bus.py`\ - \ (which TASK-1-8 already creates) \u2014 say so explicitly instead of vaguely\ - \ deferring.\n\n7. **TASK-1-6 ignores the orchestrator-reentrancy problem flagged\ - \ by risk_analyst R4.** TASK-1-6 (lines 387-419) says \"Build `run_pipeline_in_process(issue,\ - \ role, ...)`\" and \"instantiates `select_substrate(os.environ)`, runs the\ - \ refine phase with refiner-only\". The orchestrator today is a Flask/waitress\ - \ HTTP daemon (`orchestrator/cli.py:83-150 cmd_serve` \u2014 verified). It has\ - \ no in-process API. `ConcurrentPhaseExecutor` (line 114) uses a `ThreadPoolExecutor`\ - \ for background dispatch; `PeerConsensusTracker` holds its own lock and runs\ - \ message poll loops. Wrapping all of this in a Python generator that yields\ - \ `HITLDecision` and resumes via `.send()` is a non-trivial refactor \u2014\ - \ the risk_analyst R4 marks this `requires_human_review: true` with `effort:\ - \ HIGH`. The plan offers neither a refactor task nor a HITL gate, just an acceptance\ - \ bullet (\"the generator runs the refiner through to completion\"). **Fix:**\ - \ either (a) split TASK-1-6 into a \"build in-process orchestrator entry point\"\ - \ task with its own acceptance criteria covering heartbeat-during-HITL, background-thread\ - \ lifetime, and contract-state synchronization; or (b) register a HITL decision\ - \ asking the operator to bound the generator/AskUserQuestion model before this\ - \ slice merges. Calling it a one-line acceptance bullet is hiding hours of refactor\ - \ work.\n\n8. **No risk-mitigation tasks for the five risk_analyst HIGH-severity\ - \ / human-review-required items (R1, R2, R4, R7, R10).** The risk_analyst's\ - \ proposal flagged five risks requiring human review: R1 credential trust-boundary\ - \ inversion (security, HIGH/CERTAIN), R2 PreToolUse hook role-routing unverified\ - \ (design, HIGH/HIGH), R4 in-process orchestrator reentrancy (covered above),\ - \ R7 subagent context budget vs deep-refine (performance, MEDIUM/HIGH), R10\ - \ single-role spike freezing interfaces (design, HIGH/MEDIUM). The plan acknowledges\ - \ none of these as task-level concerns, decisions, or HITL gates. R1 in particular\ - \ \u2014 moving the Anthropic credential from a gateway-isolated process into\ - \ the user's parent session \u2014 is a structural threat-model change that\ - \ the ADR must document and the operator must explicitly accept (per risk_analyst\ - \ recommendation_rationale and REC2). The plan's TASK-1-9 ADR acceptance does\ - \ NOT call this out \u2014 bullets only cover the 11 cq decisions, 6 feedback\ - \ items, and primitives table. **Fix:** add explicit acceptance criteria in\ - \ TASK-1-9 for each of R1, R2, R7, R10 (\"ADR's 'Trust-context shift' subsection\ - \ names credential exposure\"; \"ADR documents PreToolUse hook role-routing\ - \ fallback path to MCP-validator-side enforcement (cq-6 opt-2) if spike evidence\ - \ is negative\"; \"ADR documents subagent context budget regression and the\ - \ smaller-budget fallback per cq-10 opt-2\"; \"interfaces marked unstable /\ - \ v0.x until \u22653 roles exercise them\"). Without this, the spike encodes\ - \ design decisions that the second wave will need to revisit, expensively.\n\ - \n9. **TASK-1-8 acceptance #2 \u2014 kubectl gating claim does not match the\ - \ chosen test.** \"the parametrized regression test passes under both substrates\ - \ locally (kubectl available for k3s; no kubectl needed for claude-code)\".\ - \ The chosen test (`test_brc_single_cycle.py`) does not require kubectl for\ - \ any parameter \u2014 see blocking issue #2. **Fix:** if you keep this test\ - \ as the parametrize target, drop the \"kubectl available for k3s\" half because\ - \ both parameters run pure-Python. Better: replace the chosen test as in blocking\ - \ issue #2.\n\n10. **PyPI package name `egg-shared` is invented (TASK-1-7 and\ - \ TASK-1-11).** Plan line 432 and line 569: \"lists `egg-shared` (or the canonical\ - \ pip name) as a required dep\". There is no PyPI package called `egg-shared`.\ - \ The risk_analyst R13 flagged this: the `egg` name is already taken on PyPI,\ - \ and no published wheel exists for this repo. Picking `egg-shared` without\ - \ operator authority pre-empts a packaging decision the project hasn't made.\ - \ **Fix:** either register a HITL decision asking the operator to pick the canonical\ - \ pip name (and what registry \u2014 PyPI vs git+https), or accept the dep declaration\ - \ as a TODO placeholder string with a comment, not as a real install instruction.\ - \ The current language (\"or the canonical pip name\") will produce a coder\ - \ asking the same question downstream.\n\n11. **Primitive line numbers off \u2014\ - \ minor but a pattern.** Plan line 124: \"`integration_tests/conftest.py:78`\ - \ `EggStack` dataclass\". Grep: `class EggStack` is at `integration_tests/conftest.py:71`,\ - \ not :78. (line 78 is the `gateway_url: str` field inside the dataclass.) Plan\ - \ line 121-123 lists `class HITLDecision` at `orchestrator/models.py:300` \u2014\ - \ that one's correct. **Fix:** scan the Primitives table for off-by-N line numbers;\ - \ cite the class definition line, not a field-in-class line.\n\n### Non-blocking\n\ - \n- **Slice-sizing advisory (#2137 size advisory; never NACK):** the plan's\ - \ own LOC estimate is 1,800\u20132,500 LOC, over the 1k soft target. Per HITL\ - \ decision-6 opt-2 I will not NACK on size; this is informational only. The\ - \ plan acknowledges this and ties it to cq-11's spike-shape choice \u2014 that\ - \ justification holds, but the next reviewer of the implement-phase BRC cycle\ - \ should know the slice will be expensive to NACK.\n- **Consider expanding spike\ - \ to 2 roles (risk_analyst REC4).** A single-role spike does not exercise the\ - \ BRC consensus path (PROPOSE \u2192 ACK), which is the actual structural concern\ - \ the orchestrator-as-bus model needs to prove. A `refiner + reviewer_refine`\ - \ pair stays within cq-11's \"walking skeleton\" framing but exercises producer\ - \ dispatch AND review verdict round-trip. Operator decision; not a blocker.\n\ - - **Subagent type model (R15).** The plan implicitly assumes `subagent_type='general-purpose'`\ - \ (matching the existing refine-plan skill) but does not commit. Custom `subagent_type`\ - \ via `.claude/agents/.md` gives structural tool restrictions per role;\ - \ the existing model relies on prompt discipline + PreToolUse hooks. Picking\ - \ one and documenting in the ADR would tighten the design.\n- **Cost cap (risk_analyst\ - \ REC5).** Consider adding an `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` env var with\ - \ conservative default. Without it, a buggy NACK loop bills directly to the\ - \ user's Anthropic account \u2014 a real regression from the gateway model.\n\ - - **TASK-1-2 list of files modified should explicitly include `orchestrator/concurrent_executor.py`**\ - \ (it does \u2014 line 288, good) \u2014 but the acceptance criteria say \"\ - verified by an `assert isinstance(...)` in a unit test\"; consider also a smoke\ - \ test that `_spawn_agent` dispatches through the bundle, otherwise the contradiction\ - \ in blocking #3 could land silently.\n- **TASK-1-1 Files list does not include\ - \ `orchestrator/substrate/k3s_adapter.py`** \u2014 but TASK-1-2 introduces it.\ - \ If the adapter is the k3s leg of `select_substrate({})`, it logically belongs\ - \ to TASK-1-1's deliverable surface. Re-organize so the protocol module and\ - \ the k3s adapter shim ship together, or document that TASK-1-1's k3s leg is\ - \ a stub and TASK-1-2 replaces it with the shim.\n- **Plan's \"Manual verification\"\ - \ step 3 (\"HITL surfaces through AskUserQuestion\")** requires a working in-process\ - \ generator with HITL round-trip \u2014 circular with blocking #7. Resolve that\ - \ first.\n" - artifact_references: - - .egg-state/drafts/2623-plan.md - - .egg-state/drafts/2623-analysis.md - - orchestrator/concurrent_executor.py - - orchestrator/kubernetes_spawner.py - - orchestrator/message_store.py - - orchestrator/models.py - - orchestrator/peer_consensus.py - - orchestrator/tests/test_auto_ack_pure_producers.py - - shared/egg_harness/prompt.py - - shared/egg_restrictions/patterns.py - - gateway/worktree_manager.py - - gateway/phase_filter.py - - gateway/gateway.py - - integration_tests/regression/conftest.py - - integration_tests/regression/test_brc_single_cycle.py - - integration_tests/conftest.py - - docs/architecture/integration-test-trust-boundary.md - nack_version: 1 - reason: "\n### Blocking\n\n1. **`.egg-state/drafts/2623-plan.md` \xA7\"Trust-boundary\ - \ scope\" + Primitives table \u2014 non-existent fixture cited (\xA79 primitive-existence\ - \ audit failure).** The plan cites `local_pipeline/conftest.py:261 gateway_url\ - \ fixture` as load-bearing evidence that the parametrized substrate test is \"\ - already kubectl-gated through `local_pipeline_stack` (via the parent `integration_tests/conftest.py:78`\ - \ `EggStack` dataclass + the `local_pipeline/conftest.py:261` `gateway_url` fixture)\"\ - . Grep verdict: `ls integration_tests/local_pipeline/conftest.py` \u2192 \"No\ - \ such file or directory\". The directory was deleted in commit `f7803637d1 test:\ - \ delete deprecated local_pipeline + squid tests; file follow-up issues`. The\ - \ trust-boundary doc on this branch is stale, but the plan cannot ride on a deleted\ - \ file. **Fix:** drop the `local_pipeline/conftest.py:261` reference; rewrite\ - \ the Trust-Boundary section against the actual fixture layout: `EggStack` at\ - \ `integration_tests/conftest.py:71`, `orchestrator_url` fixture at `integration_tests/conftest.py:357`,\ - \ `egg_stack` fixture at `integration_tests/conftest.py:340`.\n\n2. **`.egg-state/drafts/2623-plan.md`\ - \ Trust-Boundary claim \"test_brc_single_cycle.py is already kubectl-gated through\ - \ local_pipeline_stack\" \u2014 factually wrong.** Read `integration_tests/regression/test_brc_single_cycle.py`:\ - \ it uses `single_reviewer_graph` (regression/conftest.py:564), `event_capture`\ - \ (regression/conftest.py:519), and `filter_events` (regression/conftest.py:552),\ - \ then drives the orchestrator's BRC Python API directly via `make_tracker` (`integration_tests/regression/_helpers.py:156`)\ - \ and `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`). The test does\ - \ not touch kubectl, does not depend on `egg_stack`/`local_pipeline_stack`, and\ - \ never instantiates a spawner or message bus. Both `substrate=\"k3s\"` and `substrate=\"\ - claude-code\"` parameters would run identical Python code \u2014 the test is not\ - \ a substrate-distinguishing artifact. **Fix:** pick a test (existing or new)\ - \ that actually exercises `AgentSpawner.spawn(...)` and/or `MessageBus.add_message/get_messages`\ - \ so the two substrate parameters can produce divergent execution paths. Without\ - \ that, TASK-1-8's \"matrix proof\" is vacuous.\n\n3. **Internal contradiction\ - \ between TASK-1-1 and TASK-1-2 about the k3s adapter \u2014 plan is incoherent\ - \ on whether k3s still works.** TASK-1-1 acceptance #3 (lines 222-226): \"`select_substrate({})`\ - \ \u2026 returns the bundle pointing at the k3s implementations *but* the k3s\ - \ adapters raise `NotImplementedError` from the protocol seam with a clear message\ - \ naming the follow-up issue \u2014 this is the explicit TODO marker\". TASK-1-2\ - \ description (lines 257-266): \"the k3s code path keeps working via the (deferred)\ - \ k3s adapter raising `NotImplementedError`; this is acceptable because the spike's\ - \ acceptance is claude-code-substrate-only, **and the regression matrix keeps\ - \ `EGG_SUBSTRATE=k3s` selecting the existing in-tree `KubernetesSpawner`**\" and\ - \ TASK-1-2 acceptance #4 (lines 280-283): \"`orchestrator/concurrent_executor.py:504`\ - \ is patched to dispatch through `select_substrate(...)`; the existing k3s path\ - \ continues to work unchanged **because the k3s implementation of the protocol\ - \ is a thin shim around the existing `KubernetesSpawner.spawn_agent_job`**\".\ - \ The two are mutually exclusive \u2014 either the k3s adapter is a working shim\ - \ around `spawn_agent_job` (TASK-1-2 description) or it raises `NotImplementedError`\ - \ (TASK-1-1). If TASK-1-2 patches `concurrent_executor.py:504` (the only call\ - \ site to `spawn_fn`) to dispatch through a `select_substrate(...)` bundle whose\ - \ k3s leg raises `NotImplementedError`, every k3s deployment is broken on this\ - \ PR. **Fix:** pick one model and apply consistently. The architect's tasks_for_task_planner_to_pick_up\ - \ #1 prescribes the working-shim approach: refactor the existing `create_concurrent_spawn_fn`\ - \ at `orchestrator/kubernetes_spawner.py:1564` to return an `AgentSpawner`-conforming\ - \ object \u2014 that path keeps k3s green. If you instead want the `NotImplementedError`\ - \ k3s leg, do not also rewrite `concurrent_executor.py:504`; leave the k3s path\ - \ on the direct import for this spike.\n\n4. **AgentResult missing `commit_sha`\ - \ field \u2192 BRC INV-6 violation, hard correctness regression.** TASK-1-2 acceptance\ - \ #2 defines `AgentResult` with `stdout, exit_code, duration_seconds, worktree`\ - \ \u2014 no `commit_sha`. `orchestrator/action_guards.py::validate_invariants`\ - \ enforces INV-6 (ack_commit_sha consistency) \u2014 the orchestrator must know\ - \ each agent's resulting commit SHA so reviewers can attach commit-bound ACKs\ - \ and the consensus tracker can detect commit drift between proposal and ack.\ - \ The architect's spawner-component design called this out explicitly: \"the spawner\ - \ runs `git -C rev-parse HEAD` and records the SHA in AgentResult.commit_sha\ - \ so the orchestrator can attach commit-bound ACKs (action_guards.validate_invariants\ - \ INV-6 at orchestrator/action_guards.py:753 requires this)\" (architect output,\ - \ component \"egg_orchestrator/spawners/claude_code_spawner.py\", design.commit_sha_capture).\ - \ Dropping commit_sha makes the spawner unable to satisfy the existing BRC invariant.\ - \ **Fix:** add `commit_sha: str | None` to the `AgentResult` dataclass shape in\ - \ TASK-1-1 and require capture-via-`git rev-parse HEAD` in TASK-1-2's acceptance\ - \ criteria.\n\n5. **TASK-1-5 cites a non-existent path-escape primitive (\xA7\ - 9 primitive-existence audit failure).** Plan line 380-382: \"Safety: `tear_down`\ - \ resolves the target through `path.resolve().is_relative_to(base.resolve())`\ - \ so a malicious pipeline ID can't escape the base \u2014 **same defense as `gateway/gateway.py:7903`**.\"\ - \ Grep verdict: `grep -n 'is_relative_to\\|path.resolve()' gateway/gateway.py`\ - \ returns zero hits. The defense pattern actually lives at `gateway/worktree_manager.py:1711`\ - \ (call site) and `gateway/phase_api.py:96-108` (an unrelated phase-api path-resolve).\ - \ **Fix:** cite `gateway/worktree_manager.py:1711` as the reference implementation.\ - \ Mis-citing primitives in acceptance criteria pushes the coder into the same\ - \ hunt I just did \u2014 and risks them inventing a different defense.\n\n6. **TASK-1-3\ - \ acceptance #2 cites BRC unit tests in the wrong directory (\xA79 primitive-existence\ - \ audit failure).** Plan line 308-311: \"Existing BRC unit tests under **`shared/tests/test_brc_*`**\ - \ (or wherever they live) pass when run against `InProcessMessageBus`\". Grep\ - \ verdict: `ls shared/tests/` returns `test_agent_roles_has_contract.py / test_cross_process_lock.py\ - \ / test_egg_harness / test_egg_health.py / test_egg_restrictions.py / test_egg_restrictions_hints.py\ - \ / test_github_block_alignment.py / test_impasse_schema.py / test_overseer_advisor.py`\ - \ \u2014 zero `test_brc_*` files. The BRC unit tests are at `orchestrator/tests/test_brc_*`\ - \ (15+ files: `test_brc_confirmation_nudge.py`, `test_brc_content_validation.py`,\ - \ `test_brc_history.py`, `test_brc_nack_iteration.py`, `test_brc_open_nacks_barrier.py`,\ - \ `test_brc_phase_propagation.py`, plus `test_auto_ack_pure_producers.py` which\ - \ exercises `PeerConsensusTracker` directly). The \"or wherever they live\" hedge\ - \ is not adequate \u2014 the tester role needs a precise path-set to parametrize.\ - \ **Fix:** point TASK-1-3 acceptance #2 at the actual BRC test directory (`orchestrator/tests/test_brc_*.py`)\ - \ and confirm the tester role is allowed to write there. If not, the cross-substrate\ - \ BRC-invariant test belongs in `shared/tests/test_in_process_message_bus.py`\ - \ (which TASK-1-8 already creates) \u2014 say so explicitly instead of vaguely\ - \ deferring.\n\n7. **TASK-1-6 ignores the orchestrator-reentrancy problem flagged\ - \ by risk_analyst R4.** TASK-1-6 (lines 387-419) says \"Build `run_pipeline_in_process(issue,\ - \ role, ...)`\" and \"instantiates `select_substrate(os.environ)`, runs the refine\ - \ phase with refiner-only\". The orchestrator today is a Flask/waitress HTTP daemon\ - \ (`orchestrator/cli.py:83-150 cmd_serve` \u2014 verified). It has no in-process\ - \ API. `ConcurrentPhaseExecutor` (line 114) uses a `ThreadPoolExecutor` for background\ - \ dispatch; `PeerConsensusTracker` holds its own lock and runs message poll loops.\ - \ Wrapping all of this in a Python generator that yields `HITLDecision` and resumes\ - \ via `.send()` is a non-trivial refactor \u2014 the risk_analyst R4 marks this\ - \ `requires_human_review: true` with `effort: HIGH`. The plan offers neither a\ - \ refactor task nor a HITL gate, just an acceptance bullet (\"the generator runs\ - \ the refiner through to completion\"). **Fix:** either (a) split TASK-1-6 into\ - \ a \"build in-process orchestrator entry point\" task with its own acceptance\ - \ criteria covering heartbeat-during-HITL, background-thread lifetime, and contract-state\ - \ synchronization; or (b) register a HITL decision asking the operator to bound\ - \ the generator/AskUserQuestion model before this slice merges. Calling it a one-line\ - \ acceptance bullet is hiding hours of refactor work.\n\n8. **No risk-mitigation\ - \ tasks for the five risk_analyst HIGH-severity / human-review-required items\ - \ (R1, R2, R4, R7, R10).** The risk_analyst's proposal flagged five risks requiring\ - \ human review: R1 credential trust-boundary inversion (security, HIGH/CERTAIN),\ - \ R2 PreToolUse hook role-routing unverified (design, HIGH/HIGH), R4 in-process\ - \ orchestrator reentrancy (covered above), R7 subagent context budget vs deep-refine\ - \ (performance, MEDIUM/HIGH), R10 single-role spike freezing interfaces (design,\ - \ HIGH/MEDIUM). The plan acknowledges none of these as task-level concerns, decisions,\ - \ or HITL gates. R1 in particular \u2014 moving the Anthropic credential from\ - \ a gateway-isolated process into the user's parent session \u2014 is a structural\ - \ threat-model change that the ADR must document and the operator must explicitly\ - \ accept (per risk_analyst recommendation_rationale and REC2). The plan's TASK-1-9\ - \ ADR acceptance does NOT call this out \u2014 bullets only cover the 11 cq decisions,\ - \ 6 feedback items, and primitives table. **Fix:** add explicit acceptance criteria\ - \ in TASK-1-9 for each of R1, R2, R7, R10 (\"ADR's 'Trust-context shift' subsection\ - \ names credential exposure\"; \"ADR documents PreToolUse hook role-routing fallback\ - \ path to MCP-validator-side enforcement (cq-6 opt-2) if spike evidence is negative\"\ - ; \"ADR documents subagent context budget regression and the smaller-budget fallback\ - \ per cq-10 opt-2\"; \"interfaces marked unstable / v0.x until \u22653 roles exercise\ - \ them\"). Without this, the spike encodes design decisions that the second wave\ - \ will need to revisit, expensively.\n\n9. **TASK-1-8 acceptance #2 \u2014 kubectl\ - \ gating claim does not match the chosen test.** \"the parametrized regression\ - \ test passes under both substrates locally (kubectl available for k3s; no kubectl\ - \ needed for claude-code)\". The chosen test (`test_brc_single_cycle.py`) does\ - \ not require kubectl for any parameter \u2014 see blocking issue #2. **Fix:**\ - \ if you keep this test as the parametrize target, drop the \"kubectl available\ - \ for k3s\" half because both parameters run pure-Python. Better: replace the\ - \ chosen test as in blocking issue #2.\n\n10. **PyPI package name `egg-shared`\ - \ is invented (TASK-1-7 and TASK-1-11).** Plan line 432 and line 569: \"lists\ - \ `egg-shared` (or the canonical pip name) as a required dep\". There is no PyPI\ - \ package called `egg-shared`. The risk_analyst R13 flagged this: the `egg` name\ - \ is already taken on PyPI, and no published wheel exists for this repo. Picking\ - \ `egg-shared` without operator authority pre-empts a packaging decision the project\ - \ hasn't made. **Fix:** either register a HITL decision asking the operator to\ - \ pick the canonical pip name (and what registry \u2014 PyPI vs git+https), or\ - \ accept the dep declaration as a TODO placeholder string with a comment, not\ - \ as a real install instruction. The current language (\"or the canonical pip\ - \ name\") will produce a coder asking the same question downstream.\n\n11. **Primitive\ - \ line numbers off \u2014 minor but a pattern.** Plan line 124: \"`integration_tests/conftest.py:78`\ - \ `EggStack` dataclass\". Grep: `class EggStack` is at `integration_tests/conftest.py:71`,\ - \ not :78. (line 78 is the `gateway_url: str` field inside the dataclass.) Plan\ - \ line 121-123 lists `class HITLDecision` at `orchestrator/models.py:300` \u2014\ - \ that one's correct. **Fix:** scan the Primitives table for off-by-N line numbers;\ - \ cite the class definition line, not a field-in-class line.\n\n### Non-blocking\n\ - \n- **Slice-sizing advisory (#2137 size advisory; never NACK):** the plan's own\ - \ LOC estimate is 1,800\u20132,500 LOC, over the 1k soft target. Per HITL decision-6\ - \ opt-2 I will not NACK on size; this is informational only. The plan acknowledges\ - \ this and ties it to cq-11's spike-shape choice \u2014 that justification holds,\ - \ but the next reviewer of the implement-phase BRC cycle should know the slice\ - \ will be expensive to NACK.\n- **Consider expanding spike to 2 roles (risk_analyst\ - \ REC4).** A single-role spike does not exercise the BRC consensus path (PROPOSE\ - \ \u2192 ACK), which is the actual structural concern the orchestrator-as-bus\ - \ model needs to prove. A `refiner + reviewer_refine` pair stays within cq-11's\ - \ \"walking skeleton\" framing but exercises producer dispatch AND review verdict\ - \ round-trip. Operator decision; not a blocker.\n- **Subagent type model (R15).**\ - \ The plan implicitly assumes `subagent_type='general-purpose'` (matching the\ - \ existing refine-plan skill) but does not commit. Custom `subagent_type` via\ - \ `.claude/agents/.md` gives structural tool restrictions per role; the\ - \ existing model relies on prompt discipline + PreToolUse hooks. Picking one and\ - \ documenting in the ADR would tighten the design.\n- **Cost cap (risk_analyst\ - \ REC5).** Consider adding an `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` env var with\ - \ conservative default. Without it, a buggy NACK loop bills directly to the user's\ - \ Anthropic account \u2014 a real regression from the gateway model.\n- **TASK-1-2\ - \ list of files modified should explicitly include `orchestrator/concurrent_executor.py`**\ - \ (it does \u2014 line 288, good) \u2014 but the acceptance criteria say \"verified\ - \ by an `assert isinstance(...)` in a unit test\"; consider also a smoke test\ - \ that `_spawn_agent` dispatches through the bundle, otherwise the contradiction\ - \ in blocking #3 could land silently.\n- **TASK-1-1 Files list does not include\ - \ `orchestrator/substrate/k3s_adapter.py`** \u2014 but TASK-1-2 introduces it.\ - \ If the adapter is the k3s leg of `select_substrate({})`, it logically belongs\ - \ to TASK-1-1's deliverable surface. Re-organize so the protocol module and the\ - \ k3s adapter shim ship together, or document that TASK-1-1's k3s leg is a stub\ - \ and TASK-1-2 replaces it with the shim.\n- **Plan's \"Manual verification\"\ - \ step 3 (\"HITL surfaces through AskUserQuestion\")** requires a working in-process\ - \ generator with HITL round-trip \u2014 circular with blocking #7. Resolve that\ - \ first.\n" - revision_count: 1 -```` - -### [2026-05-19T00:13:51Z] task_planner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 412c2595-b714-49 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:13:51Z] reviewer_plan → architect (CONSENSUS_ACK): ACK from reviewer_plan for architect - - -Reviewed the architect's substrate-swap walking-skeleton analysis at `.egg-state/agent-outputs/2623-architect-output.json` (360 lines, 9 components, 12 primitives audited, 9 candidate tasks for task_planner, 7 risks delegated to risk_analyst, 48-entry evidence_citations_index). Spot-checked every primitive citation against the actual codebase: - -- `orchestrator/concurrent_executor.py:52` SpawnFn type alias — confirmed (line 52, `SpawnFn = Callable[..., Any]`). -- `orchestrator/concurrent_executor.py:114` ConcurrentPhaseExecutor class def — confirmed (the architect's :137 cites the constructor; both fit). -- `orchestrator/concurrent_executor.py:469` ThreadPoolExecutor dispatch — confirmed (`with ThreadPoolExecutor(max_workers=self.max_concurrent) as pool:`). -- `orchestrator/concurrent_executor.py:504 _spawn_agent` — confirmed. -- `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn` — confirmed. -- `orchestrator/message_store.py:200 in-memory MessageStore`, :531 `get_message_store`, :552 `EGG_MESSAGE_STORE_BACKEND` — all confirmed. -- `orchestrator/redis_message_store.py:107 RedisMessageStore` — confirmed. -- `orchestrator/models.py:1106 add_decision` / `:1130 resolve_decision` — confirmed. -- `gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` — confirmed. Architect calls this out as "second definition" (parallel to `orchestrator/kubernetes_spawner.py:65`); both exist. -- `gateway/agent_restrictions.py:58 partition_files_by_role` — confirmed. -- `gateway/phase_filter.py:1036 check_phase_file_restrictions`, `:1061 check_agent_restrictions` — both confirmed. -- `shared/egg_restrictions/patterns.py:57 AgentFilePattern.can_write`, `:768 build_agent_patterns` — confirmed. Note: `AgentFilePattern` class is defined at line 44; the architect's `:57` refers to the `can_write` method within it. Both are accurate. -- `shared/egg_harness/prompt.py:24 build_system_prompt` — confirmed. - -The component-by-component breakdown correctly slots each interface into the cq-N decisions (cq-1 → substrate registry; cq-4 → synchronous spawn; cq-5 → port WORKTREE_BASE_DIR; cq-6 → PreToolUse hook; cq-7 → heredoc HITL; cq-8 → plugin.json pip dep; cq-11 → spike scope). The `execution_context_and_primitive_audit` section correctly handles the new `parent-claude-code-session` context as a first-class entry alongside `in-sandbox-agent` / `trusted-CI-runner` / `human-operator`. The `scope_explicitly_in_spike` vs `scope_explicitly_deferred_to_followups` split is exactly the bookkeeping cq-11 needs. - -The architect correctly identified the spawner's `commit_sha` requirement against INV-6 (`orchestrator/action_guards.py:753`) — a structural detail the task_planner subsequently dropped from `AgentResult`, which I'm NACKing the task_planner for (my blocking #4 there). The architect's design here is sound. - -Forest-violation check: this proposal does not modify the contract's slice DAG (architect output is analysis, not contract surgery). No forest violation. - -### Non-blocking - -- **Component "egg_orchestrator/policy/pretooluse_hook.py" assumes the hook can resolve calling-subagent role at hook-execution time.** The risk_analyst's R2 marks this as an unverified runtime primitive. Recommend the architect's follow-up note (or the ADR) explicitly state the fallback path: if PreToolUse hooks cannot distinguish the calling subagent's role at runtime, the policy enforcer falls back to MCP-validator-side enforcement (cq-6 option 2). Otherwise the spike commits to a primitive that may not exist in the form assumed. -- **Component "plugins/sdlc/" agents/ design (line 127)** says "Empty in the spike — the orchestrator uses build_system_prompt(sources) to assemble per-role prompts dynamically rather than per-role markdown files." This is a deliberate choice (model-(a) prompt-injected, per risk_analyst R15). The trade-off vs. custom `subagent_type` (model-(b) structural tool restrictions) is real and should be a named decision in the ADR rather than implicit in the file layout. -- **Recommended approach line 17 — "regression suite is already substrate-portable in its mocking layout"** somewhat overstates the case. The risk_analyst R6 flags that 12/14 regression tests are BRC-tracker-level (substrate-agnostic) but several still implicitly depend on gateway primitives or k3s metadata. The architect's "adding `pytest.mark.parametrize('substrate', [...])` is a CI-matrix change, not a test-rewrite" is the optimistic framing; the realistic framing per R6 REC3 is "classify each test as portable / k3s-only / claude-code-only first." Not blocking the architect since this lands as task_planner / tester work, but worth aligning in any rewrite. -- **`evidence_citations_index` is excellent (48 entries, file:line each) — this format should be cargo-culted to future architect outputs.** -- **`questions_for_followup_iterations` (3 items)** are well-scoped; none rise to plan-phase HITL gates. Operator may want to register Q3 (`EGG_SUBSTRATE` default) as a contract decision once the spike merges and before the follow-up issue starts. - - -````yaml -id: 2f9c97d9-94e3-43 -phase: plan -metadata: - payload: - artifact_references: - - .egg-state/agent-outputs/2623-architect-output.json - - orchestrator/concurrent_executor.py - - orchestrator/kubernetes_spawner.py - - orchestrator/message_store.py - - orchestrator/models.py - - orchestrator/action_guards.py - - shared/egg_harness/prompt.py - - shared/egg_restrictions/patterns.py - - gateway/worktree_manager.py - - gateway/phase_filter.py - - gateway/agent_restrictions.py - reason: "\nReviewed the architect's substrate-swap walking-skeleton analysis at\ - \ `.egg-state/agent-outputs/2623-architect-output.json` (360 lines, 9 components,\ - \ 12 primitives audited, 9 candidate tasks for task_planner, 7 risks delegated\ - \ to risk_analyst, 48-entry evidence_citations_index). Spot-checked every primitive\ - \ citation against the actual codebase:\n\n- `orchestrator/concurrent_executor.py:52`\ - \ SpawnFn type alias \u2014 confirmed (line 52, `SpawnFn = Callable[..., Any]`).\n\ - - `orchestrator/concurrent_executor.py:114` ConcurrentPhaseExecutor class def\ - \ \u2014 confirmed (the architect's :137 cites the constructor; both fit).\n\ - - `orchestrator/concurrent_executor.py:469` ThreadPoolExecutor dispatch \u2014\ - \ confirmed (`with ThreadPoolExecutor(max_workers=self.max_concurrent) as pool:`).\n\ - - `orchestrator/concurrent_executor.py:504 _spawn_agent` \u2014 confirmed.\n\ - - `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn` \u2014\ - \ confirmed.\n- `orchestrator/message_store.py:200 in-memory MessageStore`,\ - \ :531 `get_message_store`, :552 `EGG_MESSAGE_STORE_BACKEND` \u2014 all confirmed.\n\ - - `orchestrator/redis_message_store.py:107 RedisMessageStore` \u2014 confirmed.\n\ - - `orchestrator/models.py:1106 add_decision` / `:1130 resolve_decision` \u2014\ - \ confirmed.\n- `gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` \u2014 confirmed.\ - \ Architect calls this out as \"second definition\" (parallel to `orchestrator/kubernetes_spawner.py:65`);\ - \ both exist.\n- `gateway/agent_restrictions.py:58 partition_files_by_role`\ - \ \u2014 confirmed.\n- `gateway/phase_filter.py:1036 check_phase_file_restrictions`,\ - \ `:1061 check_agent_restrictions` \u2014 both confirmed.\n- `shared/egg_restrictions/patterns.py:57\ - \ AgentFilePattern.can_write`, `:768 build_agent_patterns` \u2014 confirmed.\ - \ Note: `AgentFilePattern` class is defined at line 44; the architect's `:57`\ - \ refers to the `can_write` method within it. Both are accurate.\n- `shared/egg_harness/prompt.py:24\ - \ build_system_prompt` \u2014 confirmed.\n\nThe component-by-component breakdown\ - \ correctly slots each interface into the cq-N decisions (cq-1 \u2192 substrate\ - \ registry; cq-4 \u2192 synchronous spawn; cq-5 \u2192 port WORKTREE_BASE_DIR;\ - \ cq-6 \u2192 PreToolUse hook; cq-7 \u2192 heredoc HITL; cq-8 \u2192 plugin.json\ - \ pip dep; cq-11 \u2192 spike scope). The `execution_context_and_primitive_audit`\ - \ section correctly handles the new `parent-claude-code-session` context as\ - \ a first-class entry alongside `in-sandbox-agent` / `trusted-CI-runner` / `human-operator`.\ - \ The `scope_explicitly_in_spike` vs `scope_explicitly_deferred_to_followups`\ - \ split is exactly the bookkeeping cq-11 needs.\n\nThe architect correctly identified\ - \ the spawner's `commit_sha` requirement against INV-6 (`orchestrator/action_guards.py:753`)\ - \ \u2014 a structural detail the task_planner subsequently dropped from `AgentResult`,\ - \ which I'm NACKing the task_planner for (my blocking #4 there). The architect's\ - \ design here is sound.\n\nForest-violation check: this proposal does not modify\ - \ the contract's slice DAG (architect output is analysis, not contract surgery).\ - \ No forest violation.\n\n### Non-blocking\n\n- **Component \"egg_orchestrator/policy/pretooluse_hook.py\"\ - \ assumes the hook can resolve calling-subagent role at hook-execution time.**\ - \ The risk_analyst's R2 marks this as an unverified runtime primitive. Recommend\ - \ the architect's follow-up note (or the ADR) explicitly state the fallback\ - \ path: if PreToolUse hooks cannot distinguish the calling subagent's role at\ - \ runtime, the policy enforcer falls back to MCP-validator-side enforcement\ - \ (cq-6 option 2). Otherwise the spike commits to a primitive that may not exist\ - \ in the form assumed.\n- **Component \"plugins/sdlc/\" agents/ design (line\ - \ 127)** says \"Empty in the spike \u2014 the orchestrator uses build_system_prompt(sources)\ - \ to assemble per-role prompts dynamically rather than per-role markdown files.\"\ - \ This is a deliberate choice (model-(a) prompt-injected, per risk_analyst R15).\ - \ The trade-off vs. custom `subagent_type` (model-(b) structural tool restrictions)\ - \ is real and should be a named decision in the ADR rather than implicit in\ - \ the file layout.\n- **Recommended approach line 17 \u2014 \"regression suite\ - \ is already substrate-portable in its mocking layout\"** somewhat overstates\ - \ the case. The risk_analyst R6 flags that 12/14 regression tests are BRC-tracker-level\ - \ (substrate-agnostic) but several still implicitly depend on gateway primitives\ - \ or k3s metadata. The architect's \"adding `pytest.mark.parametrize('substrate',\ - \ [...])` is a CI-matrix change, not a test-rewrite\" is the optimistic framing;\ - \ the realistic framing per R6 REC3 is \"classify each test as portable / k3s-only\ - \ / claude-code-only first.\" Not blocking the architect since this lands as\ - \ task_planner / tester work, but worth aligning in any rewrite.\n- **`evidence_citations_index`\ - \ is excellent (48 entries, file:line each) \u2014 this format should be cargo-culted\ - \ to future architect outputs.**\n- **`questions_for_followup_iterations` (3\ - \ items)** are well-scoped; none rise to plan-phase HITL gates. Operator may\ - \ want to register Q3 (`EGG_SUBSTRATE` default) as a contract decision once\ - \ the spike merges and before the follow-up issue starts.\n" - ack_version: 1 - version: 1 -```` - -### [2026-05-19T00:13:51Z] orchestrator → architect (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: 8d9c3460-2cea-4d -phase: plan -metadata: - ready_to_confirm: true - version: 1 -```` - -### [2026-05-19T00:13:51Z] architect (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f0547a66-c9e1-46 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:13:51Z] reviewer_plan → risk_analyst (CONSENSUS_ACK): ACK from reviewer_plan for risk_analyst - - -Reviewed the risk_analyst's 572-line output covering 16 risks across security/design/performance/compatibility, 5 runtime primitives flagged for spike-time verification, 5 trust-boundary shifts named with mitigations, 7 implementation recommendations, 4 open questions for plan phase. Cross-referenced against the issue body's constraints (`SendMessage` gating, subagent context windows, HITL-through-parent-session, concurrency ceiling, install footprint) and the architect's parallel proposal. - -The structural risks are correctly identified and graded: - -- **R1 (credential trust-boundary inversion, HIGH/CERTAIN)** — confirmed by reading `gateway/anthropic_credentials.py` plus the analysis's section "Gateway is an HTTP sidecar with portable policy logic" (line 53-57). The gateway's `anthropic_credentials.py` does in fact inject server-side; the sandbox sees a placeholder. Moving the orchestrator in-process to a Claude Code session moves the credential into the parent's process, and any subagent inherits access. This is a real structural threat-model change that the issue body's "credentials are inherited from the user's session — no injection needed" framing understates. R1's mitigation strategy (document trust-context shift; scope use to trusted repos; consider opt-in proxy fallback) is the right shape — risk_analyst correctly flags this for human review (operator must explicitly accept the new boundary). - -- **R2 (PreToolUse hook role-routing unverified, HIGH/HIGH)** — exactly the #2594 failure class. Plan cannot rely on a primitive whose role-awareness is unverified. The fallback path (cq-6 option 2 MCP-validator-side enforcement) is real — `sandbox/egg_agent_tools/handlers/restrictions.py:70 check_file_restriction` already wraps `shared/egg_restrictions/patterns.py` and runs in-sandbox today. The recommendation that the spike's exit criteria require a 2-subagent worked example before the abstraction freezes is structurally correct. - -- **R4 (in-process orchestrator reentrancy, MEDIUM/HIGH)** — confirmed by reading `orchestrator/concurrent_executor.py`: `ConcurrentPhaseExecutor` (line 114) holds a `ThreadPoolExecutor` (line 469); `PeerConsensusTracker` runs its own lock; heartbeat / message-poll loops run as background threads. The generator-yields-HITLDecision model demands that the orchestrator pause cleanly while preserving these threads. The risk_analyst's mitigation ("spike must exercise HITL round-trip end-to-end"; "pause heartbeat timers during HITL or extend dramatically") is the right bound to put on the spike. - -- **R7 (subagent context budget vs deep-refine, MEDIUM/HIGH)** — correctly distinguishes k3s-substrate checkpoint primitives (sandbox restart with full prompt rebuild) from Claude Code subagent re-spawn (orchestrator must re-summarize and re-feed). The honest assessment ("some refines will fit, some won't; the mitigation is acknowledging the gap") is more useful than a false promise. - -- **R10 (single-role spike freezing interfaces, HIGH/MEDIUM)** — the strongest structural critique of cq-11 itself. A single-role spike doesn't exercise the orchestrator-as-bus model (BRC's PROPOSE→ACK round-trip). The recommendation to expand to 2 roles (refiner + reviewer_refine) is a defensible counter-proposal to cq-11 that the plan reviewer should surface; risk_analyst correctly notes it requires human review. - -The five trust-boundary shifts (credential isolation, file-write enforcement timing, cost/rate-limit control, agent liveness, push serialization) are the right list and each has a concrete mitigation. The `rollback_plan` is realistic (env-var flip back to `EGG_SUBSTRATE=k3s`, no data migration needed because contracts are filesystem-native). - -The 7 `implementation_recommendations` (REC1–REC7) and 4 `open_questions_for_plan_phase` correctly target structural items that should land in the plan's tasks or HITL gates. None of these recommendations exceed risk_analyst's scope (they're properly framed as "the planner should…" not "I will…"). - -The output is well-structured for downstream consumption: every risk has `id`, `category`, `severity`, `likelihood`, `impact`, `description`, `affected_files`, `mitigation` with `strategy`/`effort`/`residual_risk`, and `requires_human_review` flag. This is the schema the plan reviewer needs to cross-reference against the planner's task list. - -### Non-blocking - -- **R8 (pip install drift) recommendation #5 has a JSON shape glitch (line 475-477)** — the entry uses `"id": "MEDIUM"` and then a comment field `"id_override_note": "REC5"`. This is a serialization mistake; the actual `id` should be `"REC5"`. Fix in any re-propose. Not blocking — content is clear, just self-consistent IDs would help downstream consumers. -- **R3 (subagent concurrency ceiling) "MEDIUM/MEDIUM"** could be argued up to MEDIUM/HIGH given egg's `ThreadPoolExecutor(max_workers=6)` is hard-coded for k3s and the implement-phase slice-DAG fans out to 8+ agents per the issue body. Not blocking — the mitigation (per-substrate `max_workers` override + semaphore) is sound at either grading. -- **REC4 (expand spike to 2 roles) directly contradicts cq-11's HITL resolution** ("Spike then plan: a single 'walking skeleton' slice that gets one role (refiner) running through one substrate (claude-code) end-to-end"). The risk_analyst correctly flags this as a recommendation, not a verdict. Plan reviewer's call on whether to surface as a re-open of cq-11 to the operator or accept the single-role spike as-is. I lean toward surfacing — the consensus path is the actually-novel surface — but cq-11 is a resolved HITL and the planner cannot unilaterally reopen. -- **R14 (SendMessage / Agent Teams gating)** is correctly bounded as known-known with the right mitigation (don't let scope creep absorb #2612). No action. -- **`alignment_with_issue_done_criteria`** is a nice closing section; consider including this shape in future risk_analyst outputs as a contract-vs-risk cross-check. -- **`performance_assessment.note`** ("'slower than k3s in a datacenter' may still be 'faster than nothing' from the solo-developer perspective") is the kind of framing the operator needs and was missing from the refine analysis. Good to surface. - - -````yaml -id: 3de219a8-9c20-49 -phase: plan -metadata: - payload: - artifact_references: - - .egg-state/agent-outputs/2623-risk_analyst-output.json - - .egg-state/drafts/2623-analysis.md - - docs/architecture/integration-test-trust-boundary.md - - orchestrator/concurrent_executor.py - - gateway/anthropic_credentials.py - - shared/egg_restrictions/patterns.py - - shared/egg_harness/prompt.py - reason: "\nReviewed the risk_analyst's 572-line output covering 16 risks across\ - \ security/design/performance/compatibility, 5 runtime primitives flagged for\ - \ spike-time verification, 5 trust-boundary shifts named with mitigations, 7\ - \ implementation recommendations, 4 open questions for plan phase. Cross-referenced\ - \ against the issue body's constraints (`SendMessage` gating, subagent context\ - \ windows, HITL-through-parent-session, concurrency ceiling, install footprint)\ - \ and the architect's parallel proposal.\n\nThe structural risks are correctly\ - \ identified and graded:\n\n- **R1 (credential trust-boundary inversion, HIGH/CERTAIN)**\ - \ \u2014 confirmed by reading `gateway/anthropic_credentials.py` plus the analysis's\ - \ section \"Gateway is an HTTP sidecar with portable policy logic\" (line 53-57).\ - \ The gateway's `anthropic_credentials.py` does in fact inject server-side;\ - \ the sandbox sees a placeholder. Moving the orchestrator in-process to a Claude\ - \ Code session moves the credential into the parent's process, and any subagent\ - \ inherits access. This is a real structural threat-model change that the issue\ - \ body's \"credentials are inherited from the user's session \u2014 no injection\ - \ needed\" framing understates. R1's mitigation strategy (document trust-context\ - \ shift; scope use to trusted repos; consider opt-in proxy fallback) is the\ - \ right shape \u2014 risk_analyst correctly flags this for human review (operator\ - \ must explicitly accept the new boundary).\n\n- **R2 (PreToolUse hook role-routing\ - \ unverified, HIGH/HIGH)** \u2014 exactly the #2594 failure class. Plan cannot\ - \ rely on a primitive whose role-awareness is unverified. The fallback path\ - \ (cq-6 option 2 MCP-validator-side enforcement) is real \u2014 `sandbox/egg_agent_tools/handlers/restrictions.py:70\ - \ check_file_restriction` already wraps `shared/egg_restrictions/patterns.py`\ - \ and runs in-sandbox today. The recommendation that the spike's exit criteria\ - \ require a 2-subagent worked example before the abstraction freezes is structurally\ - \ correct.\n\n- **R4 (in-process orchestrator reentrancy, MEDIUM/HIGH)** \u2014\ - \ confirmed by reading `orchestrator/concurrent_executor.py`: `ConcurrentPhaseExecutor`\ - \ (line 114) holds a `ThreadPoolExecutor` (line 469); `PeerConsensusTracker`\ - \ runs its own lock; heartbeat / message-poll loops run as background threads.\ - \ The generator-yields-HITLDecision model demands that the orchestrator pause\ - \ cleanly while preserving these threads. The risk_analyst's mitigation (\"\ - spike must exercise HITL round-trip end-to-end\"; \"pause heartbeat timers during\ - \ HITL or extend dramatically\") is the right bound to put on the spike.\n\n\ - - **R7 (subagent context budget vs deep-refine, MEDIUM/HIGH)** \u2014 correctly\ - \ distinguishes k3s-substrate checkpoint primitives (sandbox restart with full\ - \ prompt rebuild) from Claude Code subagent re-spawn (orchestrator must re-summarize\ - \ and re-feed). The honest assessment (\"some refines will fit, some won't;\ - \ the mitigation is acknowledging the gap\") is more useful than a false promise.\n\ - \n- **R10 (single-role spike freezing interfaces, HIGH/MEDIUM)** \u2014 the\ - \ strongest structural critique of cq-11 itself. A single-role spike doesn't\ - \ exercise the orchestrator-as-bus model (BRC's PROPOSE\u2192ACK round-trip).\ - \ The recommendation to expand to 2 roles (refiner + reviewer_refine) is a defensible\ - \ counter-proposal to cq-11 that the plan reviewer should surface; risk_analyst\ - \ correctly notes it requires human review.\n\nThe five trust-boundary shifts\ - \ (credential isolation, file-write enforcement timing, cost/rate-limit control,\ - \ agent liveness, push serialization) are the right list and each has a concrete\ - \ mitigation. The `rollback_plan` is realistic (env-var flip back to `EGG_SUBSTRATE=k3s`,\ - \ no data migration needed because contracts are filesystem-native).\n\nThe\ - \ 7 `implementation_recommendations` (REC1\u2013REC7) and 4 `open_questions_for_plan_phase`\ - \ correctly target structural items that should land in the plan's tasks or\ - \ HITL gates. None of these recommendations exceed risk_analyst's scope (they're\ - \ properly framed as \"the planner should\u2026\" not \"I will\u2026\").\n\n\ - The output is well-structured for downstream consumption: every risk has `id`,\ - \ `category`, `severity`, `likelihood`, `impact`, `description`, `affected_files`,\ - \ `mitigation` with `strategy`/`effort`/`residual_risk`, and `requires_human_review`\ - \ flag. This is the schema the plan reviewer needs to cross-reference against\ - \ the planner's task list.\n\n### Non-blocking\n\n- **R8 (pip install drift)\ - \ recommendation #5 has a JSON shape glitch (line 475-477)** \u2014 the entry\ - \ uses `\"id\": \"MEDIUM\"` and then a comment field `\"id_override_note\":\ - \ \"REC5\"`. This is a serialization mistake; the actual `id` should be `\"\ - REC5\"`. Fix in any re-propose. Not blocking \u2014 content is clear, just self-consistent\ - \ IDs would help downstream consumers.\n- **R3 (subagent concurrency ceiling)\ - \ \"MEDIUM/MEDIUM\"** could be argued up to MEDIUM/HIGH given egg's `ThreadPoolExecutor(max_workers=6)`\ - \ is hard-coded for k3s and the implement-phase slice-DAG fans out to 8+ agents\ - \ per the issue body. Not blocking \u2014 the mitigation (per-substrate `max_workers`\ - \ override + semaphore) is sound at either grading.\n- **REC4 (expand spike\ - \ to 2 roles) directly contradicts cq-11's HITL resolution** (\"Spike then plan:\ - \ a single 'walking skeleton' slice that gets one role (refiner) running through\ - \ one substrate (claude-code) end-to-end\"). The risk_analyst correctly flags\ - \ this as a recommendation, not a verdict. Plan reviewer's call on whether to\ - \ surface as a re-open of cq-11 to the operator or accept the single-role spike\ - \ as-is. I lean toward surfacing \u2014 the consensus path is the actually-novel\ - \ surface \u2014 but cq-11 is a resolved HITL and the planner cannot unilaterally\ - \ reopen.\n- **R14 (SendMessage / Agent Teams gating)** is correctly bounded\ - \ as known-known with the right mitigation (don't let scope creep absorb #2612).\ - \ No action.\n- **`alignment_with_issue_done_criteria`** is a nice closing section;\ - \ consider including this shape in future risk_analyst outputs as a contract-vs-risk\ - \ cross-check.\n- **`performance_assessment.note`** (\"'slower than k3s in a\ - \ datacenter' may still be 'faster than nothing' from the solo-developer perspective\"\ - ) is the kind of framing the operator needs and was missing from the refine\ - \ analysis. Good to surface.\n" - ack_version: 1 - version: 1 -```` - -### [2026-05-19T00:13:56Z] architect (CONSENSUS_CONFIRMED): Confirmed by architect - -````yaml -id: 9f421a3c-3419-4f -phase: plan -metadata: - consensus_reached: false -```` - -### [2026-05-19T00:13:56Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 51264ca5-4325-4b -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:14:03Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 3db1f8bb-6451-47 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:14:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 73e96e13-e91f-48 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:14:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: e14d3f73-2cc0-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:15:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: fb650e95-0846-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:15:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 7009b36f-f1e1-4e -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:15:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 81ebd72b-e93b-45 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:16:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c3666cc3-bc0c-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:16:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: d22d958e-5f87-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:16:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 9d9f32d3-e124-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:17:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 95e2614b-01b1-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:17:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: c2dcbb37-7bc6-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:17:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 07825e67-10ab-45 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:18:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c9c7e3ee-d51e-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:18:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 6d18c834-0500-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:18:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 047d3fb2-68fc-45 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:19:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 1822671a-bfee-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:19:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: a0945205-6558-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:19:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 62a85bc7-e8eb-4e -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:20:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5c582760-c53b-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:20:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 02e7d738-9edf-45 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:20:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: e2535f66-02f5-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:21:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: fb326fad-d6b8-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:21:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 4463d38c-9405-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:21:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: f6a0c4b5-25a7-4e -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:22:04Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 4869401e-931a-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:22:05Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: b4bf027a-cea8-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:22:07Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 9f0cd47f-04e6-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:22:09Z] overseer → task_planner (STATUS): NUDGE: BRC CONSENSUS_CONFIRMED required - -task_planner: You are in producer_phase=WORKING after receiving your ACK at 00:13:51 UTC. You have been in WORKING state for 400+ seconds without sending CONSENSUS_CONFIRMED. Architect confirmed at 00:13:56 (5s), risk_analyst confirmed at 00:11:48 (4s). reviewer_plan is blocked in REVIEWING state waiting for your confirmation. Please send CONSENSUS_CONFIRMED for your proposal immediately to unblock BRC. - -````yaml -id: 58a29a77-44bd-40 -phase: plan -```` - -### [2026-05-19T00:23:09Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5c4c4e2e-ccbf-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:03.952398+00:00' -```` - -### [2026-05-19T00:23:09Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 35612627-8aab-4d -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:23:09Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 7a3a4051-bcb0-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:23:09Z] task_planner (CONSENSUS_PROPOSE): Proposal from task_planner - -Re-propose addressing all 11 reviewer_plan blockers: (1) Trust-Boundary rewritten to cite integration_tests/conftest.py:71/340/357 (dropped deleted local_pipeline reference); (2) NEW substrate-distinguishing test test_substrate_smoke.py exercises select_substrate().spawner.spawn() and .bus directly under both substrate params; (3) k3s adapter contradiction resolved — K3sSpawnerAdapter is WORKING (wraps orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn); NotImplementedError only in TASK-1-6's run_pipeline_in_process k3s leg (cq-11 scope-fence); (4) AgentResult.commit_sha added for INV-6 (orchestrator/action_guards.py:631 body :757), captured via git rev-parse HEAD in both spawners; (5) TASK-1-5 cites gateway/worktree_manager.py:1711 is_relative_to defense; (6) TASK-1-3 oracle now orchestrator/tests/test_brc_*.py with named scenarios from test_brc_open_nacks_barrier.py and test_brc_content_validation.py; (7) TASK-1-6 expanded with R4 refactor acceptance: heartbeat-during-HITL, background-thread lifetime on GeneratorExit, contract-state sync; (8) TASK-1-9 ADR covers R1 trust-context shift, R2 hook fallback, R7 context budget, R10 interface stability marker, REC5 cost cap, R15 subagent_type choice; (9) both substrate dimensions pure-Python (kubectl claim corrected); (10) cq-12 HITL registered for pip name selection — TASK-1-7 carries operator's selection verbatim; (11) EggStack line :78 → :71. Non-blocking items addressed inline. - -````yaml -id: 29c8900e-0336-47 -phase: plan -metadata: - payload: - summary: "Re-propose addressing all 11 reviewer_plan blockers: (1) Trust-Boundary\ - \ rewritten to cite integration_tests/conftest.py:71/340/357 (dropped deleted\ - \ local_pipeline reference); (2) NEW substrate-distinguishing test test_substrate_smoke.py\ - \ exercises select_substrate().spawner.spawn() and .bus directly under both\ - \ substrate params; (3) k3s adapter contradiction resolved \u2014 K3sSpawnerAdapter\ - \ is WORKING (wraps orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn);\ - \ NotImplementedError only in TASK-1-6's run_pipeline_in_process k3s leg (cq-11\ - \ scope-fence); (4) AgentResult.commit_sha added for INV-6 (orchestrator/action_guards.py:631\ - \ body :757), captured via git rev-parse HEAD in both spawners; (5) TASK-1-5\ - \ cites gateway/worktree_manager.py:1711 is_relative_to defense; (6) TASK-1-3\ - \ oracle now orchestrator/tests/test_brc_*.py with named scenarios from test_brc_open_nacks_barrier.py\ - \ and test_brc_content_validation.py; (7) TASK-1-6 expanded with R4 refactor\ - \ acceptance: heartbeat-during-HITL, background-thread lifetime on GeneratorExit,\ - \ contract-state sync; (8) TASK-1-9 ADR covers R1 trust-context shift, R2 hook\ - \ fallback, R7 context budget, R10 interface stability marker, REC5 cost cap,\ - \ R15 subagent_type choice; (9) both substrate dimensions pure-Python (kubectl\ - \ claim corrected); (10) cq-12 HITL registered for pip name selection \u2014\ - \ TASK-1-7 carries operator's selection verbatim; (11) EggStack line :78 \u2192\ - \ :71. Non-blocking items addressed inline." - attestation: {} - artifacts: - - .egg-state/drafts/2623-plan.md - risk_considered: "Single-slice plan over 1k LOC advisory ceiling is unchanged\ - \ (cq-11 spike-then-plan). New cq-12 HITL adds one pending decision the operator\ - \ must resolve for TASK-1-7 to ship; documented in plan body. TASK-1-6 is now\ - \ an explicit R4 refactor task with three acceptance bullets (heartbeat, lifetime,\ - \ state sync) \u2014 the most expensive task in the slice but the cost is now\ - \ visible to the coder rather than hidden." - commit_sha: f760fba16 - files_changed: - - .egg-state/drafts/2623-plan.md - tests_run: [] - tasks_satisfied: [] - version: 2 - commit_sha: f760fba16 -```` - -### [2026-05-19T00:23:09Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 3fa8dbed-b2a3-46 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:23:34Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 2138136f-0010-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:23:15.055576+00:00' -```` - -### [2026-05-19T00:23:34Z] task_planner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6d3a1592-7dd9-43 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:23:49Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ebd6cbd3-457d-4d -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:23:49.864906+00:00' -```` - -### [2026-05-19T00:24:09Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 6660f40d-4cb4-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:24:09Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 3f548e9e-11da-4d -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:24:50Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3e32ef0a-80b8-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:23:49.864906+00:00' -```` - -### [2026-05-19T00:25:09Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 96300726-3b2c-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:25:09Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 04d14f8e-1bce-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:25:50Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8e668321-1dc8-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:23:49.864906+00:00' -```` - -### [2026-05-19T00:26:06Z] reviewer_plan → task_planner (CONSENSUS_ACK): ACK from reviewer_plan for task_planner - - -Re-reviewed `.egg-state/drafts/2623-plan.md` (v2, commit `f760fba16`, 1,348 lines, +525/-219 since v1). Walked every blocker from the v1 NACK against the re-propose: - -1. **Trust-Boundary scope** — rewritten. v2 cites `EggStack` at `integration_tests/conftest.py:71` (verified — `grep -n 'class EggStack' integration_tests/conftest.py` → `71:class EggStack(GatewayClientMixin):`), `egg_stack` fixture at `:340`, `orchestrator_url` fixture at `:357` (both verified). Drops the deleted `local_pipeline/` reference and explicitly names `f7803637d1` as the deletion commit. Implications block clearly states "Both parameters run **pure-Python in-process**. No kubectl gate is needed (or claimed) for either parameter." Cleanly addresses v1 blockers #1, #9, #11. - -2. **Substrate-distinguishing test** — replaced. v2 introduces a NEW `integration_tests/regression/test_substrate_smoke.py` that drives `select_substrate(...).spawner.spawn(...)` and `.bus.add_message/get_messages` directly. TASK-1-8 acceptance #2 (lines 1220-1234): k3s dimension routes through `K3sSpawnerAdapter` (with the underlying `create_concurrent_spawn_fn` mocked); claude-code dimension routes through `ClaudeCodeSpawner` + `InProcessMessageBus`. Both produce divergent execution paths and the test asserts `AgentResult.commit_sha` populated and INV-3 stale-version rejection round-trip. v1 blocker #2 cleanly addressed. - -3. **k3s adapter contradiction** — resolved. TASK-1-1 acceptance #3 (lines 276-280): `select_substrate({})` defaults to "k3s" and returns a **working** `K3sSpawnerAdapter` wrapping `create_concurrent_spawn_fn` (`orchestrator/kubernetes_spawner.py:1564` — verified by grep). TASK-1-2 description lines 320-326: "Both substrate legs are working under this patch: the k3s leg returns the `K3sSpawnerAdapter` shim from TASK-1-1 … the claude-code leg returns this task's `ClaudeCodeSpawner`. The patch is a re-wiring, not a behavior change — k3s deployments stay green." The only `NotImplementedError` raised in this slice is now TASK-1-6's `run_pipeline_in_process` k3s leg — the cq-11 scope-fence is on the in-process *boot path*, not on the spawner protocol. Internally consistent. v1 blocker #3 cleanly addressed. - -4. **AgentResult.commit_sha for INV-6** — added. TASK-1-1 acceptance #2 (lines 272-275): "`AgentResult` includes `commit_sha: str | None`". TASK-1-2 acceptance #2 (lines 333-337): "captured via `git -C rev-parse HEAD`"; "A unit test asserts `commit_sha` is a 40-char hex string after a spawn that produced a commit." TASK-1-1 adapter and TASK-1-2 ClaudeCodeSpawner both capture commit_sha (lines 261-263 and 311-315 respectively). INV-6 (`orchestrator/action_guards.py:631`, body at `:757` — verified line numbers) is preserved across both substrates. v1 blocker #4 cleanly addressed. - -5. **TASK-1-5 path-escape primitive** — corrected. Plan now cites `gateway/worktree_manager.py:1711` (call site within `_remove_worktree`) and `:1700` (matching `base.resolve()`) — verified by grep (`gateway/worktree_manager.py:1693` docstring; `:1711` is `if not resolved.is_relative_to(base_resolved):`). The bogus `gateway/gateway.py:7903` reference is gone. v1 blocker #5 cleanly addressed. - -6. **TASK-1-3 BRC oracle directory** — corrected. TASK-1-3 description (lines 1046-1051) and acceptance (lines 1052-1059) point at `orchestrator/tests/test_brc_*.py` with named scenarios from `test_brc_open_nacks_barrier.py` and `test_brc_content_validation.py` (both verified to exist). The bogus `shared/tests/test_brc_*` reference is gone. Tester role's behavioral test lands at `shared/tests/test_in_process_message_bus.py` (TASK-1-8 file list line 1252) — tester-allowed path. v1 blocker #6 cleanly addressed. - -7. **TASK-1-6 R4 acceptance criteria** — added. TASK-1-6 description (lines 1117-1131) explicitly calls out the refactor scope: "This is the slice's most expensive task — risk_analyst R4 flagged the orchestrator-reentrancy refactor as `requires_human_review` (effort HIGH)." Acceptance now has three R4-specific bullets: **heartbeat-during-HITL** ("background heartbeat poll, BRC re-review, and message-bus tick threads remain alive while the generator is paused at a yield boundary"), **background-thread lifetime** ("the generator returns cleanly on both normal completion AND on `GeneratorExit`"), **contract-state synchronization** ("the in-process orchestrator writes to the same `.egg-state/contracts/.json` filesystem path the HTTP daemon uses"). Each has a concrete verification path. v1 blocker #7 cleanly addressed. - -8. **Risk-mitigation acceptance in TASK-1-9 ADR** — added. ADR acceptance (lines 1269-1291) now has dedicated subsections for R1 ("Trust-context shift" — credential exposure), R2 ("PreToolUse hook fallback" — primary + MCP-validator-side fallback), R7 ("Subagent context budget regression"), R10 ("Interface stability marker" — `# v0.x unstable` until ≥3 roles), REC5 ("Cost cap" — `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`), R15 ("Subagent type model" — `general-purpose` choice + trade-off). Each is required as an explicit acceptance bullet, not buried in narrative. v1 blocker #8 cleanly addressed. - -9. **TASK-1-8 kubectl claim** — corrected per #1 above. - -10. **cq-12 HITL for pip name** — registered. `mcp__sdlc__show_contract` confirms a `cq-12` decision in the contract with three substantive options (PyPI `egg-shared` publish; `git+https://github.com/jwbron/egg.git#subdirectory=shared`; "Defer to follow-up TODO placeholder") plus "Other". TASK-1-7 description (lines 1170-1180) and acceptance (lines 1182-1193) carry the operator's selection verbatim with TODO-placeholder fallback. v1 blocker #10 cleanly addressed. - -11. **EggStack line number** — corrected to `:71` per #1. - -Primitive-audit grep verdict on the v2 Primitives table (lines 109-139): all newly added rows cite real file:line locations — `validate_invariants` at `:631`/body at `:757` (verified — `def validate_invariants` is at `orchestrator/action_guards.py:631`); `create_concurrent_spawn_fn` at `:1564`; `make_tracker` at `integration_tests/regression/_helpers.py:156`; `PeerConsensusTracker` at `orchestrator/peer_consensus.py:69`; existing regression fixtures `single_reviewer_graph`/`event_capture`/`filter_events` at `:564`/`:519`/`:552` (verified). BRC test directory `orchestrator/tests/test_brc_*.py` — verified the directory contains 15+ matching files. The Primitives table is now grep-clean. - -Slice-DAG and forest checks: single root slice (slice id 1), no `serialized_chain_order` required, no forest violation. Plan ingestion would accept this. - -### Non-blocking - -The structural plan is sound and I will not re-NACK on these. Each is a mechanical cleanup; please pick up before opening the PR so the auto-rendered PR body matches the actual code. - -1. **`yaml-tasks → pr.description.Changes[1]` still says "The k3s adapter is a deferred TODO that raises `NotImplementedError` (cq-11 explicit boundary)" (line 866-867).** This directly contradicts the v2 task definitions: TASK-1-1 acceptance #3 ships a *working* `K3sSpawnerAdapter`. The yaml PR description is what becomes the actual GitHub PR body when the implement-phase coder runs `gh pr create` — future readers of the PR will be misled. Replace with: "The k3s leg uses a working `K3sSpawnerAdapter` shim wrapping `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn`; the `NotImplementedError` scope-fence applies only to TASK-1-6's `run_pipeline_in_process` boot path (cq-11)." - -2. **`yaml-tasks → pr.test_plan` still references the wrong test file (line 907):** "`test_brc_single_cycle.py` passes under both `substrate` parameters". The new substrate-distinguishing test is `test_substrate_smoke.py` (TASK-1-8 acceptance #2, file at line 1248). Replace `test_brc_single_cycle.py` with `test_substrate_smoke.py` in the yaml test_plan. - -3. **Plan §"Test strategy" → "Behavioral parity" bullet (lines 778-781)** has the same stale reference: "the existing `test_brc_single_cycle.py` runs under both `substrate` parameters; the claude-code parameter exercises `InProcessMessageBus` + `ClaudeCodeSpawner`. Pass on both = behavioral parity proof." Replace with the new test_substrate_smoke.py phrasing. - -4. **Primitives "New primitives" table row "test_brc_single_cycle_claude_code" at `integration_tests/regression/test_brc_single_cycle.py (existing — extended)" (line 161)** is stale. Remove this row entirely; the actual artifact is the new `test_substrate_smoke.py` row that needs to be added in its place (`integration_tests/regression/test_substrate_smoke.py` NEW, created by TASK-1-8 tester). - -5. **`yaml-tasks → pr.description.Impact` and "Manual verification (reviewer)" step 1 and `yaml.test_plan` still hardcode `pip install egg-shared`** (lines 798, 893, 909). The actual install string is whatever cq-12 resolves to. Replace with conditional phrasing: "`pip install ` (currently TBD pending operator selection — see `plugins/egg-sdlc/.claude-plugin/plugin.json` after merge)" or leave the placeholder explicitly bracketed (``) so a coder picks it up at implement time. - -6. **The cq-12 decision is currently unresolved.** The plan's TASK-1-7 handles this gracefully (TODO placeholder if cq-12 defers), but if the operator wants to resolve cq-12 *before* implement-phase starts, that gives the coder a deterministic string to ship. Not a plan-phase blocker; just calling it out for the orchestrator's HITL gate scheduling. - -7. **Slice-sizing advisory (#2137 size advisory; never NACK):** v2's estimated 1,800–2,500 LOC stands (no change). Per HITL decision-6 opt-2 I do NOT NACK on size; this is advisory only and the spike-then-plan framing (cq-11) justifies the size. The next reviewer of the implement-phase BRC cycle should know the slice will be expensive to NACK and the producer should land in tight, well-scoped commits per task. - -8. **R5 (git lock contention on concurrent push) and R12 (heartbeat tautology / wall-clock timeouts) from the risk_analyst** are still not surfaced in any acceptance criteria. The spike's single-role / single-phase scope makes these unobservable (no concurrent producer dispatch in the refine phase), so this is acceptable scope-fencing — but worth listing in TASK-1-9's "Open work" appendix so the follow-up issue inherits them. Currently the Open work list (line 1285-1291) names plan/implement/pr, BRC concurrency, full conformance, k3s adapter, EggHarnessSpawner, prune verb, fork delegation, cost cap, subagent_type migration — but not R5's push-serialization or R12's wall-clock-timeout watchdog. Consider adding these two items. - -9. **Risk_analyst's REC4 (expand spike to 2 roles)** is NOT picked up — the plan stays at refiner-only, consistent with cq-11. This is the right call per the resolved HITL; the operator can revisit by re-opening cq-11 if the spike's interface lessons end up too thin from one role's perspective. Calling it out so the producer / operator know the path explicitly. - - -````yaml -id: eba47701-2da3-4e -phase: plan -metadata: - payload: - artifact_references: - - .egg-state/drafts/2623-plan.md - - .egg-state/contracts/issue-2623.json - - orchestrator/concurrent_executor.py - - orchestrator/kubernetes_spawner.py - - orchestrator/peer_consensus.py - - orchestrator/action_guards.py - - integration_tests/conftest.py - - integration_tests/regression/conftest.py - - integration_tests/regression/test_brc_single_cycle.py - - gateway/worktree_manager.py - - shared/egg_restrictions/patterns.py - reason: "\nRe-reviewed `.egg-state/drafts/2623-plan.md` (v2, commit `f760fba16`,\ - \ 1,348 lines, +525/-219 since v1). Walked every blocker from the v1 NACK against\ - \ the re-propose:\n\n1. **Trust-Boundary scope** \u2014 rewritten. v2 cites\ - \ `EggStack` at `integration_tests/conftest.py:71` (verified \u2014 `grep -n\ - \ 'class EggStack' integration_tests/conftest.py` \u2192 `71:class EggStack(GatewayClientMixin):`),\ - \ `egg_stack` fixture at `:340`, `orchestrator_url` fixture at `:357` (both\ - \ verified). Drops the deleted `local_pipeline/` reference and explicitly names\ - \ `f7803637d1` as the deletion commit. Implications block clearly states \"\ - Both parameters run **pure-Python in-process**. No kubectl gate is needed (or\ - \ claimed) for either parameter.\" Cleanly addresses v1 blockers #1, #9, #11.\n\ - \n2. **Substrate-distinguishing test** \u2014 replaced. v2 introduces a NEW\ - \ `integration_tests/regression/test_substrate_smoke.py` that drives `select_substrate(...).spawner.spawn(...)`\ - \ and `.bus.add_message/get_messages` directly. TASK-1-8 acceptance #2 (lines\ - \ 1220-1234): k3s dimension routes through `K3sSpawnerAdapter` (with the underlying\ - \ `create_concurrent_spawn_fn` mocked); claude-code dimension routes through\ - \ `ClaudeCodeSpawner` + `InProcessMessageBus`. Both produce divergent execution\ - \ paths and the test asserts `AgentResult.commit_sha` populated and INV-3 stale-version\ - \ rejection round-trip. v1 blocker #2 cleanly addressed.\n\n3. **k3s adapter\ - \ contradiction** \u2014 resolved. TASK-1-1 acceptance #3 (lines 276-280): `select_substrate({})`\ - \ defaults to \"k3s\" and returns a **working** `K3sSpawnerAdapter` wrapping\ - \ `create_concurrent_spawn_fn` (`orchestrator/kubernetes_spawner.py:1564` \u2014\ - \ verified by grep). TASK-1-2 description lines 320-326: \"Both substrate legs\ - \ are working under this patch: the k3s leg returns the `K3sSpawnerAdapter`\ - \ shim from TASK-1-1 \u2026 the claude-code leg returns this task's `ClaudeCodeSpawner`.\ - \ The patch is a re-wiring, not a behavior change \u2014 k3s deployments stay\ - \ green.\" The only `NotImplementedError` raised in this slice is now TASK-1-6's\ - \ `run_pipeline_in_process` k3s leg \u2014 the cq-11 scope-fence is on the in-process\ - \ *boot path*, not on the spawner protocol. Internally consistent. v1 blocker\ - \ #3 cleanly addressed.\n\n4. **AgentResult.commit_sha for INV-6** \u2014 added.\ - \ TASK-1-1 acceptance #2 (lines 272-275): \"`AgentResult` includes `commit_sha:\ - \ str | None`\". TASK-1-2 acceptance #2 (lines 333-337): \"captured via `git\ - \ -C rev-parse HEAD`\"; \"A unit test asserts `commit_sha` is a 40-char\ - \ hex string after a spawn that produced a commit.\" TASK-1-1 adapter and TASK-1-2\ - \ ClaudeCodeSpawner both capture commit_sha (lines 261-263 and 311-315 respectively).\ - \ INV-6 (`orchestrator/action_guards.py:631`, body at `:757` \u2014 verified\ - \ line numbers) is preserved across both substrates. v1 blocker #4 cleanly addressed.\n\ - \n5. **TASK-1-5 path-escape primitive** \u2014 corrected. Plan now cites `gateway/worktree_manager.py:1711`\ - \ (call site within `_remove_worktree`) and `:1700` (matching `base.resolve()`)\ - \ \u2014 verified by grep (`gateway/worktree_manager.py:1693` docstring; `:1711`\ - \ is `if not resolved.is_relative_to(base_resolved):`). The bogus `gateway/gateway.py:7903`\ - \ reference is gone. v1 blocker #5 cleanly addressed.\n\n6. **TASK-1-3 BRC oracle\ - \ directory** \u2014 corrected. TASK-1-3 description (lines 1046-1051) and acceptance\ - \ (lines 1052-1059) point at `orchestrator/tests/test_brc_*.py` with named scenarios\ - \ from `test_brc_open_nacks_barrier.py` and `test_brc_content_validation.py`\ - \ (both verified to exist). The bogus `shared/tests/test_brc_*` reference is\ - \ gone. Tester role's behavioral test lands at `shared/tests/test_in_process_message_bus.py`\ - \ (TASK-1-8 file list line 1252) \u2014 tester-allowed path. v1 blocker #6 cleanly\ - \ addressed.\n\n7. **TASK-1-6 R4 acceptance criteria** \u2014 added. TASK-1-6\ - \ description (lines 1117-1131) explicitly calls out the refactor scope: \"\ - This is the slice's most expensive task \u2014 risk_analyst R4 flagged the orchestrator-reentrancy\ - \ refactor as `requires_human_review` (effort HIGH).\" Acceptance now has three\ - \ R4-specific bullets: **heartbeat-during-HITL** (\"background heartbeat poll,\ - \ BRC re-review, and message-bus tick threads remain alive while the generator\ - \ is paused at a yield boundary\"), **background-thread lifetime** (\"the generator\ - \ returns cleanly on both normal completion AND on `GeneratorExit`\"), **contract-state\ - \ synchronization** (\"the in-process orchestrator writes to the same `.egg-state/contracts/.json`\ - \ filesystem path the HTTP daemon uses\"). Each has a concrete verification\ - \ path. v1 blocker #7 cleanly addressed.\n\n8. **Risk-mitigation acceptance\ - \ in TASK-1-9 ADR** \u2014 added. ADR acceptance (lines 1269-1291) now has dedicated\ - \ subsections for R1 (\"Trust-context shift\" \u2014 credential exposure), R2\ - \ (\"PreToolUse hook fallback\" \u2014 primary + MCP-validator-side fallback),\ - \ R7 (\"Subagent context budget regression\"), R10 (\"Interface stability marker\"\ - \ \u2014 `# v0.x unstable` until \u22653 roles), REC5 (\"Cost cap\" \u2014 `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`),\ - \ R15 (\"Subagent type model\" \u2014 `general-purpose` choice + trade-off).\ - \ Each is required as an explicit acceptance bullet, not buried in narrative.\ - \ v1 blocker #8 cleanly addressed.\n\n9. **TASK-1-8 kubectl claim** \u2014 corrected\ - \ per #1 above.\n\n10. **cq-12 HITL for pip name** \u2014 registered. `mcp__sdlc__show_contract`\ - \ confirms a `cq-12` decision in the contract with three substantive options\ - \ (PyPI `egg-shared` publish; `git+https://github.com/jwbron/egg.git#subdirectory=shared`;\ - \ \"Defer to follow-up TODO placeholder\") plus \"Other\". TASK-1-7 description\ - \ (lines 1170-1180) and acceptance (lines 1182-1193) carry the operator's selection\ - \ verbatim with TODO-placeholder fallback. v1 blocker #10 cleanly addressed.\n\ - \n11. **EggStack line number** \u2014 corrected to `:71` per #1.\n\nPrimitive-audit\ - \ grep verdict on the v2 Primitives table (lines 109-139): all newly added rows\ - \ cite real file:line locations \u2014 `validate_invariants` at `:631`/body\ - \ at `:757` (verified \u2014 `def validate_invariants` is at `orchestrator/action_guards.py:631`);\ - \ `create_concurrent_spawn_fn` at `:1564`; `make_tracker` at `integration_tests/regression/_helpers.py:156`;\ - \ `PeerConsensusTracker` at `orchestrator/peer_consensus.py:69`; existing regression\ - \ fixtures `single_reviewer_graph`/`event_capture`/`filter_events` at `:564`/`:519`/`:552`\ - \ (verified). BRC test directory `orchestrator/tests/test_brc_*.py` \u2014 verified\ - \ the directory contains 15+ matching files. The Primitives table is now grep-clean.\n\ - \nSlice-DAG and forest checks: single root slice (slice id 1), no `serialized_chain_order`\ - \ required, no forest violation. Plan ingestion would accept this.\n\n### Non-blocking\n\ - \nThe structural plan is sound and I will not re-NACK on these. Each is a mechanical\ - \ cleanup; please pick up before opening the PR so the auto-rendered PR body\ - \ matches the actual code.\n\n1. **`yaml-tasks \u2192 pr.description.Changes[1]`\ - \ still says \"The k3s adapter is a deferred TODO that raises `NotImplementedError`\ - \ (cq-11 explicit boundary)\" (line 866-867).** This directly contradicts the\ - \ v2 task definitions: TASK-1-1 acceptance #3 ships a *working* `K3sSpawnerAdapter`.\ - \ The yaml PR description is what becomes the actual GitHub PR body when the\ - \ implement-phase coder runs `gh pr create` \u2014 future readers of the PR\ - \ will be misled. Replace with: \"The k3s leg uses a working `K3sSpawnerAdapter`\ - \ shim wrapping `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn`;\ - \ the `NotImplementedError` scope-fence applies only to TASK-1-6's `run_pipeline_in_process`\ - \ boot path (cq-11).\"\n\n2. **`yaml-tasks \u2192 pr.test_plan` still references\ - \ the wrong test file (line 907):** \"`test_brc_single_cycle.py` passes under\ - \ both `substrate` parameters\". The new substrate-distinguishing test is `test_substrate_smoke.py`\ - \ (TASK-1-8 acceptance #2, file at line 1248). Replace `test_brc_single_cycle.py`\ - \ with `test_substrate_smoke.py` in the yaml test_plan.\n\n3. **Plan \xA7\"\ - Test strategy\" \u2192 \"Behavioral parity\" bullet (lines 778-781)** has the\ - \ same stale reference: \"the existing `test_brc_single_cycle.py` runs under\ - \ both `substrate` parameters; the claude-code parameter exercises `InProcessMessageBus`\ - \ + `ClaudeCodeSpawner`. Pass on both = behavioral parity proof.\" Replace with\ - \ the new test_substrate_smoke.py phrasing.\n\n4. **Primitives \"New primitives\"\ - \ table row \"test_brc_single_cycle_claude_code\" at `integration_tests/regression/test_brc_single_cycle.py\ - \ (existing \u2014 extended)\" (line 161)** is stale. Remove this row entirely;\ - \ the actual artifact is the new `test_substrate_smoke.py` row that needs to\ - \ be added in its place (`integration_tests/regression/test_substrate_smoke.py`\ - \ NEW, created by TASK-1-8 tester).\n\n5. **`yaml-tasks \u2192 pr.description.Impact`\ - \ and \"Manual verification (reviewer)\" step 1 and `yaml.test_plan` still hardcode\ - \ `pip install egg-shared`** (lines 798, 893, 909). The actual install string\ - \ is whatever cq-12 resolves to. Replace with conditional phrasing: \"`pip install\ - \ ` (currently TBD pending operator selection \u2014\ - \ see `plugins/egg-sdlc/.claude-plugin/plugin.json` after merge)\" or leave\ - \ the placeholder explicitly bracketed (``) so a coder\ - \ picks it up at implement time.\n\n6. **The cq-12 decision is currently unresolved.**\ - \ The plan's TASK-1-7 handles this gracefully (TODO placeholder if cq-12 defers),\ - \ but if the operator wants to resolve cq-12 *before* implement-phase starts,\ - \ that gives the coder a deterministic string to ship. Not a plan-phase blocker;\ - \ just calling it out for the orchestrator's HITL gate scheduling.\n\n7. **Slice-sizing\ - \ advisory (#2137 size advisory; never NACK):** v2's estimated 1,800\u20132,500\ - \ LOC stands (no change). Per HITL decision-6 opt-2 I do NOT NACK on size; this\ - \ is advisory only and the spike-then-plan framing (cq-11) justifies the size.\ - \ The next reviewer of the implement-phase BRC cycle should know the slice will\ - \ be expensive to NACK and the producer should land in tight, well-scoped commits\ - \ per task.\n\n8. **R5 (git lock contention on concurrent push) and R12 (heartbeat\ - \ tautology / wall-clock timeouts) from the risk_analyst** are still not surfaced\ - \ in any acceptance criteria. The spike's single-role / single-phase scope makes\ - \ these unobservable (no concurrent producer dispatch in the refine phase),\ - \ so this is acceptable scope-fencing \u2014 but worth listing in TASK-1-9's\ - \ \"Open work\" appendix so the follow-up issue inherits them. Currently the\ - \ Open work list (line 1285-1291) names plan/implement/pr, BRC concurrency,\ - \ full conformance, k3s adapter, EggHarnessSpawner, prune verb, fork delegation,\ - \ cost cap, subagent_type migration \u2014 but not R5's push-serialization or\ - \ R12's wall-clock-timeout watchdog. Consider adding these two items.\n\n9.\ - \ **Risk_analyst's REC4 (expand spike to 2 roles)** is NOT picked up \u2014\ - \ the plan stays at refiner-only, consistent with cq-11. This is the right call\ - \ per the resolved HITL; the operator can revisit by re-opening cq-11 if the\ - \ spike's interface lessons end up too thin from one role's perspective. Calling\ - \ it out so the producer / operator know the path explicitly.\n" - ack_version: 2 - version: 2 -```` - -### [2026-05-19T00:26:06Z] orchestrator → task_planner (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 2) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: d639bbf7-f495-47 -phase: plan -metadata: - ready_to_confirm: true - version: 2 -```` - -### [2026-05-19T00:26:06Z] task_planner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c0377c7d-355a-4d -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:26:09Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 0320d5e9-a8c8-45 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:04.318709+00:00' -```` - -### [2026-05-19T00:26:09Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 78c87376-9b21-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:14:07.149250+00:00' -```` - -### [2026-05-19T00:26:19Z] reviewer_plan (CONSENSUS_CONFIRMED): Confirmed by reviewer_plan - -````yaml -id: 9abb692e-e971-4b -phase: plan -metadata: - consensus_reached: false -```` - -### [2026-05-19T00:26:19Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 1b39451e-005a-4d -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:26:19Z] architect (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: bac486c3-f75c-45 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:26:29Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 696eb922-af87-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:26:29.251699+00:00' -```` - -### [2026-05-19T00:26:29Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: cb9ddd3a-739e-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:26:29.959878+00:00' -```` - -### [2026-05-19T00:26:30Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 900672ca-b373-45 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:26:32Z] task_planner (CONSENSUS_CONFIRMED): Confirmed by task_planner - -````yaml -id: b3451bb7-e71d-4d -phase: plan -metadata: - consensus_reached: true -```` - -### [2026-05-19T00:26:32Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f03ebb75-f0f3-43 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:26:33Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: de289b86-8d47-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:26:33.858056+00:00' -```` - -### [2026-05-19T00:26:34Z] architect (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 13696345-3ab7-4b -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T00:27:02Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 0a1c07b9-0250-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:26:48.496740+00:00' -```` - -### [2026-05-19T00:27:02Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 87b9ce76-749d-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T00:26:39.211574+00:00' -```` - -### [2026-05-19T00:27:02Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6b67ccd2-d8e5-45 -phase: plan -metadata: - state: WORKING -```` diff --git a/.egg-state/brc-history/2623-refine.json b/.egg-state/brc-history/2623-refine.json deleted file mode 100644 index 346e7cdfdf..0000000000 --- a/.egg-state/brc-history/2623-refine.json +++ /dev/null @@ -1,409 +0,0 @@ -[ - { - "id": "2d10ec48-b040-44", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:26:54.704960+00:00", - "phase": "refine" - }, - { - "id": "970eb82b-3bbe-4a", - "pipeline_id": "issue-2623", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:27:15.382011+00:00" - }, - "timestamp": "2026-05-18T22:27:22.196399+00:00", - "phase": "refine" - }, - { - "id": "ffc0873b-004e-4a", - "pipeline_id": "issue-2623", - "from_role": "overseer", - "to_role": "all", - "message_type": "OVERSEER_ALERT", - "subject": "agent-heartbeat-stall [high]", - "body": "Orchestrator unreachable for pipeline issue-2623; refine phase has not started\n\nDetail:\nPipeline issue-2623 is in 'refine' phase with no agent executions, no checkpoints, and no BRC history. The orchestrator HTTP endpoint (orchestrator.egg-system.svc.cluster.local:9849) is timing out on all calls: /api/v1/pipelines/issue-2623/status, BRC state queries, and progress queries. The contract gateway is reachable and confirms contract schemaVersion 1.1 with an empty slate. The overseer monitor script reports pipeline_unreachable and is running with default calibration thresholds. No work can be dispatched until the orchestrator recovers.\n\nRecommended action:\nCheck orchestrator pod health (kubectl get pods -n egg-system, kubectl logs -n egg-system deploy/egg-orchestrator). If the orchestrator pod is CrashLooping or not ready, restart it. If the network policy is blocking overseer\u2192orchestrator traffic, patch the NetworkPolicy. Once orchestrator recovers, pipeline refine phase should self-start.", - "metadata": {}, - "timestamp": "2026-05-18T22:27:50.849089+00:00", - "phase": "refine" - }, - { - "id": "e75e6b7b-9e89-4c", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:27:54.753275+00:00", - "phase": "refine" - }, - { - "id": "a37b45ec-609d-47", - "pipeline_id": "issue-2623", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_ON_ROLE", - "body": "Reviewer restarted by consensus wrapper; waiting for refiner CONSENSUS_PROPOSE before reviewing.", - "metadata": { - "state": "WAITING_ON_ROLE", - "waiting_on": "refiner" - }, - "timestamp": "2026-05-18T22:28:20.248398+00:00", - "phase": "refine" - }, - { - "id": "ed2d7095-1dc0-47", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:28:54.806488+00:00", - "phase": "refine" - }, - { - "id": "fe3bd29d-d3b6-41", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:29:54.893327+00:00", - "phase": "refine" - }, - { - "id": "8e97a9d7-97e3-4b", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:30:54.958776+00:00", - "phase": "refine" - }, - { - "id": "ebab821c-e19d-44", - "pipeline_id": "issue-2623", - "from_role": "overseer", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "Overseer active: monitoring refine BRC cycle. Refiner running at 305s, reviewers healthy and waiting on CONSENSUS_PROPOSE. No stalls on pipeline agents. Calibration artifact alert (calibration_only=true) is suppressed. Advisor gate cleared.", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-18T22:31:26.136864+00:00", - "phase": "refine" - }, - { - "id": "18f08d5f-c27e-4f", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:31:55.028799+00:00", - "phase": "refine" - }, - { - "id": "28f81880-88c4-46", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:32:55.088886+00:00", - "phase": "refine" - }, - { - "id": "c3c8bc8b-673a-49", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:33:55.147873+00:00", - "phase": "refine" - }, - { - "id": "fd551e87-b63f-48", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:26:40.395698+00:00" - }, - "timestamp": "2026-05-18T22:34:55.278507+00:00", - "phase": "refine" - }, - { - "id": "2e92f1a5-8b3f-43", - "pipeline_id": "issue-2623", - "from_role": "refiner", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from refiner", - "body": "Drafted the refine-phase analysis for #2623 (substrate swap: run the full egg SDLC stack natively in Claude Code). The document characterises today's k3s/Redis/Docker/gateway substrate against the issue's substrate-swap table; traces the abstraction seams in the orchestrator (MessageStore already abstracted, KubernetesSpawner has no AgentSpawner interface yet, gateway policy in shared/egg_restrictions is portable, contracts/checkpoints already filesystem-native, refine-plan skill is a parallel implementation rather than the destination); evaluates four implementation options and recommends Option A (parallel substrates behind named AgentSpawner/MessageBus/PolicyEnforcer interfaces, env-var-selected, cross-substrate conformance CI matrix) because it makes the issue's \"behavioral conformance on the same tests\" definition-of-done structurally provable rather than narratively asserted, contains migration risk, and lets the depth-gap (#2622) close as a byproduct of routing through real build_system_prompt. Every uncertainty is registered: 11 multiple-choice decisions (cq-1..cq-11: substrate coexistence; phase scope; conformance suite scoping; spawner interface shape; worktree ownership; policy enforcement seam; HITL surface; install footprint; k3s deprecation; context-window strategy; slice-DAG shape) plus an open-ended feedback record (feedback-1) with six questions covering conformance-set selection, latency budgets, third-party-dep constraints, non-Claude-Code callers, #2622 absorption appetite, and telemetry/privacy regressions. Issue body's framing-update content (children dispositions, substrate-swap intent) is noted as pre-resolved in the analysis to keep the plan phase from re-litigating them.", - "metadata": { - "payload": { - "summary": "Drafted the refine-phase analysis for #2623 (substrate swap: run the full egg SDLC stack natively in Claude Code). The document characterises today's k3s/Redis/Docker/gateway substrate against the issue's substrate-swap table; traces the abstraction seams in the orchestrator (MessageStore already abstracted, KubernetesSpawner has no AgentSpawner interface yet, gateway policy in shared/egg_restrictions is portable, contracts/checkpoints already filesystem-native, refine-plan skill is a parallel implementation rather than the destination); evaluates four implementation options and recommends Option A (parallel substrates behind named AgentSpawner/MessageBus/PolicyEnforcer interfaces, env-var-selected, cross-substrate conformance CI matrix) because it makes the issue's \"behavioral conformance on the same tests\" definition-of-done structurally provable rather than narratively asserted, contains migration risk, and lets the depth-gap (#2622) close as a byproduct of routing through real build_system_prompt. Every uncertainty is registered: 11 multiple-choice decisions (cq-1..cq-11: substrate coexistence; phase scope; conformance suite scoping; spawner interface shape; worktree ownership; policy enforcement seam; HITL surface; install footprint; k3s deprecation; context-window strategy; slice-DAG shape) plus an open-ended feedback record (feedback-1) with six questions covering conformance-set selection, latency budgets, third-party-dep constraints, non-Claude-Code callers, #2622 absorption appetite, and telemetry/privacy regressions. Issue body's framing-update content (children dispositions, substrate-swap intent) is noted as pre-resolved in the analysis to keep the plan phase from re-litigating them.", - "attestation": {}, - "artifacts": [ - ".egg-state/drafts/2623-analysis.md" - ], - "risk_considered": "", - "commit_sha": "82e04b84c", - "files_changed": [ - ".egg-state/drafts/2623-analysis.md" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 1, - "commit_sha": "82e04b84c" - }, - "timestamp": "2026-05-18T22:35:32.145660+00:00", - "phase": "refine" - }, - { - "id": "a8d78eac-804c-4f", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-18T22:35:32.183692+00:00", - "phase": "refine" - }, - { - "id": "ee5686d9-2e14-41", - "pipeline_id": "issue-2623", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:35:37.571687+00:00" - }, - "timestamp": "2026-05-18T22:36:00.681814+00:00", - "phase": "refine" - }, - { - "id": "5eb4b9b1-2bbc-40", - "pipeline_id": "issue-2623", - "from_role": "reviewer_agent_design", - "to_role": "refiner", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_agent_design for refiner", - "body": "Refine artifact covers the agent-design seams thoroughly. The 11 HITL decisions surface every architectural axis the substrate swap touches \u2014 spawner interface shape (cq-4), worktree ownership (cq-5), policy enforcement seam (cq-6), HITL surface (cq-7), and subagent context strategy (cq-10) are the load-bearing agent-design decisions and each option is articulated with the right trade-offs (sync vs async vs stream-shaped, native worktree vs egg's WORKTREE_BASE_DIR, hook vs MCP vs prompt enforcement, etc.). The 6 open-ended feedback items appropriately probe acceptance criteria (conformance-issue selection, latency budgets, dep constraints, non-Claude-Code drivers, cross-issue scope with #2622, telemetry/privacy). The decision options are framed so the operator can pick without writing free-form text. No agent-design gaps identified.", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/contract-issue-2623.json" - ], - "reason": "Refine artifact covers the agent-design seams thoroughly. The 11 HITL decisions surface every architectural axis the substrate swap touches \u2014 spawner interface shape (cq-4), worktree ownership (cq-5), policy enforcement seam (cq-6), HITL surface (cq-7), and subagent context strategy (cq-10) are the load-bearing agent-design decisions and each option is articulated with the right trade-offs (sync vs async vs stream-shaped, native worktree vs egg's WORKTREE_BASE_DIR, hook vs MCP vs prompt enforcement, etc.). The 6 open-ended feedback items appropriately probe acceptance criteria (conformance-issue selection, latency budgets, dep constraints, non-Claude-Code drivers, cross-issue scope with #2622, telemetry/privacy). The decision options are framed so the operator can pick without writing free-form text. No agent-design gaps identified.", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-18T22:36:32.317760+00:00", - "phase": "refine" - }, - { - "id": "d15b2724-a1d4-44", - "pipeline_id": "issue-2623", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-18T22:36:32.422292+00:00", - "phase": "refine" - }, - { - "id": "dca25e4d-e669-47", - "pipeline_id": "issue-2623", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:36:36.351878+00:00" - }, - "timestamp": "2026-05-18T22:36:36.375945+00:00", - "phase": "refine" - }, - { - "id": "095de55a-bdbd-4e", - "pipeline_id": "issue-2623", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_agent_design", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-18T22:36:41.856399+00:00", - "phase": "refine" - }, - { - "id": "9b326f70-eb6a-45", - "pipeline_id": "issue-2623", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-18T22:36:36.351878+00:00" - }, - "timestamp": "2026-05-18T22:37:36.421799+00:00", - "phase": "refine" - }, - { - "id": "2e00e6d7-4043-4a", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "refiner", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_refine for refiner", - "body": "\nReviewed `.egg-state/drafts/2623-analysis.md` (326 lines) end-to-end and cross-checked HITL registration via `mcp__sdlc__show_contract`. All 11 decisions (cq-1..cq-11) and the 6-question feedback record (feedback-1, Q1..Q6) are populated on the canonical contract with full option text. Approving.\n\n### Section-by-section evaluation\n\n**1. Problem Understanding \u2014 strong.** The draft correctly reframes the issue from \"approximate k3s pipeline quality with a Markdown skill\" to \"swap the substrate underneath the real stack\" (lines 7\u201311) and faithfully reproduces the issue body's substrate-swap table (lines 13\u201322). The four desired outcomes (lines 28\u201331) map 1:1 onto the issue's definition-of-done plus the implicit \"user can run egg locally\" goal. The placement of PR #2608 / the existing refine-plan skill as \"starting substrate, not destination\" (lines 65\u201369) is the right framing \u2014 it prevents the plan phase from drifting back toward parity-chasing.\n\n**2. Research Quality \u2014 strong.** Evidence is anchored to specific files and call sites: `orchestrator/cli.py::cmd_serve()` at lines 83\u2013150, `KubernetesSpawner.spawn_agent_job()`, `concurrent_executor.py::_spawn_agent()`, `orchestrator/message_store.py`'s `MessageStore` abstraction vs. `redis_message_store.py`, the 36-line `container_spawner.py` back-compat shim, `gateway/agent_restrictions.py` as a pure-Python module, `shared/egg_harness/prompt.py::build_system_prompt(sources)`, `orchestrator/action_guards.py::validate_invariants()` for INV-1..5, and `shared/egg_restrictions/patterns.py` for file-write boundaries. The observation that \"no `AgentSpawner` interface today\" is \"the largest abstraction gap in the codebase for the substrate swap\" (line 51) is correct and load-bearing for the recommended approach. The note that only three files import `redis` directly (line 45) is accurate and materially shrinks the bus-swap surface.\n\n**3. Options Analysis \u2014 strong.** Four genuinely distinct options:\n- A (abstraction-first parallel substrates + CI matrix) \u2014 recommended\n- B (delete k3s entirely)\n- C (skill-only; never touch the orchestrator)\n- D (in-process binding without named interfaces)\nEach option's pros/cons are specific and avoid hedging. Option C is explicitly flagged as contradicting the issue's North Star but listed for completeness, which is the correct discipline. Option D's failure mode (\"substrate boundary isn't visible in the code\") is the right concern to surface.\n\n**4. Constraints \u2014 strong.** Hard constraints (SendMessage gating with bug link to anthropics/claude-code#36196, subagent context windows referencing `max_turns: 1000` at `docs/guides/concurrent-execution.md:97`, HITL surface, concurrency ceiling, install footprint), architectural constraints (depth gap, BRC invariants, gateway-equivalent enforcement, file-write boundaries), conformance/proof obligation, and dependencies (marketplace packaging, harness-mode selector) are all enumerated with file pointers. Lines 90\u201391 correctly flag that some k3s-shaped tests (\"live-pod-guard on restart\") are k3s-specific and won't translate \u2014 the conformance set needs to be factored, not blindly re-run.\n\n**5. Open Questions \u2014 strong.** 11 decisions cover the structural surfaces (substrate coexistence, phase scope, conformance scoping, spawner interface shape, worktree ownership, policy enforcement seam, HITL surface, packaging, k3s deprecation, context-window strategy, slice-DAG shape) and 6 open-ended feedback questions cover the policy decisions that need human judgment (representative-issue selection, latency budgets, dependency constraints, non-Claude-Code callers, #2622 cause-#5/#6 scope, telemetry/privacy). The \"Pre-Refine Context\" section (lines 180\u2013189) explicitly captures what the operator already settled in the issue body and comment, which protects the plan phase from re-litigating settled framing. Every question is actionable: each is paired with 4\u20135 named options containing the substantive trade-offs in the option labels themselves, so the operator does not have to cross-reference back to the analysis to vote.\n\n**6. Recommendation Quality \u2014 strong.** Option A is justified by five concrete arguments tied to the issue body and the analysis findings: conformance proof is executable (not narrative), Claude Code constraints surface naturally as interface contracts, depth-gap closure becomes structural rather than rubric-based, child-issue absorption is verified rather than asserted, and migration safety is preserved. The closing line (line 174) correctly scopes the plan phase to \"slice-DAG shape\" rather than re-opening the framing.\n\n**7. HITL Registration \u2014 verified.** `mcp__sdlc__show_contract` returns 11 fully-populated decisions and a 6-question feedback record. Each cq-N decision in the draft corresponds to a `decisions[N-1]` entry on the contract with matching question text and option labels. `feedback.questions` Q1..Q6 match the draft's open-ended block at lines 312\u2013322. No prose-only open questions were left unregistered.\n\n### Non-blocking\n\n- **`.egg-state/contracts/issue-2623.json` is a stub copy on the work branch.** The on-disk JSON at HEAD shows `decisions: []` and `feedback: null`; only the gateway has the full state. This is expected (gateway is source of truth) but the divergence may surprise a future reader doing local-only inspection \u2014 a quick note in implementation phase to teach the local-snapshot writer to flush on registration would close the loop. Not a refine-phase issue.\n- **Feedback marker uses `` (line 306).** The review-criteria convention spells this ``. Functionally fine because the feedback record IS registered on the gateway and discoverable, but normalizing the marker in a later pass would prevent future grep-based tooling from missing it.\n- **cq-2 option 4 (\"Refine + plan + implement (no pr): \u2026 let pr-phase keep using k3s for now\") fights the North Star.** The issue body explicitly wants the full SDLC running without k3s. Listing this option for completeness is fine; the operator can reject it. Worth flagging because plan phase should not silently treat \"pr keeps using k3s\" as a low-risk fallback if the operator declines to choose it explicitly.\n- **cq-11 option 2 labels itself \"Two-wave parallel\" but enumerates three stages** ([interfaces+ADR] \u2192 [4 parallel implementations] \u2192 [conformance+entry-point]). Cosmetic; either the label or the stage count needs to drop a wave. cq-11 option 3 is correctly labeled \"Three-wave parallel.\"\n- **Option C in the options-analysis prose (line 141) says \"Effectively closes #2622 only, leaving #2612 and the parent's substrate-swap intent unresolved\"** \u2014 slight tension with the Pre-Refine Context section's claim that #2612 is largely absorbed by the parent-issue framing. Reads correctly in context (Option C *would* fail to absorb #2612 because it doesn't bring the real orchestrator in-process), but a one-line clarification (\"\u2026which is why this option contradicts the framing the operator already settled\") would help the operator weigh it.\n- **The substrate table in the issue body lists \"Persistent volume\" and \"kubectl get pods / health checks\" rows** that the draft summarizes but doesn't anchor to specific code paths (no equivalent of the `WORKTREE_BASE_DIR` and `egg_health` callouts the prose uses elsewhere). These are mentioned generically (lines 21\u201322 of the draft table) but not researched at the same depth as the spawner, bus, and gateway. Plan phase can fill these in; refine-phase scope is adequate without them.\n- **Pre-Refine Context section (line 187) phrases #2622 as \"largely absorbed; real `build_system_prompt` closes the depth gap structurally\"** \u2014 true for the four causes the analysis enumerates in Option A's pros, but the issue body explicitly carves out \"tactical shrinkers (depth-check reviewer criterion, parity-test harness) survive as cross-substrate validators.\" Question Q5 in the feedback block correctly opens this scope, but the Pre-Refine Context could note that the absorption isn't 100% to keep the plan phase from prematurely closing #2622.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/drafts/2623-analysis.md", - ".egg-state/contracts/issue-2623.json" - ], - "reason": "\nReviewed `.egg-state/drafts/2623-analysis.md` (326 lines) end-to-end and cross-checked HITL registration via `mcp__sdlc__show_contract`. All 11 decisions (cq-1..cq-11) and the 6-question feedback record (feedback-1, Q1..Q6) are populated on the canonical contract with full option text. Approving.\n\n### Section-by-section evaluation\n\n**1. Problem Understanding \u2014 strong.** The draft correctly reframes the issue from \"approximate k3s pipeline quality with a Markdown skill\" to \"swap the substrate underneath the real stack\" (lines 7\u201311) and faithfully reproduces the issue body's substrate-swap table (lines 13\u201322). The four desired outcomes (lines 28\u201331) map 1:1 onto the issue's definition-of-done plus the implicit \"user can run egg locally\" goal. The placement of PR #2608 / the existing refine-plan skill as \"starting substrate, not destination\" (lines 65\u201369) is the right framing \u2014 it prevents the plan phase from drifting back toward parity-chasing.\n\n**2. Research Quality \u2014 strong.** Evidence is anchored to specific files and call sites: `orchestrator/cli.py::cmd_serve()` at lines 83\u2013150, `KubernetesSpawner.spawn_agent_job()`, `concurrent_executor.py::_spawn_agent()`, `orchestrator/message_store.py`'s `MessageStore` abstraction vs. `redis_message_store.py`, the 36-line `container_spawner.py` back-compat shim, `gateway/agent_restrictions.py` as a pure-Python module, `shared/egg_harness/prompt.py::build_system_prompt(sources)`, `orchestrator/action_guards.py::validate_invariants()` for INV-1..5, and `shared/egg_restrictions/patterns.py` for file-write boundaries. The observation that \"no `AgentSpawner` interface today\" is \"the largest abstraction gap in the codebase for the substrate swap\" (line 51) is correct and load-bearing for the recommended approach. The note that only three files import `redis` directly (line 45) is accurate and materially shrinks the bus-swap surface.\n\n**3. Options Analysis \u2014 strong.** Four genuinely distinct options:\n- A (abstraction-first parallel substrates + CI matrix) \u2014 recommended\n- B (delete k3s entirely)\n- C (skill-only; never touch the orchestrator)\n- D (in-process binding without named interfaces)\nEach option's pros/cons are specific and avoid hedging. Option C is explicitly flagged as contradicting the issue's North Star but listed for completeness, which is the correct discipline. Option D's failure mode (\"substrate boundary isn't visible in the code\") is the right concern to surface.\n\n**4. Constraints \u2014 strong.** Hard constraints (SendMessage gating with bug link to anthropics/claude-code#36196, subagent context windows referencing `max_turns: 1000` at `docs/guides/concurrent-execution.md:97`, HITL surface, concurrency ceiling, install footprint), architectural constraints (depth gap, BRC invariants, gateway-equivalent enforcement, file-write boundaries), conformance/proof obligation, and dependencies (marketplace packaging, harness-mode selector) are all enumerated with file pointers. Lines 90\u201391 correctly flag that some k3s-shaped tests (\"live-pod-guard on restart\") are k3s-specific and won't translate \u2014 the conformance set needs to be factored, not blindly re-run.\n\n**5. Open Questions \u2014 strong.** 11 decisions cover the structural surfaces (substrate coexistence, phase scope, conformance scoping, spawner interface shape, worktree ownership, policy enforcement seam, HITL surface, packaging, k3s deprecation, context-window strategy, slice-DAG shape) and 6 open-ended feedback questions cover the policy decisions that need human judgment (representative-issue selection, latency budgets, dependency constraints, non-Claude-Code callers, #2622 cause-#5/#6 scope, telemetry/privacy). The \"Pre-Refine Context\" section (lines 180\u2013189) explicitly captures what the operator already settled in the issue body and comment, which protects the plan phase from re-litigating settled framing. Every question is actionable: each is paired with 4\u20135 named options containing the substantive trade-offs in the option labels themselves, so the operator does not have to cross-reference back to the analysis to vote.\n\n**6. Recommendation Quality \u2014 strong.** Option A is justified by five concrete arguments tied to the issue body and the analysis findings: conformance proof is executable (not narrative), Claude Code constraints surface naturally as interface contracts, depth-gap closure becomes structural rather than rubric-based, child-issue absorption is verified rather than asserted, and migration safety is preserved. The closing line (line 174) correctly scopes the plan phase to \"slice-DAG shape\" rather than re-opening the framing.\n\n**7. HITL Registration \u2014 verified.** `mcp__sdlc__show_contract` returns 11 fully-populated decisions and a 6-question feedback record. Each cq-N decision in the draft corresponds to a `decisions[N-1]` entry on the contract with matching question text and option labels. `feedback.questions` Q1..Q6 match the draft's open-ended block at lines 312\u2013322. No prose-only open questions were left unregistered.\n\n### Non-blocking\n\n- **`.egg-state/contracts/issue-2623.json` is a stub copy on the work branch.** The on-disk JSON at HEAD shows `decisions: []` and `feedback: null`; only the gateway has the full state. This is expected (gateway is source of truth) but the divergence may surprise a future reader doing local-only inspection \u2014 a quick note in implementation phase to teach the local-snapshot writer to flush on registration would close the loop. Not a refine-phase issue.\n- **Feedback marker uses `` (line 306).** The review-criteria convention spells this ``. Functionally fine because the feedback record IS registered on the gateway and discoverable, but normalizing the marker in a later pass would prevent future grep-based tooling from missing it.\n- **cq-2 option 4 (\"Refine + plan + implement (no pr): \u2026 let pr-phase keep using k3s for now\") fights the North Star.** The issue body explicitly wants the full SDLC running without k3s. Listing this option for completeness is fine; the operator can reject it. Worth flagging because plan phase should not silently treat \"pr keeps using k3s\" as a low-risk fallback if the operator declines to choose it explicitly.\n- **cq-11 option 2 labels itself \"Two-wave parallel\" but enumerates three stages** ([interfaces+ADR] \u2192 [4 parallel implementations] \u2192 [conformance+entry-point]). Cosmetic; either the label or the stage count needs to drop a wave. cq-11 option 3 is correctly labeled \"Three-wave parallel.\"\n- **Option C in the options-analysis prose (line 141) says \"Effectively closes #2622 only, leaving #2612 and the parent's substrate-swap intent unresolved\"** \u2014 slight tension with the Pre-Refine Context section's claim that #2612 is largely absorbed by the parent-issue framing. Reads correctly in context (Option C *would* fail to absorb #2612 because it doesn't bring the real orchestrator in-process), but a one-line clarification (\"\u2026which is why this option contradicts the framing the operator already settled\") would help the operator weigh it.\n- **The substrate table in the issue body lists \"Persistent volume\" and \"kubectl get pods / health checks\" rows** that the draft summarizes but doesn't anchor to specific code paths (no equivalent of the `WORKTREE_BASE_DIR` and `egg_health` callouts the prose uses elsewhere). These are mentioned generically (lines 21\u201322 of the draft table) but not researched at the same depth as the spawner, bus, and gateway. Plan phase can fill these in; refine-phase scope is adequate without them.\n- **Pre-Refine Context section (line 187) phrases #2622 as \"largely absorbed; real `build_system_prompt` closes the depth gap structurally\"** \u2014 true for the four causes the analysis enumerates in Option A's pros, but the issue body explicitly carves out \"tactical shrinkers (depth-check reviewer criterion, parity-test harness) survive as cross-substrate validators.\" Question Q5 in the feedback block correctly opens this scope, but the Pre-Refine Context could note that the absorption isn't 100% to keep the plan phase from prematurely closing #2622.\n", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-18T22:38:08.776811+00:00", - "phase": "refine" - }, - { - "id": "a46239b0-0802-4d", - "pipeline_id": "issue-2623", - "from_role": "orchestrator", - "to_role": "refiner", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1 - }, - "timestamp": "2026-05-18T22:38:08.776870+00:00", - "phase": "refine" - }, - { - "id": "f40f5c2a-5a82-47", - "pipeline_id": "issue-2623", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-18T22:38:08.834370+00:00", - "phase": "refine" - }, - { - "id": "8cf1082b-56e3-46", - "pipeline_id": "issue-2623", - "from_role": "refiner", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by refiner", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-18T22:38:11.838278+00:00", - "phase": "refine" - }, - { - "id": "3f445157-9779-4c", - "pipeline_id": "issue-2623", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_refine", - "body": "", - "metadata": { - "consensus_reached": true - }, - "timestamp": "2026-05-18T22:38:12.814172+00:00", - "phase": "refine" - } -] \ No newline at end of file diff --git a/.egg-state/brc-history/2623-refine.md b/.egg-state/brc-history/2623-refine.md deleted file mode 100644 index f94f2c38b0..0000000000 --- a/.egg-state/brc-history/2623-refine.md +++ /dev/null @@ -1,494 +0,0 @@ -# BRC Consensus History — refine phase - -Generated: 2026-05-18T22:38:12Z -Pipeline: issue-2623 - -### [2026-05-18T22:26:54Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2d10ec48-b040-44 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:27:22Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 970eb82b-3bbe-4a -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:27:15.382011+00:00' -```` - -### [2026-05-18T22:27:50Z] overseer (OVERSEER_ALERT): agent-heartbeat-stall [high] - -Orchestrator unreachable for pipeline issue-2623; refine phase has not started - -Detail: -Pipeline issue-2623 is in 'refine' phase with no agent executions, no checkpoints, and no BRC history. The orchestrator HTTP endpoint (orchestrator.egg-system.svc.cluster.local:9849) is timing out on all calls: /api/v1/pipelines/issue-2623/status, BRC state queries, and progress queries. The contract gateway is reachable and confirms contract schemaVersion 1.1 with an empty slate. The overseer monitor script reports pipeline_unreachable and is running with default calibration thresholds. No work can be dispatched until the orchestrator recovers. - -Recommended action: -Check orchestrator pod health (kubectl get pods -n egg-system, kubectl logs -n egg-system deploy/egg-orchestrator). If the orchestrator pod is CrashLooping or not ready, restart it. If the network policy is blocking overseer→orchestrator traffic, patch the NetworkPolicy. Once orchestrator recovers, pipeline refine phase should self-start. - -````yaml -id: ffc0873b-004e-4a -phase: refine -```` - -### [2026-05-18T22:27:54Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e75e6b7b-9e89-4c -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:28:20Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_ON_ROLE - -Reviewer restarted by consensus wrapper; waiting for refiner CONSENSUS_PROPOSE before reviewing. - -````yaml -id: a37b45ec-609d-47 -phase: refine -metadata: - state: WAITING_ON_ROLE - waiting_on: refiner -```` - -### [2026-05-18T22:28:54Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: ed2d7095-1dc0-47 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:29:54Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: fe3bd29d-d3b6-41 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:30:54Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8e97a9d7-97e3-4b -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:31:26Z] overseer (HEARTBEAT): heartbeat: WORKING - -Overseer active: monitoring refine BRC cycle. Refiner running at 305s, reviewers healthy and waiting on CONSENSUS_PROPOSE. No stalls on pipeline agents. Calibration artifact alert (calibration_only=true) is suppressed. Advisor gate cleared. - -````yaml -id: ebab821c-e19d-44 -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-18T22:31:55Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 18f08d5f-c27e-4f -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:32:55Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 28f81880-88c4-46 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:33:55Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c3c8bc8b-673a-49 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:34:55Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: fd551e87-b63f-48 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:26:40.395698+00:00' -```` - -### [2026-05-18T22:35:32Z] refiner (CONSENSUS_PROPOSE): Proposal from refiner - -Drafted the refine-phase analysis for #2623 (substrate swap: run the full egg SDLC stack natively in Claude Code). The document characterises today's k3s/Redis/Docker/gateway substrate against the issue's substrate-swap table; traces the abstraction seams in the orchestrator (MessageStore already abstracted, KubernetesSpawner has no AgentSpawner interface yet, gateway policy in shared/egg_restrictions is portable, contracts/checkpoints already filesystem-native, refine-plan skill is a parallel implementation rather than the destination); evaluates four implementation options and recommends Option A (parallel substrates behind named AgentSpawner/MessageBus/PolicyEnforcer interfaces, env-var-selected, cross-substrate conformance CI matrix) because it makes the issue's "behavioral conformance on the same tests" definition-of-done structurally provable rather than narratively asserted, contains migration risk, and lets the depth-gap (#2622) close as a byproduct of routing through real build_system_prompt. Every uncertainty is registered: 11 multiple-choice decisions (cq-1..cq-11: substrate coexistence; phase scope; conformance suite scoping; spawner interface shape; worktree ownership; policy enforcement seam; HITL surface; install footprint; k3s deprecation; context-window strategy; slice-DAG shape) plus an open-ended feedback record (feedback-1) with six questions covering conformance-set selection, latency budgets, third-party-dep constraints, non-Claude-Code callers, #2622 absorption appetite, and telemetry/privacy regressions. Issue body's framing-update content (children dispositions, substrate-swap intent) is noted as pre-resolved in the analysis to keep the plan phase from re-litigating them. - -````yaml -id: 2e92f1a5-8b3f-43 -phase: refine -metadata: - payload: - summary: 'Drafted the refine-phase analysis for #2623 (substrate swap: run the - full egg SDLC stack natively in Claude Code). The document characterises today''s - k3s/Redis/Docker/gateway substrate against the issue''s substrate-swap table; - traces the abstraction seams in the orchestrator (MessageStore already abstracted, - KubernetesSpawner has no AgentSpawner interface yet, gateway policy in shared/egg_restrictions - is portable, contracts/checkpoints already filesystem-native, refine-plan skill - is a parallel implementation rather than the destination); evaluates four implementation - options and recommends Option A (parallel substrates behind named AgentSpawner/MessageBus/PolicyEnforcer - interfaces, env-var-selected, cross-substrate conformance CI matrix) because - it makes the issue''s "behavioral conformance on the same tests" definition-of-done - structurally provable rather than narratively asserted, contains migration risk, - and lets the depth-gap (#2622) close as a byproduct of routing through real - build_system_prompt. Every uncertainty is registered: 11 multiple-choice decisions - (cq-1..cq-11: substrate coexistence; phase scope; conformance suite scoping; - spawner interface shape; worktree ownership; policy enforcement seam; HITL surface; - install footprint; k3s deprecation; context-window strategy; slice-DAG shape) - plus an open-ended feedback record (feedback-1) with six questions covering - conformance-set selection, latency budgets, third-party-dep constraints, non-Claude-Code - callers, #2622 absorption appetite, and telemetry/privacy regressions. Issue - body''s framing-update content (children dispositions, substrate-swap intent) - is noted as pre-resolved in the analysis to keep the plan phase from re-litigating - them.' - attestation: {} - artifacts: - - .egg-state/drafts/2623-analysis.md - risk_considered: '' - commit_sha: 82e04b84c - files_changed: - - .egg-state/drafts/2623-analysis.md - tests_run: [] - tasks_satisfied: [] - version: 1 - commit_sha: 82e04b84c -```` - -### [2026-05-18T22:35:32Z] reviewer_refine (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a8d78eac-804c-4f -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-18T22:36:00Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ee5686d9-2e14-41 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:35:37.571687+00:00' -```` - -### [2026-05-18T22:36:32Z] reviewer_agent_design → refiner (CONSENSUS_ACK): ACK from reviewer_agent_design for refiner - -Refine artifact covers the agent-design seams thoroughly. The 11 HITL decisions surface every architectural axis the substrate swap touches — spawner interface shape (cq-4), worktree ownership (cq-5), policy enforcement seam (cq-6), HITL surface (cq-7), and subagent context strategy (cq-10) are the load-bearing agent-design decisions and each option is articulated with the right trade-offs (sync vs async vs stream-shaped, native worktree vs egg's WORKTREE_BASE_DIR, hook vs MCP vs prompt enforcement, etc.). The 6 open-ended feedback items appropriately probe acceptance criteria (conformance-issue selection, latency budgets, dep constraints, non-Claude-Code drivers, cross-issue scope with #2622, telemetry/privacy). The decision options are framed so the operator can pick without writing free-form text. No agent-design gaps identified. - -````yaml -id: 5eb4b9b1-2bbc-40 -phase: refine -metadata: - payload: - artifact_references: - - .egg-state/contract-issue-2623.json - reason: "Refine artifact covers the agent-design seams thoroughly. The 11 HITL\ - \ decisions surface every architectural axis the substrate swap touches \u2014\ - \ spawner interface shape (cq-4), worktree ownership (cq-5), policy enforcement\ - \ seam (cq-6), HITL surface (cq-7), and subagent context strategy (cq-10) are\ - \ the load-bearing agent-design decisions and each option is articulated with\ - \ the right trade-offs (sync vs async vs stream-shaped, native worktree vs egg's\ - \ WORKTREE_BASE_DIR, hook vs MCP vs prompt enforcement, etc.). The 6 open-ended\ - \ feedback items appropriately probe acceptance criteria (conformance-issue\ - \ selection, latency budgets, dep constraints, non-Claude-Code drivers, cross-issue\ - \ scope with #2622, telemetry/privacy). The decision options are framed so the\ - \ operator can pick without writing free-form text. No agent-design gaps identified." - ack_version: 1 - version: 1 -```` - -### [2026-05-18T22:36:32Z] refiner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d15b2724-a1d4-44 -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-18T22:36:36Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: dca25e4d-e669-47 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:36:36.351878+00:00' -```` - -### [2026-05-18T22:36:41Z] reviewer_agent_design (CONSENSUS_CONFIRMED): Confirmed by reviewer_agent_design - -````yaml -id: 095de55a-bdbd-4e -phase: refine -metadata: - consensus_reached: false -```` - -### [2026-05-18T22:37:36Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 9b326f70-eb6a-45 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-18T22:36:36.351878+00:00' -```` - -### [2026-05-18T22:38:08Z] reviewer_refine → refiner (CONSENSUS_ACK): ACK from reviewer_refine for refiner - - -Reviewed `.egg-state/drafts/2623-analysis.md` (326 lines) end-to-end and cross-checked HITL registration via `mcp__sdlc__show_contract`. All 11 decisions (cq-1..cq-11) and the 6-question feedback record (feedback-1, Q1..Q6) are populated on the canonical contract with full option text. Approving. - -### Section-by-section evaluation - -**1. Problem Understanding — strong.** The draft correctly reframes the issue from "approximate k3s pipeline quality with a Markdown skill" to "swap the substrate underneath the real stack" (lines 7–11) and faithfully reproduces the issue body's substrate-swap table (lines 13–22). The four desired outcomes (lines 28–31) map 1:1 onto the issue's definition-of-done plus the implicit "user can run egg locally" goal. The placement of PR #2608 / the existing refine-plan skill as "starting substrate, not destination" (lines 65–69) is the right framing — it prevents the plan phase from drifting back toward parity-chasing. - -**2. Research Quality — strong.** Evidence is anchored to specific files and call sites: `orchestrator/cli.py::cmd_serve()` at lines 83–150, `KubernetesSpawner.spawn_agent_job()`, `concurrent_executor.py::_spawn_agent()`, `orchestrator/message_store.py`'s `MessageStore` abstraction vs. `redis_message_store.py`, the 36-line `container_spawner.py` back-compat shim, `gateway/agent_restrictions.py` as a pure-Python module, `shared/egg_harness/prompt.py::build_system_prompt(sources)`, `orchestrator/action_guards.py::validate_invariants()` for INV-1..5, and `shared/egg_restrictions/patterns.py` for file-write boundaries. The observation that "no `AgentSpawner` interface today" is "the largest abstraction gap in the codebase for the substrate swap" (line 51) is correct and load-bearing for the recommended approach. The note that only three files import `redis` directly (line 45) is accurate and materially shrinks the bus-swap surface. - -**3. Options Analysis — strong.** Four genuinely distinct options: -- A (abstraction-first parallel substrates + CI matrix) — recommended -- B (delete k3s entirely) -- C (skill-only; never touch the orchestrator) -- D (in-process binding without named interfaces) -Each option's pros/cons are specific and avoid hedging. Option C is explicitly flagged as contradicting the issue's North Star but listed for completeness, which is the correct discipline. Option D's failure mode ("substrate boundary isn't visible in the code") is the right concern to surface. - -**4. Constraints — strong.** Hard constraints (SendMessage gating with bug link to anthropics/claude-code#36196, subagent context windows referencing `max_turns: 1000` at `docs/guides/concurrent-execution.md:97`, HITL surface, concurrency ceiling, install footprint), architectural constraints (depth gap, BRC invariants, gateway-equivalent enforcement, file-write boundaries), conformance/proof obligation, and dependencies (marketplace packaging, harness-mode selector) are all enumerated with file pointers. Lines 90–91 correctly flag that some k3s-shaped tests ("live-pod-guard on restart") are k3s-specific and won't translate — the conformance set needs to be factored, not blindly re-run. - -**5. Open Questions — strong.** 11 decisions cover the structural surfaces (substrate coexistence, phase scope, conformance scoping, spawner interface shape, worktree ownership, policy enforcement seam, HITL surface, packaging, k3s deprecation, context-window strategy, slice-DAG shape) and 6 open-ended feedback questions cover the policy decisions that need human judgment (representative-issue selection, latency budgets, dependency constraints, non-Claude-Code callers, #2622 cause-#5/#6 scope, telemetry/privacy). The "Pre-Refine Context" section (lines 180–189) explicitly captures what the operator already settled in the issue body and comment, which protects the plan phase from re-litigating settled framing. Every question is actionable: each is paired with 4–5 named options containing the substantive trade-offs in the option labels themselves, so the operator does not have to cross-reference back to the analysis to vote. - -**6. Recommendation Quality — strong.** Option A is justified by five concrete arguments tied to the issue body and the analysis findings: conformance proof is executable (not narrative), Claude Code constraints surface naturally as interface contracts, depth-gap closure becomes structural rather than rubric-based, child-issue absorption is verified rather than asserted, and migration safety is preserved. The closing line (line 174) correctly scopes the plan phase to "slice-DAG shape" rather than re-opening the framing. - -**7. HITL Registration — verified.** `mcp__sdlc__show_contract` returns 11 fully-populated decisions and a 6-question feedback record. Each cq-N decision in the draft corresponds to a `decisions[N-1]` entry on the contract with matching question text and option labels. `feedback.questions` Q1..Q6 match the draft's open-ended block at lines 312–322. No prose-only open questions were left unregistered. - -### Non-blocking - -- **`.egg-state/contracts/issue-2623.json` is a stub copy on the work branch.** The on-disk JSON at HEAD shows `decisions: []` and `feedback: null`; only the gateway has the full state. This is expected (gateway is source of truth) but the divergence may surprise a future reader doing local-only inspection — a quick note in implementation phase to teach the local-snapshot writer to flush on registration would close the loop. Not a refine-phase issue. -- **Feedback marker uses `` (line 306).** The review-criteria convention spells this ``. Functionally fine because the feedback record IS registered on the gateway and discoverable, but normalizing the marker in a later pass would prevent future grep-based tooling from missing it. -- **cq-2 option 4 ("Refine + plan + implement (no pr): … let pr-phase keep using k3s for now") fights the North Star.** The issue body explicitly wants the full SDLC running without k3s. Listing this option for completeness is fine; the operator can reject it. Worth flagging because plan phase should not silently treat "pr keeps using k3s" as a low-risk fallback if the operator declines to choose it explicitly. -- **cq-11 option 2 labels itself "Two-wave parallel" but enumerates three stages** ([interfaces+ADR] → [4 parallel implementations] → [conformance+entry-point]). Cosmetic; either the label or the stage count needs to drop a wave. cq-11 option 3 is correctly labeled "Three-wave parallel." -- **Option C in the options-analysis prose (line 141) says "Effectively closes #2622 only, leaving #2612 and the parent's substrate-swap intent unresolved"** — slight tension with the Pre-Refine Context section's claim that #2612 is largely absorbed by the parent-issue framing. Reads correctly in context (Option C *would* fail to absorb #2612 because it doesn't bring the real orchestrator in-process), but a one-line clarification ("…which is why this option contradicts the framing the operator already settled") would help the operator weigh it. -- **The substrate table in the issue body lists "Persistent volume" and "kubectl get pods / health checks" rows** that the draft summarizes but doesn't anchor to specific code paths (no equivalent of the `WORKTREE_BASE_DIR` and `egg_health` callouts the prose uses elsewhere). These are mentioned generically (lines 21–22 of the draft table) but not researched at the same depth as the spawner, bus, and gateway. Plan phase can fill these in; refine-phase scope is adequate without them. -- **Pre-Refine Context section (line 187) phrases #2622 as "largely absorbed; real `build_system_prompt` closes the depth gap structurally"** — true for the four causes the analysis enumerates in Option A's pros, but the issue body explicitly carves out "tactical shrinkers (depth-check reviewer criterion, parity-test harness) survive as cross-substrate validators." Question Q5 in the feedback block correctly opens this scope, but the Pre-Refine Context could note that the absorption isn't 100% to keep the plan phase from prematurely closing #2622. - - -````yaml -id: 2e00e6d7-4043-4a -phase: refine -metadata: - payload: - artifact_references: - - .egg-state/drafts/2623-analysis.md - - .egg-state/contracts/issue-2623.json - reason: "\nReviewed `.egg-state/drafts/2623-analysis.md` (326 lines) end-to-end\ - \ and cross-checked HITL registration via `mcp__sdlc__show_contract`. All 11\ - \ decisions (cq-1..cq-11) and the 6-question feedback record (feedback-1, Q1..Q6)\ - \ are populated on the canonical contract with full option text. Approving.\n\ - \n### Section-by-section evaluation\n\n**1. Problem Understanding \u2014 strong.**\ - \ The draft correctly reframes the issue from \"approximate k3s pipeline quality\ - \ with a Markdown skill\" to \"swap the substrate underneath the real stack\"\ - \ (lines 7\u201311) and faithfully reproduces the issue body's substrate-swap\ - \ table (lines 13\u201322). The four desired outcomes (lines 28\u201331) map\ - \ 1:1 onto the issue's definition-of-done plus the implicit \"user can run egg\ - \ locally\" goal. The placement of PR #2608 / the existing refine-plan skill\ - \ as \"starting substrate, not destination\" (lines 65\u201369) is the right\ - \ framing \u2014 it prevents the plan phase from drifting back toward parity-chasing.\n\ - \n**2. Research Quality \u2014 strong.** Evidence is anchored to specific files\ - \ and call sites: `orchestrator/cli.py::cmd_serve()` at lines 83\u2013150, `KubernetesSpawner.spawn_agent_job()`,\ - \ `concurrent_executor.py::_spawn_agent()`, `orchestrator/message_store.py`'s\ - \ `MessageStore` abstraction vs. `redis_message_store.py`, the 36-line `container_spawner.py`\ - \ back-compat shim, `gateway/agent_restrictions.py` as a pure-Python module,\ - \ `shared/egg_harness/prompt.py::build_system_prompt(sources)`, `orchestrator/action_guards.py::validate_invariants()`\ - \ for INV-1..5, and `shared/egg_restrictions/patterns.py` for file-write boundaries.\ - \ The observation that \"no `AgentSpawner` interface today\" is \"the largest\ - \ abstraction gap in the codebase for the substrate swap\" (line 51) is correct\ - \ and load-bearing for the recommended approach. The note that only three files\ - \ import `redis` directly (line 45) is accurate and materially shrinks the bus-swap\ - \ surface.\n\n**3. Options Analysis \u2014 strong.** Four genuinely distinct\ - \ options:\n- A (abstraction-first parallel substrates + CI matrix) \u2014 recommended\n\ - - B (delete k3s entirely)\n- C (skill-only; never touch the orchestrator)\n\ - - D (in-process binding without named interfaces)\nEach option's pros/cons are\ - \ specific and avoid hedging. Option C is explicitly flagged as contradicting\ - \ the issue's North Star but listed for completeness, which is the correct discipline.\ - \ Option D's failure mode (\"substrate boundary isn't visible in the code\"\ - ) is the right concern to surface.\n\n**4. Constraints \u2014 strong.** Hard\ - \ constraints (SendMessage gating with bug link to anthropics/claude-code#36196,\ - \ subagent context windows referencing `max_turns: 1000` at `docs/guides/concurrent-execution.md:97`,\ - \ HITL surface, concurrency ceiling, install footprint), architectural constraints\ - \ (depth gap, BRC invariants, gateway-equivalent enforcement, file-write boundaries),\ - \ conformance/proof obligation, and dependencies (marketplace packaging, harness-mode\ - \ selector) are all enumerated with file pointers. Lines 90\u201391 correctly\ - \ flag that some k3s-shaped tests (\"live-pod-guard on restart\") are k3s-specific\ - \ and won't translate \u2014 the conformance set needs to be factored, not blindly\ - \ re-run.\n\n**5. Open Questions \u2014 strong.** 11 decisions cover the structural\ - \ surfaces (substrate coexistence, phase scope, conformance scoping, spawner\ - \ interface shape, worktree ownership, policy enforcement seam, HITL surface,\ - \ packaging, k3s deprecation, context-window strategy, slice-DAG shape) and\ - \ 6 open-ended feedback questions cover the policy decisions that need human\ - \ judgment (representative-issue selection, latency budgets, dependency constraints,\ - \ non-Claude-Code callers, #2622 cause-#5/#6 scope, telemetry/privacy). The\ - \ \"Pre-Refine Context\" section (lines 180\u2013189) explicitly captures what\ - \ the operator already settled in the issue body and comment, which protects\ - \ the plan phase from re-litigating settled framing. Every question is actionable:\ - \ each is paired with 4\u20135 named options containing the substantive trade-offs\ - \ in the option labels themselves, so the operator does not have to cross-reference\ - \ back to the analysis to vote.\n\n**6. Recommendation Quality \u2014 strong.**\ - \ Option A is justified by five concrete arguments tied to the issue body and\ - \ the analysis findings: conformance proof is executable (not narrative), Claude\ - \ Code constraints surface naturally as interface contracts, depth-gap closure\ - \ becomes structural rather than rubric-based, child-issue absorption is verified\ - \ rather than asserted, and migration safety is preserved. The closing line\ - \ (line 174) correctly scopes the plan phase to \"slice-DAG shape\" rather than\ - \ re-opening the framing.\n\n**7. HITL Registration \u2014 verified.** `mcp__sdlc__show_contract`\ - \ returns 11 fully-populated decisions and a 6-question feedback record. Each\ - \ cq-N decision in the draft corresponds to a `decisions[N-1]` entry on the\ - \ contract with matching question text and option labels. `feedback.questions`\ - \ Q1..Q6 match the draft's open-ended block at lines 312\u2013322. No prose-only\ - \ open questions were left unregistered.\n\n### Non-blocking\n\n- **`.egg-state/contracts/issue-2623.json`\ - \ is a stub copy on the work branch.** The on-disk JSON at HEAD shows `decisions:\ - \ []` and `feedback: null`; only the gateway has the full state. This is expected\ - \ (gateway is source of truth) but the divergence may surprise a future reader\ - \ doing local-only inspection \u2014 a quick note in implementation phase to\ - \ teach the local-snapshot writer to flush on registration would close the loop.\ - \ Not a refine-phase issue.\n- **Feedback marker uses `` (line 306).** The review-criteria convention spells this ``. Functionally fine because the feedback record IS registered on the\ - \ gateway and discoverable, but normalizing the marker in a later pass would\ - \ prevent future grep-based tooling from missing it.\n- **cq-2 option 4 (\"\ - Refine + plan + implement (no pr): \u2026 let pr-phase keep using k3s for now\"\ - ) fights the North Star.** The issue body explicitly wants the full SDLC running\ - \ without k3s. Listing this option for completeness is fine; the operator can\ - \ reject it. Worth flagging because plan phase should not silently treat \"\ - pr keeps using k3s\" as a low-risk fallback if the operator declines to choose\ - \ it explicitly.\n- **cq-11 option 2 labels itself \"Two-wave parallel\" but\ - \ enumerates three stages** ([interfaces+ADR] \u2192 [4 parallel implementations]\ - \ \u2192 [conformance+entry-point]). Cosmetic; either the label or the stage\ - \ count needs to drop a wave. cq-11 option 3 is correctly labeled \"Three-wave\ - \ parallel.\"\n- **Option C in the options-analysis prose (line 141) says \"\ - Effectively closes #2622 only, leaving #2612 and the parent's substrate-swap\ - \ intent unresolved\"** \u2014 slight tension with the Pre-Refine Context section's\ - \ claim that #2612 is largely absorbed by the parent-issue framing. Reads correctly\ - \ in context (Option C *would* fail to absorb #2612 because it doesn't bring\ - \ the real orchestrator in-process), but a one-line clarification (\"\u2026\ - which is why this option contradicts the framing the operator already settled\"\ - ) would help the operator weigh it.\n- **The substrate table in the issue body\ - \ lists \"Persistent volume\" and \"kubectl get pods / health checks\" rows**\ - \ that the draft summarizes but doesn't anchor to specific code paths (no equivalent\ - \ of the `WORKTREE_BASE_DIR` and `egg_health` callouts the prose uses elsewhere).\ - \ These are mentioned generically (lines 21\u201322 of the draft table) but\ - \ not researched at the same depth as the spawner, bus, and gateway. Plan phase\ - \ can fill these in; refine-phase scope is adequate without them.\n- **Pre-Refine\ - \ Context section (line 187) phrases #2622 as \"largely absorbed; real `build_system_prompt`\ - \ closes the depth gap structurally\"** \u2014 true for the four causes the\ - \ analysis enumerates in Option A's pros, but the issue body explicitly carves\ - \ out \"tactical shrinkers (depth-check reviewer criterion, parity-test harness)\ - \ survive as cross-substrate validators.\" Question Q5 in the feedback block\ - \ correctly opens this scope, but the Pre-Refine Context could note that the\ - \ absorption isn't 100% to keep the plan phase from prematurely closing #2622.\n" - ack_version: 1 - version: 1 -```` - -### [2026-05-18T22:38:08Z] orchestrator → refiner (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: a46239b0-0802-4d -phase: refine -metadata: - ready_to_confirm: true - version: 1 -```` - -### [2026-05-18T22:38:08Z] refiner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f40f5c2a-5a82-47 -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-18T22:38:11Z] refiner (CONSENSUS_CONFIRMED): Confirmed by refiner - -````yaml -id: 8cf1082b-56e3-46 -phase: refine -metadata: - consensus_reached: false -```` - -### [2026-05-18T22:38:12Z] reviewer_refine (CONSENSUS_CONFIRMED): Confirmed by reviewer_refine - -````yaml -id: 3f445157-9779-4c -phase: refine -metadata: - consensus_reached: true -```` diff --git a/.egg-state/brc-history/2717-implement-slice-1.json b/.egg-state/brc-history/2717-implement-slice-1.json deleted file mode 100644 index 5828849f55..0000000000 --- a/.egg-state/brc-history/2717-implement-slice-1.json +++ /dev/null @@ -1,7022 +0,0 @@ -[ - { - "id": "8ee719ed-15c5-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:39.347242+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:45:46.176696+00:00", - "phase": "implement" - }, - { - "id": "aad4e217-17c2-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:45:46.202840+00:00", - "phase": "implement" - }, - { - "id": "25d509b8-0f4c-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:46:46.681747+00:00", - "phase": "implement" - }, - { - "id": "1ee6d089-7024-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:39.347242+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:46:46.685896+00:00", - "phase": "implement" - }, - { - "id": "724c6344-9ff3-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:46:46.686462+00:00", - "phase": "implement" - }, - { - "id": "3f914a9d-3916-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:46:46.687114+00:00", - "phase": "implement" - }, - { - "id": "f8c1ca72-7448-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:47:25.653188+00:00", - "phase": "implement" - }, - { - "id": "8d1a2a9a-c024-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:12.376437+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:47:25.689349+00:00", - "phase": "implement" - }, - { - "id": "1a2cb803-31e4-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:47:37.178972+00:00", - "phase": "implement" - }, - { - "id": "704666c6-107d-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:47:46.806945+00:00", - "phase": "implement" - }, - { - "id": "90f511c6-6ec8-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:47:46.817220+00:00", - "phase": "implement" - }, - { - "id": "c14ce1df-746c-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:48:25.365640+00:00", - "phase": "implement" - }, - { - "id": "dbc53669-3874-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:12.376437+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:48:25.751604+00:00", - "phase": "implement" - }, - { - "id": "b810c5b4-5e66-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:48:37.270642+00:00", - "phase": "implement" - }, - { - "id": "7d058934-1245-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:48:46.902671+00:00", - "phase": "implement" - }, - { - "id": "1a5c733a-7af9-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:48:46.917837+00:00", - "phase": "implement" - }, - { - "id": "40176315-999c-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:49:25.427853+00:00", - "phase": "implement" - }, - { - "id": "75f24eaf-92e2-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:12.376437+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:49:25.807272+00:00", - "phase": "implement" - }, - { - "id": "6f797313-d32a-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:49:37.348144+00:00", - "phase": "implement" - }, - { - "id": "cf8ee2ec-ed39-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:49:47.007723+00:00", - "phase": "implement" - }, - { - "id": "34bdb5b2-0bfb-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:49:47.035725+00:00", - "phase": "implement" - }, - { - "id": "4757899d-b940-42", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "Scaffolding three tester test files (test_rubric_loader.py, test_bridge_flattened_round_trip.py, test_pretooluse_hook_nested.py) while waiting on coder's CONSENSUS_PROPOSE", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:49:49.288181+00:00", - "phase": "implement" - }, - { - "id": "62d9e767-804a-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:50:24.156617+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:50:24.206399+00:00", - "phase": "implement" - }, - { - "id": "7e286a99-fce9-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:50:25.456925+00:00", - "phase": "implement" - }, - { - "id": "3e5225be-94b2-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:50:37.391788+00:00", - "phase": "implement" - }, - { - "id": "651b9866-22cf-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:50:47.092458+00:00", - "phase": "implement" - }, - { - "id": "a42456c2-4503-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:50:47.119668+00:00", - "phase": "implement" - }, - { - "id": "829d83f5-31ed-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:50:24.156617+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:51:24.306363+00:00", - "phase": "implement" - }, - { - "id": "b6d5f3ea-c1cf-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:51:25.527426+00:00", - "phase": "implement" - }, - { - "id": "fdcf4ea8-c40b-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:51:37.447290+00:00", - "phase": "implement" - }, - { - "id": "008b6076-8754-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:51:47.165949+00:00", - "phase": "implement" - }, - { - "id": "dda17458-c129-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:51:47.233924+00:00", - "phase": "implement" - }, - { - "id": "74ebecc1-c1b1-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:50:24.156617+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:52:24.414858+00:00", - "phase": "implement" - }, - { - "id": "12e6ad93-4a25-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:52:25.586321+00:00", - "phase": "implement" - }, - { - "id": "cf2a2430-07e8-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:52:37.688069+00:00", - "phase": "implement" - }, - { - "id": "0d43a2b7-7ad0-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:52:47.218384+00:00", - "phase": "implement" - }, - { - "id": "c76502b2-bab5-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:52:47.289722+00:00", - "phase": "implement" - }, - { - "id": "968a2b9e-f1ca-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:50:24.156617+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:53:24.496586+00:00", - "phase": "implement" - }, - { - "id": "63ba3813-cdc8-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:53:25.631968+00:00", - "phase": "implement" - }, - { - "id": "1ed59bc5-0600-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:53:37.745060+00:00", - "phase": "implement" - }, - { - "id": "e5df3e05-e1ca-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:53:47.439007+00:00", - "phase": "implement" - }, - { - "id": "f085a577-14b1-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:53:47.444967+00:00", - "phase": "implement" - }, - { - "id": "1f7180ad-af19-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:50:24.156617+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:54:24.601335+00:00", - "phase": "implement" - }, - { - "id": "d2120111-4dc5-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:54:25.718432+00:00", - "phase": "implement" - }, - { - "id": "124385b1-d0b6-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:54:37.811132+00:00", - "phase": "implement" - }, - { - "id": "8bdcf047-4c4f-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:54:47.480422+00:00", - "phase": "implement" - }, - { - "id": "2aa5d4a1-7468-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:54:47.487312+00:00", - "phase": "implement" - }, - { - "id": "e022449f-7cbd-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:50:24.156617+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:55:24.653047+00:00", - "phase": "implement" - }, - { - "id": "469df723-0da6-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:55:25.770699+00:00", - "phase": "implement" - }, - { - "id": "0ab7afa8-5258-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:55:58.475601+00:00", - "phase": "implement" - }, - { - "id": "56e05919-9163-4c", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:55:46.419132+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:55:58.513813+00:00", - "phase": "implement" - }, - { - "id": "e4ed5abe-c070-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:55:58.514881+00:00", - "phase": "implement" - }, - { - "id": "9af9587e-2758-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:55:58.515213+00:00", - "phase": "implement" - }, - { - "id": "9f05b628-d016-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:50:24.156617+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:56:24.686958+00:00", - "phase": "implement" - }, - { - "id": "9bfe42b6-844a-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:47:10.300331+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:56:25.808038+00:00", - "phase": "implement" - }, - { - "id": "427f66bd-eff0-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:22.097972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:56:59.402610+00:00", - "phase": "implement" - }, - { - "id": "42721b47-d78e-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:55:46.419132+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:56:59.452829+00:00", - "phase": "implement" - }, - { - "id": "c8556b5d-8ebc-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:45:41.280300+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:56:59.455291+00:00", - "phase": "implement" - }, - { - "id": "7723eaf3-b5e3-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:46:40.861835+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:56:59.455782+00:00", - "phase": "implement" - }, - { - "id": "3cf81114-684e-47", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from coder", - "body": "slice-1 coder: bridge driver (TASK-1-1), R2 nested-dispatch fake (TASK-1-9), and rubric loader expansion (TASK-1-6). The bridge driver `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` is the cq-1 Option C flattened refine/plan path \u2014 it advances `run_pipeline_in_process` to its next yield, serialises the yielded HITLDecision into `.egg-state/contracts/.json#pending_hitl` under a stable schema (version, decision, answer, answer_log, status, result, error), and exits; cross-process state is recovered by replaying answer_log per invocation. Slice-3's daemon variant (TASK-3-2) consumes the same envelope schema (risk_analyst R17 mitigation). The `_agent_tool_fake.py` is test-only infrastructure (import-guarded against production use) that simulates nested Agent-tool dispatch by spawning a child subprocess with controlled EGG_AGENT_ROLE which then invokes `hook_entry.decide(...)` \u2014 answers the hook-logic half of R2. Loader update introduces `_ROLE_RUBRIC_SLICES` mapping so reviewer_refine / reviewer_agent_design load when the documenter's rubric .md files land (TASK-1-4), plan/implement roles raise the new \"deferred to follow-up slice-N\" structured error per the acceptance criterion. End-to-end smoke confirmed the driver round-trips an answer through the contract and advances to the next yield. Lint+format clean (ruff); mypy clean on new files (6 pre-existing substrate/__init__.py errors on lines 180-198 unrelated). All 52 existing claude_code substrate tests still pass.", - "metadata": { - "payload": { - "summary": "slice-1 coder: bridge driver (TASK-1-1), R2 nested-dispatch fake (TASK-1-9), and rubric loader expansion (TASK-1-6). The bridge driver `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` is the cq-1 Option C flattened refine/plan path \u2014 it advances `run_pipeline_in_process` to its next yield, serialises the yielded HITLDecision into `.egg-state/contracts/.json#pending_hitl` under a stable schema (version, decision, answer, answer_log, status, result, error), and exits; cross-process state is recovered by replaying answer_log per invocation. Slice-3's daemon variant (TASK-3-2) consumes the same envelope schema (risk_analyst R17 mitigation). The `_agent_tool_fake.py` is test-only infrastructure (import-guarded against production use) that simulates nested Agent-tool dispatch by spawning a child subprocess with controlled EGG_AGENT_ROLE which then invokes `hook_entry.decide(...)` \u2014 answers the hook-logic half of R2. Loader update introduces `_ROLE_RUBRIC_SLICES` mapping so reviewer_refine / reviewer_agent_design load when the documenter's rubric .md files land (TASK-1-4), plan/implement roles raise the new \"deferred to follow-up slice-N\" structured error per the acceptance criterion. End-to-end smoke confirmed the driver round-trips an answer through the contract and advances to the next yield. Lint+format clean (ruff); mypy clean on new files (6 pre-existing substrate/__init__.py errors on lines 180-198 unrelated). All 52 existing claude_code substrate tests still pass.", - "attestation": {}, - "artifacts": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "risk_considered": "R17 dual-bridge schema risk mitigated by exposing PENDING_HITL_SCHEMA_VERSION and documenting the envelope as a stable contract. R2 hook-logic question answered by the test fake (slice-1 tester's TASK-1-5 will assert the deny verdict). Loader fence preserves the structured-error contract for unsupported roles. Driver `_advance_generator` always closes the generator in a finally block so background threads join cleanly via GeneratorExit. Mypy regression risk: my changes don't introduce new errors \u2014 the 6 pre-existing errors in substrate/__init__.py:180-198 are baseline.", - "commit_sha": "fddeb82bf46b3acbf65a20283469690dcb2af481", - "files_changed": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "tests_run": [], - "tasks_satisfied": [ - "task-1-1", - "task-1-9", - "task-1-6" - ] - }, - "version": 1, - "commit_sha": "fddeb82bf46b3acbf65a20283469690dcb2af481", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:24.671244+00:00", - "phase": "implement" - }, - { - "id": "09580a4d-5dff-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:24.838309+00:00", - "phase": "implement" - }, - { - "id": "6b5f0096-c1a1-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:24.885870+00:00", - "phase": "implement" - }, - { - "id": "58a3c176-ddda-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:24.901500+00:00", - "phase": "implement" - }, - { - "id": "c62f7fa2-64a9-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:24.909512+00:00", - "phase": "implement" - }, - { - "id": "4123058c-af69-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:24.919300+00:00", - "phase": "implement" - }, - { - "id": "48f228e5-10c6-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:24.923968+00:00", - "phase": "implement" - }, - { - "id": "e86647cf-75b8-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:57:37.143439+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:57:52.185448+00:00", - "phase": "implement" - }, - { - "id": "24bbacba-c6a2-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:57:37.143439+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:59:07.508576+00:00", - "phase": "implement" - }, - { - "id": "7ae96e4b-cc68-47", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from documenter", - "body": "Slice 1 of #2717 documenter half: refine-team rubrics + flattened-bridge docs + ADR rollout-deltas tracker. Adds two new reviewer rubric files (reviewer_refine.md, reviewer_agent_design.md) under plugins/egg-sdlc/skills/egg-sdlc/agents/ mirroring the layout of plugins/refine-plan/skills/refine-plan/agents/ (TASK-1-4). The filenames match the coder's loader expectation (orchestrator/substrate/__init__.py:330 `{role_name}.md`); the underscore form lines up with the AgentRole identifiers and the loader's `_RUBRIC_LANDED_ROLES` frozenset. Updates plugins/egg-sdlc/skills/egg-sdlc/SKILL.md to replace the \"Walking-skeleton bridge gap\" callout with a \"How the flattened bridge works\" section naming pending_hitl as the single-yield carrier and documenting the skill\u2192bin/run_pipeline.py loop; updates the R2 PreToolUse-hook section to point at the new slice-1 test infrastructure (integration_tests/regression/test_pretooluse_hook_nested.py + _agent_tool_fake.py); reframes \"What's NOT in this skill\" against the slice-2..5 rollout map (TASK-1-2). Updates docs/architecture/claude-code-substrate.md: status banner reframes from spike to spike\u2192rollout, cq-2/cq-7/cq-11 rows reflect slice-1 deltas, the in-process orchestrator section grows 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 (R17 mitigation), R2 / R15 subsections reflect the slice-1 worked example and the slice-5 contingent fallback (cq-6 option 2 + R15 model (b)), the unified \"Rollout deltas\" section split into Completed-in-this-rollout (3 [x] items for slice 1) and Pending-in-this-rollout (9 items mapped to slices 2-5), primitives + conformance-proof tables pick up the new slice-1 modules (TASK-1-8). Files reviewed: all four touched files plus the plan draft (.egg-state/drafts/2717-plan.md slice-1 task definitions), the coder's orchestrator/substrate/__init__.py loader changes (confirmed underscore filenames match _RUBRIC_LANDED_ROLES), and the two existing refine-plan rubrics that the new substrate rubrics mirror.", - "metadata": { - "payload": { - "summary": "Slice 1 of #2717 documenter half: refine-team rubrics + flattened-bridge docs + ADR rollout-deltas tracker. Adds two new reviewer rubric files (reviewer_refine.md, reviewer_agent_design.md) under plugins/egg-sdlc/skills/egg-sdlc/agents/ mirroring the layout of plugins/refine-plan/skills/refine-plan/agents/ (TASK-1-4). The filenames match the coder's loader expectation (orchestrator/substrate/__init__.py:330 `{role_name}.md`); the underscore form lines up with the AgentRole identifiers and the loader's `_RUBRIC_LANDED_ROLES` frozenset. Updates plugins/egg-sdlc/skills/egg-sdlc/SKILL.md to replace the \"Walking-skeleton bridge gap\" callout with a \"How the flattened bridge works\" section naming pending_hitl as the single-yield carrier and documenting the skill\u2192bin/run_pipeline.py loop; updates the R2 PreToolUse-hook section to point at the new slice-1 test infrastructure (integration_tests/regression/test_pretooluse_hook_nested.py + _agent_tool_fake.py); reframes \"What's NOT in this skill\" against the slice-2..5 rollout map (TASK-1-2). Updates docs/architecture/claude-code-substrate.md: status banner reframes from spike to spike\u2192rollout, cq-2/cq-7/cq-11 rows reflect slice-1 deltas, the in-process orchestrator section grows 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 (R17 mitigation), R2 / R15 subsections reflect the slice-1 worked example and the slice-5 contingent fallback (cq-6 option 2 + R15 model (b)), the unified \"Rollout deltas\" section split into Completed-in-this-rollout (3 [x] items for slice 1) and Pending-in-this-rollout (9 items mapped to slices 2-5), primitives + conformance-proof tables pick up the new slice-1 modules (TASK-1-8). Files reviewed: all four touched files plus the plan draft (.egg-state/drafts/2717-plan.md slice-1 task definitions), the coder's orchestrator/substrate/__init__.py loader changes (confirmed underscore filenames match _RUBRIC_LANDED_ROLES), and the two existing refine-plan rubrics that the new substrate rubrics mirror.", - "attestation": {}, - "artifacts": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "docs/architecture/claude-code-substrate.md" - ], - "risk_considered": "Doc-only slice; no source or test files touched. Risk surface: (1) docs referencing files that may not be on the branch yet \u2014 the coder's commit fddeb82bf is now merged in, so bin/run_pipeline.py + _agent_tool_fake.py + the loader changes are present; test_pretooluse_hook_nested.py and test_rubric_loader.py will be supplied by the tester on the same slice-1 branch before the slice merges; both are referenced in declarative \"lands in slice 1\" voice. (2) Stale anchor / link references \u2014 addressed by replace_all on the removed \"Follow-up issue draft\" anchor across the ADR. (3) Frontmatter shape for the two new reviewer rubrics \u2014 mirrors the existing plugins/refine-plan/skills/refine-plan/agents/reviewer-*.md frontmatter so the orchestrator's rubric loader does not need new parsing logic; loader reads the body verbatim and prepends to the system prompt (frontmatter is informational only per refiner.md). (4) Filename underscore-vs-hyphen \u2014 the new files use underscores (reviewer_refine.md, reviewer_agent_design.md) matching the AgentRole.value identifiers and the loader's `{role_name}.md` resolution at orchestrator/substrate/__init__.py:330. (5) Loader hint message \u2014 the coder's _RUBRIC_LANDED_ROLES check raises ValueError with a \"follow-up slice 2/3\" pointer for plan/implement roles; the ADR's \"What is and is not in this slice\" subsections cross-reference this exact mapping so a future grep for the slice-hint string finds both surfaces.", - "commit_sha": "473b109cbcaa54d4f89b84c4422139a6377b11c9", - "files_changed": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "docs/architecture/claude-code-substrate.md" - ], - "tests_run": [], - "tasks_satisfied": [ - "TASK-1-2", - "TASK-1-4", - "TASK-1-8" - ] - }, - "version": 1, - "commit_sha": "473b109cbcaa54d4f89b84c4422139a6377b11c9", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:59:07.901162+00:00", - "phase": "implement" - }, - { - "id": "b94d1e1b-831f-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T18:59:57.649250+00:00", - "phase": "implement" - }, - { - "id": "683a5120-d456-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:57:37.143439+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:00:07.274832+00:00", - "phase": "implement" - }, - { - "id": "e5346af9-176b-4f", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:00:53.533650+00:00", - "phase": "implement" - }, - { - "id": "5725d046-75e5-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:57:37.143439+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:07.319122+00:00", - "phase": "implement" - }, - { - "id": "ad4e09d5-300b-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_concurrency for coder", - "body": "\n**Concurrency-lens review of coder v1 (commit fddeb82bf)**\n\nReviewed all three files for race conditions, deadlocks, shared-state mutation, async-context leakage, retry-storm patterns, resource-cleanup ordering, and BRC-protocol invariants. No blocking concurrency findings.\n\n### What I checked\n\n**1. `bin/run_pipeline.py` \u2014 flattened single-yield driver**\n\n- **Cross-process state model**: each `python3 run_pipeline.py` invocation is a fresh process; generator state is rebuilt by replaying `answer_log` into a fresh generator. No live state shared across invocations \u2192 no cross-process race surface.\n- **Atomic contract write** (`_write_contract:140-147`): `tmp.write_text()` + `os.replace()` is correct for atomic single-writer; concurrent **readers** cannot observe a half-written file. Single-writer is enforced by the skill protocol (driver and skill body alternate sequentially).\n- **Generator lifecycle** (`_advance_generator:267-327`): `next() \u2192 for replay: send() \u2192 finally: generator.close()`. The `try/finally` reliably triggers `GeneratorExit` before `_persist_envelope` writes the new envelope, so `_InProcessOrchestrator`'s 3 daemon threads (heartbeat / BRC re-review / bus tick) are joined inside the orchestrator's own `finally` before the driver writes the final envelope. No thread-write race against the contract file at exit. Daemon threads that *don't* terminate promptly die with the process anyway (daemon=True) \u2014 no thread leak across invocations.\n- **env snapshot** (`:284-287`): `{**os.environ, \"EGG_SUBSTRATE\": ...}` builds a copy. Safe even if a thread mutated `os.environ` mid-call (which nothing does here).\n- **Status-promotion read-after-close** (`:502-505`): reads `envelope[\"answer_log\"][-1]` *after* the generator is closed \u2014 local variable only, no race.\n- **Abort handler** (`:467-475`, `:487-497`): error envelope is written on every failure path with `answer_log` preserved; no resource leaks on the unhappy path.\n\n**2. `_agent_tool_fake.py` \u2014 subprocess R2-hook fake**\n\n- **Subprocess timeout** (`:230`, default 10s): bounded wall-clock cap. `subprocess.run(..., timeout=timeout)` kills the child with SIGKILL on expiry. Acceptable for the R2 test scenario (hook invocation is sub-second).\n- **Pipe-fill deadlock**: `capture_output=True, text=True` routes both stdout/stderr through `subprocess.Popen.communicate()` under the hood, which drains both pipes concurrently. No risk of a full stderr buffer blocking the child.\n- **Blob-file cleanup** (`:261-333`): `tempfile.mkstemp()` + `try/finally: Path(blob_path).unlink(missing_ok=True)`. The finally runs on every exit path *including* `TimeoutExpired` propagation \u2014 temp file is always cleaned up. The `fdopen` `with` block closes the parent's fd before the subprocess opens its own descriptor to the path.\n- **sys.path manipulation** (`:101-126`): module-import-time only, inside `try/finally` that removes the injected path. Python's import lock serializes this; concurrent imports from multiple threads are safe. The `from run_pipeline import ...` happens once per process.\n- **Child isolation** (`:293-301`): fresh Python interpreter per dispatch; no shared mutable state with parent. The `env = {**os.environ, \"EGG_AGENT_ROLE\": role}` snapshot guarantees the child sees a controlled role.\n- **xdist-worker safety**: tempfile names use `tempfile.mkstemp(prefix=\"agent_tool_fake_\", ...)` \u2014 globally unique per-call, no fixture-file collision risk between pytest workers running this fake in parallel.\n\n**3. `orchestrator/substrate/__init__.py` \u2014 rubric loader expansion**\n\n- Adds two **immutable module-level constants** (`_ROLE_RUBRIC_SLICES` dict, `_RUBRIC_LANDED_ROLES` frozenset). The dict is mutable in principle but never mutated in code. Both are read-only references inside `_load_egg_sdlc_role_rubric`. No write-side concurrency surface.\n- The loader is pure read-only file I/O (`rubric_path.is_file()` + `rubric_path.read_text()`). No shared state, no locks needed, no async context.\n\n### BRC-protocol invariants\n\nThis PR does not touch the BRC message bus, consensus protocol, send\u2192wait ordering, `--since` cursor threading, heartbeat cadence, `stale_reviewers` invalidation, or the `max_flip_flops=3` cap. No BRC-invariant concerns.\n\n### Non-blocking\n\n- **run_pipeline.py:145** \u2014 `_write_contract` uses a fixed `.json.tmp` filename. The skill protocol is sequential by design, but as defense-in-depth (e.g. a stuck previous driver invocation) consider a unique tempfile via `tempfile.NamedTemporaryFile(dir=contract_path.parent, prefix=f\".{contract_path.stem}_\", suffix=\".json.tmp\", delete=False)` so two concurrent processes cannot clobber each other's pre-replace temp.\n- **run_pipeline.py:326** \u2014 `try: generator.close() except Exception: pass` silently swallows any exception from the orchestrator's `finally` block (including a future thread-join hang). Consider `traceback.print_exc(file=sys.stderr)` inside the except \u2014 keeps the defensive swallow but leaves a diagnostic trail when `_InProcessOrchestrator` cleanup misbehaves.\n- **_agent_tool_fake.py:298** \u2014 `subprocess.TimeoutExpired` propagates unhandled from `pre_tool_use_callback`; the surrounding `dispatch()` returns no `DispatchResult` on timeout. For test-infrastructure uniformity, consider catching it and returning `{\"decision\": \"block\", \"reason\": \"_agent_tool_fake child timed out after Xs\"}` \u2014 mirrors the other structured-failure paths at `:306-314` and `:318-325`.\n", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "\n**Concurrency-lens review of coder v1 (commit fddeb82bf)**\n\nReviewed all three files for race conditions, deadlocks, shared-state mutation, async-context leakage, retry-storm patterns, resource-cleanup ordering, and BRC-protocol invariants. No blocking concurrency findings.\n\n### What I checked\n\n**1. `bin/run_pipeline.py` \u2014 flattened single-yield driver**\n\n- **Cross-process state model**: each `python3 run_pipeline.py` invocation is a fresh process; generator state is rebuilt by replaying `answer_log` into a fresh generator. No live state shared across invocations \u2192 no cross-process race surface.\n- **Atomic contract write** (`_write_contract:140-147`): `tmp.write_text()` + `os.replace()` is correct for atomic single-writer; concurrent **readers** cannot observe a half-written file. Single-writer is enforced by the skill protocol (driver and skill body alternate sequentially).\n- **Generator lifecycle** (`_advance_generator:267-327`): `next() \u2192 for replay: send() \u2192 finally: generator.close()`. The `try/finally` reliably triggers `GeneratorExit` before `_persist_envelope` writes the new envelope, so `_InProcessOrchestrator`'s 3 daemon threads (heartbeat / BRC re-review / bus tick) are joined inside the orchestrator's own `finally` before the driver writes the final envelope. No thread-write race against the contract file at exit. Daemon threads that *don't* terminate promptly die with the process anyway (daemon=True) \u2014 no thread leak across invocations.\n- **env snapshot** (`:284-287`): `{**os.environ, \"EGG_SUBSTRATE\": ...}` builds a copy. Safe even if a thread mutated `os.environ` mid-call (which nothing does here).\n- **Status-promotion read-after-close** (`:502-505`): reads `envelope[\"answer_log\"][-1]` *after* the generator is closed \u2014 local variable only, no race.\n- **Abort handler** (`:467-475`, `:487-497`): error envelope is written on every failure path with `answer_log` preserved; no resource leaks on the unhappy path.\n\n**2. `_agent_tool_fake.py` \u2014 subprocess R2-hook fake**\n\n- **Subprocess timeout** (`:230`, default 10s): bounded wall-clock cap. `subprocess.run(..., timeout=timeout)` kills the child with SIGKILL on expiry. Acceptable for the R2 test scenario (hook invocation is sub-second).\n- **Pipe-fill deadlock**: `capture_output=True, text=True` routes both stdout/stderr through `subprocess.Popen.communicate()` under the hood, which drains both pipes concurrently. No risk of a full stderr buffer blocking the child.\n- **Blob-file cleanup** (`:261-333`): `tempfile.mkstemp()` + `try/finally: Path(blob_path).unlink(missing_ok=True)`. The finally runs on every exit path *including* `TimeoutExpired` propagation \u2014 temp file is always cleaned up. The `fdopen` `with` block closes the parent's fd before the subprocess opens its own descriptor to the path.\n- **sys.path manipulation** (`:101-126`): module-import-time only, inside `try/finally` that removes the injected path. Python's import lock serializes this; concurrent imports from multiple threads are safe. The `from run_pipeline import ...` happens once per process.\n- **Child isolation** (`:293-301`): fresh Python interpreter per dispatch; no shared mutable state with parent. The `env = {**os.environ, \"EGG_AGENT_ROLE\": role}` snapshot guarantees the child sees a controlled role.\n- **xdist-worker safety**: tempfile names use `tempfile.mkstemp(prefix=\"agent_tool_fake_\", ...)` \u2014 globally unique per-call, no fixture-file collision risk between pytest workers running this fake in parallel.\n\n**3. `orchestrator/substrate/__init__.py` \u2014 rubric loader expansion**\n\n- Adds two **immutable module-level constants** (`_ROLE_RUBRIC_SLICES` dict, `_RUBRIC_LANDED_ROLES` frozenset). The dict is mutable in principle but never mutated in code. Both are read-only references inside `_load_egg_sdlc_role_rubric`. No write-side concurrency surface.\n- The loader is pure read-only file I/O (`rubric_path.is_file()` + `rubric_path.read_text()`). No shared state, no locks needed, no async context.\n\n### BRC-protocol invariants\n\nThis PR does not touch the BRC message bus, consensus protocol, send\u2192wait ordering, `--since` cursor threading, heartbeat cadence, `stale_reviewers` invalidation, or the `max_flip_flops=3` cap. No BRC-invariant concerns.\n\n### Non-blocking\n\n- **run_pipeline.py:145** \u2014 `_write_contract` uses a fixed `.json.tmp` filename. The skill protocol is sequential by design, but as defense-in-depth (e.g. a stuck previous driver invocation) consider a unique tempfile via `tempfile.NamedTemporaryFile(dir=contract_path.parent, prefix=f\".{contract_path.stem}_\", suffix=\".json.tmp\", delete=False)` so two concurrent processes cannot clobber each other's pre-replace temp.\n- **run_pipeline.py:326** \u2014 `try: generator.close() except Exception: pass` silently swallows any exception from the orchestrator's `finally` block (including a future thread-join hang). Consider `traceback.print_exc(file=sys.stderr)` inside the except \u2014 keeps the defensive swallow but leaves a diagnostic trail when `_InProcessOrchestrator` cleanup misbehaves.\n- **_agent_tool_fake.py:298** \u2014 `subprocess.TimeoutExpired` propagates unhandled from `pre_tool_use_callback`; the surrounding `dispatch()` returns no `DispatchResult` on timeout. For test-infrastructure uniformity, consider catching it and returning `{\"decision\": \"block\", \"reason\": \"_agent_tool_fake child timed out after Xs\"}` \u2014 mirrors the other structured-failure paths at `:306-314` and `:318-325`.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:44.174837+00:00", - "phase": "implement" - }, - { - "id": "a3a0b153-6f9a-41", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:44.227632+00:00", - "phase": "implement" - }, - { - "id": "337cc6c7-04cb-48", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:49.332411+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:49.418468+00:00", - "phase": "implement" - }, - { - "id": "2418b70d-7baa-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:49.925403+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:49.984135+00:00", - "phase": "implement" - }, - { - "id": "af1855e2-8568-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:50.063502+00:00", - "phase": "implement" - }, - { - "id": "bebd5620-9972-46", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:53.574608+00:00", - "phase": "implement" - }, - { - "id": "b56978de-afab-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:01:59.018691+00:00", - "phase": "implement" - }, - { - "id": "967fae6c-e98c-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:49.332411+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:49.469054+00:00", - "phase": "implement" - }, - { - "id": "e38d7773-4e34-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_security for coder", - "body": "Security-lens review of slice-1 coder proposal (3 files, +1070 lines).\n\n**Scope reviewed:** trust-boundary changes (the new bridge driver under the egg-sdlc skill bin and the role-rubric loader expansion), cross-file allowlist mismatches between the rubric loader's role allowlist and the disk layout, the test-only nested-Agent-tool fake's subprocess shape and import guard, and information-disclosure surfaces in the persisted error envelope.\n\n**Verified clean:**\n\n1. **Rubric loader path safety (`orchestrator/substrate/__init__.py:280-365`).** The role\u2192file mapping `_RUBRIC_LANDED_ROLES = {\"refiner\", \"reviewer_refine\", \"reviewer_agent_design\"}` is an exact-string allowlist consulted BEFORE the disk read at line 365, so an attacker-supplied `role` containing `../` cannot reach `rubric_path.read_text()` \u2014 the `if role_name not in _RUBRIC_LANDED_ROLES` fence raises `ValueError` first. The `_ROLE_RUBRIC_SLICES` mapping also uses exact match. No path-traversal reach.\n\n2. **Nested-Agent-tool fake (`integration_tests/regression/_agent_tool_fake.py`).** Import guard at lines 84-96 rejects production callers via `__name__` prefix check. The child subprocess invocation at line 293 uses list-form `subprocess.run([py, \"-m\", module_name, blob_path], ...)` with a hardcoded `module_name` \u2014 no shell injection, no agent-controlled argv beyond the JSON blob path (a `tempfile.mkstemp` output unlinked in `finally`). The child JSON output is parsed with `json.JSONDecodeError` handled defensively (lines 315-325) and `isinstance(verdict, dict)` enforced (line 326). The fake delegates to `hook_entry.decide(...)` only \u2014 it queries the verdict, it never executes the write \u2014 so the simulated nested dispatch does not bypass any real authorization check.\n\n3. **Cross-file `pending_hitl` envelope schema.** Driver (`run_pipeline.py:94`) and fake (`_agent_tool_fake.py:115-126`) both pin `PENDING_HITL_SCHEMA_VERSION = 1`; the driver's `_coerce_envelope` rejects newer-version envelopes with `ValueError` (lines 211-217) so a slice-3 daemon writing v2 can't silently downgrade through this driver. Schema-version invariant holds across the changed files.\n\n4. **No uncommitted-artifact / Dockerfile-symlink mismatches.** Diff only touches the three Python files and state artifacts; no new symlinks, COPY targets, or entry points were introduced.\n\n5. **No new gateway routes or credential shims.** `sandbox/scripts/` is untouched; the bridge driver runs in the outer-session trust context (per its docstring), not as an egress wrapper. The role-rubric loader does not embed credentials.\n\n### Non-blocking\n\n- **`run_pipeline.py:413-419, 130, 146-147` \u2014 Defense-in-depth: validate `pipeline_id` and `--state-root` before path construction.** Both flow unvalidated from `argv` into `contract_path = state_root / \"contracts\" / f\"{pipeline_id}.json\"`, which is then read (`Path.read_text()` in `_read_contract`) and written (`tmp.write_text()` + `os.replace()` in `_write_contract`). The current threat model puts the driver in the outer-session trust context where this is benign, but a prompt-injection vector (e.g., an attacker-controlled GitHub issue body steering Claude Code in the outer session to invoke the driver with `pipeline_id=\"../foo\"`) would let a single malformed argv land a JSON write outside `.egg-state/contracts/`. The orchestrator's `_InProcessOrchestrator._write_pending_decision` is paired with the same `os.replace` shape but presumably sanitises pipeline_id upstream \u2014 the driver should mirror that defence locally. Suggested fix: validate `pipeline_id` with `re.fullmatch(r\"[a-zA-Z0-9_-]{1,64}\", pipeline_id)` and assert `contract_path.resolve().is_relative_to(state_root.resolve())` before any read/write; move the validation *before* `_ensure_contracts_dir()` so the driver cannot `mkdir(parents=True)` into an out-of-bound path.\n\n- **`run_pipeline.py:487-497` \u2014 Persisted traceback is committed to git as part of `.egg-state/contracts/.json`.** `traceback.format_exc(limit=8)` may include absolute filesystem paths, library versions, and other internal state that the contract file then carries into version control. Minor information disclosure to anyone who reads the repo history. Consider stripping absolute paths (replace `repo_root` with ``) or capturing tracebacks only into a non-committed sidecar log (e.g., `.egg-state/logs/-driver.log`) and persisting only the exception type + message in the contract.\n\n- **`_agent_tool_fake.py:114-126` \u2014 Hardcoded `PENDING_HITL_SCHEMA_VERSION = 1` fallback masks a real schema bump.** If the driver later moves to v2 but the fake's path-walk import fails (sys.path race, missing skill bin), the fallback at line 126 silently pins v1 and tests may pass against an incompatible schema. Given the docstring's explicit \"STABLE contract \u2014 slice-3 daemon inherits this shape\" framing, the fallback should raise instead \u2014 better to fail loudly than to silently disagree.\n\n- **`_agent_tool_fake.py:266` \u2014 Child subprocess inherits the full parent env via `{**os.environ, \"EGG_AGENT_ROLE\": role}`.** For test infrastructure this is acceptable, but it propagates `EGG_SESSION_TOKEN` / `GITHUB_TOKEN` / etc. to a subprocess that only needs `EGG_AGENT_ROLE` + `PYTHONPATH` + `EGG_REPO_ROOT`. Consider an opt-in minimal-env shape so future security-sensitive tests don't accidentally leak credentials into the fake-subagent's child.", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "Security-lens review of slice-1 coder proposal (3 files, +1070 lines).\n\n**Scope reviewed:** trust-boundary changes (the new bridge driver under the egg-sdlc skill bin and the role-rubric loader expansion), cross-file allowlist mismatches between the rubric loader's role allowlist and the disk layout, the test-only nested-Agent-tool fake's subprocess shape and import guard, and information-disclosure surfaces in the persisted error envelope.\n\n**Verified clean:**\n\n1. **Rubric loader path safety (`orchestrator/substrate/__init__.py:280-365`).** The role\u2192file mapping `_RUBRIC_LANDED_ROLES = {\"refiner\", \"reviewer_refine\", \"reviewer_agent_design\"}` is an exact-string allowlist consulted BEFORE the disk read at line 365, so an attacker-supplied `role` containing `../` cannot reach `rubric_path.read_text()` \u2014 the `if role_name not in _RUBRIC_LANDED_ROLES` fence raises `ValueError` first. The `_ROLE_RUBRIC_SLICES` mapping also uses exact match. No path-traversal reach.\n\n2. **Nested-Agent-tool fake (`integration_tests/regression/_agent_tool_fake.py`).** Import guard at lines 84-96 rejects production callers via `__name__` prefix check. The child subprocess invocation at line 293 uses list-form `subprocess.run([py, \"-m\", module_name, blob_path], ...)` with a hardcoded `module_name` \u2014 no shell injection, no agent-controlled argv beyond the JSON blob path (a `tempfile.mkstemp` output unlinked in `finally`). The child JSON output is parsed with `json.JSONDecodeError` handled defensively (lines 315-325) and `isinstance(verdict, dict)` enforced (line 326). The fake delegates to `hook_entry.decide(...)` only \u2014 it queries the verdict, it never executes the write \u2014 so the simulated nested dispatch does not bypass any real authorization check.\n\n3. **Cross-file `pending_hitl` envelope schema.** Driver (`run_pipeline.py:94`) and fake (`_agent_tool_fake.py:115-126`) both pin `PENDING_HITL_SCHEMA_VERSION = 1`; the driver's `_coerce_envelope` rejects newer-version envelopes with `ValueError` (lines 211-217) so a slice-3 daemon writing v2 can't silently downgrade through this driver. Schema-version invariant holds across the changed files.\n\n4. **No uncommitted-artifact / Dockerfile-symlink mismatches.** Diff only touches the three Python files and state artifacts; no new symlinks, COPY targets, or entry points were introduced.\n\n5. **No new gateway routes or credential shims.** `sandbox/scripts/` is untouched; the bridge driver runs in the outer-session trust context (per its docstring), not as an egress wrapper. The role-rubric loader does not embed credentials.\n\n### Non-blocking\n\n- **`run_pipeline.py:413-419, 130, 146-147` \u2014 Defense-in-depth: validate `pipeline_id` and `--state-root` before path construction.** Both flow unvalidated from `argv` into `contract_path = state_root / \"contracts\" / f\"{pipeline_id}.json\"`, which is then read (`Path.read_text()` in `_read_contract`) and written (`tmp.write_text()` + `os.replace()` in `_write_contract`). The current threat model puts the driver in the outer-session trust context where this is benign, but a prompt-injection vector (e.g., an attacker-controlled GitHub issue body steering Claude Code in the outer session to invoke the driver with `pipeline_id=\"../foo\"`) would let a single malformed argv land a JSON write outside `.egg-state/contracts/`. The orchestrator's `_InProcessOrchestrator._write_pending_decision` is paired with the same `os.replace` shape but presumably sanitises pipeline_id upstream \u2014 the driver should mirror that defence locally. Suggested fix: validate `pipeline_id` with `re.fullmatch(r\"[a-zA-Z0-9_-]{1,64}\", pipeline_id)` and assert `contract_path.resolve().is_relative_to(state_root.resolve())` before any read/write; move the validation *before* `_ensure_contracts_dir()` so the driver cannot `mkdir(parents=True)` into an out-of-bound path.\n\n- **`run_pipeline.py:487-497` \u2014 Persisted traceback is committed to git as part of `.egg-state/contracts/.json`.** `traceback.format_exc(limit=8)` may include absolute filesystem paths, library versions, and other internal state that the contract file then carries into version control. Minor information disclosure to anyone who reads the repo history. Consider stripping absolute paths (replace `repo_root` with ``) or capturing tracebacks only into a non-committed sidecar log (e.g., `.egg-state/logs/-driver.log`) and persisting only the exception type + message in the contract.\n\n- **`_agent_tool_fake.py:114-126` \u2014 Hardcoded `PENDING_HITL_SCHEMA_VERSION = 1` fallback masks a real schema bump.** If the driver later moves to v2 but the fake's path-walk import fails (sys.path race, missing skill bin), the fallback at line 126 silently pins v1 and tests may pass against an incompatible schema. Given the docstring's explicit \"STABLE contract \u2014 slice-3 daemon inherits this shape\" framing, the fallback should raise instead \u2014 better to fail loudly than to silently disagree.\n\n- **`_agent_tool_fake.py:266` \u2014 Child subprocess inherits the full parent env via `{**os.environ, \"EGG_AGENT_ROLE\": role}`.** For test infrastructure this is acceptable, but it propagates `EGG_SESSION_TOKEN` / `GITHUB_TOKEN` / etc. to a subprocess that only needs `EGG_AGENT_ROLE` + `PYTHONPATH` + `EGG_REPO_ROOT`. Consider an opt-in minimal-env shape so future security-sensitive tests don't accidentally leak credentials into the fake-subagent's child.", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:50.977125+00:00", - "phase": "implement" - }, - { - "id": "1075b5ed-a477-45", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:51.005663+00:00", - "phase": "implement" - }, - { - "id": "2bd71d30-ab0c-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:53.624913+00:00", - "phase": "implement" - }, - { - "id": "694d133e-b4f6-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:02:55.753143+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:55.785493+00:00", - "phase": "implement" - }, - { - "id": "c731e931-13d7-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:55.866547+00:00", - "phase": "implement" - }, - { - "id": "80021494-3175-48", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:02:56.998153+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:57.023715+00:00", - "phase": "implement" - }, - { - "id": "eea41f70-1360-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:02:59.666499+00:00", - "phase": "implement" - }, - { - "id": "f4c92493-f937-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:03:03.933132+00:00", - "phase": "implement" - }, - { - "id": "2df03d34-2703-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_contract for coder", - "body": "Contract verification \u2014 slice-1 coder proposal v1 @ fddeb82bf.\n\nReviewed three coder-owned files end-to-end against the slice-1 contract's per-task acceptance criteria. All three tasks substantively meet their AC. Files changed match the per-task `files_affected` exactly; no scope creep into documenter/tester territory.\n\n### TASK-1-1 (`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py`) \u2014 VERIFIED\nAC quote: \"`bin/run_pipeline.py` exists; it imports `run_pipeline_in_process` from `orchestrator.substrate.in_process` (line 84); on invocation it round-trips a single `HITLDecision` through `.egg-state/contracts/.json` and exits with status 0 when the generator yields, status 1 on internal error. The `SKILL.md` 'Usage' section is updated to call this driver in a loop. The `pending_hitl` envelope schema is documented as a stable contract (top-of-file comment listing the contract fields).\"\n\nEvidence:\n- File exists, 530 lines, shebang + executable bit (mode 100755). \u2713\n- Imports `run_pipeline_in_process` from `orchestrator.substrate.in_process` at line 248 (inside `_import_runner` \u2014 lazy import to surface ImportError as a structured envelope error per the docstring). \u2713\n- `.egg-state/contracts/.json` round-trip implemented via `_read_contract`/`_persist_envelope` (lines 118\u2013147) using temp+`os.replace` for atomicity, mirroring `_InProcessOrchestrator._write_pending_decision` per the comment. \u2713\n- Exit-code contract: smoke-tested locally \u2014 empty `pipeline_id` \u2192 exit 1, ImportError (no orchestrator on PYTHONPATH) \u2192 exit 1, `--daemon` short-circuit \u2192 exit 1, generator-yield path \u2192 exit 0 (returns 0 at line 525). \u2713\n- Stable-contract schema documented at lines 20\u201346 with explicit \"STABLE contract \u2014 slice-3 daemon inherits this shape; do NOT change field names/types without bumping version\" callout. Field set: `version, pipeline_id, timestamp, decision, answer, status, result, error, answer_log` \u2014 superset of the AC-named `decision, answer, version, pipeline_id, timestamp`. Extras (`status`, `answer_log`, `result`, `error`) are mechanics needed for cross-process generator-state replay and structured error reporting; they are documented as part of the stable contract so the daemon variant cannot accidentally diverge. \u2713\n- `PENDING_HITL_SCHEMA_VERSION = 1` constant (line 94) provides the bump-point the comment promises. `_coerce_envelope` (line 200) rejects future-version envelopes \u2014 smoke-verified: passing `{\"version\": 99}` raises ValueError with the documented message. \u2713\n- SKILL.md \"Usage\" section update is the documenter's TASK-1-2 deliverable, not coder-owned. The merged slice branch already carries the documenter's commit `473b109cb` which rewrites the \"Usage\" section to call `bin/run_pipeline.py` in a loop and documents the `pending_hitl` envelope (verified by inspection of the diff against `origin/main`). The cross-cutting AC will be re-verified when documenter proposes; coder's deliverable is complete.\n\n### TASK-1-9 (`integration_tests/regression/_agent_tool_fake.py`) \u2014 VERIFIED\nAC quote: \"`_agent_tool_fake.py` exists; exposes a `dispatch(parent_role, child_role, write_target)` helper; the helper invokes `hook_entry.decide(...)` via the simulated child's `pre_tool_use_callback` and returns the hook verdict. Documented in the module docstring as test-only; protected with a top-of-file `if not __name__.startswith(\\\"integration_tests\\\")` import guard so it can't be silently imported by production code.\"\n\nEvidence:\n- File exists, 451 lines. \u2713\n- `dispatch(parent_role, child_role, write_target, ...)` helper defined at line 336 with the signature the AC names; trailing kwargs (`tool_name`, `extra_env`, `repo_root`) are additive and reasonable. \u2713\n- Helper invokes `hook_entry.decide(...)` via `pre_tool_use_callback` \u2192 spawns `python3 -m \u2026 _agent_tool_fake ` \u2192 `_child_main` calls `from orchestrator.substrate.claude_code.hook_entry import decide` and `verdict = decide(stdin_blob)` (line 211). The dispatch path is correct: parent \u2192 subprocess(child_role) \u2192 hook_entry.decide \u2192 verdict. \u2713\n- Returns `DispatchResult` carrying `decision` (the hook verdict dict) plus `denied`/`deny_reason`/`child_pid`/`child_exit_code`/`stderr` for test observability. The raw hook verdict is preserved on `.decision`. \u2713\n- Module docstring (lines 1\u201362) explicitly states \"**This is TEST INFRASTRUCTURE ONLY.**\" and explains why (cq-3: production stays on harness re-host). \u2713\n- Top-of-file import guard at lines 84\u201396, raises `ImportError` if `__name__` does not start with any of `(integration_tests, _agent_tool_fake, __main__)`. Smoke-verified by exec-ing the module body with `__name__ = \"orchestrator.fake_user\"` \u2014 the guard fires and rejects with the expected diagnostic. \u2713\n\n### TASK-1-6 (`orchestrator/substrate/__init__.py`) \u2014 VERIFIED\nAC quote: \"`_load_egg_sdlc_role_rubric(REVIEWER_REFINE)` returns the markdown body of `reviewer_refine.md`; `_load_egg_sdlc_role_rubric(REVIEWER_AGENT_DESIGN)` returns the body of `reviewer_agent_design.md`; `_load_egg_sdlc_role_rubric(ARCHITECT)` still raises `ValueError` with the 'follow-up issue per cq-11' hint updated to 'follow-up slice 2'.\"\n\nEvidence (smoke-tested directly in this worktree, post-merge of slice-1):\n- `_load_egg_sdlc_role_rubric('reviewer_refine')` \u2192 returns 6628-char markdown body starting with `---\\n# Role data file. \u2026` \u2713\n- `_load_egg_sdlc_role_rubric('reviewer_agent_design')` \u2192 returns 6558-char markdown body, same shape. \u2713\n- `_load_egg_sdlc_role_rubric('architect')` \u2192 raises `ValueError(\"egg-sdlc role rubric for role='architect' is deferred to follow-up slice-2 of issue #2717's rollout. See docs/architecture/claude-code-substrate.md for the slice DAG.\")` \u2014 the prior cq-11 hint is replaced with the slice-2 pointer the AC requires. \u2713 (Pre-change wording at 802f77d9e:264-267 said \"Walking-skeleton spike (#2623) only ships the refiner rubric; other roles are deferred to the follow-up issue per cq-11\"; new wording correctly cites slice-2 for architect.)\n- Regression checks:\n - `_load_egg_sdlc_role_rubric('refiner')` still returns the original 8087-char rubric (no regression on the #2715 walking-skeleton refiner support).\n - `_load_egg_sdlc_role_rubric('coder')` raises ValueError citing slice-3 (consistent with the rollout DAG).\n - `_load_egg_sdlc_role_rubric('overseer')` (not in `_ROLE_RUBRIC_SLICES`) raises a distinct \"not part of the #2717 rollout's rubric set; file a follow-up issue\" error \u2014 good differentiation between \"deferred to a known slice\" and \"indefinitely deferred\" (overseer/inspector/autofixer/conflict_resolver per the comment at lines 235\u2013237).\n- The two new data structures (`_ROLE_RUBRIC_SLICES` mapping, `_RUBRIC_LANDED_ROLES` frozenset) cleanly separate the rubric-slice DAG from the on-disk-availability gate, so later slices extend declaratively without touching the loader body. \u2713\n- The TASK-1-6 description's \"sequence TASK-1-4 \u2192 TASK-1-6 within the slice\" risk is also mitigated: when a role is in `_RUBRIC_LANDED_ROLES` but the .md file is missing, the loader raises a distinct \"rubric missing on disk \u2026 sequence the documenter's rubric task before the loader update\" error (lines 352\u2013364). The documenter has already landed `reviewer_refine.md` and `reviewer_agent_design.md` in the merged slice branch (commit `473b109cb`) so this fallback is not reached today, but the safety net exists for re-runs.\n\n### Phase consistency\n- All three coder tasks are still in `status: pending` in the contract; the producer is expected to call `mcp__task__complete` on `task-1-1`, `task-1-6`, `task-1-9` (linking commit `fddeb82bf`) after CONSENSUS_CONFIRMED lands. Not a blocker for ACK \u2014 task-complete is normally done in the converge step.\n- No orphaned code: every file in the coder commit maps to exactly one task's `files_affected`.\n\n### Non-blocking\n- **`_agent_tool_fake.py` import guard breadth**: AC literal text was `if not __name__.startswith(\"integration_tests\")`. Implementation uses a 3-prefix allow-list `(integration_tests, _agent_tool_fake, __main__)` because the `python3 -m integration_tests.regression._agent_tool_fake ` invocation done by the parent helper lands as `__name__ == \"__main__\"` in the child, and conftest-driven sys.path injection can land it as the bare `_agent_tool_fake`. Both additions are necessary for the dispatch model to function; the guard's intent (block production imports) is preserved. Worth a brief code comment cross-referencing the parent helper's `module_name` selection so future readers don't tighten the guard and break the child spawn \u2014 but no behavior change needed.\n- **`follow-up slice 2` wording**: AC said \"follow-up slice 2\" (space), implementation produces \"follow-up slice-2\" (hyphen, matching the slice-ID format `slice-N` used throughout `_ROLE_RUBRIC_SLICES` and the rest of the rollout). Substantively compliant; the hyphenated form is the consistent convention.\n- **`run_pipeline.py` `--repo` default flow**: line 460 falls back through `args.repo \u2192 EGG_REPO \u2192 EGG_PIPELINE_REPO`. The CLI help text only mentions `EGG_REPO`. Worth a one-line help-string note documenting the `EGG_PIPELINE_REPO` fallback so operators don't waste time hunting for the env var name.\n- **Schema doc completeness**: the top-of-file `pending_hitl` schema comment names `decision/answer/status/result/error` fields but lists `answer_log` separately in the implementation; consider folding `answer_log` into the same table-of-fields comment block so the \"stable contract\" inventory is self-contained without forcing the daemon-author to read the body. Pure documentation polish.", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "Contract verification \u2014 slice-1 coder proposal v1 @ fddeb82bf.\n\nReviewed three coder-owned files end-to-end against the slice-1 contract's per-task acceptance criteria. All three tasks substantively meet their AC. Files changed match the per-task `files_affected` exactly; no scope creep into documenter/tester territory.\n\n### TASK-1-1 (`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py`) \u2014 VERIFIED\nAC quote: \"`bin/run_pipeline.py` exists; it imports `run_pipeline_in_process` from `orchestrator.substrate.in_process` (line 84); on invocation it round-trips a single `HITLDecision` through `.egg-state/contracts/.json` and exits with status 0 when the generator yields, status 1 on internal error. The `SKILL.md` 'Usage' section is updated to call this driver in a loop. The `pending_hitl` envelope schema is documented as a stable contract (top-of-file comment listing the contract fields).\"\n\nEvidence:\n- File exists, 530 lines, shebang + executable bit (mode 100755). \u2713\n- Imports `run_pipeline_in_process` from `orchestrator.substrate.in_process` at line 248 (inside `_import_runner` \u2014 lazy import to surface ImportError as a structured envelope error per the docstring). \u2713\n- `.egg-state/contracts/.json` round-trip implemented via `_read_contract`/`_persist_envelope` (lines 118\u2013147) using temp+`os.replace` for atomicity, mirroring `_InProcessOrchestrator._write_pending_decision` per the comment. \u2713\n- Exit-code contract: smoke-tested locally \u2014 empty `pipeline_id` \u2192 exit 1, ImportError (no orchestrator on PYTHONPATH) \u2192 exit 1, `--daemon` short-circuit \u2192 exit 1, generator-yield path \u2192 exit 0 (returns 0 at line 525). \u2713\n- Stable-contract schema documented at lines 20\u201346 with explicit \"STABLE contract \u2014 slice-3 daemon inherits this shape; do NOT change field names/types without bumping version\" callout. Field set: `version, pipeline_id, timestamp, decision, answer, status, result, error, answer_log` \u2014 superset of the AC-named `decision, answer, version, pipeline_id, timestamp`. Extras (`status`, `answer_log`, `result`, `error`) are mechanics needed for cross-process generator-state replay and structured error reporting; they are documented as part of the stable contract so the daemon variant cannot accidentally diverge. \u2713\n- `PENDING_HITL_SCHEMA_VERSION = 1` constant (line 94) provides the bump-point the comment promises. `_coerce_envelope` (line 200) rejects future-version envelopes \u2014 smoke-verified: passing `{\"version\": 99}` raises ValueError with the documented message. \u2713\n- SKILL.md \"Usage\" section update is the documenter's TASK-1-2 deliverable, not coder-owned. The merged slice branch already carries the documenter's commit `473b109cb` which rewrites the \"Usage\" section to call `bin/run_pipeline.py` in a loop and documents the `pending_hitl` envelope (verified by inspection of the diff against `origin/main`). The cross-cutting AC will be re-verified when documenter proposes; coder's deliverable is complete.\n\n### TASK-1-9 (`integration_tests/regression/_agent_tool_fake.py`) \u2014 VERIFIED\nAC quote: \"`_agent_tool_fake.py` exists; exposes a `dispatch(parent_role, child_role, write_target)` helper; the helper invokes `hook_entry.decide(...)` via the simulated child's `pre_tool_use_callback` and returns the hook verdict. Documented in the module docstring as test-only; protected with a top-of-file `if not __name__.startswith(\\\"integration_tests\\\")` import guard so it can't be silently imported by production code.\"\n\nEvidence:\n- File exists, 451 lines. \u2713\n- `dispatch(parent_role, child_role, write_target, ...)` helper defined at line 336 with the signature the AC names; trailing kwargs (`tool_name`, `extra_env`, `repo_root`) are additive and reasonable. \u2713\n- Helper invokes `hook_entry.decide(...)` via `pre_tool_use_callback` \u2192 spawns `python3 -m \u2026 _agent_tool_fake ` \u2192 `_child_main` calls `from orchestrator.substrate.claude_code.hook_entry import decide` and `verdict = decide(stdin_blob)` (line 211). The dispatch path is correct: parent \u2192 subprocess(child_role) \u2192 hook_entry.decide \u2192 verdict. \u2713\n- Returns `DispatchResult` carrying `decision` (the hook verdict dict) plus `denied`/`deny_reason`/`child_pid`/`child_exit_code`/`stderr` for test observability. The raw hook verdict is preserved on `.decision`. \u2713\n- Module docstring (lines 1\u201362) explicitly states \"**This is TEST INFRASTRUCTURE ONLY.**\" and explains why (cq-3: production stays on harness re-host). \u2713\n- Top-of-file import guard at lines 84\u201396, raises `ImportError` if `__name__` does not start with any of `(integration_tests, _agent_tool_fake, __main__)`. Smoke-verified by exec-ing the module body with `__name__ = \"orchestrator.fake_user\"` \u2014 the guard fires and rejects with the expected diagnostic. \u2713\n\n### TASK-1-6 (`orchestrator/substrate/__init__.py`) \u2014 VERIFIED\nAC quote: \"`_load_egg_sdlc_role_rubric(REVIEWER_REFINE)` returns the markdown body of `reviewer_refine.md`; `_load_egg_sdlc_role_rubric(REVIEWER_AGENT_DESIGN)` returns the body of `reviewer_agent_design.md`; `_load_egg_sdlc_role_rubric(ARCHITECT)` still raises `ValueError` with the 'follow-up issue per cq-11' hint updated to 'follow-up slice 2'.\"\n\nEvidence (smoke-tested directly in this worktree, post-merge of slice-1):\n- `_load_egg_sdlc_role_rubric('reviewer_refine')` \u2192 returns 6628-char markdown body starting with `---\\n# Role data file. \u2026` \u2713\n- `_load_egg_sdlc_role_rubric('reviewer_agent_design')` \u2192 returns 6558-char markdown body, same shape. \u2713\n- `_load_egg_sdlc_role_rubric('architect')` \u2192 raises `ValueError(\"egg-sdlc role rubric for role='architect' is deferred to follow-up slice-2 of issue #2717's rollout. See docs/architecture/claude-code-substrate.md for the slice DAG.\")` \u2014 the prior cq-11 hint is replaced with the slice-2 pointer the AC requires. \u2713 (Pre-change wording at 802f77d9e:264-267 said \"Walking-skeleton spike (#2623) only ships the refiner rubric; other roles are deferred to the follow-up issue per cq-11\"; new wording correctly cites slice-2 for architect.)\n- Regression checks:\n - `_load_egg_sdlc_role_rubric('refiner')` still returns the original 8087-char rubric (no regression on the #2715 walking-skeleton refiner support).\n - `_load_egg_sdlc_role_rubric('coder')` raises ValueError citing slice-3 (consistent with the rollout DAG).\n - `_load_egg_sdlc_role_rubric('overseer')` (not in `_ROLE_RUBRIC_SLICES`) raises a distinct \"not part of the #2717 rollout's rubric set; file a follow-up issue\" error \u2014 good differentiation between \"deferred to a known slice\" and \"indefinitely deferred\" (overseer/inspector/autofixer/conflict_resolver per the comment at lines 235\u2013237).\n- The two new data structures (`_ROLE_RUBRIC_SLICES` mapping, `_RUBRIC_LANDED_ROLES` frozenset) cleanly separate the rubric-slice DAG from the on-disk-availability gate, so later slices extend declaratively without touching the loader body. \u2713\n- The TASK-1-6 description's \"sequence TASK-1-4 \u2192 TASK-1-6 within the slice\" risk is also mitigated: when a role is in `_RUBRIC_LANDED_ROLES` but the .md file is missing, the loader raises a distinct \"rubric missing on disk \u2026 sequence the documenter's rubric task before the loader update\" error (lines 352\u2013364). The documenter has already landed `reviewer_refine.md` and `reviewer_agent_design.md` in the merged slice branch (commit `473b109cb`) so this fallback is not reached today, but the safety net exists for re-runs.\n\n### Phase consistency\n- All three coder tasks are still in `status: pending` in the contract; the producer is expected to call `mcp__task__complete` on `task-1-1`, `task-1-6`, `task-1-9` (linking commit `fddeb82bf`) after CONSENSUS_CONFIRMED lands. Not a blocker for ACK \u2014 task-complete is normally done in the converge step.\n- No orphaned code: every file in the coder commit maps to exactly one task's `files_affected`.\n\n### Non-blocking\n- **`_agent_tool_fake.py` import guard breadth**: AC literal text was `if not __name__.startswith(\"integration_tests\")`. Implementation uses a 3-prefix allow-list `(integration_tests, _agent_tool_fake, __main__)` because the `python3 -m integration_tests.regression._agent_tool_fake ` invocation done by the parent helper lands as `__name__ == \"__main__\"` in the child, and conftest-driven sys.path injection can land it as the bare `_agent_tool_fake`. Both additions are necessary for the dispatch model to function; the guard's intent (block production imports) is preserved. Worth a brief code comment cross-referencing the parent helper's `module_name` selection so future readers don't tighten the guard and break the child spawn \u2014 but no behavior change needed.\n- **`follow-up slice 2` wording**: AC said \"follow-up slice 2\" (space), implementation produces \"follow-up slice-2\" (hyphen, matching the slice-ID format `slice-N` used throughout `_ROLE_RUBRIC_SLICES` and the rest of the rollout). Substantively compliant; the hyphenated form is the consistent convention.\n- **`run_pipeline.py` `--repo` default flow**: line 460 falls back through `args.repo \u2192 EGG_REPO \u2192 EGG_PIPELINE_REPO`. The CLI help text only mentions `EGG_REPO`. Worth a one-line help-string note documenting the `EGG_PIPELINE_REPO` fallback so operators don't waste time hunting for the env var name.\n- **Schema doc completeness**: the top-of-file `pending_hitl` schema comment names `decision/answer/status/result/error` fields but lists `answer_log` separately in the implementation; consider folding `answer_log` into the same table-of-fields comment block so the \"stable contract\" inventory is self-contained without forcing the daemon-author to read the body. Pure documentation polish.", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:03:49.948992+00:00", - "phase": "implement" - }, - { - "id": "1c16fef1-e278-4f", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:03:49.998524+00:00", - "phase": "implement" - }, - { - "id": "02b785cc-1de5-41", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:03:53.671992+00:00", - "phase": "implement" - }, - { - "id": "74c036fb-4733-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:55.730536+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:03:55.758832+00:00", - "phase": "implement" - }, - { - "id": "fd8f715e-2ea7-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:03:59.704545+00:00", - "phase": "implement" - }, - { - "id": "97375d9b-4853-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_contract (pending_acks)", - "body": "Agent reviewer_contract cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus.", - "metadata": { - "pending_acks": true, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:04:02.038040+00:00", - "phase": "implement" - }, - { - "id": "4c28fed6-c4ec-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:04:03.991368+00:00", - "phase": "implement" - }, - { - "id": "c0f04524-4c77-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:04:09.316695+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:04:09.391612+00:00", - "phase": "implement" - }, - { - "id": "324cbe8a-13e0-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:04:53.768353+00:00", - "phase": "implement" - }, - { - "id": "2cdf566c-36d1-41", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:55.730536+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:04:55.812574+00:00", - "phase": "implement" - }, - { - "id": "43695226-5309-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:04:59.763457+00:00", - "phase": "implement" - }, - { - "id": "ec5d7bad-7856-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:05:04.052653+00:00", - "phase": "implement" - }, - { - "id": "72587a36-ca64-4c", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from tester for coder", - "body": "\nReviewed the coder's three artifacts (run_pipeline.py, _agent_tool_fake.py, orchestrator/substrate/__init__.py) plus drove all three acceptance criteria end-to-end via the tester-side tests I committed alongside this ACK.\n\n### What I verified\n\n**TASK-1-1 (`bin/run_pipeline.py`) \u2014 bridge driver:**\n- Round-trip works end-to-end. My `test_bridge_flattened_round_trip` invokes the driver twice via `runpy` in a fresh subprocess each call. Stage A captures `pending_hitl.decision.question == \"Confirm the refiner will run against this repo + issue?\"` (the preflight); after I write `answer=\"approve\" + status=\"answered\"` to the contract, Stage B replays the answer through the `answer_log` mechanism and lands on the refine-gate decision (`decision_type=phase_gate`, distinct question). Exits 0 on each yield as the AC requires.\n- The `pending_hitl` envelope schema is documented as a stable contract at the top of `run_pipeline.py` (lines 20-46) \u2014 fields `version`, `pipeline_id`, `timestamp`, `decision`, `answer`, `status`, `result`, `error`, `answer_log`. The slice-3 daemon variant (TASK-3-2) inheriting this shape is correctly called out for R17 mitigation.\n- `_advance_generator` replay model is sound for a deterministic generator: closes the generator in `finally` (GeneratorExit discipline), atomic temp+os.replace contract writes. The `--daemon` flag short-circuits with a structured error pointing at slice-3, which is the right deferral.\n- Idempotency invariant holds: my `test_driver_is_idempotent_when_answer_unchanged` invokes the driver twice without writing an answer in between, and the second invocation correctly yields the SAME preflight decision (no silent advance of the state machine without operator input).\n\n**TASK-1-9 (`integration_tests/regression/_agent_tool_fake.py`) \u2014 nested-dispatch fake:**\n- Import-guard works: the module rejects imports from outside `integration_tests`/`_agent_tool_fake`/`__main__` prefixes (lines 84-96).\n- `dispatch(parent_role, child_role, write_target)` end-to-end correct for R2: spawns the child subprocess with `EGG_AGENT_ROLE=child_role`, the child invokes `hook_entry.decide(...)` and writes JSON to stdout, parent JSON-decodes the verdict. My `test_pretooluse_hook_denies_nested_child_write` confirms parent=architect + child=tester writing `orchestrator/foo.py` resolves to `decision=block` with a `tester`-naming reason \u2014 proving the hook resolves the child's role correctly, not the parent's.\n- Cross-role probe (parent=coder, child=tester writing orchestrator/*) also denies with a tester-scoped reason \u2014 the negative-control case proves there is no parent-side fallback masking the child resolution.\n- No env leakage: `EGG_AGENT_ROLE` is passed via `subprocess.run(env=...)` rather than `os.environ[...]=`; my leak-guard test confirms the parent process's `EGG_AGENT_ROLE` stays unset after dispatch.\n- `DispatchResult` dataclass is a reasonable wrapper (vs. raw dict) \u2014 the `.decision` field IS the raw hook verdict, so the AC (\"returns the hook verdict\") is satisfied through that attribute.\n\n**TASK-1-6 (`orchestrator/substrate/__init__.py` loader update):**\n- `_ROLE_RUBRIC_SLICES` mapping + `_RUBRIC_LANDED_ROLES` frozenset is a clean declarative extension; future slices can extend the set without rewriting the loader.\n- `REFINER` regression case still passes (10/10 cases in my `test_rubric_loader.py` are green after the documenter's task-1-4 rubric .md files landed).\n- `REVIEWER_REFINE` / `REVIEWER_AGENT_DESIGN` load successfully \u2014 verified body contains the role name.\n- `ARCHITECT` raises `ValueError` with `follow-up slice-2 of issue #2717's rollout` diagnostic. Note: the AC literal wording was \"follow-up slice 2\" (space); coder shipped \"follow-up slice-2\" (hyphen). The intent is satisfied \u2014 my test accepts either form. Non-blocking.\n- `REVIEWER_PLAN` / `REVIEWER_CODE` / `TASK_PLANNER` still raise as expected (the structured-error fence preserves the slice-2/3 contract).\n- Path-traversal defence: `_load(\"../../../etc/passwd\")` resolves to a `ValueError` (\"not part of the #2717 rollout's rubric set\") \u2014 defence-in-depth holds.\n\n### Non-blocking\n\n- **`run_pipeline.py:343-352 `_is_aborted_status`**: The substring set `{\"abort\", \"stop\", \"cancel\"}` does not match what `orchestrator.substrate.in_process._answer_is_abort` actually treats as an abort. The two paths diverge \u2014 if the orchestrator widens its abort vocabulary, the driver's status-emission will silently mis-classify. Consider importing the orchestrator's helper directly so they cannot drift.\n- **`_agent_tool_fake.py:302-314`** \u2014 a child-subprocess crash returns a synthetic `decision: block` verdict. This is good for surfacing failures but means a test that only checks `denied == True` could pass on a subprocess crash. My tests defend against this by also asserting the deny reason contains `\"tester\"`, but downstream consumers of the fake should be aware.\n- **`run_pipeline.py:121-136 `_read_contract`** silently returns a default skeleton on `json.JSONDecodeError`. A corrupt contract is silently overwritten on the next `_persist_envelope`, losing forensic data. Not blocking for the walking-skeleton scope.\n\nThe proposal is structurally sound and the round-trip / nested-dispatch / loader behaviour all match the AC. The two tests in my `test_rubric_loader.py` that depended on the documenter's rubric .md files now pass after the documenter's proposal landed (commit 473b109).\n", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "\nReviewed the coder's three artifacts (run_pipeline.py, _agent_tool_fake.py, orchestrator/substrate/__init__.py) plus drove all three acceptance criteria end-to-end via the tester-side tests I committed alongside this ACK.\n\n### What I verified\n\n**TASK-1-1 (`bin/run_pipeline.py`) \u2014 bridge driver:**\n- Round-trip works end-to-end. My `test_bridge_flattened_round_trip` invokes the driver twice via `runpy` in a fresh subprocess each call. Stage A captures `pending_hitl.decision.question == \"Confirm the refiner will run against this repo + issue?\"` (the preflight); after I write `answer=\"approve\" + status=\"answered\"` to the contract, Stage B replays the answer through the `answer_log` mechanism and lands on the refine-gate decision (`decision_type=phase_gate`, distinct question). Exits 0 on each yield as the AC requires.\n- The `pending_hitl` envelope schema is documented as a stable contract at the top of `run_pipeline.py` (lines 20-46) \u2014 fields `version`, `pipeline_id`, `timestamp`, `decision`, `answer`, `status`, `result`, `error`, `answer_log`. The slice-3 daemon variant (TASK-3-2) inheriting this shape is correctly called out for R17 mitigation.\n- `_advance_generator` replay model is sound for a deterministic generator: closes the generator in `finally` (GeneratorExit discipline), atomic temp+os.replace contract writes. The `--daemon` flag short-circuits with a structured error pointing at slice-3, which is the right deferral.\n- Idempotency invariant holds: my `test_driver_is_idempotent_when_answer_unchanged` invokes the driver twice without writing an answer in between, and the second invocation correctly yields the SAME preflight decision (no silent advance of the state machine without operator input).\n\n**TASK-1-9 (`integration_tests/regression/_agent_tool_fake.py`) \u2014 nested-dispatch fake:**\n- Import-guard works: the module rejects imports from outside `integration_tests`/`_agent_tool_fake`/`__main__` prefixes (lines 84-96).\n- `dispatch(parent_role, child_role, write_target)` end-to-end correct for R2: spawns the child subprocess with `EGG_AGENT_ROLE=child_role`, the child invokes `hook_entry.decide(...)` and writes JSON to stdout, parent JSON-decodes the verdict. My `test_pretooluse_hook_denies_nested_child_write` confirms parent=architect + child=tester writing `orchestrator/foo.py` resolves to `decision=block` with a `tester`-naming reason \u2014 proving the hook resolves the child's role correctly, not the parent's.\n- Cross-role probe (parent=coder, child=tester writing orchestrator/*) also denies with a tester-scoped reason \u2014 the negative-control case proves there is no parent-side fallback masking the child resolution.\n- No env leakage: `EGG_AGENT_ROLE` is passed via `subprocess.run(env=...)` rather than `os.environ[...]=`; my leak-guard test confirms the parent process's `EGG_AGENT_ROLE` stays unset after dispatch.\n- `DispatchResult` dataclass is a reasonable wrapper (vs. raw dict) \u2014 the `.decision` field IS the raw hook verdict, so the AC (\"returns the hook verdict\") is satisfied through that attribute.\n\n**TASK-1-6 (`orchestrator/substrate/__init__.py` loader update):**\n- `_ROLE_RUBRIC_SLICES` mapping + `_RUBRIC_LANDED_ROLES` frozenset is a clean declarative extension; future slices can extend the set without rewriting the loader.\n- `REFINER` regression case still passes (10/10 cases in my `test_rubric_loader.py` are green after the documenter's task-1-4 rubric .md files landed).\n- `REVIEWER_REFINE` / `REVIEWER_AGENT_DESIGN` load successfully \u2014 verified body contains the role name.\n- `ARCHITECT` raises `ValueError` with `follow-up slice-2 of issue #2717's rollout` diagnostic. Note: the AC literal wording was \"follow-up slice 2\" (space); coder shipped \"follow-up slice-2\" (hyphen). The intent is satisfied \u2014 my test accepts either form. Non-blocking.\n- `REVIEWER_PLAN` / `REVIEWER_CODE` / `TASK_PLANNER` still raise as expected (the structured-error fence preserves the slice-2/3 contract).\n- Path-traversal defence: `_load(\"../../../etc/passwd\")` resolves to a `ValueError` (\"not part of the #2717 rollout's rubric set\") \u2014 defence-in-depth holds.\n\n### Non-blocking\n\n- **`run_pipeline.py:343-352 `_is_aborted_status`**: The substring set `{\"abort\", \"stop\", \"cancel\"}` does not match what `orchestrator.substrate.in_process._answer_is_abort` actually treats as an abort. The two paths diverge \u2014 if the orchestrator widens its abort vocabulary, the driver's status-emission will silently mis-classify. Consider importing the orchestrator's helper directly so they cannot drift.\n- **`_agent_tool_fake.py:302-314`** \u2014 a child-subprocess crash returns a synthetic `decision: block` verdict. This is good for surfacing failures but means a test that only checks `denied == True` could pass on a subprocess crash. My tests defend against this by also asserting the deny reason contains `\"tester\"`, but downstream consumers of the fake should be aware.\n- **`run_pipeline.py:121-136 `_read_contract`** silently returns a default skeleton on `json.JSONDecodeError`. A corrupt contract is silently overwritten on the next `_persist_envelope`, losing forensic data. Not blocking for the walking-skeleton scope.\n\nThe proposal is structurally sound and the round-trip / nested-dispatch / loader behaviour all match the AC. The two tests in my `test_rubric_loader.py` that depended on the documenter's rubric .md files now pass after the documenter's proposal landed (commit 473b109).\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:05:07.041672+00:00", - "phase": "implement" - }, - { - "id": "29817464-1b7e-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:05:07.091369+00:00", - "phase": "implement" - }, - { - "id": "a5c28f96-1721-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:04:09.316695+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:05:09.448301+00:00", - "phase": "implement" - }, - { - "id": "488b7d93-c92e-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:05:12.574649+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:05:12.644871+00:00", - "phase": "implement" - }, - { - "id": "47913044-9592-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:05:53.838393+00:00", - "phase": "implement" - }, - { - "id": "f487ac12-2bd4-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:05:59.820379+00:00", - "phase": "implement" - }, - { - "id": "d3cd4964-456e-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:04.160791+00:00", - "phase": "implement" - }, - { - "id": "4f1bd5f8-9933-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:04:09.316695+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:09.506374+00:00", - "phase": "implement" - }, - { - "id": "f1758e17-c439-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:05:12.574649+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:12.699251+00:00", - "phase": "implement" - }, - { - "id": "8d7b9ac9-c7b3-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code_holistic for coder", - "body": "\nHolistic review of coder v1 (commit fddeb82b) \u2014 ran all four mandatory passes against the three files: `run_pipeline.py` (new, 529 LOC), `_agent_tool_fake.py` (new, 451 LOC), `substrate/__init__.py` (+100/-10 for `_load_egg_sdlc_role_rubric` rework). Tracing the primary advertised use case \u2014 skill body invokes the driver in a loop, ferries `pending_hitl.decision` \u2194 `pending_hitl.answer` \u2194 `pending_hitl.answer_log` across process exits \u2014 the producer/consumer round-trip is internally consistent: `_advance_generator` starts a fresh generator, `next()` lands on yield #1, the replay loop calls `send(replay)` for each historical answer in `answer_log`, the post-replay yield is what gets persisted, and `StopIteration` / `_PreflightAborted` (translated to `StopIteration` by `_InProcessOrchestrator.run`) is caught and converted to `status=\"completed\"` / `status=\"aborted\"` per `_is_aborted_status`. The R17 mitigation is structurally intact (the `_agent_tool_fake` test helper re-exports `PENDING_HITL_SCHEMA_VERSION` from the driver module and `build_pending_hitl_envelope` agrees on the 9-field shape including `answer_log`). The R2 hook-logic fake is correctly scope-fenced: import-guarded against production callers (line 84-96), env-propagation of `EGG_AGENT_ROLE` mirrors what real Agent-tool dispatch would set, and the docstring honestly names the empirical-vs-test-fake limitation. The loader's `_ROLE_RUBRIC_SLICES` / `_RUBRIC_LANDED_ROLES` split is structured: roles outside the slice's rubric-supported set raise with a slice-N pointer, roles inside but missing on disk raise with a \"documenter task is still in flight\" pointer \u2014 the cross-task sequence (task-1-4 documenter \u2194 task-1-6 coder) is handled gracefully even mid-slice. No CRITICAL cross-module asymmetry on the primary refine round-trip; ACKing.\n\n### Non-blocking\n\n1. **`run_pipeline.py:24-46` \u2014 STABLE-contract schema block omits `answer_log`.** The top-of-file `pending_hitl` schema documented under \"STABLE contract \u2014 slice-3 daemon inherits this shape; do NOT change field names/types without bumping `version`\" enumerates 8 fields (`version, pipeline_id, timestamp, decision, answer, status, result, error`) but the actual envelope persists a 9th \u2014 `answer_log: list[Any]` \u2014 that is **load-bearing** for the cross-process generator-state replay. The \"Generator state across invocations\" section later in the same docstring (lines 53-69) does mention `answer_log`, and `_coerce_envelope` (line 225) explicitly defaults it to `[]` so older / forward-compat envelopes don't break, so the implementations agree. But the AC for task-1-1 says the schema must be \"documented as a stable contract (top-of-file comment listing the contract fields)\" and the explicitly-labelled STABLE block is incomplete. The slice-3 daemon (TASK-3-2) implementer who copies just the STABLE block would miss the field that risk_analyst R17 mitigation actually depends on. **Fix:** add an `answer_log: list[Any]` line to the schema block with a one-line note (`# replayed answer history; the driver appends pending answers here on each invocation and feeds them into a fresh generator via send()`).\n\n2. **`run_pipeline.py:454` \u2014 `status == \"answered\"` synthetic-key coordination is fragile.** The driver promotes `envelope.answer \u2192 answer_log` only when `envelope.status == \"answered\"` AND `envelope.answer is not None`. If documenter task-1-2's SKILL.md ferries a render result back by writing only `pending_hitl.answer = X` (and leaving status at the driver's last-written `\"pending\"`), the answer is silently dropped on the next invocation \u2014 the replay loop sees an unchanged `answer_log`, the generator re-yields the same decision, and the user-visible failure shape is \"the loop doesn't advance, no error printed.\" This is the same architectural shape as the `__checkout__` synthetic-key dead-end on PR #2105 \u2014 producer emits a value, consumer's filter excludes it, no error. Since the skill body and the driver are serialized by the shell loop (no concurrent-writer race needs guarding), the `status == \"answered\"` two-phase signal is defensive overhead, not load-bearing. **Fix (preferred):** drop the `status == \"answered\"` predicate and auto-promote whenever `envelope.answer is not None`, then have the driver write `envelope.status = \"pending\"` (or \"completed\"/\"aborted\"/\"error\") authoritatively from its own state machine. **Fix (alternative):** leave the strict check but assert in the driver that `envelope.answer is None or envelope.status == \"answered\"` \u2014 a stuck answer with the wrong status should be a hard error, not a silent drop. I'm not blocking on this because the documenter would naturally read the schema docstring (lines 32-43, which is clear that both fields must be set) and the documenter's task-1-2 is being reviewed by `reviewer_code` / `reviewer_contract`, but the coordination point is exactly the holistic-lens canonical miss shape and warrants flagging.\n\n3. **`run_pipeline.py:127-136` \u2014 `_read_contract` silently resets on corrupt JSON.** When the contract file exists but `json.loads(...)` raises `OSError` or `json.JSONDecodeError`, the driver returns a fresh skeleton (`{\"schemaVersion\": \"1.1\", \"pipeline_id\": pipeline_id, \"current_phase\": \"refine\", \"decisions\": []}`) and proceeds. The subsequent `_coerce_envelope(contract.get(\"pending_hitl\"), ...)` sees a missing `pending_hitl` field and returns a fresh `_new_envelope`. Net effect: a partially-written or corrupt contract silently wipes the operator's `answer_log` \u2014 the operator sees the preflight decision pop up again with no diagnostic. The atomic temp-then-os.replace write discipline makes corruption rare in the happy path, but a concurrent-writer crash, full disk, or out-of-band edit would trigger this. **Fix:** when the file exists but unparseable, log a clear stderr warning naming the path and the parse error, and either back up the corrupt file (`.corrupt.`) or refuse to overwrite (exit 1 with a clear error) so the operator can inspect.\n\n4. **`run_pipeline.py:200-210` \u2014 `_coerce_envelope` silently resets when raw is non-dict.** Same shape as #3 but at the envelope layer: if `contract[\"pending_hitl\"]` is somehow not a dict (legacy shape, manual edit, partial overwrite), the driver silently resets to a fresh envelope and loses `answer_log`. **Fix:** treat this as the corrupt-envelope path \u2014 surface a stderr warning with the bad type/value before falling back.\n\n5. **`run_pipeline.py:487 + orchestrator/substrate/in_process.py:807-826` \u2014 `_maybe_fence` `NotImplementedError` produces `status=\"error\"`, not a clean fence signal.** When the operator chooses `approve_continue` at the refine HITL gate, `_InProcessOrchestrator._maybe_fence` raises `NotImplementedError` as the documented walking-skeleton scope fence \u2014 but the driver's top-level `except Exception` catches it, populates `envelope.error` with the traceback, and writes `status=\"error\"`. The user experiences \"the loop says error\" on what is documented as the expected fence behavior of `approve_continue`. This is reachable on the primary advertised happy path (refine \u2192 continue to plan). **Fix:** in `_advance_generator` or `main()`, special-case `NotImplementedError` from `generator.send(...)` and translate to `status=\"completed\"` with the fence message in `result` (or introduce a `status=\"fenced\"` discriminator so the skill body can surface a friendlier message). For slice-1 this will be transient \u2014 slices 2-5 will replace the NotImplementedError with real plan/implement/pr paths \u2014 but slice-1 ships a refine-only bridge today, and the documented happy path produces a misleading error envelope.\n\n6. **`run_pipeline.py:322-327` \u2014 `generator.close()` `except Exception: pass`** swallows GeneratorExit propagation errors silently. Documented as defensive but could hide background-thread join failures. Worth at least a `logger.debug(...)` (or stderr trace under `EGG_DEBUG=1`) rather than a bare swallow.\n\n7. **`integration_tests/regression/_agent_tool_fake.py:411-441` \u2014 `build_pending_hitl_envelope` is a public-shaped helper but is not used by the test under task-1-5 (which the tester hasn't proposed yet).** If the tester's `test_pretooluse_hook_nested.py` doesn't actually round-trip an envelope through the fake, this helper is dead code in the slice. Will re-check once the tester proposes; if it's actually consumed by R17-validation tests in slice-3, leave a `# used by slice-3 tests` pointer to avoid the dead-code reading.\n\n8. **`_agent_tool_fake.py:84-96` import guard relies on `__name__.startswith(\"integration_tests\")` / `\"_agent_tool_fake\"` / `\"__main__\"`** \u2014 a future test module named e.g. `integration_tests_extras._agent_tool_fake_wrapper` would slip past the guard. Tight today, but the prefix-match (vs. exact-match) is slightly looser than the docstring claim \"test infrastructure only \u2014 it must not be imported by production code.\" Non-blocking; the looseness only matters if a future test tree adopts a `integration_tests*`-shaped name outside `integration_tests/`.\n\n9. **`substrate/__init__.py:271-277` \u2014 `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` and `reviewer_agent_design` ahead of the documenter's task-1-4 actually landing those rubric .md files.** Today on the slice-1 branch only `agents/refiner.md` exists. The \"rubric missing on disk\" branch (line 352-364) handles this with a clear structured error, and the cross-task sequencing is called out in the comment, so the failure mode is graceful \u2014 but a reader who treats `_RUBRIC_LANDED_ROLES` as ground truth (e.g., for a TODO checklist) would be misled. Either rename the constant (`_RUBRIC_TARGETED_ROLES` to make \"intended for this slice but may not be on disk yet\" explicit) or drop reviewer_refine/reviewer_agent_design from the set until task-1-4 lands and have the documenter add them back in their proposal.\n", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "\nHolistic review of coder v1 (commit fddeb82b) \u2014 ran all four mandatory passes against the three files: `run_pipeline.py` (new, 529 LOC), `_agent_tool_fake.py` (new, 451 LOC), `substrate/__init__.py` (+100/-10 for `_load_egg_sdlc_role_rubric` rework). Tracing the primary advertised use case \u2014 skill body invokes the driver in a loop, ferries `pending_hitl.decision` \u2194 `pending_hitl.answer` \u2194 `pending_hitl.answer_log` across process exits \u2014 the producer/consumer round-trip is internally consistent: `_advance_generator` starts a fresh generator, `next()` lands on yield #1, the replay loop calls `send(replay)` for each historical answer in `answer_log`, the post-replay yield is what gets persisted, and `StopIteration` / `_PreflightAborted` (translated to `StopIteration` by `_InProcessOrchestrator.run`) is caught and converted to `status=\"completed\"` / `status=\"aborted\"` per `_is_aborted_status`. The R17 mitigation is structurally intact (the `_agent_tool_fake` test helper re-exports `PENDING_HITL_SCHEMA_VERSION` from the driver module and `build_pending_hitl_envelope` agrees on the 9-field shape including `answer_log`). The R2 hook-logic fake is correctly scope-fenced: import-guarded against production callers (line 84-96), env-propagation of `EGG_AGENT_ROLE` mirrors what real Agent-tool dispatch would set, and the docstring honestly names the empirical-vs-test-fake limitation. The loader's `_ROLE_RUBRIC_SLICES` / `_RUBRIC_LANDED_ROLES` split is structured: roles outside the slice's rubric-supported set raise with a slice-N pointer, roles inside but missing on disk raise with a \"documenter task is still in flight\" pointer \u2014 the cross-task sequence (task-1-4 documenter \u2194 task-1-6 coder) is handled gracefully even mid-slice. No CRITICAL cross-module asymmetry on the primary refine round-trip; ACKing.\n\n### Non-blocking\n\n1. **`run_pipeline.py:24-46` \u2014 STABLE-contract schema block omits `answer_log`.** The top-of-file `pending_hitl` schema documented under \"STABLE contract \u2014 slice-3 daemon inherits this shape; do NOT change field names/types without bumping `version`\" enumerates 8 fields (`version, pipeline_id, timestamp, decision, answer, status, result, error`) but the actual envelope persists a 9th \u2014 `answer_log: list[Any]` \u2014 that is **load-bearing** for the cross-process generator-state replay. The \"Generator state across invocations\" section later in the same docstring (lines 53-69) does mention `answer_log`, and `_coerce_envelope` (line 225) explicitly defaults it to `[]` so older / forward-compat envelopes don't break, so the implementations agree. But the AC for task-1-1 says the schema must be \"documented as a stable contract (top-of-file comment listing the contract fields)\" and the explicitly-labelled STABLE block is incomplete. The slice-3 daemon (TASK-3-2) implementer who copies just the STABLE block would miss the field that risk_analyst R17 mitigation actually depends on. **Fix:** add an `answer_log: list[Any]` line to the schema block with a one-line note (`# replayed answer history; the driver appends pending answers here on each invocation and feeds them into a fresh generator via send()`).\n\n2. **`run_pipeline.py:454` \u2014 `status == \"answered\"` synthetic-key coordination is fragile.** The driver promotes `envelope.answer \u2192 answer_log` only when `envelope.status == \"answered\"` AND `envelope.answer is not None`. If documenter task-1-2's SKILL.md ferries a render result back by writing only `pending_hitl.answer = X` (and leaving status at the driver's last-written `\"pending\"`), the answer is silently dropped on the next invocation \u2014 the replay loop sees an unchanged `answer_log`, the generator re-yields the same decision, and the user-visible failure shape is \"the loop doesn't advance, no error printed.\" This is the same architectural shape as the `__checkout__` synthetic-key dead-end on PR #2105 \u2014 producer emits a value, consumer's filter excludes it, no error. Since the skill body and the driver are serialized by the shell loop (no concurrent-writer race needs guarding), the `status == \"answered\"` two-phase signal is defensive overhead, not load-bearing. **Fix (preferred):** drop the `status == \"answered\"` predicate and auto-promote whenever `envelope.answer is not None`, then have the driver write `envelope.status = \"pending\"` (or \"completed\"/\"aborted\"/\"error\") authoritatively from its own state machine. **Fix (alternative):** leave the strict check but assert in the driver that `envelope.answer is None or envelope.status == \"answered\"` \u2014 a stuck answer with the wrong status should be a hard error, not a silent drop. I'm not blocking on this because the documenter would naturally read the schema docstring (lines 32-43, which is clear that both fields must be set) and the documenter's task-1-2 is being reviewed by `reviewer_code` / `reviewer_contract`, but the coordination point is exactly the holistic-lens canonical miss shape and warrants flagging.\n\n3. **`run_pipeline.py:127-136` \u2014 `_read_contract` silently resets on corrupt JSON.** When the contract file exists but `json.loads(...)` raises `OSError` or `json.JSONDecodeError`, the driver returns a fresh skeleton (`{\"schemaVersion\": \"1.1\", \"pipeline_id\": pipeline_id, \"current_phase\": \"refine\", \"decisions\": []}`) and proceeds. The subsequent `_coerce_envelope(contract.get(\"pending_hitl\"), ...)` sees a missing `pending_hitl` field and returns a fresh `_new_envelope`. Net effect: a partially-written or corrupt contract silently wipes the operator's `answer_log` \u2014 the operator sees the preflight decision pop up again with no diagnostic. The atomic temp-then-os.replace write discipline makes corruption rare in the happy path, but a concurrent-writer crash, full disk, or out-of-band edit would trigger this. **Fix:** when the file exists but unparseable, log a clear stderr warning naming the path and the parse error, and either back up the corrupt file (`.corrupt.`) or refuse to overwrite (exit 1 with a clear error) so the operator can inspect.\n\n4. **`run_pipeline.py:200-210` \u2014 `_coerce_envelope` silently resets when raw is non-dict.** Same shape as #3 but at the envelope layer: if `contract[\"pending_hitl\"]` is somehow not a dict (legacy shape, manual edit, partial overwrite), the driver silently resets to a fresh envelope and loses `answer_log`. **Fix:** treat this as the corrupt-envelope path \u2014 surface a stderr warning with the bad type/value before falling back.\n\n5. **`run_pipeline.py:487 + orchestrator/substrate/in_process.py:807-826` \u2014 `_maybe_fence` `NotImplementedError` produces `status=\"error\"`, not a clean fence signal.** When the operator chooses `approve_continue` at the refine HITL gate, `_InProcessOrchestrator._maybe_fence` raises `NotImplementedError` as the documented walking-skeleton scope fence \u2014 but the driver's top-level `except Exception` catches it, populates `envelope.error` with the traceback, and writes `status=\"error\"`. The user experiences \"the loop says error\" on what is documented as the expected fence behavior of `approve_continue`. This is reachable on the primary advertised happy path (refine \u2192 continue to plan). **Fix:** in `_advance_generator` or `main()`, special-case `NotImplementedError` from `generator.send(...)` and translate to `status=\"completed\"` with the fence message in `result` (or introduce a `status=\"fenced\"` discriminator so the skill body can surface a friendlier message). For slice-1 this will be transient \u2014 slices 2-5 will replace the NotImplementedError with real plan/implement/pr paths \u2014 but slice-1 ships a refine-only bridge today, and the documented happy path produces a misleading error envelope.\n\n6. **`run_pipeline.py:322-327` \u2014 `generator.close()` `except Exception: pass`** swallows GeneratorExit propagation errors silently. Documented as defensive but could hide background-thread join failures. Worth at least a `logger.debug(...)` (or stderr trace under `EGG_DEBUG=1`) rather than a bare swallow.\n\n7. **`integration_tests/regression/_agent_tool_fake.py:411-441` \u2014 `build_pending_hitl_envelope` is a public-shaped helper but is not used by the test under task-1-5 (which the tester hasn't proposed yet).** If the tester's `test_pretooluse_hook_nested.py` doesn't actually round-trip an envelope through the fake, this helper is dead code in the slice. Will re-check once the tester proposes; if it's actually consumed by R17-validation tests in slice-3, leave a `# used by slice-3 tests` pointer to avoid the dead-code reading.\n\n8. **`_agent_tool_fake.py:84-96` import guard relies on `__name__.startswith(\"integration_tests\")` / `\"_agent_tool_fake\"` / `\"__main__\"`** \u2014 a future test module named e.g. `integration_tests_extras._agent_tool_fake_wrapper` would slip past the guard. Tight today, but the prefix-match (vs. exact-match) is slightly looser than the docstring claim \"test infrastructure only \u2014 it must not be imported by production code.\" Non-blocking; the looseness only matters if a future test tree adopts a `integration_tests*`-shaped name outside `integration_tests/`.\n\n9. **`substrate/__init__.py:271-277` \u2014 `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` and `reviewer_agent_design` ahead of the documenter's task-1-4 actually landing those rubric .md files.** Today on the slice-1 branch only `agents/refiner.md` exists. The \"rubric missing on disk\" branch (line 352-364) handles this with a clear structured error, and the cross-task sequencing is called out in the comment, so the failure mode is graceful \u2014 but a reader who treats `_RUBRIC_LANDED_ROLES` as ground truth (e.g., for a TODO checklist) would be misled. Either rename the constant (`_RUBRIC_TARGETED_ROLES` to make \"intended for this slice but may not be on disk yet\" explicit) or drop reviewer_refine/reviewer_agent_design from the set until task-1-4 lands and have the documenter add them back in their proposal.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:19.357270+00:00", - "phase": "implement" - }, - { - "id": "0f314031-ab6b-45", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:19.413527+00:00", - "phase": "implement" - }, - { - "id": "c2a2cfa7-18ec-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:25.335654+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:25.361341+00:00", - "phase": "implement" - }, - { - "id": "b0899694-3474-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:28.242194+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:28.295866+00:00", - "phase": "implement" - }, - { - "id": "7819a381-a22f-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:28.363917+00:00", - "phase": "implement" - }, - { - "id": "d4f1b4d3-ae1b-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:38.316246+00:00", - "phase": "implement" - }, - { - "id": "ece175eb-b953-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:53.899293+00:00", - "phase": "implement" - }, - { - "id": "ecc3d772-700f-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:06:59.872931+00:00", - "phase": "implement" - }, - { - "id": "e0ba256e-3637-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:04.334944+00:00", - "phase": "implement" - }, - { - "id": "5a13fe44-ed4f-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:04:09.316695+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:09.756365+00:00", - "phase": "implement" - }, - { - "id": "1e438367-6a80-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:25.335654+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:25.459359+00:00", - "phase": "implement" - }, - { - "id": "cf0963f1-db8d-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for coder", - "body": "\nReviewed all 3 files from commit fddeb82bf (slice-1 coder: bridge driver + R2 fake + loader expansion) and traced data flow end-to-end against the contract acceptance criteria and the cq-1 Option C bridge design. Existing claude_code substrate test suite (`shared/tests/test_claude_code_spawner.py`, `test_pretooluse_hook_policy.py`, `test_run_pipeline_in_process*.py`, `test_substrate_interfaces.py`) passes (54 passed, 4 skipped). Acceptance criteria for TASK-1-1, TASK-1-6, and TASK-1-9 are met. The R2-fake import-guard fires correctly when imported under a non-test name. No security vulnerabilities, no resource leaks, no crashes.\n\nI am ACKing rather than NACKing because the contract's literal task acceptance is satisfied and the per-file logic is correct, but several substantive design / robustness concerns surfaced in tracing the cross-module data flow. They are listed below as non-blocking; please consider addressing the top three in this PR (cheap docstring / control-flow fixes), and at minimum file follow-ups for the rest before slice-3's daemon variant inherits the same envelope schema.\n\n### Non-blocking\n\n1. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:258-327` \u2014 replay strategy re-executes all side effects on every invocation.** The docstring (lines 53-69) calls `run_pipeline_in_process` \"deterministic\", but the determinism only applies to *which yield boundary the generator reaches*. The side effects between yields \u2014 most importantly `_spawn_refiner()` in `orchestrator/substrate/in_process.py:617` \u2014 are NOT idempotent: each replay invocation creates a worktree, dispatches a real Claude Code Agent (LLM cost), writes/overwrites `.egg-state/drafts/-analysis.md`, then tears the worktree down again. The concrete operator-visible consequence in the refine-only spike:\n - I2 (after preflight answer): refiner spawns once, refine_gate yielded, operator approves based on I2's artifact content.\n - I3 (after gate \"approve_continue\" answer): driver replays preflight \u2192 `_spawn_refiner()` runs AGAIN before `_maybe_fence` raises NotImplementedError. The artifact-on-disk is now potentially DIFFERENT content from what the operator approved (LLM non-determinism), and the operator paid for an extra Claude Code Agent dispatch they cannot see.\n This is a structural mismatch with cq-1 Option B's literal description (\"Flatten generator into a hand-shaped sequence of single-yield `python3 .py` invocations\" \u2014 i.e. *separate stage scripts*, not one driver that replays from scratch). The task-1-1 description is what the coder followed; the architectural concern is upstream. Cheapest fix: add a 3-line idempotency check in `_spawn_refiner` (skip the `bundle.spawner.spawn(...)` when `artifact_path.exists()` AND content is non-placeholder). Alternatively, update the driver docstring lines 64-69 to honestly state \"each replay re-runs all side effects between yields, including the refiner subagent dispatch \u2014 operators using this against real Anthropic credentials incur 2x refiner cost per approved refine cycle.\" Today the docstring is misleading; future maintainers will assume \"deterministic\" means \"free to replay\".\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:434-447` \u2014 `--daemon` short-circuit wipes `answer_log` AND uses exit code 1.** The argparse help text (lines 391-401) advertises `--daemon` as exiting with \"a structured error so the skill can fall back to the flattened path\", but: (a) the constructed envelope (line 435) calls `_new_envelope(...)` WITHOUT `answer_log=envelope[\"answer_log\"]`, so any operator answers already accumulated are silently destroyed; (b) the exit code is 1, which the module docstring (lines 71-77) defines as \"internal error\" \u2014 the skill body has no way to differentiate \"daemon path not yet implemented\" from a real driver crash. Fix: preserve `answer_log` on the daemon path and either align the docstring with the actual exit semantics or pick a distinct status string (e.g. `\"daemon_unavailable\"`) so the skill body can branch.\n\n3. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:423-429` \u2014 envelope-coerce error path also wipes `answer_log`.** Same shape as finding #2: when `_coerce_envelope` raises `ValueError` (envelope version newer than driver supports), the replacement envelope is built without `answer_log=...`, losing the operator's history. This is the path future slice-3 / version-bump scenarios will exercise. Preserve `answer_log` even on coerce error \u2014 the new envelope is a diagnostic for the operator, not a state reset.\n\n4. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:113-115` \u2014 no path-traversal validation on `pipeline_id`.** `_contract_path(state_root, pipeline_id)` returns `contracts / f\"{pipeline_id}.json\"` with no validation. A pipeline_id of `../../foo` writes outside the contracts dir. SKILL.md explicitly cites \"Path-escape safety mirrors the existing `is_relative_to` + `resolve()` defense in the gateway\" \u2014 the driver does not implement that defense. Add a `re.fullmatch(r\"^[A-Za-z0-9._-]+$\", pipeline_id)` validation (or equivalent `is_relative_to(contracts)` check after resolution). Local-trust scope makes this defense-in-depth rather than load-bearing, but the SKILL.md claim is currently false.\n\n5. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:297, 320` \u2014 dead `next_answer` variable.** `_advance_generator` initialises `next_answer: Any = None` and returns it in all three exit paths, never updating it. Always `None`. Either remove from the tuple shape or wire it correctly to the next pending answer the skill should ferry. Currently dead code that misleads readers about the function's return contract.\n\n6. **`integration_tests/regression/_agent_tool_fake.py:404` \u2014 `DispatchResult.child_pid` is always 0.** `dispatch(...)` reads `int(verdict.get(\"_fake_child_pid\", 0) or 0)`, but `pre_tool_use_callback` (lines 222-333) never sets `_fake_child_pid` in the returned dict \u2014 only `_fake_child_exit_code` and `_fake_child_stderr`. The PID is unavailable post-exit through `subprocess.run`. Either remove the field from `DispatchResult` or refactor to `subprocess.Popen` and capture `.pid`.\n\n7. **`integration_tests/regression/_agent_tool_fake.py:84-96` \u2014 import guard is more permissive than the acceptance criterion.** Task-1-9 acceptance says `if not __name__.startswith(\"integration_tests\")`; the implementation uses three prefixes including `\"_agent_tool_fake\"`, which also matches sibling modules like `_agent_tool_fake_helper.py` (verified). Use an exact-match `set` rather than a prefix tuple, or tighten the prefix to the exact module name `\"_agent_tool_fake\"` followed by a sentinel.\n\n8. **`integration_tests/regression/_agent_tool_fake.py:122-126` \u2014 silent version fallback.** If the driver bumps `PENDING_HITL_SCHEMA_VERSION` to 2 but the path-walk import fails (e.g., plugin layout changes per the TODO at `orchestrator/substrate/__init__.py:319-328`), the fake silently downgrades the constant to 1. Tests using the fake's constant would miss the bump. Either propagate the ImportError (fail loudly) or log a clear warning to stderr.\n\n9. **`integration_tests/regression/_agent_tool_fake.py:266` \u2014 child subprocess inherits full parent env**, including `ANTHROPIC_API_KEY`. The child only invokes `hook_entry.decide()` and does not network, so impact is minimal \u2014 but defense-in-depth would whitelist only `EGG_*`, `HOME`, `PATH`, `PYTHONPATH`. Non-blocking; the child's tool-call surface is restricted by the test-only scope.\n\n10. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:487-496` \u2014 traceback written verbatim to the contract file.** `traceback.format_exc(limit=8)` is serialised into `pending_hitl.error`, which is then JSON-serialised and read by the skill body. Could expose absolute filesystem paths in the contract artifact. Local-trust scope OK; consider truncating or stripping the path prefix before serialising in production.\n\n11. **`orchestrator/substrate/__init__.py:271-277` \u2014 `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` / `reviewer_agent_design` before the documenter (task-1-4) has landed the rubric markdown files in the same slice.** The behaviour is graceful \u2014 `_load_egg_sdlc_role_rubric` raises a clear \"rubric missing on disk \u2026 sequence the documenter's rubric task before the loader update\" ValueError \u2014 so the acceptance criterion's `_load_egg_sdlc_role_rubric(REVIEWER_REFINE) returns the markdown body of reviewer_refine.md` will only be satisfied after documenter merges. This is the expected concurrent-BRC dependency; calling out for the re-review when documenter ACKs.\n\n### Acceptance criteria check\n\n- **TASK-1-1**: `bin/run_pipeline.py` exists at the correct path; imports `run_pipeline_in_process`; round-trips `HITLDecision` through `pending_hitl`; exit codes 0/1 per docstring; `pending_hitl` envelope schema documented at top-of-file as stable contract. \u2713\n- **TASK-1-9**: `_agent_tool_fake.py` exists; exposes `dispatch(parent_role, child_role, write_target)`; helper invokes `hook_entry.decide(...)` via `pre_tool_use_callback`; module docstring marks it test-only; import guard present (with broader prefix than spec'd \u2014 finding #7). \u2713 (minor deviation)\n- **TASK-1-6**: `reviewer_refine` and `reviewer_agent_design` accepted by the loader (returns markdown once TASK-1-4 lands); `architect` raises ValueError with \"deferred to follow-up slice-2 of issue #2717's rollout\" (matches acceptance's \"follow-up slice 2\" hint). \u2713\n", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py", - "integration_tests/regression/_agent_tool_fake.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "\nReviewed all 3 files from commit fddeb82bf (slice-1 coder: bridge driver + R2 fake + loader expansion) and traced data flow end-to-end against the contract acceptance criteria and the cq-1 Option C bridge design. Existing claude_code substrate test suite (`shared/tests/test_claude_code_spawner.py`, `test_pretooluse_hook_policy.py`, `test_run_pipeline_in_process*.py`, `test_substrate_interfaces.py`) passes (54 passed, 4 skipped). Acceptance criteria for TASK-1-1, TASK-1-6, and TASK-1-9 are met. The R2-fake import-guard fires correctly when imported under a non-test name. No security vulnerabilities, no resource leaks, no crashes.\n\nI am ACKing rather than NACKing because the contract's literal task acceptance is satisfied and the per-file logic is correct, but several substantive design / robustness concerns surfaced in tracing the cross-module data flow. They are listed below as non-blocking; please consider addressing the top three in this PR (cheap docstring / control-flow fixes), and at minimum file follow-ups for the rest before slice-3's daemon variant inherits the same envelope schema.\n\n### Non-blocking\n\n1. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:258-327` \u2014 replay strategy re-executes all side effects on every invocation.** The docstring (lines 53-69) calls `run_pipeline_in_process` \"deterministic\", but the determinism only applies to *which yield boundary the generator reaches*. The side effects between yields \u2014 most importantly `_spawn_refiner()` in `orchestrator/substrate/in_process.py:617` \u2014 are NOT idempotent: each replay invocation creates a worktree, dispatches a real Claude Code Agent (LLM cost), writes/overwrites `.egg-state/drafts/-analysis.md`, then tears the worktree down again. The concrete operator-visible consequence in the refine-only spike:\n - I2 (after preflight answer): refiner spawns once, refine_gate yielded, operator approves based on I2's artifact content.\n - I3 (after gate \"approve_continue\" answer): driver replays preflight \u2192 `_spawn_refiner()` runs AGAIN before `_maybe_fence` raises NotImplementedError. The artifact-on-disk is now potentially DIFFERENT content from what the operator approved (LLM non-determinism), and the operator paid for an extra Claude Code Agent dispatch they cannot see.\n This is a structural mismatch with cq-1 Option B's literal description (\"Flatten generator into a hand-shaped sequence of single-yield `python3 .py` invocations\" \u2014 i.e. *separate stage scripts*, not one driver that replays from scratch). The task-1-1 description is what the coder followed; the architectural concern is upstream. Cheapest fix: add a 3-line idempotency check in `_spawn_refiner` (skip the `bundle.spawner.spawn(...)` when `artifact_path.exists()` AND content is non-placeholder). Alternatively, update the driver docstring lines 64-69 to honestly state \"each replay re-runs all side effects between yields, including the refiner subagent dispatch \u2014 operators using this against real Anthropic credentials incur 2x refiner cost per approved refine cycle.\" Today the docstring is misleading; future maintainers will assume \"deterministic\" means \"free to replay\".\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:434-447` \u2014 `--daemon` short-circuit wipes `answer_log` AND uses exit code 1.** The argparse help text (lines 391-401) advertises `--daemon` as exiting with \"a structured error so the skill can fall back to the flattened path\", but: (a) the constructed envelope (line 435) calls `_new_envelope(...)` WITHOUT `answer_log=envelope[\"answer_log\"]`, so any operator answers already accumulated are silently destroyed; (b) the exit code is 1, which the module docstring (lines 71-77) defines as \"internal error\" \u2014 the skill body has no way to differentiate \"daemon path not yet implemented\" from a real driver crash. Fix: preserve `answer_log` on the daemon path and either align the docstring with the actual exit semantics or pick a distinct status string (e.g. `\"daemon_unavailable\"`) so the skill body can branch.\n\n3. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:423-429` \u2014 envelope-coerce error path also wipes `answer_log`.** Same shape as finding #2: when `_coerce_envelope` raises `ValueError` (envelope version newer than driver supports), the replacement envelope is built without `answer_log=...`, losing the operator's history. This is the path future slice-3 / version-bump scenarios will exercise. Preserve `answer_log` even on coerce error \u2014 the new envelope is a diagnostic for the operator, not a state reset.\n\n4. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:113-115` \u2014 no path-traversal validation on `pipeline_id`.** `_contract_path(state_root, pipeline_id)` returns `contracts / f\"{pipeline_id}.json\"` with no validation. A pipeline_id of `../../foo` writes outside the contracts dir. SKILL.md explicitly cites \"Path-escape safety mirrors the existing `is_relative_to` + `resolve()` defense in the gateway\" \u2014 the driver does not implement that defense. Add a `re.fullmatch(r\"^[A-Za-z0-9._-]+$\", pipeline_id)` validation (or equivalent `is_relative_to(contracts)` check after resolution). Local-trust scope makes this defense-in-depth rather than load-bearing, but the SKILL.md claim is currently false.\n\n5. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:297, 320` \u2014 dead `next_answer` variable.** `_advance_generator` initialises `next_answer: Any = None` and returns it in all three exit paths, never updating it. Always `None`. Either remove from the tuple shape or wire it correctly to the next pending answer the skill should ferry. Currently dead code that misleads readers about the function's return contract.\n\n6. **`integration_tests/regression/_agent_tool_fake.py:404` \u2014 `DispatchResult.child_pid` is always 0.** `dispatch(...)` reads `int(verdict.get(\"_fake_child_pid\", 0) or 0)`, but `pre_tool_use_callback` (lines 222-333) never sets `_fake_child_pid` in the returned dict \u2014 only `_fake_child_exit_code` and `_fake_child_stderr`. The PID is unavailable post-exit through `subprocess.run`. Either remove the field from `DispatchResult` or refactor to `subprocess.Popen` and capture `.pid`.\n\n7. **`integration_tests/regression/_agent_tool_fake.py:84-96` \u2014 import guard is more permissive than the acceptance criterion.** Task-1-9 acceptance says `if not __name__.startswith(\"integration_tests\")`; the implementation uses three prefixes including `\"_agent_tool_fake\"`, which also matches sibling modules like `_agent_tool_fake_helper.py` (verified). Use an exact-match `set` rather than a prefix tuple, or tighten the prefix to the exact module name `\"_agent_tool_fake\"` followed by a sentinel.\n\n8. **`integration_tests/regression/_agent_tool_fake.py:122-126` \u2014 silent version fallback.** If the driver bumps `PENDING_HITL_SCHEMA_VERSION` to 2 but the path-walk import fails (e.g., plugin layout changes per the TODO at `orchestrator/substrate/__init__.py:319-328`), the fake silently downgrades the constant to 1. Tests using the fake's constant would miss the bump. Either propagate the ImportError (fail loudly) or log a clear warning to stderr.\n\n9. **`integration_tests/regression/_agent_tool_fake.py:266` \u2014 child subprocess inherits full parent env**, including `ANTHROPIC_API_KEY`. The child only invokes `hook_entry.decide()` and does not network, so impact is minimal \u2014 but defense-in-depth would whitelist only `EGG_*`, `HOME`, `PATH`, `PYTHONPATH`. Non-blocking; the child's tool-call surface is restricted by the test-only scope.\n\n10. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:487-496` \u2014 traceback written verbatim to the contract file.** `traceback.format_exc(limit=8)` is serialised into `pending_hitl.error`, which is then JSON-serialised and read by the skill body. Could expose absolute filesystem paths in the contract artifact. Local-trust scope OK; consider truncating or stripping the path prefix before serialising in production.\n\n11. **`orchestrator/substrate/__init__.py:271-277` \u2014 `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` / `reviewer_agent_design` before the documenter (task-1-4) has landed the rubric markdown files in the same slice.** The behaviour is graceful \u2014 `_load_egg_sdlc_role_rubric` raises a clear \"rubric missing on disk \u2026 sequence the documenter's rubric task before the loader update\" ValueError \u2014 so the acceptance criterion's `_load_egg_sdlc_role_rubric(REVIEWER_REFINE) returns the markdown body of reviewer_refine.md` will only be satisfied after documenter merges. This is the expected concurrent-BRC dependency; calling out for the re-review when documenter ACKs.\n\n### Acceptance criteria check\n\n- **TASK-1-1**: `bin/run_pipeline.py` exists at the correct path; imports `run_pipeline_in_process`; round-trips `HITLDecision` through `pending_hitl`; exit codes 0/1 per docstring; `pending_hitl` envelope schema documented at top-of-file as stable contract. \u2713\n- **TASK-1-9**: `_agent_tool_fake.py` exists; exposes `dispatch(parent_role, child_role, write_target)`; helper invokes `hook_entry.decide(...)` via `pre_tool_use_callback`; module docstring marks it test-only; import guard present (with broader prefix than spec'd \u2014 finding #7). \u2713 (minor deviation)\n- **TASK-1-6**: `reviewer_refine` and `reviewer_agent_design` accepted by the loader (returns markdown once TASK-1-4 lands); `architect` raises ValueError with \"deferred to follow-up slice-2 of issue #2717's rollout\" (matches acceptance's \"follow-up slice 2\" hint). \u2713\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:27.587805+00:00", - "phase": "implement" - }, - { - "id": "8d234ce0-4a10-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:27.638162+00:00", - "phase": "implement" - }, - { - "id": "396bf7c7-7f5c-4f", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by coder (pending_acks)", - "body": "Agent coder cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus.", - "metadata": { - "pending_acks": true, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:35.625970+00:00", - "phase": "implement" - }, - { - "id": "472f8205-e415-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:35.667311+00:00", - "phase": "implement" - }, - { - "id": "5820a72a-dc0a-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:38.764531+00:00", - "phase": "implement" - }, - { - "id": "998f4855-9e74-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:07:39.835769+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:39.866039+00:00", - "phase": "implement" - }, - { - "id": "fabd92fb-cb52-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:39.963495+00:00", - "phase": "implement" - }, - { - "id": "87cf4305-0838-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:07:41.024684+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:41.064407+00:00", - "phase": "implement" - }, - { - "id": "23737e47-39af-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:07:50.285090+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:50.349649+00:00", - "phase": "implement" - }, - { - "id": "c8a31a60-becb-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:07:53.944436+00:00", - "phase": "implement" - }, - { - "id": "5a5b88bc-10ac-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:08:21.099396+00:00", - "phase": "implement" - }, - { - "id": "2a4dd715-3035-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:08:21.131799+00:00", - "phase": "implement" - }, - { - "id": "419fb1c5-9b0d-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:08:38.851639+00:00", - "phase": "implement" - }, - { - "id": "d4ecfc17-5704-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:07:41.024684+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:08:41.124461+00:00", - "phase": "implement" - }, - { - "id": "a7d0ade2-274c-41", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:07:50.285090+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:08:50.414217+00:00", - "phase": "implement" - }, - { - "id": "4b094316-00ae-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:59:38.473276+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:08:54.025152+00:00", - "phase": "implement" - }, - { - "id": "c31a305f-7483-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:14.950598+00:00", - "phase": "implement" - }, - { - "id": "0b62fe99-7f29-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:19.392069+00:00", - "phase": "implement" - }, - { - "id": "4ed9d84b-0fd6-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:38.930742+00:00", - "phase": "implement" - }, - { - "id": "ef6a77eb-bc48-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:07:41.024684+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:41.335759+00:00", - "phase": "implement" - }, - { - "id": "74e5538e-7dff-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: PROPOSED", - "body": "documenter: TASK-1-2 + TASK-1-4 + TASK-1-8 committed and proposed v1; awaiting reviewer_code ACK/NACK", - "metadata": { - "state": "PROPOSED", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:47.195961+00:00", - "phase": "implement" - }, - { - "id": "db7412eb-ae3e-4b", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:07:50.285090+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:50.476767+00:00", - "phase": "implement" - }, - { - "id": "43610bf4-6ce3-44", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:52.919188+00:00", - "phase": "implement" - }, - { - "id": "0dad4b12-4ad7-4d", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:52.929427+00:00", - "phase": "implement" - }, - { - "id": "8e442476-3cdb-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:52.936854+00:00", - "phase": "implement" - }, - { - "id": "8e319355-4c3f-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:09:57.927975+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:09:57.962629+00:00", - "phase": "implement" - }, - { - "id": "6afabf8c-ac05-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_ON_ROLE", - "body": "All 6 reviewers ACKed coder v1 at fddeb82bf. confirm() blocked on tester (producer) \u2014 version=0; waiting per overseer guidance.", - "metadata": { - "state": "WAITING_ON_ROLE", - "waiting_on": "tester", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:01.466825+00:00", - "phase": "implement" - }, - { - "id": "3fd58f85-1af7-4b", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:01.880478+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:01.909638+00:00", - "phase": "implement" - }, - { - "id": "4439b7a6-3eb4-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:15.045220+00:00", - "phase": "implement" - }, - { - "id": "324f473f-6aa6-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by documenter (pending_acks)", - "body": "Agent documenter cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus.", - "metadata": { - "pending_acks": true, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:19.167045+00:00", - "phase": "implement" - }, - { - "id": "80b189dd-1ff4-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:19.226708+00:00", - "phase": "implement" - }, - { - "id": "0e79f9e8-d224-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:19.440503+00:00", - "phase": "implement" - }, - { - "id": "8e02f21b-f2f5-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:23.268376+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:23.312347+00:00", - "phase": "implement" - }, - { - "id": "d83ddde0-ecf0-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:29.767135+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:29.826261+00:00", - "phase": "implement" - }, - { - "id": "f7a699c9-ed68-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:10:38.986892+00:00", - "phase": "implement" - }, - { - "id": "4d7eddd6-8025-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:01.880478+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:02.019820+00:00", - "phase": "implement" - }, - { - "id": "326fd3e7-7f41-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:15.096818+00:00", - "phase": "implement" - }, - { - "id": "a0e72273-ede8-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "documenter", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_code for documenter", - "body": "\nReviewed all 4 files in commit 473b109cb. The two new rubric files (`reviewer_refine.md`, `reviewer_agent_design.md`) and the ADR rewrite (`docs/architecture/claude-code-substrate.md`) are well-shaped and meet their acceptance criteria. **The SKILL.md update has three blocking documentation defects** that make the \"documented usage section cannot actually be executed\" \u2014 directly contradicting task-1-2's acceptance bullet (\"so the loop can actually run\").\n\n### Blocking\n\n1. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:90-129` \u2014 documented `bin/run_pipeline.py` CLI invocations use wrong flag names; the example will fail at argparse.** The skill loop example shows:\n ```bash\n python3 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py \\\n --pipeline-id \"issue-${ISSUE}\" \\\n --repo \"${REPO}\" \\\n --issue \"${ISSUE}\"\n ```\n But the actual driver at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402` accepts:\n - `pipeline_id` as a **positional** argument (no `--pipeline-id` flag exists)\n - `--issue-number` (not `--issue`)\n\n An operator (or future skill body) copying this invocation gets `argparse: unrecognized arguments: --pipeline-id`. The skill loop as documented cannot run. Fix: change the documented invocations to `python3 .../run_pipeline.py \"issue-${ISSUE}\" --repo \"${REPO}\" --issue-number \"${ISSUE}\"` (positional pipeline_id, `--issue-number` flag), or update the driver to accept `--pipeline-id` / `--issue` as aliases. Pick one and make the doc match the code.\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:96-110` \u2014 the documented `pending_hitl` envelope is incomplete; the skill body cannot tell \"completed\" from \"error\" from \"aborted\".** SKILL.md documents the envelope as:\n ```json\n { \"pending_hitl\": { \"version\": 1, \"pipeline_id\": \"...\", \"timestamp\": \"...\", \"decision\": {...}, \"answer\": null } }\n ```\n But the driver's actual envelope shape (per `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:24-46, 176-197`) contains four additional fields the skill body needs:\n - `status` \u2208 {`pending`, `answered`, `completed`, `aborted`, `error`} \u2014 the **loop predicate** the skill body must inspect to decide whether to render another decision, exit cleanly, or surface an error. SKILL.md's documented termination condition (\"Repeat until the driver reports `pending_hitl.decision == null`\") happens to work for the completed case (decision is None on StopIteration) but gives the skill body no way to differentiate clean completion from error or operator-abort.\n - `result` \u2014 the generator's return value (analysis path on completion, abort diagnostic on `_PreflightAborted`).\n - `error` \u2014 diagnostic string when the driver hit an internal failure (exit 1).\n - `answer_log` \u2014 the operator's accumulated answer history, central to the replay-style cross-process state recovery the driver documents in lines 53-69. SKILL.md's \"How the flattened bridge works\" section is silent on `answer_log`'s role even though future readers (and the slice-3 daemon's review) will need to know it's part of the cross-bridge contract.\n\n Fix: copy the full envelope schema (with field-by-field docstrings) from the driver's top-of-file comment into SKILL.md so the two surfaces stay in sync, and update the loop-termination text to read `status` instead of (or in addition to) `decision == null`.\n\n3. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:67, 80, 132-133` \u2014 no documented mechanism for the skill body to write `pending_hitl.answer` back to the contract file.** Steps 4-5 and the \"skill loop\" pseudocode all wave at \"write the operator's selected option back to `pending_hitl.answer`\" but the SKILL.md frontmatter `allowed-tools` does NOT include `Write` (the tool that would write a file directly). The only allowed tools that can mutate the contract JSON are `Bash(python3 *:*)`, `Bash(cat:*)`, and `Bash(cp:*)`. None of those is sufficient on its own to do a JSON read-modify-write; the only path is an inline `python3 -c \"...\"` invocation, which is nowhere documented. Result: a skill author / future maintainer reading SKILL.md cannot construct a working invocation chain.\n\n Fix: either (a) ship an explicit `python3 -c \"import json; ... ; json.dump(...)\"` example in the \"skill loop\" code block so the reader sees the mechanism; (b) extend the driver to accept `--answer \"\"` (or `--answer-file `) and document that path; or (c) add a separate companion helper like `bin/write_answer.py ` (mirroring `bin/preflight.py`'s shape) and document its use. Option (b) is cheapest and keeps the round-trip atomic; option (c) gives the skill body a Bash-friendly verb. Either way, the doc-and-code surfaces must agree.\n\n### Non-blocking\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:88` \u2014 minor inconsistency about loop entry/exit semantics.** The \"Iteration N+1\" comment in the bash block reads: \"the driver picks up `pending_hitl.answer`, calls `generator.send(answer)`, serialises the next yield.\" This describes resumption-by-send, but the driver actually uses **replay-from-start** (`bin/run_pipeline.py:258-327`): it spawns a fresh generator each invocation, calls `next()` to land on the first yield, then loops `generator.send(replay)` over the full `answer_log`. The user-facing distinction matters because replay re-runs all side effects (refiner subagent dispatch, worktree create/teardown, artifact write) on each invocation \u2014 see my coder ACK finding #1. The doc should either name \"replay\" explicitly or at minimum drop the \"the driver picks up `pending_hitl.answer` and calls `generator.send(answer)`\" framing, which suggests cheap single-step resumption.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:7` \u2014 `allowed-tools` frontmatter unchanged.** The skill cannot write the contract file without explicit user/operator consent to `Bash(python3 -c \u2026)` invocations. The acceptance criterion only requires `Bash(python3 *:*)` (which is present) but consider whether a more constrained `Bash(python3 -c *:*)` or a dedicated helper-script-only allowlist would be safer \u2014 non-blocking, but defense-in-depth.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8` \u2014 header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`.** I did not verify that path exists \u2014 if it doesn't, the comment is a dangling reference. Drop the comment or add the actual k3s prompt file path the rubric was sourced from.\n\n- **`docs/architecture/claude-code-substrate.md:112` \u2014 minor inconsistency with the coder's actual driver.** The ADR says \"On `StopIteration`, the driver clears `pending_hitl.decision` to signal the phase is complete.\" Confirmed: the driver does set `decision=None` on completion (`bin/run_pipeline.py:507-515`), but it also sets `status=\"completed\"` and `result=` \u2014 the ADR's framing reads \"decision-only\" and inherits the same gap as SKILL.md blocking #2. Worth adding \"and sets `status='completed'` with `result=`\" to keep the two surfaces consistent.\n\n- **`docs/architecture/claude-code-substrate.md:112-113` \u2014 envelope schema fields named `version, pipeline_id, timestamp, decision, answer` only.** Same issue as SKILL.md: the cross-bridge contract is described as 5 fields but the actual stable contract is 9 fields. The slice-3 daemon variant will need to honor all 9, not just 5. Update the ADR to list the full set so slice-3's reviewer can compare against the actual driver source.\n\n### Acceptance criteria check\n\n- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed \u2713; new usage section documents flattened stage-script loop \u26a0 (documented but with wrong CLI args and incomplete envelope schema); `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713. **Acceptance NOT met** \u2014 the loop as documented cannot run (see blocking #1, #2, #3).\n- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files exist \u2713; both carry valid frontmatter (`name`, `description`) \u2713; both bodies open with \"You are the **reviewer_refine** running on the **Claude Code substrate**\" / \"reviewer_agent_design\" preamble \u2713; both reference the same review criteria the k3s reviewer uses \u2713. **Acceptance met.**\n- **TASK-1-8 (ADR)**: Bridge-gap callout removed from ADR \u2713; \"Rollout deltas\" first bullet uses ~~strikethrough~~ format for slice-1-landed items \u2713; \"Open empirical question (R2)\" section documents the slice-5 fallback path \u2713. **Acceptance met.**\n\nPlease address the three SKILL.md blocking items and re-propose. The two rubric files and the ADR are good to go as-is; only SKILL.md needs revision.\n", - "metadata": { - "payload": { - "reason": "\nReviewed all 4 files in commit 473b109cb. The two new rubric files (`reviewer_refine.md`, `reviewer_agent_design.md`) and the ADR rewrite (`docs/architecture/claude-code-substrate.md`) are well-shaped and meet their acceptance criteria. **The SKILL.md update has three blocking documentation defects** that make the \"documented usage section cannot actually be executed\" \u2014 directly contradicting task-1-2's acceptance bullet (\"so the loop can actually run\").\n\n### Blocking\n\n1. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:90-129` \u2014 documented `bin/run_pipeline.py` CLI invocations use wrong flag names; the example will fail at argparse.** The skill loop example shows:\n ```bash\n python3 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py \\\n --pipeline-id \"issue-${ISSUE}\" \\\n --repo \"${REPO}\" \\\n --issue \"${ISSUE}\"\n ```\n But the actual driver at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402` accepts:\n - `pipeline_id` as a **positional** argument (no `--pipeline-id` flag exists)\n - `--issue-number` (not `--issue`)\n\n An operator (or future skill body) copying this invocation gets `argparse: unrecognized arguments: --pipeline-id`. The skill loop as documented cannot run. Fix: change the documented invocations to `python3 .../run_pipeline.py \"issue-${ISSUE}\" --repo \"${REPO}\" --issue-number \"${ISSUE}\"` (positional pipeline_id, `--issue-number` flag), or update the driver to accept `--pipeline-id` / `--issue` as aliases. Pick one and make the doc match the code.\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:96-110` \u2014 the documented `pending_hitl` envelope is incomplete; the skill body cannot tell \"completed\" from \"error\" from \"aborted\".** SKILL.md documents the envelope as:\n ```json\n { \"pending_hitl\": { \"version\": 1, \"pipeline_id\": \"...\", \"timestamp\": \"...\", \"decision\": {...}, \"answer\": null } }\n ```\n But the driver's actual envelope shape (per `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:24-46, 176-197`) contains four additional fields the skill body needs:\n - `status` \u2208 {`pending`, `answered`, `completed`, `aborted`, `error`} \u2014 the **loop predicate** the skill body must inspect to decide whether to render another decision, exit cleanly, or surface an error. SKILL.md's documented termination condition (\"Repeat until the driver reports `pending_hitl.decision == null`\") happens to work for the completed case (decision is None on StopIteration) but gives the skill body no way to differentiate clean completion from error or operator-abort.\n - `result` \u2014 the generator's return value (analysis path on completion, abort diagnostic on `_PreflightAborted`).\n - `error` \u2014 diagnostic string when the driver hit an internal failure (exit 1).\n - `answer_log` \u2014 the operator's accumulated answer history, central to the replay-style cross-process state recovery the driver documents in lines 53-69. SKILL.md's \"How the flattened bridge works\" section is silent on `answer_log`'s role even though future readers (and the slice-3 daemon's review) will need to know it's part of the cross-bridge contract.\n\n Fix: copy the full envelope schema (with field-by-field docstrings) from the driver's top-of-file comment into SKILL.md so the two surfaces stay in sync, and update the loop-termination text to read `status` instead of (or in addition to) `decision == null`.\n\n3. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:67, 80, 132-133` \u2014 no documented mechanism for the skill body to write `pending_hitl.answer` back to the contract file.** Steps 4-5 and the \"skill loop\" pseudocode all wave at \"write the operator's selected option back to `pending_hitl.answer`\" but the SKILL.md frontmatter `allowed-tools` does NOT include `Write` (the tool that would write a file directly). The only allowed tools that can mutate the contract JSON are `Bash(python3 *:*)`, `Bash(cat:*)`, and `Bash(cp:*)`. None of those is sufficient on its own to do a JSON read-modify-write; the only path is an inline `python3 -c \"...\"` invocation, which is nowhere documented. Result: a skill author / future maintainer reading SKILL.md cannot construct a working invocation chain.\n\n Fix: either (a) ship an explicit `python3 -c \"import json; ... ; json.dump(...)\"` example in the \"skill loop\" code block so the reader sees the mechanism; (b) extend the driver to accept `--answer \"\"` (or `--answer-file `) and document that path; or (c) add a separate companion helper like `bin/write_answer.py ` (mirroring `bin/preflight.py`'s shape) and document its use. Option (b) is cheapest and keeps the round-trip atomic; option (c) gives the skill body a Bash-friendly verb. Either way, the doc-and-code surfaces must agree.\n\n### Non-blocking\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:88` \u2014 minor inconsistency about loop entry/exit semantics.** The \"Iteration N+1\" comment in the bash block reads: \"the driver picks up `pending_hitl.answer`, calls `generator.send(answer)`, serialises the next yield.\" This describes resumption-by-send, but the driver actually uses **replay-from-start** (`bin/run_pipeline.py:258-327`): it spawns a fresh generator each invocation, calls `next()` to land on the first yield, then loops `generator.send(replay)` over the full `answer_log`. The user-facing distinction matters because replay re-runs all side effects (refiner subagent dispatch, worktree create/teardown, artifact write) on each invocation \u2014 see my coder ACK finding #1. The doc should either name \"replay\" explicitly or at minimum drop the \"the driver picks up `pending_hitl.answer` and calls `generator.send(answer)`\" framing, which suggests cheap single-step resumption.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:7` \u2014 `allowed-tools` frontmatter unchanged.** The skill cannot write the contract file without explicit user/operator consent to `Bash(python3 -c \u2026)` invocations. The acceptance criterion only requires `Bash(python3 *:*)` (which is present) but consider whether a more constrained `Bash(python3 -c *:*)` or a dedicated helper-script-only allowlist would be safer \u2014 non-blocking, but defense-in-depth.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8` \u2014 header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`.** I did not verify that path exists \u2014 if it doesn't, the comment is a dangling reference. Drop the comment or add the actual k3s prompt file path the rubric was sourced from.\n\n- **`docs/architecture/claude-code-substrate.md:112` \u2014 minor inconsistency with the coder's actual driver.** The ADR says \"On `StopIteration`, the driver clears `pending_hitl.decision` to signal the phase is complete.\" Confirmed: the driver does set `decision=None` on completion (`bin/run_pipeline.py:507-515`), but it also sets `status=\"completed\"` and `result=` \u2014 the ADR's framing reads \"decision-only\" and inherits the same gap as SKILL.md blocking #2. Worth adding \"and sets `status='completed'` with `result=`\" to keep the two surfaces consistent.\n\n- **`docs/architecture/claude-code-substrate.md:112-113` \u2014 envelope schema fields named `version, pipeline_id, timestamp, decision, answer` only.** Same issue as SKILL.md: the cross-bridge contract is described as 5 fields but the actual stable contract is 9 fields. The slice-3 daemon variant will need to honor all 9, not just 5. Update the ADR to list the full set so slice-3's reviewer can compare against the actual driver source.\n\n### Acceptance criteria check\n\n- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed \u2713; new usage section documents flattened stage-script loop \u26a0 (documented but with wrong CLI args and incomplete envelope schema); `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713. **Acceptance NOT met** \u2014 the loop as documented cannot run (see blocking #1, #2, #3).\n- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files exist \u2713; both carry valid frontmatter (`name`, `description`) \u2713; both bodies open with \"You are the **reviewer_refine** running on the **Claude Code substrate**\" / \"reviewer_agent_design\" preamble \u2713; both reference the same review criteria the k3s reviewer uses \u2713. **Acceptance met.**\n- **TASK-1-8 (ADR)**: Bridge-gap callout removed from ADR \u2713; \"Rollout deltas\" first bullet uses ~~strikethrough~~ format for slice-1-landed items \u2713; \"Open empirical question (R2)\" section documents the slice-5 fallback path \u2713. **Acceptance met.**\n\nPlease address the three SKILL.md blocking items and re-propose. The two rubric files and the ADR are good to go as-is; only SKILL.md needs revision.\n", - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md", - "docs/architecture/claude-code-substrate.md" - ], - "nack_version": 1 - }, - "reason": "\nReviewed all 4 files in commit 473b109cb. The two new rubric files (`reviewer_refine.md`, `reviewer_agent_design.md`) and the ADR rewrite (`docs/architecture/claude-code-substrate.md`) are well-shaped and meet their acceptance criteria. **The SKILL.md update has three blocking documentation defects** that make the \"documented usage section cannot actually be executed\" \u2014 directly contradicting task-1-2's acceptance bullet (\"so the loop can actually run\").\n\n### Blocking\n\n1. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:90-129` \u2014 documented `bin/run_pipeline.py` CLI invocations use wrong flag names; the example will fail at argparse.** The skill loop example shows:\n ```bash\n python3 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py \\\n --pipeline-id \"issue-${ISSUE}\" \\\n --repo \"${REPO}\" \\\n --issue \"${ISSUE}\"\n ```\n But the actual driver at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402` accepts:\n - `pipeline_id` as a **positional** argument (no `--pipeline-id` flag exists)\n - `--issue-number` (not `--issue`)\n\n An operator (or future skill body) copying this invocation gets `argparse: unrecognized arguments: --pipeline-id`. The skill loop as documented cannot run. Fix: change the documented invocations to `python3 .../run_pipeline.py \"issue-${ISSUE}\" --repo \"${REPO}\" --issue-number \"${ISSUE}\"` (positional pipeline_id, `--issue-number` flag), or update the driver to accept `--pipeline-id` / `--issue` as aliases. Pick one and make the doc match the code.\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:96-110` \u2014 the documented `pending_hitl` envelope is incomplete; the skill body cannot tell \"completed\" from \"error\" from \"aborted\".** SKILL.md documents the envelope as:\n ```json\n { \"pending_hitl\": { \"version\": 1, \"pipeline_id\": \"...\", \"timestamp\": \"...\", \"decision\": {...}, \"answer\": null } }\n ```\n But the driver's actual envelope shape (per `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:24-46, 176-197`) contains four additional fields the skill body needs:\n - `status` \u2208 {`pending`, `answered`, `completed`, `aborted`, `error`} \u2014 the **loop predicate** the skill body must inspect to decide whether to render another decision, exit cleanly, or surface an error. SKILL.md's documented termination condition (\"Repeat until the driver reports `pending_hitl.decision == null`\") happens to work for the completed case (decision is None on StopIteration) but gives the skill body no way to differentiate clean completion from error or operator-abort.\n - `result` \u2014 the generator's return value (analysis path on completion, abort diagnostic on `_PreflightAborted`).\n - `error` \u2014 diagnostic string when the driver hit an internal failure (exit 1).\n - `answer_log` \u2014 the operator's accumulated answer history, central to the replay-style cross-process state recovery the driver documents in lines 53-69. SKILL.md's \"How the flattened bridge works\" section is silent on `answer_log`'s role even though future readers (and the slice-3 daemon's review) will need to know it's part of the cross-bridge contract.\n\n Fix: copy the full envelope schema (with field-by-field docstrings) from the driver's top-of-file comment into SKILL.md so the two surfaces stay in sync, and update the loop-termination text to read `status` instead of (or in addition to) `decision == null`.\n\n3. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:67, 80, 132-133` \u2014 no documented mechanism for the skill body to write `pending_hitl.answer` back to the contract file.** Steps 4-5 and the \"skill loop\" pseudocode all wave at \"write the operator's selected option back to `pending_hitl.answer`\" but the SKILL.md frontmatter `allowed-tools` does NOT include `Write` (the tool that would write a file directly). The only allowed tools that can mutate the contract JSON are `Bash(python3 *:*)`, `Bash(cat:*)`, and `Bash(cp:*)`. None of those is sufficient on its own to do a JSON read-modify-write; the only path is an inline `python3 -c \"...\"` invocation, which is nowhere documented. Result: a skill author / future maintainer reading SKILL.md cannot construct a working invocation chain.\n\n Fix: either (a) ship an explicit `python3 -c \"import json; ... ; json.dump(...)\"` example in the \"skill loop\" code block so the reader sees the mechanism; (b) extend the driver to accept `--answer \"\"` (or `--answer-file `) and document that path; or (c) add a separate companion helper like `bin/write_answer.py ` (mirroring `bin/preflight.py`'s shape) and document its use. Option (b) is cheapest and keeps the round-trip atomic; option (c) gives the skill body a Bash-friendly verb. Either way, the doc-and-code surfaces must agree.\n\n### Non-blocking\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:88` \u2014 minor inconsistency about loop entry/exit semantics.** The \"Iteration N+1\" comment in the bash block reads: \"the driver picks up `pending_hitl.answer`, calls `generator.send(answer)`, serialises the next yield.\" This describes resumption-by-send, but the driver actually uses **replay-from-start** (`bin/run_pipeline.py:258-327`): it spawns a fresh generator each invocation, calls `next()` to land on the first yield, then loops `generator.send(replay)` over the full `answer_log`. The user-facing distinction matters because replay re-runs all side effects (refiner subagent dispatch, worktree create/teardown, artifact write) on each invocation \u2014 see my coder ACK finding #1. The doc should either name \"replay\" explicitly or at minimum drop the \"the driver picks up `pending_hitl.answer` and calls `generator.send(answer)`\" framing, which suggests cheap single-step resumption.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:7` \u2014 `allowed-tools` frontmatter unchanged.** The skill cannot write the contract file without explicit user/operator consent to `Bash(python3 -c \u2026)` invocations. The acceptance criterion only requires `Bash(python3 *:*)` (which is present) but consider whether a more constrained `Bash(python3 -c *:*)` or a dedicated helper-script-only allowlist would be safer \u2014 non-blocking, but defense-in-depth.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8` \u2014 header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`.** I did not verify that path exists \u2014 if it doesn't, the comment is a dangling reference. Drop the comment or add the actual k3s prompt file path the rubric was sourced from.\n\n- **`docs/architecture/claude-code-substrate.md:112` \u2014 minor inconsistency with the coder's actual driver.** The ADR says \"On `StopIteration`, the driver clears `pending_hitl.decision` to signal the phase is complete.\" Confirmed: the driver does set `decision=None` on completion (`bin/run_pipeline.py:507-515`), but it also sets `status=\"completed\"` and `result=` \u2014 the ADR's framing reads \"decision-only\" and inherits the same gap as SKILL.md blocking #2. Worth adding \"and sets `status='completed'` with `result=`\" to keep the two surfaces consistent.\n\n- **`docs/architecture/claude-code-substrate.md:112-113` \u2014 envelope schema fields named `version, pipeline_id, timestamp, decision, answer` only.** Same issue as SKILL.md: the cross-bridge contract is described as 5 fields but the actual stable contract is 9 fields. The slice-3 daemon variant will need to honor all 9, not just 5. Update the ADR to list the full set so slice-3's reviewer can compare against the actual driver source.\n\n### Acceptance criteria check\n\n- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed \u2713; new usage section documents flattened stage-script loop \u26a0 (documented but with wrong CLI args and incomplete envelope schema); `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713. **Acceptance NOT met** \u2014 the loop as documented cannot run (see blocking #1, #2, #3).\n- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files exist \u2713; both carry valid frontmatter (`name`, `description`) \u2713; both bodies open with \"You are the **reviewer_refine** running on the **Claude Code substrate**\" / \"reviewer_agent_design\" preamble \u2713; both reference the same review criteria the k3s reviewer uses \u2713. **Acceptance met.**\n- **TASK-1-8 (ADR)**: Bridge-gap callout removed from ADR \u2713; \"Rollout deltas\" first bullet uses ~~strikethrough~~ format for slice-1-landed items \u2713; \"Open empirical question (R2)\" section documents the slice-5 fallback path \u2713. **Acceptance met.**\n\nPlease address the three SKILL.md blocking items and re-propose. The two rubric files and the ADR are good to go as-is; only SKILL.md needs revision.\n", - "revision_count": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:16.482546+00:00", - "phase": "implement" - }, - { - "id": "bdd907ac-9e11-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:16.559760+00:00", - "phase": "implement" - }, - { - "id": "1308cf73-5bca-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:19.484391+00:00", - "phase": "implement" - }, - { - "id": "b1200264-5c63-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:23.268376+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:23.362504+00:00", - "phase": "implement" - }, - { - "id": "aeabc58c-ce55-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:11:32.544708+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:32.642802+00:00", - "phase": "implement" - }, - { - "id": "c124654c-67a3-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:11:39.061555+00:00", - "phase": "implement" - }, - { - "id": "be5bf453-439a-47", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:01.880478+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:12:02.135389+00:00", - "phase": "implement" - }, - { - "id": "da6bca70-8497-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:12:15.189445+00:00", - "phase": "implement" - }, - { - "id": "bd306bca-1f06-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:12:19.533623+00:00", - "phase": "implement" - }, - { - "id": "9e67be94-8a64-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:23.268376+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:12:23.497550+00:00", - "phase": "implement" - }, - { - "id": "28727a22-014f-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:11:32.544708+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:12:32.718246+00:00", - "phase": "implement" - }, - { - "id": "90dd97b1-d406-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:12:39.132269+00:00", - "phase": "implement" - }, - { - "id": "7763b7a2-934f-4a", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:01.880478+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:13:02.184038+00:00", - "phase": "implement" - }, - { - "id": "f0f5be5b-a1d4-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:13:15.238000+00:00", - "phase": "implement" - }, - { - "id": "dc3bb88f-1723-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:13:19.582370+00:00", - "phase": "implement" - }, - { - "id": "a2db82d6-81ad-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:23.268376+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:13:23.559729+00:00", - "phase": "implement" - }, - { - "id": "7db5726b-3f28-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:11:32.544708+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:13:32.863835+00:00", - "phase": "implement" - }, - { - "id": "f58ac214-54d6-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:13:39.253072+00:00", - "phase": "implement" - }, - { - "id": "27cbaae3-676b-41", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:01.880478+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:14:02.241839+00:00", - "phase": "implement" - }, - { - "id": "884c0224-924d-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:14:15.421621+00:00", - "phase": "implement" - }, - { - "id": "c35a53de-102f-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:03:03.912729+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:14:19.630752+00:00", - "phase": "implement" - }, - { - "id": "9f190e30-4a28-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:23.268376+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:14:23.691037+00:00", - "phase": "implement" - }, - { - "id": "48b88c63-53bc-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:11:32.544708+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:14:32.910635+00:00", - "phase": "implement" - }, - { - "id": "8abebd35-4b17-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:06:38.269267+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:14:39.465985+00:00", - "phase": "implement" - }, - { - "id": "e6b1f181-aef9-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:10:01.880478+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:12.232701+00:00", - "phase": "implement" - }, - { - "id": "d749bc80-61d8-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:01:58.994144+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:15.478447+00:00", - "phase": "implement" - }, - { - "id": "b7a23062-7df0-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:17.207867+00:00", - "phase": "implement" - }, - { - "id": "98dc3fb3-9545-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:17.215048+00:00", - "phase": "implement" - }, - { - "id": "539388f6-7912-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from documenter", - "body": "Re-propose v2 of slice-1 documenter half. Addresses reviewer_code v1 NACK (commit cf8f71a93) on SKILL.md envelope + CLI documentation. Blocker fixes: (1) CLI invocation example rewritten to use the driver's actual argparse signature \u2014 positional pipeline_id + --issue-number / --repo flags (verified against bin/run_pipeline.py:355-402); (2) pending_hitl envelope expanded from 5 fields to the full 9-field schema (adds status, result, error, answer_log) with per-field semantics including the status loop-predicate values (pending/answered/completed/aborted/error); (3) inline python3 -c \"...\" mechanism for writing pending_hitl.answer documented in the skill-loop code block, covered by the existing Bash(python3 *:*) allowed-tool \u2014 no new tool permission needed. Non-blocking polish: replay semantics now named explicitly (driver spawns fresh generator and replays answer_log every call; side effects re-run every invocation; slice-3 daemon variant is the escape hatch for implement-phase concurrency); failure-mode diagnostic bullet replaced with the more general status \u2208 {completed, aborted, error} predicate so each terminal state has guidance. ADR additions: flattened-bridge bullet now names replay path + 9-field shape, daemon-variant bullet enumerates the same 9 fields, source-of-truth pointer added to bin/run_pipeline.py:20-46. Rubric files (reviewer_refine.md, reviewer_agent_design.md) unchanged \u2014 reviewer_code ACKed them in v1.", - "metadata": { - "payload": { - "summary": "Re-propose v2 of slice-1 documenter half. Addresses reviewer_code v1 NACK (commit cf8f71a93) on SKILL.md envelope + CLI documentation. Blocker fixes: (1) CLI invocation example rewritten to use the driver's actual argparse signature \u2014 positional pipeline_id + --issue-number / --repo flags (verified against bin/run_pipeline.py:355-402); (2) pending_hitl envelope expanded from 5 fields to the full 9-field schema (adds status, result, error, answer_log) with per-field semantics including the status loop-predicate values (pending/answered/completed/aborted/error); (3) inline python3 -c \"...\" mechanism for writing pending_hitl.answer documented in the skill-loop code block, covered by the existing Bash(python3 *:*) allowed-tool \u2014 no new tool permission needed. Non-blocking polish: replay semantics now named explicitly (driver spawns fresh generator and replays answer_log every call; side effects re-run every invocation; slice-3 daemon variant is the escape hatch for implement-phase concurrency); failure-mode diagnostic bullet replaced with the more general status \u2208 {completed, aborted, error} predicate so each terminal state has guidance. ADR additions: flattened-bridge bullet now names replay path + 9-field shape, daemon-variant bullet enumerates the same 9 fields, source-of-truth pointer added to bin/run_pipeline.py:20-46. Rubric files (reviewer_refine.md, reviewer_agent_design.md) unchanged \u2014 reviewer_code ACKed them in v1.", - "attestation": {}, - "artifacts": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "docs/architecture/claude-code-substrate.md" - ], - "risk_considered": "v2 risk surface narrows to SKILL.md correctness against the actual driver source. Re-verified: (a) argparse signature at bin/run_pipeline.py:355-402 \u2014 pipeline_id positional, --issue-number int flag, --repo str flag, --issue-body / --state-root / --daemon optional; the SKILL.md example uses the matching shape so copy-paste runs. (b) envelope schema mirrored verbatim from the driver's module docstring at bin/run_pipeline.py:20-46 \u2014 9 fields with the same names and semantics; future drift catches in one place because the SKILL.md \"How the flattened bridge works\" section explicitly cites that line range as the source of truth. (c) answer-writing mechanism uses Bash(python3 *:*) which is already on the frontmatter allowed-tools line; no allowed-tools change needed; the inline python3 -c snippet is JSON-shape-correct. (d) replay-semantics framing matches the driver's docstring at bin/run_pipeline.py:53-69 and the implementation at :258-327 (loops generator.send(replay) over answer_log). The ADR's 9-field enumeration is consistent with SKILL.md's enumeration; the slice-3 daemon variant in TASK-3-2 will consume the same shape per the risk_analyst R17 mitigation.", - "commit_sha": "cf8f71a938a0426392b8db69f0798a5b7d4f4b65", - "files_changed": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "docs/architecture/claude-code-substrate.md" - ], - "tests_run": [], - "tasks_satisfied": [ - "TASK-1-2", - "TASK-1-4", - "TASK-1-8" - ] - }, - "version": 2, - "commit_sha": "cf8f71a938a0426392b8db69f0798a5b7d4f4b65", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:18.652904+00:00", - "phase": "implement" - }, - { - "id": "0859c364-4bec-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:18.758119+00:00", - "phase": "implement" - }, - { - "id": "91c2ba0c-4ddc-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:18.805768+00:00", - "phase": "implement" - }, - { - "id": "6e6d6d45-85ac-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:18.818120+00:00", - "phase": "implement" - }, - { - "id": "9808e540-2591-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:18.829293+00:00", - "phase": "implement" - }, - { - "id": "49978527-bd34-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:22.331532+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.304733+00:00", - "phase": "implement" - }, - { - "id": "78e1db98-35a7-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.315216+00:00", - "phase": "implement" - }, - { - "id": "e4489055-433c-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.322595+00:00", - "phase": "implement" - }, - { - "id": "ed905ad0-7b4a-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.331839+00:00", - "phase": "implement" - }, - { - "id": "c27dacd5-9f30-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.335606+00:00", - "phase": "implement" - }, - { - "id": "9fac7c01-e3a2-41", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.991636+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.422583+00:00", - "phase": "implement" - }, - { - "id": "f945bb42-7d8d-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.438004+00:00", - "phase": "implement" - }, - { - "id": "abc07178-6a49-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:15:40.453934+00:00", - "phase": "implement" - }, - { - "id": "0ec02a91-3781-46", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:47.377178+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:05.308758+00:00", - "phase": "implement" - }, - { - "id": "44444d94-8aac-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:35.464372+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:35.491525+00:00", - "phase": "implement" - }, - { - "id": "3194673c-492a-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:38.846998+00:00", - "phase": "implement" - }, - { - "id": "f5a35fd6-2327-48", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:38.949754+00:00", - "phase": "implement" - }, - { - "id": "96024cba-af32-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:39.303660+00:00", - "phase": "implement" - }, - { - "id": "ebbb6f5c-b129-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:39.389396+00:00", - "phase": "implement" - }, - { - "id": "d28966be-df70-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "documenter", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for documenter", - "body": "\nReviewed commit cf8f71a93 (documenter v2). All three v1 blockers are resolved cleanly and the four non-blocking polish items from v1 are also addressed in both SKILL.md and the ADR. The two rubric files are unchanged (they ACKed in v1) \u2014 verified no regression. The skill loop example as documented can now actually run.\n\n### Verification of v1 blocking fixes\n\n1. **Blocking #1 (CLI args mismatch) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:134-137` now invokes the driver with `\"${PIPELINE_ID}\"` as a positional argument plus `--repo \"${REPO}\" --issue-number \"${ISSUE}\"`, matching the driver's argparse signature at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402`. The bash comment at line 131-133 explicitly points readers to the driver's argparse range. Doc-and-code surfaces agree.\n\n2. **Blocking #2 (incomplete envelope schema) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:75-94` now documents the full 9-field envelope (`version`, `pipeline_id`, `timestamp`, `decision`, `answer`, `status`, `result`, `error`, `answer_log`), followed by per-field semantics at lines 96-111. The `status` field is named as **the skill's loop predicate** with all five enum values (`pending`, `answered`, `completed`, `aborted`, `error`) documented. The schema source-of-truth pointer to `bin/run_pipeline.py:20-46` at line 73 keeps the two surfaces in sync.\n\n3. **Blocking #3 (missing write mechanism) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:148-157` shows an inline `python3 -c \"...\"` snippet that reads the contract, mutates `pending_hitl.answer` + `status`, and writes back. The text at line 173 explicitly names the `Bash(python3 *:*)` allowed-tool as the permission backing this approach and notes that the option-(b) `--answer` flag / option-(c) `bin/write_answer.py` helper are reserved if the inline shape proves awkward. The skill body now has a documented, executable mechanism.\n\n### Verification of v1 non-blocking polish\n\n- **Replay semantics \u2014 addressed.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:115-119` has a dedicated \"Generator state across invocations (replay semantics)\" subsection naming \"replay\" explicitly, documenting that side effects (refiner subagent dispatch, worktree create/teardown, artifact write) re-run on every invocation, and pointing at slice 3 as the daemon-variant escape hatch. The ADR at `docs/architecture/claude-code-substrate.md:112` also names the replay path with the deterministic-same-yield-boundary property.\n- **ADR envelope schema \u2014 addressed.** `docs/architecture/claude-code-substrate.md:112-115` now enumerates all 9 fields and points at `bin/run_pipeline.py:20-46` as the source of truth.\n- **Failure-mode diagnostic guidance \u2014 addressed.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:262` replaces the old \"decision == null\" terminus with a status-based bullet that documents what `pending_hitl.result` / `pending_hitl.error` mean for each terminal state.\n- **Rubric files** \u2014 unchanged (correctly noted in commit msg as ACKed-in-v1, no churn).\n\n### Non-blocking observations (v2)\n\nThese are not regressions from v1; they are minor polish items I noticed while verifying the v2 diff. Not blocking.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:155` \u2014 `datetime.utcnow()` is deprecated.** The example uses `datetime.datetime.utcnow().isoformat() + 'Z'`, which Python \u22653.12 deprecates in favor of `datetime.datetime.now(datetime.UTC).isoformat()`. The driver's `_now_iso()` at `bin/run_pipeline.py:101-103` uses the modern form. Skill body Python 3.11 will work; Python 3.12+ will print a DeprecationWarning. Cheapest fix: change the example to `datetime.datetime.now(datetime.UTC).isoformat()`.\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:153` \u2014 `${ANSWER}` shell substitution is unsanitized.** The comment `# operator's selection; JSON-encode appropriately` acknowledges this, but a future skill body author who copy-pastes the example and forgets to JSON-encode will produce a Python NameError (if ANSWER is a bare string like `approve`) or a shell-injection vector (if ANSWER contains backticks, double quotes, or `$(\u2026)`). Consider replacing the example with a JSON-via-stdin pattern (`echo \"${ANSWER_JSON}\" | python3 -c \"import json,sys; \u2026\"`) so the substitution boundary is shell\u2192stdin rather than shell\u2192python source.\n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8`** \u2014 header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`. These paths still need verification (or the comment needs to point at the actual k3s prompt sources the rubric was sourced from). Carried over from v1 non-blocking; not a blocker.\n\n### Acceptance criteria check (final)\n\n- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed \u2713; new usage section documents the flattened stage-script loop with correct CLI args, full envelope schema, and documented write mechanism \u2713; `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713 \u2014 and the inline `python3 -c \"...\"` write path now backs the loop in practice. **Acceptance met.**\n- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files unchanged from v1; acceptance previously confirmed met.\n- **TASK-1-8 (ADR)**: Updated to mirror SKILL.md's 9-field envelope and replay semantics; previously confirmed met, still met.\n", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md", - "docs/architecture/claude-code-substrate.md" - ], - "reason": "\nReviewed commit cf8f71a93 (documenter v2). All three v1 blockers are resolved cleanly and the four non-blocking polish items from v1 are also addressed in both SKILL.md and the ADR. The two rubric files are unchanged (they ACKed in v1) \u2014 verified no regression. The skill loop example as documented can now actually run.\n\n### Verification of v1 blocking fixes\n\n1. **Blocking #1 (CLI args mismatch) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:134-137` now invokes the driver with `\"${PIPELINE_ID}\"` as a positional argument plus `--repo \"${REPO}\" --issue-number \"${ISSUE}\"`, matching the driver's argparse signature at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402`. The bash comment at line 131-133 explicitly points readers to the driver's argparse range. Doc-and-code surfaces agree.\n\n2. **Blocking #2 (incomplete envelope schema) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:75-94` now documents the full 9-field envelope (`version`, `pipeline_id`, `timestamp`, `decision`, `answer`, `status`, `result`, `error`, `answer_log`), followed by per-field semantics at lines 96-111. The `status` field is named as **the skill's loop predicate** with all five enum values (`pending`, `answered`, `completed`, `aborted`, `error`) documented. The schema source-of-truth pointer to `bin/run_pipeline.py:20-46` at line 73 keeps the two surfaces in sync.\n\n3. **Blocking #3 (missing write mechanism) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:148-157` shows an inline `python3 -c \"...\"` snippet that reads the contract, mutates `pending_hitl.answer` + `status`, and writes back. The text at line 173 explicitly names the `Bash(python3 *:*)` allowed-tool as the permission backing this approach and notes that the option-(b) `--answer` flag / option-(c) `bin/write_answer.py` helper are reserved if the inline shape proves awkward. The skill body now has a documented, executable mechanism.\n\n### Verification of v1 non-blocking polish\n\n- **Replay semantics \u2014 addressed.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:115-119` has a dedicated \"Generator state across invocations (replay semantics)\" subsection naming \"replay\" explicitly, documenting that side effects (refiner subagent dispatch, worktree create/teardown, artifact write) re-run on every invocation, and pointing at slice 3 as the daemon-variant escape hatch. The ADR at `docs/architecture/claude-code-substrate.md:112` also names the replay path with the deterministic-same-yield-boundary property.\n- **ADR envelope schema \u2014 addressed.** `docs/architecture/claude-code-substrate.md:112-115` now enumerates all 9 fields and points at `bin/run_pipeline.py:20-46` as the source of truth.\n- **Failure-mode diagnostic guidance \u2014 addressed.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:262` replaces the old \"decision == null\" terminus with a status-based bullet that documents what `pending_hitl.result` / `pending_hitl.error` mean for each terminal state.\n- **Rubric files** \u2014 unchanged (correctly noted in commit msg as ACKed-in-v1, no churn).\n\n### Non-blocking observations (v2)\n\nThese are not regressions from v1; they are minor polish items I noticed while verifying the v2 diff. Not blocking.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:155` \u2014 `datetime.utcnow()` is deprecated.** The example uses `datetime.datetime.utcnow().isoformat() + 'Z'`, which Python \u22653.12 deprecates in favor of `datetime.datetime.now(datetime.UTC).isoformat()`. The driver's `_now_iso()` at `bin/run_pipeline.py:101-103` uses the modern form. Skill body Python 3.11 will work; Python 3.12+ will print a DeprecationWarning. Cheapest fix: change the example to `datetime.datetime.now(datetime.UTC).isoformat()`.\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:153` \u2014 `${ANSWER}` shell substitution is unsanitized.** The comment `# operator's selection; JSON-encode appropriately` acknowledges this, but a future skill body author who copy-pastes the example and forgets to JSON-encode will produce a Python NameError (if ANSWER is a bare string like `approve`) or a shell-injection vector (if ANSWER contains backticks, double quotes, or `$(\u2026)`). Consider replacing the example with a JSON-via-stdin pattern (`echo \"${ANSWER_JSON}\" | python3 -c \"import json,sys; \u2026\"`) so the substitution boundary is shell\u2192stdin rather than shell\u2192python source.\n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8`** \u2014 header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`. These paths still need verification (or the comment needs to point at the actual k3s prompt sources the rubric was sourced from). Carried over from v1 non-blocking; not a blocker.\n\n### Acceptance criteria check (final)\n\n- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed \u2713; new usage section documents the flattened stage-script loop with correct CLI args, full envelope schema, and documented write mechanism \u2713; `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713 \u2014 and the inline `python3 -c \"...\"` write path now backs the loop in practice. **Acceptance met.**\n- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files unchanged from v1; acceptance previously confirmed met.\n- **TASK-1-8 (ADR)**: Updated to mirror SKILL.md's 9-field envelope and replay semantics; previously confirmed met, still met.\n", - "ack_version": 2 - }, - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:42.645004+00:00", - "phase": "implement" - }, - { - "id": "556198be-53ae-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:42.701034+00:00", - "phase": "implement" - }, - { - "id": "6aa24858-0204-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:16:54.694280+00:00", - "phase": "implement" - }, - { - "id": "8b825fe3-ba0e-46", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:17:03.569885+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:17:03.606209+00:00", - "phase": "implement" - }, - { - "id": "39f9f3ff-8ad0-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:35.464372+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:17:35.551428+00:00", - "phase": "implement" - }, - { - "id": "71cd3e97-e0ee-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:17:38.893418+00:00", - "phase": "implement" - }, - { - "id": "5ea455c1-193b-42", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:17:39.004260+00:00", - "phase": "implement" - }, - { - "id": "13ff5ea9-c33a-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:17:39.339282+00:00", - "phase": "implement" - }, - { - "id": "543ddb37-c531-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:17:39.420059+00:00", - "phase": "implement" - }, - { - "id": "66295bf2-30b9-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:17:54.768700+00:00", - "phase": "implement" - }, - { - "id": "6d8bb72e-d7cc-4e", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:17:03.569885+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:18:03.652723+00:00", - "phase": "implement" - }, - { - "id": "d5f296d4-2832-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:35.464372+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:18:35.606983+00:00", - "phase": "implement" - }, - { - "id": "f5a020d9-4eb0-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:18:38.932156+00:00", - "phase": "implement" - }, - { - "id": "c5347d81-9f1f-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:18:39.044884+00:00", - "phase": "implement" - }, - { - "id": "298396e9-cab7-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:18:39.373229+00:00", - "phase": "implement" - }, - { - "id": "32e0f6d4-b38b-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:18:39.455715+00:00", - "phase": "implement" - }, - { - "id": "0a809cf9-a423-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:18:54.800195+00:00", - "phase": "implement" - }, - { - "id": "c2b2c082-0f00-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:17:03.569885+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:19:03.766571+00:00", - "phase": "implement" - }, - { - "id": "1c80905c-be14-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:35.464372+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:19:35.693132+00:00", - "phase": "implement" - }, - { - "id": "9b64fdfc-618b-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:19:38.985323+00:00", - "phase": "implement" - }, - { - "id": "03719bac-2caa-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:19:39.096420+00:00", - "phase": "implement" - }, - { - "id": "98cbc3c2-53f1-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:19:39.410054+00:00", - "phase": "implement" - }, - { - "id": "c3dc1d20-ed15-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:19:39.506798+00:00", - "phase": "implement" - }, - { - "id": "7fa01948-db4d-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:19:54.904146+00:00", - "phase": "implement" - }, - { - "id": "86062cbe-cd07-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:17:03.569885+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:20:03.840357+00:00", - "phase": "implement" - }, - { - "id": "f8a18f73-4742-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:35.464372+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:20:35.745561+00:00", - "phase": "implement" - }, - { - "id": "7205e89f-807c-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:20:39.036598+00:00", - "phase": "implement" - }, - { - "id": "27ce5714-9180-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:20:39.146224+00:00", - "phase": "implement" - }, - { - "id": "25bcfd2a-9806-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:20:39.460361+00:00", - "phase": "implement" - }, - { - "id": "d65359a4-8ce7-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:20:39.553605+00:00", - "phase": "implement" - }, - { - "id": "f16ecb84-baf4-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:20:54.968951+00:00", - "phase": "implement" - }, - { - "id": "0774021a-cbec-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:17:03.569885+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:21:03.896973+00:00", - "phase": "implement" - }, - { - "id": "f346b117-92cb-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:35.464372+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:21:35.801345+00:00", - "phase": "implement" - }, - { - "id": "d5f0c745-e525-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:21:39.132548+00:00", - "phase": "implement" - }, - { - "id": "1a2baa26-2453-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:21:39.193955+00:00", - "phase": "implement" - }, - { - "id": "ac7006cc-0041-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:21:39.508082+00:00", - "phase": "implement" - }, - { - "id": "64d07de2-fc96-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:21:39.610067+00:00", - "phase": "implement" - }, - { - "id": "e54aee40-fe2b-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:21:55.033812+00:00", - "phase": "implement" - }, - { - "id": "5f5b8937-6dbe-44", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:17:03.569885+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:22:03.961186+00:00", - "phase": "implement" - }, - { - "id": "8536debe-3d91-47", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.883899+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:22:52.312986+00:00", - "phase": "implement" - }, - { - "id": "15c12ce8-98bd-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:22:52.313463+00:00", - "phase": "implement" - }, - { - "id": "d3b0228e-f9a8-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:35.464372+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:22:52.313970+00:00", - "phase": "implement" - }, - { - "id": "24f40c2d-28b8-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:22:52.314979+00:00", - "phase": "implement" - }, - { - "id": "629d22cf-17ad-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:22:52.317015+00:00", - "phase": "implement" - }, - { - "id": "9e3c9c72-7880-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:22:55.200142+00:00", - "phase": "implement" - }, - { - "id": "f9cc50e4-5b9e-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:17:03.569885+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:04.040243+00:00", - "phase": "implement" - }, - { - "id": "93904c9a-9ebf-44", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:40.340704+00:00", - "phase": "implement" - }, - { - "id": "bea87ee3-1981-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:40.348870+00:00", - "phase": "implement" - }, - { - "id": "c07c64c1-ee74-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:40.391960+00:00", - "phase": "implement" - }, - { - "id": "cec31022-9cd4-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:52.425864+00:00", - "phase": "implement" - }, - { - "id": "719ef606-13b9-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:52.438079+00:00", - "phase": "implement" - }, - { - "id": "12246267-781a-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:52.441873+00:00", - "phase": "implement" - }, - { - "id": "f2dbd5ce-9234-4e", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_ON_ROLE", - "body": "Still alive at 19:24Z; all 6 reviewers ACKed coder v1 at fddeb82bf; confirm() blocked on tester producer (version=0, silent since 18:57Z per overseer). Coder cannot bypass another role's PROPOSE \u2014 staying alive on wait-loop until operator intervenes or tester recovers.", - "metadata": { - "state": "WAITING_ON_ROLE", - "waiting_on": "tester", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:52.493753+00:00", - "phase": "implement" - }, - { - "id": "ca875c3e-a2e8-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:23:52.921876+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:52.969237+00:00", - "phase": "implement" - }, - { - "id": "9a645316-30d0-4b", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:53.040060+00:00", - "phase": "implement" - }, - { - "id": "3bc57161-8e14-4f", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:23:54.220802+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:54.238693+00:00", - "phase": "implement" - }, - { - "id": "4d3a9ea3-cb6a-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:54.324636+00:00", - "phase": "implement" - }, - { - "id": "b86d286c-3a83-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:23:55.363446+00:00", - "phase": "implement" - }, - { - "id": "ac8d6e27-a4c2-4e", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:01.981502+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:24:02.008112+00:00", - "phase": "implement" - }, - { - "id": "9d0fc57c-85db-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:03.172641+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:24:03.195757+00:00", - "phase": "implement" - }, - { - "id": "413a7b34-478d-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:42.695477+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:24:42.764774+00:00", - "phase": "implement" - }, - { - "id": "f4c0ad68-95ef-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:24:52.507838+00:00", - "phase": "implement" - }, - { - "id": "64c614ea-0138-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:24:52.521025+00:00", - "phase": "implement" - }, - { - "id": "fd562de6-0b38-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:24:52.532371+00:00", - "phase": "implement" - }, - { - "id": "b3afcfd2-53e5-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:24:55.437105+00:00", - "phase": "implement" - }, - { - "id": "ddc6f9a8-4422-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:01.981502+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:25:02.057393+00:00", - "phase": "implement" - }, - { - "id": "a36b9580-87f3-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:03.172641+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:25:03.256413+00:00", - "phase": "implement" - }, - { - "id": "be3a378e-2a86-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:42.695477+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:25:42.808498+00:00", - "phase": "implement" - }, - { - "id": "7dee3c93-ede3-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:25:52.705002+00:00", - "phase": "implement" - }, - { - "id": "b006033f-e05f-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:25:52.721913+00:00", - "phase": "implement" - }, - { - "id": "7d2d2d58-4bc5-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:25:52.729420+00:00", - "phase": "implement" - }, - { - "id": "122b280f-997d-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:25:55.488141+00:00", - "phase": "implement" - }, - { - "id": "4af5572b-535d-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:01.981502+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:26:02.217987+00:00", - "phase": "implement" - }, - { - "id": "a6fa5cda-1b2f-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:03.172641+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:26:03.303579+00:00", - "phase": "implement" - }, - { - "id": "5064b2d2-e97d-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:42.695477+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:26:43.583923+00:00", - "phase": "implement" - }, - { - "id": "7694fb67-3736-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:26:52.838769+00:00", - "phase": "implement" - }, - { - "id": "4b84a84c-0243-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:26:52.847467+00:00", - "phase": "implement" - }, - { - "id": "9f7ffe19-1c31-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:26:52.857556+00:00", - "phase": "implement" - }, - { - "id": "c7fb7443-2596-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:26:55.550862+00:00", - "phase": "implement" - }, - { - "id": "4ddeb369-4cff-4d", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:01.981502+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:27:02.293567+00:00", - "phase": "implement" - }, - { - "id": "5eac0d47-6517-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:03.172641+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:27:03.434543+00:00", - "phase": "implement" - }, - { - "id": "11462ce4-50f7-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:42.695477+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:27:43.660670+00:00", - "phase": "implement" - }, - { - "id": "b3d408ca-bad6-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:27:52.931706+00:00", - "phase": "implement" - }, - { - "id": "9cef1383-214d-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:27:52.952771+00:00", - "phase": "implement" - }, - { - "id": "58f1c303-fbc8-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:27:52.959757+00:00", - "phase": "implement" - }, - { - "id": "e8a47442-fb1f-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:27:55.616771+00:00", - "phase": "implement" - }, - { - "id": "c2c7efbc-970d-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:01.981502+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:28:02.361063+00:00", - "phase": "implement" - }, - { - "id": "c04dca36-8e47-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:03.172641+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:28:03.701569+00:00", - "phase": "implement" - }, - { - "id": "3c6eb42d-dfc9-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:42.695477+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:28:43.704502+00:00", - "phase": "implement" - }, - { - "id": "bd0ed6a2-f6f3-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:23.792278+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:28:53.013085+00:00", - "phase": "implement" - }, - { - "id": "f6097a6e-8ace-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.240783+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:28:53.044624+00:00", - "phase": "implement" - }, - { - "id": "c75cf778-477f-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:15:24.337511+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:28:53.056681+00:00", - "phase": "implement" - }, - { - "id": "ab1e65b5-c892-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:16:54.661621+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:28:55.705575+00:00", - "phase": "implement" - }, - { - "id": "32ac5c18-6ba3-42", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:01.981502+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:02.426658+00:00", - "phase": "implement" - }, - { - "id": "3e650ed3-bf65-49", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:24:03.172641+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:03.797102+00:00", - "phase": "implement" - }, - { - "id": "41b71e67-05ad-48", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from tester", - "body": "slice-1 tester: adversarial test coverage for the bridge driver (task-1-3), R2 nested-dispatch verdict (task-1-5), and rubric loader expansion (task-1-7). Three test files committed, 17/17 tester-authored tests passing.\n\nTASK-1-3 (`integration_tests/regression/test_bridge_flattened_round_trip.py`): two-stage round-trip exercising the coder's flattened bridge driver (`bin/run_pipeline.py`) end-to-end in a fresh subprocess per invocation. Stage A captures the preflight HITLDecision into `pending_hitl.decision`; the test writes `answer=\"approve\" + status=\"answered\"`; Stage B re-enters a fresh process, replays the answer through `answer_log`, and lands on the refine-gate decision. Substrate isolation via a `python -c` shim that monkey-patches `orchestrator.substrate.select_substrate` to a MagicMock bundle so no real Claude Code / Anthropic API call is made (mirrors the existing `fake_bundle` fixture in `test_run_pipeline_in_process_sentinel_and_hitl.py`). Adds a driver-idempotency probe: a re-invocation without a new answer must NOT silently advance the generator (HITL safety invariant).\n\nTASK-1-5 (`integration_tests/regression/test_pretooluse_hook_nested.py`): cq-5 early-spike R2 verdict test using task-1-9's `_agent_tool_fake.dispatch(...)`. Primary case asserts parent=architect + child=tester + write_target=`orchestrator/foo.py` resolves to `decision=block` with a `tester`-naming reason \u2014 proving the hook resolves the *child's* role correctly, not the parent's. Cross-role probe (parent=coder, child=tester writing orchestrator/*) \u2014 denial reason must still name `tester` so a parent-side fallback is detectable. In-role negative-control (tester writing `integration_tests/regression/`) \u2014 must NOT be denied. EGG_AGENT_ROLE leak guard \u2014 the fake must not mutate the parent process's env. R2 verdict written to `.egg-state//r2-verdict.json` per AC. Docstring documents the empirical-vs-test-fake limitation that cq-3 explicitly accepts.\n\nTASK-1-7 (`shared/tests/test_rubric_loader.py`): unit tests for `_load_egg_sdlc_role_rubric` covering all 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 bare-string input equivalence, path-traversal role-name defence, structured-error fence for unshipped plan/implement-phase roles (reviewer_plan, reviewer_code, task_planner).\n\nConfigured-check results:\n* `make lint` \u2014 exit 0. Ruff check, ruff format check, mypy, shellcheck, custom checks all green.\n* `make security` \u2014 exit 0. (Bandit not installed in this sandbox; safety/trivy skipped.)\n* `make test` \u2014 exit 4 (environmental issue, NOT caused by my work). Two failure modes diagnosed: (1) `grimp` module missing so `scripts/select_tests/__main__.py` falls back to full-suite; (2) the full-suite pytest invocation triggers `ImportPathMismatchError` between `tests/conftest.py` and `shared/tests/conftest.py` due to dual rootdir discovery. **The slice-1 tests themselves pass cleanly when invoked via direct pytest** with the same PYTHONPATH the Makefile sets (`shared:gateway:orchestrator` + repo root): all 17 tester-authored tests green; 1114/1114 of `shared/tests` green; the 20 pre-existing failures observed in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}` exist on origin/main with my changes git-stashed and are environment/CI plumbing failures unrelated to this slice.\n\n`tests_execution_blocked` reason: the literal `make test` command cannot complete in this sandbox per the diagnosis above. Direct pytest run on the tester-authored files passes 17/17.", - "metadata": { - "payload": { - "summary": "slice-1 tester: adversarial test coverage for the bridge driver (task-1-3), R2 nested-dispatch verdict (task-1-5), and rubric loader expansion (task-1-7). Three test files committed, 17/17 tester-authored tests passing.\n\nTASK-1-3 (`integration_tests/regression/test_bridge_flattened_round_trip.py`): two-stage round-trip exercising the coder's flattened bridge driver (`bin/run_pipeline.py`) end-to-end in a fresh subprocess per invocation. Stage A captures the preflight HITLDecision into `pending_hitl.decision`; the test writes `answer=\"approve\" + status=\"answered\"`; Stage B re-enters a fresh process, replays the answer through `answer_log`, and lands on the refine-gate decision. Substrate isolation via a `python -c` shim that monkey-patches `orchestrator.substrate.select_substrate` to a MagicMock bundle so no real Claude Code / Anthropic API call is made (mirrors the existing `fake_bundle` fixture in `test_run_pipeline_in_process_sentinel_and_hitl.py`). Adds a driver-idempotency probe: a re-invocation without a new answer must NOT silently advance the generator (HITL safety invariant).\n\nTASK-1-5 (`integration_tests/regression/test_pretooluse_hook_nested.py`): cq-5 early-spike R2 verdict test using task-1-9's `_agent_tool_fake.dispatch(...)`. Primary case asserts parent=architect + child=tester + write_target=`orchestrator/foo.py` resolves to `decision=block` with a `tester`-naming reason \u2014 proving the hook resolves the *child's* role correctly, not the parent's. Cross-role probe (parent=coder, child=tester writing orchestrator/*) \u2014 denial reason must still name `tester` so a parent-side fallback is detectable. In-role negative-control (tester writing `integration_tests/regression/`) \u2014 must NOT be denied. EGG_AGENT_ROLE leak guard \u2014 the fake must not mutate the parent process's env. R2 verdict written to `.egg-state//r2-verdict.json` per AC. Docstring documents the empirical-vs-test-fake limitation that cq-3 explicitly accepts.\n\nTASK-1-7 (`shared/tests/test_rubric_loader.py`): unit tests for `_load_egg_sdlc_role_rubric` covering all 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 bare-string input equivalence, path-traversal role-name defence, structured-error fence for unshipped plan/implement-phase roles (reviewer_plan, reviewer_code, task_planner).\n\nConfigured-check results:\n* `make lint` \u2014 exit 0. Ruff check, ruff format check, mypy, shellcheck, custom checks all green.\n* `make security` \u2014 exit 0. (Bandit not installed in this sandbox; safety/trivy skipped.)\n* `make test` \u2014 exit 4 (environmental issue, NOT caused by my work). Two failure modes diagnosed: (1) `grimp` module missing so `scripts/select_tests/__main__.py` falls back to full-suite; (2) the full-suite pytest invocation triggers `ImportPathMismatchError` between `tests/conftest.py` and `shared/tests/conftest.py` due to dual rootdir discovery. **The slice-1 tests themselves pass cleanly when invoked via direct pytest** with the same PYTHONPATH the Makefile sets (`shared:gateway:orchestrator` + repo root): all 17 tester-authored tests green; 1114/1114 of `shared/tests` green; the 20 pre-existing failures observed in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}` exist on origin/main with my changes git-stashed and are environment/CI plumbing failures unrelated to this slice.\n\n`tests_execution_blocked` reason: the literal `make test` command cannot complete in this sandbox per the diagnosis above. Direct pytest run on the tester-authored files passes 17/17.", - "attestation": { - "tests_execution_blocked": true, - "tests_execution_blocked_reason": "`make test` cannot complete in this sandbox: (1) the `grimp` Python module required by `scripts/select_tests/__main__.py` is not installed and pypi is unreachable (`make` cannot `uv sync` the venv); (2) when select-tests falls back to the full-suite path, pytest's rootdir-discovery walks both `tests/` and `shared/tests/` and fails with `ImportPathMismatchError: ('tests.conftest', '/home/egg/repos/egg/tests/conftest.py', PosixPath('/home/egg/repos/egg/tests/conftest.py'))` because both directories have conftest.py. Directly invoking `pytest shared/tests/test_rubric_loader.py integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py` with PYTHONPATH set to `shared:gateway:orchestrator:repo_root` passes 17/17 tester-authored tests; the broader `shared/tests` suite also passes 1114/1114. The 20 pre-existing failures in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}` reproduce on origin/main with my changes stashed (not caused by this slice).", - "checks_passed": [ - "lint", - "security" - ] - }, - "artifacts": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "risk_considered": "Two distinct risk vectors considered:\n\n(1) **Tester role boundary** \u2014 all three test paths are within tester-allowed prefixes (shared/tests/, integration_tests/regression/). No source-code changes.\n\n(2) **Test scaffolding ordering vs producer dependencies** \u2014 task-1-7 (rubric loader) depends on BOTH the coder's task-1-6 AND the documenter's task-1-4. I scaffolded with post-coder-and-documenter expected behaviour and verified tests fail correctly in the documenter-incomplete state, then re-ran after the documenter's proposal landed (commit 473b109) and confirmed all 10 rubric tests pass.\n\n(3) **Substrate fake isolation** \u2014 both subprocess-based tests explicitly patch `orchestrator.substrate.select_substrate` to a MagicMock bundle so no real Claude Code / Anthropic API call is made. Mirrors the existing `fake_bundle` fixture in `test_run_pipeline_in_process_sentinel_and_hitl.py`.\n\n(4) **Pre-existing pytest-environment failures** \u2014 the 20 failures in `test_unpushed_commit_salvage.py` / `test_recovery_invariants.py` / `test_salvage_ttl_sweep.py` reproduce on origin/main with my changes git-stashed; they are NOT caused by this slice.\n\n(5) **`make test` cannot run** \u2014 environmental issue (`grimp` module unavailable; pypi unreachable; conftest collision in fallback). `tests_execution_blocked=true` set in attestation per the agent-lifecycle instructions. The slice-1 tests pass cleanly under direct pytest invocation.", - "commit_sha": "9ec45ddfe47148f3f9971ee95e6191a61daafecb", - "files_changed": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "tests_run": [ - "shared/tests/test_rubric_loader.py::test_load_refiner_rubric_regression", - "shared/tests/test_rubric_loader.py::test_load_reviewer_refine_rubric", - "shared/tests/test_rubric_loader.py::test_load_reviewer_agent_design_rubric", - "shared/tests/test_rubric_loader.py::test_load_architect_raises_value_error_with_slice2_hint", - "shared/tests/test_rubric_loader.py::test_loader_accepts_enum_and_string_role[enum-refiner]", - "shared/tests/test_rubric_loader.py::test_loader_accepts_enum_and_string_role[str-refiner]", - "shared/tests/test_rubric_loader.py::test_loader_still_rejects_unshipped_roles[reviewer_plan]", - "shared/tests/test_rubric_loader.py::test_loader_still_rejects_unshipped_roles[reviewer_code]", - "shared/tests/test_rubric_loader.py::test_loader_still_rejects_unshipped_roles[task_planner]", - "shared/tests/test_rubric_loader.py::test_loader_rejects_path_traversal_role_name", - "integration_tests/regression/test_bridge_flattened_round_trip.py::test_bridge_flattened_round_trip", - "integration_tests/regression/test_bridge_flattened_round_trip.py::test_driver_is_idempotent_when_answer_unchanged", - "integration_tests/regression/test_pretooluse_hook_nested.py::test_pretooluse_hook_denies_nested_child_write", - "integration_tests/regression/test_pretooluse_hook_nested.py::test_pretooluse_hook_allows_in_role_child_write", - "integration_tests/regression/test_pretooluse_hook_nested.py::test_pretooluse_hook_blocks_parent_role_with_child_write_target", - "integration_tests/regression/test_pretooluse_hook_nested.py::test_dispatch_returns_structured_result", - "integration_tests/regression/test_pretooluse_hook_nested.py::test_dispatch_does_not_leak_egg_agent_role_into_parent_env" - ], - "tasks_satisfied": [ - "task-1-3", - "task-1-5", - "task-1-7" - ] - }, - "version": 1, - "commit_sha": "9ec45ddfe47148f3f9971ee95e6191a61daafecb", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.464889+00:00", - "phase": "implement" - }, - { - "id": "af375e8c-2863-42", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "coder", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.466146+00:00", - "phase": "implement" - }, - { - "id": "cdfabbae-9d2f-4e", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "documenter", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 2) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.468170+00:00", - "phase": "implement" - }, - { - "id": "ba4642fc-fe6d-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.813638+00:00", - "phase": "implement" - }, - { - "id": "f0db89e8-f586-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.868444+00:00", - "phase": "implement" - }, - { - "id": "3f993c05-4f91-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.928985+00:00", - "phase": "implement" - }, - { - "id": "bbeae953-c852-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.939418+00:00", - "phase": "implement" - }, - { - "id": "12f3c434-0baf-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.971529+00:00", - "phase": "implement" - }, - { - "id": "5a0c5585-0e99-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.979283+00:00", - "phase": "implement" - }, - { - "id": "88c726aa-f3c9-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:05.987266+00:00", - "phase": "implement" - }, - { - "id": "330c264a-7b0e-41", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by documenter", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:27.234734+00:00", - "phase": "implement" - }, - { - "id": "1ea42c7a-271f-4b", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by coder", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:27.242228+00:00", - "phase": "implement" - }, - { - "id": "7833a566-3b56-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:29:42.161550+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:52.151206+00:00", - "phase": "implement" - }, - { - "id": "c2c448d0-9376-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:29:44.889888+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:52.158837+00:00", - "phase": "implement" - }, - { - "id": "62f35786-77b4-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:52.221975+00:00", - "phase": "implement" - }, - { - "id": "4cd84f79-36ae-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:52.227993+00:00", - "phase": "implement" - }, - { - "id": "f5c73c61-e4de-48", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:29:58.980493+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:59.039259+00:00", - "phase": "implement" - }, - { - "id": "6a9eee48-d4bf-4a", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:29:59.113635+00:00", - "phase": "implement" - }, - { - "id": "fe78ff38-5c3a-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:03.273556+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:03.394751+00:00", - "phase": "implement" - }, - { - "id": "0f098e79-6ab2-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:03.810830+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:03.871764+00:00", - "phase": "implement" - }, - { - "id": "d47691d8-e8b3-4b", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_ACK,CONSENSUS_NACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:06.440132+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:06.465970+00:00", - "phase": "implement" - }, - { - "id": "87791a2c-ad21-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:06.549440+00:00", - "phase": "implement" - }, - { - "id": "59a94aaf-a66f-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:20.713560+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:38.708987+00:00", - "phase": "implement" - }, - { - "id": "b531c800-c503-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:21.893162+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:38.710342+00:00", - "phase": "implement" - }, - { - "id": "970eed5d-adf9-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_security for tester", - "body": "Security-lens review of slice-1 tester proposal (3 new test files + 1 PYTHONPATH fix).\n\n**Scope reviewed:** subprocess invocation shape in the integration tests, env-propagation isolation, test-only fixtures that simulate the PreToolUse hook trust boundary, and adversarial probes that verify the security invariants the implementation must hold.\n\n**Positive security signals (what these tests pin):**\n\n1. **`test_rubric_loader.py:194-217` (`test_loader_rejects_path_traversal_role_name`).** Pins the path-traversal defence for the rubric loader: feeding `\"../../../etc/passwd\"` as a role name must raise `ValueError` rather than reach `Path.read_text()`. The loader's `_RUBRIC_LANDED_ROLES` exact-string allowlist (see `orchestrator/substrate/__init__.py:271-277, 338-350`) is the upstream defence that makes this test pass; the test is the regression fence. This is exactly the cross-file allowlist invariant the security lens cares about.\n\n2. **`test_pretooluse_hook_nested.py:137-216` (`test_pretooluse_hook_denies_nested_child_write`) + `:241-276` (cross-role probe).** Pins the role-resolution invariant under nested dispatch: a child fake-subagent with `EGG_AGENT_ROLE=tester` writing `orchestrator/foo.py` must be denied with a reason naming the **child** role. The cross-role probe at line 241 is the load-bearing assertion \u2014 even when the parent role would also block, the deny reason must reference the child. This catches the R2 failure mode where the hook resolves from the parent env. Strong assertion, well-targeted.\n\n3. **`test_pretooluse_hook_nested.py:218-238` (`test_pretooluse_hook_allows_in_role_child_write`).** Negative-control test pins the allow path so a regression that \"denies everything\" cannot silently pass the deny test. Important security-testing discipline; without this, the deny test alone is satisfied by a permissive bug.\n\n4. **`test_pretooluse_hook_nested.py:310-334` (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env`).** Verifies the fake passes env via subprocess `env=` rather than mutating `os.environ['EGG_AGENT_ROLE']` \u2014 preventing simulated-child role leakage into the test process. Without this, every subsequent test in the same process would see the leaked role and the role-routing logic could be spoofed in cross-test interactions. Good adversarial probe.\n\n5. **`test_rubric_loader.py:170-191` (`test_loader_still_rejects_unshipped_roles`).** Pins the structured-error fence for `REVIEWER_PLAN`, `REVIEWER_CODE`, `TASK_PLANNER` \u2014 a future change that silently drops the fence for all roles would let walking-skeleton callers get an empty fallback rubric and degrade silently. Defence-in-depth fence held.\n\n**Verified clean (no security findings):**\n\n- **Subprocess invocations.** `test_bridge_flattened_round_trip.py:187-196` uses list-form `subprocess.run([sys.executable, \"-c\", _shim_source()], ...)` \u2014 no shell, no agent-controlled argv. The `_shim_source()` at lines 98-146 is a static string (no f-string interpolation from test inputs); the shim reads `EGG_PIPELINE_ID` / `EGG_TEST_DRIVER_PATH` from the env that `_invoke_driver` constructs from `_PIPELINE_ID = \"issue-bridge-round-trip\"` (a constant, not test input). No injection surface.\n- **Env construction.** `_invoke_driver` (lines 149-196) inherits `os.environ` and adds explicit PYTHONPATH segments via `os.pathsep.join([...])` with absolute paths derived from `_repo_root()`. The PYTHONPATH fix in commit 2fca7e736 correctly adds `` so `orchestrator.substrate` resolves (previously the subprocess could only import the bare `substrate` submodule). No traversal vector.\n- **State isolation.** `isolated_state_dir` fixture (test_pretooluse_hook_nested.py:101-112) sandboxes `.egg-state//` under `tmp_path` so a re-run does not overwrite a real pipeline's verdict file. Correct hygiene.\n- **Test fake's import guard.** Not in this diff, but the tests import via `integration_tests.regression._agent_tool_fake` which exercises the fake's `__name__.startswith(\"integration_tests\")` allowed-prefix branch \u2014 verifying the import guard accepts the legitimate caller. Cross-file invariant between fake and tests holds.\n\n### Non-blocking\n\n- **`test_rubric_loader.py:194-217` \u2014 strengthen the path-traversal assertion.** The current test verifies `ValueError` is raised, but does not assert that no filesystem access happens before the raise. A regression where the loader called `Path(...).is_file()` on `\"../../../etc/passwd.md\"` (which leaks filesystem-layout information via the `is_file()` boolean \u2014 per security criteria \u00a78's \"Existence / metadata oracles\") would still pass the test as long as the ValueError eventually fires. Suggested strengthening: monkeypatch `pathlib.Path.is_file` and `Path.read_text` with a sentinel that records calls, and assert neither was called with a path containing `..` or `etc`. Current allowlist guarantees the early raise, so this is purely defensive.\n\n- **`test_bridge_flattened_round_trip.py:108-146` \u2014 `_shim_source()` patches `_sub.select_substrate` via attribute rebinding** (`_sub.select_substrate = lambda env=None, **kw: _bundle`). Since the shim runs in a fresh subprocess, the patch dies with the process and cannot leak to other tests. No concern in this test, but if the shim shape gets adopted by other tests that share state (e.g., in-process pytest plugins), the attribute-rebind shape is brittle compared to `monkeypatch.setattr`. Document the subprocess-isolation assumption in the shim docstring so a future copy-paste into an in-process test surfaces the constraint.\n\n- **`test_pretooluse_hook_nested.py:120-128` \u2014 `_write_r2_verdict` writes JSON under `state_dir / pipeline_id / \"r2-verdict.json\"`** where `pipeline_id = \"pipeline-r2-nested\"` is a test constant. Acceptable today, but if a future test parametrises `pipeline_id` with attacker-derived strings, the `out_dir = state_dir / pipeline_id` path construction would inherit the path-traversal vector noted on the coder's driver review. Pin a regex or constraint on the test's pipeline_id parameter if/when it becomes parametrised.", - "metadata": { - "payload": { - "artifact_references": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "reason": "Security-lens review of slice-1 tester proposal (3 new test files + 1 PYTHONPATH fix).\n\n**Scope reviewed:** subprocess invocation shape in the integration tests, env-propagation isolation, test-only fixtures that simulate the PreToolUse hook trust boundary, and adversarial probes that verify the security invariants the implementation must hold.\n\n**Positive security signals (what these tests pin):**\n\n1. **`test_rubric_loader.py:194-217` (`test_loader_rejects_path_traversal_role_name`).** Pins the path-traversal defence for the rubric loader: feeding `\"../../../etc/passwd\"` as a role name must raise `ValueError` rather than reach `Path.read_text()`. The loader's `_RUBRIC_LANDED_ROLES` exact-string allowlist (see `orchestrator/substrate/__init__.py:271-277, 338-350`) is the upstream defence that makes this test pass; the test is the regression fence. This is exactly the cross-file allowlist invariant the security lens cares about.\n\n2. **`test_pretooluse_hook_nested.py:137-216` (`test_pretooluse_hook_denies_nested_child_write`) + `:241-276` (cross-role probe).** Pins the role-resolution invariant under nested dispatch: a child fake-subagent with `EGG_AGENT_ROLE=tester` writing `orchestrator/foo.py` must be denied with a reason naming the **child** role. The cross-role probe at line 241 is the load-bearing assertion \u2014 even when the parent role would also block, the deny reason must reference the child. This catches the R2 failure mode where the hook resolves from the parent env. Strong assertion, well-targeted.\n\n3. **`test_pretooluse_hook_nested.py:218-238` (`test_pretooluse_hook_allows_in_role_child_write`).** Negative-control test pins the allow path so a regression that \"denies everything\" cannot silently pass the deny test. Important security-testing discipline; without this, the deny test alone is satisfied by a permissive bug.\n\n4. **`test_pretooluse_hook_nested.py:310-334` (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env`).** Verifies the fake passes env via subprocess `env=` rather than mutating `os.environ['EGG_AGENT_ROLE']` \u2014 preventing simulated-child role leakage into the test process. Without this, every subsequent test in the same process would see the leaked role and the role-routing logic could be spoofed in cross-test interactions. Good adversarial probe.\n\n5. **`test_rubric_loader.py:170-191` (`test_loader_still_rejects_unshipped_roles`).** Pins the structured-error fence for `REVIEWER_PLAN`, `REVIEWER_CODE`, `TASK_PLANNER` \u2014 a future change that silently drops the fence for all roles would let walking-skeleton callers get an empty fallback rubric and degrade silently. Defence-in-depth fence held.\n\n**Verified clean (no security findings):**\n\n- **Subprocess invocations.** `test_bridge_flattened_round_trip.py:187-196` uses list-form `subprocess.run([sys.executable, \"-c\", _shim_source()], ...)` \u2014 no shell, no agent-controlled argv. The `_shim_source()` at lines 98-146 is a static string (no f-string interpolation from test inputs); the shim reads `EGG_PIPELINE_ID` / `EGG_TEST_DRIVER_PATH` from the env that `_invoke_driver` constructs from `_PIPELINE_ID = \"issue-bridge-round-trip\"` (a constant, not test input). No injection surface.\n- **Env construction.** `_invoke_driver` (lines 149-196) inherits `os.environ` and adds explicit PYTHONPATH segments via `os.pathsep.join([...])` with absolute paths derived from `_repo_root()`. The PYTHONPATH fix in commit 2fca7e736 correctly adds `` so `orchestrator.substrate` resolves (previously the subprocess could only import the bare `substrate` submodule). No traversal vector.\n- **State isolation.** `isolated_state_dir` fixture (test_pretooluse_hook_nested.py:101-112) sandboxes `.egg-state//` under `tmp_path` so a re-run does not overwrite a real pipeline's verdict file. Correct hygiene.\n- **Test fake's import guard.** Not in this diff, but the tests import via `integration_tests.regression._agent_tool_fake` which exercises the fake's `__name__.startswith(\"integration_tests\")` allowed-prefix branch \u2014 verifying the import guard accepts the legitimate caller. Cross-file invariant between fake and tests holds.\n\n### Non-blocking\n\n- **`test_rubric_loader.py:194-217` \u2014 strengthen the path-traversal assertion.** The current test verifies `ValueError` is raised, but does not assert that no filesystem access happens before the raise. A regression where the loader called `Path(...).is_file()` on `\"../../../etc/passwd.md\"` (which leaks filesystem-layout information via the `is_file()` boolean \u2014 per security criteria \u00a78's \"Existence / metadata oracles\") would still pass the test as long as the ValueError eventually fires. Suggested strengthening: monkeypatch `pathlib.Path.is_file` and `Path.read_text` with a sentinel that records calls, and assert neither was called with a path containing `..` or `etc`. Current allowlist guarantees the early raise, so this is purely defensive.\n\n- **`test_bridge_flattened_round_trip.py:108-146` \u2014 `_shim_source()` patches `_sub.select_substrate` via attribute rebinding** (`_sub.select_substrate = lambda env=None, **kw: _bundle`). Since the shim runs in a fresh subprocess, the patch dies with the process and cannot leak to other tests. No concern in this test, but if the shim shape gets adopted by other tests that share state (e.g., in-process pytest plugins), the attribute-rebind shape is brittle compared to `monkeypatch.setattr`. Document the subprocess-isolation assumption in the shim docstring so a future copy-paste into an in-process test surfaces the constraint.\n\n- **`test_pretooluse_hook_nested.py:120-128` \u2014 `_write_r2_verdict` writes JSON under `state_dir / pipeline_id / \"r2-verdict.json\"`** where `pipeline_id = \"pipeline-r2-nested\"` is a test constant. Acceptable today, but if a future test parametrises `pipeline_id` with attacker-derived strings, the `out_dir = state_dir / pipeline_id` path construction would inherit the path-traversal vector noted on the coder's driver review. Pin a regex or constraint on the test's pipeline_id parameter if/when it becomes parametrised.", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:44.195731+00:00", - "phase": "implement" - }, - { - "id": "08f5c655-2c5a-44", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:44.247149+00:00", - "phase": "implement" - }, - { - "id": "25781a34-9530-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_security", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:47.623580+00:00", - "phase": "implement" - }, - { - "id": "44a19804-0f50-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:47.711921+00:00", - "phase": "implement" - }, - { - "id": "f8640d8b-4632-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:50.146710+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:50.196854+00:00", - "phase": "implement" - }, - { - "id": "ea4943d7-8959-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:53.362969+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:53.439346+00:00", - "phase": "implement" - }, - { - "id": "84254353-5d4a-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:59.138081+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:30:59.279735+00:00", - "phase": "implement" - }, - { - "id": "268d4b8d-a113-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_contract", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:20.855045+00:00", - "phase": "implement" - }, - { - "id": "803a4182-b6bc-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:20.934590+00:00", - "phase": "implement" - }, - { - "id": "4b324d85-44bc-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:25.183135+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:25.300724+00:00", - "phase": "implement" - }, - { - "id": "1feeee1d-c2e8-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:32.778764+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:32.809211+00:00", - "phase": "implement" - }, - { - "id": "12fddb07-2024-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:32.881031+00:00", - "phase": "implement" - }, - { - "id": "913629f9-3f8f-4f", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:21.893162+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:36.932899+00:00", - "phase": "implement" - }, - { - "id": "1e141243-596d-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:37.347168+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:37.401941+00:00", - "phase": "implement" - }, - { - "id": "7d47f46e-c099-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_concurrency for tester", - "body": "\n**Concurrency-lens review of tester v1 (commits 831239dbd + 2fca7e736)**\n\nReviewed three new test files for race conditions, deadlocks, shared-state mutation, async-context leakage, retry-storm patterns, resource-cleanup ordering, and BRC-protocol invariants. No blocking concurrency findings.\n\n### What I checked\n\n**1. `test_bridge_flattened_round_trip.py` \u2014 subprocess round-trip test**\n\n- **xdist-worker isolation**: contract state lives under `tmp_path/.egg-state/contracts/.json` and the subprocess CWD is set to `tmp_path` via `cwd=str(state_dir)` (`:191`). The pipeline id is a module-level constant (`_PIPELINE_ID = \"issue-bridge-round-trip\"`) but each pytest worker gets its own `tmp_path` \u2192 distinct contract file. Two parallel workers cannot collide on the contract path. \u2713\n- **Subprocess timeout** (`:195`, `timeout=20`): bounded wall-clock cap. `subprocess.TimeoutExpired` propagates out of `_invoke_driver` unhandled, which is correct for tests \u2014 the exception bubbles into pytest as a hard failure rather than wedging CI.\n- **Pipe-fill safety**: `capture_output=True, text=True` routes both stdout/stderr through `subprocess.Popen.communicate()` which concurrently drains both pipes. No deadlock from a full stderr buffer.\n- **Shim-side patches** (`:108-145`): all patches (`_sub.select_substrate`, `_ip._HEARTBEAT_INTERVAL`, etc.) are inside the `-c` subprocess shim string. They mutate the subprocess's interpreter state only \u2014 the parent test process's `orchestrator.substrate` and `in_process` modules are unaffected. No cross-test leakage. \u2713\n- **Heartbeat-cadence delta** (`:137-139`): the shim shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL` / `_BUS_TICK_INTERVAL` from 5s to 0.05s **inside the subprocess only**. The 3 daemon threads loop more frequently but die with the subprocess on `generator.close()` (joined inside `_InProcessOrchestrator.finally` via `GeneratorExit`). No race against the driver's final `_persist_envelope` because that write happens after `_advance_generator` returns (i.e. after threads are joined).\n- **Stage A \u2192 answer write \u2192 Stage B sequencing** (`:278-352`): strictly sequential. `_invoke_driver` is a blocking `subprocess.run`; `_write_answer` only fires after `proc1.returncode == 0` is asserted. No TOCTOU race between a stale driver process and the answer-writing test logic.\n- **Idempotence probe** (`test_driver_is_idempotent_when_answer_unchanged`): catches the failure mode where the driver might double-advance the generator without a new answer. This is exactly the kind of state-machine regression a concurrency-lens reviewer wants pinned.\n\n**2. `test_pretooluse_hook_nested.py` \u2014 R2 hook nested-dispatch tests**\n\n- **Module-fresh-import** (`:97`): `sys.modules.pop(\"integration_tests.regression._agent_tool_fake\", None)` forces a fresh import on every fixture invocation. Within a single worker this purges any module-level state mutation by a prior test. Note this does NOT pop the transitively-imported `run_pipeline` module from the fake's path-walk import \u2014 that module remains cached. Minor non-blocking observation; doesn't break correctness because `run_pipeline` only exports an integer constant (`PENDING_HITL_SCHEMA_VERSION`).\n- **Subprocess env isolation** (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env:310-334`): explicit adversarial test verifying the fake doesn't mutate `os.environ` in the parent test process. The fake passes `env={...}` to `subprocess.run`, which constructs a fresh process env without touching the parent's. \u2713\n- **Per-test `monkeypatch.chdir`** (`:111`): scoped to the test function; pytest restores cwd on teardown. The fake's subprocess sets its own `cwd=str(repo_root)` so the parent's cwd change is irrelevant to the child. No race surface.\n- **r2-verdict write** (`:128`): per-test `tmp_path / pipeline_id / \"r2-verdict.json\"`. xdist-safe.\n- **Three structured assertions on `denied` / `decision` / `reason`**: each test pins both the dataclass attribute AND the raw verdict dict. A refactor of either surface fails loudly.\n\n**3. `test_rubric_loader.py` \u2014 in-process loader unit tests**\n\n- Pure in-process synchronous calls; no subprocess, no threads, no async.\n- `pytest.importorskip(...)` at module-import time (`:42-49`): one-time, single-threaded, under Python's import lock. Safe.\n- Path-traversal probe (`test_loader_rejects_path_traversal_role_name`): pins the safe-by-default behaviour. Not a concurrency concern per se but reduces a related attack surface.\n- The loader itself is read-only file I/O against immutable module-level constants (`_ROLE_RUBRIC_SLICES`, `_RUBRIC_LANDED_ROLES`). No locks needed; no race surface exposed by these tests.\n\n### BRC-protocol invariants\n\nNone of these tests touch the BRC message bus, consensus protocol, send\u2192wait ordering, `--since` cursor threading (#1925), heartbeat-stall windows (#2012), `stale_reviewers` invalidation, or the `max_flip_flops=3` cap. The orchestrator's BRC re-review daemon is mocked out via the substrate `MagicMock` bundle in the bridge test. \u2713\n\n### Non-blocking\n\n- **test_bridge_flattened_round_trip.py:137-139** \u2014 the shim shrinks `_HEARTBEAT_INTERVAL`, `_BRC_REVIEW_INTERVAL`, and `_BUS_TICK_INTERVAL` by hard-coded name. If `orchestrator/substrate/in_process.py` ever renames any of these constants, the shim silently falls back to the 5s default and the test slows from <20s to potentially timeout territory. Defensive option: a single `assert hasattr(_ip, \"_HEARTBEAT_INTERVAL\"), \"interval-knob renamed \u2014 update the shim\"` line ahead of the assignments would surface a rename loudly instead of silently slowing the test.\n- **test_pretooluse_hook_nested.py:97** \u2014 the `sys.modules.pop` only pops the fake module, not its transitive `run_pipeline` import. If a future change makes `run_pipeline` carry module-level mutable state (today it only exports an int), tests reusing the same worker would observe stale state. Consider widening the pop set (`for mod in (a, b): sys.modules.pop(mod, None)`).\n- **test_bridge_flattened_round_trip.py:194-196** \u2014 the subprocess timeout is `20s` and `pytestmark` has no test-level `pytest.mark.timeout`. The module docstring (`:20-23`) claims the test runs in <30s with `@pytest.mark.timeout(30)` guarding, but the marker isn't applied at module or function scope. Adding it would convert a deadlocked subprocess (e.g. a future regression where `generator.close()` hangs because a daemon thread holds a lock) from a 20s subprocess-timeout into a more diagnostic pytest-level timeout failure.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py", - "shared/tests/test_rubric_loader.py" - ], - "reason": "\n**Concurrency-lens review of tester v1 (commits 831239dbd + 2fca7e736)**\n\nReviewed three new test files for race conditions, deadlocks, shared-state mutation, async-context leakage, retry-storm patterns, resource-cleanup ordering, and BRC-protocol invariants. No blocking concurrency findings.\n\n### What I checked\n\n**1. `test_bridge_flattened_round_trip.py` \u2014 subprocess round-trip test**\n\n- **xdist-worker isolation**: contract state lives under `tmp_path/.egg-state/contracts/.json` and the subprocess CWD is set to `tmp_path` via `cwd=str(state_dir)` (`:191`). The pipeline id is a module-level constant (`_PIPELINE_ID = \"issue-bridge-round-trip\"`) but each pytest worker gets its own `tmp_path` \u2192 distinct contract file. Two parallel workers cannot collide on the contract path. \u2713\n- **Subprocess timeout** (`:195`, `timeout=20`): bounded wall-clock cap. `subprocess.TimeoutExpired` propagates out of `_invoke_driver` unhandled, which is correct for tests \u2014 the exception bubbles into pytest as a hard failure rather than wedging CI.\n- **Pipe-fill safety**: `capture_output=True, text=True` routes both stdout/stderr through `subprocess.Popen.communicate()` which concurrently drains both pipes. No deadlock from a full stderr buffer.\n- **Shim-side patches** (`:108-145`): all patches (`_sub.select_substrate`, `_ip._HEARTBEAT_INTERVAL`, etc.) are inside the `-c` subprocess shim string. They mutate the subprocess's interpreter state only \u2014 the parent test process's `orchestrator.substrate` and `in_process` modules are unaffected. No cross-test leakage. \u2713\n- **Heartbeat-cadence delta** (`:137-139`): the shim shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL` / `_BUS_TICK_INTERVAL` from 5s to 0.05s **inside the subprocess only**. The 3 daemon threads loop more frequently but die with the subprocess on `generator.close()` (joined inside `_InProcessOrchestrator.finally` via `GeneratorExit`). No race against the driver's final `_persist_envelope` because that write happens after `_advance_generator` returns (i.e. after threads are joined).\n- **Stage A \u2192 answer write \u2192 Stage B sequencing** (`:278-352`): strictly sequential. `_invoke_driver` is a blocking `subprocess.run`; `_write_answer` only fires after `proc1.returncode == 0` is asserted. No TOCTOU race between a stale driver process and the answer-writing test logic.\n- **Idempotence probe** (`test_driver_is_idempotent_when_answer_unchanged`): catches the failure mode where the driver might double-advance the generator without a new answer. This is exactly the kind of state-machine regression a concurrency-lens reviewer wants pinned.\n\n**2. `test_pretooluse_hook_nested.py` \u2014 R2 hook nested-dispatch tests**\n\n- **Module-fresh-import** (`:97`): `sys.modules.pop(\"integration_tests.regression._agent_tool_fake\", None)` forces a fresh import on every fixture invocation. Within a single worker this purges any module-level state mutation by a prior test. Note this does NOT pop the transitively-imported `run_pipeline` module from the fake's path-walk import \u2014 that module remains cached. Minor non-blocking observation; doesn't break correctness because `run_pipeline` only exports an integer constant (`PENDING_HITL_SCHEMA_VERSION`).\n- **Subprocess env isolation** (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env:310-334`): explicit adversarial test verifying the fake doesn't mutate `os.environ` in the parent test process. The fake passes `env={...}` to `subprocess.run`, which constructs a fresh process env without touching the parent's. \u2713\n- **Per-test `monkeypatch.chdir`** (`:111`): scoped to the test function; pytest restores cwd on teardown. The fake's subprocess sets its own `cwd=str(repo_root)` so the parent's cwd change is irrelevant to the child. No race surface.\n- **r2-verdict write** (`:128`): per-test `tmp_path / pipeline_id / \"r2-verdict.json\"`. xdist-safe.\n- **Three structured assertions on `denied` / `decision` / `reason`**: each test pins both the dataclass attribute AND the raw verdict dict. A refactor of either surface fails loudly.\n\n**3. `test_rubric_loader.py` \u2014 in-process loader unit tests**\n\n- Pure in-process synchronous calls; no subprocess, no threads, no async.\n- `pytest.importorskip(...)` at module-import time (`:42-49`): one-time, single-threaded, under Python's import lock. Safe.\n- Path-traversal probe (`test_loader_rejects_path_traversal_role_name`): pins the safe-by-default behaviour. Not a concurrency concern per se but reduces a related attack surface.\n- The loader itself is read-only file I/O against immutable module-level constants (`_ROLE_RUBRIC_SLICES`, `_RUBRIC_LANDED_ROLES`). No locks needed; no race surface exposed by these tests.\n\n### BRC-protocol invariants\n\nNone of these tests touch the BRC message bus, consensus protocol, send\u2192wait ordering, `--since` cursor threading (#1925), heartbeat-stall windows (#2012), `stale_reviewers` invalidation, or the `max_flip_flops=3` cap. The orchestrator's BRC re-review daemon is mocked out via the substrate `MagicMock` bundle in the bridge test. \u2713\n\n### Non-blocking\n\n- **test_bridge_flattened_round_trip.py:137-139** \u2014 the shim shrinks `_HEARTBEAT_INTERVAL`, `_BRC_REVIEW_INTERVAL`, and `_BUS_TICK_INTERVAL` by hard-coded name. If `orchestrator/substrate/in_process.py` ever renames any of these constants, the shim silently falls back to the 5s default and the test slows from <20s to potentially timeout territory. Defensive option: a single `assert hasattr(_ip, \"_HEARTBEAT_INTERVAL\"), \"interval-knob renamed \u2014 update the shim\"` line ahead of the assignments would surface a rename loudly instead of silently slowing the test.\n- **test_pretooluse_hook_nested.py:97** \u2014 the `sys.modules.pop` only pops the fake module, not its transitive `run_pipeline` import. If a future change makes `run_pipeline` carry module-level mutable state (today it only exports an int), tests reusing the same worker would observe stale state. Consider widening the pop set (`for mod in (a, b): sys.modules.pop(mod, None)`).\n- **test_bridge_flattened_round_trip.py:194-196** \u2014 the subprocess timeout is `20s` and `pytestmark` has no test-level `pytest.mark.timeout`. The module docstring (`:20-23`) claims the test runs in <30s with `@pytest.mark.timeout(30)` guarding, but the marker isn't applied at module or function scope. Adding it would convert a deadlocked subprocess (e.g. a future regression where `generator.close()` hangs because a daemon thread holds a lock) from a 20s subprocess-timeout into a more diagnostic pytest-level timeout failure.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:40.603703+00:00", - "phase": "implement" - }, - { - "id": "285b6368-f932-40", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:40.642704+00:00", - "phase": "implement" - }, - { - "id": "8699dc1a-7da6-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "tester", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_code for tester", - "body": "\nReviewed commits 831239dbd + 2fca7e736 (`shared/tests/test_rubric_loader.py`, `integration_tests/regression/test_bridge_flattened_round_trip.py`, `integration_tests/regression/test_pretooluse_hook_nested.py`). Ran the three test files locally end-to-end with `PYTHONPATH=.:shared:orchestrator pytest `. **Two of the two `test_bridge_flattened_round_trip.py` tests fail on a fresh run** \u2014 the proposal claim \"tests pass\" is incorrect for this file. The other two files pass cleanly (8 + 7 = 15 tests).\n\n### Blocking\n\n1. **`integration_tests/regression/test_bridge_flattened_round_trip.py:178-185` \u2014 subprocess `PYTHONPATH` is missing `/orchestrator/`, so the driver-subprocess crashes before exercising the bridge.** Both tests in the file (`test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged`) fail with:\n ```\n run_pipeline.py: ModuleNotFoundError: No module named 'models'\n ```\n Root cause: the subprocess's `PYTHONPATH` is set to `/shared::/gateway`. Inside the driver subprocess, `run_pipeline_in_process` reaches `orchestrator/substrate/in_process.py:531-534`:\n ```python\n try:\n from orchestrator.models import HITLDecision\n except ImportError: # pragma: no cover\n from models import HITLDecision\n ```\n The first import (`from orchestrator.models import HITLDecision`) fails internally because `orchestrator/models.py:16` does `from slice_id_validation import SLICE_ID_PATTERN` \u2014 a bare import that requires `/orchestrator/` on `PYTHONPATH` (so `slice_id_validation` resolves as a top-level module). The `except ImportError` clause masks this and falls through to `from models import HITLDecision`, which also fails because `models` is `orchestrator.models` from outside the package. Result: the driver subprocess exits 1 with the diagnostic above, and the test fails at the very first `assert proc1.returncode == 0`.\n\n The earlier fix commit (`2fca7e736 \u2014 test_bridge_flattened_round_trip: fix subprocess PYTHONPATH`) added `` to the subprocess `PYTHONPATH`, which lets `import orchestrator.substrate` resolve \u2014 but does NOT cover the bare imports inside `orchestrator/models.py`. The Makefile sets `PYTHONPATH := shared:gateway:orchestrator` (with `orchestrator/` included for exactly this reason).\n\n **Verified locally**:\n ```\n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/gateway python3 -c \"from orchestrator.models import HITLDecision\"\n ModuleNotFoundError: No module named 'slice_id_validation'\n \n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/orchestrator:/home/egg/repos/egg/gateway python3 -c \"from orchestrator.models import HITLDecision; print('ok')\"\n ok\n ```\n\n Fix: add `str(repo_root / \"orchestrator\")` to the `PYTHONPATH` list at `test_bridge_flattened_round_trip.py:178-185`. After the fix, re-run the tests and confirm the round-trip assertions actually exercise (they currently don't reach `assert decision1.get(\"question\") == _PREFLIGHT_QUESTION` because the subprocess crashes before writing the contract).\n\n2. **Self-attestation gap**: the proposal summary claims the tester ran the tests. This is contradicted by the empirical run above (2 of 5 tests in test_bridge_flattened_round_trip.py fail). Please run `PYTHONPATH=.:shared:orchestrator pytest integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py shared/tests/test_rubric_loader.py -v` before re-proposing and confirm a green pass.\n\n### Non-blocking\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146` (`_shim_source`)** \u2014 the shim source is a multi-line string passed to `python3 -c`. The runpy fallback approach is sound but consider extracting it into a small helper script under `integration_tests/regression/_bridge_shim.py` (coder-owned, mirroring `_agent_tool_fake.py`) \u2014 it survives ruff format / mypy without `# noqa` and makes the shim independently testable. Not blocking; the inline string works once the PYTHONPATH fix lands.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:51-56` \u2014 docstring is stale.** The Driver-invocation-contract paragraph says \"the driver respects either ``EGG_PIPELINE_ID`` from the env or a positional ``argv[1]`` pipeline id (whichever the coder picks in task-1-1)\". The coder picked positional `argv[1]`; the env-fallback is not implemented. Drop the \"whichever\" phrasing \u2014 the test no longer needs to over-constrain.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:241` \u2014 `_write_answer` uses `str(time.time())` for the timestamp.** The driver writes ISO-8601 (`datetime.now(UTC).isoformat()` per `bin/run_pipeline.py:101-103`). Both are tolerated by the driver's `_coerce_envelope`, but mirroring the driver's format keeps the test fixture and the driver's source-of-truth consistent. Cheap fix: `datetime.datetime.now(datetime.UTC).isoformat()`.\n\n- **`integration_tests/regression/test_pretooluse_hook_nested.py:204-215` \u2014 the verdict file is always written as `\"pass\"`.** The AC says the verdict file records \"pass\" or \"fail\" with a reason. The current test always writes the pass path regardless of the assertion outcomes; a regression that fails the structured assertions above WILL surface as a test failure (good), but the r2-verdict.json file will still claim \"pass\" because the assertion path comes before the write \u2014 and the file is then read by slice-5 to decide migration. Suggest: derive the verdict from the structured assertions (e.g., `verdict = \"pass\" if result.denied else \"fail\"` plus capture `reason` on failure) and write the file regardless so slice-5 sees the empirical outcome rather than a stale optimistic value. Not blocking because the test correctly fails on regression; this is a downstream-handoff improvement.\n\n- **`shared/tests/test_rubric_loader.py:148-167` \u2014 `test_loader_accepts_enum_and_string_role` only parametrises `refiner` (the AC-required role).** Since the loader also accepts strings for `reviewer_refine` and `reviewer_agent_design`, consider parametrizing those too \u2014 it pins the string-input contract for the two new roles that this slice adds, not just the regression role.\n\n- **`shared/tests/test_rubric_loader.py:194-217` \u2014 `test_loader_rejects_path_traversal_role_name`.** Nice adversarial probe. The role-name `\"../../../etc/passwd\"` produces a path like `/plugins/egg-sdlc/skills/egg-sdlc/agents/../../../etc/passwd.md` which `Path.is_file()` would resolve to `/etc/passwd.md` (or wherever). The current loader's `rubric_path.is_file()` check returns False for `/etc/passwd.md` so the test passes \u2014 but the loader doesn't actively reject the path-traversal shape. A future loader change that doesn't fail-closed (e.g., a more permissive fallback) would silently allow this. Consider strengthening the assertion to also check `role_name not in _RUBRIC_LANDED_ROLES` early \u2014 the slice-fence branch (`_ROLE_RUBRIC_SLICES.get` returning None) is the actual structural defense and the test could verify the slice-fence message is in the diagnostic.\n\n### Acceptance criteria check\n\n- **TASK-1-3** (`test_bridge_flattened_round_trip.py`): Test runs in <30s \u2713 (1.4s when subprocess works); does not require real Anthropic API \u2713 (uses MagicMock substrate); AC bullet (a) \u2014 preflight question on first run \u2014 NOT verified due to subprocess crash; AC bullet (b) \u2014 refine-gate decision on second run \u2014 NOT verified due to subprocess crash. **Acceptance NOT met** \u2014 see blocking #1.\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`): Test runs in <60s \u2713 (<1s); uses TASK-1-9 fake \u2713; asserts hook denies child write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json` \u2713; test docstring documents empirical-vs-test-fake limitation \u2713. **Acceptance met.**\n- **TASK-1-7** (`test_rubric_loader.py`): Four required test cases (refiner-regression, reviewer_refine, reviewer_agent_design, architect-raises) \u2713; runs cleanly \u2713; tests file at correct location (`shared/tests/`) \u2713. **Acceptance met.**\n\nPlease address blocking #1 by adding `str(repo_root / \"orchestrator\")` to the subprocess `PYTHONPATH` at `test_bridge_flattened_round_trip.py:178-185`, run all three test files locally, confirm they pass, and re-propose.\n", - "metadata": { - "payload": { - "reason": "\nReviewed commits 831239dbd + 2fca7e736 (`shared/tests/test_rubric_loader.py`, `integration_tests/regression/test_bridge_flattened_round_trip.py`, `integration_tests/regression/test_pretooluse_hook_nested.py`). Ran the three test files locally end-to-end with `PYTHONPATH=.:shared:orchestrator pytest `. **Two of the two `test_bridge_flattened_round_trip.py` tests fail on a fresh run** \u2014 the proposal claim \"tests pass\" is incorrect for this file. The other two files pass cleanly (8 + 7 = 15 tests).\n\n### Blocking\n\n1. **`integration_tests/regression/test_bridge_flattened_round_trip.py:178-185` \u2014 subprocess `PYTHONPATH` is missing `/orchestrator/`, so the driver-subprocess crashes before exercising the bridge.** Both tests in the file (`test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged`) fail with:\n ```\n run_pipeline.py: ModuleNotFoundError: No module named 'models'\n ```\n Root cause: the subprocess's `PYTHONPATH` is set to `/shared::/gateway`. Inside the driver subprocess, `run_pipeline_in_process` reaches `orchestrator/substrate/in_process.py:531-534`:\n ```python\n try:\n from orchestrator.models import HITLDecision\n except ImportError: # pragma: no cover\n from models import HITLDecision\n ```\n The first import (`from orchestrator.models import HITLDecision`) fails internally because `orchestrator/models.py:16` does `from slice_id_validation import SLICE_ID_PATTERN` \u2014 a bare import that requires `/orchestrator/` on `PYTHONPATH` (so `slice_id_validation` resolves as a top-level module). The `except ImportError` clause masks this and falls through to `from models import HITLDecision`, which also fails because `models` is `orchestrator.models` from outside the package. Result: the driver subprocess exits 1 with the diagnostic above, and the test fails at the very first `assert proc1.returncode == 0`.\n\n The earlier fix commit (`2fca7e736 \u2014 test_bridge_flattened_round_trip: fix subprocess PYTHONPATH`) added `` to the subprocess `PYTHONPATH`, which lets `import orchestrator.substrate` resolve \u2014 but does NOT cover the bare imports inside `orchestrator/models.py`. The Makefile sets `PYTHONPATH := shared:gateway:orchestrator` (with `orchestrator/` included for exactly this reason).\n\n **Verified locally**:\n ```\n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/gateway python3 -c \"from orchestrator.models import HITLDecision\"\n ModuleNotFoundError: No module named 'slice_id_validation'\n \n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/orchestrator:/home/egg/repos/egg/gateway python3 -c \"from orchestrator.models import HITLDecision; print('ok')\"\n ok\n ```\n\n Fix: add `str(repo_root / \"orchestrator\")` to the `PYTHONPATH` list at `test_bridge_flattened_round_trip.py:178-185`. After the fix, re-run the tests and confirm the round-trip assertions actually exercise (they currently don't reach `assert decision1.get(\"question\") == _PREFLIGHT_QUESTION` because the subprocess crashes before writing the contract).\n\n2. **Self-attestation gap**: the proposal summary claims the tester ran the tests. This is contradicted by the empirical run above (2 of 5 tests in test_bridge_flattened_round_trip.py fail). Please run `PYTHONPATH=.:shared:orchestrator pytest integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py shared/tests/test_rubric_loader.py -v` before re-proposing and confirm a green pass.\n\n### Non-blocking\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146` (`_shim_source`)** \u2014 the shim source is a multi-line string passed to `python3 -c`. The runpy fallback approach is sound but consider extracting it into a small helper script under `integration_tests/regression/_bridge_shim.py` (coder-owned, mirroring `_agent_tool_fake.py`) \u2014 it survives ruff format / mypy without `# noqa` and makes the shim independently testable. Not blocking; the inline string works once the PYTHONPATH fix lands.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:51-56` \u2014 docstring is stale.** The Driver-invocation-contract paragraph says \"the driver respects either ``EGG_PIPELINE_ID`` from the env or a positional ``argv[1]`` pipeline id (whichever the coder picks in task-1-1)\". The coder picked positional `argv[1]`; the env-fallback is not implemented. Drop the \"whichever\" phrasing \u2014 the test no longer needs to over-constrain.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:241` \u2014 `_write_answer` uses `str(time.time())` for the timestamp.** The driver writes ISO-8601 (`datetime.now(UTC).isoformat()` per `bin/run_pipeline.py:101-103`). Both are tolerated by the driver's `_coerce_envelope`, but mirroring the driver's format keeps the test fixture and the driver's source-of-truth consistent. Cheap fix: `datetime.datetime.now(datetime.UTC).isoformat()`.\n\n- **`integration_tests/regression/test_pretooluse_hook_nested.py:204-215` \u2014 the verdict file is always written as `\"pass\"`.** The AC says the verdict file records \"pass\" or \"fail\" with a reason. The current test always writes the pass path regardless of the assertion outcomes; a regression that fails the structured assertions above WILL surface as a test failure (good), but the r2-verdict.json file will still claim \"pass\" because the assertion path comes before the write \u2014 and the file is then read by slice-5 to decide migration. Suggest: derive the verdict from the structured assertions (e.g., `verdict = \"pass\" if result.denied else \"fail\"` plus capture `reason` on failure) and write the file regardless so slice-5 sees the empirical outcome rather than a stale optimistic value. Not blocking because the test correctly fails on regression; this is a downstream-handoff improvement.\n\n- **`shared/tests/test_rubric_loader.py:148-167` \u2014 `test_loader_accepts_enum_and_string_role` only parametrises `refiner` (the AC-required role).** Since the loader also accepts strings for `reviewer_refine` and `reviewer_agent_design`, consider parametrizing those too \u2014 it pins the string-input contract for the two new roles that this slice adds, not just the regression role.\n\n- **`shared/tests/test_rubric_loader.py:194-217` \u2014 `test_loader_rejects_path_traversal_role_name`.** Nice adversarial probe. The role-name `\"../../../etc/passwd\"` produces a path like `/plugins/egg-sdlc/skills/egg-sdlc/agents/../../../etc/passwd.md` which `Path.is_file()` would resolve to `/etc/passwd.md` (or wherever). The current loader's `rubric_path.is_file()` check returns False for `/etc/passwd.md` so the test passes \u2014 but the loader doesn't actively reject the path-traversal shape. A future loader change that doesn't fail-closed (e.g., a more permissive fallback) would silently allow this. Consider strengthening the assertion to also check `role_name not in _RUBRIC_LANDED_ROLES` early \u2014 the slice-fence branch (`_ROLE_RUBRIC_SLICES.get` returning None) is the actual structural defense and the test could verify the slice-fence message is in the diagnostic.\n\n### Acceptance criteria check\n\n- **TASK-1-3** (`test_bridge_flattened_round_trip.py`): Test runs in <30s \u2713 (1.4s when subprocess works); does not require real Anthropic API \u2713 (uses MagicMock substrate); AC bullet (a) \u2014 preflight question on first run \u2014 NOT verified due to subprocess crash; AC bullet (b) \u2014 refine-gate decision on second run \u2014 NOT verified due to subprocess crash. **Acceptance NOT met** \u2014 see blocking #1.\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`): Test runs in <60s \u2713 (<1s); uses TASK-1-9 fake \u2713; asserts hook denies child write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json` \u2713; test docstring documents empirical-vs-test-fake limitation \u2713. **Acceptance met.**\n- **TASK-1-7** (`test_rubric_loader.py`): Four required test cases (refiner-regression, reviewer_refine, reviewer_agent_design, architect-raises) \u2713; runs cleanly \u2713; tests file at correct location (`shared/tests/`) \u2713. **Acceptance met.**\n\nPlease address blocking #1 by adding `str(repo_root / \"orchestrator\")` to the subprocess `PYTHONPATH` at `test_bridge_flattened_round_trip.py:178-185`, run all three test files locally, confirm they pass, and re-propose.\n", - "artifact_references": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "nack_version": 1 - }, - "reason": "\nReviewed commits 831239dbd + 2fca7e736 (`shared/tests/test_rubric_loader.py`, `integration_tests/regression/test_bridge_flattened_round_trip.py`, `integration_tests/regression/test_pretooluse_hook_nested.py`). Ran the three test files locally end-to-end with `PYTHONPATH=.:shared:orchestrator pytest `. **Two of the two `test_bridge_flattened_round_trip.py` tests fail on a fresh run** \u2014 the proposal claim \"tests pass\" is incorrect for this file. The other two files pass cleanly (8 + 7 = 15 tests).\n\n### Blocking\n\n1. **`integration_tests/regression/test_bridge_flattened_round_trip.py:178-185` \u2014 subprocess `PYTHONPATH` is missing `/orchestrator/`, so the driver-subprocess crashes before exercising the bridge.** Both tests in the file (`test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged`) fail with:\n ```\n run_pipeline.py: ModuleNotFoundError: No module named 'models'\n ```\n Root cause: the subprocess's `PYTHONPATH` is set to `/shared::/gateway`. Inside the driver subprocess, `run_pipeline_in_process` reaches `orchestrator/substrate/in_process.py:531-534`:\n ```python\n try:\n from orchestrator.models import HITLDecision\n except ImportError: # pragma: no cover\n from models import HITLDecision\n ```\n The first import (`from orchestrator.models import HITLDecision`) fails internally because `orchestrator/models.py:16` does `from slice_id_validation import SLICE_ID_PATTERN` \u2014 a bare import that requires `/orchestrator/` on `PYTHONPATH` (so `slice_id_validation` resolves as a top-level module). The `except ImportError` clause masks this and falls through to `from models import HITLDecision`, which also fails because `models` is `orchestrator.models` from outside the package. Result: the driver subprocess exits 1 with the diagnostic above, and the test fails at the very first `assert proc1.returncode == 0`.\n\n The earlier fix commit (`2fca7e736 \u2014 test_bridge_flattened_round_trip: fix subprocess PYTHONPATH`) added `` to the subprocess `PYTHONPATH`, which lets `import orchestrator.substrate` resolve \u2014 but does NOT cover the bare imports inside `orchestrator/models.py`. The Makefile sets `PYTHONPATH := shared:gateway:orchestrator` (with `orchestrator/` included for exactly this reason).\n\n **Verified locally**:\n ```\n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/gateway python3 -c \"from orchestrator.models import HITLDecision\"\n ModuleNotFoundError: No module named 'slice_id_validation'\n \n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/orchestrator:/home/egg/repos/egg/gateway python3 -c \"from orchestrator.models import HITLDecision; print('ok')\"\n ok\n ```\n\n Fix: add `str(repo_root / \"orchestrator\")` to the `PYTHONPATH` list at `test_bridge_flattened_round_trip.py:178-185`. After the fix, re-run the tests and confirm the round-trip assertions actually exercise (they currently don't reach `assert decision1.get(\"question\") == _PREFLIGHT_QUESTION` because the subprocess crashes before writing the contract).\n\n2. **Self-attestation gap**: the proposal summary claims the tester ran the tests. This is contradicted by the empirical run above (2 of 5 tests in test_bridge_flattened_round_trip.py fail). Please run `PYTHONPATH=.:shared:orchestrator pytest integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py shared/tests/test_rubric_loader.py -v` before re-proposing and confirm a green pass.\n\n### Non-blocking\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146` (`_shim_source`)** \u2014 the shim source is a multi-line string passed to `python3 -c`. The runpy fallback approach is sound but consider extracting it into a small helper script under `integration_tests/regression/_bridge_shim.py` (coder-owned, mirroring `_agent_tool_fake.py`) \u2014 it survives ruff format / mypy without `# noqa` and makes the shim independently testable. Not blocking; the inline string works once the PYTHONPATH fix lands.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:51-56` \u2014 docstring is stale.** The Driver-invocation-contract paragraph says \"the driver respects either ``EGG_PIPELINE_ID`` from the env or a positional ``argv[1]`` pipeline id (whichever the coder picks in task-1-1)\". The coder picked positional `argv[1]`; the env-fallback is not implemented. Drop the \"whichever\" phrasing \u2014 the test no longer needs to over-constrain.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:241` \u2014 `_write_answer` uses `str(time.time())` for the timestamp.** The driver writes ISO-8601 (`datetime.now(UTC).isoformat()` per `bin/run_pipeline.py:101-103`). Both are tolerated by the driver's `_coerce_envelope`, but mirroring the driver's format keeps the test fixture and the driver's source-of-truth consistent. Cheap fix: `datetime.datetime.now(datetime.UTC).isoformat()`.\n\n- **`integration_tests/regression/test_pretooluse_hook_nested.py:204-215` \u2014 the verdict file is always written as `\"pass\"`.** The AC says the verdict file records \"pass\" or \"fail\" with a reason. The current test always writes the pass path regardless of the assertion outcomes; a regression that fails the structured assertions above WILL surface as a test failure (good), but the r2-verdict.json file will still claim \"pass\" because the assertion path comes before the write \u2014 and the file is then read by slice-5 to decide migration. Suggest: derive the verdict from the structured assertions (e.g., `verdict = \"pass\" if result.denied else \"fail\"` plus capture `reason` on failure) and write the file regardless so slice-5 sees the empirical outcome rather than a stale optimistic value. Not blocking because the test correctly fails on regression; this is a downstream-handoff improvement.\n\n- **`shared/tests/test_rubric_loader.py:148-167` \u2014 `test_loader_accepts_enum_and_string_role` only parametrises `refiner` (the AC-required role).** Since the loader also accepts strings for `reviewer_refine` and `reviewer_agent_design`, consider parametrizing those too \u2014 it pins the string-input contract for the two new roles that this slice adds, not just the regression role.\n\n- **`shared/tests/test_rubric_loader.py:194-217` \u2014 `test_loader_rejects_path_traversal_role_name`.** Nice adversarial probe. The role-name `\"../../../etc/passwd\"` produces a path like `/plugins/egg-sdlc/skills/egg-sdlc/agents/../../../etc/passwd.md` which `Path.is_file()` would resolve to `/etc/passwd.md` (or wherever). The current loader's `rubric_path.is_file()` check returns False for `/etc/passwd.md` so the test passes \u2014 but the loader doesn't actively reject the path-traversal shape. A future loader change that doesn't fail-closed (e.g., a more permissive fallback) would silently allow this. Consider strengthening the assertion to also check `role_name not in _RUBRIC_LANDED_ROLES` early \u2014 the slice-fence branch (`_ROLE_RUBRIC_SLICES.get` returning None) is the actual structural defense and the test could verify the slice-fence message is in the diagnostic.\n\n### Acceptance criteria check\n\n- **TASK-1-3** (`test_bridge_flattened_round_trip.py`): Test runs in <30s \u2713 (1.4s when subprocess works); does not require real Anthropic API \u2713 (uses MagicMock substrate); AC bullet (a) \u2014 preflight question on first run \u2014 NOT verified due to subprocess crash; AC bullet (b) \u2014 refine-gate decision on second run \u2014 NOT verified due to subprocess crash. **Acceptance NOT met** \u2014 see blocking #1.\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`): Test runs in <60s \u2713 (<1s); uses TASK-1-9 fake \u2713; asserts hook denies child write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json` \u2713; test docstring documents empirical-vs-test-fake limitation \u2713. **Acceptance met.**\n- **TASK-1-7** (`test_rubric_loader.py`): Four required test cases (refiner-regression, reviewer_refine, reviewer_agent_design, architect-raises) \u2713; runs cleanly \u2713; tests file at correct location (`shared/tests/`) \u2713. **Acceptance met.**\n\nPlease address blocking #1 by adding `str(repo_root / \"orchestrator\")` to the subprocess `PYTHONPATH` at `test_bridge_flattened_round_trip.py:178-185`, run all three test files locally, confirm they pass, and re-propose.\n", - "revision_count": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:42.074669+00:00", - "phase": "implement" - }, - { - "id": "4179b472-e498-47", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:46.357354+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:46.394572+00:00", - "phase": "implement" - }, - { - "id": "82ccef72-a5fe-44", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:46.520273+00:00", - "phase": "implement" - }, - { - "id": "841f6236-2370-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_concurrency", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:48.665950+00:00", - "phase": "implement" - }, - { - "id": "98f66095-fe9b-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:48.746882+00:00", - "phase": "implement" - }, - { - "id": "ddedd355-65a3-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:48.784177+00:00", - "phase": "implement" - }, - { - "id": "82066a41-3e0f-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:53.185575+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:53.219305+00:00", - "phase": "implement" - }, - { - "id": "3ba48978-6857-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:53.569530+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:53.602986+00:00", - "phase": "implement" - }, - { - "id": "9fdd7caa-5bf6-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:56.884305+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:57.014598+00:00", - "phase": "implement" - }, - { - "id": "ea7f0f2d-dc3d-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:57.904893+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:57.930733+00:00", - "phase": "implement" - }, - { - "id": "dd924b54-e2bc-46", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:59.138081+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:31:59.316964+00:00", - "phase": "implement" - }, - { - "id": "6f02e6d4-b504-4d", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:21.893162+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:32:37.014793+00:00", - "phase": "implement" - }, - { - "id": "5996028a-5747-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:53.185575+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:32:53.328911+00:00", - "phase": "implement" - }, - { - "id": "df199ac8-0466-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:53.569530+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:32:53.650959+00:00", - "phase": "implement" - }, - { - "id": "2a1cb7ca-5752-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:56.884305+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:32:57.074674+00:00", - "phase": "implement" - }, - { - "id": "3eb949f0-f62c-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:57.904893+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:32:57.988702+00:00", - "phase": "implement" - }, - { - "id": "7c277553-98c0-4f", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:59.138081+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:32:59.400793+00:00", - "phase": "implement" - }, - { - "id": "6bf93726-3557-4e", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:21.893162+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:37.103326+00:00", - "phase": "implement" - }, - { - "id": "3aed5529-4188-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code_holistic for tester", - "body": "\nHolistic review of tester v1 (commit 9ec45ddf, tester-authored content from 831239db + 2fca7e73) \u2014 ran all four mandatory passes against the three test files: `test_bridge_flattened_round_trip.py` (new, 406 LOC), `test_pretooluse_hook_nested.py` (new, 334 LOC), `test_rubric_loader.py` (new, 217 LOC).\n\n**Pass 1 \u2014 End-to-end primary use case.** The bridge round-trip test walks the documented happy path verbatim: invocation 1 \u2192 preflight question \u2192 write `pending_hitl.answer=\"approve\"` + `status=\"answered\"` \u2192 invocation 2 \u2192 refine-gate question. The shim correctly patches `orchestrator.substrate.select_substrate` (which is imported lazily inside `_InProcessOrchestrator._spawn_refiner` via `from . import select_substrate` per `in_process.py:420`, so the module-attribute patch propagates), shrinks the background-thread intervals to 50ms so the test doesn't block on the default 5s tick, and runs the **real** driver via `runpy.run_path(_driver, run_name='__main__')` rather than a re-implementation. The R2 test exercises the **real** `hook_entry.decide` via the fake's child subprocess and asserts on `{\"decision\": \"block\", \"reason\": ...}` matching the actual code shape. The rubric loader test exercises the real `_load_egg_sdlc_role_rubric` and verifies all four AC cases. No producer/consumer dead-ends \u2014 the tests trace the documented use cases end-to-end.\n\n**Pass 2 \u2014 Doc/code symmetry.** Two doc/code drifts worth noting (both non-blocking):\n\n1. **`test_bridge_flattened_round_trip.py:50-56` docstring claims the driver \"respects either `EGG_PIPELINE_ID` from the env or a positional `argv[1]`\".** The driver as shipped (verified via `grep -n EGG_PIPELINE_ID run_pipeline.py`) reads `EGG_REPO`, `EGG_PIPELINE_REPO`, `EGG_ISSUE_NUMBER`, `EGG_SUBSTRATE` from env but **does NOT** consult `EGG_PIPELINE_ID` \u2014 the pipeline id is positional-only via argparse (`run_pipeline.py:365-368`). The shim sets both as a hedge so the test passes either way, but the docstring's \"whichever the coder picks in task-1-1\" hedge is stale relative to the coder's actual choice. Fix: drop the env-or-positional claim from the docstring and just say \"positional pipeline_id per `run_pipeline.py:_parse_args`.\"\n\n2. **`task-1-5` contract AC text vs `hook_entry.decide()` actual return shape.** The contract AC for task-1-5 (which I fetched via `mcp__sdlc__show_contract`) prescribes the hook verdict as `{\"action\": \"deny\", \"message\": \"...\"}` \u2014 but the real `hook_entry.decide()` at `orchestrator/substrate/claude_code/hook_entry.py:673` returns `{\"decision\": \"block\", \"reason\": ...}`. The tester correctly tested the code, not the stale AC text \u2014 but this is a contract-doc drift that would mislead a future implementer reading the AC. The tester didn't fix the contract text (and shouldn't \u2014 the contract is upstream artifact), but a `# AC text uses {action, deny, message} \u2014 actual hook shape is {decision, block, reason}; this test pins the implementation, not the stale wording` callout in the test docstring would prevent the next reader from being whiplashed.\n\n**Pass 3 \u2014 Synthetic-key / sentinel coordination.** Three cross-module coordination points exercised:\n\n1. **`pending_hitl.status = \"answered\"` is set by the test** (line 237 \u2014 `pending[\"status\"] = \"answered\"`) alongside `pending[\"answer\"] = \"approve\"`. This is exactly the coordination point I flagged in the coder's review: the driver only promotes `answer \u2192 answer_log` when `status == \"answered\"`. The tester writes both \u2014 so the test exercises the driver's strict-coordination path. **What is NOT exercised:** the failure shape where the skill body writes `answer` without setting `status=\"answered\"` (the silent-drop case from coder finding #2). If the documenter's SKILL.md sets only `answer`, the production loop would wedge \u2014 a regression test that pins \"answer-without-status \u2192 driver re-yields same decision\" would have caught the silent-drop class. Non-blocking but worth adding.\n\n2. **Driver's pending_hitl schema vs test assertions on the envelope.** The test pins `pending_hitl.{pipeline_id, version, timestamp, decision}` (lines 308-320) \u2014 4 of the 9 fields. It does NOT pin `status`, `result`, `error`, `answer`, or `answer_log`. This is consistent with the coder's incomplete STABLE-contract schema block (8-of-9 fields documented) but means the test would not catch a regression where the driver stopped persisting `answer_log` \u2014 exactly the cross-bridge schema-coordination point R17 mitigation depends on. Non-blocking but worth a `# Pin the full 9-field schema once the coder docstring catches up to the implementation` follow-up.\n\n3. **`r2-verdict.json` envelope shape.** The test writes `{\"r2_verdict\": \"pass\"}` per the AC, and the test docstring's failure variant is `{\"r2_verdict\": \"fail\", \"reason\": \"...\"}`. Future slice-5 R15 consumer reads from this path. The schema is documented in the test docstring (lines 41-43) and the AC. No consumer in the diff today; this is fine.\n\n**Pass 4 \u2014 Silent-fallback hunt.** The test suite covers three classes of silent-fallback regression:\n\n1. `test_driver_is_idempotent_when_answer_unchanged` pins \"no new answer \u2192 driver re-yields same decision, MUST NOT advance silently\" (lines 373-406). Good \u2014 catches the \"operator answer would be lost\" silent advance.\n2. `test_dispatch_does_not_leak_egg_agent_role_into_parent_env` pins env isolation under nested-dispatch simulation. Good \u2014 catches a fake-that-mutates-parent-env regression.\n3. `test_loader_rejects_path_traversal_role_name` pins \"role names with `..` \u2192 ValueError, no silent file read\". Good \u2014 catches the path-escape silent-fallback.\n\n**Gaps relative to coder findings:** the silent fallbacks I flagged in the coder review (corrupt-contract silent reset in `_read_contract`, non-dict envelope silent reset in `_coerce_envelope`, NotImplementedError-from-`_maybe_fence` \u2192 `status=error` path) have no regression test. Non-blocking \u2014 the AC didn't require them \u2014 but a `test_driver_handles_corrupt_contract_without_resetting_state` (writes garbage to the contract, asserts driver exits with a structured error rather than silently overwriting) and a `test_driver_treats_approve_continue_as_completed_not_error` (operator chooses approve_continue, driver's NotImplementedError path is special-cased to `status=completed` with the fence message) would close the holistic-lens loop on those findings.\n\n### Non-blocking\n\n- **`test_bridge_flattened_round_trip.py:50-56`** \u2014 drop the \"either env or positional argv\" hedge from the driver-invocation-contract docstring; the driver is positional-only.\n\n- **`test_bridge_flattened_round_trip.py:308-320`** \u2014 extend the Stage A envelope assertions to pin `pending_hitl.{status, answer_log, result, error}` so a future regression that drops `answer_log` (the load-bearing replay field for cross-process state) is caught immediately. Today the test would let a regression that removes `answer_log` pass.\n\n- **`test_pretooluse_hook_nested.py` top docstring** \u2014 add a `# AC text in contract task-1-5 uses {action, deny, message}; actual hook shape is {decision, block, reason} \u2014 this test pins the code, not the stale AC` callout so future readers don't churn on the contract/code drift.\n\n- **Add `test_driver_consumes_answer_only_when_status_answered`** \u2014 write `answer=\"approve\"` to `pending_hitl` while leaving `status=\"pending\"`, re-invoke, assert the driver did NOT advance the generator. This pins the strict-coordination contract the coder chose and would catch a documenter-side regression where the SKILL.md sets only `answer`.\n\n- **Add `test_driver_handles_corrupt_contract_without_silent_reset`** \u2014 write `garbage_not_json` to the contract file, re-invoke, assert the driver exits with a clear error and does NOT overwrite a present-but-corrupt contract with a fresh skeleton. This closes the silent-reset finding from the coder review.\n\n- **`test_rubric_loader.py:194-217`** \u2014 the path-traversal test asserts the error mentions \"missing\" OR \"rubric\"; verify against the actual error path. The loader for `\"../../../etc/passwd\"` (not in `_RUBRIC_LANDED_ROLES`, not in `_ROLE_RUBRIC_SLICES`) raises the \"This role is not part of the #2717 rollout's rubric set\" message (per `substrate/__init__.py:341-345`), which contains \"rubric\". OK \u2014 but the assertion could be tighter (e.g., assert the error does NOT contain `etc/passwd` or any absolute filesystem path that would suggest the loader actually tried to read).\n\nACKing \u2014 all three tests are correct, the AC bullets are covered, and the adversarial probing is in the spirit of the tester's dual role. The non-blocking items are post-merge hygiene rather than slice-blocking gaps.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py", - "shared/tests/test_rubric_loader.py" - ], - "reason": "\nHolistic review of tester v1 (commit 9ec45ddf, tester-authored content from 831239db + 2fca7e73) \u2014 ran all four mandatory passes against the three test files: `test_bridge_flattened_round_trip.py` (new, 406 LOC), `test_pretooluse_hook_nested.py` (new, 334 LOC), `test_rubric_loader.py` (new, 217 LOC).\n\n**Pass 1 \u2014 End-to-end primary use case.** The bridge round-trip test walks the documented happy path verbatim: invocation 1 \u2192 preflight question \u2192 write `pending_hitl.answer=\"approve\"` + `status=\"answered\"` \u2192 invocation 2 \u2192 refine-gate question. The shim correctly patches `orchestrator.substrate.select_substrate` (which is imported lazily inside `_InProcessOrchestrator._spawn_refiner` via `from . import select_substrate` per `in_process.py:420`, so the module-attribute patch propagates), shrinks the background-thread intervals to 50ms so the test doesn't block on the default 5s tick, and runs the **real** driver via `runpy.run_path(_driver, run_name='__main__')` rather than a re-implementation. The R2 test exercises the **real** `hook_entry.decide` via the fake's child subprocess and asserts on `{\"decision\": \"block\", \"reason\": ...}` matching the actual code shape. The rubric loader test exercises the real `_load_egg_sdlc_role_rubric` and verifies all four AC cases. No producer/consumer dead-ends \u2014 the tests trace the documented use cases end-to-end.\n\n**Pass 2 \u2014 Doc/code symmetry.** Two doc/code drifts worth noting (both non-blocking):\n\n1. **`test_bridge_flattened_round_trip.py:50-56` docstring claims the driver \"respects either `EGG_PIPELINE_ID` from the env or a positional `argv[1]`\".** The driver as shipped (verified via `grep -n EGG_PIPELINE_ID run_pipeline.py`) reads `EGG_REPO`, `EGG_PIPELINE_REPO`, `EGG_ISSUE_NUMBER`, `EGG_SUBSTRATE` from env but **does NOT** consult `EGG_PIPELINE_ID` \u2014 the pipeline id is positional-only via argparse (`run_pipeline.py:365-368`). The shim sets both as a hedge so the test passes either way, but the docstring's \"whichever the coder picks in task-1-1\" hedge is stale relative to the coder's actual choice. Fix: drop the env-or-positional claim from the docstring and just say \"positional pipeline_id per `run_pipeline.py:_parse_args`.\"\n\n2. **`task-1-5` contract AC text vs `hook_entry.decide()` actual return shape.** The contract AC for task-1-5 (which I fetched via `mcp__sdlc__show_contract`) prescribes the hook verdict as `{\"action\": \"deny\", \"message\": \"...\"}` \u2014 but the real `hook_entry.decide()` at `orchestrator/substrate/claude_code/hook_entry.py:673` returns `{\"decision\": \"block\", \"reason\": ...}`. The tester correctly tested the code, not the stale AC text \u2014 but this is a contract-doc drift that would mislead a future implementer reading the AC. The tester didn't fix the contract text (and shouldn't \u2014 the contract is upstream artifact), but a `# AC text uses {action, deny, message} \u2014 actual hook shape is {decision, block, reason}; this test pins the implementation, not the stale wording` callout in the test docstring would prevent the next reader from being whiplashed.\n\n**Pass 3 \u2014 Synthetic-key / sentinel coordination.** Three cross-module coordination points exercised:\n\n1. **`pending_hitl.status = \"answered\"` is set by the test** (line 237 \u2014 `pending[\"status\"] = \"answered\"`) alongside `pending[\"answer\"] = \"approve\"`. This is exactly the coordination point I flagged in the coder's review: the driver only promotes `answer \u2192 answer_log` when `status == \"answered\"`. The tester writes both \u2014 so the test exercises the driver's strict-coordination path. **What is NOT exercised:** the failure shape where the skill body writes `answer` without setting `status=\"answered\"` (the silent-drop case from coder finding #2). If the documenter's SKILL.md sets only `answer`, the production loop would wedge \u2014 a regression test that pins \"answer-without-status \u2192 driver re-yields same decision\" would have caught the silent-drop class. Non-blocking but worth adding.\n\n2. **Driver's pending_hitl schema vs test assertions on the envelope.** The test pins `pending_hitl.{pipeline_id, version, timestamp, decision}` (lines 308-320) \u2014 4 of the 9 fields. It does NOT pin `status`, `result`, `error`, `answer`, or `answer_log`. This is consistent with the coder's incomplete STABLE-contract schema block (8-of-9 fields documented) but means the test would not catch a regression where the driver stopped persisting `answer_log` \u2014 exactly the cross-bridge schema-coordination point R17 mitigation depends on. Non-blocking but worth a `# Pin the full 9-field schema once the coder docstring catches up to the implementation` follow-up.\n\n3. **`r2-verdict.json` envelope shape.** The test writes `{\"r2_verdict\": \"pass\"}` per the AC, and the test docstring's failure variant is `{\"r2_verdict\": \"fail\", \"reason\": \"...\"}`. Future slice-5 R15 consumer reads from this path. The schema is documented in the test docstring (lines 41-43) and the AC. No consumer in the diff today; this is fine.\n\n**Pass 4 \u2014 Silent-fallback hunt.** The test suite covers three classes of silent-fallback regression:\n\n1. `test_driver_is_idempotent_when_answer_unchanged` pins \"no new answer \u2192 driver re-yields same decision, MUST NOT advance silently\" (lines 373-406). Good \u2014 catches the \"operator answer would be lost\" silent advance.\n2. `test_dispatch_does_not_leak_egg_agent_role_into_parent_env` pins env isolation under nested-dispatch simulation. Good \u2014 catches a fake-that-mutates-parent-env regression.\n3. `test_loader_rejects_path_traversal_role_name` pins \"role names with `..` \u2192 ValueError, no silent file read\". Good \u2014 catches the path-escape silent-fallback.\n\n**Gaps relative to coder findings:** the silent fallbacks I flagged in the coder review (corrupt-contract silent reset in `_read_contract`, non-dict envelope silent reset in `_coerce_envelope`, NotImplementedError-from-`_maybe_fence` \u2192 `status=error` path) have no regression test. Non-blocking \u2014 the AC didn't require them \u2014 but a `test_driver_handles_corrupt_contract_without_resetting_state` (writes garbage to the contract, asserts driver exits with a structured error rather than silently overwriting) and a `test_driver_treats_approve_continue_as_completed_not_error` (operator chooses approve_continue, driver's NotImplementedError path is special-cased to `status=completed` with the fence message) would close the holistic-lens loop on those findings.\n\n### Non-blocking\n\n- **`test_bridge_flattened_round_trip.py:50-56`** \u2014 drop the \"either env or positional argv\" hedge from the driver-invocation-contract docstring; the driver is positional-only.\n\n- **`test_bridge_flattened_round_trip.py:308-320`** \u2014 extend the Stage A envelope assertions to pin `pending_hitl.{status, answer_log, result, error}` so a future regression that drops `answer_log` (the load-bearing replay field for cross-process state) is caught immediately. Today the test would let a regression that removes `answer_log` pass.\n\n- **`test_pretooluse_hook_nested.py` top docstring** \u2014 add a `# AC text in contract task-1-5 uses {action, deny, message}; actual hook shape is {decision, block, reason} \u2014 this test pins the code, not the stale AC` callout so future readers don't churn on the contract/code drift.\n\n- **Add `test_driver_consumes_answer_only_when_status_answered`** \u2014 write `answer=\"approve\"` to `pending_hitl` while leaving `status=\"pending\"`, re-invoke, assert the driver did NOT advance the generator. This pins the strict-coordination contract the coder chose and would catch a documenter-side regression where the SKILL.md sets only `answer`.\n\n- **Add `test_driver_handles_corrupt_contract_without_silent_reset`** \u2014 write `garbage_not_json` to the contract file, re-invoke, assert the driver exits with a clear error and does NOT overwrite a present-but-corrupt contract with a fresh skeleton. This closes the silent-reset finding from the coder review.\n\n- **`test_rubric_loader.py:194-217`** \u2014 the path-traversal test asserts the error mentions \"missing\" OR \"rubric\"; verify against the actual error path. The loader for `\"../../../etc/passwd\"` (not in `_RUBRIC_LANDED_ROLES`, not in `_ROLE_RUBRIC_SLICES`) raises the \"This role is not part of the #2717 rollout's rubric set\" message (per `substrate/__init__.py:341-345`), which contains \"rubric\". OK \u2014 but the assertion could be tighter (e.g., assert the error does NOT contain `etc/passwd` or any absolute filesystem path that would suggest the loader actually tried to read).\n\nACKing \u2014 all three tests are correct, the AC bullets are covered, and the adversarial probing is in the spirit of the tester's dual role. The non-blocking items are post-merge hygiene rather than slice-blocking gaps.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:39.059990+00:00", - "phase": "implement" - }, - { - "id": "262ac678-e0d5-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_code_holistic", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:45.318973+00:00", - "phase": "implement" - }, - { - "id": "57198924-dc03-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:45.439689+00:00", - "phase": "implement" - }, - { - "id": "d7f9a41f-1137-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:45.490755+00:00", - "phase": "implement" - }, - { - "id": "7070a952-bb81-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:45.511452+00:00", - "phase": "implement" - }, - { - "id": "60144848-a89b-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:49.461849+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:49.502786+00:00", - "phase": "implement" - }, - { - "id": "e8a38ba9-a5d5-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:50.178099+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:50.221434+00:00", - "phase": "implement" - }, - { - "id": "6526eb70-2306-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:50.180187+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:50.228097+00:00", - "phase": "implement" - }, - { - "id": "092b6f5d-0945-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:54.181347+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:54.210534+00:00", - "phase": "implement" - }, - { - "id": "b5039b21-35ce-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:56.884305+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:57.131819+00:00", - "phase": "implement" - }, - { - "id": "d2c3b688-aa29-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:59.138081+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:33:59.434143+00:00", - "phase": "implement" - }, - { - "id": "b2e2fc17-67ae-4e", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:21.893162+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:34:48.646591+00:00", - "phase": "implement" - }, - { - "id": "2af85bcf-d191-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:49.461849+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:34:49.561338+00:00", - "phase": "implement" - }, - { - "id": "076a693c-4084-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:50.180187+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:34:50.440581+00:00", - "phase": "implement" - }, - { - "id": "9c43e749-2b17-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:50.178099+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:34:50.504577+00:00", - "phase": "implement" - }, - { - "id": "d7d7e771-69e5-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:33:54.181347+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:34:54.269797+00:00", - "phase": "implement" - }, - { - "id": "4279d652-e118-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:31:56.884305+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:34:57.187109+00:00", - "phase": "implement" - }, - { - "id": "d12e0dcf-d19f-40", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:59.138081+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:34:59.488003+00:00", - "phase": "implement" - }, - { - "id": "a55131a3-b598-47", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from tester", - "body": "slice-1 tester v2: addresses reviewer_code v1 blocking #1 (subprocess PYTHONPATH) and three non-blocking items. 21/21 tester-authored tests pass under reviewer_code's exact reproduction env (`PYTHONPATH=.:shared:orchestrator pytest `).\n\n**Blocking fix \u2014 subprocess PYTHONPATH (reviewer_code v1 #1):**\n`integration_tests/regression/test_bridge_flattened_round_trip.py:168-196` \u2014 add `/orchestrator` to the subprocess shim's PYTHONPATH so bare-name top-level imports inside the `orchestrator/` tree resolve. Without this, `orchestrator/models.py:16` (`from slice_id_validation import SLICE_ID_PATTERN`) and `in_process.py:531-534`'s `from models import HITLDecision` fallback both fail, crashing the driver subprocess with `ModuleNotFoundError` before it yields the first HITL decision. Now mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` (test target) exactly.\n\n**Non-blocking improvements (reviewer_code v1):**\n* test_bridge_flattened_round_trip.py \u2014 dropped stale \"whichever the coder picks\" docstring phrasing (driver locked in positional argv[1]); mirror the driver's ISO-8601 UTC timestamp format in `_write_answer` instead of `str(time.time())`.\n* test_rubric_loader.py \u2014 extended `test_loader_accepts_enum_and_string_role` parametrization to cover the two NEW slice-1 roles (reviewer_refine, reviewer_agent_design), not just the regression role. Strengthened the path-traversal assertion to pin the allowlist's slice-fence branch (the structural defence) rather than the file-missing-on-disk branch.\n* test_pretooluse_hook_nested.py \u2014 `r2-verdict.json` content is now derived from the dispatch outcome and written BEFORE the structured assertions, so slice-5's contingent R15 migration task sees an accurate empirical record (`{\"r2_verdict\": \"fail\", \"reason\": ...}`) on a regression rather than a stale optimistic \"pass\" file.\n\n**Three test files committed:**\n1. `shared/tests/test_rubric_loader.py` \u2014 4 AC cases + 3 adversarial probe groups (enum-vs-string \u00d7 3 roles, unshipped roles, path-traversal slice-fence branch). 14 cases.\n2. `integration_tests/regression/test_bridge_flattened_round_trip.py` \u2014 Two-stage round-trip + driver-idempotency probe. Substrate isolation via `python -c` shim that monkey-patches `select_substrate` to a MagicMock bundle (no real Anthropic API). 2 cases.\n3. `integration_tests/regression/test_pretooluse_hook_nested.py` \u2014 R2 nested-dispatch verdict (parent=architect + child=tester writing source must deny with tester-naming reason), cross-role probe (parent=coder + child=tester), in-role negative-control, dataclass shape pin, EGG_AGENT_ROLE leak guard. 5 cases.\n\n**Configured-check results:**\n* `make lint` \u2014 exit 0. Ruff check, ruff format check, mypy, shellcheck, custom checks all green.\n* `make security` \u2014 exit 0. (Bandit not installed in this sandbox.)\n* `make test` \u2014 exit 4 (environmental \u2014 grimp module unavailable, conftest collision in select-tests fallback; reproduces on origin/main). The slice-1 tests pass cleanly under direct pytest invocation in both the canonical Makefile PYTHONPATH shape AND reviewer_code v1's `.:shared:orchestrator` reproduction shape (21/21).", - "metadata": { - "payload": { - "summary": "slice-1 tester v2: addresses reviewer_code v1 blocking #1 (subprocess PYTHONPATH) and three non-blocking items. 21/21 tester-authored tests pass under reviewer_code's exact reproduction env (`PYTHONPATH=.:shared:orchestrator pytest `).\n\n**Blocking fix \u2014 subprocess PYTHONPATH (reviewer_code v1 #1):**\n`integration_tests/regression/test_bridge_flattened_round_trip.py:168-196` \u2014 add `/orchestrator` to the subprocess shim's PYTHONPATH so bare-name top-level imports inside the `orchestrator/` tree resolve. Without this, `orchestrator/models.py:16` (`from slice_id_validation import SLICE_ID_PATTERN`) and `in_process.py:531-534`'s `from models import HITLDecision` fallback both fail, crashing the driver subprocess with `ModuleNotFoundError` before it yields the first HITL decision. Now mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` (test target) exactly.\n\n**Non-blocking improvements (reviewer_code v1):**\n* test_bridge_flattened_round_trip.py \u2014 dropped stale \"whichever the coder picks\" docstring phrasing (driver locked in positional argv[1]); mirror the driver's ISO-8601 UTC timestamp format in `_write_answer` instead of `str(time.time())`.\n* test_rubric_loader.py \u2014 extended `test_loader_accepts_enum_and_string_role` parametrization to cover the two NEW slice-1 roles (reviewer_refine, reviewer_agent_design), not just the regression role. Strengthened the path-traversal assertion to pin the allowlist's slice-fence branch (the structural defence) rather than the file-missing-on-disk branch.\n* test_pretooluse_hook_nested.py \u2014 `r2-verdict.json` content is now derived from the dispatch outcome and written BEFORE the structured assertions, so slice-5's contingent R15 migration task sees an accurate empirical record (`{\"r2_verdict\": \"fail\", \"reason\": ...}`) on a regression rather than a stale optimistic \"pass\" file.\n\n**Three test files committed:**\n1. `shared/tests/test_rubric_loader.py` \u2014 4 AC cases + 3 adversarial probe groups (enum-vs-string \u00d7 3 roles, unshipped roles, path-traversal slice-fence branch). 14 cases.\n2. `integration_tests/regression/test_bridge_flattened_round_trip.py` \u2014 Two-stage round-trip + driver-idempotency probe. Substrate isolation via `python -c` shim that monkey-patches `select_substrate` to a MagicMock bundle (no real Anthropic API). 2 cases.\n3. `integration_tests/regression/test_pretooluse_hook_nested.py` \u2014 R2 nested-dispatch verdict (parent=architect + child=tester writing source must deny with tester-naming reason), cross-role probe (parent=coder + child=tester), in-role negative-control, dataclass shape pin, EGG_AGENT_ROLE leak guard. 5 cases.\n\n**Configured-check results:**\n* `make lint` \u2014 exit 0. Ruff check, ruff format check, mypy, shellcheck, custom checks all green.\n* `make security` \u2014 exit 0. (Bandit not installed in this sandbox.)\n* `make test` \u2014 exit 4 (environmental \u2014 grimp module unavailable, conftest collision in select-tests fallback; reproduces on origin/main). The slice-1 tests pass cleanly under direct pytest invocation in both the canonical Makefile PYTHONPATH shape AND reviewer_code v1's `.:shared:orchestrator` reproduction shape (21/21).", - "attestation": { - "tests_execution_blocked": true, - "tests_execution_blocked_reason": "`make test` cannot complete in this sandbox: (1) `grimp` Python module required by `scripts/select_tests/__main__.py` is not installed and pypi is unreachable (`make` cannot `uv sync` the venv); (2) when select-tests falls back to the full-suite path, pytest's rootdir-discovery walks both `tests/` and `shared/tests/` and fails with `ImportPathMismatchError`. Directly invoking `pytest shared/tests/test_rubric_loader.py integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py` with PYTHONPATH set to either `shared:gateway:orchestrator:repo_root` (Makefile shape) or `.:shared:orchestrator` (reviewer_code v1 repro shape) passes 21/21 tester-authored tests. The 20 pre-existing failures in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}` reproduce on origin/main with my changes stashed (not caused by this slice).", - "checks_passed": [ - "lint", - "security" - ] - }, - "artifacts": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "risk_considered": "v2 changes are entirely test-side: PYTHONPATH widening (already-present in Makefile), assertion strengthening, verdict-file derivation logic. No source code changed. All assertions on the production behavior are preserved or strengthened; none are loosened. The substrate-fake isolation is unchanged (MagicMock bundle), so the subprocess-based tests still cannot reach the real Anthropic API. The verdict-file derivation in test_pretooluse_hook_nested.py now writes \"fail\" with a structured reason on a regression instead of always \"pass\" \u2014 this is a strictly downstream-handoff improvement (slice-5's R15 migration task sees the empirical answer).", - "commit_sha": "601df9041a5018b3c8deed3f8527ff5dcffde46e", - "files_changed": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "tests_run": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "tasks_satisfied": [ - "task-1-3", - "task-1-5", - "task-1-7" - ] - }, - "version": 2, - "commit_sha": "601df9041a5018b3c8deed3f8527ff5dcffde46e", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.410165+00:00", - "phase": "implement" - }, - { - "id": "4c7cbd12-1e94-41", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "reviewer_code_holistic", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: tester submitted new proposal v2", - "body": "Producer tester has submitted a new proposal (version 2) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "tester", - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.410321+00:00", - "phase": "implement" - }, - { - "id": "ba613aab-72c6-4c", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "reviewer_security", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: tester submitted new proposal v2", - "body": "Producer tester has submitted a new proposal (version 2) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "tester", - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.410364+00:00", - "phase": "implement" - }, - { - "id": "7dfb5b24-fccb-40", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "reviewer_concurrency", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: tester submitted new proposal v2", - "body": "Producer tester has submitted a new proposal (version 2) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "tester", - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.410400+00:00", - "phase": "implement" - }, - { - "id": "f67690f4-2d7a-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.624541+00:00", - "phase": "implement" - }, - { - "id": "607d333e-4bfc-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.646955+00:00", - "phase": "implement" - }, - { - "id": "c4160373-4731-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.658984+00:00", - "phase": "implement" - }, - { - "id": "615a507a-004c-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.715470+00:00", - "phase": "implement" - }, - { - "id": "ac99c6e0-e35a-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.724870+00:00", - "phase": "implement" - }, - { - "id": "83f039df-8b06-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:24.733814+00:00", - "phase": "implement" - }, - { - "id": "01d55f5c-9ef2-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:35:29.939223+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:45.966837+00:00", - "phase": "implement" - }, - { - "id": "5b64ef1c-bf8f-45", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:35:29.997637+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:46.085426+00:00", - "phase": "implement" - }, - { - "id": "53da1e3e-4ff0-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:35:46.141428+00:00", - "phase": "implement" - }, - { - "id": "ec65b636-a8a6-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:35:53.207450+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:11.081047+00:00", - "phase": "implement" - }, - { - "id": "529ddc0d-803a-44", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:30:59.138081+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:11.180180+00:00", - "phase": "implement" - }, - { - "id": "a8d35365-3663-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE,CONSENSUS_ACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:02.373422+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:11.189458+00:00", - "phase": "implement" - }, - { - "id": "1f89f721-498f-4b", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:11.257659+00:00", - "phase": "implement" - }, - { - "id": "6a0ed5a0-c4d2-4a", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE,CONSENSUS_ACK", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:21.845500+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:35.833105+00:00", - "phase": "implement" - }, - { - "id": "6e2525a1-818f-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_security for tester", - "body": "Security-lens re-review of tester v2 (commit 601df9041).\n\n**v2 deltas reviewed:** PYTHONPATH fix in the subprocess shim, ISO-8601 timestamp normalisation, parametrisation extended to the new rubric-supported roles, path-traversal assertion strengthened, and r2-verdict.json now derived from the dispatch outcome.\n\n**Positive security signal \u2014 v1 non-blocking #1 addressed (`test_rubric_loader.py:194-240`).**\n\nThe strengthened path-traversal test now asserts that the diagnostic identifies the role as `\"not part of\"` / `\"rollout\"` / `\"rubric set\"` rather than `\"missing on disk\"`. This pins the *structural* defence: the `_ROLE_RUBRIC_SLICES` allowlist intercepts BEFORE any `Path.is_file()` / `Path.read_text()` reaches a user-controlled path. A regression that dropped the allowlist (e.g., letting roles fall through to the filesystem-walk branch) would now fail this test \u2014 closing the existence-oracle vector flagged in security criteria \u00a78 (\"`Path.is_file()` on agent-supplied paths leaks filesystem layout\"). Exactly the kind of structural pin the security lens cares about; the test is now load-bearing for the defence, not just for the behavioural outcome.\n\n**Verified clean (no new security concerns):**\n\n- **PYTHONPATH addition (`test_bridge_flattened_round_trip.py:178-191`).** Adds `/orchestrator` to the subprocess shim's `PYTHONPATH`. The addition is purely import-resolution scaffolding \u2014 it doesn't expose new arguments to argv, doesn't change which binary is invoked, and the path is derived from `_repo_root()` (a static path-walk from the test file, not from agent input). No injection or traversal surface introduced. Mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` which is the existing trusted shape.\n\n- **Timestamp normalisation (`test_bridge_flattened_round_trip.py:238` import + `_write_answer`).** Switches from `str(time.time())` to `datetime.now(UTC).isoformat()`. Cosmetic; no security impact.\n\n- **Parametrisation extension (`test_rubric_loader.py:148-165`).** Adds `enum-reviewer_refine` / `str-reviewer_refine` / `enum-reviewer_agent_design` / `str-reviewer_agent_design` cases to `test_loader_accepts_enum_and_string_role`. Pins the str\u2192enum normalisation for the two NEW roles, so a future loader change that breaks `str(role)` for `reviewer_refine` (e.g., accidentally using `repr(role)`) is caught and the file-path lookup divergence is surfaced. Test improvement; the allowlist still gates the actual filesystem read.\n\n- **r2-verdict derived from outcome (`test_pretooluse_hook_nested.py:174-205`).** The verdict file now records `{\"r2_verdict\": \"fail\", \"reason\": ...}` when the structured assertions would fail, written BEFORE the assertions so slice-5's R15 contingent task gets an accurate empirical record even on regression. Defensive improvement; the failure-payload's `reason` field includes `verdict!r` and `reason!r` \u2014 these are derived from `hook_entry.decide(...)`'s output, not from agent input, so no log-injection or secret-leakage risk. The reason text is bounded by what the hook itself emits.\n\n### Non-blocking\n\n- **`test_pretooluse_hook_nested.py:185-205` \u2014 verdict payload's `reason` text is built via f-string interpolation of `repr(verdict)` and `repr(reason)`.** The values come from `hook_entry.decide(...)` (test-controlled) so no injection vector here, but if a future copy-paste of this pattern interpolates attacker-controlled data into a verdict reason, the `repr()` form would preserve any embedded backslash-escape sequences. Today the values are bounded; flag for the pattern's reuse only.\n\n- **Carryover from v1 non-blocking #2** (`test_bridge_flattened_round_trip.py:108-146`): The subprocess-isolation assumption in `_shim_source()` is still implicit. v2 did not document it in the shim docstring \u2014 keep this as a follow-up if the shim shape is reused in an in-process test where attribute-rebind would leak state. No new finding; not blocking.\n\n- **Carryover from v1 non-blocking #3** (`_write_r2_verdict` path construction): `pipeline_id` is still a test constant (`\"pipeline-r2-nested\"`); the path-traversal exposure documented in v1 remains a future-parameterisation concern only.", - "metadata": { - "payload": { - "artifact_references": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "reason": "Security-lens re-review of tester v2 (commit 601df9041).\n\n**v2 deltas reviewed:** PYTHONPATH fix in the subprocess shim, ISO-8601 timestamp normalisation, parametrisation extended to the new rubric-supported roles, path-traversal assertion strengthened, and r2-verdict.json now derived from the dispatch outcome.\n\n**Positive security signal \u2014 v1 non-blocking #1 addressed (`test_rubric_loader.py:194-240`).**\n\nThe strengthened path-traversal test now asserts that the diagnostic identifies the role as `\"not part of\"` / `\"rollout\"` / `\"rubric set\"` rather than `\"missing on disk\"`. This pins the *structural* defence: the `_ROLE_RUBRIC_SLICES` allowlist intercepts BEFORE any `Path.is_file()` / `Path.read_text()` reaches a user-controlled path. A regression that dropped the allowlist (e.g., letting roles fall through to the filesystem-walk branch) would now fail this test \u2014 closing the existence-oracle vector flagged in security criteria \u00a78 (\"`Path.is_file()` on agent-supplied paths leaks filesystem layout\"). Exactly the kind of structural pin the security lens cares about; the test is now load-bearing for the defence, not just for the behavioural outcome.\n\n**Verified clean (no new security concerns):**\n\n- **PYTHONPATH addition (`test_bridge_flattened_round_trip.py:178-191`).** Adds `/orchestrator` to the subprocess shim's `PYTHONPATH`. The addition is purely import-resolution scaffolding \u2014 it doesn't expose new arguments to argv, doesn't change which binary is invoked, and the path is derived from `_repo_root()` (a static path-walk from the test file, not from agent input). No injection or traversal surface introduced. Mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` which is the existing trusted shape.\n\n- **Timestamp normalisation (`test_bridge_flattened_round_trip.py:238` import + `_write_answer`).** Switches from `str(time.time())` to `datetime.now(UTC).isoformat()`. Cosmetic; no security impact.\n\n- **Parametrisation extension (`test_rubric_loader.py:148-165`).** Adds `enum-reviewer_refine` / `str-reviewer_refine` / `enum-reviewer_agent_design` / `str-reviewer_agent_design` cases to `test_loader_accepts_enum_and_string_role`. Pins the str\u2192enum normalisation for the two NEW roles, so a future loader change that breaks `str(role)` for `reviewer_refine` (e.g., accidentally using `repr(role)`) is caught and the file-path lookup divergence is surfaced. Test improvement; the allowlist still gates the actual filesystem read.\n\n- **r2-verdict derived from outcome (`test_pretooluse_hook_nested.py:174-205`).** The verdict file now records `{\"r2_verdict\": \"fail\", \"reason\": ...}` when the structured assertions would fail, written BEFORE the assertions so slice-5's R15 contingent task gets an accurate empirical record even on regression. Defensive improvement; the failure-payload's `reason` field includes `verdict!r` and `reason!r` \u2014 these are derived from `hook_entry.decide(...)`'s output, not from agent input, so no log-injection or secret-leakage risk. The reason text is bounded by what the hook itself emits.\n\n### Non-blocking\n\n- **`test_pretooluse_hook_nested.py:185-205` \u2014 verdict payload's `reason` text is built via f-string interpolation of `repr(verdict)` and `repr(reason)`.** The values come from `hook_entry.decide(...)` (test-controlled) so no injection vector here, but if a future copy-paste of this pattern interpolates attacker-controlled data into a verdict reason, the `repr()` form would preserve any embedded backslash-escape sequences. Today the values are bounded; flag for the pattern's reuse only.\n\n- **Carryover from v1 non-blocking #2** (`test_bridge_flattened_round_trip.py:108-146`): The subprocess-isolation assumption in `_shim_source()` is still implicit. v2 did not document it in the shim docstring \u2014 keep this as a follow-up if the shim shape is reused in an in-process test where attribute-rebind would leak state. No new finding; not blocking.\n\n- **Carryover from v1 non-blocking #3** (`_write_r2_verdict` path construction): `pipeline_id` is still a test constant (`\"pipeline-r2-nested\"`); the path-traversal exposure documented in v1 remains a future-parameterisation concern only.", - "ack_version": 2 - }, - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:35.877927+00:00", - "phase": "implement" - }, - { - "id": "eff20799-add4-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for tester", - "body": "\nReviewed commit 601df9041 (slice-1 tester v2). Ran all three test files locally with the same reproduction command from my v1 NACK (`PYTHONPATH=.:shared:orchestrator pytest shared/tests/test_rubric_loader.py integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py -v`). **21/21 tests pass cleanly in 1.68s.** The v1 blocker is fully resolved and all five non-blocking items from v1 are also addressed.\n\n### Verification of v1 blocking fix\n\n1. **Blocking #1 (subprocess PYTHONPATH missing `/orchestrator`) \u2014 FIXED.** `integration_tests/regression/test_bridge_flattened_round_trip.py:178-191` now includes `str(repo_root / \"orchestrator\")` between `str(repo_root)` and `str(repo_root / \"gateway\")` in the subprocess `PYTHONPATH` list. The comment block (lines 168-186) explicitly documents *why* each entry is needed \u2014 naming `orchestrator/models.py:16`'s bare `from slice_id_validation import SLICE_ID_PATTERN` and `in_process.py:531-534`'s bare `from models import HITLDecision` fallback as the imports that fail without it. The fix mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` shape with absolute paths. **Both `test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged` now pass.**\n\n2. **Self-attestation gap \u2014 addressed in commit message.** Commit msg explicitly states \"Tests verified: 21/21 pass under both the canonical Makefile PYTHONPATH shape and reviewer_code's `.:shared:orchestrator` reproduction shape.\" Confirmed empirically on my end.\n\n### Verification of v1 non-blocking polish (all addressed)\n\n- **Stale docstring (`test_bridge_flattened_round_trip.py:51-57`)** \u2014 \"whichever the coder picks\" phrasing replaced with \"The coder's driver (task-1-1) accepts the pipeline id as a positional argv[1]; the shim passes it that way.\" Concrete, no longer over-constrains.\n- **`time.time()` timestamp (`test_bridge_flattened_round_trip.py`)** \u2014 replaced with `datetime.now(UTC).isoformat()` at line 251 (matching the driver's `_now_iso()` format). Import updated at line 67 (`from datetime import UTC, datetime`). Test fixture now mirrors the driver's source-of-truth.\n- **Parametrize new roles (`test_rubric_loader.py:149-160`)** \u2014 `test_loader_accepts_enum_and_string_role` now parametrizes all three roles: `refiner` (regression), `reviewer_refine` (new), `reviewer_agent_design` (new) \u2014 both enum and string shapes for each. Six test cases instead of two; all pass.\n- **Path-traversal slice-fence pinning (`test_rubric_loader.py:209-228`)** \u2014 assertion now verifies the path-traversal value resolves to the slice-fence branch (which produces a \"this role is not part of the rollout\" diagnostic) rather than the file-not-on-disk branch. The structural defence (`_RUBRIC_LANDED_ROLES`) is what's load-bearing; the test now pins that.\n- **r2-verdict.json derivation (`test_pretooluse_hook_nested.py`)** \u2014 verdict is now derived from the dispatch outcome and written BEFORE the structured assertions. On regression (dispatch fails to deny), the file records `{\"r2_verdict\": \"fail\", \"reason\": ...}` so slice-5 sees the empirical outcome rather than a stale optimistic value. The `_derive_r2_verdict` helper at lines 132-160 documents the pass/fail decision logic.\n\n### Acceptance criteria check (final)\n\n- **TASK-1-3** (`test_bridge_flattened_round_trip.py`): Test runs in <30s \u2713 (~0.5s wall-clock for the round-trip case); does not require real Anthropic API \u2713 (MagicMock substrate); AC bullet (a) \u2014 preflight question on first run \u2014 VERIFIED via `decision1.get(\"question\") == _PREFLIGHT_QUESTION`; AC bullet (b) \u2014 refine-gate decision on second run \u2014 VERIFIED via `decision2.get(\"question\") != _PREFLIGHT_QUESTION` plus `decision_type in {\"phase_gate\", \"choice\"}`. **Acceptance met.**\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`): Test runs in <60s \u2713; uses TASK-1-9 fake \u2713; asserts hook denies child write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json` \u2713 (now with outcome-derived pass/fail payload); docstring documents empirical-vs-test-fake limitation \u2713. **Acceptance met.**\n- **TASK-1-7** (`test_rubric_loader.py`): All four required cases + adversarial probing \u2713; runs cleanly \u2713; correct location (`shared/tests/`) \u2713. **Acceptance met.**\n\n### Non-blocking (tester v2)\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146` \u2014 `_shim_source` still inline.** Carried over from v1 non-blocking; not addressed. The inline string works fine now that the PYTHONPATH bug is fixed; an extracted `_bridge_shim.py` helper would be a future polish item.\n- **`integration_tests/regression/test_pretooluse_hook_nested.py` \u2014 `_derive_r2_verdict` writes the verdict before the structured assertions.** This is the correct ordering for the downstream slice-5 handoff, but means a flaky assertion (e.g., a test infrastructure bug) could record a misleading \"pass\" on the .json. Worth a follow-up to add a `try/finally`-style \"verdict from final outcome\" pattern. Non-blocking.\n\nNo new findings. The producer's work meets the contract acceptance for all three test files and the cross-module dependency on the coder's `bin/run_pipeline.py` and `_agent_tool_fake.py` is exercised end-to-end.\n", - "metadata": { - "payload": { - "artifact_references": [ - "shared/tests/test_rubric_loader.py", - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "reason": "\nReviewed commit 601df9041 (slice-1 tester v2). Ran all three test files locally with the same reproduction command from my v1 NACK (`PYTHONPATH=.:shared:orchestrator pytest shared/tests/test_rubric_loader.py integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py -v`). **21/21 tests pass cleanly in 1.68s.** The v1 blocker is fully resolved and all five non-blocking items from v1 are also addressed.\n\n### Verification of v1 blocking fix\n\n1. **Blocking #1 (subprocess PYTHONPATH missing `/orchestrator`) \u2014 FIXED.** `integration_tests/regression/test_bridge_flattened_round_trip.py:178-191` now includes `str(repo_root / \"orchestrator\")` between `str(repo_root)` and `str(repo_root / \"gateway\")` in the subprocess `PYTHONPATH` list. The comment block (lines 168-186) explicitly documents *why* each entry is needed \u2014 naming `orchestrator/models.py:16`'s bare `from slice_id_validation import SLICE_ID_PATTERN` and `in_process.py:531-534`'s bare `from models import HITLDecision` fallback as the imports that fail without it. The fix mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` shape with absolute paths. **Both `test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged` now pass.**\n\n2. **Self-attestation gap \u2014 addressed in commit message.** Commit msg explicitly states \"Tests verified: 21/21 pass under both the canonical Makefile PYTHONPATH shape and reviewer_code's `.:shared:orchestrator` reproduction shape.\" Confirmed empirically on my end.\n\n### Verification of v1 non-blocking polish (all addressed)\n\n- **Stale docstring (`test_bridge_flattened_round_trip.py:51-57`)** \u2014 \"whichever the coder picks\" phrasing replaced with \"The coder's driver (task-1-1) accepts the pipeline id as a positional argv[1]; the shim passes it that way.\" Concrete, no longer over-constrains.\n- **`time.time()` timestamp (`test_bridge_flattened_round_trip.py`)** \u2014 replaced with `datetime.now(UTC).isoformat()` at line 251 (matching the driver's `_now_iso()` format). Import updated at line 67 (`from datetime import UTC, datetime`). Test fixture now mirrors the driver's source-of-truth.\n- **Parametrize new roles (`test_rubric_loader.py:149-160`)** \u2014 `test_loader_accepts_enum_and_string_role` now parametrizes all three roles: `refiner` (regression), `reviewer_refine` (new), `reviewer_agent_design` (new) \u2014 both enum and string shapes for each. Six test cases instead of two; all pass.\n- **Path-traversal slice-fence pinning (`test_rubric_loader.py:209-228`)** \u2014 assertion now verifies the path-traversal value resolves to the slice-fence branch (which produces a \"this role is not part of the rollout\" diagnostic) rather than the file-not-on-disk branch. The structural defence (`_RUBRIC_LANDED_ROLES`) is what's load-bearing; the test now pins that.\n- **r2-verdict.json derivation (`test_pretooluse_hook_nested.py`)** \u2014 verdict is now derived from the dispatch outcome and written BEFORE the structured assertions. On regression (dispatch fails to deny), the file records `{\"r2_verdict\": \"fail\", \"reason\": ...}` so slice-5 sees the empirical outcome rather than a stale optimistic value. The `_derive_r2_verdict` helper at lines 132-160 documents the pass/fail decision logic.\n\n### Acceptance criteria check (final)\n\n- **TASK-1-3** (`test_bridge_flattened_round_trip.py`): Test runs in <30s \u2713 (~0.5s wall-clock for the round-trip case); does not require real Anthropic API \u2713 (MagicMock substrate); AC bullet (a) \u2014 preflight question on first run \u2014 VERIFIED via `decision1.get(\"question\") == _PREFLIGHT_QUESTION`; AC bullet (b) \u2014 refine-gate decision on second run \u2014 VERIFIED via `decision2.get(\"question\") != _PREFLIGHT_QUESTION` plus `decision_type in {\"phase_gate\", \"choice\"}`. **Acceptance met.**\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`): Test runs in <60s \u2713; uses TASK-1-9 fake \u2713; asserts hook denies child write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json` \u2713 (now with outcome-derived pass/fail payload); docstring documents empirical-vs-test-fake limitation \u2713. **Acceptance met.**\n- **TASK-1-7** (`test_rubric_loader.py`): All four required cases + adversarial probing \u2713; runs cleanly \u2713; correct location (`shared/tests/`) \u2713. **Acceptance met.**\n\n### Non-blocking (tester v2)\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146` \u2014 `_shim_source` still inline.** Carried over from v1 non-blocking; not addressed. The inline string works fine now that the PYTHONPATH bug is fixed; an extracted `_bridge_shim.py` helper would be a future polish item.\n- **`integration_tests/regression/test_pretooluse_hook_nested.py` \u2014 `_derive_r2_verdict` writes the verdict before the structured assertions.** This is the correct ordering for the downstream slice-5 handoff, but means a flaky assertion (e.g., a test infrastructure bug) could record a misleading \"pass\" on the .json. Worth a follow-up to add a `try/finally`-style \"verdict from final outcome\" pattern. Non-blocking.\n\nNo new findings. The producer's work meets the contract acceptance for all three test files and the cross-module dependency on the coder's `bin/run_pipeline.py` and `_agent_tool_fake.py` is exercised end-to-end.\n", - "ack_version": 2 - }, - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:35.904057+00:00", - "phase": "implement" - }, - { - "id": "fe90958e-0975-41", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:35.953267+00:00", - "phase": "implement" - }, - { - "id": "3dad9998-7c15-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:41.779205+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:41.808645+00:00", - "phase": "implement" - }, - { - "id": "8b63a4a3-cd07-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:41.925127+00:00", - "phase": "implement" - }, - { - "id": "3619b18d-ec17-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:44.749608+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:44.797257+00:00", - "phase": "implement" - }, - { - "id": "013557eb-6382-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:35:29.939223+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:45.013835+00:00", - "phase": "implement" - }, - { - "id": "a4133543-536f-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code_holistic for tester", - "body": "\nHolistic re-review of tester v2 (commit 601df904) against v1 (9ec45ddf). Diff scope: 100 added / 40 removed across the same three test files. Verified each delta against the four mandatory passes.\n\n**Pass 1 \u2014 End-to-end primary use case.** v2 fixes the blocking subprocess PYTHONPATH issue (reviewer_code's #1) by adding `/orchestrator` to the shim env, which is load-bearing for `orchestrator/models.py:16`'s bare-name `from slice_id_validation import SLICE_ID_PATTERN` import and `in_process.py:531-534`'s bare `from models import HITLDecision` fallback. Verified `orchestrator/slice_id_validation.py` exists and that `orchestrator/models.py:16` does the bare-name import. Without v2's fix the subprocess would crash with `ModuleNotFoundError` before the driver yielded its first HITL decision \u2014 so v1 wasn't actually exercising the use case end-to-end. v2 makes the primary refine round-trip walk-through real. \u2713\n\n**Pass 2 \u2014 Doc/code symmetry.** v2 updates the bridge test's \"Driver invocation contract probed\" docstring to drop the stale \"either `EGG_PIPELINE_ID` env or positional `argv[1]`\" hedge and accurately state that the driver accepts positional argv only (with the env still set so any downstream tool reading `EGG_PIPELINE_ID` sees the same id). Addresses my v1 non-blocking #1. The PYTHONPATH comment now enumerates each entry's purpose with concrete bare-import examples. \u2713\n\n**Pass 3 \u2014 Synthetic-key / sentinel coordination.** Three v2 improvements on the cross-module coordination front:\n- `_write_answer` now writes `datetime.now(UTC).isoformat()` matching the driver's `_now_iso()` format at `run_pipeline.py:101-103`. The previous `str(time.time())` would have drifted from the driver's timestamp shape and forced any future schema validator to special-case the test fixture. v2 closes this drift. \u2713\n- The R2 verdict file is now derived from the empirical dispatch outcome: when the structural assertions would fail (hook not denying, verdict dict not block-shaped, reason missing \"tester\"), v2 writes `{\"r2_verdict\": \"fail\", \"reason\": \"\"}` instead of optimistically writing `{\"r2_verdict\": \"pass\"}`. Slice-5's R15 migration task reads this file to decide whether to proceed \u2014 v1's optimistic constant would have lied to that downstream consumer; v2 reflects reality. Addresses my v1 non-blocking observation about empirical-vs-constant verdict. \u2713\n- The verdict file is now written **before** the structural assertions, so a failed assertion still produces a record for the downstream slice-5 consumer instead of a missing-file silent gap. \u2713\n\n**Pass 4 \u2014 Silent-fallback hunt.** v2 strengthens two adversarial probes:\n- `test_loader_accepts_enum_and_string_role` parametrize list extends from `{enum-refiner, str-refiner}` to also cover `{enum-reviewer_refine, str-reviewer_refine, enum-reviewer_agent_design, str-reviewer_agent_design}`. A future loader-normalisation regression that broke the str\u2192enum path only for the **new** slice-1 roles would now be caught \u2014 the previous coverage only pinned the regression role.\n- `test_loader_rejects_path_traversal_role_name` adds an assertion that the diagnostic must contain \"not part of\" / \"rollout\" / \"rubric set\" rather than \"missing on disk\" \u2014 pinning that the `_ROLE_RUBRIC_SLICES` allowlist **intercepts before** any `Path(...).is_file()` walk against attacker-controlled segments. This closes the information-leak existence-oracle vector I flagged in v1 non-blocking #6 and tightens the assertion considerably. \u2713\n\n**Net verdict:** v2 fixes one real blocker (the PYTHONPATH issue that prevented v1 tests from actually exercising the driver subprocess) and incorporates four of my v1 non-blocking observations into the code. All four holistic passes come back clean. Non-blocking items I did NOT see addressed in v2 are the missing regression tests I suggested (corrupt-contract silent reset, answer-without-status silent drop, NotImplementedError \u2192 status=completed) \u2014 these remain post-merge hygiene rather than slice blockers and were never a NACK in v1.\n\n### Non-blocking\n\n- v2 still doesn't add the regression tests I suggested for the coder's silent-fallback findings (`_read_contract` corrupt-JSON reset, status=\"answered\" coordination, NotImplementedError fence). The AC didn't require them; flagging again as worthy post-merge follow-up rather than a blocker.\n\n- v2's r2-verdict \"fail\" payload writes a debug-shaped `reason` (`f\"DispatchResult denied={...}; raw_decision={...}; reason={...}\"`). When slice-5 R15 reads this, the structured fields would be easier to consume than a single repr-formatted string. Consider a richer payload \u2014 `{\"denied\": result.denied, \"raw_decision\": verdict, \"reason\": reason}` \u2014 under the failure path so the downstream consumer doesn't have to regex-parse a diagnostic blob. Non-blocking; the optimistic-pass-removal alone is the bigger win.\n\nACKing.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py", - "shared/tests/test_rubric_loader.py" - ], - "reason": "\nHolistic re-review of tester v2 (commit 601df904) against v1 (9ec45ddf). Diff scope: 100 added / 40 removed across the same three test files. Verified each delta against the four mandatory passes.\n\n**Pass 1 \u2014 End-to-end primary use case.** v2 fixes the blocking subprocess PYTHONPATH issue (reviewer_code's #1) by adding `/orchestrator` to the shim env, which is load-bearing for `orchestrator/models.py:16`'s bare-name `from slice_id_validation import SLICE_ID_PATTERN` import and `in_process.py:531-534`'s bare `from models import HITLDecision` fallback. Verified `orchestrator/slice_id_validation.py` exists and that `orchestrator/models.py:16` does the bare-name import. Without v2's fix the subprocess would crash with `ModuleNotFoundError` before the driver yielded its first HITL decision \u2014 so v1 wasn't actually exercising the use case end-to-end. v2 makes the primary refine round-trip walk-through real. \u2713\n\n**Pass 2 \u2014 Doc/code symmetry.** v2 updates the bridge test's \"Driver invocation contract probed\" docstring to drop the stale \"either `EGG_PIPELINE_ID` env or positional `argv[1]`\" hedge and accurately state that the driver accepts positional argv only (with the env still set so any downstream tool reading `EGG_PIPELINE_ID` sees the same id). Addresses my v1 non-blocking #1. The PYTHONPATH comment now enumerates each entry's purpose with concrete bare-import examples. \u2713\n\n**Pass 3 \u2014 Synthetic-key / sentinel coordination.** Three v2 improvements on the cross-module coordination front:\n- `_write_answer` now writes `datetime.now(UTC).isoformat()` matching the driver's `_now_iso()` format at `run_pipeline.py:101-103`. The previous `str(time.time())` would have drifted from the driver's timestamp shape and forced any future schema validator to special-case the test fixture. v2 closes this drift. \u2713\n- The R2 verdict file is now derived from the empirical dispatch outcome: when the structural assertions would fail (hook not denying, verdict dict not block-shaped, reason missing \"tester\"), v2 writes `{\"r2_verdict\": \"fail\", \"reason\": \"\"}` instead of optimistically writing `{\"r2_verdict\": \"pass\"}`. Slice-5's R15 migration task reads this file to decide whether to proceed \u2014 v1's optimistic constant would have lied to that downstream consumer; v2 reflects reality. Addresses my v1 non-blocking observation about empirical-vs-constant verdict. \u2713\n- The verdict file is now written **before** the structural assertions, so a failed assertion still produces a record for the downstream slice-5 consumer instead of a missing-file silent gap. \u2713\n\n**Pass 4 \u2014 Silent-fallback hunt.** v2 strengthens two adversarial probes:\n- `test_loader_accepts_enum_and_string_role` parametrize list extends from `{enum-refiner, str-refiner}` to also cover `{enum-reviewer_refine, str-reviewer_refine, enum-reviewer_agent_design, str-reviewer_agent_design}`. A future loader-normalisation regression that broke the str\u2192enum path only for the **new** slice-1 roles would now be caught \u2014 the previous coverage only pinned the regression role.\n- `test_loader_rejects_path_traversal_role_name` adds an assertion that the diagnostic must contain \"not part of\" / \"rollout\" / \"rubric set\" rather than \"missing on disk\" \u2014 pinning that the `_ROLE_RUBRIC_SLICES` allowlist **intercepts before** any `Path(...).is_file()` walk against attacker-controlled segments. This closes the information-leak existence-oracle vector I flagged in v1 non-blocking #6 and tightens the assertion considerably. \u2713\n\n**Net verdict:** v2 fixes one real blocker (the PYTHONPATH issue that prevented v1 tests from actually exercising the driver subprocess) and incorporates four of my v1 non-blocking observations into the code. All four holistic passes come back clean. Non-blocking items I did NOT see addressed in v2 are the missing regression tests I suggested (corrupt-contract silent reset, answer-without-status silent drop, NotImplementedError \u2192 status=completed) \u2014 these remain post-merge hygiene rather than slice blockers and were never a NACK in v1.\n\n### Non-blocking\n\n- v2 still doesn't add the regression tests I suggested for the coder's silent-fallback findings (`_read_contract` corrupt-JSON reset, status=\"answered\" coordination, NotImplementedError fence). The AC didn't require them; flagging again as worthy post-merge follow-up rather than a blocker.\n\n- v2's r2-verdict \"fail\" payload writes a debug-shaped `reason` (`f\"DispatchResult denied={...}; raw_decision={...}; reason={...}\"`). When slice-5 R15 reads this, the structured fields would be easier to consume than a single repr-formatted string. Consider a richer payload \u2014 `{\"denied\": result.denied, \"raw_decision\": verdict, \"reason\": reason}` \u2014 under the failure path so the downstream consumer doesn't have to regex-parse a diagnostic blob. Non-blocking; the optimistic-pass-removal alone is the bigger win.\n\nACKing.\n", - "ack_version": 2 - }, - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:45.767437+00:00", - "phase": "implement" - }, - { - "id": "07dd4c4a-3203-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_code", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:48.330667+00:00", - "phase": "implement" - }, - { - "id": "02b47dfd-da16-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:48.406614+00:00", - "phase": "implement" - }, - { - "id": "921cc5ae-4f18-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:48.412773+00:00", - "phase": "implement" - }, - { - "id": "0e508f01-84fe-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:52.355972+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:52.391858+00:00", - "phase": "implement" - }, - { - "id": "209a5393-5a3f-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:52.354482+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:52.399258+00:00", - "phase": "implement" - }, - { - "id": "cca5ac65-4e87-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:52.547441+00:00", - "phase": "implement" - }, - { - "id": "521669d3-a09c-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:52.939357+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:52.964995+00:00", - "phase": "implement" - }, - { - "id": "42289b40-d041-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_concurrency for tester", - "body": "\n**Concurrency-lens re-review of tester v2 (commit 601df9041)**\n\nRe-reviewed the v1\u2192v2 delta with attention to concurrency invariants. No blocking concurrency findings; v2 does not introduce any new concurrency surface and the changes are well-contained.\n\n### v2 delta against v1\n\n**1. `test_bridge_flattened_round_trip.py`**\n\n- **PYTHONPATH expansion** (`:166-189`): adds `/orchestrator` to the subprocess PYTHONPATH list. Same `os.pathsep.join(...)` construction, same per-subprocess env snapshot semantics. The new path enables bare-name imports inside the orchestrator tree (`orchestrator/models.py:16 from slice_id_validation import SLICE_ID_PATTERN`, `in_process.py:531-534 from models import HITLDecision` fallback). No new race surface \u2014 PYTHONPATH is read once at subprocess startup and never mutated thereafter.\n- **Timestamp format swap** (`:246-252`): `str(time.time())` \u2192 `datetime.now(UTC).isoformat()`. `datetime.now(UTC)` returns a fresh immutable instance per call; thread-safe. The change aligns the test's `_write_answer` output with `run_pipeline.py:101-103`'s ISO-8601 envelope timestamp, eliminating a possible format-drift surface between the test fixture and the driver's source of truth.\n- **Docstring clarification** (`:49-57`): purely documentation; no behavioural change.\n\n**2. `test_pretooluse_hook_nested.py`**\n\n- **r2-verdict write reordered to before assertions** (`:171-205`): the verdict file is now derived from the empirical `dispatch(...)` result and written *before* the `denied/decision/reason` assertions. Strictly sequential within the test function \u2014 no concurrency surface change. The reordering actually improves diagnostic value: if a future regression breaks one of the structured assertions, the verdict file still records the empirical failure (`{\"r2_verdict\": \"fail\", \"reason\": ...}`) for slice-5's R15 consumer rather than silently leaving a stale `pass` from a previous run. The fail-payload's `reason` field uses `repr()` on `verdict` which serialises any dict safely.\n- **Verdict-deriving guard chain** (`:184-189`): uses `getattr(result, ..., None)` for all fields \u2014 safe defensive read, no AttributeError surface. The boolean chain is short-circuit-evaluated in declaration order; no side effects in any predicate.\n- **Tightened non-empty dict assertion** (`:218-221`): `isinstance(verdict, dict) and verdict` adds an empty-dict catch. No concurrency relevance.\n\n**3. `test_rubric_loader.py`**\n\n- **Parametrization expansion** (`:148-164`): adds four new param cases for the new roles. Each parametrized test runs independently in pytest (and as separate test items under xdist). The loader reads distinct rubric files per role \u2014 no shared-state collision possible between workers. The `_load(role_input)` call is pure read-only file I/O. \u2713\n- **Tightened path-traversal assertion** (`:226-240`): adds an assertion that the diagnostic specifically identifies the role as \"not part of the rollout's rubric set\" rather than \"missing on disk\". This pins the allowlist-fence-fires-first invariant, eliminating an existence-oracle vector via attacker-controlled `Path.is_file()` calls. Pure assertion change \u2014 no concurrency surface.\n\n### What v2 does NOT touch\n\n- The subprocess timeouts (`20s` in bridge test) are unchanged.\n- The shim's `_HEARTBEAT_INTERVAL`/`_BRC_REVIEW_INTERVAL`/`_BUS_TICK_INTERVAL` overrides are unchanged (still 0.05s, still scoped to the subprocess only).\n- The `sys.modules.pop` in the fake fixture is unchanged.\n- The xdist-safe `tmp_path` isolation pattern is unchanged across all three files.\n- BRC-protocol invariants: still not touched.\n\n### Carry-over of v1 non-blocking observations\n\nMy v1 non-blockings (interval-knob defensive `hasattr`, module-cache widening, module-scoped `pytest.mark.timeout(30)`) remain unaddressed in v2 \u2014 none were prerequisites for ACK and v2's scope was explicitly the reviewer_code v1 NACK. They are still worth landing as a future polish pass.\n\nACK v2 unconditionally on the concurrency lens.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_bridge_flattened_round_trip.py", - "integration_tests/regression/test_pretooluse_hook_nested.py", - "shared/tests/test_rubric_loader.py" - ], - "reason": "\n**Concurrency-lens re-review of tester v2 (commit 601df9041)**\n\nRe-reviewed the v1\u2192v2 delta with attention to concurrency invariants. No blocking concurrency findings; v2 does not introduce any new concurrency surface and the changes are well-contained.\n\n### v2 delta against v1\n\n**1. `test_bridge_flattened_round_trip.py`**\n\n- **PYTHONPATH expansion** (`:166-189`): adds `/orchestrator` to the subprocess PYTHONPATH list. Same `os.pathsep.join(...)` construction, same per-subprocess env snapshot semantics. The new path enables bare-name imports inside the orchestrator tree (`orchestrator/models.py:16 from slice_id_validation import SLICE_ID_PATTERN`, `in_process.py:531-534 from models import HITLDecision` fallback). No new race surface \u2014 PYTHONPATH is read once at subprocess startup and never mutated thereafter.\n- **Timestamp format swap** (`:246-252`): `str(time.time())` \u2192 `datetime.now(UTC).isoformat()`. `datetime.now(UTC)` returns a fresh immutable instance per call; thread-safe. The change aligns the test's `_write_answer` output with `run_pipeline.py:101-103`'s ISO-8601 envelope timestamp, eliminating a possible format-drift surface between the test fixture and the driver's source of truth.\n- **Docstring clarification** (`:49-57`): purely documentation; no behavioural change.\n\n**2. `test_pretooluse_hook_nested.py`**\n\n- **r2-verdict write reordered to before assertions** (`:171-205`): the verdict file is now derived from the empirical `dispatch(...)` result and written *before* the `denied/decision/reason` assertions. Strictly sequential within the test function \u2014 no concurrency surface change. The reordering actually improves diagnostic value: if a future regression breaks one of the structured assertions, the verdict file still records the empirical failure (`{\"r2_verdict\": \"fail\", \"reason\": ...}`) for slice-5's R15 consumer rather than silently leaving a stale `pass` from a previous run. The fail-payload's `reason` field uses `repr()` on `verdict` which serialises any dict safely.\n- **Verdict-deriving guard chain** (`:184-189`): uses `getattr(result, ..., None)` for all fields \u2014 safe defensive read, no AttributeError surface. The boolean chain is short-circuit-evaluated in declaration order; no side effects in any predicate.\n- **Tightened non-empty dict assertion** (`:218-221`): `isinstance(verdict, dict) and verdict` adds an empty-dict catch. No concurrency relevance.\n\n**3. `test_rubric_loader.py`**\n\n- **Parametrization expansion** (`:148-164`): adds four new param cases for the new roles. Each parametrized test runs independently in pytest (and as separate test items under xdist). The loader reads distinct rubric files per role \u2014 no shared-state collision possible between workers. The `_load(role_input)` call is pure read-only file I/O. \u2713\n- **Tightened path-traversal assertion** (`:226-240`): adds an assertion that the diagnostic specifically identifies the role as \"not part of the rollout's rubric set\" rather than \"missing on disk\". This pins the allowlist-fence-fires-first invariant, eliminating an existence-oracle vector via attacker-controlled `Path.is_file()` calls. Pure assertion change \u2014 no concurrency surface.\n\n### What v2 does NOT touch\n\n- The subprocess timeouts (`20s` in bridge test) are unchanged.\n- The shim's `_HEARTBEAT_INTERVAL`/`_BRC_REVIEW_INTERVAL`/`_BUS_TICK_INTERVAL` overrides are unchanged (still 0.05s, still scoped to the subprocess only).\n- The `sys.modules.pop` in the fake fixture is unchanged.\n- The xdist-safe `tmp_path` isolation pattern is unchanged across all three files.\n- BRC-protocol invariants: still not touched.\n\n### Carry-over of v1 non-blocking observations\n\nMy v1 non-blockings (interval-knob defensive `hasattr`, module-cache widening, module-scoped `pytest.mark.timeout(30)`) remain unaddressed in v2 \u2014 none were prerequisites for ACK and v2's scope was explicitly the reviewer_code v1 NACK. They are still worth landing as a future polish pass.\n\nACK v2 unconditionally on the concurrency lens.\n", - "ack_version": 2 - }, - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:53.974797+00:00", - "phase": "implement" - }, - { - "id": "67477ec2-0362-43", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "tester", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 2) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 2, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:53.975252+00:00", - "phase": "implement" - }, - { - "id": "084491cb-b5da-4a", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:36:59.018651+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:59.067033+00:00", - "phase": "implement" - }, - { - "id": "7928ea5e-5e99-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:36:59.217799+00:00", - "phase": "implement" - }, - { - "id": "12d05874-794c-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:37:00.068582+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:00.136098+00:00", - "phase": "implement" - }, - { - "id": "d999b500-5f1c-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:37:00.496179+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:00.518077+00:00", - "phase": "implement" - }, - { - "id": "755d0595-80f5-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:00.587659+00:00", - "phase": "implement" - }, - { - "id": "bb5f70a1-9575-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:37:01.328027+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:01.408381+00:00", - "phase": "implement" - }, - { - "id": "f1b2fb7e-4310-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:01.640752+00:00", - "phase": "implement" - }, - { - "id": "bdb8bdd9-f544-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by tester", - "body": "", - "metadata": { - "consensus_reached": true, - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:03.410216+00:00", - "phase": "implement" - }, - { - "id": "48996506-f063-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:03.529745+00:00", - "phase": "implement" - }, - { - "id": "dd6584ed-5c69-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:03.540946+00:00", - "phase": "implement" - }, - { - "id": "e1cd69f0-7aaf-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:03.595158+00:00", - "phase": "implement" - }, - { - "id": "eb84b080-aeea-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:37:07.259426+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:07.306145+00:00", - "phase": "implement" - }, - { - "id": "79f5ae08-565b-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:07.391142+00:00", - "phase": "implement" - }, - { - "id": "483227c8-c965-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:37:08.010167+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:08.062176+00:00", - "phase": "implement" - }, - { - "id": "42c21fdf-3992-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:37:08.052053+00:00", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:08.097076+00:00", - "phase": "implement" - }, - { - "id": "ce6c5770-0884-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-1" - }, - "timestamp": "2026-05-19T19:37:08.141886+00:00", - "phase": "implement" - } -] \ No newline at end of file diff --git a/.egg-state/brc-history/2717-implement-slice-1.md b/.egg-state/brc-history/2717-implement-slice-1.md deleted file mode 100644 index b5bcfc17d3..0000000000 --- a/.egg-state/brc-history/2717-implement-slice-1.md +++ /dev/null @@ -1,8071 +0,0 @@ -# BRC Consensus History — implement phase, slice-1 - -Generated: 2026-05-19T19:37:08Z -Pipeline: issue-2717 -Slice: slice-1 - -### [2026-05-19T18:45:46Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8ee719ed-15c5-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:39.347242+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:45:46Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: aad4e217-17c2-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:46:46Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 25d509b8-0f4c-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:46:46Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1ee6d089-7024-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:39.347242+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:46:46Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 724c6344-9ff3-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:46:46Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3f914a9d-3916-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:47:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f8c1ca72-7448-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:47:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8d1a2a9a-c024-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:12.376437+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:47:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1a2cb803-31e4-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:47:46Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 704666c6-107d-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:47:46Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 90f511c6-6ec8-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:48:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c14ce1df-746c-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:48:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: dbc53669-3874-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:12.376437+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:48:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b810c5b4-5e66-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:48:46Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7d058934-1245-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:48:46Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1a5c733a-7af9-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:49:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 40176315-999c-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:49:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 75f24eaf-92e2-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:12.376437+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:49:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 6f797313-d32a-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:49:47Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: cf8ee2ec-ed39-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:49:47Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 34bdb5b2-0bfb-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:49:49Z] tester (HEARTBEAT): heartbeat: WORKING - -Scaffolding three tester test files (test_rubric_loader.py, test_bridge_flattened_round_trip.py, test_pretooluse_hook_nested.py) while waiting on coder's CONSENSUS_PROPOSE - -````yaml -id: 4757899d-b940-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T18:50:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 62d9e767-804a-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:50:24.156617+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:50:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7e286a99-fce9-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:50:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3e5225be-94b2-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:50:47Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 651b9866-22cf-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:50:47Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: a42456c2-4503-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:51:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 829d83f5-31ed-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:50:24.156617+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:51:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b6d5f3ea-c1cf-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:51:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: fdcf4ea8-c40b-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:51:47Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 008b6076-8754-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:51:47Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: dda17458-c129-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:52:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 74ebecc1-c1b1-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:50:24.156617+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:52:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 12e6ad93-4a25-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:52:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: cf2a2430-07e8-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:52:47Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0d43a2b7-7ad0-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:52:47Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c76502b2-bab5-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:53:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 968a2b9e-f1ca-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:50:24.156617+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:53:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 63ba3813-cdc8-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:53:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1ed59bc5-0600-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:53:47Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e5df3e05-e1ca-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:53:47Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f085a577-14b1-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:54:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1f7180ad-af19-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:50:24.156617+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:54:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d2120111-4dc5-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:54:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 124385b1-d0b6-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:54:47Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8bdcf047-4c4f-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:54:47Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2aa5d4a1-7468-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:55:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e022449f-7cbd-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:50:24.156617+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:55:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 469df723-0da6-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:55:58Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0ab7afa8-5258-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:55:58Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 56e05919-9163-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:55:46.419132+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:55:58Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e4ed5abe-c070-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:55:58Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9af9587e-2758-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:56:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9f05b628-d016-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:50:24.156617+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:56:25Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9bfe42b6-844a-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:47:10.300331+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:56:59Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 427f66bd-eff0-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:22.097972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:56:59Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 42721b47-d78e-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:55:46.419132+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:56:59Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c8556b5d-8ebc-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:45:41.280300+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:56:59Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7723eaf3-b5e3-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:46:40.861835+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:57:24Z] coder (CONSENSUS_PROPOSE): Proposal from coder - -slice-1 coder: bridge driver (TASK-1-1), R2 nested-dispatch fake (TASK-1-9), and rubric loader expansion (TASK-1-6). The bridge driver `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` is the cq-1 Option C flattened refine/plan path — it advances `run_pipeline_in_process` to its next yield, serialises the yielded HITLDecision into `.egg-state/contracts/.json#pending_hitl` under a stable schema (version, decision, answer, answer_log, status, result, error), and exits; cross-process state is recovered by replaying answer_log per invocation. Slice-3's daemon variant (TASK-3-2) consumes the same envelope schema (risk_analyst R17 mitigation). The `_agent_tool_fake.py` is test-only infrastructure (import-guarded against production use) that simulates nested Agent-tool dispatch by spawning a child subprocess with controlled EGG_AGENT_ROLE which then invokes `hook_entry.decide(...)` — answers the hook-logic half of R2. Loader update introduces `_ROLE_RUBRIC_SLICES` mapping so reviewer_refine / reviewer_agent_design load when the documenter's rubric .md files land (TASK-1-4), plan/implement roles raise the new "deferred to follow-up slice-N" structured error per the acceptance criterion. End-to-end smoke confirmed the driver round-trips an answer through the contract and advances to the next yield. Lint+format clean (ruff); mypy clean on new files (6 pre-existing substrate/__init__.py errors on lines 180-198 unrelated). All 52 existing claude_code substrate tests still pass. - -````yaml -id: 3cf81114-684e-47 -phase: implement -metadata: - payload: - summary: "slice-1 coder: bridge driver (TASK-1-1), R2 nested-dispatch fake (TASK-1-9),\ - \ and rubric loader expansion (TASK-1-6). The bridge driver `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py`\ - \ is the cq-1 Option C flattened refine/plan path \u2014 it advances `run_pipeline_in_process`\ - \ to its next yield, serialises the yielded HITLDecision into `.egg-state/contracts/.json#pending_hitl`\ - \ under a stable schema (version, decision, answer, answer_log, status, result,\ - \ error), and exits; cross-process state is recovered by replaying answer_log\ - \ per invocation. Slice-3's daemon variant (TASK-3-2) consumes the same envelope\ - \ schema (risk_analyst R17 mitigation). The `_agent_tool_fake.py` is test-only\ - \ infrastructure (import-guarded against production use) that simulates nested\ - \ Agent-tool dispatch by spawning a child subprocess with controlled EGG_AGENT_ROLE\ - \ which then invokes `hook_entry.decide(...)` \u2014 answers the hook-logic\ - \ half of R2. Loader update introduces `_ROLE_RUBRIC_SLICES` mapping so reviewer_refine\ - \ / reviewer_agent_design load when the documenter's rubric .md files land (TASK-1-4),\ - \ plan/implement roles raise the new \"deferred to follow-up slice-N\" structured\ - \ error per the acceptance criterion. End-to-end smoke confirmed the driver\ - \ round-trips an answer through the contract and advances to the next yield.\ - \ Lint+format clean (ruff); mypy clean on new files (6 pre-existing substrate/__init__.py\ - \ errors on lines 180-198 unrelated). All 52 existing claude_code substrate\ - \ tests still pass." - attestation: {} - artifacts: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - risk_considered: "R17 dual-bridge schema risk mitigated by exposing PENDING_HITL_SCHEMA_VERSION\ - \ and documenting the envelope as a stable contract. R2 hook-logic question\ - \ answered by the test fake (slice-1 tester's TASK-1-5 will assert the deny\ - \ verdict). Loader fence preserves the structured-error contract for unsupported\ - \ roles. Driver `_advance_generator` always closes the generator in a finally\ - \ block so background threads join cleanly via GeneratorExit. Mypy regression\ - \ risk: my changes don't introduce new errors \u2014 the 6 pre-existing errors\ - \ in substrate/__init__.py:180-198 are baseline." - commit_sha: fddeb82bf46b3acbf65a20283469690dcb2af481 - files_changed: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - tests_run: [] - tasks_satisfied: - - task-1-1 - - task-1-9 - - task-1-6 - version: 1 - commit_sha: fddeb82bf46b3acbf65a20283469690dcb2af481 - slice_id: slice-1 -```` - -### [2026-05-19T18:57:24Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 09580a4d-5dff-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T18:57:24Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6b5f0096-c1a1-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T18:57:24Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 58a3c176-ddda-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T18:57:24Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c62f7fa2-64a9-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T18:57:24Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 4123058c-af69-47 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T18:57:24Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 48f228e5-10c6-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T18:57:52Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e86647cf-75b8-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:57:37.143439+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:59:07Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 24bbacba-c6a2-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:57:37.143439+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T18:59:07Z] documenter (CONSENSUS_PROPOSE): Proposal from documenter - -Slice 1 of #2717 documenter half: refine-team rubrics + flattened-bridge docs + ADR rollout-deltas tracker. Adds two new reviewer rubric files (reviewer_refine.md, reviewer_agent_design.md) under plugins/egg-sdlc/skills/egg-sdlc/agents/ mirroring the layout of plugins/refine-plan/skills/refine-plan/agents/ (TASK-1-4). The filenames match the coder's loader expectation (orchestrator/substrate/__init__.py:330 `{role_name}.md`); the underscore form lines up with the AgentRole identifiers and the loader's `_RUBRIC_LANDED_ROLES` frozenset. Updates plugins/egg-sdlc/skills/egg-sdlc/SKILL.md to replace the "Walking-skeleton bridge gap" callout with a "How the flattened bridge works" section naming pending_hitl as the single-yield carrier and documenting the skill→bin/run_pipeline.py loop; updates the R2 PreToolUse-hook section to point at the new slice-1 test infrastructure (integration_tests/regression/test_pretooluse_hook_nested.py + _agent_tool_fake.py); reframes "What's NOT in this skill" against the slice-2..5 rollout map (TASK-1-2). Updates docs/architecture/claude-code-substrate.md: status banner reframes from spike to spike→rollout, cq-2/cq-7/cq-11 rows reflect slice-1 deltas, the in-process orchestrator section grows 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 (R17 mitigation), R2 / R15 subsections reflect the slice-1 worked example and the slice-5 contingent fallback (cq-6 option 2 + R15 model (b)), the unified "Rollout deltas" section split into Completed-in-this-rollout (3 [x] items for slice 1) and Pending-in-this-rollout (9 items mapped to slices 2-5), primitives + conformance-proof tables pick up the new slice-1 modules (TASK-1-8). Files reviewed: all four touched files plus the plan draft (.egg-state/drafts/2717-plan.md slice-1 task definitions), the coder's orchestrator/substrate/__init__.py loader changes (confirmed underscore filenames match _RUBRIC_LANDED_ROLES), and the two existing refine-plan rubrics that the new substrate rubrics mirror. - -````yaml -id: 7ae96e4b-cc68-47 -phase: implement -metadata: - payload: - summary: "Slice 1 of #2717 documenter half: refine-team rubrics + flattened-bridge\ - \ docs + ADR rollout-deltas tracker. Adds two new reviewer rubric files (reviewer_refine.md,\ - \ reviewer_agent_design.md) under plugins/egg-sdlc/skills/egg-sdlc/agents/ mirroring\ - \ the layout of plugins/refine-plan/skills/refine-plan/agents/ (TASK-1-4). The\ - \ filenames match the coder's loader expectation (orchestrator/substrate/__init__.py:330\ - \ `{role_name}.md`); the underscore form lines up with the AgentRole identifiers\ - \ and the loader's `_RUBRIC_LANDED_ROLES` frozenset. Updates plugins/egg-sdlc/skills/egg-sdlc/SKILL.md\ - \ to replace the \"Walking-skeleton bridge gap\" callout with a \"How the flattened\ - \ bridge works\" section naming pending_hitl as the single-yield carrier and\ - \ documenting the skill\u2192bin/run_pipeline.py loop; updates the R2 PreToolUse-hook\ - \ section to point at the new slice-1 test infrastructure (integration_tests/regression/test_pretooluse_hook_nested.py\ - \ + _agent_tool_fake.py); reframes \"What's NOT in this skill\" against the\ - \ slice-2..5 rollout map (TASK-1-2). Updates docs/architecture/claude-code-substrate.md:\ - \ status banner reframes from spike to spike\u2192rollout, cq-2/cq-7/cq-11 rows\ - \ reflect slice-1 deltas, the in-process orchestrator section grows 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 (R17 mitigation),\ - \ R2 / R15 subsections reflect the slice-1 worked example and the slice-5 contingent\ - \ fallback (cq-6 option 2 + R15 model (b)), the unified \"Rollout deltas\" section\ - \ split into Completed-in-this-rollout (3 [x] items for slice 1) and Pending-in-this-rollout\ - \ (9 items mapped to slices 2-5), primitives + conformance-proof tables pick\ - \ up the new slice-1 modules (TASK-1-8). Files reviewed: all four touched files\ - \ plus the plan draft (.egg-state/drafts/2717-plan.md slice-1 task definitions),\ - \ the coder's orchestrator/substrate/__init__.py loader changes (confirmed underscore\ - \ filenames match _RUBRIC_LANDED_ROLES), and the two existing refine-plan rubrics\ - \ that the new substrate rubrics mirror." - attestation: {} - artifacts: - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - docs/architecture/claude-code-substrate.md - risk_considered: "Doc-only slice; no source or test files touched. Risk surface:\ - \ (1) docs referencing files that may not be on the branch yet \u2014 the coder's\ - \ commit fddeb82bf is now merged in, so bin/run_pipeline.py + _agent_tool_fake.py\ - \ + the loader changes are present; test_pretooluse_hook_nested.py and test_rubric_loader.py\ - \ will be supplied by the tester on the same slice-1 branch before the slice\ - \ merges; both are referenced in declarative \"lands in slice 1\" voice. (2)\ - \ Stale anchor / link references \u2014 addressed by replace_all on the removed\ - \ \"Follow-up issue draft\" anchor across the ADR. (3) Frontmatter shape for\ - \ the two new reviewer rubrics \u2014 mirrors the existing plugins/refine-plan/skills/refine-plan/agents/reviewer-*.md\ - \ frontmatter so the orchestrator's rubric loader does not need new parsing\ - \ logic; loader reads the body verbatim and prepends to the system prompt (frontmatter\ - \ is informational only per refiner.md). (4) Filename underscore-vs-hyphen \u2014\ - \ the new files use underscores (reviewer_refine.md, reviewer_agent_design.md)\ - \ matching the AgentRole.value identifiers and the loader's `{role_name}.md`\ - \ resolution at orchestrator/substrate/__init__.py:330. (5) Loader hint message\ - \ \u2014 the coder's _RUBRIC_LANDED_ROLES check raises ValueError with a \"\ - follow-up slice 2/3\" pointer for plan/implement roles; the ADR's \"What is\ - \ and is not in this slice\" subsections cross-reference this exact mapping\ - \ so a future grep for the slice-hint string finds both surfaces." - commit_sha: 473b109cbcaa54d4f89b84c4422139a6377b11c9 - files_changed: - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - docs/architecture/claude-code-substrate.md - tests_run: [] - tasks_satisfied: - - TASK-1-2 - - TASK-1-4 - - TASK-1-8 - version: 1 - commit_sha: 473b109cbcaa54d4f89b84c4422139a6377b11c9 - slice_id: slice-1 -```` - -### [2026-05-19T18:59:57Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b94d1e1b-831f-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:00:07Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 683a5120-d456-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:57:37.143439+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:00:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e5346af9-176b-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:01:07Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5725d046-75e5-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:57:37.143439+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:01:44Z] reviewer_concurrency → coder (CONSENSUS_ACK): ACK from reviewer_concurrency for coder - - -**Concurrency-lens review of coder v1 (commit fddeb82bf)** - -Reviewed all three files for race conditions, deadlocks, shared-state mutation, async-context leakage, retry-storm patterns, resource-cleanup ordering, and BRC-protocol invariants. No blocking concurrency findings. - -### What I checked - -**1. `bin/run_pipeline.py` — flattened single-yield driver** - -- **Cross-process state model**: each `python3 run_pipeline.py` invocation is a fresh process; generator state is rebuilt by replaying `answer_log` into a fresh generator. No live state shared across invocations → no cross-process race surface. -- **Atomic contract write** (`_write_contract:140-147`): `tmp.write_text()` + `os.replace()` is correct for atomic single-writer; concurrent **readers** cannot observe a half-written file. Single-writer is enforced by the skill protocol (driver and skill body alternate sequentially). -- **Generator lifecycle** (`_advance_generator:267-327`): `next() → for replay: send() → finally: generator.close()`. The `try/finally` reliably triggers `GeneratorExit` before `_persist_envelope` writes the new envelope, so `_InProcessOrchestrator`'s 3 daemon threads (heartbeat / BRC re-review / bus tick) are joined inside the orchestrator's own `finally` before the driver writes the final envelope. No thread-write race against the contract file at exit. Daemon threads that *don't* terminate promptly die with the process anyway (daemon=True) — no thread leak across invocations. -- **env snapshot** (`:284-287`): `{**os.environ, "EGG_SUBSTRATE": ...}` builds a copy. Safe even if a thread mutated `os.environ` mid-call (which nothing does here). -- **Status-promotion read-after-close** (`:502-505`): reads `envelope["answer_log"][-1]` *after* the generator is closed — local variable only, no race. -- **Abort handler** (`:467-475`, `:487-497`): error envelope is written on every failure path with `answer_log` preserved; no resource leaks on the unhappy path. - -**2. `_agent_tool_fake.py` — subprocess R2-hook fake** - -- **Subprocess timeout** (`:230`, default 10s): bounded wall-clock cap. `subprocess.run(..., timeout=timeout)` kills the child with SIGKILL on expiry. Acceptable for the R2 test scenario (hook invocation is sub-second). -- **Pipe-fill deadlock**: `capture_output=True, text=True` routes both stdout/stderr through `subprocess.Popen.communicate()` under the hood, which drains both pipes concurrently. No risk of a full stderr buffer blocking the child. -- **Blob-file cleanup** (`:261-333`): `tempfile.mkstemp()` + `try/finally: Path(blob_path).unlink(missing_ok=True)`. The finally runs on every exit path *including* `TimeoutExpired` propagation — temp file is always cleaned up. The `fdopen` `with` block closes the parent's fd before the subprocess opens its own descriptor to the path. -- **sys.path manipulation** (`:101-126`): module-import-time only, inside `try/finally` that removes the injected path. Python's import lock serializes this; concurrent imports from multiple threads are safe. The `from run_pipeline import ...` happens once per process. -- **Child isolation** (`:293-301`): fresh Python interpreter per dispatch; no shared mutable state with parent. The `env = {**os.environ, "EGG_AGENT_ROLE": role}` snapshot guarantees the child sees a controlled role. -- **xdist-worker safety**: tempfile names use `tempfile.mkstemp(prefix="agent_tool_fake_", ...)` — globally unique per-call, no fixture-file collision risk between pytest workers running this fake in parallel. - -**3. `orchestrator/substrate/__init__.py` — rubric loader expansion** - -- Adds two **immutable module-level constants** (`_ROLE_RUBRIC_SLICES` dict, `_RUBRIC_LANDED_ROLES` frozenset). The dict is mutable in principle but never mutated in code. Both are read-only references inside `_load_egg_sdlc_role_rubric`. No write-side concurrency surface. -- The loader is pure read-only file I/O (`rubric_path.is_file()` + `rubric_path.read_text()`). No shared state, no locks needed, no async context. - -### BRC-protocol invariants - -This PR does not touch the BRC message bus, consensus protocol, send→wait ordering, `--since` cursor threading, heartbeat cadence, `stale_reviewers` invalidation, or the `max_flip_flops=3` cap. No BRC-invariant concerns. - -### Non-blocking - -- **run_pipeline.py:145** — `_write_contract` uses a fixed `.json.tmp` filename. The skill protocol is sequential by design, but as defense-in-depth (e.g. a stuck previous driver invocation) consider a unique tempfile via `tempfile.NamedTemporaryFile(dir=contract_path.parent, prefix=f".{contract_path.stem}_", suffix=".json.tmp", delete=False)` so two concurrent processes cannot clobber each other's pre-replace temp. -- **run_pipeline.py:326** — `try: generator.close() except Exception: pass` silently swallows any exception from the orchestrator's `finally` block (including a future thread-join hang). Consider `traceback.print_exc(file=sys.stderr)` inside the except — keeps the defensive swallow but leaves a diagnostic trail when `_InProcessOrchestrator` cleanup misbehaves. -- **_agent_tool_fake.py:298** — `subprocess.TimeoutExpired` propagates unhandled from `pre_tool_use_callback`; the surrounding `dispatch()` returns no `DispatchResult` on timeout. For test-infrastructure uniformity, consider catching it and returning `{"decision": "block", "reason": "_agent_tool_fake child timed out after Xs"}` — mirrors the other structured-failure paths at `:306-314` and `:318-325`. - - -````yaml -id: ad4e09d5-300b-4a -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - reason: "\n**Concurrency-lens review of coder v1 (commit fddeb82bf)**\n\nReviewed\ - \ all three files for race conditions, deadlocks, shared-state mutation, async-context\ - \ leakage, retry-storm patterns, resource-cleanup ordering, and BRC-protocol\ - \ invariants. No blocking concurrency findings.\n\n### What I checked\n\n**1.\ - \ `bin/run_pipeline.py` \u2014 flattened single-yield driver**\n\n- **Cross-process\ - \ state model**: each `python3 run_pipeline.py` invocation is a fresh process;\ - \ generator state is rebuilt by replaying `answer_log` into a fresh generator.\ - \ No live state shared across invocations \u2192 no cross-process race surface.\n\ - - **Atomic contract write** (`_write_contract:140-147`): `tmp.write_text()`\ - \ + `os.replace()` is correct for atomic single-writer; concurrent **readers**\ - \ cannot observe a half-written file. Single-writer is enforced by the skill\ - \ protocol (driver and skill body alternate sequentially).\n- **Generator lifecycle**\ - \ (`_advance_generator:267-327`): `next() \u2192 for replay: send() \u2192 finally:\ - \ generator.close()`. The `try/finally` reliably triggers `GeneratorExit` before\ - \ `_persist_envelope` writes the new envelope, so `_InProcessOrchestrator`'s\ - \ 3 daemon threads (heartbeat / BRC re-review / bus tick) are joined inside\ - \ the orchestrator's own `finally` before the driver writes the final envelope.\ - \ No thread-write race against the contract file at exit. Daemon threads that\ - \ *don't* terminate promptly die with the process anyway (daemon=True) \u2014\ - \ no thread leak across invocations.\n- **env snapshot** (`:284-287`): `{**os.environ,\ - \ \"EGG_SUBSTRATE\": ...}` builds a copy. Safe even if a thread mutated `os.environ`\ - \ mid-call (which nothing does here).\n- **Status-promotion read-after-close**\ - \ (`:502-505`): reads `envelope[\"answer_log\"][-1]` *after* the generator is\ - \ closed \u2014 local variable only, no race.\n- **Abort handler** (`:467-475`,\ - \ `:487-497`): error envelope is written on every failure path with `answer_log`\ - \ preserved; no resource leaks on the unhappy path.\n\n**2. `_agent_tool_fake.py`\ - \ \u2014 subprocess R2-hook fake**\n\n- **Subprocess timeout** (`:230`, default\ - \ 10s): bounded wall-clock cap. `subprocess.run(..., timeout=timeout)` kills\ - \ the child with SIGKILL on expiry. Acceptable for the R2 test scenario (hook\ - \ invocation is sub-second).\n- **Pipe-fill deadlock**: `capture_output=True,\ - \ text=True` routes both stdout/stderr through `subprocess.Popen.communicate()`\ - \ under the hood, which drains both pipes concurrently. No risk of a full stderr\ - \ buffer blocking the child.\n- **Blob-file cleanup** (`:261-333`): `tempfile.mkstemp()`\ - \ + `try/finally: Path(blob_path).unlink(missing_ok=True)`. The finally runs\ - \ on every exit path *including* `TimeoutExpired` propagation \u2014 temp file\ - \ is always cleaned up. The `fdopen` `with` block closes the parent's fd before\ - \ the subprocess opens its own descriptor to the path.\n- **sys.path manipulation**\ - \ (`:101-126`): module-import-time only, inside `try/finally` that removes the\ - \ injected path. Python's import lock serializes this; concurrent imports from\ - \ multiple threads are safe. The `from run_pipeline import ...` happens once\ - \ per process.\n- **Child isolation** (`:293-301`): fresh Python interpreter\ - \ per dispatch; no shared mutable state with parent. The `env = {**os.environ,\ - \ \"EGG_AGENT_ROLE\": role}` snapshot guarantees the child sees a controlled\ - \ role.\n- **xdist-worker safety**: tempfile names use `tempfile.mkstemp(prefix=\"\ - agent_tool_fake_\", ...)` \u2014 globally unique per-call, no fixture-file collision\ - \ risk between pytest workers running this fake in parallel.\n\n**3. `orchestrator/substrate/__init__.py`\ - \ \u2014 rubric loader expansion**\n\n- Adds two **immutable module-level constants**\ - \ (`_ROLE_RUBRIC_SLICES` dict, `_RUBRIC_LANDED_ROLES` frozenset). The dict is\ - \ mutable in principle but never mutated in code. Both are read-only references\ - \ inside `_load_egg_sdlc_role_rubric`. No write-side concurrency surface.\n\ - - The loader is pure read-only file I/O (`rubric_path.is_file()` + `rubric_path.read_text()`).\ - \ No shared state, no locks needed, no async context.\n\n### BRC-protocol invariants\n\ - \nThis PR does not touch the BRC message bus, consensus protocol, send\u2192\ - wait ordering, `--since` cursor threading, heartbeat cadence, `stale_reviewers`\ - \ invalidation, or the `max_flip_flops=3` cap. No BRC-invariant concerns.\n\n\ - ### Non-blocking\n\n- **run_pipeline.py:145** \u2014 `_write_contract` uses\ - \ a fixed `.json.tmp` filename. The skill protocol is sequential by design,\ - \ but as defense-in-depth (e.g. a stuck previous driver invocation) consider\ - \ a unique tempfile via `tempfile.NamedTemporaryFile(dir=contract_path.parent,\ - \ prefix=f\".{contract_path.stem}_\", suffix=\".json.tmp\", delete=False)` so\ - \ two concurrent processes cannot clobber each other's pre-replace temp.\n-\ - \ **run_pipeline.py:326** \u2014 `try: generator.close() except Exception: pass`\ - \ silently swallows any exception from the orchestrator's `finally` block (including\ - \ a future thread-join hang). Consider `traceback.print_exc(file=sys.stderr)`\ - \ inside the except \u2014 keeps the defensive swallow but leaves a diagnostic\ - \ trail when `_InProcessOrchestrator` cleanup misbehaves.\n- **_agent_tool_fake.py:298**\ - \ \u2014 `subprocess.TimeoutExpired` propagates unhandled from `pre_tool_use_callback`;\ - \ the surrounding `dispatch()` returns no `DispatchResult` on timeout. For test-infrastructure\ - \ uniformity, consider catching it and returning `{\"decision\": \"block\",\ - \ \"reason\": \"_agent_tool_fake child timed out after Xs\"}` \u2014 mirrors\ - \ the other structured-failure paths at `:306-314` and `:318-325`.\n" - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:01:44Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a3a0b153-6f9a-41 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:01:49Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 337cc6c7-04cb-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:49.332411+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:01:49Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2418b70d-7baa-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:49.925403+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:01:50Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: af1855e2-8568-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:01:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: bebd5620-9972-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:01:59Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b56978de-afab-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:02:49Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 967fae6c-e98c-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:49.332411+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:02:50Z] reviewer_security → coder (CONSENSUS_ACK): ACK from reviewer_security for coder - -Security-lens review of slice-1 coder proposal (3 files, +1070 lines). - -**Scope reviewed:** trust-boundary changes (the new bridge driver under the egg-sdlc skill bin and the role-rubric loader expansion), cross-file allowlist mismatches between the rubric loader's role allowlist and the disk layout, the test-only nested-Agent-tool fake's subprocess shape and import guard, and information-disclosure surfaces in the persisted error envelope. - -**Verified clean:** - -1. **Rubric loader path safety (`orchestrator/substrate/__init__.py:280-365`).** The role→file mapping `_RUBRIC_LANDED_ROLES = {"refiner", "reviewer_refine", "reviewer_agent_design"}` is an exact-string allowlist consulted BEFORE the disk read at line 365, so an attacker-supplied `role` containing `../` cannot reach `rubric_path.read_text()` — the `if role_name not in _RUBRIC_LANDED_ROLES` fence raises `ValueError` first. The `_ROLE_RUBRIC_SLICES` mapping also uses exact match. No path-traversal reach. - -2. **Nested-Agent-tool fake (`integration_tests/regression/_agent_tool_fake.py`).** Import guard at lines 84-96 rejects production callers via `__name__` prefix check. The child subprocess invocation at line 293 uses list-form `subprocess.run([py, "-m", module_name, blob_path], ...)` with a hardcoded `module_name` — no shell injection, no agent-controlled argv beyond the JSON blob path (a `tempfile.mkstemp` output unlinked in `finally`). The child JSON output is parsed with `json.JSONDecodeError` handled defensively (lines 315-325) and `isinstance(verdict, dict)` enforced (line 326). The fake delegates to `hook_entry.decide(...)` only — it queries the verdict, it never executes the write — so the simulated nested dispatch does not bypass any real authorization check. - -3. **Cross-file `pending_hitl` envelope schema.** Driver (`run_pipeline.py:94`) and fake (`_agent_tool_fake.py:115-126`) both pin `PENDING_HITL_SCHEMA_VERSION = 1`; the driver's `_coerce_envelope` rejects newer-version envelopes with `ValueError` (lines 211-217) so a slice-3 daemon writing v2 can't silently downgrade through this driver. Schema-version invariant holds across the changed files. - -4. **No uncommitted-artifact / Dockerfile-symlink mismatches.** Diff only touches the three Python files and state artifacts; no new symlinks, COPY targets, or entry points were introduced. - -5. **No new gateway routes or credential shims.** `sandbox/scripts/` is untouched; the bridge driver runs in the outer-session trust context (per its docstring), not as an egress wrapper. The role-rubric loader does not embed credentials. - -### Non-blocking - -- **`run_pipeline.py:413-419, 130, 146-147` — Defense-in-depth: validate `pipeline_id` and `--state-root` before path construction.** Both flow unvalidated from `argv` into `contract_path = state_root / "contracts" / f"{pipeline_id}.json"`, which is then read (`Path.read_text()` in `_read_contract`) and written (`tmp.write_text()` + `os.replace()` in `_write_contract`). The current threat model puts the driver in the outer-session trust context where this is benign, but a prompt-injection vector (e.g., an attacker-controlled GitHub issue body steering Claude Code in the outer session to invoke the driver with `pipeline_id="../foo"`) would let a single malformed argv land a JSON write outside `.egg-state/contracts/`. The orchestrator's `_InProcessOrchestrator._write_pending_decision` is paired with the same `os.replace` shape but presumably sanitises pipeline_id upstream — the driver should mirror that defence locally. Suggested fix: validate `pipeline_id` with `re.fullmatch(r"[a-zA-Z0-9_-]{1,64}", pipeline_id)` and assert `contract_path.resolve().is_relative_to(state_root.resolve())` before any read/write; move the validation *before* `_ensure_contracts_dir()` so the driver cannot `mkdir(parents=True)` into an out-of-bound path. - -- **`run_pipeline.py:487-497` — Persisted traceback is committed to git as part of `.egg-state/contracts/.json`.** `traceback.format_exc(limit=8)` may include absolute filesystem paths, library versions, and other internal state that the contract file then carries into version control. Minor information disclosure to anyone who reads the repo history. Consider stripping absolute paths (replace `repo_root` with ``) or capturing tracebacks only into a non-committed sidecar log (e.g., `.egg-state/logs/-driver.log`) and persisting only the exception type + message in the contract. - -- **`_agent_tool_fake.py:114-126` — Hardcoded `PENDING_HITL_SCHEMA_VERSION = 1` fallback masks a real schema bump.** If the driver later moves to v2 but the fake's path-walk import fails (sys.path race, missing skill bin), the fallback at line 126 silently pins v1 and tests may pass against an incompatible schema. Given the docstring's explicit "STABLE contract — slice-3 daemon inherits this shape" framing, the fallback should raise instead — better to fail loudly than to silently disagree. - -- **`_agent_tool_fake.py:266` — Child subprocess inherits the full parent env via `{**os.environ, "EGG_AGENT_ROLE": role}`.** For test infrastructure this is acceptable, but it propagates `EGG_SESSION_TOKEN` / `GITHUB_TOKEN` / etc. to a subprocess that only needs `EGG_AGENT_ROLE` + `PYTHONPATH` + `EGG_REPO_ROOT`. Consider an opt-in minimal-env shape so future security-sensitive tests don't accidentally leak credentials into the fake-subagent's child. - -````yaml -id: e38d7773-4e34-4b -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - reason: "Security-lens review of slice-1 coder proposal (3 files, +1070 lines).\n\ - \n**Scope reviewed:** trust-boundary changes (the new bridge driver under the\ - \ egg-sdlc skill bin and the role-rubric loader expansion), cross-file allowlist\ - \ mismatches between the rubric loader's role allowlist and the disk layout,\ - \ the test-only nested-Agent-tool fake's subprocess shape and import guard,\ - \ and information-disclosure surfaces in the persisted error envelope.\n\n**Verified\ - \ clean:**\n\n1. **Rubric loader path safety (`orchestrator/substrate/__init__.py:280-365`).**\ - \ The role\u2192file mapping `_RUBRIC_LANDED_ROLES = {\"refiner\", \"reviewer_refine\"\ - , \"reviewer_agent_design\"}` is an exact-string allowlist consulted BEFORE\ - \ the disk read at line 365, so an attacker-supplied `role` containing `../`\ - \ cannot reach `rubric_path.read_text()` \u2014 the `if role_name not in _RUBRIC_LANDED_ROLES`\ - \ fence raises `ValueError` first. The `_ROLE_RUBRIC_SLICES` mapping also uses\ - \ exact match. No path-traversal reach.\n\n2. **Nested-Agent-tool fake (`integration_tests/regression/_agent_tool_fake.py`).**\ - \ Import guard at lines 84-96 rejects production callers via `__name__` prefix\ - \ check. The child subprocess invocation at line 293 uses list-form `subprocess.run([py,\ - \ \"-m\", module_name, blob_path], ...)` with a hardcoded `module_name` \u2014\ - \ no shell injection, no agent-controlled argv beyond the JSON blob path (a\ - \ `tempfile.mkstemp` output unlinked in `finally`). The child JSON output is\ - \ parsed with `json.JSONDecodeError` handled defensively (lines 315-325) and\ - \ `isinstance(verdict, dict)` enforced (line 326). The fake delegates to `hook_entry.decide(...)`\ - \ only \u2014 it queries the verdict, it never executes the write \u2014 so\ - \ the simulated nested dispatch does not bypass any real authorization check.\n\ - \n3. **Cross-file `pending_hitl` envelope schema.** Driver (`run_pipeline.py:94`)\ - \ and fake (`_agent_tool_fake.py:115-126`) both pin `PENDING_HITL_SCHEMA_VERSION\ - \ = 1`; the driver's `_coerce_envelope` rejects newer-version envelopes with\ - \ `ValueError` (lines 211-217) so a slice-3 daemon writing v2 can't silently\ - \ downgrade through this driver. Schema-version invariant holds across the changed\ - \ files.\n\n4. **No uncommitted-artifact / Dockerfile-symlink mismatches.**\ - \ Diff only touches the three Python files and state artifacts; no new symlinks,\ - \ COPY targets, or entry points were introduced.\n\n5. **No new gateway routes\ - \ or credential shims.** `sandbox/scripts/` is untouched; the bridge driver\ - \ runs in the outer-session trust context (per its docstring), not as an egress\ - \ wrapper. The role-rubric loader does not embed credentials.\n\n### Non-blocking\n\ - \n- **`run_pipeline.py:413-419, 130, 146-147` \u2014 Defense-in-depth: validate\ - \ `pipeline_id` and `--state-root` before path construction.** Both flow unvalidated\ - \ from `argv` into `contract_path = state_root / \"contracts\" / f\"{pipeline_id}.json\"\ - `, which is then read (`Path.read_text()` in `_read_contract`) and written (`tmp.write_text()`\ - \ + `os.replace()` in `_write_contract`). The current threat model puts the\ - \ driver in the outer-session trust context where this is benign, but a prompt-injection\ - \ vector (e.g., an attacker-controlled GitHub issue body steering Claude Code\ - \ in the outer session to invoke the driver with `pipeline_id=\"../foo\"`) would\ - \ let a single malformed argv land a JSON write outside `.egg-state/contracts/`.\ - \ The orchestrator's `_InProcessOrchestrator._write_pending_decision` is paired\ - \ with the same `os.replace` shape but presumably sanitises pipeline_id upstream\ - \ \u2014 the driver should mirror that defence locally. Suggested fix: validate\ - \ `pipeline_id` with `re.fullmatch(r\"[a-zA-Z0-9_-]{1,64}\", pipeline_id)` and\ - \ assert `contract_path.resolve().is_relative_to(state_root.resolve())` before\ - \ any read/write; move the validation *before* `_ensure_contracts_dir()` so\ - \ the driver cannot `mkdir(parents=True)` into an out-of-bound path.\n\n- **`run_pipeline.py:487-497`\ - \ \u2014 Persisted traceback is committed to git as part of `.egg-state/contracts/.json`.**\ - \ `traceback.format_exc(limit=8)` may include absolute filesystem paths, library\ - \ versions, and other internal state that the contract file then carries into\ - \ version control. Minor information disclosure to anyone who reads the repo\ - \ history. Consider stripping absolute paths (replace `repo_root` with ``)\ - \ or capturing tracebacks only into a non-committed sidecar log (e.g., `.egg-state/logs/-driver.log`)\ - \ and persisting only the exception type + message in the contract.\n\n- **`_agent_tool_fake.py:114-126`\ - \ \u2014 Hardcoded `PENDING_HITL_SCHEMA_VERSION = 1` fallback masks a real schema\ - \ bump.** If the driver later moves to v2 but the fake's path-walk import fails\ - \ (sys.path race, missing skill bin), the fallback at line 126 silently pins\ - \ v1 and tests may pass against an incompatible schema. Given the docstring's\ - \ explicit \"STABLE contract \u2014 slice-3 daemon inherits this shape\" framing,\ - \ the fallback should raise instead \u2014 better to fail loudly than to silently\ - \ disagree.\n\n- **`_agent_tool_fake.py:266` \u2014 Child subprocess inherits\ - \ the full parent env via `{**os.environ, \"EGG_AGENT_ROLE\": role}`.** For\ - \ test infrastructure this is acceptable, but it propagates `EGG_SESSION_TOKEN`\ - \ / `GITHUB_TOKEN` / etc. to a subprocess that only needs `EGG_AGENT_ROLE` +\ - \ `PYTHONPATH` + `EGG_REPO_ROOT`. Consider an opt-in minimal-env shape so future\ - \ security-sensitive tests don't accidentally leak credentials into the fake-subagent's\ - \ child." - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:02:51Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 1075b5ed-a477-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:02:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 2bd71d30-ab0c-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:02:55Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 694d133e-b4f6-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:02:55.753143+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:02:55Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c731e931-13d7-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:02:57Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 80021494-3175-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:02:56.998153+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:02:59Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: eea41f70-1360-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:03:03Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f4c92493-f937-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:03:49Z] reviewer_contract → coder (CONSENSUS_ACK): ACK from reviewer_contract for coder - -Contract verification — slice-1 coder proposal v1 @ fddeb82bf. - -Reviewed three coder-owned files end-to-end against the slice-1 contract's per-task acceptance criteria. All three tasks substantively meet their AC. Files changed match the per-task `files_affected` exactly; no scope creep into documenter/tester territory. - -### TASK-1-1 (`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py`) — VERIFIED -AC quote: "`bin/run_pipeline.py` exists; it imports `run_pipeline_in_process` from `orchestrator.substrate.in_process` (line 84); on invocation it round-trips a single `HITLDecision` through `.egg-state/contracts/.json` and exits with status 0 when the generator yields, status 1 on internal error. The `SKILL.md` 'Usage' section is updated to call this driver in a loop. The `pending_hitl` envelope schema is documented as a stable contract (top-of-file comment listing the contract fields)." - -Evidence: -- File exists, 530 lines, shebang + executable bit (mode 100755). ✓ -- Imports `run_pipeline_in_process` from `orchestrator.substrate.in_process` at line 248 (inside `_import_runner` — lazy import to surface ImportError as a structured envelope error per the docstring). ✓ -- `.egg-state/contracts/.json` round-trip implemented via `_read_contract`/`_persist_envelope` (lines 118–147) using temp+`os.replace` for atomicity, mirroring `_InProcessOrchestrator._write_pending_decision` per the comment. ✓ -- Exit-code contract: smoke-tested locally — empty `pipeline_id` → exit 1, ImportError (no orchestrator on PYTHONPATH) → exit 1, `--daemon` short-circuit → exit 1, generator-yield path → exit 0 (returns 0 at line 525). ✓ -- Stable-contract schema documented at lines 20–46 with explicit "STABLE contract — slice-3 daemon inherits this shape; do NOT change field names/types without bumping version" callout. Field set: `version, pipeline_id, timestamp, decision, answer, status, result, error, answer_log` — superset of the AC-named `decision, answer, version, pipeline_id, timestamp`. Extras (`status`, `answer_log`, `result`, `error`) are mechanics needed for cross-process generator-state replay and structured error reporting; they are documented as part of the stable contract so the daemon variant cannot accidentally diverge. ✓ -- `PENDING_HITL_SCHEMA_VERSION = 1` constant (line 94) provides the bump-point the comment promises. `_coerce_envelope` (line 200) rejects future-version envelopes — smoke-verified: passing `{"version": 99}` raises ValueError with the documented message. ✓ -- SKILL.md "Usage" section update is the documenter's TASK-1-2 deliverable, not coder-owned. The merged slice branch already carries the documenter's commit `473b109cb` which rewrites the "Usage" section to call `bin/run_pipeline.py` in a loop and documents the `pending_hitl` envelope (verified by inspection of the diff against `origin/main`). The cross-cutting AC will be re-verified when documenter proposes; coder's deliverable is complete. - -### TASK-1-9 (`integration_tests/regression/_agent_tool_fake.py`) — VERIFIED -AC quote: "`_agent_tool_fake.py` exists; exposes a `dispatch(parent_role, child_role, write_target)` helper; the helper invokes `hook_entry.decide(...)` via the simulated child's `pre_tool_use_callback` and returns the hook verdict. Documented in the module docstring as test-only; protected with a top-of-file `if not __name__.startswith(\"integration_tests\")` import guard so it can't be silently imported by production code." - -Evidence: -- File exists, 451 lines. ✓ -- `dispatch(parent_role, child_role, write_target, ...)` helper defined at line 336 with the signature the AC names; trailing kwargs (`tool_name`, `extra_env`, `repo_root`) are additive and reasonable. ✓ -- Helper invokes `hook_entry.decide(...)` via `pre_tool_use_callback` → spawns `python3 -m … _agent_tool_fake ` → `_child_main` calls `from orchestrator.substrate.claude_code.hook_entry import decide` and `verdict = decide(stdin_blob)` (line 211). The dispatch path is correct: parent → subprocess(child_role) → hook_entry.decide → verdict. ✓ -- Returns `DispatchResult` carrying `decision` (the hook verdict dict) plus `denied`/`deny_reason`/`child_pid`/`child_exit_code`/`stderr` for test observability. The raw hook verdict is preserved on `.decision`. ✓ -- Module docstring (lines 1–62) explicitly states "**This is TEST INFRASTRUCTURE ONLY.**" and explains why (cq-3: production stays on harness re-host). ✓ -- Top-of-file import guard at lines 84–96, raises `ImportError` if `__name__` does not start with any of `(integration_tests, _agent_tool_fake, __main__)`. Smoke-verified by exec-ing the module body with `__name__ = "orchestrator.fake_user"` — the guard fires and rejects with the expected diagnostic. ✓ - -### TASK-1-6 (`orchestrator/substrate/__init__.py`) — VERIFIED -AC quote: "`_load_egg_sdlc_role_rubric(REVIEWER_REFINE)` returns the markdown body of `reviewer_refine.md`; `_load_egg_sdlc_role_rubric(REVIEWER_AGENT_DESIGN)` returns the body of `reviewer_agent_design.md`; `_load_egg_sdlc_role_rubric(ARCHITECT)` still raises `ValueError` with the 'follow-up issue per cq-11' hint updated to 'follow-up slice 2'." - -Evidence (smoke-tested directly in this worktree, post-merge of slice-1): -- `_load_egg_sdlc_role_rubric('reviewer_refine')` → returns 6628-char markdown body starting with `---\n# Role data file. …` ✓ -- `_load_egg_sdlc_role_rubric('reviewer_agent_design')` → returns 6558-char markdown body, same shape. ✓ -- `_load_egg_sdlc_role_rubric('architect')` → raises `ValueError("egg-sdlc role rubric for role='architect' is deferred to follow-up slice-2 of issue #2717's rollout. See docs/architecture/claude-code-substrate.md for the slice DAG.")` — the prior cq-11 hint is replaced with the slice-2 pointer the AC requires. ✓ (Pre-change wording at 802f77d9e:264-267 said "Walking-skeleton spike (#2623) only ships the refiner rubric; other roles are deferred to the follow-up issue per cq-11"; new wording correctly cites slice-2 for architect.) -- Regression checks: - - `_load_egg_sdlc_role_rubric('refiner')` still returns the original 8087-char rubric (no regression on the #2715 walking-skeleton refiner support). - - `_load_egg_sdlc_role_rubric('coder')` raises ValueError citing slice-3 (consistent with the rollout DAG). - - `_load_egg_sdlc_role_rubric('overseer')` (not in `_ROLE_RUBRIC_SLICES`) raises a distinct "not part of the #2717 rollout's rubric set; file a follow-up issue" error — good differentiation between "deferred to a known slice" and "indefinitely deferred" (overseer/inspector/autofixer/conflict_resolver per the comment at lines 235–237). -- The two new data structures (`_ROLE_RUBRIC_SLICES` mapping, `_RUBRIC_LANDED_ROLES` frozenset) cleanly separate the rubric-slice DAG from the on-disk-availability gate, so later slices extend declaratively without touching the loader body. ✓ -- The TASK-1-6 description's "sequence TASK-1-4 → TASK-1-6 within the slice" risk is also mitigated: when a role is in `_RUBRIC_LANDED_ROLES` but the .md file is missing, the loader raises a distinct "rubric missing on disk … sequence the documenter's rubric task before the loader update" error (lines 352–364). The documenter has already landed `reviewer_refine.md` and `reviewer_agent_design.md` in the merged slice branch (commit `473b109cb`) so this fallback is not reached today, but the safety net exists for re-runs. - -### Phase consistency -- All three coder tasks are still in `status: pending` in the contract; the producer is expected to call `mcp__task__complete` on `task-1-1`, `task-1-6`, `task-1-9` (linking commit `fddeb82bf`) after CONSENSUS_CONFIRMED lands. Not a blocker for ACK — task-complete is normally done in the converge step. -- No orphaned code: every file in the coder commit maps to exactly one task's `files_affected`. - -### Non-blocking -- **`_agent_tool_fake.py` import guard breadth**: AC literal text was `if not __name__.startswith("integration_tests")`. Implementation uses a 3-prefix allow-list `(integration_tests, _agent_tool_fake, __main__)` because the `python3 -m integration_tests.regression._agent_tool_fake ` invocation done by the parent helper lands as `__name__ == "__main__"` in the child, and conftest-driven sys.path injection can land it as the bare `_agent_tool_fake`. Both additions are necessary for the dispatch model to function; the guard's intent (block production imports) is preserved. Worth a brief code comment cross-referencing the parent helper's `module_name` selection so future readers don't tighten the guard and break the child spawn — but no behavior change needed. -- **`follow-up slice 2` wording**: AC said "follow-up slice 2" (space), implementation produces "follow-up slice-2" (hyphen, matching the slice-ID format `slice-N` used throughout `_ROLE_RUBRIC_SLICES` and the rest of the rollout). Substantively compliant; the hyphenated form is the consistent convention. -- **`run_pipeline.py` `--repo` default flow**: line 460 falls back through `args.repo → EGG_REPO → EGG_PIPELINE_REPO`. The CLI help text only mentions `EGG_REPO`. Worth a one-line help-string note documenting the `EGG_PIPELINE_REPO` fallback so operators don't waste time hunting for the env var name. -- **Schema doc completeness**: the top-of-file `pending_hitl` schema comment names `decision/answer/status/result/error` fields but lists `answer_log` separately in the implementation; consider folding `answer_log` into the same table-of-fields comment block so the "stable contract" inventory is self-contained without forcing the daemon-author to read the body. Pure documentation polish. - -````yaml -id: 2df03d34-2703-45 -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - reason: "Contract verification \u2014 slice-1 coder proposal v1 @ fddeb82bf.\n\ - \nReviewed three coder-owned files end-to-end against the slice-1 contract's\ - \ per-task acceptance criteria. All three tasks substantively meet their AC.\ - \ Files changed match the per-task `files_affected` exactly; no scope creep\ - \ into documenter/tester territory.\n\n### TASK-1-1 (`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py`)\ - \ \u2014 VERIFIED\nAC quote: \"`bin/run_pipeline.py` exists; it imports `run_pipeline_in_process`\ - \ from `orchestrator.substrate.in_process` (line 84); on invocation it round-trips\ - \ a single `HITLDecision` through `.egg-state/contracts/.json` and exits\ - \ with status 0 when the generator yields, status 1 on internal error. The `SKILL.md`\ - \ 'Usage' section is updated to call this driver in a loop. The `pending_hitl`\ - \ envelope schema is documented as a stable contract (top-of-file comment listing\ - \ the contract fields).\"\n\nEvidence:\n- File exists, 530 lines, shebang +\ - \ executable bit (mode 100755). \u2713\n- Imports `run_pipeline_in_process`\ - \ from `orchestrator.substrate.in_process` at line 248 (inside `_import_runner`\ - \ \u2014 lazy import to surface ImportError as a structured envelope error per\ - \ the docstring). \u2713\n- `.egg-state/contracts/.json` round-trip implemented\ - \ via `_read_contract`/`_persist_envelope` (lines 118\u2013147) using temp+`os.replace`\ - \ for atomicity, mirroring `_InProcessOrchestrator._write_pending_decision`\ - \ per the comment. \u2713\n- Exit-code contract: smoke-tested locally \u2014\ - \ empty `pipeline_id` \u2192 exit 1, ImportError (no orchestrator on PYTHONPATH)\ - \ \u2192 exit 1, `--daemon` short-circuit \u2192 exit 1, generator-yield path\ - \ \u2192 exit 0 (returns 0 at line 525). \u2713\n- Stable-contract schema documented\ - \ at lines 20\u201346 with explicit \"STABLE contract \u2014 slice-3 daemon\ - \ inherits this shape; do NOT change field names/types without bumping version\"\ - \ callout. Field set: `version, pipeline_id, timestamp, decision, answer, status,\ - \ result, error, answer_log` \u2014 superset of the AC-named `decision, answer,\ - \ version, pipeline_id, timestamp`. Extras (`status`, `answer_log`, `result`,\ - \ `error`) are mechanics needed for cross-process generator-state replay and\ - \ structured error reporting; they are documented as part of the stable contract\ - \ so the daemon variant cannot accidentally diverge. \u2713\n- `PENDING_HITL_SCHEMA_VERSION\ - \ = 1` constant (line 94) provides the bump-point the comment promises. `_coerce_envelope`\ - \ (line 200) rejects future-version envelopes \u2014 smoke-verified: passing\ - \ `{\"version\": 99}` raises ValueError with the documented message. \u2713\n\ - - SKILL.md \"Usage\" section update is the documenter's TASK-1-2 deliverable,\ - \ not coder-owned. The merged slice branch already carries the documenter's\ - \ commit `473b109cb` which rewrites the \"Usage\" section to call `bin/run_pipeline.py`\ - \ in a loop and documents the `pending_hitl` envelope (verified by inspection\ - \ of the diff against `origin/main`). The cross-cutting AC will be re-verified\ - \ when documenter proposes; coder's deliverable is complete.\n\n### TASK-1-9\ - \ (`integration_tests/regression/_agent_tool_fake.py`) \u2014 VERIFIED\nAC quote:\ - \ \"`_agent_tool_fake.py` exists; exposes a `dispatch(parent_role, child_role,\ - \ write_target)` helper; the helper invokes `hook_entry.decide(...)` via the\ - \ simulated child's `pre_tool_use_callback` and returns the hook verdict. Documented\ - \ in the module docstring as test-only; protected with a top-of-file `if not\ - \ __name__.startswith(\\\"integration_tests\\\")` import guard so it can't be\ - \ silently imported by production code.\"\n\nEvidence:\n- File exists, 451 lines.\ - \ \u2713\n- `dispatch(parent_role, child_role, write_target, ...)` helper defined\ - \ at line 336 with the signature the AC names; trailing kwargs (`tool_name`,\ - \ `extra_env`, `repo_root`) are additive and reasonable. \u2713\n- Helper invokes\ - \ `hook_entry.decide(...)` via `pre_tool_use_callback` \u2192 spawns `python3\ - \ -m \u2026 _agent_tool_fake ` \u2192 `_child_main` calls `from orchestrator.substrate.claude_code.hook_entry\ - \ import decide` and `verdict = decide(stdin_blob)` (line 211). The dispatch\ - \ path is correct: parent \u2192 subprocess(child_role) \u2192 hook_entry.decide\ - \ \u2192 verdict. \u2713\n- Returns `DispatchResult` carrying `decision` (the\ - \ hook verdict dict) plus `denied`/`deny_reason`/`child_pid`/`child_exit_code`/`stderr`\ - \ for test observability. The raw hook verdict is preserved on `.decision`.\ - \ \u2713\n- Module docstring (lines 1\u201362) explicitly states \"**This is\ - \ TEST INFRASTRUCTURE ONLY.**\" and explains why (cq-3: production stays on\ - \ harness re-host). \u2713\n- Top-of-file import guard at lines 84\u201396,\ - \ raises `ImportError` if `__name__` does not start with any of `(integration_tests,\ - \ _agent_tool_fake, __main__)`. Smoke-verified by exec-ing the module body with\ - \ `__name__ = \"orchestrator.fake_user\"` \u2014 the guard fires and rejects\ - \ with the expected diagnostic. \u2713\n\n### TASK-1-6 (`orchestrator/substrate/__init__.py`)\ - \ \u2014 VERIFIED\nAC quote: \"`_load_egg_sdlc_role_rubric(REVIEWER_REFINE)`\ - \ returns the markdown body of `reviewer_refine.md`; `_load_egg_sdlc_role_rubric(REVIEWER_AGENT_DESIGN)`\ - \ returns the body of `reviewer_agent_design.md`; `_load_egg_sdlc_role_rubric(ARCHITECT)`\ - \ still raises `ValueError` with the 'follow-up issue per cq-11' hint updated\ - \ to 'follow-up slice 2'.\"\n\nEvidence (smoke-tested directly in this worktree,\ - \ post-merge of slice-1):\n- `_load_egg_sdlc_role_rubric('reviewer_refine')`\ - \ \u2192 returns 6628-char markdown body starting with `---\\n# Role data file.\ - \ \u2026` \u2713\n- `_load_egg_sdlc_role_rubric('reviewer_agent_design')` \u2192\ - \ returns 6558-char markdown body, same shape. \u2713\n- `_load_egg_sdlc_role_rubric('architect')`\ - \ \u2192 raises `ValueError(\"egg-sdlc role rubric for role='architect' is deferred\ - \ to follow-up slice-2 of issue #2717's rollout. See docs/architecture/claude-code-substrate.md\ - \ for the slice DAG.\")` \u2014 the prior cq-11 hint is replaced with the slice-2\ - \ pointer the AC requires. \u2713 (Pre-change wording at 802f77d9e:264-267 said\ - \ \"Walking-skeleton spike (#2623) only ships the refiner rubric; other roles\ - \ are deferred to the follow-up issue per cq-11\"; new wording correctly cites\ - \ slice-2 for architect.)\n- Regression checks:\n - `_load_egg_sdlc_role_rubric('refiner')`\ - \ still returns the original 8087-char rubric (no regression on the #2715 walking-skeleton\ - \ refiner support).\n - `_load_egg_sdlc_role_rubric('coder')` raises ValueError\ - \ citing slice-3 (consistent with the rollout DAG).\n - `_load_egg_sdlc_role_rubric('overseer')`\ - \ (not in `_ROLE_RUBRIC_SLICES`) raises a distinct \"not part of the #2717 rollout's\ - \ rubric set; file a follow-up issue\" error \u2014 good differentiation between\ - \ \"deferred to a known slice\" and \"indefinitely deferred\" (overseer/inspector/autofixer/conflict_resolver\ - \ per the comment at lines 235\u2013237).\n- The two new data structures (`_ROLE_RUBRIC_SLICES`\ - \ mapping, `_RUBRIC_LANDED_ROLES` frozenset) cleanly separate the rubric-slice\ - \ DAG from the on-disk-availability gate, so later slices extend declaratively\ - \ without touching the loader body. \u2713\n- The TASK-1-6 description's \"\ - sequence TASK-1-4 \u2192 TASK-1-6 within the slice\" risk is also mitigated:\ - \ when a role is in `_RUBRIC_LANDED_ROLES` but the .md file is missing, the\ - \ loader raises a distinct \"rubric missing on disk \u2026 sequence the documenter's\ - \ rubric task before the loader update\" error (lines 352\u2013364). The documenter\ - \ has already landed `reviewer_refine.md` and `reviewer_agent_design.md` in\ - \ the merged slice branch (commit `473b109cb`) so this fallback is not reached\ - \ today, but the safety net exists for re-runs.\n\n### Phase consistency\n-\ - \ All three coder tasks are still in `status: pending` in the contract; the\ - \ producer is expected to call `mcp__task__complete` on `task-1-1`, `task-1-6`,\ - \ `task-1-9` (linking commit `fddeb82bf`) after CONSENSUS_CONFIRMED lands. Not\ - \ a blocker for ACK \u2014 task-complete is normally done in the converge step.\n\ - - No orphaned code: every file in the coder commit maps to exactly one task's\ - \ `files_affected`.\n\n### Non-blocking\n- **`_agent_tool_fake.py` import guard\ - \ breadth**: AC literal text was `if not __name__.startswith(\"integration_tests\"\ - )`. Implementation uses a 3-prefix allow-list `(integration_tests, _agent_tool_fake,\ - \ __main__)` because the `python3 -m integration_tests.regression._agent_tool_fake\ - \ ` invocation done by the parent helper lands as `__name__ == \"__main__\"\ - ` in the child, and conftest-driven sys.path injection can land it as the bare\ - \ `_agent_tool_fake`. Both additions are necessary for the dispatch model to\ - \ function; the guard's intent (block production imports) is preserved. Worth\ - \ a brief code comment cross-referencing the parent helper's `module_name` selection\ - \ so future readers don't tighten the guard and break the child spawn \u2014\ - \ but no behavior change needed.\n- **`follow-up slice 2` wording**: AC said\ - \ \"follow-up slice 2\" (space), implementation produces \"follow-up slice-2\"\ - \ (hyphen, matching the slice-ID format `slice-N` used throughout `_ROLE_RUBRIC_SLICES`\ - \ and the rest of the rollout). Substantively compliant; the hyphenated form\ - \ is the consistent convention.\n- **`run_pipeline.py` `--repo` default flow**:\ - \ line 460 falls back through `args.repo \u2192 EGG_REPO \u2192 EGG_PIPELINE_REPO`.\ - \ The CLI help text only mentions `EGG_REPO`. Worth a one-line help-string note\ - \ documenting the `EGG_PIPELINE_REPO` fallback so operators don't waste time\ - \ hunting for the env var name.\n- **Schema doc completeness**: the top-of-file\ - \ `pending_hitl` schema comment names `decision/answer/status/result/error`\ - \ fields but lists `answer_log` separately in the implementation; consider folding\ - \ `answer_log` into the same table-of-fields comment block so the \"stable contract\"\ - \ inventory is self-contained without forcing the daemon-author to read the\ - \ body. Pure documentation polish." - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:03:49Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 1c16fef1-e278-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:03:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 02b785cc-1de5-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:03:55Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 74c036fb-4733-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:55.730536+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:03:59Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: fd8f715e-2ea7-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:04:02Z] reviewer_contract (CONSENSUS_CONFIRMED): Confirmed by reviewer_contract (pending_acks) - -Agent reviewer_contract cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus. - -````yaml -id: 97375d9b-4853-4d -phase: implement -metadata: - pending_acks: true - slice_id: slice-1 -```` - -### [2026-05-19T19:04:03Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 4c28fed6-c4ec-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:04:09Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c0f04524-4c77-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:04:09.316695+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:04:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 324cbe8a-13e0-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:04:55Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 2cdf566c-36d1-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:55.730536+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:04:59Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 43695226-5309-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:05:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: ec5d7bad-7856-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:05:07Z] tester → coder (CONSENSUS_ACK): ACK from tester for coder - - -Reviewed the coder's three artifacts (run_pipeline.py, _agent_tool_fake.py, orchestrator/substrate/__init__.py) plus drove all three acceptance criteria end-to-end via the tester-side tests I committed alongside this ACK. - -### What I verified - -**TASK-1-1 (`bin/run_pipeline.py`) — bridge driver:** -- Round-trip works end-to-end. My `test_bridge_flattened_round_trip` invokes the driver twice via `runpy` in a fresh subprocess each call. Stage A captures `pending_hitl.decision.question == "Confirm the refiner will run against this repo + issue?"` (the preflight); after I write `answer="approve" + status="answered"` to the contract, Stage B replays the answer through the `answer_log` mechanism and lands on the refine-gate decision (`decision_type=phase_gate`, distinct question). Exits 0 on each yield as the AC requires. -- The `pending_hitl` envelope schema is documented as a stable contract at the top of `run_pipeline.py` (lines 20-46) — fields `version`, `pipeline_id`, `timestamp`, `decision`, `answer`, `status`, `result`, `error`, `answer_log`. The slice-3 daemon variant (TASK-3-2) inheriting this shape is correctly called out for R17 mitigation. -- `_advance_generator` replay model is sound for a deterministic generator: closes the generator in `finally` (GeneratorExit discipline), atomic temp+os.replace contract writes. The `--daemon` flag short-circuits with a structured error pointing at slice-3, which is the right deferral. -- Idempotency invariant holds: my `test_driver_is_idempotent_when_answer_unchanged` invokes the driver twice without writing an answer in between, and the second invocation correctly yields the SAME preflight decision (no silent advance of the state machine without operator input). - -**TASK-1-9 (`integration_tests/regression/_agent_tool_fake.py`) — nested-dispatch fake:** -- Import-guard works: the module rejects imports from outside `integration_tests`/`_agent_tool_fake`/`__main__` prefixes (lines 84-96). -- `dispatch(parent_role, child_role, write_target)` end-to-end correct for R2: spawns the child subprocess with `EGG_AGENT_ROLE=child_role`, the child invokes `hook_entry.decide(...)` and writes JSON to stdout, parent JSON-decodes the verdict. My `test_pretooluse_hook_denies_nested_child_write` confirms parent=architect + child=tester writing `orchestrator/foo.py` resolves to `decision=block` with a `tester`-naming reason — proving the hook resolves the child's role correctly, not the parent's. -- Cross-role probe (parent=coder, child=tester writing orchestrator/*) also denies with a tester-scoped reason — the negative-control case proves there is no parent-side fallback masking the child resolution. -- No env leakage: `EGG_AGENT_ROLE` is passed via `subprocess.run(env=...)` rather than `os.environ[...]=`; my leak-guard test confirms the parent process's `EGG_AGENT_ROLE` stays unset after dispatch. -- `DispatchResult` dataclass is a reasonable wrapper (vs. raw dict) — the `.decision` field IS the raw hook verdict, so the AC ("returns the hook verdict") is satisfied through that attribute. - -**TASK-1-6 (`orchestrator/substrate/__init__.py` loader update):** -- `_ROLE_RUBRIC_SLICES` mapping + `_RUBRIC_LANDED_ROLES` frozenset is a clean declarative extension; future slices can extend the set without rewriting the loader. -- `REFINER` regression case still passes (10/10 cases in my `test_rubric_loader.py` are green after the documenter's task-1-4 rubric .md files landed). -- `REVIEWER_REFINE` / `REVIEWER_AGENT_DESIGN` load successfully — verified body contains the role name. -- `ARCHITECT` raises `ValueError` with `follow-up slice-2 of issue #2717's rollout` diagnostic. Note: the AC literal wording was "follow-up slice 2" (space); coder shipped "follow-up slice-2" (hyphen). The intent is satisfied — my test accepts either form. Non-blocking. -- `REVIEWER_PLAN` / `REVIEWER_CODE` / `TASK_PLANNER` still raise as expected (the structured-error fence preserves the slice-2/3 contract). -- Path-traversal defence: `_load("../../../etc/passwd")` resolves to a `ValueError` ("not part of the #2717 rollout's rubric set") — defence-in-depth holds. - -### Non-blocking - -- **`run_pipeline.py:343-352 `_is_aborted_status`**: The substring set `{"abort", "stop", "cancel"}` does not match what `orchestrator.substrate.in_process._answer_is_abort` actually treats as an abort. The two paths diverge — if the orchestrator widens its abort vocabulary, the driver's status-emission will silently mis-classify. Consider importing the orchestrator's helper directly so they cannot drift. -- **`_agent_tool_fake.py:302-314`** — a child-subprocess crash returns a synthetic `decision: block` verdict. This is good for surfacing failures but means a test that only checks `denied == True` could pass on a subprocess crash. My tests defend against this by also asserting the deny reason contains `"tester"`, but downstream consumers of the fake should be aware. -- **`run_pipeline.py:121-136 `_read_contract`** silently returns a default skeleton on `json.JSONDecodeError`. A corrupt contract is silently overwritten on the next `_persist_envelope`, losing forensic data. Not blocking for the walking-skeleton scope. - -The proposal is structurally sound and the round-trip / nested-dispatch / loader behaviour all match the AC. The two tests in my `test_rubric_loader.py` that depended on the documenter's rubric .md files now pass after the documenter's proposal landed (commit 473b109). - - -````yaml -id: 72587a36-ca64-4c -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - reason: "\nReviewed the coder's three artifacts (run_pipeline.py, _agent_tool_fake.py,\ - \ orchestrator/substrate/__init__.py) plus drove all three acceptance criteria\ - \ end-to-end via the tester-side tests I committed alongside this ACK.\n\n###\ - \ What I verified\n\n**TASK-1-1 (`bin/run_pipeline.py`) \u2014 bridge driver:**\n\ - - Round-trip works end-to-end. My `test_bridge_flattened_round_trip` invokes\ - \ the driver twice via `runpy` in a fresh subprocess each call. Stage A captures\ - \ `pending_hitl.decision.question == \"Confirm the refiner will run against\ - \ this repo + issue?\"` (the preflight); after I write `answer=\"approve\" +\ - \ status=\"answered\"` to the contract, Stage B replays the answer through the\ - \ `answer_log` mechanism and lands on the refine-gate decision (`decision_type=phase_gate`,\ - \ distinct question). Exits 0 on each yield as the AC requires.\n- The `pending_hitl`\ - \ envelope schema is documented as a stable contract at the top of `run_pipeline.py`\ - \ (lines 20-46) \u2014 fields `version`, `pipeline_id`, `timestamp`, `decision`,\ - \ `answer`, `status`, `result`, `error`, `answer_log`. The slice-3 daemon variant\ - \ (TASK-3-2) inheriting this shape is correctly called out for R17 mitigation.\n\ - - `_advance_generator` replay model is sound for a deterministic generator:\ - \ closes the generator in `finally` (GeneratorExit discipline), atomic temp+os.replace\ - \ contract writes. The `--daemon` flag short-circuits with a structured error\ - \ pointing at slice-3, which is the right deferral.\n- Idempotency invariant\ - \ holds: my `test_driver_is_idempotent_when_answer_unchanged` invokes the driver\ - \ twice without writing an answer in between, and the second invocation correctly\ - \ yields the SAME preflight decision (no silent advance of the state machine\ - \ without operator input).\n\n**TASK-1-9 (`integration_tests/regression/_agent_tool_fake.py`)\ - \ \u2014 nested-dispatch fake:**\n- Import-guard works: the module rejects imports\ - \ from outside `integration_tests`/`_agent_tool_fake`/`__main__` prefixes (lines\ - \ 84-96).\n- `dispatch(parent_role, child_role, write_target)` end-to-end correct\ - \ for R2: spawns the child subprocess with `EGG_AGENT_ROLE=child_role`, the\ - \ child invokes `hook_entry.decide(...)` and writes JSON to stdout, parent JSON-decodes\ - \ the verdict. My `test_pretooluse_hook_denies_nested_child_write` confirms\ - \ parent=architect + child=tester writing `orchestrator/foo.py` resolves to\ - \ `decision=block` with a `tester`-naming reason \u2014 proving the hook resolves\ - \ the child's role correctly, not the parent's.\n- Cross-role probe (parent=coder,\ - \ child=tester writing orchestrator/*) also denies with a tester-scoped reason\ - \ \u2014 the negative-control case proves there is no parent-side fallback masking\ - \ the child resolution.\n- No env leakage: `EGG_AGENT_ROLE` is passed via `subprocess.run(env=...)`\ - \ rather than `os.environ[...]=`; my leak-guard test confirms the parent process's\ - \ `EGG_AGENT_ROLE` stays unset after dispatch.\n- `DispatchResult` dataclass\ - \ is a reasonable wrapper (vs. raw dict) \u2014 the `.decision` field IS the\ - \ raw hook verdict, so the AC (\"returns the hook verdict\") is satisfied through\ - \ that attribute.\n\n**TASK-1-6 (`orchestrator/substrate/__init__.py` loader\ - \ update):**\n- `_ROLE_RUBRIC_SLICES` mapping + `_RUBRIC_LANDED_ROLES` frozenset\ - \ is a clean declarative extension; future slices can extend the set without\ - \ rewriting the loader.\n- `REFINER` regression case still passes (10/10 cases\ - \ in my `test_rubric_loader.py` are green after the documenter's task-1-4 rubric\ - \ .md files landed).\n- `REVIEWER_REFINE` / `REVIEWER_AGENT_DESIGN` load successfully\ - \ \u2014 verified body contains the role name.\n- `ARCHITECT` raises `ValueError`\ - \ with `follow-up slice-2 of issue #2717's rollout` diagnostic. Note: the AC\ - \ literal wording was \"follow-up slice 2\" (space); coder shipped \"follow-up\ - \ slice-2\" (hyphen). The intent is satisfied \u2014 my test accepts either\ - \ form. Non-blocking.\n- `REVIEWER_PLAN` / `REVIEWER_CODE` / `TASK_PLANNER`\ - \ still raise as expected (the structured-error fence preserves the slice-2/3\ - \ contract).\n- Path-traversal defence: `_load(\"../../../etc/passwd\")` resolves\ - \ to a `ValueError` (\"not part of the #2717 rollout's rubric set\") \u2014\ - \ defence-in-depth holds.\n\n### Non-blocking\n\n- **`run_pipeline.py:343-352\ - \ `_is_aborted_status`**: The substring set `{\"abort\", \"stop\", \"cancel\"\ - }` does not match what `orchestrator.substrate.in_process._answer_is_abort`\ - \ actually treats as an abort. The two paths diverge \u2014 if the orchestrator\ - \ widens its abort vocabulary, the driver's status-emission will silently mis-classify.\ - \ Consider importing the orchestrator's helper directly so they cannot drift.\n\ - - **`_agent_tool_fake.py:302-314`** \u2014 a child-subprocess crash returns\ - \ a synthetic `decision: block` verdict. This is good for surfacing failures\ - \ but means a test that only checks `denied == True` could pass on a subprocess\ - \ crash. My tests defend against this by also asserting the deny reason contains\ - \ `\"tester\"`, but downstream consumers of the fake should be aware.\n- **`run_pipeline.py:121-136\ - \ `_read_contract`** silently returns a default skeleton on `json.JSONDecodeError`.\ - \ A corrupt contract is silently overwritten on the next `_persist_envelope`,\ - \ losing forensic data. Not blocking for the walking-skeleton scope.\n\nThe\ - \ proposal is structurally sound and the round-trip / nested-dispatch / loader\ - \ behaviour all match the AC. The two tests in my `test_rubric_loader.py` that\ - \ depended on the documenter's rubric .md files now pass after the documenter's\ - \ proposal landed (commit 473b109).\n" - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:05:07Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 29817464-1b7e-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:05:09Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a5c28f96-1721-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:04:09.316695+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:05:12Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 488b7d93-c92e-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:05:12.574649+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:05:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 47913044-9592-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:05:59Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f487ac12-2bd4-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d3cd4964-456e-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:09Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 4f1bd5f8-9933-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:04:09.316695+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:12Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: f1758e17-c439-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:05:12.574649+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:19Z] reviewer_code_holistic → coder (CONSENSUS_ACK): ACK from reviewer_code_holistic for coder - - -Holistic review of coder v1 (commit fddeb82b) — ran all four mandatory passes against the three files: `run_pipeline.py` (new, 529 LOC), `_agent_tool_fake.py` (new, 451 LOC), `substrate/__init__.py` (+100/-10 for `_load_egg_sdlc_role_rubric` rework). Tracing the primary advertised use case — skill body invokes the driver in a loop, ferries `pending_hitl.decision` ↔ `pending_hitl.answer` ↔ `pending_hitl.answer_log` across process exits — the producer/consumer round-trip is internally consistent: `_advance_generator` starts a fresh generator, `next()` lands on yield #1, the replay loop calls `send(replay)` for each historical answer in `answer_log`, the post-replay yield is what gets persisted, and `StopIteration` / `_PreflightAborted` (translated to `StopIteration` by `_InProcessOrchestrator.run`) is caught and converted to `status="completed"` / `status="aborted"` per `_is_aborted_status`. The R17 mitigation is structurally intact (the `_agent_tool_fake` test helper re-exports `PENDING_HITL_SCHEMA_VERSION` from the driver module and `build_pending_hitl_envelope` agrees on the 9-field shape including `answer_log`). The R2 hook-logic fake is correctly scope-fenced: import-guarded against production callers (line 84-96), env-propagation of `EGG_AGENT_ROLE` mirrors what real Agent-tool dispatch would set, and the docstring honestly names the empirical-vs-test-fake limitation. The loader's `_ROLE_RUBRIC_SLICES` / `_RUBRIC_LANDED_ROLES` split is structured: roles outside the slice's rubric-supported set raise with a slice-N pointer, roles inside but missing on disk raise with a "documenter task is still in flight" pointer — the cross-task sequence (task-1-4 documenter ↔ task-1-6 coder) is handled gracefully even mid-slice. No CRITICAL cross-module asymmetry on the primary refine round-trip; ACKing. - -### Non-blocking - -1. **`run_pipeline.py:24-46` — STABLE-contract schema block omits `answer_log`.** The top-of-file `pending_hitl` schema documented under "STABLE contract — slice-3 daemon inherits this shape; do NOT change field names/types without bumping `version`" enumerates 8 fields (`version, pipeline_id, timestamp, decision, answer, status, result, error`) but the actual envelope persists a 9th — `answer_log: list[Any]` — that is **load-bearing** for the cross-process generator-state replay. The "Generator state across invocations" section later in the same docstring (lines 53-69) does mention `answer_log`, and `_coerce_envelope` (line 225) explicitly defaults it to `[]` so older / forward-compat envelopes don't break, so the implementations agree. But the AC for task-1-1 says the schema must be "documented as a stable contract (top-of-file comment listing the contract fields)" and the explicitly-labelled STABLE block is incomplete. The slice-3 daemon (TASK-3-2) implementer who copies just the STABLE block would miss the field that risk_analyst R17 mitigation actually depends on. **Fix:** add an `answer_log: list[Any]` line to the schema block with a one-line note (`# replayed answer history; the driver appends pending answers here on each invocation and feeds them into a fresh generator via send()`). - -2. **`run_pipeline.py:454` — `status == "answered"` synthetic-key coordination is fragile.** The driver promotes `envelope.answer → answer_log` only when `envelope.status == "answered"` AND `envelope.answer is not None`. If documenter task-1-2's SKILL.md ferries a render result back by writing only `pending_hitl.answer = X` (and leaving status at the driver's last-written `"pending"`), the answer is silently dropped on the next invocation — the replay loop sees an unchanged `answer_log`, the generator re-yields the same decision, and the user-visible failure shape is "the loop doesn't advance, no error printed." This is the same architectural shape as the `__checkout__` synthetic-key dead-end on PR #2105 — producer emits a value, consumer's filter excludes it, no error. Since the skill body and the driver are serialized by the shell loop (no concurrent-writer race needs guarding), the `status == "answered"` two-phase signal is defensive overhead, not load-bearing. **Fix (preferred):** drop the `status == "answered"` predicate and auto-promote whenever `envelope.answer is not None`, then have the driver write `envelope.status = "pending"` (or "completed"/"aborted"/"error") authoritatively from its own state machine. **Fix (alternative):** leave the strict check but assert in the driver that `envelope.answer is None or envelope.status == "answered"` — a stuck answer with the wrong status should be a hard error, not a silent drop. I'm not blocking on this because the documenter would naturally read the schema docstring (lines 32-43, which is clear that both fields must be set) and the documenter's task-1-2 is being reviewed by `reviewer_code` / `reviewer_contract`, but the coordination point is exactly the holistic-lens canonical miss shape and warrants flagging. - -3. **`run_pipeline.py:127-136` — `_read_contract` silently resets on corrupt JSON.** When the contract file exists but `json.loads(...)` raises `OSError` or `json.JSONDecodeError`, the driver returns a fresh skeleton (`{"schemaVersion": "1.1", "pipeline_id": pipeline_id, "current_phase": "refine", "decisions": []}`) and proceeds. The subsequent `_coerce_envelope(contract.get("pending_hitl"), ...)` sees a missing `pending_hitl` field and returns a fresh `_new_envelope`. Net effect: a partially-written or corrupt contract silently wipes the operator's `answer_log` — the operator sees the preflight decision pop up again with no diagnostic. The atomic temp-then-os.replace write discipline makes corruption rare in the happy path, but a concurrent-writer crash, full disk, or out-of-band edit would trigger this. **Fix:** when the file exists but unparseable, log a clear stderr warning naming the path and the parse error, and either back up the corrupt file (`.corrupt.`) or refuse to overwrite (exit 1 with a clear error) so the operator can inspect. - -4. **`run_pipeline.py:200-210` — `_coerce_envelope` silently resets when raw is non-dict.** Same shape as #3 but at the envelope layer: if `contract["pending_hitl"]` is somehow not a dict (legacy shape, manual edit, partial overwrite), the driver silently resets to a fresh envelope and loses `answer_log`. **Fix:** treat this as the corrupt-envelope path — surface a stderr warning with the bad type/value before falling back. - -5. **`run_pipeline.py:487 + orchestrator/substrate/in_process.py:807-826` — `_maybe_fence` `NotImplementedError` produces `status="error"`, not a clean fence signal.** When the operator chooses `approve_continue` at the refine HITL gate, `_InProcessOrchestrator._maybe_fence` raises `NotImplementedError` as the documented walking-skeleton scope fence — but the driver's top-level `except Exception` catches it, populates `envelope.error` with the traceback, and writes `status="error"`. The user experiences "the loop says error" on what is documented as the expected fence behavior of `approve_continue`. This is reachable on the primary advertised happy path (refine → continue to plan). **Fix:** in `_advance_generator` or `main()`, special-case `NotImplementedError` from `generator.send(...)` and translate to `status="completed"` with the fence message in `result` (or introduce a `status="fenced"` discriminator so the skill body can surface a friendlier message). For slice-1 this will be transient — slices 2-5 will replace the NotImplementedError with real plan/implement/pr paths — but slice-1 ships a refine-only bridge today, and the documented happy path produces a misleading error envelope. - -6. **`run_pipeline.py:322-327` — `generator.close()` `except Exception: pass`** swallows GeneratorExit propagation errors silently. Documented as defensive but could hide background-thread join failures. Worth at least a `logger.debug(...)` (or stderr trace under `EGG_DEBUG=1`) rather than a bare swallow. - -7. **`integration_tests/regression/_agent_tool_fake.py:411-441` — `build_pending_hitl_envelope` is a public-shaped helper but is not used by the test under task-1-5 (which the tester hasn't proposed yet).** If the tester's `test_pretooluse_hook_nested.py` doesn't actually round-trip an envelope through the fake, this helper is dead code in the slice. Will re-check once the tester proposes; if it's actually consumed by R17-validation tests in slice-3, leave a `# used by slice-3 tests` pointer to avoid the dead-code reading. - -8. **`_agent_tool_fake.py:84-96` import guard relies on `__name__.startswith("integration_tests")` / `"_agent_tool_fake"` / `"__main__"`** — a future test module named e.g. `integration_tests_extras._agent_tool_fake_wrapper` would slip past the guard. Tight today, but the prefix-match (vs. exact-match) is slightly looser than the docstring claim "test infrastructure only — it must not be imported by production code." Non-blocking; the looseness only matters if a future test tree adopts a `integration_tests*`-shaped name outside `integration_tests/`. - -9. **`substrate/__init__.py:271-277` — `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` and `reviewer_agent_design` ahead of the documenter's task-1-4 actually landing those rubric .md files.** Today on the slice-1 branch only `agents/refiner.md` exists. The "rubric missing on disk" branch (line 352-364) handles this with a clear structured error, and the cross-task sequencing is called out in the comment, so the failure mode is graceful — but a reader who treats `_RUBRIC_LANDED_ROLES` as ground truth (e.g., for a TODO checklist) would be misled. Either rename the constant (`_RUBRIC_TARGETED_ROLES` to make "intended for this slice but may not be on disk yet" explicit) or drop reviewer_refine/reviewer_agent_design from the set until task-1-4 lands and have the documenter add them back in their proposal. - - -````yaml -id: 8d7b9ac9-c7b3-43 -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - reason: "\nHolistic review of coder v1 (commit fddeb82b) \u2014 ran all four mandatory\ - \ passes against the three files: `run_pipeline.py` (new, 529 LOC), `_agent_tool_fake.py`\ - \ (new, 451 LOC), `substrate/__init__.py` (+100/-10 for `_load_egg_sdlc_role_rubric`\ - \ rework). Tracing the primary advertised use case \u2014 skill body invokes\ - \ the driver in a loop, ferries `pending_hitl.decision` \u2194 `pending_hitl.answer`\ - \ \u2194 `pending_hitl.answer_log` across process exits \u2014 the producer/consumer\ - \ round-trip is internally consistent: `_advance_generator` starts a fresh generator,\ - \ `next()` lands on yield #1, the replay loop calls `send(replay)` for each\ - \ historical answer in `answer_log`, the post-replay yield is what gets persisted,\ - \ and `StopIteration` / `_PreflightAborted` (translated to `StopIteration` by\ - \ `_InProcessOrchestrator.run`) is caught and converted to `status=\"completed\"\ - ` / `status=\"aborted\"` per `_is_aborted_status`. The R17 mitigation is structurally\ - \ intact (the `_agent_tool_fake` test helper re-exports `PENDING_HITL_SCHEMA_VERSION`\ - \ from the driver module and `build_pending_hitl_envelope` agrees on the 9-field\ - \ shape including `answer_log`). The R2 hook-logic fake is correctly scope-fenced:\ - \ import-guarded against production callers (line 84-96), env-propagation of\ - \ `EGG_AGENT_ROLE` mirrors what real Agent-tool dispatch would set, and the\ - \ docstring honestly names the empirical-vs-test-fake limitation. The loader's\ - \ `_ROLE_RUBRIC_SLICES` / `_RUBRIC_LANDED_ROLES` split is structured: roles\ - \ outside the slice's rubric-supported set raise with a slice-N pointer, roles\ - \ inside but missing on disk raise with a \"documenter task is still in flight\"\ - \ pointer \u2014 the cross-task sequence (task-1-4 documenter \u2194 task-1-6\ - \ coder) is handled gracefully even mid-slice. No CRITICAL cross-module asymmetry\ - \ on the primary refine round-trip; ACKing.\n\n### Non-blocking\n\n1. **`run_pipeline.py:24-46`\ - \ \u2014 STABLE-contract schema block omits `answer_log`.** The top-of-file\ - \ `pending_hitl` schema documented under \"STABLE contract \u2014 slice-3 daemon\ - \ inherits this shape; do NOT change field names/types without bumping `version`\"\ - \ enumerates 8 fields (`version, pipeline_id, timestamp, decision, answer, status,\ - \ result, error`) but the actual envelope persists a 9th \u2014 `answer_log:\ - \ list[Any]` \u2014 that is **load-bearing** for the cross-process generator-state\ - \ replay. The \"Generator state across invocations\" section later in the same\ - \ docstring (lines 53-69) does mention `answer_log`, and `_coerce_envelope`\ - \ (line 225) explicitly defaults it to `[]` so older / forward-compat envelopes\ - \ don't break, so the implementations agree. But the AC for task-1-1 says the\ - \ schema must be \"documented as a stable contract (top-of-file comment listing\ - \ the contract fields)\" and the explicitly-labelled STABLE block is incomplete.\ - \ The slice-3 daemon (TASK-3-2) implementer who copies just the STABLE block\ - \ would miss the field that risk_analyst R17 mitigation actually depends on.\ - \ **Fix:** add an `answer_log: list[Any]` line to the schema block with a one-line\ - \ note (`# replayed answer history; the driver appends pending answers here\ - \ on each invocation and feeds them into a fresh generator via send()`).\n\n\ - 2. **`run_pipeline.py:454` \u2014 `status == \"answered\"` synthetic-key coordination\ - \ is fragile.** The driver promotes `envelope.answer \u2192 answer_log` only\ - \ when `envelope.status == \"answered\"` AND `envelope.answer is not None`.\ - \ If documenter task-1-2's SKILL.md ferries a render result back by writing\ - \ only `pending_hitl.answer = X` (and leaving status at the driver's last-written\ - \ `\"pending\"`), the answer is silently dropped on the next invocation \u2014\ - \ the replay loop sees an unchanged `answer_log`, the generator re-yields the\ - \ same decision, and the user-visible failure shape is \"the loop doesn't advance,\ - \ no error printed.\" This is the same architectural shape as the `__checkout__`\ - \ synthetic-key dead-end on PR #2105 \u2014 producer emits a value, consumer's\ - \ filter excludes it, no error. Since the skill body and the driver are serialized\ - \ by the shell loop (no concurrent-writer race needs guarding), the `status\ - \ == \"answered\"` two-phase signal is defensive overhead, not load-bearing.\ - \ **Fix (preferred):** drop the `status == \"answered\"` predicate and auto-promote\ - \ whenever `envelope.answer is not None`, then have the driver write `envelope.status\ - \ = \"pending\"` (or \"completed\"/\"aborted\"/\"error\") authoritatively from\ - \ its own state machine. **Fix (alternative):** leave the strict check but assert\ - \ in the driver that `envelope.answer is None or envelope.status == \"answered\"\ - ` \u2014 a stuck answer with the wrong status should be a hard error, not a\ - \ silent drop. I'm not blocking on this because the documenter would naturally\ - \ read the schema docstring (lines 32-43, which is clear that both fields must\ - \ be set) and the documenter's task-1-2 is being reviewed by `reviewer_code`\ - \ / `reviewer_contract`, but the coordination point is exactly the holistic-lens\ - \ canonical miss shape and warrants flagging.\n\n3. **`run_pipeline.py:127-136`\ - \ \u2014 `_read_contract` silently resets on corrupt JSON.** When the contract\ - \ file exists but `json.loads(...)` raises `OSError` or `json.JSONDecodeError`,\ - \ the driver returns a fresh skeleton (`{\"schemaVersion\": \"1.1\", \"pipeline_id\"\ - : pipeline_id, \"current_phase\": \"refine\", \"decisions\": []}`) and proceeds.\ - \ The subsequent `_coerce_envelope(contract.get(\"pending_hitl\"), ...)` sees\ - \ a missing `pending_hitl` field and returns a fresh `_new_envelope`. Net effect:\ - \ a partially-written or corrupt contract silently wipes the operator's `answer_log`\ - \ \u2014 the operator sees the preflight decision pop up again with no diagnostic.\ - \ The atomic temp-then-os.replace write discipline makes corruption rare in\ - \ the happy path, but a concurrent-writer crash, full disk, or out-of-band edit\ - \ would trigger this. **Fix:** when the file exists but unparseable, log a clear\ - \ stderr warning naming the path and the parse error, and either back up the\ - \ corrupt file (`.corrupt.`) or refuse to overwrite (exit 1 with a\ - \ clear error) so the operator can inspect.\n\n4. **`run_pipeline.py:200-210`\ - \ \u2014 `_coerce_envelope` silently resets when raw is non-dict.** Same shape\ - \ as #3 but at the envelope layer: if `contract[\"pending_hitl\"]` is somehow\ - \ not a dict (legacy shape, manual edit, partial overwrite), the driver silently\ - \ resets to a fresh envelope and loses `answer_log`. **Fix:** treat this as\ - \ the corrupt-envelope path \u2014 surface a stderr warning with the bad type/value\ - \ before falling back.\n\n5. **`run_pipeline.py:487 + orchestrator/substrate/in_process.py:807-826`\ - \ \u2014 `_maybe_fence` `NotImplementedError` produces `status=\"error\"`, not\ - \ a clean fence signal.** When the operator chooses `approve_continue` at the\ - \ refine HITL gate, `_InProcessOrchestrator._maybe_fence` raises `NotImplementedError`\ - \ as the documented walking-skeleton scope fence \u2014 but the driver's top-level\ - \ `except Exception` catches it, populates `envelope.error` with the traceback,\ - \ and writes `status=\"error\"`. The user experiences \"the loop says error\"\ - \ on what is documented as the expected fence behavior of `approve_continue`.\ - \ This is reachable on the primary advertised happy path (refine \u2192 continue\ - \ to plan). **Fix:** in `_advance_generator` or `main()`, special-case `NotImplementedError`\ - \ from `generator.send(...)` and translate to `status=\"completed\"` with the\ - \ fence message in `result` (or introduce a `status=\"fenced\"` discriminator\ - \ so the skill body can surface a friendlier message). For slice-1 this will\ - \ be transient \u2014 slices 2-5 will replace the NotImplementedError with real\ - \ plan/implement/pr paths \u2014 but slice-1 ships a refine-only bridge today,\ - \ and the documented happy path produces a misleading error envelope.\n\n6.\ - \ **`run_pipeline.py:322-327` \u2014 `generator.close()` `except Exception:\ - \ pass`** swallows GeneratorExit propagation errors silently. Documented as\ - \ defensive but could hide background-thread join failures. Worth at least a\ - \ `logger.debug(...)` (or stderr trace under `EGG_DEBUG=1`) rather than a bare\ - \ swallow.\n\n7. **`integration_tests/regression/_agent_tool_fake.py:411-441`\ - \ \u2014 `build_pending_hitl_envelope` is a public-shaped helper but is not\ - \ used by the test under task-1-5 (which the tester hasn't proposed yet).**\ - \ If the tester's `test_pretooluse_hook_nested.py` doesn't actually round-trip\ - \ an envelope through the fake, this helper is dead code in the slice. Will\ - \ re-check once the tester proposes; if it's actually consumed by R17-validation\ - \ tests in slice-3, leave a `# used by slice-3 tests` pointer to avoid the dead-code\ - \ reading.\n\n8. **`_agent_tool_fake.py:84-96` import guard relies on `__name__.startswith(\"\ - integration_tests\")` / `\"_agent_tool_fake\"` / `\"__main__\"`** \u2014 a future\ - \ test module named e.g. `integration_tests_extras._agent_tool_fake_wrapper`\ - \ would slip past the guard. Tight today, but the prefix-match (vs. exact-match)\ - \ is slightly looser than the docstring claim \"test infrastructure only \u2014\ - \ it must not be imported by production code.\" Non-blocking; the looseness\ - \ only matters if a future test tree adopts a `integration_tests*`-shaped name\ - \ outside `integration_tests/`.\n\n9. **`substrate/__init__.py:271-277` \u2014\ - \ `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` and `reviewer_agent_design`\ - \ ahead of the documenter's task-1-4 actually landing those rubric .md files.**\ - \ Today on the slice-1 branch only `agents/refiner.md` exists. The \"rubric\ - \ missing on disk\" branch (line 352-364) handles this with a clear structured\ - \ error, and the cross-task sequencing is called out in the comment, so the\ - \ failure mode is graceful \u2014 but a reader who treats `_RUBRIC_LANDED_ROLES`\ - \ as ground truth (e.g., for a TODO checklist) would be misled. Either rename\ - \ the constant (`_RUBRIC_TARGETED_ROLES` to make \"intended for this slice but\ - \ may not be on disk yet\" explicit) or drop reviewer_refine/reviewer_agent_design\ - \ from the set until task-1-4 lands and have the documenter add them back in\ - \ their proposal.\n" - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:06:19Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 0f314031-ab6b-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:06:25Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c2a2cfa7-18ec-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:25.335654+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:28Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b0899694-3474-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:28.242194+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:28Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 7819a381-a22f-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:06:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d4f1b4d3-ae1b-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ece175eb-b953-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:06:59Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: ecc3d772-700f-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e0ba256e-3637-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:09Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5a13fe44-ed4f-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:04:09.316695+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:25Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 1e438367-6a80-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:25.335654+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:27Z] reviewer_code → coder (CONSENSUS_ACK): ACK from reviewer_code for coder - - -Reviewed all 3 files from commit fddeb82bf (slice-1 coder: bridge driver + R2 fake + loader expansion) and traced data flow end-to-end against the contract acceptance criteria and the cq-1 Option C bridge design. Existing claude_code substrate test suite (`shared/tests/test_claude_code_spawner.py`, `test_pretooluse_hook_policy.py`, `test_run_pipeline_in_process*.py`, `test_substrate_interfaces.py`) passes (54 passed, 4 skipped). Acceptance criteria for TASK-1-1, TASK-1-6, and TASK-1-9 are met. The R2-fake import-guard fires correctly when imported under a non-test name. No security vulnerabilities, no resource leaks, no crashes. - -I am ACKing rather than NACKing because the contract's literal task acceptance is satisfied and the per-file logic is correct, but several substantive design / robustness concerns surfaced in tracing the cross-module data flow. They are listed below as non-blocking; please consider addressing the top three in this PR (cheap docstring / control-flow fixes), and at minimum file follow-ups for the rest before slice-3's daemon variant inherits the same envelope schema. - -### Non-blocking - -1. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:258-327` — replay strategy re-executes all side effects on every invocation.** The docstring (lines 53-69) calls `run_pipeline_in_process` "deterministic", but the determinism only applies to *which yield boundary the generator reaches*. The side effects between yields — most importantly `_spawn_refiner()` in `orchestrator/substrate/in_process.py:617` — are NOT idempotent: each replay invocation creates a worktree, dispatches a real Claude Code Agent (LLM cost), writes/overwrites `.egg-state/drafts/-analysis.md`, then tears the worktree down again. The concrete operator-visible consequence in the refine-only spike: - - I2 (after preflight answer): refiner spawns once, refine_gate yielded, operator approves based on I2's artifact content. - - I3 (after gate "approve_continue" answer): driver replays preflight → `_spawn_refiner()` runs AGAIN before `_maybe_fence` raises NotImplementedError. The artifact-on-disk is now potentially DIFFERENT content from what the operator approved (LLM non-determinism), and the operator paid for an extra Claude Code Agent dispatch they cannot see. - This is a structural mismatch with cq-1 Option B's literal description ("Flatten generator into a hand-shaped sequence of single-yield `python3 .py` invocations" — i.e. *separate stage scripts*, not one driver that replays from scratch). The task-1-1 description is what the coder followed; the architectural concern is upstream. Cheapest fix: add a 3-line idempotency check in `_spawn_refiner` (skip the `bundle.spawner.spawn(...)` when `artifact_path.exists()` AND content is non-placeholder). Alternatively, update the driver docstring lines 64-69 to honestly state "each replay re-runs all side effects between yields, including the refiner subagent dispatch — operators using this against real Anthropic credentials incur 2x refiner cost per approved refine cycle." Today the docstring is misleading; future maintainers will assume "deterministic" means "free to replay". - -2. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:434-447` — `--daemon` short-circuit wipes `answer_log` AND uses exit code 1.** The argparse help text (lines 391-401) advertises `--daemon` as exiting with "a structured error so the skill can fall back to the flattened path", but: (a) the constructed envelope (line 435) calls `_new_envelope(...)` WITHOUT `answer_log=envelope["answer_log"]`, so any operator answers already accumulated are silently destroyed; (b) the exit code is 1, which the module docstring (lines 71-77) defines as "internal error" — the skill body has no way to differentiate "daemon path not yet implemented" from a real driver crash. Fix: preserve `answer_log` on the daemon path and either align the docstring with the actual exit semantics or pick a distinct status string (e.g. `"daemon_unavailable"`) so the skill body can branch. - -3. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:423-429` — envelope-coerce error path also wipes `answer_log`.** Same shape as finding #2: when `_coerce_envelope` raises `ValueError` (envelope version newer than driver supports), the replacement envelope is built without `answer_log=...`, losing the operator's history. This is the path future slice-3 / version-bump scenarios will exercise. Preserve `answer_log` even on coerce error — the new envelope is a diagnostic for the operator, not a state reset. - -4. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:113-115` — no path-traversal validation on `pipeline_id`.** `_contract_path(state_root, pipeline_id)` returns `contracts / f"{pipeline_id}.json"` with no validation. A pipeline_id of `../../foo` writes outside the contracts dir. SKILL.md explicitly cites "Path-escape safety mirrors the existing `is_relative_to` + `resolve()` defense in the gateway" — the driver does not implement that defense. Add a `re.fullmatch(r"^[A-Za-z0-9._-]+$", pipeline_id)` validation (or equivalent `is_relative_to(contracts)` check after resolution). Local-trust scope makes this defense-in-depth rather than load-bearing, but the SKILL.md claim is currently false. - -5. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:297, 320` — dead `next_answer` variable.** `_advance_generator` initialises `next_answer: Any = None` and returns it in all three exit paths, never updating it. Always `None`. Either remove from the tuple shape or wire it correctly to the next pending answer the skill should ferry. Currently dead code that misleads readers about the function's return contract. - -6. **`integration_tests/regression/_agent_tool_fake.py:404` — `DispatchResult.child_pid` is always 0.** `dispatch(...)` reads `int(verdict.get("_fake_child_pid", 0) or 0)`, but `pre_tool_use_callback` (lines 222-333) never sets `_fake_child_pid` in the returned dict — only `_fake_child_exit_code` and `_fake_child_stderr`. The PID is unavailable post-exit through `subprocess.run`. Either remove the field from `DispatchResult` or refactor to `subprocess.Popen` and capture `.pid`. - -7. **`integration_tests/regression/_agent_tool_fake.py:84-96` — import guard is more permissive than the acceptance criterion.** Task-1-9 acceptance says `if not __name__.startswith("integration_tests")`; the implementation uses three prefixes including `"_agent_tool_fake"`, which also matches sibling modules like `_agent_tool_fake_helper.py` (verified). Use an exact-match `set` rather than a prefix tuple, or tighten the prefix to the exact module name `"_agent_tool_fake"` followed by a sentinel. - -8. **`integration_tests/regression/_agent_tool_fake.py:122-126` — silent version fallback.** If the driver bumps `PENDING_HITL_SCHEMA_VERSION` to 2 but the path-walk import fails (e.g., plugin layout changes per the TODO at `orchestrator/substrate/__init__.py:319-328`), the fake silently downgrades the constant to 1. Tests using the fake's constant would miss the bump. Either propagate the ImportError (fail loudly) or log a clear warning to stderr. - -9. **`integration_tests/regression/_agent_tool_fake.py:266` — child subprocess inherits full parent env**, including `ANTHROPIC_API_KEY`. The child only invokes `hook_entry.decide()` and does not network, so impact is minimal — but defense-in-depth would whitelist only `EGG_*`, `HOME`, `PATH`, `PYTHONPATH`. Non-blocking; the child's tool-call surface is restricted by the test-only scope. - -10. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:487-496` — traceback written verbatim to the contract file.** `traceback.format_exc(limit=8)` is serialised into `pending_hitl.error`, which is then JSON-serialised and read by the skill body. Could expose absolute filesystem paths in the contract artifact. Local-trust scope OK; consider truncating or stripping the path prefix before serialising in production. - -11. **`orchestrator/substrate/__init__.py:271-277` — `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` / `reviewer_agent_design` before the documenter (task-1-4) has landed the rubric markdown files in the same slice.** The behaviour is graceful — `_load_egg_sdlc_role_rubric` raises a clear "rubric missing on disk … sequence the documenter's rubric task before the loader update" ValueError — so the acceptance criterion's `_load_egg_sdlc_role_rubric(REVIEWER_REFINE) returns the markdown body of reviewer_refine.md` will only be satisfied after documenter merges. This is the expected concurrent-BRC dependency; calling out for the re-review when documenter ACKs. - -### Acceptance criteria check - -- **TASK-1-1**: `bin/run_pipeline.py` exists at the correct path; imports `run_pipeline_in_process`; round-trips `HITLDecision` through `pending_hitl`; exit codes 0/1 per docstring; `pending_hitl` envelope schema documented at top-of-file as stable contract. ✓ -- **TASK-1-9**: `_agent_tool_fake.py` exists; exposes `dispatch(parent_role, child_role, write_target)`; helper invokes `hook_entry.decide(...)` via `pre_tool_use_callback`; module docstring marks it test-only; import guard present (with broader prefix than spec'd — finding #7). ✓ (minor deviation) -- **TASK-1-6**: `reviewer_refine` and `reviewer_agent_design` accepted by the loader (returns markdown once TASK-1-4 lands); `architect` raises ValueError with "deferred to follow-up slice-2 of issue #2717's rollout" (matches acceptance's "follow-up slice 2" hint). ✓ - - -````yaml -id: cf0963f1-db8d-4d -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - integration_tests/regression/_agent_tool_fake.py - - orchestrator/substrate/__init__.py - reason: "\nReviewed all 3 files from commit fddeb82bf (slice-1 coder: bridge driver\ - \ + R2 fake + loader expansion) and traced data flow end-to-end against the\ - \ contract acceptance criteria and the cq-1 Option C bridge design. Existing\ - \ claude_code substrate test suite (`shared/tests/test_claude_code_spawner.py`,\ - \ `test_pretooluse_hook_policy.py`, `test_run_pipeline_in_process*.py`, `test_substrate_interfaces.py`)\ - \ passes (54 passed, 4 skipped). Acceptance criteria for TASK-1-1, TASK-1-6,\ - \ and TASK-1-9 are met. The R2-fake import-guard fires correctly when imported\ - \ under a non-test name. No security vulnerabilities, no resource leaks, no\ - \ crashes.\n\nI am ACKing rather than NACKing because the contract's literal\ - \ task acceptance is satisfied and the per-file logic is correct, but several\ - \ substantive design / robustness concerns surfaced in tracing the cross-module\ - \ data flow. They are listed below as non-blocking; please consider addressing\ - \ the top three in this PR (cheap docstring / control-flow fixes), and at minimum\ - \ file follow-ups for the rest before slice-3's daemon variant inherits the\ - \ same envelope schema.\n\n### Non-blocking\n\n1. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:258-327`\ - \ \u2014 replay strategy re-executes all side effects on every invocation.**\ - \ The docstring (lines 53-69) calls `run_pipeline_in_process` \"deterministic\"\ - , but the determinism only applies to *which yield boundary the generator reaches*.\ - \ The side effects between yields \u2014 most importantly `_spawn_refiner()`\ - \ in `orchestrator/substrate/in_process.py:617` \u2014 are NOT idempotent: each\ - \ replay invocation creates a worktree, dispatches a real Claude Code Agent\ - \ (LLM cost), writes/overwrites `.egg-state/drafts/-analysis.md`, then tears\ - \ the worktree down again. The concrete operator-visible consequence in the\ - \ refine-only spike:\n - I2 (after preflight answer): refiner spawns once,\ - \ refine_gate yielded, operator approves based on I2's artifact content.\n \ - \ - I3 (after gate \"approve_continue\" answer): driver replays preflight \u2192\ - \ `_spawn_refiner()` runs AGAIN before `_maybe_fence` raises NotImplementedError.\ - \ The artifact-on-disk is now potentially DIFFERENT content from what the operator\ - \ approved (LLM non-determinism), and the operator paid for an extra Claude\ - \ Code Agent dispatch they cannot see.\n This is a structural mismatch with\ - \ cq-1 Option B's literal description (\"Flatten generator into a hand-shaped\ - \ sequence of single-yield `python3 .py` invocations\" \u2014 i.e. *separate\ - \ stage scripts*, not one driver that replays from scratch). The task-1-1 description\ - \ is what the coder followed; the architectural concern is upstream. Cheapest\ - \ fix: add a 3-line idempotency check in `_spawn_refiner` (skip the `bundle.spawner.spawn(...)`\ - \ when `artifact_path.exists()` AND content is non-placeholder). Alternatively,\ - \ update the driver docstring lines 64-69 to honestly state \"each replay re-runs\ - \ all side effects between yields, including the refiner subagent dispatch \u2014\ - \ operators using this against real Anthropic credentials incur 2x refiner cost\ - \ per approved refine cycle.\" Today the docstring is misleading; future maintainers\ - \ will assume \"deterministic\" means \"free to replay\".\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:434-447`\ - \ \u2014 `--daemon` short-circuit wipes `answer_log` AND uses exit code 1.**\ - \ The argparse help text (lines 391-401) advertises `--daemon` as exiting with\ - \ \"a structured error so the skill can fall back to the flattened path\", but:\ - \ (a) the constructed envelope (line 435) calls `_new_envelope(...)` WITHOUT\ - \ `answer_log=envelope[\"answer_log\"]`, so any operator answers already accumulated\ - \ are silently destroyed; (b) the exit code is 1, which the module docstring\ - \ (lines 71-77) defines as \"internal error\" \u2014 the skill body has no way\ - \ to differentiate \"daemon path not yet implemented\" from a real driver crash.\ - \ Fix: preserve `answer_log` on the daemon path and either align the docstring\ - \ with the actual exit semantics or pick a distinct status string (e.g. `\"\ - daemon_unavailable\"`) so the skill body can branch.\n\n3. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:423-429`\ - \ \u2014 envelope-coerce error path also wipes `answer_log`.** Same shape as\ - \ finding #2: when `_coerce_envelope` raises `ValueError` (envelope version\ - \ newer than driver supports), the replacement envelope is built without `answer_log=...`,\ - \ losing the operator's history. This is the path future slice-3 / version-bump\ - \ scenarios will exercise. Preserve `answer_log` even on coerce error \u2014\ - \ the new envelope is a diagnostic for the operator, not a state reset.\n\n\ - 4. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:113-115` \u2014 no\ - \ path-traversal validation on `pipeline_id`.** `_contract_path(state_root,\ - \ pipeline_id)` returns `contracts / f\"{pipeline_id}.json\"` with no validation.\ - \ A pipeline_id of `../../foo` writes outside the contracts dir. SKILL.md explicitly\ - \ cites \"Path-escape safety mirrors the existing `is_relative_to` + `resolve()`\ - \ defense in the gateway\" \u2014 the driver does not implement that defense.\ - \ Add a `re.fullmatch(r\"^[A-Za-z0-9._-]+$\", pipeline_id)` validation (or equivalent\ - \ `is_relative_to(contracts)` check after resolution). Local-trust scope makes\ - \ this defense-in-depth rather than load-bearing, but the SKILL.md claim is\ - \ currently false.\n\n5. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:297,\ - \ 320` \u2014 dead `next_answer` variable.** `_advance_generator` initialises\ - \ `next_answer: Any = None` and returns it in all three exit paths, never updating\ - \ it. Always `None`. Either remove from the tuple shape or wire it correctly\ - \ to the next pending answer the skill should ferry. Currently dead code that\ - \ misleads readers about the function's return contract.\n\n6. **`integration_tests/regression/_agent_tool_fake.py:404`\ - \ \u2014 `DispatchResult.child_pid` is always 0.** `dispatch(...)` reads `int(verdict.get(\"\ - _fake_child_pid\", 0) or 0)`, but `pre_tool_use_callback` (lines 222-333) never\ - \ sets `_fake_child_pid` in the returned dict \u2014 only `_fake_child_exit_code`\ - \ and `_fake_child_stderr`. The PID is unavailable post-exit through `subprocess.run`.\ - \ Either remove the field from `DispatchResult` or refactor to `subprocess.Popen`\ - \ and capture `.pid`.\n\n7. **`integration_tests/regression/_agent_tool_fake.py:84-96`\ - \ \u2014 import guard is more permissive than the acceptance criterion.** Task-1-9\ - \ acceptance says `if not __name__.startswith(\"integration_tests\")`; the implementation\ - \ uses three prefixes including `\"_agent_tool_fake\"`, which also matches sibling\ - \ modules like `_agent_tool_fake_helper.py` (verified). Use an exact-match `set`\ - \ rather than a prefix tuple, or tighten the prefix to the exact module name\ - \ `\"_agent_tool_fake\"` followed by a sentinel.\n\n8. **`integration_tests/regression/_agent_tool_fake.py:122-126`\ - \ \u2014 silent version fallback.** If the driver bumps `PENDING_HITL_SCHEMA_VERSION`\ - \ to 2 but the path-walk import fails (e.g., plugin layout changes per the TODO\ - \ at `orchestrator/substrate/__init__.py:319-328`), the fake silently downgrades\ - \ the constant to 1. Tests using the fake's constant would miss the bump. Either\ - \ propagate the ImportError (fail loudly) or log a clear warning to stderr.\n\ - \n9. **`integration_tests/regression/_agent_tool_fake.py:266` \u2014 child subprocess\ - \ inherits full parent env**, including `ANTHROPIC_API_KEY`. The child only\ - \ invokes `hook_entry.decide()` and does not network, so impact is minimal \u2014\ - \ but defense-in-depth would whitelist only `EGG_*`, `HOME`, `PATH`, `PYTHONPATH`.\ - \ Non-blocking; the child's tool-call surface is restricted by the test-only\ - \ scope.\n\n10. **`plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:487-496`\ - \ \u2014 traceback written verbatim to the contract file.** `traceback.format_exc(limit=8)`\ - \ is serialised into `pending_hitl.error`, which is then JSON-serialised and\ - \ read by the skill body. Could expose absolute filesystem paths in the contract\ - \ artifact. Local-trust scope OK; consider truncating or stripping the path\ - \ prefix before serialising in production.\n\n11. **`orchestrator/substrate/__init__.py:271-277`\ - \ \u2014 `_RUBRIC_LANDED_ROLES` includes `reviewer_refine` / `reviewer_agent_design`\ - \ before the documenter (task-1-4) has landed the rubric markdown files in the\ - \ same slice.** The behaviour is graceful \u2014 `_load_egg_sdlc_role_rubric`\ - \ raises a clear \"rubric missing on disk \u2026 sequence the documenter's rubric\ - \ task before the loader update\" ValueError \u2014 so the acceptance criterion's\ - \ `_load_egg_sdlc_role_rubric(REVIEWER_REFINE) returns the markdown body of\ - \ reviewer_refine.md` will only be satisfied after documenter merges. This is\ - \ the expected concurrent-BRC dependency; calling out for the re-review when\ - \ documenter ACKs.\n\n### Acceptance criteria check\n\n- **TASK-1-1**: `bin/run_pipeline.py`\ - \ exists at the correct path; imports `run_pipeline_in_process`; round-trips\ - \ `HITLDecision` through `pending_hitl`; exit codes 0/1 per docstring; `pending_hitl`\ - \ envelope schema documented at top-of-file as stable contract. \u2713\n- **TASK-1-9**:\ - \ `_agent_tool_fake.py` exists; exposes `dispatch(parent_role, child_role, write_target)`;\ - \ helper invokes `hook_entry.decide(...)` via `pre_tool_use_callback`; module\ - \ docstring marks it test-only; import guard present (with broader prefix than\ - \ spec'd \u2014 finding #7). \u2713 (minor deviation)\n- **TASK-1-6**: `reviewer_refine`\ - \ and `reviewer_agent_design` accepted by the loader (returns markdown once\ - \ TASK-1-4 lands); `architect` raises ValueError with \"deferred to follow-up\ - \ slice-2 of issue #2717's rollout\" (matches acceptance's \"follow-up slice\ - \ 2\" hint). \u2713\n" - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:07:27Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 8d234ce0-4a10-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:07:35Z] coder (CONSENSUS_CONFIRMED): Confirmed by coder (pending_acks) - -Agent coder cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus. - -````yaml -id: 396bf7c7-7f5c-4f -phase: implement -metadata: - pending_acks: true - slice_id: slice-1 -```` - -### [2026-05-19T19:07:35Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 472f8205-e415-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:07:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 5820a72a-dc0a-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:39Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 998f4855-9e74-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:07:39.835769+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:39Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: fabd92fb-cb52-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:07:41Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 87cf4305-0838-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:07:41.024684+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:50Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 23737e47-39af-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:07:50.285090+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:07:53Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c8a31a60-becb-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:08:21Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 5a5b88bc-10ac-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:08:21Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2a4dd715-3035-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:08:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 419fb1c5-9b0d-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:08:41Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: d4ecfc17-5704-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:07:41.024684+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:08:50Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: a7d0ade2-274c-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:07:50.285090+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:08:54Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 4b094316-00ae-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:59:38.473276+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:09:14Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c31a305f-7483-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:09:19Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0b62fe99-7f29-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:09:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 4ed9d84b-0fd6-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:09:41Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: ef6a77eb-bc48-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:07:41.024684+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:09:47Z] documenter (HEARTBEAT): heartbeat: PROPOSED - -documenter: TASK-1-2 + TASK-1-4 + TASK-1-8 committed and proposed v1; awaiting reviewer_code ACK/NACK - -````yaml -id: 74e5538e-7dff-4a -phase: implement -metadata: - state: PROPOSED - slice_id: slice-1 -```` - -### [2026-05-19T19:09:50Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: db7412eb-ae3e-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:07:50.285090+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:09:52Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 43610bf4-6ce3-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:09:52Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 0dad4b12-4ad7-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:09:52Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 8e442476-3cdb-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:09:57Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 8e319355-4c3f-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:09:57.927975+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:10:01Z] coder (HEARTBEAT): heartbeat: WAITING_ON_ROLE - -All 6 reviewers ACKed coder v1 at fddeb82bf. confirm() blocked on tester (producer) — version=0; waiting per overseer guidance. - -````yaml -id: 6afabf8c-ac05-43 -phase: implement -metadata: - state: WAITING_ON_ROLE - waiting_on: tester - slice_id: slice-1 -```` - -### [2026-05-19T19:10:01Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3fd58f85-1af7-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:01.880478+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:10:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 4439b7a6-3eb4-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:10:19Z] documenter (CONSENSUS_CONFIRMED): Confirmed by documenter (pending_acks) - -Agent documenter cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus. - -````yaml -id: 324f473f-6aa6-42 -phase: implement -metadata: - pending_acks: true - slice_id: slice-1 -```` - -### [2026-05-19T19:10:19Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 80b189dd-1ff4-47 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:10:19Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0e79f9e8-d224-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:10:23Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 8e02f21b-f2f5-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:23.268376+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:10:29Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d83ddde0-ecf0-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:29.767135+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:10:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f7a699c9-ed68-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:11:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 4d7eddd6-8025-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:01.880478+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:11:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 326fd3e7-7f41-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:11:16Z] reviewer_code → documenter (CONSENSUS_NACK): NACK from reviewer_code for documenter - - -Reviewed all 4 files in commit 473b109cb. The two new rubric files (`reviewer_refine.md`, `reviewer_agent_design.md`) and the ADR rewrite (`docs/architecture/claude-code-substrate.md`) are well-shaped and meet their acceptance criteria. **The SKILL.md update has three blocking documentation defects** that make the "documented usage section cannot actually be executed" — directly contradicting task-1-2's acceptance bullet ("so the loop can actually run"). - -### Blocking - -1. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:90-129` — documented `bin/run_pipeline.py` CLI invocations use wrong flag names; the example will fail at argparse.** The skill loop example shows: - ```bash - python3 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py \ - --pipeline-id "issue-${ISSUE}" \ - --repo "${REPO}" \ - --issue "${ISSUE}" - ``` - But the actual driver at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402` accepts: - - `pipeline_id` as a **positional** argument (no `--pipeline-id` flag exists) - - `--issue-number` (not `--issue`) - - An operator (or future skill body) copying this invocation gets `argparse: unrecognized arguments: --pipeline-id`. The skill loop as documented cannot run. Fix: change the documented invocations to `python3 .../run_pipeline.py "issue-${ISSUE}" --repo "${REPO}" --issue-number "${ISSUE}"` (positional pipeline_id, `--issue-number` flag), or update the driver to accept `--pipeline-id` / `--issue` as aliases. Pick one and make the doc match the code. - -2. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:96-110` — the documented `pending_hitl` envelope is incomplete; the skill body cannot tell "completed" from "error" from "aborted".** SKILL.md documents the envelope as: - ```json - { "pending_hitl": { "version": 1, "pipeline_id": "...", "timestamp": "...", "decision": {...}, "answer": null } } - ``` - But the driver's actual envelope shape (per `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:24-46, 176-197`) contains four additional fields the skill body needs: - - `status` ∈ {`pending`, `answered`, `completed`, `aborted`, `error`} — the **loop predicate** the skill body must inspect to decide whether to render another decision, exit cleanly, or surface an error. SKILL.md's documented termination condition ("Repeat until the driver reports `pending_hitl.decision == null`") happens to work for the completed case (decision is None on StopIteration) but gives the skill body no way to differentiate clean completion from error or operator-abort. - - `result` — the generator's return value (analysis path on completion, abort diagnostic on `_PreflightAborted`). - - `error` — diagnostic string when the driver hit an internal failure (exit 1). - - `answer_log` — the operator's accumulated answer history, central to the replay-style cross-process state recovery the driver documents in lines 53-69. SKILL.md's "How the flattened bridge works" section is silent on `answer_log`'s role even though future readers (and the slice-3 daemon's review) will need to know it's part of the cross-bridge contract. - - Fix: copy the full envelope schema (with field-by-field docstrings) from the driver's top-of-file comment into SKILL.md so the two surfaces stay in sync, and update the loop-termination text to read `status` instead of (or in addition to) `decision == null`. - -3. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:67, 80, 132-133` — no documented mechanism for the skill body to write `pending_hitl.answer` back to the contract file.** Steps 4-5 and the "skill loop" pseudocode all wave at "write the operator's selected option back to `pending_hitl.answer`" but the SKILL.md frontmatter `allowed-tools` does NOT include `Write` (the tool that would write a file directly). The only allowed tools that can mutate the contract JSON are `Bash(python3 *:*)`, `Bash(cat:*)`, and `Bash(cp:*)`. None of those is sufficient on its own to do a JSON read-modify-write; the only path is an inline `python3 -c "..."` invocation, which is nowhere documented. Result: a skill author / future maintainer reading SKILL.md cannot construct a working invocation chain. - - Fix: either (a) ship an explicit `python3 -c "import json; ... ; json.dump(...)"` example in the "skill loop" code block so the reader sees the mechanism; (b) extend the driver to accept `--answer ""` (or `--answer-file `) and document that path; or (c) add a separate companion helper like `bin/write_answer.py ` (mirroring `bin/preflight.py`'s shape) and document its use. Option (b) is cheapest and keeps the round-trip atomic; option (c) gives the skill body a Bash-friendly verb. Either way, the doc-and-code surfaces must agree. - -### Non-blocking - -- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:88` — minor inconsistency about loop entry/exit semantics.** The "Iteration N+1" comment in the bash block reads: "the driver picks up `pending_hitl.answer`, calls `generator.send(answer)`, serialises the next yield." This describes resumption-by-send, but the driver actually uses **replay-from-start** (`bin/run_pipeline.py:258-327`): it spawns a fresh generator each invocation, calls `next()` to land on the first yield, then loops `generator.send(replay)` over the full `answer_log`. The user-facing distinction matters because replay re-runs all side effects (refiner subagent dispatch, worktree create/teardown, artifact write) on each invocation — see my coder ACK finding #1. The doc should either name "replay" explicitly or at minimum drop the "the driver picks up `pending_hitl.answer` and calls `generator.send(answer)`" framing, which suggests cheap single-step resumption. - -- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:7` — `allowed-tools` frontmatter unchanged.** The skill cannot write the contract file without explicit user/operator consent to `Bash(python3 -c …)` invocations. The acceptance criterion only requires `Bash(python3 *:*)` (which is present) but consider whether a more constrained `Bash(python3 -c *:*)` or a dedicated helper-script-only allowlist would be safer — non-blocking, but defense-in-depth. - -- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8` — header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`.** I did not verify that path exists — if it doesn't, the comment is a dangling reference. Drop the comment or add the actual k3s prompt file path the rubric was sourced from. - -- **`docs/architecture/claude-code-substrate.md:112` — minor inconsistency with the coder's actual driver.** The ADR says "On `StopIteration`, the driver clears `pending_hitl.decision` to signal the phase is complete." Confirmed: the driver does set `decision=None` on completion (`bin/run_pipeline.py:507-515`), but it also sets `status="completed"` and `result=` — the ADR's framing reads "decision-only" and inherits the same gap as SKILL.md blocking #2. Worth adding "and sets `status='completed'` with `result=`" to keep the two surfaces consistent. - -- **`docs/architecture/claude-code-substrate.md:112-113` — envelope schema fields named `version, pipeline_id, timestamp, decision, answer` only.** Same issue as SKILL.md: the cross-bridge contract is described as 5 fields but the actual stable contract is 9 fields. The slice-3 daemon variant will need to honor all 9, not just 5. Update the ADR to list the full set so slice-3's reviewer can compare against the actual driver source. - -### Acceptance criteria check - -- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed ✓; new usage section documents flattened stage-script loop ⚠ (documented but with wrong CLI args and incomplete envelope schema); `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` ✓. **Acceptance NOT met** — the loop as documented cannot run (see blocking #1, #2, #3). -- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files exist ✓; both carry valid frontmatter (`name`, `description`) ✓; both bodies open with "You are the **reviewer_refine** running on the **Claude Code substrate**" / "reviewer_agent_design" preamble ✓; both reference the same review criteria the k3s reviewer uses ✓. **Acceptance met.** -- **TASK-1-8 (ADR)**: Bridge-gap callout removed from ADR ✓; "Rollout deltas" first bullet uses ~~strikethrough~~ format for slice-1-landed items ✓; "Open empirical question (R2)" section documents the slice-5 fallback path ✓. **Acceptance met.** - -Please address the three SKILL.md blocking items and re-propose. The two rubric files and the ADR are good to go as-is; only SKILL.md needs revision. - - -````yaml -id: a0e72273-ede8-44 -phase: implement -metadata: - payload: - reason: "\nReviewed all 4 files in commit 473b109cb. The two new rubric files\ - \ (`reviewer_refine.md`, `reviewer_agent_design.md`) and the ADR rewrite (`docs/architecture/claude-code-substrate.md`)\ - \ are well-shaped and meet their acceptance criteria. **The SKILL.md update\ - \ has three blocking documentation defects** that make the \"documented usage\ - \ section cannot actually be executed\" \u2014 directly contradicting task-1-2's\ - \ acceptance bullet (\"so the loop can actually run\").\n\n### Blocking\n\n\ - 1. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:90-129` \u2014 documented `bin/run_pipeline.py`\ - \ CLI invocations use wrong flag names; the example will fail at argparse.**\ - \ The skill loop example shows:\n ```bash\n python3 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py\ - \ \\\n --pipeline-id \"issue-${ISSUE}\" \\\n --repo \"${REPO}\"\ - \ \\\n --issue \"${ISSUE}\"\n ```\n But the actual driver at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402`\ - \ accepts:\n - `pipeline_id` as a **positional** argument (no `--pipeline-id`\ - \ flag exists)\n - `--issue-number` (not `--issue`)\n\n An operator (or\ - \ future skill body) copying this invocation gets `argparse: unrecognized arguments:\ - \ --pipeline-id`. The skill loop as documented cannot run. Fix: change the documented\ - \ invocations to `python3 .../run_pipeline.py \"issue-${ISSUE}\" --repo \"${REPO}\"\ - \ --issue-number \"${ISSUE}\"` (positional pipeline_id, `--issue-number` flag),\ - \ or update the driver to accept `--pipeline-id` / `--issue` as aliases. Pick\ - \ one and make the doc match the code.\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:96-110`\ - \ \u2014 the documented `pending_hitl` envelope is incomplete; the skill body\ - \ cannot tell \"completed\" from \"error\" from \"aborted\".** SKILL.md documents\ - \ the envelope as:\n ```json\n { \"pending_hitl\": { \"version\": 1, \"\ - pipeline_id\": \"...\", \"timestamp\": \"...\", \"decision\": {...}, \"answer\"\ - : null } }\n ```\n But the driver's actual envelope shape (per `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:24-46,\ - \ 176-197`) contains four additional fields the skill body needs:\n - `status`\ - \ \u2208 {`pending`, `answered`, `completed`, `aborted`, `error`} \u2014 the\ - \ **loop predicate** the skill body must inspect to decide whether to render\ - \ another decision, exit cleanly, or surface an error. SKILL.md's documented\ - \ termination condition (\"Repeat until the driver reports `pending_hitl.decision\ - \ == null`\") happens to work for the completed case (decision is None on StopIteration)\ - \ but gives the skill body no way to differentiate clean completion from error\ - \ or operator-abort.\n - `result` \u2014 the generator's return value (analysis\ - \ path on completion, abort diagnostic on `_PreflightAborted`).\n - `error`\ - \ \u2014 diagnostic string when the driver hit an internal failure (exit 1).\n\ - \ - `answer_log` \u2014 the operator's accumulated answer history, central\ - \ to the replay-style cross-process state recovery the driver documents in lines\ - \ 53-69. SKILL.md's \"How the flattened bridge works\" section is silent on\ - \ `answer_log`'s role even though future readers (and the slice-3 daemon's review)\ - \ will need to know it's part of the cross-bridge contract.\n\n Fix: copy\ - \ the full envelope schema (with field-by-field docstrings) from the driver's\ - \ top-of-file comment into SKILL.md so the two surfaces stay in sync, and update\ - \ the loop-termination text to read `status` instead of (or in addition to)\ - \ `decision == null`.\n\n3. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:67,\ - \ 80, 132-133` \u2014 no documented mechanism for the skill body to write `pending_hitl.answer`\ - \ back to the contract file.** Steps 4-5 and the \"skill loop\" pseudocode all\ - \ wave at \"write the operator's selected option back to `pending_hitl.answer`\"\ - \ but the SKILL.md frontmatter `allowed-tools` does NOT include `Write` (the\ - \ tool that would write a file directly). The only allowed tools that can mutate\ - \ the contract JSON are `Bash(python3 *:*)`, `Bash(cat:*)`, and `Bash(cp:*)`.\ - \ None of those is sufficient on its own to do a JSON read-modify-write; the\ - \ only path is an inline `python3 -c \"...\"` invocation, which is nowhere documented.\ - \ Result: a skill author / future maintainer reading SKILL.md cannot construct\ - \ a working invocation chain.\n\n Fix: either (a) ship an explicit `python3\ - \ -c \"import json; ... ; json.dump(...)\"` example in the \"skill loop\" code\ - \ block so the reader sees the mechanism; (b) extend the driver to accept `--answer\ - \ \"\"` (or `--answer-file `) and document that path; or (c) add\ - \ a separate companion helper like `bin/write_answer.py `\ - \ (mirroring `bin/preflight.py`'s shape) and document its use. Option (b) is\ - \ cheapest and keeps the round-trip atomic; option (c) gives the skill body\ - \ a Bash-friendly verb. Either way, the doc-and-code surfaces must agree.\n\n\ - ### Non-blocking\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:88` \u2014\ - \ minor inconsistency about loop entry/exit semantics.** The \"Iteration N+1\"\ - \ comment in the bash block reads: \"the driver picks up `pending_hitl.answer`,\ - \ calls `generator.send(answer)`, serialises the next yield.\" This describes\ - \ resumption-by-send, but the driver actually uses **replay-from-start** (`bin/run_pipeline.py:258-327`):\ - \ it spawns a fresh generator each invocation, calls `next()` to land on the\ - \ first yield, then loops `generator.send(replay)` over the full `answer_log`.\ - \ The user-facing distinction matters because replay re-runs all side effects\ - \ (refiner subagent dispatch, worktree create/teardown, artifact write) on each\ - \ invocation \u2014 see my coder ACK finding #1. The doc should either name\ - \ \"replay\" explicitly or at minimum drop the \"the driver picks up `pending_hitl.answer`\ - \ and calls `generator.send(answer)`\" framing, which suggests cheap single-step\ - \ resumption.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:7` \u2014 `allowed-tools`\ - \ frontmatter unchanged.** The skill cannot write the contract file without\ - \ explicit user/operator consent to `Bash(python3 -c \u2026)` invocations. The\ - \ acceptance criterion only requires `Bash(python3 *:*)` (which is present)\ - \ but consider whether a more constrained `Bash(python3 -c *:*)` or a dedicated\ - \ helper-script-only allowlist would be safer \u2014 non-blocking, but defense-in-depth.\n\ - \n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8`\ - \ \u2014 header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md`\ - \ / `reviewer-agent-design.md`.** I did not verify that path exists \u2014 if\ - \ it doesn't, the comment is a dangling reference. Drop the comment or add the\ - \ actual k3s prompt file path the rubric was sourced from.\n\n- **`docs/architecture/claude-code-substrate.md:112`\ - \ \u2014 minor inconsistency with the coder's actual driver.** The ADR says\ - \ \"On `StopIteration`, the driver clears `pending_hitl.decision` to signal\ - \ the phase is complete.\" Confirmed: the driver does set `decision=None` on\ - \ completion (`bin/run_pipeline.py:507-515`), but it also sets `status=\"completed\"\ - ` and `result=` \u2014 the ADR's framing reads \"decision-only\" and inherits\ - \ the same gap as SKILL.md blocking #2. Worth adding \"and sets `status='completed'`\ - \ with `result=`\" to keep the two surfaces consistent.\n\n- **`docs/architecture/claude-code-substrate.md:112-113`\ - \ \u2014 envelope schema fields named `version, pipeline_id, timestamp, decision,\ - \ answer` only.** Same issue as SKILL.md: the cross-bridge contract is described\ - \ as 5 fields but the actual stable contract is 9 fields. The slice-3 daemon\ - \ variant will need to honor all 9, not just 5. Update the ADR to list the full\ - \ set so slice-3's reviewer can compare against the actual driver source.\n\n\ - ### Acceptance criteria check\n\n- **TASK-1-2 (SKILL.md)**: Walking-skeleton\ - \ callout removed \u2713; new usage section documents flattened stage-script\ - \ loop \u26A0 (documented but with wrong CLI args and incomplete envelope schema);\ - \ `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713\ - . **Acceptance NOT met** \u2014 the loop as documented cannot run (see blocking\ - \ #1, #2, #3).\n- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**:\ - \ Both files exist \u2713; both carry valid frontmatter (`name`, `description`)\ - \ \u2713; both bodies open with \"You are the **reviewer_refine** running on\ - \ the **Claude Code substrate**\" / \"reviewer_agent_design\" preamble \u2713\ - ; both reference the same review criteria the k3s reviewer uses \u2713. **Acceptance\ - \ met.**\n- **TASK-1-8 (ADR)**: Bridge-gap callout removed from ADR \u2713;\ - \ \"Rollout deltas\" first bullet uses ~~strikethrough~~ format for slice-1-landed\ - \ items \u2713; \"Open empirical question (R2)\" section documents the slice-5\ - \ fallback path \u2713. **Acceptance met.**\n\nPlease address the three SKILL.md\ - \ blocking items and re-propose. The two rubric files and the ADR are good to\ - \ go as-is; only SKILL.md needs revision.\n" - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md - - docs/architecture/claude-code-substrate.md - nack_version: 1 - reason: "\nReviewed all 4 files in commit 473b109cb. The two new rubric files (`reviewer_refine.md`,\ - \ `reviewer_agent_design.md`) and the ADR rewrite (`docs/architecture/claude-code-substrate.md`)\ - \ are well-shaped and meet their acceptance criteria. **The SKILL.md update has\ - \ three blocking documentation defects** that make the \"documented usage section\ - \ cannot actually be executed\" \u2014 directly contradicting task-1-2's acceptance\ - \ bullet (\"so the loop can actually run\").\n\n### Blocking\n\n1. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:90-129`\ - \ \u2014 documented `bin/run_pipeline.py` CLI invocations use wrong flag names;\ - \ the example will fail at argparse.** The skill loop example shows:\n ```bash\n\ - \ python3 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py \\\n --pipeline-id\ - \ \"issue-${ISSUE}\" \\\n --repo \"${REPO}\" \\\n --issue \"${ISSUE}\"\ - \n ```\n But the actual driver at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402`\ - \ accepts:\n - `pipeline_id` as a **positional** argument (no `--pipeline-id`\ - \ flag exists)\n - `--issue-number` (not `--issue`)\n\n An operator (or future\ - \ skill body) copying this invocation gets `argparse: unrecognized arguments:\ - \ --pipeline-id`. The skill loop as documented cannot run. Fix: change the documented\ - \ invocations to `python3 .../run_pipeline.py \"issue-${ISSUE}\" --repo \"${REPO}\"\ - \ --issue-number \"${ISSUE}\"` (positional pipeline_id, `--issue-number` flag),\ - \ or update the driver to accept `--pipeline-id` / `--issue` as aliases. Pick\ - \ one and make the doc match the code.\n\n2. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:96-110`\ - \ \u2014 the documented `pending_hitl` envelope is incomplete; the skill body\ - \ cannot tell \"completed\" from \"error\" from \"aborted\".** SKILL.md documents\ - \ the envelope as:\n ```json\n { \"pending_hitl\": { \"version\": 1, \"pipeline_id\"\ - : \"...\", \"timestamp\": \"...\", \"decision\": {...}, \"answer\": null } }\n\ - \ ```\n But the driver's actual envelope shape (per `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:24-46,\ - \ 176-197`) contains four additional fields the skill body needs:\n - `status`\ - \ \u2208 {`pending`, `answered`, `completed`, `aborted`, `error`} \u2014 the **loop\ - \ predicate** the skill body must inspect to decide whether to render another\ - \ decision, exit cleanly, or surface an error. SKILL.md's documented termination\ - \ condition (\"Repeat until the driver reports `pending_hitl.decision == null`\"\ - ) happens to work for the completed case (decision is None on StopIteration) but\ - \ gives the skill body no way to differentiate clean completion from error or\ - \ operator-abort.\n - `result` \u2014 the generator's return value (analysis\ - \ path on completion, abort diagnostic on `_PreflightAborted`).\n - `error`\ - \ \u2014 diagnostic string when the driver hit an internal failure (exit 1).\n\ - \ - `answer_log` \u2014 the operator's accumulated answer history, central to\ - \ the replay-style cross-process state recovery the driver documents in lines\ - \ 53-69. SKILL.md's \"How the flattened bridge works\" section is silent on `answer_log`'s\ - \ role even though future readers (and the slice-3 daemon's review) will need\ - \ to know it's part of the cross-bridge contract.\n\n Fix: copy the full envelope\ - \ schema (with field-by-field docstrings) from the driver's top-of-file comment\ - \ into SKILL.md so the two surfaces stay in sync, and update the loop-termination\ - \ text to read `status` instead of (or in addition to) `decision == null`.\n\n\ - 3. **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:67, 80, 132-133` \u2014 no documented\ - \ mechanism for the skill body to write `pending_hitl.answer` back to the contract\ - \ file.** Steps 4-5 and the \"skill loop\" pseudocode all wave at \"write the\ - \ operator's selected option back to `pending_hitl.answer`\" but the SKILL.md\ - \ frontmatter `allowed-tools` does NOT include `Write` (the tool that would write\ - \ a file directly). The only allowed tools that can mutate the contract JSON are\ - \ `Bash(python3 *:*)`, `Bash(cat:*)`, and `Bash(cp:*)`. None of those is sufficient\ - \ on its own to do a JSON read-modify-write; the only path is an inline `python3\ - \ -c \"...\"` invocation, which is nowhere documented. Result: a skill author\ - \ / future maintainer reading SKILL.md cannot construct a working invocation chain.\n\ - \n Fix: either (a) ship an explicit `python3 -c \"import json; ... ; json.dump(...)\"\ - ` example in the \"skill loop\" code block so the reader sees the mechanism; (b)\ - \ extend the driver to accept `--answer \"\"` (or `--answer-file `)\ - \ and document that path; or (c) add a separate companion helper like `bin/write_answer.py\ - \ ` (mirroring `bin/preflight.py`'s shape) and document\ - \ its use. Option (b) is cheapest and keeps the round-trip atomic; option (c)\ - \ gives the skill body a Bash-friendly verb. Either way, the doc-and-code surfaces\ - \ must agree.\n\n### Non-blocking\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:88`\ - \ \u2014 minor inconsistency about loop entry/exit semantics.** The \"Iteration\ - \ N+1\" comment in the bash block reads: \"the driver picks up `pending_hitl.answer`,\ - \ calls `generator.send(answer)`, serialises the next yield.\" This describes\ - \ resumption-by-send, but the driver actually uses **replay-from-start** (`bin/run_pipeline.py:258-327`):\ - \ it spawns a fresh generator each invocation, calls `next()` to land on the first\ - \ yield, then loops `generator.send(replay)` over the full `answer_log`. The user-facing\ - \ distinction matters because replay re-runs all side effects (refiner subagent\ - \ dispatch, worktree create/teardown, artifact write) on each invocation \u2014\ - \ see my coder ACK finding #1. The doc should either name \"replay\" explicitly\ - \ or at minimum drop the \"the driver picks up `pending_hitl.answer` and calls\ - \ `generator.send(answer)`\" framing, which suggests cheap single-step resumption.\n\ - \n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:7` \u2014 `allowed-tools` frontmatter\ - \ unchanged.** The skill cannot write the contract file without explicit user/operator\ - \ consent to `Bash(python3 -c \u2026)` invocations. The acceptance criterion only\ - \ requires `Bash(python3 *:*)` (which is present) but consider whether a more\ - \ constrained `Bash(python3 -c *:*)` or a dedicated helper-script-only allowlist\ - \ would be safer \u2014 non-blocking, but defense-in-depth.\n\n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8`\ - \ & `reviewer_agent_design.md:8` \u2014 header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md`\ - \ / `reviewer-agent-design.md`.** I did not verify that path exists \u2014 if\ - \ it doesn't, the comment is a dangling reference. Drop the comment or add the\ - \ actual k3s prompt file path the rubric was sourced from.\n\n- **`docs/architecture/claude-code-substrate.md:112`\ - \ \u2014 minor inconsistency with the coder's actual driver.** The ADR says \"\ - On `StopIteration`, the driver clears `pending_hitl.decision` to signal the phase\ - \ is complete.\" Confirmed: the driver does set `decision=None` on completion\ - \ (`bin/run_pipeline.py:507-515`), but it also sets `status=\"completed\"` and\ - \ `result=` \u2014 the ADR's framing reads \"decision-only\" and inherits\ - \ the same gap as SKILL.md blocking #2. Worth adding \"and sets `status='completed'`\ - \ with `result=`\" to keep the two surfaces consistent.\n\n- **`docs/architecture/claude-code-substrate.md:112-113`\ - \ \u2014 envelope schema fields named `version, pipeline_id, timestamp, decision,\ - \ answer` only.** Same issue as SKILL.md: the cross-bridge contract is described\ - \ as 5 fields but the actual stable contract is 9 fields. The slice-3 daemon variant\ - \ will need to honor all 9, not just 5. Update the ADR to list the full set so\ - \ slice-3's reviewer can compare against the actual driver source.\n\n### Acceptance\ - \ criteria check\n\n- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed\ - \ \u2713; new usage section documents flattened stage-script loop \u26A0 (documented\ - \ but with wrong CLI args and incomplete envelope schema); `allowed-tools` includes\ - \ `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713. **Acceptance NOT met**\ - \ \u2014 the loop as documented cannot run (see blocking #1, #2, #3).\n- **TASK-1-4\ - \ (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files exist \u2713\ - ; both carry valid frontmatter (`name`, `description`) \u2713; both bodies open\ - \ with \"You are the **reviewer_refine** running on the **Claude Code substrate**\"\ - \ / \"reviewer_agent_design\" preamble \u2713; both reference the same review\ - \ criteria the k3s reviewer uses \u2713. **Acceptance met.**\n- **TASK-1-8 (ADR)**:\ - \ Bridge-gap callout removed from ADR \u2713; \"Rollout deltas\" first bullet\ - \ uses ~~strikethrough~~ format for slice-1-landed items \u2713; \"Open empirical\ - \ question (R2)\" section documents the slice-5 fallback path \u2713. **Acceptance\ - \ met.**\n\nPlease address the three SKILL.md blocking items and re-propose. The\ - \ two rubric files and the ADR are good to go as-is; only SKILL.md needs revision.\n" - revision_count: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:11:16Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: bdd907ac-9e11-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:11:19Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1308cf73-5bca-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:11:23Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: b1200264-5c63-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:23.268376+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:11:32Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: aeabc58c-ce55-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:11:32.544708+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:11:39Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c124654c-67a3-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:12:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: be5bf453-439a-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:01.880478+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:12:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: da6bca70-8497-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:12:19Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bd306bca-1f06-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:12:23Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 9e67be94-8a64-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:23.268376+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:12:32Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 28727a22-014f-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:11:32.544708+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:12:39Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 90dd97b1-d406-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:13:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7763b7a2-934f-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:01.880478+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:13:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f0f5be5b-a1d4-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:13:19Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: dc3bb88f-1723-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:13:23Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a2db82d6-81ad-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:23.268376+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:13:32Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7db5726b-3f28-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:11:32.544708+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:13:39Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f58ac214-54d6-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:14:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 27cbaae3-676b-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:01.880478+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:14:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 884c0224-924d-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:14:19Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c35a53de-102f-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:03:03.912729+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:14:23Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 9f190e30-4a28-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:23.268376+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:14:32Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 48b88c63-53bc-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:11:32.544708+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:14:39Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8abebd35-4b17-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:06:38.269267+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:12Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e6b1f181-aef9-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:10:01.880478+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d749bc80-61d8-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:01:58.994144+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:17Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: b7a23062-7df0-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:15:17Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 98dc3fb3-9545-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:15:18Z] documenter (CONSENSUS_PROPOSE): Proposal from documenter - -Re-propose v2 of slice-1 documenter half. Addresses reviewer_code v1 NACK (commit cf8f71a93) on SKILL.md envelope + CLI documentation. Blocker fixes: (1) CLI invocation example rewritten to use the driver's actual argparse signature — positional pipeline_id + --issue-number / --repo flags (verified against bin/run_pipeline.py:355-402); (2) pending_hitl envelope expanded from 5 fields to the full 9-field schema (adds status, result, error, answer_log) with per-field semantics including the status loop-predicate values (pending/answered/completed/aborted/error); (3) inline python3 -c "..." mechanism for writing pending_hitl.answer documented in the skill-loop code block, covered by the existing Bash(python3 *:*) allowed-tool — no new tool permission needed. Non-blocking polish: replay semantics now named explicitly (driver spawns fresh generator and replays answer_log every call; side effects re-run every invocation; slice-3 daemon variant is the escape hatch for implement-phase concurrency); failure-mode diagnostic bullet replaced with the more general status ∈ {completed, aborted, error} predicate so each terminal state has guidance. ADR additions: flattened-bridge bullet now names replay path + 9-field shape, daemon-variant bullet enumerates the same 9 fields, source-of-truth pointer added to bin/run_pipeline.py:20-46. Rubric files (reviewer_refine.md, reviewer_agent_design.md) unchanged — reviewer_code ACKed them in v1. - -````yaml -id: 539388f6-7912-42 -phase: implement -metadata: - payload: - summary: "Re-propose v2 of slice-1 documenter half. Addresses reviewer_code v1\ - \ NACK (commit cf8f71a93) on SKILL.md envelope + CLI documentation. Blocker\ - \ fixes: (1) CLI invocation example rewritten to use the driver's actual argparse\ - \ signature \u2014 positional pipeline_id + --issue-number / --repo flags (verified\ - \ against bin/run_pipeline.py:355-402); (2) pending_hitl envelope expanded from\ - \ 5 fields to the full 9-field schema (adds status, result, error, answer_log)\ - \ with per-field semantics including the status loop-predicate values (pending/answered/completed/aborted/error);\ - \ (3) inline python3 -c \"...\" mechanism for writing pending_hitl.answer documented\ - \ in the skill-loop code block, covered by the existing Bash(python3 *:*) allowed-tool\ - \ \u2014 no new tool permission needed. Non-blocking polish: replay semantics\ - \ now named explicitly (driver spawns fresh generator and replays answer_log\ - \ every call; side effects re-run every invocation; slice-3 daemon variant is\ - \ the escape hatch for implement-phase concurrency); failure-mode diagnostic\ - \ bullet replaced with the more general status \u2208 {completed, aborted, error}\ - \ predicate so each terminal state has guidance. ADR additions: flattened-bridge\ - \ bullet now names replay path + 9-field shape, daemon-variant bullet enumerates\ - \ the same 9 fields, source-of-truth pointer added to bin/run_pipeline.py:20-46.\ - \ Rubric files (reviewer_refine.md, reviewer_agent_design.md) unchanged \u2014\ - \ reviewer_code ACKed them in v1." - attestation: {} - artifacts: - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - docs/architecture/claude-code-substrate.md - risk_considered: "v2 risk surface narrows to SKILL.md correctness against the\ - \ actual driver source. Re-verified: (a) argparse signature at bin/run_pipeline.py:355-402\ - \ \u2014 pipeline_id positional, --issue-number int flag, --repo str flag, --issue-body\ - \ / --state-root / --daemon optional; the SKILL.md example uses the matching\ - \ shape so copy-paste runs. (b) envelope schema mirrored verbatim from the driver's\ - \ module docstring at bin/run_pipeline.py:20-46 \u2014 9 fields with the same\ - \ names and semantics; future drift catches in one place because the SKILL.md\ - \ \"How the flattened bridge works\" section explicitly cites that line range\ - \ as the source of truth. (c) answer-writing mechanism uses Bash(python3 *:*)\ - \ which is already on the frontmatter allowed-tools line; no allowed-tools change\ - \ needed; the inline python3 -c snippet is JSON-shape-correct. (d) replay-semantics\ - \ framing matches the driver's docstring at bin/run_pipeline.py:53-69 and the\ - \ implementation at :258-327 (loops generator.send(replay) over answer_log).\ - \ The ADR's 9-field enumeration is consistent with SKILL.md's enumeration; the\ - \ slice-3 daemon variant in TASK-3-2 will consume the same shape per the risk_analyst\ - \ R17 mitigation." - commit_sha: cf8f71a938a0426392b8db69f0798a5b7d4f4b65 - files_changed: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - docs/architecture/claude-code-substrate.md - tests_run: [] - tasks_satisfied: - - TASK-1-2 - - TASK-1-4 - - TASK-1-8 - version: 2 - commit_sha: cf8f71a938a0426392b8db69f0798a5b7d4f4b65 - slice_id: slice-1 -```` - -### [2026-05-19T19:15:18Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 0859c364-4bec-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:15:18Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 91c2ba0c-4ddc-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:15:18Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6e6d6d45-85ac-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:15:18Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 9808e540-2591-41 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 49978527-bd34-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:22.331532+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 78e1db98-35a7-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e4489055-433c-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: ed905ad0-7b4a-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: c27dacd5-9f30-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 9fac7c01-e3a2-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.991636+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f945bb42-7d8d-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:15:40Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: abc07178-6a49-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:16:05Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 0ec02a91-3781-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:47.377178+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:16:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 44444d94-8aac-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:35.464372+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:16:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3194673c-492a-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:16:38Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: f5a35fd6-2327-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:16:39Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 96024cba-af32-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:16:39Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: ebbb6f5c-b129-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:16:42Z] reviewer_code → documenter (CONSENSUS_ACK): ACK from reviewer_code for documenter - - -Reviewed commit cf8f71a93 (documenter v2). All three v1 blockers are resolved cleanly and the four non-blocking polish items from v1 are also addressed in both SKILL.md and the ADR. The two rubric files are unchanged (they ACKed in v1) — verified no regression. The skill loop example as documented can now actually run. - -### Verification of v1 blocking fixes - -1. **Blocking #1 (CLI args mismatch) — FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:134-137` now invokes the driver with `"${PIPELINE_ID}"` as a positional argument plus `--repo "${REPO}" --issue-number "${ISSUE}"`, matching the driver's argparse signature at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402`. The bash comment at line 131-133 explicitly points readers to the driver's argparse range. Doc-and-code surfaces agree. - -2. **Blocking #2 (incomplete envelope schema) — FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:75-94` now documents the full 9-field envelope (`version`, `pipeline_id`, `timestamp`, `decision`, `answer`, `status`, `result`, `error`, `answer_log`), followed by per-field semantics at lines 96-111. The `status` field is named as **the skill's loop predicate** with all five enum values (`pending`, `answered`, `completed`, `aborted`, `error`) documented. The schema source-of-truth pointer to `bin/run_pipeline.py:20-46` at line 73 keeps the two surfaces in sync. - -3. **Blocking #3 (missing write mechanism) — FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:148-157` shows an inline `python3 -c "..."` snippet that reads the contract, mutates `pending_hitl.answer` + `status`, and writes back. The text at line 173 explicitly names the `Bash(python3 *:*)` allowed-tool as the permission backing this approach and notes that the option-(b) `--answer` flag / option-(c) `bin/write_answer.py` helper are reserved if the inline shape proves awkward. The skill body now has a documented, executable mechanism. - -### Verification of v1 non-blocking polish - -- **Replay semantics — addressed.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:115-119` has a dedicated "Generator state across invocations (replay semantics)" subsection naming "replay" explicitly, documenting that side effects (refiner subagent dispatch, worktree create/teardown, artifact write) re-run on every invocation, and pointing at slice 3 as the daemon-variant escape hatch. The ADR at `docs/architecture/claude-code-substrate.md:112` also names the replay path with the deterministic-same-yield-boundary property. -- **ADR envelope schema — addressed.** `docs/architecture/claude-code-substrate.md:112-115` now enumerates all 9 fields and points at `bin/run_pipeline.py:20-46` as the source of truth. -- **Failure-mode diagnostic guidance — addressed.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:262` replaces the old "decision == null" terminus with a status-based bullet that documents what `pending_hitl.result` / `pending_hitl.error` mean for each terminal state. -- **Rubric files** — unchanged (correctly noted in commit msg as ACKed-in-v1, no churn). - -### Non-blocking observations (v2) - -These are not regressions from v1; they are minor polish items I noticed while verifying the v2 diff. Not blocking. - -- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:155` — `datetime.utcnow()` is deprecated.** The example uses `datetime.datetime.utcnow().isoformat() + 'Z'`, which Python ≥3.12 deprecates in favor of `datetime.datetime.now(datetime.UTC).isoformat()`. The driver's `_now_iso()` at `bin/run_pipeline.py:101-103` uses the modern form. Skill body Python 3.11 will work; Python 3.12+ will print a DeprecationWarning. Cheapest fix: change the example to `datetime.datetime.now(datetime.UTC).isoformat()`. -- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:153` — `${ANSWER}` shell substitution is unsanitized.** The comment `# operator's selection; JSON-encode appropriately` acknowledges this, but a future skill body author who copy-pastes the example and forgets to JSON-encode will produce a Python NameError (if ANSWER is a bare string like `approve`) or a shell-injection vector (if ANSWER contains backticks, double quotes, or `$(…)`). Consider replacing the example with a JSON-via-stdin pattern (`echo "${ANSWER_JSON}" | python3 -c "import json,sys; …"`) so the substitution boundary is shell→stdin rather than shell→python source. -- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8` & `reviewer_agent_design.md:8`** — header comment claims layout mirrors `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`. These paths still need verification (or the comment needs to point at the actual k3s prompt sources the rubric was sourced from). Carried over from v1 non-blocking; not a blocker. - -### Acceptance criteria check (final) - -- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed ✓; new usage section documents the flattened stage-script loop with correct CLI args, full envelope schema, and documented write mechanism ✓; `allowed-tools` includes `AskUserQuestion`, `Read`, `Bash(python3 *:*)` ✓ — and the inline `python3 -c "..."` write path now backs the loop in practice. **Acceptance met.** -- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**: Both files unchanged from v1; acceptance previously confirmed met. -- **TASK-1-8 (ADR)**: Updated to mirror SKILL.md's 9-field envelope and replay semantics; previously confirmed met, still met. - - -````yaml -id: d28966be-df70-45 -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md - - docs/architecture/claude-code-substrate.md - reason: "\nReviewed commit cf8f71a93 (documenter v2). All three v1 blockers are\ - \ resolved cleanly and the four non-blocking polish items from v1 are also addressed\ - \ in both SKILL.md and the ADR. The two rubric files are unchanged (they ACKed\ - \ in v1) \u2014 verified no regression. The skill loop example as documented\ - \ can now actually run.\n\n### Verification of v1 blocking fixes\n\n1. **Blocking\ - \ #1 (CLI args mismatch) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:134-137`\ - \ now invokes the driver with `\"${PIPELINE_ID}\"` as a positional argument\ - \ plus `--repo \"${REPO}\" --issue-number \"${ISSUE}\"`, matching the driver's\ - \ argparse signature at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402`.\ - \ The bash comment at line 131-133 explicitly points readers to the driver's\ - \ argparse range. Doc-and-code surfaces agree.\n\n2. **Blocking #2 (incomplete\ - \ envelope schema) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:75-94`\ - \ now documents the full 9-field envelope (`version`, `pipeline_id`, `timestamp`,\ - \ `decision`, `answer`, `status`, `result`, `error`, `answer_log`), followed\ - \ by per-field semantics at lines 96-111. The `status` field is named as **the\ - \ skill's loop predicate** with all five enum values (`pending`, `answered`,\ - \ `completed`, `aborted`, `error`) documented. The schema source-of-truth pointer\ - \ to `bin/run_pipeline.py:20-46` at line 73 keeps the two surfaces in sync.\n\ - \n3. **Blocking #3 (missing write mechanism) \u2014 FIXED.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:148-157`\ - \ shows an inline `python3 -c \"...\"` snippet that reads the contract, mutates\ - \ `pending_hitl.answer` + `status`, and writes back. The text at line 173 explicitly\ - \ names the `Bash(python3 *:*)` allowed-tool as the permission backing this\ - \ approach and notes that the option-(b) `--answer` flag / option-(c) `bin/write_answer.py`\ - \ helper are reserved if the inline shape proves awkward. The skill body now\ - \ has a documented, executable mechanism.\n\n### Verification of v1 non-blocking\ - \ polish\n\n- **Replay semantics \u2014 addressed.** `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:115-119`\ - \ has a dedicated \"Generator state across invocations (replay semantics)\"\ - \ subsection naming \"replay\" explicitly, documenting that side effects (refiner\ - \ subagent dispatch, worktree create/teardown, artifact write) re-run on every\ - \ invocation, and pointing at slice 3 as the daemon-variant escape hatch. The\ - \ ADR at `docs/architecture/claude-code-substrate.md:112` also names the replay\ - \ path with the deterministic-same-yield-boundary property.\n- **ADR envelope\ - \ schema \u2014 addressed.** `docs/architecture/claude-code-substrate.md:112-115`\ - \ now enumerates all 9 fields and points at `bin/run_pipeline.py:20-46` as the\ - \ source of truth.\n- **Failure-mode diagnostic guidance \u2014 addressed.**\ - \ `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:262` replaces the old \"decision\ - \ == null\" terminus with a status-based bullet that documents what `pending_hitl.result`\ - \ / `pending_hitl.error` mean for each terminal state.\n- **Rubric files** \u2014\ - \ unchanged (correctly noted in commit msg as ACKed-in-v1, no churn).\n\n###\ - \ Non-blocking observations (v2)\n\nThese are not regressions from v1; they\ - \ are minor polish items I noticed while verifying the v2 diff. Not blocking.\n\ - \n- **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:155` \u2014 `datetime.utcnow()`\ - \ is deprecated.** The example uses `datetime.datetime.utcnow().isoformat()\ - \ + 'Z'`, which Python \u22653.12 deprecates in favor of `datetime.datetime.now(datetime.UTC).isoformat()`.\ - \ The driver's `_now_iso()` at `bin/run_pipeline.py:101-103` uses the modern\ - \ form. Skill body Python 3.11 will work; Python 3.12+ will print a DeprecationWarning.\ - \ Cheapest fix: change the example to `datetime.datetime.now(datetime.UTC).isoformat()`.\n\ - - **`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:153` \u2014 `${ANSWER}` shell\ - \ substitution is unsanitized.** The comment `# operator's selection; JSON-encode\ - \ appropriately` acknowledges this, but a future skill body author who copy-pastes\ - \ the example and forgets to JSON-encode will produce a Python NameError (if\ - \ ANSWER is a bare string like `approve`) or a shell-injection vector (if ANSWER\ - \ contains backticks, double quotes, or `$(\u2026)`). Consider replacing the\ - \ example with a JSON-via-stdin pattern (`echo \"${ANSWER_JSON}\" | python3\ - \ -c \"import json,sys; \u2026\"`) so the substitution boundary is shell\u2192\ - stdin rather than shell\u2192python source.\n- **`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md:8`\ - \ & `reviewer_agent_design.md:8`** \u2014 header comment claims layout mirrors\ - \ `plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md` / `reviewer-agent-design.md`.\ - \ These paths still need verification (or the comment needs to point at the\ - \ actual k3s prompt sources the rubric was sourced from). Carried over from\ - \ v1 non-blocking; not a blocker.\n\n### Acceptance criteria check (final)\n\ - \n- **TASK-1-2 (SKILL.md)**: Walking-skeleton callout removed \u2713; new usage\ - \ section documents the flattened stage-script loop with correct CLI args, full\ - \ envelope schema, and documented write mechanism \u2713; `allowed-tools` includes\ - \ `AskUserQuestion`, `Read`, `Bash(python3 *:*)` \u2713 \u2014 and the inline\ - \ `python3 -c \"...\"` write path now backs the loop in practice. **Acceptance\ - \ met.**\n- **TASK-1-4 (`reviewer_refine.md`, `reviewer_agent_design.md`)**:\ - \ Both files unchanged from v1; acceptance previously confirmed met.\n- **TASK-1-8\ - \ (ADR)**: Updated to mirror SKILL.md's 9-field envelope and replay semantics;\ - \ previously confirmed met, still met.\n" - ack_version: 2 - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:16:42Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 556198be-53ae-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:16:54Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 6aa24858-0204-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:17:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8b825fe3-ba0e-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:17:03.569885+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:17:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 39f9f3ff-8ad0-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:35.464372+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:17:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 71cd3e97-e0ee-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:17:39Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5ea455c1-193b-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:17:39Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 13ff5ea9-c33a-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:17:39Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 543ddb37-c531-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:17:54Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 66295bf2-30b9-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:18:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 6d8bb72e-d7cc-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:17:03.569885+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:18:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: d5f296d4-2832-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:35.464372+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:18:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f5a020d9-4eb0-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:18:39Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c5347d81-9f1f-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:18:39Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 298396e9-cab7-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:18:39Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 32e0f6d4-b38b-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:18:54Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0a809cf9-a423-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:19:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c2b2c082-0f00-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:17:03.569885+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:19:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 1c80905c-be14-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:35.464372+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:19:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9b64fdfc-618b-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:19:39Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 03719bac-2caa-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:19:39Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 98cbc3c2-53f1-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:19:39Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: c3dc1d20-ed15-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:19:54Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7fa01948-db4d-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:20:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 86062cbe-cd07-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:17:03.569885+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:20:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: f8a18f73-4742-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:35.464372+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:20:39Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7205e89f-807c-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:20:39Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 27ce5714-9180-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:20:39Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 25bcfd2a-9806-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:20:39Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: d65359a4-8ce7-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:20:54Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f16ecb84-baf4-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:21:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 0774021a-cbec-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:17:03.569885+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:21:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: f346b117-92cb-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:35.464372+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:21:39Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d5f0c745-e525-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:21:39Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 1a2baa26-2453-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:21:39Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: ac7006cc-0041-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:21:39Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 64d07de2-fc96-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:21:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e54aee40-fe2b-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:22:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5f5b8937-6dbe-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:17:03.569885+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:22:52Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8536debe-3d91-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.883899+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:22:52Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 15c12ce8-98bd-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:22:52Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: d3b0228e-f9a8-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:35.464372+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:22:52Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 24f40c2d-28b8-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:22:52Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 629d22cf-17ad-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:22:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9e3c9c72-7880-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:23:04Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: f9cc50e4-5b9e-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:17:03.569885+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:23:40Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 93904c9a-9ebf-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:23:40Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: bea87ee3-1981-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:23:40Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c07c64c1-ee74-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:23:52Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: cec31022-9cd4-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:23:52Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 719ef606-13b9-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:23:52Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 12246267-781a-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:23:52Z] coder (HEARTBEAT): heartbeat: WAITING_ON_ROLE - -Still alive at 19:24Z; all 6 reviewers ACKed coder v1 at fddeb82bf; confirm() blocked on tester producer (version=0, silent since 18:57Z per overseer). Coder cannot bypass another role's PROPOSE — staying alive on wait-loop until operator intervenes or tester recovers. - -````yaml -id: f2dbd5ce-9234-4e -phase: implement -metadata: - state: WAITING_ON_ROLE - waiting_on: tester - slice_id: slice-1 -```` - -### [2026-05-19T19:23:52Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_CONFIRMED - -````yaml -id: ca875c3e-a2e8-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:23:52.921876+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:23:53Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 9a645316-30d0-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:23:54Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_CONFIRMED - -````yaml -id: 3bc57161-8e14-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:23:54.220802+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:23:54Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 4d3a9ea3-cb6a-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:23:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b86d286c-3a83-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:24:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: ac8d6e27-a4c2-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:01.981502+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:24:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 9d0fc57c-85db-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:03.172641+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:24:42Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 413a7b34-478d-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:42.695477+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:24:52Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f4c0ad68-95ef-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:24:52Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 64c614ea-0138-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:24:52Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: fd562de6-0b38-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:24:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b3afcfd2-53e5-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:25:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: ddc6f9a8-4422-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:01.981502+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:25:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: a36b9580-87f3-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:03.172641+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:25:42Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: be3a378e-2a86-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:42.695477+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:25:52Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7dee3c93-ede3-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:25:52Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b006033f-e05f-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:25:52Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 7d2d2d58-4bc5-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:25:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 122b280f-997d-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:26:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 4af5572b-535d-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:01.981502+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:26:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: a6fa5cda-1b2f-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:03.172641+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:26:43Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5064b2d2-e97d-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:42.695477+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:26:52Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7694fb67-3736-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:26:52Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 4b84a84c-0243-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:26:52Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 9f7ffe19-1c31-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:26:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c7fb7443-2596-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:27:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 4ddeb369-4cff-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:01.981502+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:27:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 5eac0d47-6517-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:03.172641+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:27:43Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 11462ce4-50f7-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:42.695477+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:27:52Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b3d408ca-bad6-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:27:52Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9cef1383-214d-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:27:52Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 58f1c303-fbc8-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:27:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e8a47442-fb1f-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:28:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: c2c7efbc-970d-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:01.981502+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:28:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: c04dca36-8e47-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:03.172641+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:28:43Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 3c6eb42d-dfc9-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:42.695477+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:28:53Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bd0ed6a2-f6f3-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:23.792278+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:28:53Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f6097a6e-8ace-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.240783+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:28:53Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: c75cf778-477f-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:15:24.337511+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:28:55Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: ab1e65b5-c892-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:16:54.661621+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:29:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 32ac5c18-6ba3-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:01.981502+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:29:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 3e650ed3-bf65-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:24:03.172641+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] tester (CONSENSUS_PROPOSE): Proposal from tester - -slice-1 tester: adversarial test coverage for the bridge driver (task-1-3), R2 nested-dispatch verdict (task-1-5), and rubric loader expansion (task-1-7). Three test files committed, 17/17 tester-authored tests passing. - -TASK-1-3 (`integration_tests/regression/test_bridge_flattened_round_trip.py`): two-stage round-trip exercising the coder's flattened bridge driver (`bin/run_pipeline.py`) end-to-end in a fresh subprocess per invocation. Stage A captures the preflight HITLDecision into `pending_hitl.decision`; the test writes `answer="approve" + status="answered"`; Stage B re-enters a fresh process, replays the answer through `answer_log`, and lands on the refine-gate decision. Substrate isolation via a `python -c` shim that monkey-patches `orchestrator.substrate.select_substrate` to a MagicMock bundle so no real Claude Code / Anthropic API call is made (mirrors the existing `fake_bundle` fixture in `test_run_pipeline_in_process_sentinel_and_hitl.py`). Adds a driver-idempotency probe: a re-invocation without a new answer must NOT silently advance the generator (HITL safety invariant). - -TASK-1-5 (`integration_tests/regression/test_pretooluse_hook_nested.py`): cq-5 early-spike R2 verdict test using task-1-9's `_agent_tool_fake.dispatch(...)`. Primary case asserts parent=architect + child=tester + write_target=`orchestrator/foo.py` resolves to `decision=block` with a `tester`-naming reason — proving the hook resolves the *child's* role correctly, not the parent's. Cross-role probe (parent=coder, child=tester writing orchestrator/*) — denial reason must still name `tester` so a parent-side fallback is detectable. In-role negative-control (tester writing `integration_tests/regression/`) — must NOT be denied. EGG_AGENT_ROLE leak guard — the fake must not mutate the parent process's env. R2 verdict written to `.egg-state//r2-verdict.json` per AC. Docstring documents the empirical-vs-test-fake limitation that cq-3 explicitly accepts. - -TASK-1-7 (`shared/tests/test_rubric_loader.py`): unit tests for `_load_egg_sdlc_role_rubric` covering all 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 bare-string input equivalence, path-traversal role-name defence, structured-error fence for unshipped plan/implement-phase roles (reviewer_plan, reviewer_code, task_planner). - -Configured-check results: -* `make lint` — exit 0. Ruff check, ruff format check, mypy, shellcheck, custom checks all green. -* `make security` — exit 0. (Bandit not installed in this sandbox; safety/trivy skipped.) -* `make test` — exit 4 (environmental issue, NOT caused by my work). Two failure modes diagnosed: (1) `grimp` module missing so `scripts/select_tests/__main__.py` falls back to full-suite; (2) the full-suite pytest invocation triggers `ImportPathMismatchError` between `tests/conftest.py` and `shared/tests/conftest.py` due to dual rootdir discovery. **The slice-1 tests themselves pass cleanly when invoked via direct pytest** with the same PYTHONPATH the Makefile sets (`shared:gateway:orchestrator` + repo root): all 17 tester-authored tests green; 1114/1114 of `shared/tests` green; the 20 pre-existing failures observed in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}` exist on origin/main with my changes git-stashed and are environment/CI plumbing failures unrelated to this slice. - -`tests_execution_blocked` reason: the literal `make test` command cannot complete in this sandbox per the diagnosis above. Direct pytest run on the tester-authored files passes 17/17. - -````yaml -id: 41b71e67-05ad-48 -phase: implement -metadata: - payload: - summary: "slice-1 tester: adversarial test coverage for the bridge driver (task-1-3),\ - \ R2 nested-dispatch verdict (task-1-5), and rubric loader expansion (task-1-7).\ - \ Three test files committed, 17/17 tester-authored tests passing.\n\nTASK-1-3\ - \ (`integration_tests/regression/test_bridge_flattened_round_trip.py`): two-stage\ - \ round-trip exercising the coder's flattened bridge driver (`bin/run_pipeline.py`)\ - \ end-to-end in a fresh subprocess per invocation. Stage A captures the preflight\ - \ HITLDecision into `pending_hitl.decision`; the test writes `answer=\"approve\"\ - \ + status=\"answered\"`; Stage B re-enters a fresh process, replays the answer\ - \ through `answer_log`, and lands on the refine-gate decision. Substrate isolation\ - \ via a `python -c` shim that monkey-patches `orchestrator.substrate.select_substrate`\ - \ to a MagicMock bundle so no real Claude Code / Anthropic API call is made\ - \ (mirrors the existing `fake_bundle` fixture in `test_run_pipeline_in_process_sentinel_and_hitl.py`).\ - \ Adds a driver-idempotency probe: a re-invocation without a new answer must\ - \ NOT silently advance the generator (HITL safety invariant).\n\nTASK-1-5 (`integration_tests/regression/test_pretooluse_hook_nested.py`):\ - \ cq-5 early-spike R2 verdict test using task-1-9's `_agent_tool_fake.dispatch(...)`.\ - \ Primary case asserts parent=architect + child=tester + write_target=`orchestrator/foo.py`\ - \ resolves to `decision=block` with a `tester`-naming reason \u2014 proving\ - \ the hook resolves the *child's* role correctly, not the parent's. Cross-role\ - \ probe (parent=coder, child=tester writing orchestrator/*) \u2014 denial reason\ - \ must still name `tester` so a parent-side fallback is detectable. In-role\ - \ negative-control (tester writing `integration_tests/regression/`) \u2014 must\ - \ NOT be denied. EGG_AGENT_ROLE leak guard \u2014 the fake must not mutate the\ - \ parent process's env. R2 verdict written to `.egg-state//r2-verdict.json`\ - \ per AC. Docstring documents the empirical-vs-test-fake limitation that cq-3\ - \ explicitly accepts.\n\nTASK-1-7 (`shared/tests/test_rubric_loader.py`): unit\ - \ tests for `_load_egg_sdlc_role_rubric` covering all 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 bare-string input equivalence, path-traversal\ - \ role-name defence, structured-error fence for unshipped plan/implement-phase\ - \ roles (reviewer_plan, reviewer_code, task_planner).\n\nConfigured-check results:\n\ - * `make lint` \u2014 exit 0. Ruff check, ruff format check, mypy, shellcheck,\ - \ custom checks all green.\n* `make security` \u2014 exit 0. (Bandit not installed\ - \ in this sandbox; safety/trivy skipped.)\n* `make test` \u2014 exit 4 (environmental\ - \ issue, NOT caused by my work). Two failure modes diagnosed: (1) `grimp` module\ - \ missing so `scripts/select_tests/__main__.py` falls back to full-suite; (2)\ - \ the full-suite pytest invocation triggers `ImportPathMismatchError` between\ - \ `tests/conftest.py` and `shared/tests/conftest.py` due to dual rootdir discovery.\ - \ **The slice-1 tests themselves pass cleanly when invoked via direct pytest**\ - \ with the same PYTHONPATH the Makefile sets (`shared:gateway:orchestrator`\ - \ + repo root): all 17 tester-authored tests green; 1114/1114 of `shared/tests`\ - \ green; the 20 pre-existing failures observed in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}`\ - \ exist on origin/main with my changes git-stashed and are environment/CI plumbing\ - \ failures unrelated to this slice.\n\n`tests_execution_blocked` reason: the\ - \ literal `make test` command cannot complete in this sandbox per the diagnosis\ - \ above. Direct pytest run on the tester-authored files passes 17/17." - attestation: - tests_execution_blocked: true - tests_execution_blocked_reason: '`make test` cannot complete in this sandbox: - (1) the `grimp` Python module required by `scripts/select_tests/__main__.py` - is not installed and pypi is unreachable (`make` cannot `uv sync` the venv); - (2) when select-tests falls back to the full-suite path, pytest''s rootdir-discovery - walks both `tests/` and `shared/tests/` and fails with `ImportPathMismatchError: - (''tests.conftest'', ''/home/egg/repos/egg/tests/conftest.py'', PosixPath(''/home/egg/repos/egg/tests/conftest.py''))` - because both directories have conftest.py. Directly invoking `pytest shared/tests/test_rubric_loader.py - integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py` - with PYTHONPATH set to `shared:gateway:orchestrator:repo_root` passes 17/17 - tester-authored tests; the broader `shared/tests` suite also passes 1114/1114. - The 20 pre-existing failures in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}` - reproduce on origin/main with my changes stashed (not caused by this slice).' - checks_passed: - - lint - - security - artifacts: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - risk_considered: "Two distinct risk vectors considered:\n\n(1) **Tester role boundary**\ - \ \u2014 all three test paths are within tester-allowed prefixes (shared/tests/,\ - \ integration_tests/regression/). No source-code changes.\n\n(2) **Test scaffolding\ - \ ordering vs producer dependencies** \u2014 task-1-7 (rubric loader) depends\ - \ on BOTH the coder's task-1-6 AND the documenter's task-1-4. I scaffolded with\ - \ post-coder-and-documenter expected behaviour and verified tests fail correctly\ - \ in the documenter-incomplete state, then re-ran after the documenter's proposal\ - \ landed (commit 473b109) and confirmed all 10 rubric tests pass.\n\n(3) **Substrate\ - \ fake isolation** \u2014 both subprocess-based tests explicitly patch `orchestrator.substrate.select_substrate`\ - \ to a MagicMock bundle so no real Claude Code / Anthropic API call is made.\ - \ Mirrors the existing `fake_bundle` fixture in `test_run_pipeline_in_process_sentinel_and_hitl.py`.\n\ - \n(4) **Pre-existing pytest-environment failures** \u2014 the 20 failures in\ - \ `test_unpushed_commit_salvage.py` / `test_recovery_invariants.py` / `test_salvage_ttl_sweep.py`\ - \ reproduce on origin/main with my changes git-stashed; they are NOT caused\ - \ by this slice.\n\n(5) **`make test` cannot run** \u2014 environmental issue\ - \ (`grimp` module unavailable; pypi unreachable; conftest collision in fallback).\ - \ `tests_execution_blocked=true` set in attestation per the agent-lifecycle\ - \ instructions. The slice-1 tests pass cleanly under direct pytest invocation." - commit_sha: 9ec45ddfe47148f3f9971ee95e6191a61daafecb - files_changed: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - tests_run: - - shared/tests/test_rubric_loader.py::test_load_refiner_rubric_regression - - shared/tests/test_rubric_loader.py::test_load_reviewer_refine_rubric - - shared/tests/test_rubric_loader.py::test_load_reviewer_agent_design_rubric - - shared/tests/test_rubric_loader.py::test_load_architect_raises_value_error_with_slice2_hint - - shared/tests/test_rubric_loader.py::test_loader_accepts_enum_and_string_role[enum-refiner] - - shared/tests/test_rubric_loader.py::test_loader_accepts_enum_and_string_role[str-refiner] - - shared/tests/test_rubric_loader.py::test_loader_still_rejects_unshipped_roles[reviewer_plan] - - shared/tests/test_rubric_loader.py::test_loader_still_rejects_unshipped_roles[reviewer_code] - - shared/tests/test_rubric_loader.py::test_loader_still_rejects_unshipped_roles[task_planner] - - shared/tests/test_rubric_loader.py::test_loader_rejects_path_traversal_role_name - - integration_tests/regression/test_bridge_flattened_round_trip.py::test_bridge_flattened_round_trip - - integration_tests/regression/test_bridge_flattened_round_trip.py::test_driver_is_idempotent_when_answer_unchanged - - integration_tests/regression/test_pretooluse_hook_nested.py::test_pretooluse_hook_denies_nested_child_write - - integration_tests/regression/test_pretooluse_hook_nested.py::test_pretooluse_hook_allows_in_role_child_write - - integration_tests/regression/test_pretooluse_hook_nested.py::test_pretooluse_hook_blocks_parent_role_with_child_write_target - - integration_tests/regression/test_pretooluse_hook_nested.py::test_dispatch_returns_structured_result - - integration_tests/regression/test_pretooluse_hook_nested.py::test_dispatch_does_not_leak_egg_agent_role_into_parent_env - tasks_satisfied: - - task-1-3 - - task-1-5 - - task-1-7 - version: 1 - commit_sha: 9ec45ddfe47148f3f9971ee95e6191a61daafecb - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] orchestrator → coder (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: af375e8c-2863-42 -phase: implement -metadata: - ready_to_confirm: true - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] orchestrator → documenter (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 2) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: cdfabbae-9d2f-4e -phase: implement -metadata: - ready_to_confirm: true - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ba4642fc-fe6d-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f0db89e8-f586-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 3f993c05-4f91-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: bbeae953-c852-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 12f3c434-0baf-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 5a0c5585-0e99-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:05Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 88c726aa-f3c9-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:27Z] documenter (CONSENSUS_CONFIRMED): Confirmed by documenter - -````yaml -id: 330c264a-7b0e-41 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-1 -```` - -### [2026-05-19T19:29:27Z] coder (CONSENSUS_CONFIRMED): Confirmed by coder - -````yaml -id: 1ea42c7a-271f-4b -phase: implement -metadata: - consensus_reached: false - slice_id: slice-1 -```` - -### [2026-05-19T19:29:52Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 7833a566-3b56-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:29:42.161550+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:29:52Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c2c448d0-9376-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:29:44.889888+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:29:52Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 62f35786-77b4-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:52Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 4cd84f79-36ae-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:29:59Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: f5c73c61-e4de-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:29:58.980493+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:29:59Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6a9eee48-d4bf-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:30:03Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: fe78ff38-5c3a-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:03.273556+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:30:03Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: 0f098e79-6ab2-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:03.810830+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:30:06Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_ACK,CONSENSUS_NACK - -````yaml -id: d47691d8-e8b3-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:06.440132+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:30:06Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 87791a2c-ad21-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:30:38Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 59a94aaf-a66f-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:20.713560+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:30:38Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE - -````yaml -id: b531c800-c503-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:21.893162+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:30:44Z] reviewer_security → tester (CONSENSUS_ACK): ACK from reviewer_security for tester - -Security-lens review of slice-1 tester proposal (3 new test files + 1 PYTHONPATH fix). - -**Scope reviewed:** subprocess invocation shape in the integration tests, env-propagation isolation, test-only fixtures that simulate the PreToolUse hook trust boundary, and adversarial probes that verify the security invariants the implementation must hold. - -**Positive security signals (what these tests pin):** - -1. **`test_rubric_loader.py:194-217` (`test_loader_rejects_path_traversal_role_name`).** Pins the path-traversal defence for the rubric loader: feeding `"../../../etc/passwd"` as a role name must raise `ValueError` rather than reach `Path.read_text()`. The loader's `_RUBRIC_LANDED_ROLES` exact-string allowlist (see `orchestrator/substrate/__init__.py:271-277, 338-350`) is the upstream defence that makes this test pass; the test is the regression fence. This is exactly the cross-file allowlist invariant the security lens cares about. - -2. **`test_pretooluse_hook_nested.py:137-216` (`test_pretooluse_hook_denies_nested_child_write`) + `:241-276` (cross-role probe).** Pins the role-resolution invariant under nested dispatch: a child fake-subagent with `EGG_AGENT_ROLE=tester` writing `orchestrator/foo.py` must be denied with a reason naming the **child** role. The cross-role probe at line 241 is the load-bearing assertion — even when the parent role would also block, the deny reason must reference the child. This catches the R2 failure mode where the hook resolves from the parent env. Strong assertion, well-targeted. - -3. **`test_pretooluse_hook_nested.py:218-238` (`test_pretooluse_hook_allows_in_role_child_write`).** Negative-control test pins the allow path so a regression that "denies everything" cannot silently pass the deny test. Important security-testing discipline; without this, the deny test alone is satisfied by a permissive bug. - -4. **`test_pretooluse_hook_nested.py:310-334` (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env`).** Verifies the fake passes env via subprocess `env=` rather than mutating `os.environ['EGG_AGENT_ROLE']` — preventing simulated-child role leakage into the test process. Without this, every subsequent test in the same process would see the leaked role and the role-routing logic could be spoofed in cross-test interactions. Good adversarial probe. - -5. **`test_rubric_loader.py:170-191` (`test_loader_still_rejects_unshipped_roles`).** Pins the structured-error fence for `REVIEWER_PLAN`, `REVIEWER_CODE`, `TASK_PLANNER` — a future change that silently drops the fence for all roles would let walking-skeleton callers get an empty fallback rubric and degrade silently. Defence-in-depth fence held. - -**Verified clean (no security findings):** - -- **Subprocess invocations.** `test_bridge_flattened_round_trip.py:187-196` uses list-form `subprocess.run([sys.executable, "-c", _shim_source()], ...)` — no shell, no agent-controlled argv. The `_shim_source()` at lines 98-146 is a static string (no f-string interpolation from test inputs); the shim reads `EGG_PIPELINE_ID` / `EGG_TEST_DRIVER_PATH` from the env that `_invoke_driver` constructs from `_PIPELINE_ID = "issue-bridge-round-trip"` (a constant, not test input). No injection surface. -- **Env construction.** `_invoke_driver` (lines 149-196) inherits `os.environ` and adds explicit PYTHONPATH segments via `os.pathsep.join([...])` with absolute paths derived from `_repo_root()`. The PYTHONPATH fix in commit 2fca7e736 correctly adds `` so `orchestrator.substrate` resolves (previously the subprocess could only import the bare `substrate` submodule). No traversal vector. -- **State isolation.** `isolated_state_dir` fixture (test_pretooluse_hook_nested.py:101-112) sandboxes `.egg-state//` under `tmp_path` so a re-run does not overwrite a real pipeline's verdict file. Correct hygiene. -- **Test fake's import guard.** Not in this diff, but the tests import via `integration_tests.regression._agent_tool_fake` which exercises the fake's `__name__.startswith("integration_tests")` allowed-prefix branch — verifying the import guard accepts the legitimate caller. Cross-file invariant between fake and tests holds. - -### Non-blocking - -- **`test_rubric_loader.py:194-217` — strengthen the path-traversal assertion.** The current test verifies `ValueError` is raised, but does not assert that no filesystem access happens before the raise. A regression where the loader called `Path(...).is_file()` on `"../../../etc/passwd.md"` (which leaks filesystem-layout information via the `is_file()` boolean — per security criteria §8's "Existence / metadata oracles") would still pass the test as long as the ValueError eventually fires. Suggested strengthening: monkeypatch `pathlib.Path.is_file` and `Path.read_text` with a sentinel that records calls, and assert neither was called with a path containing `..` or `etc`. Current allowlist guarantees the early raise, so this is purely defensive. - -- **`test_bridge_flattened_round_trip.py:108-146` — `_shim_source()` patches `_sub.select_substrate` via attribute rebinding** (`_sub.select_substrate = lambda env=None, **kw: _bundle`). Since the shim runs in a fresh subprocess, the patch dies with the process and cannot leak to other tests. No concern in this test, but if the shim shape gets adopted by other tests that share state (e.g., in-process pytest plugins), the attribute-rebind shape is brittle compared to `monkeypatch.setattr`. Document the subprocess-isolation assumption in the shim docstring so a future copy-paste into an in-process test surfaces the constraint. - -- **`test_pretooluse_hook_nested.py:120-128` — `_write_r2_verdict` writes JSON under `state_dir / pipeline_id / "r2-verdict.json"`** where `pipeline_id = "pipeline-r2-nested"` is a test constant. Acceptable today, but if a future test parametrises `pipeline_id` with attacker-derived strings, the `out_dir = state_dir / pipeline_id` path construction would inherit the path-traversal vector noted on the coder's driver review. Pin a regex or constraint on the test's pipeline_id parameter if/when it becomes parametrised. - -````yaml -id: 970eed5d-adf9-49 -phase: implement -metadata: - payload: - artifact_references: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - reason: "Security-lens review of slice-1 tester proposal (3 new test files + 1\ - \ PYTHONPATH fix).\n\n**Scope reviewed:** subprocess invocation shape in the\ - \ integration tests, env-propagation isolation, test-only fixtures that simulate\ - \ the PreToolUse hook trust boundary, and adversarial probes that verify the\ - \ security invariants the implementation must hold.\n\n**Positive security signals\ - \ (what these tests pin):**\n\n1. **`test_rubric_loader.py:194-217` (`test_loader_rejects_path_traversal_role_name`).**\ - \ Pins the path-traversal defence for the rubric loader: feeding `\"../../../etc/passwd\"\ - ` as a role name must raise `ValueError` rather than reach `Path.read_text()`.\ - \ The loader's `_RUBRIC_LANDED_ROLES` exact-string allowlist (see `orchestrator/substrate/__init__.py:271-277,\ - \ 338-350`) is the upstream defence that makes this test pass; the test is the\ - \ regression fence. This is exactly the cross-file allowlist invariant the security\ - \ lens cares about.\n\n2. **`test_pretooluse_hook_nested.py:137-216` (`test_pretooluse_hook_denies_nested_child_write`)\ - \ + `:241-276` (cross-role probe).** Pins the role-resolution invariant under\ - \ nested dispatch: a child fake-subagent with `EGG_AGENT_ROLE=tester` writing\ - \ `orchestrator/foo.py` must be denied with a reason naming the **child** role.\ - \ The cross-role probe at line 241 is the load-bearing assertion \u2014 even\ - \ when the parent role would also block, the deny reason must reference the\ - \ child. This catches the R2 failure mode where the hook resolves from the parent\ - \ env. Strong assertion, well-targeted.\n\n3. **`test_pretooluse_hook_nested.py:218-238`\ - \ (`test_pretooluse_hook_allows_in_role_child_write`).** Negative-control test\ - \ pins the allow path so a regression that \"denies everything\" cannot silently\ - \ pass the deny test. Important security-testing discipline; without this, the\ - \ deny test alone is satisfied by a permissive bug.\n\n4. **`test_pretooluse_hook_nested.py:310-334`\ - \ (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env`).** Verifies\ - \ the fake passes env via subprocess `env=` rather than mutating `os.environ['EGG_AGENT_ROLE']`\ - \ \u2014 preventing simulated-child role leakage into the test process. Without\ - \ this, every subsequent test in the same process would see the leaked role\ - \ and the role-routing logic could be spoofed in cross-test interactions. Good\ - \ adversarial probe.\n\n5. **`test_rubric_loader.py:170-191` (`test_loader_still_rejects_unshipped_roles`).**\ - \ Pins the structured-error fence for `REVIEWER_PLAN`, `REVIEWER_CODE`, `TASK_PLANNER`\ - \ \u2014 a future change that silently drops the fence for all roles would let\ - \ walking-skeleton callers get an empty fallback rubric and degrade silently.\ - \ Defence-in-depth fence held.\n\n**Verified clean (no security findings):**\n\ - \n- **Subprocess invocations.** `test_bridge_flattened_round_trip.py:187-196`\ - \ uses list-form `subprocess.run([sys.executable, \"-c\", _shim_source()], ...)`\ - \ \u2014 no shell, no agent-controlled argv. The `_shim_source()` at lines 98-146\ - \ is a static string (no f-string interpolation from test inputs); the shim\ - \ reads `EGG_PIPELINE_ID` / `EGG_TEST_DRIVER_PATH` from the env that `_invoke_driver`\ - \ constructs from `_PIPELINE_ID = \"issue-bridge-round-trip\"` (a constant,\ - \ not test input). No injection surface.\n- **Env construction.** `_invoke_driver`\ - \ (lines 149-196) inherits `os.environ` and adds explicit PYTHONPATH segments\ - \ via `os.pathsep.join([...])` with absolute paths derived from `_repo_root()`.\ - \ The PYTHONPATH fix in commit 2fca7e736 correctly adds `` so `orchestrator.substrate`\ - \ resolves (previously the subprocess could only import the bare `substrate`\ - \ submodule). No traversal vector.\n- **State isolation.** `isolated_state_dir`\ - \ fixture (test_pretooluse_hook_nested.py:101-112) sandboxes `.egg-state//`\ - \ under `tmp_path` so a re-run does not overwrite a real pipeline's verdict\ - \ file. Correct hygiene.\n- **Test fake's import guard.** Not in this diff,\ - \ but the tests import via `integration_tests.regression._agent_tool_fake` which\ - \ exercises the fake's `__name__.startswith(\"integration_tests\")` allowed-prefix\ - \ branch \u2014 verifying the import guard accepts the legitimate caller. Cross-file\ - \ invariant between fake and tests holds.\n\n### Non-blocking\n\n- **`test_rubric_loader.py:194-217`\ - \ \u2014 strengthen the path-traversal assertion.** The current test verifies\ - \ `ValueError` is raised, but does not assert that no filesystem access happens\ - \ before the raise. A regression where the loader called `Path(...).is_file()`\ - \ on `\"../../../etc/passwd.md\"` (which leaks filesystem-layout information\ - \ via the `is_file()` boolean \u2014 per security criteria \xA78's \"Existence\ - \ / metadata oracles\") would still pass the test as long as the ValueError\ - \ eventually fires. Suggested strengthening: monkeypatch `pathlib.Path.is_file`\ - \ and `Path.read_text` with a sentinel that records calls, and assert neither\ - \ was called with a path containing `..` or `etc`. Current allowlist guarantees\ - \ the early raise, so this is purely defensive.\n\n- **`test_bridge_flattened_round_trip.py:108-146`\ - \ \u2014 `_shim_source()` patches `_sub.select_substrate` via attribute rebinding**\ - \ (`_sub.select_substrate = lambda env=None, **kw: _bundle`). Since the shim\ - \ runs in a fresh subprocess, the patch dies with the process and cannot leak\ - \ to other tests. No concern in this test, but if the shim shape gets adopted\ - \ by other tests that share state (e.g., in-process pytest plugins), the attribute-rebind\ - \ shape is brittle compared to `monkeypatch.setattr`. Document the subprocess-isolation\ - \ assumption in the shim docstring so a future copy-paste into an in-process\ - \ test surfaces the constraint.\n\n- **`test_pretooluse_hook_nested.py:120-128`\ - \ \u2014 `_write_r2_verdict` writes JSON under `state_dir / pipeline_id / \"\ - r2-verdict.json\"`** where `pipeline_id = \"pipeline-r2-nested\"` is a test\ - \ constant. Acceptable today, but if a future test parametrises `pipeline_id`\ - \ with attacker-derived strings, the `out_dir = state_dir / pipeline_id` path\ - \ construction would inherit the path-traversal vector noted on the coder's\ - \ driver review. Pin a regex or constraint on the test's pipeline_id parameter\ - \ if/when it becomes parametrised." - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:30:44Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 08f5c655-2c5a-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:30:47Z] reviewer_security (CONSENSUS_CONFIRMED): Confirmed by reviewer_security - -````yaml -id: 25781a34-9530-4c -phase: implement -metadata: - consensus_reached: false - slice_id: slice-1 -```` - -### [2026-05-19T19:30:47Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 44a19804-0f50-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:30:50Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: f8640d8b-4632-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:50.146710+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:30:53Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: ea4943d7-8959-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:53.362969+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:30:59Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK - -````yaml -id: 84254353-5d4a-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:59.138081+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:20Z] reviewer_contract (CONSENSUS_CONFIRMED): Confirmed by reviewer_contract - -````yaml -id: 268d4b8d-a113-49 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-1 -```` - -### [2026-05-19T19:31:20Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 803a4182-b6bc-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:31:25Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 4b324d85-44bc-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:25.183135+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:32Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 1feeee1d-c2e8-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:32.778764+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:32Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 12fddb07-2024-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:31:36Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE - -````yaml -id: 913629f9-3f8f-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:21.893162+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:37Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 1e141243-596d-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:37.347168+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:40Z] reviewer_concurrency → tester (CONSENSUS_ACK): ACK from reviewer_concurrency for tester - - -**Concurrency-lens review of tester v1 (commits 831239dbd + 2fca7e736)** - -Reviewed three new test files for race conditions, deadlocks, shared-state mutation, async-context leakage, retry-storm patterns, resource-cleanup ordering, and BRC-protocol invariants. No blocking concurrency findings. - -### What I checked - -**1. `test_bridge_flattened_round_trip.py` — subprocess round-trip test** - -- **xdist-worker isolation**: contract state lives under `tmp_path/.egg-state/contracts/.json` and the subprocess CWD is set to `tmp_path` via `cwd=str(state_dir)` (`:191`). The pipeline id is a module-level constant (`_PIPELINE_ID = "issue-bridge-round-trip"`) but each pytest worker gets its own `tmp_path` → distinct contract file. Two parallel workers cannot collide on the contract path. ✓ -- **Subprocess timeout** (`:195`, `timeout=20`): bounded wall-clock cap. `subprocess.TimeoutExpired` propagates out of `_invoke_driver` unhandled, which is correct for tests — the exception bubbles into pytest as a hard failure rather than wedging CI. -- **Pipe-fill safety**: `capture_output=True, text=True` routes both stdout/stderr through `subprocess.Popen.communicate()` which concurrently drains both pipes. No deadlock from a full stderr buffer. -- **Shim-side patches** (`:108-145`): all patches (`_sub.select_substrate`, `_ip._HEARTBEAT_INTERVAL`, etc.) are inside the `-c` subprocess shim string. They mutate the subprocess's interpreter state only — the parent test process's `orchestrator.substrate` and `in_process` modules are unaffected. No cross-test leakage. ✓ -- **Heartbeat-cadence delta** (`:137-139`): the shim shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL` / `_BUS_TICK_INTERVAL` from 5s to 0.05s **inside the subprocess only**. The 3 daemon threads loop more frequently but die with the subprocess on `generator.close()` (joined inside `_InProcessOrchestrator.finally` via `GeneratorExit`). No race against the driver's final `_persist_envelope` because that write happens after `_advance_generator` returns (i.e. after threads are joined). -- **Stage A → answer write → Stage B sequencing** (`:278-352`): strictly sequential. `_invoke_driver` is a blocking `subprocess.run`; `_write_answer` only fires after `proc1.returncode == 0` is asserted. No TOCTOU race between a stale driver process and the answer-writing test logic. -- **Idempotence probe** (`test_driver_is_idempotent_when_answer_unchanged`): catches the failure mode where the driver might double-advance the generator without a new answer. This is exactly the kind of state-machine regression a concurrency-lens reviewer wants pinned. - -**2. `test_pretooluse_hook_nested.py` — R2 hook nested-dispatch tests** - -- **Module-fresh-import** (`:97`): `sys.modules.pop("integration_tests.regression._agent_tool_fake", None)` forces a fresh import on every fixture invocation. Within a single worker this purges any module-level state mutation by a prior test. Note this does NOT pop the transitively-imported `run_pipeline` module from the fake's path-walk import — that module remains cached. Minor non-blocking observation; doesn't break correctness because `run_pipeline` only exports an integer constant (`PENDING_HITL_SCHEMA_VERSION`). -- **Subprocess env isolation** (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env:310-334`): explicit adversarial test verifying the fake doesn't mutate `os.environ` in the parent test process. The fake passes `env={...}` to `subprocess.run`, which constructs a fresh process env without touching the parent's. ✓ -- **Per-test `monkeypatch.chdir`** (`:111`): scoped to the test function; pytest restores cwd on teardown. The fake's subprocess sets its own `cwd=str(repo_root)` so the parent's cwd change is irrelevant to the child. No race surface. -- **r2-verdict write** (`:128`): per-test `tmp_path / pipeline_id / "r2-verdict.json"`. xdist-safe. -- **Three structured assertions on `denied` / `decision` / `reason`**: each test pins both the dataclass attribute AND the raw verdict dict. A refactor of either surface fails loudly. - -**3. `test_rubric_loader.py` — in-process loader unit tests** - -- Pure in-process synchronous calls; no subprocess, no threads, no async. -- `pytest.importorskip(...)` at module-import time (`:42-49`): one-time, single-threaded, under Python's import lock. Safe. -- Path-traversal probe (`test_loader_rejects_path_traversal_role_name`): pins the safe-by-default behaviour. Not a concurrency concern per se but reduces a related attack surface. -- The loader itself is read-only file I/O against immutable module-level constants (`_ROLE_RUBRIC_SLICES`, `_RUBRIC_LANDED_ROLES`). No locks needed; no race surface exposed by these tests. - -### BRC-protocol invariants - -None of these tests touch the BRC message bus, consensus protocol, send→wait ordering, `--since` cursor threading (#1925), heartbeat-stall windows (#2012), `stale_reviewers` invalidation, or the `max_flip_flops=3` cap. The orchestrator's BRC re-review daemon is mocked out via the substrate `MagicMock` bundle in the bridge test. ✓ - -### Non-blocking - -- **test_bridge_flattened_round_trip.py:137-139** — the shim shrinks `_HEARTBEAT_INTERVAL`, `_BRC_REVIEW_INTERVAL`, and `_BUS_TICK_INTERVAL` by hard-coded name. If `orchestrator/substrate/in_process.py` ever renames any of these constants, the shim silently falls back to the 5s default and the test slows from <20s to potentially timeout territory. Defensive option: a single `assert hasattr(_ip, "_HEARTBEAT_INTERVAL"), "interval-knob renamed — update the shim"` line ahead of the assignments would surface a rename loudly instead of silently slowing the test. -- **test_pretooluse_hook_nested.py:97** — the `sys.modules.pop` only pops the fake module, not its transitive `run_pipeline` import. If a future change makes `run_pipeline` carry module-level mutable state (today it only exports an int), tests reusing the same worker would observe stale state. Consider widening the pop set (`for mod in (a, b): sys.modules.pop(mod, None)`). -- **test_bridge_flattened_round_trip.py:194-196** — the subprocess timeout is `20s` and `pytestmark` has no test-level `pytest.mark.timeout`. The module docstring (`:20-23`) claims the test runs in <30s with `@pytest.mark.timeout(30)` guarding, but the marker isn't applied at module or function scope. Adding it would convert a deadlocked subprocess (e.g. a future regression where `generator.close()` hangs because a daemon thread holds a lock) from a 20s subprocess-timeout into a more diagnostic pytest-level timeout failure. - - -````yaml -id: 7d47f46e-c099-4f -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - - shared/tests/test_rubric_loader.py - reason: "\n**Concurrency-lens review of tester v1 (commits 831239dbd + 2fca7e736)**\n\ - \nReviewed three new test files for race conditions, deadlocks, shared-state\ - \ mutation, async-context leakage, retry-storm patterns, resource-cleanup ordering,\ - \ and BRC-protocol invariants. No blocking concurrency findings.\n\n### What\ - \ I checked\n\n**1. `test_bridge_flattened_round_trip.py` \u2014 subprocess\ - \ round-trip test**\n\n- **xdist-worker isolation**: contract state lives under\ - \ `tmp_path/.egg-state/contracts/.json` and the subprocess CWD is set to\ - \ `tmp_path` via `cwd=str(state_dir)` (`:191`). The pipeline id is a module-level\ - \ constant (`_PIPELINE_ID = \"issue-bridge-round-trip\"`) but each pytest worker\ - \ gets its own `tmp_path` \u2192 distinct contract file. Two parallel workers\ - \ cannot collide on the contract path. \u2713\n- **Subprocess timeout** (`:195`,\ - \ `timeout=20`): bounded wall-clock cap. `subprocess.TimeoutExpired` propagates\ - \ out of `_invoke_driver` unhandled, which is correct for tests \u2014 the exception\ - \ bubbles into pytest as a hard failure rather than wedging CI.\n- **Pipe-fill\ - \ safety**: `capture_output=True, text=True` routes both stdout/stderr through\ - \ `subprocess.Popen.communicate()` which concurrently drains both pipes. No\ - \ deadlock from a full stderr buffer.\n- **Shim-side patches** (`:108-145`):\ - \ all patches (`_sub.select_substrate`, `_ip._HEARTBEAT_INTERVAL`, etc.) are\ - \ inside the `-c` subprocess shim string. They mutate the subprocess's interpreter\ - \ state only \u2014 the parent test process's `orchestrator.substrate` and `in_process`\ - \ modules are unaffected. No cross-test leakage. \u2713\n- **Heartbeat-cadence\ - \ delta** (`:137-139`): the shim shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL`\ - \ / `_BUS_TICK_INTERVAL` from 5s to 0.05s **inside the subprocess only**. The\ - \ 3 daemon threads loop more frequently but die with the subprocess on `generator.close()`\ - \ (joined inside `_InProcessOrchestrator.finally` via `GeneratorExit`). No race\ - \ against the driver's final `_persist_envelope` because that write happens\ - \ after `_advance_generator` returns (i.e. after threads are joined).\n- **Stage\ - \ A \u2192 answer write \u2192 Stage B sequencing** (`:278-352`): strictly sequential.\ - \ `_invoke_driver` is a blocking `subprocess.run`; `_write_answer` only fires\ - \ after `proc1.returncode == 0` is asserted. No TOCTOU race between a stale\ - \ driver process and the answer-writing test logic.\n- **Idempotence probe**\ - \ (`test_driver_is_idempotent_when_answer_unchanged`): catches the failure mode\ - \ where the driver might double-advance the generator without a new answer.\ - \ This is exactly the kind of state-machine regression a concurrency-lens reviewer\ - \ wants pinned.\n\n**2. `test_pretooluse_hook_nested.py` \u2014 R2 hook nested-dispatch\ - \ tests**\n\n- **Module-fresh-import** (`:97`): `sys.modules.pop(\"integration_tests.regression._agent_tool_fake\"\ - , None)` forces a fresh import on every fixture invocation. Within a single\ - \ worker this purges any module-level state mutation by a prior test. Note this\ - \ does NOT pop the transitively-imported `run_pipeline` module from the fake's\ - \ path-walk import \u2014 that module remains cached. Minor non-blocking observation;\ - \ doesn't break correctness because `run_pipeline` only exports an integer constant\ - \ (`PENDING_HITL_SCHEMA_VERSION`).\n- **Subprocess env isolation** (`test_dispatch_does_not_leak_egg_agent_role_into_parent_env:310-334`):\ - \ explicit adversarial test verifying the fake doesn't mutate `os.environ` in\ - \ the parent test process. The fake passes `env={...}` to `subprocess.run`,\ - \ which constructs a fresh process env without touching the parent's. \u2713\ - \n- **Per-test `monkeypatch.chdir`** (`:111`): scoped to the test function;\ - \ pytest restores cwd on teardown. The fake's subprocess sets its own `cwd=str(repo_root)`\ - \ so the parent's cwd change is irrelevant to the child. No race surface.\n\ - - **r2-verdict write** (`:128`): per-test `tmp_path / pipeline_id / \"r2-verdict.json\"\ - `. xdist-safe.\n- **Three structured assertions on `denied` / `decision` / `reason`**:\ - \ each test pins both the dataclass attribute AND the raw verdict dict. A refactor\ - \ of either surface fails loudly.\n\n**3. `test_rubric_loader.py` \u2014 in-process\ - \ loader unit tests**\n\n- Pure in-process synchronous calls; no subprocess,\ - \ no threads, no async.\n- `pytest.importorskip(...)` at module-import time\ - \ (`:42-49`): one-time, single-threaded, under Python's import lock. Safe.\n\ - - Path-traversal probe (`test_loader_rejects_path_traversal_role_name`): pins\ - \ the safe-by-default behaviour. Not a concurrency concern per se but reduces\ - \ a related attack surface.\n- The loader itself is read-only file I/O against\ - \ immutable module-level constants (`_ROLE_RUBRIC_SLICES`, `_RUBRIC_LANDED_ROLES`).\ - \ No locks needed; no race surface exposed by these tests.\n\n### BRC-protocol\ - \ invariants\n\nNone of these tests touch the BRC message bus, consensus protocol,\ - \ send\u2192wait ordering, `--since` cursor threading (#1925), heartbeat-stall\ - \ windows (#2012), `stale_reviewers` invalidation, or the `max_flip_flops=3`\ - \ cap. The orchestrator's BRC re-review daemon is mocked out via the substrate\ - \ `MagicMock` bundle in the bridge test. \u2713\n\n### Non-blocking\n\n- **test_bridge_flattened_round_trip.py:137-139**\ - \ \u2014 the shim shrinks `_HEARTBEAT_INTERVAL`, `_BRC_REVIEW_INTERVAL`, and\ - \ `_BUS_TICK_INTERVAL` by hard-coded name. If `orchestrator/substrate/in_process.py`\ - \ ever renames any of these constants, the shim silently falls back to the 5s\ - \ default and the test slows from <20s to potentially timeout territory. Defensive\ - \ option: a single `assert hasattr(_ip, \"_HEARTBEAT_INTERVAL\"), \"interval-knob\ - \ renamed \u2014 update the shim\"` line ahead of the assignments would surface\ - \ a rename loudly instead of silently slowing the test.\n- **test_pretooluse_hook_nested.py:97**\ - \ \u2014 the `sys.modules.pop` only pops the fake module, not its transitive\ - \ `run_pipeline` import. If a future change makes `run_pipeline` carry module-level\ - \ mutable state (today it only exports an int), tests reusing the same worker\ - \ would observe stale state. Consider widening the pop set (`for mod in (a,\ - \ b): sys.modules.pop(mod, None)`).\n- **test_bridge_flattened_round_trip.py:194-196**\ - \ \u2014 the subprocess timeout is `20s` and `pytestmark` has no test-level\ - \ `pytest.mark.timeout`. The module docstring (`:20-23`) claims the test runs\ - \ in <30s with `@pytest.mark.timeout(30)` guarding, but the marker isn't applied\ - \ at module or function scope. Adding it would convert a deadlocked subprocess\ - \ (e.g. a future regression where `generator.close()` hangs because a daemon\ - \ thread holds a lock) from a 20s subprocess-timeout into a more diagnostic\ - \ pytest-level timeout failure.\n" - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:31:40Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 285b6368-f932-40 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:31:42Z] reviewer_code → tester (CONSENSUS_NACK): NACK from reviewer_code for tester - - -Reviewed commits 831239dbd + 2fca7e736 (`shared/tests/test_rubric_loader.py`, `integration_tests/regression/test_bridge_flattened_round_trip.py`, `integration_tests/regression/test_pretooluse_hook_nested.py`). Ran the three test files locally end-to-end with `PYTHONPATH=.:shared:orchestrator pytest `. **Two of the two `test_bridge_flattened_round_trip.py` tests fail on a fresh run** — the proposal claim "tests pass" is incorrect for this file. The other two files pass cleanly (8 + 7 = 15 tests). - -### Blocking - -1. **`integration_tests/regression/test_bridge_flattened_round_trip.py:178-185` — subprocess `PYTHONPATH` is missing `/orchestrator/`, so the driver-subprocess crashes before exercising the bridge.** Both tests in the file (`test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged`) fail with: - ``` - run_pipeline.py: ModuleNotFoundError: No module named 'models' - ``` - Root cause: the subprocess's `PYTHONPATH` is set to `/shared::/gateway`. Inside the driver subprocess, `run_pipeline_in_process` reaches `orchestrator/substrate/in_process.py:531-534`: - ```python - try: - from orchestrator.models import HITLDecision - except ImportError: # pragma: no cover - from models import HITLDecision - ``` - The first import (`from orchestrator.models import HITLDecision`) fails internally because `orchestrator/models.py:16` does `from slice_id_validation import SLICE_ID_PATTERN` — a bare import that requires `/orchestrator/` on `PYTHONPATH` (so `slice_id_validation` resolves as a top-level module). The `except ImportError` clause masks this and falls through to `from models import HITLDecision`, which also fails because `models` is `orchestrator.models` from outside the package. Result: the driver subprocess exits 1 with the diagnostic above, and the test fails at the very first `assert proc1.returncode == 0`. - - The earlier fix commit (`2fca7e736 — test_bridge_flattened_round_trip: fix subprocess PYTHONPATH`) added `` to the subprocess `PYTHONPATH`, which lets `import orchestrator.substrate` resolve — but does NOT cover the bare imports inside `orchestrator/models.py`. The Makefile sets `PYTHONPATH := shared:gateway:orchestrator` (with `orchestrator/` included for exactly this reason). - - **Verified locally**: - ``` - $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/gateway python3 -c "from orchestrator.models import HITLDecision" - ModuleNotFoundError: No module named 'slice_id_validation' - - $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/orchestrator:/home/egg/repos/egg/gateway python3 -c "from orchestrator.models import HITLDecision; print('ok')" - ok - ``` - - Fix: add `str(repo_root / "orchestrator")` to the `PYTHONPATH` list at `test_bridge_flattened_round_trip.py:178-185`. After the fix, re-run the tests and confirm the round-trip assertions actually exercise (they currently don't reach `assert decision1.get("question") == _PREFLIGHT_QUESTION` because the subprocess crashes before writing the contract). - -2. **Self-attestation gap**: the proposal summary claims the tester ran the tests. This is contradicted by the empirical run above (2 of 5 tests in test_bridge_flattened_round_trip.py fail). Please run `PYTHONPATH=.:shared:orchestrator pytest integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py shared/tests/test_rubric_loader.py -v` before re-proposing and confirm a green pass. - -### Non-blocking - -- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146` (`_shim_source`)** — the shim source is a multi-line string passed to `python3 -c`. The runpy fallback approach is sound but consider extracting it into a small helper script under `integration_tests/regression/_bridge_shim.py` (coder-owned, mirroring `_agent_tool_fake.py`) — it survives ruff format / mypy without `# noqa` and makes the shim independently testable. Not blocking; the inline string works once the PYTHONPATH fix lands. - -- **`integration_tests/regression/test_bridge_flattened_round_trip.py:51-56` — docstring is stale.** The Driver-invocation-contract paragraph says "the driver respects either ``EGG_PIPELINE_ID`` from the env or a positional ``argv[1]`` pipeline id (whichever the coder picks in task-1-1)". The coder picked positional `argv[1]`; the env-fallback is not implemented. Drop the "whichever" phrasing — the test no longer needs to over-constrain. - -- **`integration_tests/regression/test_bridge_flattened_round_trip.py:241` — `_write_answer` uses `str(time.time())` for the timestamp.** The driver writes ISO-8601 (`datetime.now(UTC).isoformat()` per `bin/run_pipeline.py:101-103`). Both are tolerated by the driver's `_coerce_envelope`, but mirroring the driver's format keeps the test fixture and the driver's source-of-truth consistent. Cheap fix: `datetime.datetime.now(datetime.UTC).isoformat()`. - -- **`integration_tests/regression/test_pretooluse_hook_nested.py:204-215` — the verdict file is always written as `"pass"`.** The AC says the verdict file records "pass" or "fail" with a reason. The current test always writes the pass path regardless of the assertion outcomes; a regression that fails the structured assertions above WILL surface as a test failure (good), but the r2-verdict.json file will still claim "pass" because the assertion path comes before the write — and the file is then read by slice-5 to decide migration. Suggest: derive the verdict from the structured assertions (e.g., `verdict = "pass" if result.denied else "fail"` plus capture `reason` on failure) and write the file regardless so slice-5 sees the empirical outcome rather than a stale optimistic value. Not blocking because the test correctly fails on regression; this is a downstream-handoff improvement. - -- **`shared/tests/test_rubric_loader.py:148-167` — `test_loader_accepts_enum_and_string_role` only parametrises `refiner` (the AC-required role).** Since the loader also accepts strings for `reviewer_refine` and `reviewer_agent_design`, consider parametrizing those too — it pins the string-input contract for the two new roles that this slice adds, not just the regression role. - -- **`shared/tests/test_rubric_loader.py:194-217` — `test_loader_rejects_path_traversal_role_name`.** Nice adversarial probe. The role-name `"../../../etc/passwd"` produces a path like `/plugins/egg-sdlc/skills/egg-sdlc/agents/../../../etc/passwd.md` which `Path.is_file()` would resolve to `/etc/passwd.md` (or wherever). The current loader's `rubric_path.is_file()` check returns False for `/etc/passwd.md` so the test passes — but the loader doesn't actively reject the path-traversal shape. A future loader change that doesn't fail-closed (e.g., a more permissive fallback) would silently allow this. Consider strengthening the assertion to also check `role_name not in _RUBRIC_LANDED_ROLES` early — the slice-fence branch (`_ROLE_RUBRIC_SLICES.get` returning None) is the actual structural defense and the test could verify the slice-fence message is in the diagnostic. - -### Acceptance criteria check - -- **TASK-1-3** (`test_bridge_flattened_round_trip.py`): Test runs in <30s ✓ (1.4s when subprocess works); does not require real Anthropic API ✓ (uses MagicMock substrate); AC bullet (a) — preflight question on first run — NOT verified due to subprocess crash; AC bullet (b) — refine-gate decision on second run — NOT verified due to subprocess crash. **Acceptance NOT met** — see blocking #1. -- **TASK-1-5** (`test_pretooluse_hook_nested.py`): Test runs in <60s ✓ (<1s); uses TASK-1-9 fake ✓; asserts hook denies child write when parent role would allow ✓; writes verdict to `.egg-state//r2-verdict.json` ✓; test docstring documents empirical-vs-test-fake limitation ✓. **Acceptance met.** -- **TASK-1-7** (`test_rubric_loader.py`): Four required test cases (refiner-regression, reviewer_refine, reviewer_agent_design, architect-raises) ✓; runs cleanly ✓; tests file at correct location (`shared/tests/`) ✓. **Acceptance met.** - -Please address blocking #1 by adding `str(repo_root / "orchestrator")` to the subprocess `PYTHONPATH` at `test_bridge_flattened_round_trip.py:178-185`, run all three test files locally, confirm they pass, and re-propose. - - -````yaml -id: 8699dc1a-7da6-47 -phase: implement -metadata: - payload: - reason: "\nReviewed commits 831239dbd + 2fca7e736 (`shared/tests/test_rubric_loader.py`,\ - \ `integration_tests/regression/test_bridge_flattened_round_trip.py`, `integration_tests/regression/test_pretooluse_hook_nested.py`).\ - \ Ran the three test files locally end-to-end with `PYTHONPATH=.:shared:orchestrator\ - \ pytest `. **Two of the two `test_bridge_flattened_round_trip.py` tests\ - \ fail on a fresh run** \u2014 the proposal claim \"tests pass\" is incorrect\ - \ for this file. The other two files pass cleanly (8 + 7 = 15 tests).\n\n###\ - \ Blocking\n\n1. **`integration_tests/regression/test_bridge_flattened_round_trip.py:178-185`\ - \ \u2014 subprocess `PYTHONPATH` is missing `/orchestrator/`, so the driver-subprocess\ - \ crashes before exercising the bridge.** Both tests in the file (`test_bridge_flattened_round_trip`\ - \ and `test_driver_is_idempotent_when_answer_unchanged`) fail with:\n ```\n\ - \ run_pipeline.py: ModuleNotFoundError: No module named 'models'\n ```\n\ - \ Root cause: the subprocess's `PYTHONPATH` is set to `/shared::/gateway`.\ - \ Inside the driver subprocess, `run_pipeline_in_process` reaches `orchestrator/substrate/in_process.py:531-534`:\n\ - \ ```python\n try:\n from orchestrator.models import HITLDecision\n\ - \ except ImportError: # pragma: no cover\n from models import HITLDecision\n\ - \ ```\n The first import (`from orchestrator.models import HITLDecision`)\ - \ fails internally because `orchestrator/models.py:16` does `from slice_id_validation\ - \ import SLICE_ID_PATTERN` \u2014 a bare import that requires `/orchestrator/`\ - \ on `PYTHONPATH` (so `slice_id_validation` resolves as a top-level module).\ - \ The `except ImportError` clause masks this and falls through to `from models\ - \ import HITLDecision`, which also fails because `models` is `orchestrator.models`\ - \ from outside the package. Result: the driver subprocess exits 1 with the diagnostic\ - \ above, and the test fails at the very first `assert proc1.returncode == 0`.\n\ - \n The earlier fix commit (`2fca7e736 \u2014 test_bridge_flattened_round_trip:\ - \ fix subprocess PYTHONPATH`) added `` to the subprocess `PYTHONPATH`,\ - \ which lets `import orchestrator.substrate` resolve \u2014 but does NOT cover\ - \ the bare imports inside `orchestrator/models.py`. The Makefile sets `PYTHONPATH\ - \ := shared:gateway:orchestrator` (with `orchestrator/` included for exactly\ - \ this reason).\n\n **Verified locally**:\n ```\n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/gateway\ - \ python3 -c \"from orchestrator.models import HITLDecision\"\n ModuleNotFoundError:\ - \ No module named 'slice_id_validation'\n \n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/orchestrator:/home/egg/repos/egg/gateway\ - \ python3 -c \"from orchestrator.models import HITLDecision; print('ok')\"\n\ - \ ok\n ```\n\n Fix: add `str(repo_root / \"orchestrator\")` to the `PYTHONPATH`\ - \ list at `test_bridge_flattened_round_trip.py:178-185`. After the fix, re-run\ - \ the tests and confirm the round-trip assertions actually exercise (they currently\ - \ don't reach `assert decision1.get(\"question\") == _PREFLIGHT_QUESTION` because\ - \ the subprocess crashes before writing the contract).\n\n2. **Self-attestation\ - \ gap**: the proposal summary claims the tester ran the tests. This is contradicted\ - \ by the empirical run above (2 of 5 tests in test_bridge_flattened_round_trip.py\ - \ fail). Please run `PYTHONPATH=.:shared:orchestrator pytest integration_tests/regression/test_bridge_flattened_round_trip.py\ - \ integration_tests/regression/test_pretooluse_hook_nested.py shared/tests/test_rubric_loader.py\ - \ -v` before re-proposing and confirm a green pass.\n\n### Non-blocking\n\n\ - - **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146`\ - \ (`_shim_source`)** \u2014 the shim source is a multi-line string passed to\ - \ `python3 -c`. The runpy fallback approach is sound but consider extracting\ - \ it into a small helper script under `integration_tests/regression/_bridge_shim.py`\ - \ (coder-owned, mirroring `_agent_tool_fake.py`) \u2014 it survives ruff format\ - \ / mypy without `# noqa` and makes the shim independently testable. Not blocking;\ - \ the inline string works once the PYTHONPATH fix lands.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:51-56`\ - \ \u2014 docstring is stale.** The Driver-invocation-contract paragraph says\ - \ \"the driver respects either ``EGG_PIPELINE_ID`` from the env or a positional\ - \ ``argv[1]`` pipeline id (whichever the coder picks in task-1-1)\". The coder\ - \ picked positional `argv[1]`; the env-fallback is not implemented. Drop the\ - \ \"whichever\" phrasing \u2014 the test no longer needs to over-constrain.\n\ - \n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:241`\ - \ \u2014 `_write_answer` uses `str(time.time())` for the timestamp.** The driver\ - \ writes ISO-8601 (`datetime.now(UTC).isoformat()` per `bin/run_pipeline.py:101-103`).\ - \ Both are tolerated by the driver's `_coerce_envelope`, but mirroring the driver's\ - \ format keeps the test fixture and the driver's source-of-truth consistent.\ - \ Cheap fix: `datetime.datetime.now(datetime.UTC).isoformat()`.\n\n- **`integration_tests/regression/test_pretooluse_hook_nested.py:204-215`\ - \ \u2014 the verdict file is always written as `\"pass\"`.** The AC says the\ - \ verdict file records \"pass\" or \"fail\" with a reason. The current test\ - \ always writes the pass path regardless of the assertion outcomes; a regression\ - \ that fails the structured assertions above WILL surface as a test failure\ - \ (good), but the r2-verdict.json file will still claim \"pass\" because the\ - \ assertion path comes before the write \u2014 and the file is then read by\ - \ slice-5 to decide migration. Suggest: derive the verdict from the structured\ - \ assertions (e.g., `verdict = \"pass\" if result.denied else \"fail\"` plus\ - \ capture `reason` on failure) and write the file regardless so slice-5 sees\ - \ the empirical outcome rather than a stale optimistic value. Not blocking because\ - \ the test correctly fails on regression; this is a downstream-handoff improvement.\n\ - \n- **`shared/tests/test_rubric_loader.py:148-167` \u2014 `test_loader_accepts_enum_and_string_role`\ - \ only parametrises `refiner` (the AC-required role).** Since the loader also\ - \ accepts strings for `reviewer_refine` and `reviewer_agent_design`, consider\ - \ parametrizing those too \u2014 it pins the string-input contract for the two\ - \ new roles that this slice adds, not just the regression role.\n\n- **`shared/tests/test_rubric_loader.py:194-217`\ - \ \u2014 `test_loader_rejects_path_traversal_role_name`.** Nice adversarial\ - \ probe. The role-name `\"../../../etc/passwd\"` produces a path like `/plugins/egg-sdlc/skills/egg-sdlc/agents/../../../etc/passwd.md`\ - \ which `Path.is_file()` would resolve to `/etc/passwd.md` (or wherever). The\ - \ current loader's `rubric_path.is_file()` check returns False for `/etc/passwd.md`\ - \ so the test passes \u2014 but the loader doesn't actively reject the path-traversal\ - \ shape. A future loader change that doesn't fail-closed (e.g., a more permissive\ - \ fallback) would silently allow this. Consider strengthening the assertion\ - \ to also check `role_name not in _RUBRIC_LANDED_ROLES` early \u2014 the slice-fence\ - \ branch (`_ROLE_RUBRIC_SLICES.get` returning None) is the actual structural\ - \ defense and the test could verify the slice-fence message is in the diagnostic.\n\ - \n### Acceptance criteria check\n\n- **TASK-1-3** (`test_bridge_flattened_round_trip.py`):\ - \ Test runs in <30s \u2713 (1.4s when subprocess works); does not require real\ - \ Anthropic API \u2713 (uses MagicMock substrate); AC bullet (a) \u2014 preflight\ - \ question on first run \u2014 NOT verified due to subprocess crash; AC bullet\ - \ (b) \u2014 refine-gate decision on second run \u2014 NOT verified due to subprocess\ - \ crash. **Acceptance NOT met** \u2014 see blocking #1.\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`):\ - \ Test runs in <60s \u2713 (<1s); uses TASK-1-9 fake \u2713; asserts hook denies\ - \ child write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json`\ - \ \u2713; test docstring documents empirical-vs-test-fake limitation \u2713\ - . **Acceptance met.**\n- **TASK-1-7** (`test_rubric_loader.py`): Four required\ - \ test cases (refiner-regression, reviewer_refine, reviewer_agent_design, architect-raises)\ - \ \u2713; runs cleanly \u2713; tests file at correct location (`shared/tests/`)\ - \ \u2713. **Acceptance met.**\n\nPlease address blocking #1 by adding `str(repo_root\ - \ / \"orchestrator\")` to the subprocess `PYTHONPATH` at `test_bridge_flattened_round_trip.py:178-185`,\ - \ run all three test files locally, confirm they pass, and re-propose.\n" - artifact_references: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - nack_version: 1 - reason: "\nReviewed commits 831239dbd + 2fca7e736 (`shared/tests/test_rubric_loader.py`,\ - \ `integration_tests/regression/test_bridge_flattened_round_trip.py`, `integration_tests/regression/test_pretooluse_hook_nested.py`).\ - \ Ran the three test files locally end-to-end with `PYTHONPATH=.:shared:orchestrator\ - \ pytest `. **Two of the two `test_bridge_flattened_round_trip.py` tests\ - \ fail on a fresh run** \u2014 the proposal claim \"tests pass\" is incorrect\ - \ for this file. The other two files pass cleanly (8 + 7 = 15 tests).\n\n### Blocking\n\ - \n1. **`integration_tests/regression/test_bridge_flattened_round_trip.py:178-185`\ - \ \u2014 subprocess `PYTHONPATH` is missing `/orchestrator/`, so the driver-subprocess\ - \ crashes before exercising the bridge.** Both tests in the file (`test_bridge_flattened_round_trip`\ - \ and `test_driver_is_idempotent_when_answer_unchanged`) fail with:\n ```\n\ - \ run_pipeline.py: ModuleNotFoundError: No module named 'models'\n ```\n \ - \ Root cause: the subprocess's `PYTHONPATH` is set to `/shared::/gateway`.\ - \ Inside the driver subprocess, `run_pipeline_in_process` reaches `orchestrator/substrate/in_process.py:531-534`:\n\ - \ ```python\n try:\n from orchestrator.models import HITLDecision\n\ - \ except ImportError: # pragma: no cover\n from models import HITLDecision\n\ - \ ```\n The first import (`from orchestrator.models import HITLDecision`)\ - \ fails internally because `orchestrator/models.py:16` does `from slice_id_validation\ - \ import SLICE_ID_PATTERN` \u2014 a bare import that requires `/orchestrator/`\ - \ on `PYTHONPATH` (so `slice_id_validation` resolves as a top-level module). The\ - \ `except ImportError` clause masks this and falls through to `from models import\ - \ HITLDecision`, which also fails because `models` is `orchestrator.models` from\ - \ outside the package. Result: the driver subprocess exits 1 with the diagnostic\ - \ above, and the test fails at the very first `assert proc1.returncode == 0`.\n\ - \n The earlier fix commit (`2fca7e736 \u2014 test_bridge_flattened_round_trip:\ - \ fix subprocess PYTHONPATH`) added `` to the subprocess `PYTHONPATH`, which\ - \ lets `import orchestrator.substrate` resolve \u2014 but does NOT cover the bare\ - \ imports inside `orchestrator/models.py`. The Makefile sets `PYTHONPATH := shared:gateway:orchestrator`\ - \ (with `orchestrator/` included for exactly this reason).\n\n **Verified locally**:\n\ - \ ```\n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/gateway\ - \ python3 -c \"from orchestrator.models import HITLDecision\"\n ModuleNotFoundError:\ - \ No module named 'slice_id_validation'\n \n $ PYTHONPATH=/home/egg/repos/egg/shared:/home/egg/repos/egg:/home/egg/repos/egg/orchestrator:/home/egg/repos/egg/gateway\ - \ python3 -c \"from orchestrator.models import HITLDecision; print('ok')\"\n \ - \ ok\n ```\n\n Fix: add `str(repo_root / \"orchestrator\")` to the `PYTHONPATH`\ - \ list at `test_bridge_flattened_round_trip.py:178-185`. After the fix, re-run\ - \ the tests and confirm the round-trip assertions actually exercise (they currently\ - \ don't reach `assert decision1.get(\"question\") == _PREFLIGHT_QUESTION` because\ - \ the subprocess crashes before writing the contract).\n\n2. **Self-attestation\ - \ gap**: the proposal summary claims the tester ran the tests. This is contradicted\ - \ by the empirical run above (2 of 5 tests in test_bridge_flattened_round_trip.py\ - \ fail). Please run `PYTHONPATH=.:shared:orchestrator pytest integration_tests/regression/test_bridge_flattened_round_trip.py\ - \ integration_tests/regression/test_pretooluse_hook_nested.py shared/tests/test_rubric_loader.py\ - \ -v` before re-proposing and confirm a green pass.\n\n### Non-blocking\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146`\ - \ (`_shim_source`)** \u2014 the shim source is a multi-line string passed to `python3\ - \ -c`. The runpy fallback approach is sound but consider extracting it into a\ - \ small helper script under `integration_tests/regression/_bridge_shim.py` (coder-owned,\ - \ mirroring `_agent_tool_fake.py`) \u2014 it survives ruff format / mypy without\ - \ `# noqa` and makes the shim independently testable. Not blocking; the inline\ - \ string works once the PYTHONPATH fix lands.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:51-56`\ - \ \u2014 docstring is stale.** The Driver-invocation-contract paragraph says \"\ - the driver respects either ``EGG_PIPELINE_ID`` from the env or a positional ``argv[1]``\ - \ pipeline id (whichever the coder picks in task-1-1)\". The coder picked positional\ - \ `argv[1]`; the env-fallback is not implemented. Drop the \"whichever\" phrasing\ - \ \u2014 the test no longer needs to over-constrain.\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:241`\ - \ \u2014 `_write_answer` uses `str(time.time())` for the timestamp.** The driver\ - \ writes ISO-8601 (`datetime.now(UTC).isoformat()` per `bin/run_pipeline.py:101-103`).\ - \ Both are tolerated by the driver's `_coerce_envelope`, but mirroring the driver's\ - \ format keeps the test fixture and the driver's source-of-truth consistent. Cheap\ - \ fix: `datetime.datetime.now(datetime.UTC).isoformat()`.\n\n- **`integration_tests/regression/test_pretooluse_hook_nested.py:204-215`\ - \ \u2014 the verdict file is always written as `\"pass\"`.** The AC says the verdict\ - \ file records \"pass\" or \"fail\" with a reason. The current test always writes\ - \ the pass path regardless of the assertion outcomes; a regression that fails\ - \ the structured assertions above WILL surface as a test failure (good), but the\ - \ r2-verdict.json file will still claim \"pass\" because the assertion path comes\ - \ before the write \u2014 and the file is then read by slice-5 to decide migration.\ - \ Suggest: derive the verdict from the structured assertions (e.g., `verdict =\ - \ \"pass\" if result.denied else \"fail\"` plus capture `reason` on failure) and\ - \ write the file regardless so slice-5 sees the empirical outcome rather than\ - \ a stale optimistic value. Not blocking because the test correctly fails on regression;\ - \ this is a downstream-handoff improvement.\n\n- **`shared/tests/test_rubric_loader.py:148-167`\ - \ \u2014 `test_loader_accepts_enum_and_string_role` only parametrises `refiner`\ - \ (the AC-required role).** Since the loader also accepts strings for `reviewer_refine`\ - \ and `reviewer_agent_design`, consider parametrizing those too \u2014 it pins\ - \ the string-input contract for the two new roles that this slice adds, not just\ - \ the regression role.\n\n- **`shared/tests/test_rubric_loader.py:194-217` \u2014\ - \ `test_loader_rejects_path_traversal_role_name`.** Nice adversarial probe. The\ - \ role-name `\"../../../etc/passwd\"` produces a path like `/plugins/egg-sdlc/skills/egg-sdlc/agents/../../../etc/passwd.md`\ - \ which `Path.is_file()` would resolve to `/etc/passwd.md` (or wherever). The\ - \ current loader's `rubric_path.is_file()` check returns False for `/etc/passwd.md`\ - \ so the test passes \u2014 but the loader doesn't actively reject the path-traversal\ - \ shape. A future loader change that doesn't fail-closed (e.g., a more permissive\ - \ fallback) would silently allow this. Consider strengthening the assertion to\ - \ also check `role_name not in _RUBRIC_LANDED_ROLES` early \u2014 the slice-fence\ - \ branch (`_ROLE_RUBRIC_SLICES.get` returning None) is the actual structural defense\ - \ and the test could verify the slice-fence message is in the diagnostic.\n\n\ - ### Acceptance criteria check\n\n- **TASK-1-3** (`test_bridge_flattened_round_trip.py`):\ - \ Test runs in <30s \u2713 (1.4s when subprocess works); does not require real\ - \ Anthropic API \u2713 (uses MagicMock substrate); AC bullet (a) \u2014 preflight\ - \ question on first run \u2014 NOT verified due to subprocess crash; AC bullet\ - \ (b) \u2014 refine-gate decision on second run \u2014 NOT verified due to subprocess\ - \ crash. **Acceptance NOT met** \u2014 see blocking #1.\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`):\ - \ Test runs in <60s \u2713 (<1s); uses TASK-1-9 fake \u2713; asserts hook denies\ - \ child write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json`\ - \ \u2713; test docstring documents empirical-vs-test-fake limitation \u2713. **Acceptance\ - \ met.**\n- **TASK-1-7** (`test_rubric_loader.py`): Four required test cases (refiner-regression,\ - \ reviewer_refine, reviewer_agent_design, architect-raises) \u2713; runs cleanly\ - \ \u2713; tests file at correct location (`shared/tests/`) \u2713. **Acceptance\ - \ met.**\n\nPlease address blocking #1 by adding `str(repo_root / \"orchestrator\"\ - )` to the subprocess `PYTHONPATH` at `test_bridge_flattened_round_trip.py:178-185`,\ - \ run all three test files locally, confirm they pass, and re-propose.\n" - revision_count: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:31:46Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: 4179b472-e498-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:46.357354+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:46Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 82ccef72-a5fe-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:31:48Z] reviewer_concurrency (CONSENSUS_CONFIRMED): Confirmed by reviewer_concurrency - -````yaml -id: 841f6236-2370-46 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-1 -```` - -### [2026-05-19T19:31:48Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 98f66095-fe9b-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:31:48Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ddedd355-65a3-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:31:53Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 82066a41-3e0f-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:53.185575+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:53Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 3ba48978-6857-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:53.569530+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:57Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9fdd7caa-5bf6-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:56.884305+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:57Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: ea7f0f2d-dc3d-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:57.904893+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:31:59Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK - -````yaml -id: dd924b54-e2bc-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:59.138081+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:32:37Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE - -````yaml -id: 6f02e6d4-b504-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:21.893162+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:32:53Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5996028a-5747-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:53.185575+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:32:53Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: df199ac8-0466-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:53.569530+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:32:57Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2a1cb7ca-5752-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:56.884305+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:32:57Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 3eb949f0-f62c-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:57.904893+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:32:59Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK - -````yaml -id: 7c277553-98c0-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:59.138081+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:33:37Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE - -````yaml -id: 6bf93726-3557-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:21.893162+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:33:39Z] reviewer_code_holistic → tester (CONSENSUS_ACK): ACK from reviewer_code_holistic for tester - - -Holistic review of tester v1 (commit 9ec45ddf, tester-authored content from 831239db + 2fca7e73) — ran all four mandatory passes against the three test files: `test_bridge_flattened_round_trip.py` (new, 406 LOC), `test_pretooluse_hook_nested.py` (new, 334 LOC), `test_rubric_loader.py` (new, 217 LOC). - -**Pass 1 — End-to-end primary use case.** The bridge round-trip test walks the documented happy path verbatim: invocation 1 → preflight question → write `pending_hitl.answer="approve"` + `status="answered"` → invocation 2 → refine-gate question. The shim correctly patches `orchestrator.substrate.select_substrate` (which is imported lazily inside `_InProcessOrchestrator._spawn_refiner` via `from . import select_substrate` per `in_process.py:420`, so the module-attribute patch propagates), shrinks the background-thread intervals to 50ms so the test doesn't block on the default 5s tick, and runs the **real** driver via `runpy.run_path(_driver, run_name='__main__')` rather than a re-implementation. The R2 test exercises the **real** `hook_entry.decide` via the fake's child subprocess and asserts on `{"decision": "block", "reason": ...}` matching the actual code shape. The rubric loader test exercises the real `_load_egg_sdlc_role_rubric` and verifies all four AC cases. No producer/consumer dead-ends — the tests trace the documented use cases end-to-end. - -**Pass 2 — Doc/code symmetry.** Two doc/code drifts worth noting (both non-blocking): - -1. **`test_bridge_flattened_round_trip.py:50-56` docstring claims the driver "respects either `EGG_PIPELINE_ID` from the env or a positional `argv[1]`".** The driver as shipped (verified via `grep -n EGG_PIPELINE_ID run_pipeline.py`) reads `EGG_REPO`, `EGG_PIPELINE_REPO`, `EGG_ISSUE_NUMBER`, `EGG_SUBSTRATE` from env but **does NOT** consult `EGG_PIPELINE_ID` — the pipeline id is positional-only via argparse (`run_pipeline.py:365-368`). The shim sets both as a hedge so the test passes either way, but the docstring's "whichever the coder picks in task-1-1" hedge is stale relative to the coder's actual choice. Fix: drop the env-or-positional claim from the docstring and just say "positional pipeline_id per `run_pipeline.py:_parse_args`." - -2. **`task-1-5` contract AC text vs `hook_entry.decide()` actual return shape.** The contract AC for task-1-5 (which I fetched via `mcp__sdlc__show_contract`) prescribes the hook verdict as `{"action": "deny", "message": "..."}` — but the real `hook_entry.decide()` at `orchestrator/substrate/claude_code/hook_entry.py:673` returns `{"decision": "block", "reason": ...}`. The tester correctly tested the code, not the stale AC text — but this is a contract-doc drift that would mislead a future implementer reading the AC. The tester didn't fix the contract text (and shouldn't — the contract is upstream artifact), but a `# AC text uses {action, deny, message} — actual hook shape is {decision, block, reason}; this test pins the implementation, not the stale wording` callout in the test docstring would prevent the next reader from being whiplashed. - -**Pass 3 — Synthetic-key / sentinel coordination.** Three cross-module coordination points exercised: - -1. **`pending_hitl.status = "answered"` is set by the test** (line 237 — `pending["status"] = "answered"`) alongside `pending["answer"] = "approve"`. This is exactly the coordination point I flagged in the coder's review: the driver only promotes `answer → answer_log` when `status == "answered"`. The tester writes both — so the test exercises the driver's strict-coordination path. **What is NOT exercised:** the failure shape where the skill body writes `answer` without setting `status="answered"` (the silent-drop case from coder finding #2). If the documenter's SKILL.md sets only `answer`, the production loop would wedge — a regression test that pins "answer-without-status → driver re-yields same decision" would have caught the silent-drop class. Non-blocking but worth adding. - -2. **Driver's pending_hitl schema vs test assertions on the envelope.** The test pins `pending_hitl.{pipeline_id, version, timestamp, decision}` (lines 308-320) — 4 of the 9 fields. It does NOT pin `status`, `result`, `error`, `answer`, or `answer_log`. This is consistent with the coder's incomplete STABLE-contract schema block (8-of-9 fields documented) but means the test would not catch a regression where the driver stopped persisting `answer_log` — exactly the cross-bridge schema-coordination point R17 mitigation depends on. Non-blocking but worth a `# Pin the full 9-field schema once the coder docstring catches up to the implementation` follow-up. - -3. **`r2-verdict.json` envelope shape.** The test writes `{"r2_verdict": "pass"}` per the AC, and the test docstring's failure variant is `{"r2_verdict": "fail", "reason": "..."}`. Future slice-5 R15 consumer reads from this path. The schema is documented in the test docstring (lines 41-43) and the AC. No consumer in the diff today; this is fine. - -**Pass 4 — Silent-fallback hunt.** The test suite covers three classes of silent-fallback regression: - -1. `test_driver_is_idempotent_when_answer_unchanged` pins "no new answer → driver re-yields same decision, MUST NOT advance silently" (lines 373-406). Good — catches the "operator answer would be lost" silent advance. -2. `test_dispatch_does_not_leak_egg_agent_role_into_parent_env` pins env isolation under nested-dispatch simulation. Good — catches a fake-that-mutates-parent-env regression. -3. `test_loader_rejects_path_traversal_role_name` pins "role names with `..` → ValueError, no silent file read". Good — catches the path-escape silent-fallback. - -**Gaps relative to coder findings:** the silent fallbacks I flagged in the coder review (corrupt-contract silent reset in `_read_contract`, non-dict envelope silent reset in `_coerce_envelope`, NotImplementedError-from-`_maybe_fence` → `status=error` path) have no regression test. Non-blocking — the AC didn't require them — but a `test_driver_handles_corrupt_contract_without_resetting_state` (writes garbage to the contract, asserts driver exits with a structured error rather than silently overwriting) and a `test_driver_treats_approve_continue_as_completed_not_error` (operator chooses approve_continue, driver's NotImplementedError path is special-cased to `status=completed` with the fence message) would close the holistic-lens loop on those findings. - -### Non-blocking - -- **`test_bridge_flattened_round_trip.py:50-56`** — drop the "either env or positional argv" hedge from the driver-invocation-contract docstring; the driver is positional-only. - -- **`test_bridge_flattened_round_trip.py:308-320`** — extend the Stage A envelope assertions to pin `pending_hitl.{status, answer_log, result, error}` so a future regression that drops `answer_log` (the load-bearing replay field for cross-process state) is caught immediately. Today the test would let a regression that removes `answer_log` pass. - -- **`test_pretooluse_hook_nested.py` top docstring** — add a `# AC text in contract task-1-5 uses {action, deny, message}; actual hook shape is {decision, block, reason} — this test pins the code, not the stale AC` callout so future readers don't churn on the contract/code drift. - -- **Add `test_driver_consumes_answer_only_when_status_answered`** — write `answer="approve"` to `pending_hitl` while leaving `status="pending"`, re-invoke, assert the driver did NOT advance the generator. This pins the strict-coordination contract the coder chose and would catch a documenter-side regression where the SKILL.md sets only `answer`. - -- **Add `test_driver_handles_corrupt_contract_without_silent_reset`** — write `garbage_not_json` to the contract file, re-invoke, assert the driver exits with a clear error and does NOT overwrite a present-but-corrupt contract with a fresh skeleton. This closes the silent-reset finding from the coder review. - -- **`test_rubric_loader.py:194-217`** — the path-traversal test asserts the error mentions "missing" OR "rubric"; verify against the actual error path. The loader for `"../../../etc/passwd"` (not in `_RUBRIC_LANDED_ROLES`, not in `_ROLE_RUBRIC_SLICES`) raises the "This role is not part of the #2717 rollout's rubric set" message (per `substrate/__init__.py:341-345`), which contains "rubric". OK — but the assertion could be tighter (e.g., assert the error does NOT contain `etc/passwd` or any absolute filesystem path that would suggest the loader actually tried to read). - -ACKing — all three tests are correct, the AC bullets are covered, and the adversarial probing is in the spirit of the tester's dual role. The non-blocking items are post-merge hygiene rather than slice-blocking gaps. - - -````yaml -id: 3aed5529-4188-4c -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - - shared/tests/test_rubric_loader.py - reason: "\nHolistic review of tester v1 (commit 9ec45ddf, tester-authored content\ - \ from 831239db + 2fca7e73) \u2014 ran all four mandatory passes against the\ - \ three test files: `test_bridge_flattened_round_trip.py` (new, 406 LOC), `test_pretooluse_hook_nested.py`\ - \ (new, 334 LOC), `test_rubric_loader.py` (new, 217 LOC).\n\n**Pass 1 \u2014\ - \ End-to-end primary use case.** The bridge round-trip test walks the documented\ - \ happy path verbatim: invocation 1 \u2192 preflight question \u2192 write `pending_hitl.answer=\"\ - approve\"` + `status=\"answered\"` \u2192 invocation 2 \u2192 refine-gate question.\ - \ The shim correctly patches `orchestrator.substrate.select_substrate` (which\ - \ is imported lazily inside `_InProcessOrchestrator._spawn_refiner` via `from\ - \ . import select_substrate` per `in_process.py:420`, so the module-attribute\ - \ patch propagates), shrinks the background-thread intervals to 50ms so the\ - \ test doesn't block on the default 5s tick, and runs the **real** driver via\ - \ `runpy.run_path(_driver, run_name='__main__')` rather than a re-implementation.\ - \ The R2 test exercises the **real** `hook_entry.decide` via the fake's child\ - \ subprocess and asserts on `{\"decision\": \"block\", \"reason\": ...}` matching\ - \ the actual code shape. The rubric loader test exercises the real `_load_egg_sdlc_role_rubric`\ - \ and verifies all four AC cases. No producer/consumer dead-ends \u2014 the\ - \ tests trace the documented use cases end-to-end.\n\n**Pass 2 \u2014 Doc/code\ - \ symmetry.** Two doc/code drifts worth noting (both non-blocking):\n\n1. **`test_bridge_flattened_round_trip.py:50-56`\ - \ docstring claims the driver \"respects either `EGG_PIPELINE_ID` from the env\ - \ or a positional `argv[1]`\".** The driver as shipped (verified via `grep -n\ - \ EGG_PIPELINE_ID run_pipeline.py`) reads `EGG_REPO`, `EGG_PIPELINE_REPO`, `EGG_ISSUE_NUMBER`,\ - \ `EGG_SUBSTRATE` from env but **does NOT** consult `EGG_PIPELINE_ID` \u2014\ - \ the pipeline id is positional-only via argparse (`run_pipeline.py:365-368`).\ - \ The shim sets both as a hedge so the test passes either way, but the docstring's\ - \ \"whichever the coder picks in task-1-1\" hedge is stale relative to the coder's\ - \ actual choice. Fix: drop the env-or-positional claim from the docstring and\ - \ just say \"positional pipeline_id per `run_pipeline.py:_parse_args`.\"\n\n\ - 2. **`task-1-5` contract AC text vs `hook_entry.decide()` actual return shape.**\ - \ The contract AC for task-1-5 (which I fetched via `mcp__sdlc__show_contract`)\ - \ prescribes the hook verdict as `{\"action\": \"deny\", \"message\": \"...\"\ - }` \u2014 but the real `hook_entry.decide()` at `orchestrator/substrate/claude_code/hook_entry.py:673`\ - \ returns `{\"decision\": \"block\", \"reason\": ...}`. The tester correctly\ - \ tested the code, not the stale AC text \u2014 but this is a contract-doc drift\ - \ that would mislead a future implementer reading the AC. The tester didn't\ - \ fix the contract text (and shouldn't \u2014 the contract is upstream artifact),\ - \ but a `# AC text uses {action, deny, message} \u2014 actual hook shape is\ - \ {decision, block, reason}; this test pins the implementation, not the stale\ - \ wording` callout in the test docstring would prevent the next reader from\ - \ being whiplashed.\n\n**Pass 3 \u2014 Synthetic-key / sentinel coordination.**\ - \ Three cross-module coordination points exercised:\n\n1. **`pending_hitl.status\ - \ = \"answered\"` is set by the test** (line 237 \u2014 `pending[\"status\"\ - ] = \"answered\"`) alongside `pending[\"answer\"] = \"approve\"`. This is exactly\ - \ the coordination point I flagged in the coder's review: the driver only promotes\ - \ `answer \u2192 answer_log` when `status == \"answered\"`. The tester writes\ - \ both \u2014 so the test exercises the driver's strict-coordination path. **What\ - \ is NOT exercised:** the failure shape where the skill body writes `answer`\ - \ without setting `status=\"answered\"` (the silent-drop case from coder finding\ - \ #2). If the documenter's SKILL.md sets only `answer`, the production loop\ - \ would wedge \u2014 a regression test that pins \"answer-without-status \u2192\ - \ driver re-yields same decision\" would have caught the silent-drop class.\ - \ Non-blocking but worth adding.\n\n2. **Driver's pending_hitl schema vs test\ - \ assertions on the envelope.** The test pins `pending_hitl.{pipeline_id, version,\ - \ timestamp, decision}` (lines 308-320) \u2014 4 of the 9 fields. It does NOT\ - \ pin `status`, `result`, `error`, `answer`, or `answer_log`. This is consistent\ - \ with the coder's incomplete STABLE-contract schema block (8-of-9 fields documented)\ - \ but means the test would not catch a regression where the driver stopped persisting\ - \ `answer_log` \u2014 exactly the cross-bridge schema-coordination point R17\ - \ mitigation depends on. Non-blocking but worth a `# Pin the full 9-field schema\ - \ once the coder docstring catches up to the implementation` follow-up.\n\n\ - 3. **`r2-verdict.json` envelope shape.** The test writes `{\"r2_verdict\": \"\ - pass\"}` per the AC, and the test docstring's failure variant is `{\"r2_verdict\"\ - : \"fail\", \"reason\": \"...\"}`. Future slice-5 R15 consumer reads from this\ - \ path. The schema is documented in the test docstring (lines 41-43) and the\ - \ AC. No consumer in the diff today; this is fine.\n\n**Pass 4 \u2014 Silent-fallback\ - \ hunt.** The test suite covers three classes of silent-fallback regression:\n\ - \n1. `test_driver_is_idempotent_when_answer_unchanged` pins \"no new answer\ - \ \u2192 driver re-yields same decision, MUST NOT advance silently\" (lines\ - \ 373-406). Good \u2014 catches the \"operator answer would be lost\" silent\ - \ advance.\n2. `test_dispatch_does_not_leak_egg_agent_role_into_parent_env`\ - \ pins env isolation under nested-dispatch simulation. Good \u2014 catches a\ - \ fake-that-mutates-parent-env regression.\n3. `test_loader_rejects_path_traversal_role_name`\ - \ pins \"role names with `..` \u2192 ValueError, no silent file read\". Good\ - \ \u2014 catches the path-escape silent-fallback.\n\n**Gaps relative to coder\ - \ findings:** the silent fallbacks I flagged in the coder review (corrupt-contract\ - \ silent reset in `_read_contract`, non-dict envelope silent reset in `_coerce_envelope`,\ - \ NotImplementedError-from-`_maybe_fence` \u2192 `status=error` path) have no\ - \ regression test. Non-blocking \u2014 the AC didn't require them \u2014 but\ - \ a `test_driver_handles_corrupt_contract_without_resetting_state` (writes garbage\ - \ to the contract, asserts driver exits with a structured error rather than\ - \ silently overwriting) and a `test_driver_treats_approve_continue_as_completed_not_error`\ - \ (operator chooses approve_continue, driver's NotImplementedError path is special-cased\ - \ to `status=completed` with the fence message) would close the holistic-lens\ - \ loop on those findings.\n\n### Non-blocking\n\n- **`test_bridge_flattened_round_trip.py:50-56`**\ - \ \u2014 drop the \"either env or positional argv\" hedge from the driver-invocation-contract\ - \ docstring; the driver is positional-only.\n\n- **`test_bridge_flattened_round_trip.py:308-320`**\ - \ \u2014 extend the Stage A envelope assertions to pin `pending_hitl.{status,\ - \ answer_log, result, error}` so a future regression that drops `answer_log`\ - \ (the load-bearing replay field for cross-process state) is caught immediately.\ - \ Today the test would let a regression that removes `answer_log` pass.\n\n\ - - **`test_pretooluse_hook_nested.py` top docstring** \u2014 add a `# AC text\ - \ in contract task-1-5 uses {action, deny, message}; actual hook shape is {decision,\ - \ block, reason} \u2014 this test pins the code, not the stale AC` callout so\ - \ future readers don't churn on the contract/code drift.\n\n- **Add `test_driver_consumes_answer_only_when_status_answered`**\ - \ \u2014 write `answer=\"approve\"` to `pending_hitl` while leaving `status=\"\ - pending\"`, re-invoke, assert the driver did NOT advance the generator. This\ - \ pins the strict-coordination contract the coder chose and would catch a documenter-side\ - \ regression where the SKILL.md sets only `answer`.\n\n- **Add `test_driver_handles_corrupt_contract_without_silent_reset`**\ - \ \u2014 write `garbage_not_json` to the contract file, re-invoke, assert the\ - \ driver exits with a clear error and does NOT overwrite a present-but-corrupt\ - \ contract with a fresh skeleton. This closes the silent-reset finding from\ - \ the coder review.\n\n- **`test_rubric_loader.py:194-217`** \u2014 the path-traversal\ - \ test asserts the error mentions \"missing\" OR \"rubric\"; verify against\ - \ the actual error path. The loader for `\"../../../etc/passwd\"` (not in `_RUBRIC_LANDED_ROLES`,\ - \ not in `_ROLE_RUBRIC_SLICES`) raises the \"This role is not part of the #2717\ - \ rollout's rubric set\" message (per `substrate/__init__.py:341-345`), which\ - \ contains \"rubric\". OK \u2014 but the assertion could be tighter (e.g., assert\ - \ the error does NOT contain `etc/passwd` or any absolute filesystem path that\ - \ would suggest the loader actually tried to read).\n\nACKing \u2014 all three\ - \ tests are correct, the AC bullets are covered, and the adversarial probing\ - \ is in the spirit of the tester's dual role. The non-blocking items are post-merge\ - \ hygiene rather than slice-blocking gaps.\n" - ack_version: 1 - version: 1 - slice_id: slice-1 -```` - -### [2026-05-19T19:33:45Z] reviewer_code_holistic (CONSENSUS_CONFIRMED): Confirmed by reviewer_code_holistic - -````yaml -id: 262ac678-e0d5-45 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-1 -```` - -### [2026-05-19T19:33:45Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 57198924-dc03-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:33:45Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d7f9a41f-1137-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:33:45Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 7070a952-bb81-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:33:49Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 60144848-a89b-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:49.461849+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:33:50Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: e8a38ba9-a5d5-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:50.178099+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:33:50Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 6526eb70-2306-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:50.180187+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:33:54Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 092b6f5d-0945-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:54.181347+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:33:57Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b5039b21-35ce-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:56.884305+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:33:59Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK - -````yaml -id: d2c3b688-aa29-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:59.138081+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:34:48Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE - -````yaml -id: b2e2fc17-67ae-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:21.893162+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:34:49Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 2af85bcf-d191-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:49.461849+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:34:50Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 076a693c-4084-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:50.180187+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:34:50Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 9c43e749-2b17-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:50.178099+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:34:54Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: d7d7e771-69e5-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:33:54.181347+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:34:57Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 4279d652-e118-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:31:56.884305+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:34:59Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK - -````yaml -id: d12e0dcf-d19f-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:59.138081+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] tester (CONSENSUS_PROPOSE): Proposal from tester - -slice-1 tester v2: addresses reviewer_code v1 blocking #1 (subprocess PYTHONPATH) and three non-blocking items. 21/21 tester-authored tests pass under reviewer_code's exact reproduction env (`PYTHONPATH=.:shared:orchestrator pytest `). - -**Blocking fix — subprocess PYTHONPATH (reviewer_code v1 #1):** -`integration_tests/regression/test_bridge_flattened_round_trip.py:168-196` — add `/orchestrator` to the subprocess shim's PYTHONPATH so bare-name top-level imports inside the `orchestrator/` tree resolve. Without this, `orchestrator/models.py:16` (`from slice_id_validation import SLICE_ID_PATTERN`) and `in_process.py:531-534`'s `from models import HITLDecision` fallback both fail, crashing the driver subprocess with `ModuleNotFoundError` before it yields the first HITL decision. Now mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` (test target) exactly. - -**Non-blocking improvements (reviewer_code v1):** -* test_bridge_flattened_round_trip.py — dropped stale "whichever the coder picks" docstring phrasing (driver locked in positional argv[1]); mirror the driver's ISO-8601 UTC timestamp format in `_write_answer` instead of `str(time.time())`. -* test_rubric_loader.py — extended `test_loader_accepts_enum_and_string_role` parametrization to cover the two NEW slice-1 roles (reviewer_refine, reviewer_agent_design), not just the regression role. Strengthened the path-traversal assertion to pin the allowlist's slice-fence branch (the structural defence) rather than the file-missing-on-disk branch. -* test_pretooluse_hook_nested.py — `r2-verdict.json` content is now derived from the dispatch outcome and written BEFORE the structured assertions, so slice-5's contingent R15 migration task sees an accurate empirical record (`{"r2_verdict": "fail", "reason": ...}`) on a regression rather than a stale optimistic "pass" file. - -**Three test files committed:** -1. `shared/tests/test_rubric_loader.py` — 4 AC cases + 3 adversarial probe groups (enum-vs-string × 3 roles, unshipped roles, path-traversal slice-fence branch). 14 cases. -2. `integration_tests/regression/test_bridge_flattened_round_trip.py` — Two-stage round-trip + driver-idempotency probe. Substrate isolation via `python -c` shim that monkey-patches `select_substrate` to a MagicMock bundle (no real Anthropic API). 2 cases. -3. `integration_tests/regression/test_pretooluse_hook_nested.py` — R2 nested-dispatch verdict (parent=architect + child=tester writing source must deny with tester-naming reason), cross-role probe (parent=coder + child=tester), in-role negative-control, dataclass shape pin, EGG_AGENT_ROLE leak guard. 5 cases. - -**Configured-check results:** -* `make lint` — exit 0. Ruff check, ruff format check, mypy, shellcheck, custom checks all green. -* `make security` — exit 0. (Bandit not installed in this sandbox.) -* `make test` — exit 4 (environmental — grimp module unavailable, conftest collision in select-tests fallback; reproduces on origin/main). The slice-1 tests pass cleanly under direct pytest invocation in both the canonical Makefile PYTHONPATH shape AND reviewer_code v1's `.:shared:orchestrator` reproduction shape (21/21). - -````yaml -id: a55131a3-b598-47 -phase: implement -metadata: - payload: - summary: "slice-1 tester v2: addresses reviewer_code v1 blocking #1 (subprocess\ - \ PYTHONPATH) and three non-blocking items. 21/21 tester-authored tests pass\ - \ under reviewer_code's exact reproduction env (`PYTHONPATH=.:shared:orchestrator\ - \ pytest `).\n\n**Blocking fix \u2014 subprocess PYTHONPATH (reviewer_code\ - \ v1 #1):**\n`integration_tests/regression/test_bridge_flattened_round_trip.py:168-196`\ - \ \u2014 add `/orchestrator` to the subprocess shim's PYTHONPATH so bare-name\ - \ top-level imports inside the `orchestrator/` tree resolve. Without this, `orchestrator/models.py:16`\ - \ (`from slice_id_validation import SLICE_ID_PATTERN`) and `in_process.py:531-534`'s\ - \ `from models import HITLDecision` fallback both fail, crashing the driver\ - \ subprocess with `ModuleNotFoundError` before it yields the first HITL decision.\ - \ Now mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` (test\ - \ target) exactly.\n\n**Non-blocking improvements (reviewer_code v1):**\n* test_bridge_flattened_round_trip.py\ - \ \u2014 dropped stale \"whichever the coder picks\" docstring phrasing (driver\ - \ locked in positional argv[1]); mirror the driver's ISO-8601 UTC timestamp\ - \ format in `_write_answer` instead of `str(time.time())`.\n* test_rubric_loader.py\ - \ \u2014 extended `test_loader_accepts_enum_and_string_role` parametrization\ - \ to cover the two NEW slice-1 roles (reviewer_refine, reviewer_agent_design),\ - \ not just the regression role. Strengthened the path-traversal assertion to\ - \ pin the allowlist's slice-fence branch (the structural defence) rather than\ - \ the file-missing-on-disk branch.\n* test_pretooluse_hook_nested.py \u2014\ - \ `r2-verdict.json` content is now derived from the dispatch outcome and written\ - \ BEFORE the structured assertions, so slice-5's contingent R15 migration task\ - \ sees an accurate empirical record (`{\"r2_verdict\": \"fail\", \"reason\"\ - : ...}`) on a regression rather than a stale optimistic \"pass\" file.\n\n**Three\ - \ test files committed:**\n1. `shared/tests/test_rubric_loader.py` \u2014 4\ - \ AC cases + 3 adversarial probe groups (enum-vs-string \xD7 3 roles, unshipped\ - \ roles, path-traversal slice-fence branch). 14 cases.\n2. `integration_tests/regression/test_bridge_flattened_round_trip.py`\ - \ \u2014 Two-stage round-trip + driver-idempotency probe. Substrate isolation\ - \ via `python -c` shim that monkey-patches `select_substrate` to a MagicMock\ - \ bundle (no real Anthropic API). 2 cases.\n3. `integration_tests/regression/test_pretooluse_hook_nested.py`\ - \ \u2014 R2 nested-dispatch verdict (parent=architect + child=tester writing\ - \ source must deny with tester-naming reason), cross-role probe (parent=coder\ - \ + child=tester), in-role negative-control, dataclass shape pin, EGG_AGENT_ROLE\ - \ leak guard. 5 cases.\n\n**Configured-check results:**\n* `make lint` \u2014\ - \ exit 0. Ruff check, ruff format check, mypy, shellcheck, custom checks all\ - \ green.\n* `make security` \u2014 exit 0. (Bandit not installed in this sandbox.)\n\ - * `make test` \u2014 exit 4 (environmental \u2014 grimp module unavailable,\ - \ conftest collision in select-tests fallback; reproduces on origin/main). The\ - \ slice-1 tests pass cleanly under direct pytest invocation in both the canonical\ - \ Makefile PYTHONPATH shape AND reviewer_code v1's `.:shared:orchestrator` reproduction\ - \ shape (21/21)." - attestation: - tests_execution_blocked: true - tests_execution_blocked_reason: '`make test` cannot complete in this sandbox: - (1) `grimp` Python module required by `scripts/select_tests/__main__.py` is - not installed and pypi is unreachable (`make` cannot `uv sync` the venv); - (2) when select-tests falls back to the full-suite path, pytest''s rootdir-discovery - walks both `tests/` and `shared/tests/` and fails with `ImportPathMismatchError`. - Directly invoking `pytest shared/tests/test_rubric_loader.py integration_tests/regression/test_bridge_flattened_round_trip.py - integration_tests/regression/test_pretooluse_hook_nested.py` with PYTHONPATH - set to either `shared:gateway:orchestrator:repo_root` (Makefile shape) or - `.:shared:orchestrator` (reviewer_code v1 repro shape) passes 21/21 tester-authored - tests. The 20 pre-existing failures in `integration_tests/regression/{test_unpushed_commit_salvage,test_recovery_invariants,test_salvage_ttl_sweep}` - reproduce on origin/main with my changes stashed (not caused by this slice).' - checks_passed: - - lint - - security - artifacts: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - risk_considered: "v2 changes are entirely test-side: PYTHONPATH widening (already-present\ - \ in Makefile), assertion strengthening, verdict-file derivation logic. No source\ - \ code changed. All assertions on the production behavior are preserved or strengthened;\ - \ none are loosened. The substrate-fake isolation is unchanged (MagicMock bundle),\ - \ so the subprocess-based tests still cannot reach the real Anthropic API. The\ - \ verdict-file derivation in test_pretooluse_hook_nested.py now writes \"fail\"\ - \ with a structured reason on a regression instead of always \"pass\" \u2014\ - \ this is a strictly downstream-handoff improvement (slice-5's R15 migration\ - \ task sees the empirical answer)." - commit_sha: 601df9041a5018b3c8deed3f8527ff5dcffde46e - files_changed: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - tests_run: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - tasks_satisfied: - - task-1-3 - - task-1-5 - - task-1-7 - version: 2 - commit_sha: 601df9041a5018b3c8deed3f8527ff5dcffde46e - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] orchestrator → reviewer_code_holistic (CONSENSUS_RE_REVIEW): Re-review required: tester submitted new proposal v2 - -Producer tester has submitted a new proposal (version 2) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: 4c7cbd12-1e94-41 -phase: implement -metadata: - producer_role: tester - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] orchestrator → reviewer_security (CONSENSUS_RE_REVIEW): Re-review required: tester submitted new proposal v2 - -Producer tester has submitted a new proposal (version 2) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: ba613aab-72c6-4c -phase: implement -metadata: - producer_role: tester - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] orchestrator → reviewer_concurrency (CONSENSUS_RE_REVIEW): Re-review required: tester submitted new proposal v2 - -Producer tester has submitted a new proposal (version 2) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: 7dfb5b24-fccb-40 -phase: implement -metadata: - producer_role: tester - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f67690f4-2d7a-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 607d333e-4bfc-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c4160373-4731-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 615a507a-004c-41 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ac99c6e0-e35a-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:35:24Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 83f039df-8b06-40 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:35:45Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 01d55f5c-9ef2-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:35:29.939223+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:35:46Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: 5b64ef1c-bf8f-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:35:29.997637+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:35:46Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 53da1e3e-4ff0-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:36:11Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: ec65b636-a8a6-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:35:53.207450+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:11Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,OVERSEER_ALERT,STATUS,CONSENSUS_NACK - -````yaml -id: 529ddc0d-803a-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:30:59.138081+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:11Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE,CONSENSUS_ACK - -````yaml -id: a8d35365-3663-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:02.373422+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:11Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 1f89f721-498f-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:36:35Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_NACK,CONSENSUS_PROPOSE,CONSENSUS_ACK - -````yaml -id: 6a0ed5a0-c4d2-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:21.845500+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:35Z] reviewer_security → tester (CONSENSUS_ACK): ACK from reviewer_security for tester - -Security-lens re-review of tester v2 (commit 601df9041). - -**v2 deltas reviewed:** PYTHONPATH fix in the subprocess shim, ISO-8601 timestamp normalisation, parametrisation extended to the new rubric-supported roles, path-traversal assertion strengthened, and r2-verdict.json now derived from the dispatch outcome. - -**Positive security signal — v1 non-blocking #1 addressed (`test_rubric_loader.py:194-240`).** - -The strengthened path-traversal test now asserts that the diagnostic identifies the role as `"not part of"` / `"rollout"` / `"rubric set"` rather than `"missing on disk"`. This pins the *structural* defence: the `_ROLE_RUBRIC_SLICES` allowlist intercepts BEFORE any `Path.is_file()` / `Path.read_text()` reaches a user-controlled path. A regression that dropped the allowlist (e.g., letting roles fall through to the filesystem-walk branch) would now fail this test — closing the existence-oracle vector flagged in security criteria §8 ("`Path.is_file()` on agent-supplied paths leaks filesystem layout"). Exactly the kind of structural pin the security lens cares about; the test is now load-bearing for the defence, not just for the behavioural outcome. - -**Verified clean (no new security concerns):** - -- **PYTHONPATH addition (`test_bridge_flattened_round_trip.py:178-191`).** Adds `/orchestrator` to the subprocess shim's `PYTHONPATH`. The addition is purely import-resolution scaffolding — it doesn't expose new arguments to argv, doesn't change which binary is invoked, and the path is derived from `_repo_root()` (a static path-walk from the test file, not from agent input). No injection or traversal surface introduced. Mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` which is the existing trusted shape. - -- **Timestamp normalisation (`test_bridge_flattened_round_trip.py:238` import + `_write_answer`).** Switches from `str(time.time())` to `datetime.now(UTC).isoformat()`. Cosmetic; no security impact. - -- **Parametrisation extension (`test_rubric_loader.py:148-165`).** Adds `enum-reviewer_refine` / `str-reviewer_refine` / `enum-reviewer_agent_design` / `str-reviewer_agent_design` cases to `test_loader_accepts_enum_and_string_role`. Pins the str→enum normalisation for the two NEW roles, so a future loader change that breaks `str(role)` for `reviewer_refine` (e.g., accidentally using `repr(role)`) is caught and the file-path lookup divergence is surfaced. Test improvement; the allowlist still gates the actual filesystem read. - -- **r2-verdict derived from outcome (`test_pretooluse_hook_nested.py:174-205`).** The verdict file now records `{"r2_verdict": "fail", "reason": ...}` when the structured assertions would fail, written BEFORE the assertions so slice-5's R15 contingent task gets an accurate empirical record even on regression. Defensive improvement; the failure-payload's `reason` field includes `verdict!r` and `reason!r` — these are derived from `hook_entry.decide(...)`'s output, not from agent input, so no log-injection or secret-leakage risk. The reason text is bounded by what the hook itself emits. - -### Non-blocking - -- **`test_pretooluse_hook_nested.py:185-205` — verdict payload's `reason` text is built via f-string interpolation of `repr(verdict)` and `repr(reason)`.** The values come from `hook_entry.decide(...)` (test-controlled) so no injection vector here, but if a future copy-paste of this pattern interpolates attacker-controlled data into a verdict reason, the `repr()` form would preserve any embedded backslash-escape sequences. Today the values are bounded; flag for the pattern's reuse only. - -- **Carryover from v1 non-blocking #2** (`test_bridge_flattened_round_trip.py:108-146`): The subprocess-isolation assumption in `_shim_source()` is still implicit. v2 did not document it in the shim docstring — keep this as a follow-up if the shim shape is reused in an in-process test where attribute-rebind would leak state. No new finding; not blocking. - -- **Carryover from v1 non-blocking #3** (`_write_r2_verdict` path construction): `pipeline_id` is still a test constant (`"pipeline-r2-nested"`); the path-traversal exposure documented in v1 remains a future-parameterisation concern only. - -````yaml -id: 6e2525a1-818f-47 -phase: implement -metadata: - payload: - artifact_references: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - reason: "Security-lens re-review of tester v2 (commit 601df9041).\n\n**v2 deltas\ - \ reviewed:** PYTHONPATH fix in the subprocess shim, ISO-8601 timestamp normalisation,\ - \ parametrisation extended to the new rubric-supported roles, path-traversal\ - \ assertion strengthened, and r2-verdict.json now derived from the dispatch\ - \ outcome.\n\n**Positive security signal \u2014 v1 non-blocking #1 addressed\ - \ (`test_rubric_loader.py:194-240`).**\n\nThe strengthened path-traversal test\ - \ now asserts that the diagnostic identifies the role as `\"not part of\"` /\ - \ `\"rollout\"` / `\"rubric set\"` rather than `\"missing on disk\"`. This pins\ - \ the *structural* defence: the `_ROLE_RUBRIC_SLICES` allowlist intercepts BEFORE\ - \ any `Path.is_file()` / `Path.read_text()` reaches a user-controlled path.\ - \ A regression that dropped the allowlist (e.g., letting roles fall through\ - \ to the filesystem-walk branch) would now fail this test \u2014 closing the\ - \ existence-oracle vector flagged in security criteria \xA78 (\"`Path.is_file()`\ - \ on agent-supplied paths leaks filesystem layout\"). Exactly the kind of structural\ - \ pin the security lens cares about; the test is now load-bearing for the defence,\ - \ not just for the behavioural outcome.\n\n**Verified clean (no new security\ - \ concerns):**\n\n- **PYTHONPATH addition (`test_bridge_flattened_round_trip.py:178-191`).**\ - \ Adds `/orchestrator` to the subprocess shim's `PYTHONPATH`. The addition\ - \ is purely import-resolution scaffolding \u2014 it doesn't expose new arguments\ - \ to argv, doesn't change which binary is invoked, and the path is derived from\ - \ `_repo_root()` (a static path-walk from the test file, not from agent input).\ - \ No injection or traversal surface introduced. Mirrors the Makefile's `PYTHONPATH\ - \ := shared:gateway:orchestrator` which is the existing trusted shape.\n\n-\ - \ **Timestamp normalisation (`test_bridge_flattened_round_trip.py:238` import\ - \ + `_write_answer`).** Switches from `str(time.time())` to `datetime.now(UTC).isoformat()`.\ - \ Cosmetic; no security impact.\n\n- **Parametrisation extension (`test_rubric_loader.py:148-165`).**\ - \ Adds `enum-reviewer_refine` / `str-reviewer_refine` / `enum-reviewer_agent_design`\ - \ / `str-reviewer_agent_design` cases to `test_loader_accepts_enum_and_string_role`.\ - \ Pins the str\u2192enum normalisation for the two NEW roles, so a future loader\ - \ change that breaks `str(role)` for `reviewer_refine` (e.g., accidentally using\ - \ `repr(role)`) is caught and the file-path lookup divergence is surfaced. Test\ - \ improvement; the allowlist still gates the actual filesystem read.\n\n- **r2-verdict\ - \ derived from outcome (`test_pretooluse_hook_nested.py:174-205`).** The verdict\ - \ file now records `{\"r2_verdict\": \"fail\", \"reason\": ...}` when the structured\ - \ assertions would fail, written BEFORE the assertions so slice-5's R15 contingent\ - \ task gets an accurate empirical record even on regression. Defensive improvement;\ - \ the failure-payload's `reason` field includes `verdict!r` and `reason!r` \u2014\ - \ these are derived from `hook_entry.decide(...)`'s output, not from agent input,\ - \ so no log-injection or secret-leakage risk. The reason text is bounded by\ - \ what the hook itself emits.\n\n### Non-blocking\n\n- **`test_pretooluse_hook_nested.py:185-205`\ - \ \u2014 verdict payload's `reason` text is built via f-string interpolation\ - \ of `repr(verdict)` and `repr(reason)`.** The values come from `hook_entry.decide(...)`\ - \ (test-controlled) so no injection vector here, but if a future copy-paste\ - \ of this pattern interpolates attacker-controlled data into a verdict reason,\ - \ the `repr()` form would preserve any embedded backslash-escape sequences.\ - \ Today the values are bounded; flag for the pattern's reuse only.\n\n- **Carryover\ - \ from v1 non-blocking #2** (`test_bridge_flattened_round_trip.py:108-146`):\ - \ The subprocess-isolation assumption in `_shim_source()` is still implicit.\ - \ v2 did not document it in the shim docstring \u2014 keep this as a follow-up\ - \ if the shim shape is reused in an in-process test where attribute-rebind would\ - \ leak state. No new finding; not blocking.\n\n- **Carryover from v1 non-blocking\ - \ #3** (`_write_r2_verdict` path construction): `pipeline_id` is still a test\ - \ constant (`\"pipeline-r2-nested\"`); the path-traversal exposure documented\ - \ in v1 remains a future-parameterisation concern only." - ack_version: 2 - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:36:35Z] reviewer_code → tester (CONSENSUS_ACK): ACK from reviewer_code for tester - - -Reviewed commit 601df9041 (slice-1 tester v2). Ran all three test files locally with the same reproduction command from my v1 NACK (`PYTHONPATH=.:shared:orchestrator pytest shared/tests/test_rubric_loader.py integration_tests/regression/test_bridge_flattened_round_trip.py integration_tests/regression/test_pretooluse_hook_nested.py -v`). **21/21 tests pass cleanly in 1.68s.** The v1 blocker is fully resolved and all five non-blocking items from v1 are also addressed. - -### Verification of v1 blocking fix - -1. **Blocking #1 (subprocess PYTHONPATH missing `/orchestrator`) — FIXED.** `integration_tests/regression/test_bridge_flattened_round_trip.py:178-191` now includes `str(repo_root / "orchestrator")` between `str(repo_root)` and `str(repo_root / "gateway")` in the subprocess `PYTHONPATH` list. The comment block (lines 168-186) explicitly documents *why* each entry is needed — naming `orchestrator/models.py:16`'s bare `from slice_id_validation import SLICE_ID_PATTERN` and `in_process.py:531-534`'s bare `from models import HITLDecision` fallback as the imports that fail without it. The fix mirrors the Makefile's `PYTHONPATH := shared:gateway:orchestrator` shape with absolute paths. **Both `test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged` now pass.** - -2. **Self-attestation gap — addressed in commit message.** Commit msg explicitly states "Tests verified: 21/21 pass under both the canonical Makefile PYTHONPATH shape and reviewer_code's `.:shared:orchestrator` reproduction shape." Confirmed empirically on my end. - -### Verification of v1 non-blocking polish (all addressed) - -- **Stale docstring (`test_bridge_flattened_round_trip.py:51-57`)** — "whichever the coder picks" phrasing replaced with "The coder's driver (task-1-1) accepts the pipeline id as a positional argv[1]; the shim passes it that way." Concrete, no longer over-constrains. -- **`time.time()` timestamp (`test_bridge_flattened_round_trip.py`)** — replaced with `datetime.now(UTC).isoformat()` at line 251 (matching the driver's `_now_iso()` format). Import updated at line 67 (`from datetime import UTC, datetime`). Test fixture now mirrors the driver's source-of-truth. -- **Parametrize new roles (`test_rubric_loader.py:149-160`)** — `test_loader_accepts_enum_and_string_role` now parametrizes all three roles: `refiner` (regression), `reviewer_refine` (new), `reviewer_agent_design` (new) — both enum and string shapes for each. Six test cases instead of two; all pass. -- **Path-traversal slice-fence pinning (`test_rubric_loader.py:209-228`)** — assertion now verifies the path-traversal value resolves to the slice-fence branch (which produces a "this role is not part of the rollout" diagnostic) rather than the file-not-on-disk branch. The structural defence (`_RUBRIC_LANDED_ROLES`) is what's load-bearing; the test now pins that. -- **r2-verdict.json derivation (`test_pretooluse_hook_nested.py`)** — verdict is now derived from the dispatch outcome and written BEFORE the structured assertions. On regression (dispatch fails to deny), the file records `{"r2_verdict": "fail", "reason": ...}` so slice-5 sees the empirical outcome rather than a stale optimistic value. The `_derive_r2_verdict` helper at lines 132-160 documents the pass/fail decision logic. - -### Acceptance criteria check (final) - -- **TASK-1-3** (`test_bridge_flattened_round_trip.py`): Test runs in <30s ✓ (~0.5s wall-clock for the round-trip case); does not require real Anthropic API ✓ (MagicMock substrate); AC bullet (a) — preflight question on first run — VERIFIED via `decision1.get("question") == _PREFLIGHT_QUESTION`; AC bullet (b) — refine-gate decision on second run — VERIFIED via `decision2.get("question") != _PREFLIGHT_QUESTION` plus `decision_type in {"phase_gate", "choice"}`. **Acceptance met.** -- **TASK-1-5** (`test_pretooluse_hook_nested.py`): Test runs in <60s ✓; uses TASK-1-9 fake ✓; asserts hook denies child write when parent role would allow ✓; writes verdict to `.egg-state//r2-verdict.json` ✓ (now with outcome-derived pass/fail payload); docstring documents empirical-vs-test-fake limitation ✓. **Acceptance met.** -- **TASK-1-7** (`test_rubric_loader.py`): All four required cases + adversarial probing ✓; runs cleanly ✓; correct location (`shared/tests/`) ✓. **Acceptance met.** - -### Non-blocking (tester v2) - -- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146` — `_shim_source` still inline.** Carried over from v1 non-blocking; not addressed. The inline string works fine now that the PYTHONPATH bug is fixed; an extracted `_bridge_shim.py` helper would be a future polish item. -- **`integration_tests/regression/test_pretooluse_hook_nested.py` — `_derive_r2_verdict` writes the verdict before the structured assertions.** This is the correct ordering for the downstream slice-5 handoff, but means a flaky assertion (e.g., a test infrastructure bug) could record a misleading "pass" on the .json. Worth a follow-up to add a `try/finally`-style "verdict from final outcome" pattern. Non-blocking. - -No new findings. The producer's work meets the contract acceptance for all three test files and the cross-module dependency on the coder's `bin/run_pipeline.py` and `_agent_tool_fake.py` is exercised end-to-end. - - -````yaml -id: eff20799-add4-49 -phase: implement -metadata: - payload: - artifact_references: - - shared/tests/test_rubric_loader.py - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - reason: "\nReviewed commit 601df9041 (slice-1 tester v2). Ran all three test files\ - \ locally with the same reproduction command from my v1 NACK (`PYTHONPATH=.:shared:orchestrator\ - \ pytest shared/tests/test_rubric_loader.py integration_tests/regression/test_bridge_flattened_round_trip.py\ - \ integration_tests/regression/test_pretooluse_hook_nested.py -v`). **21/21\ - \ tests pass cleanly in 1.68s.** The v1 blocker is fully resolved and all five\ - \ non-blocking items from v1 are also addressed.\n\n### Verification of v1 blocking\ - \ fix\n\n1. **Blocking #1 (subprocess PYTHONPATH missing `/orchestrator`)\ - \ \u2014 FIXED.** `integration_tests/regression/test_bridge_flattened_round_trip.py:178-191`\ - \ now includes `str(repo_root / \"orchestrator\")` between `str(repo_root)`\ - \ and `str(repo_root / \"gateway\")` in the subprocess `PYTHONPATH` list. The\ - \ comment block (lines 168-186) explicitly documents *why* each entry is needed\ - \ \u2014 naming `orchestrator/models.py:16`'s bare `from slice_id_validation\ - \ import SLICE_ID_PATTERN` and `in_process.py:531-534`'s bare `from models import\ - \ HITLDecision` fallback as the imports that fail without it. The fix mirrors\ - \ the Makefile's `PYTHONPATH := shared:gateway:orchestrator` shape with absolute\ - \ paths. **Both `test_bridge_flattened_round_trip` and `test_driver_is_idempotent_when_answer_unchanged`\ - \ now pass.**\n\n2. **Self-attestation gap \u2014 addressed in commit message.**\ - \ Commit msg explicitly states \"Tests verified: 21/21 pass under both the canonical\ - \ Makefile PYTHONPATH shape and reviewer_code's `.:shared:orchestrator` reproduction\ - \ shape.\" Confirmed empirically on my end.\n\n### Verification of v1 non-blocking\ - \ polish (all addressed)\n\n- **Stale docstring (`test_bridge_flattened_round_trip.py:51-57`)**\ - \ \u2014 \"whichever the coder picks\" phrasing replaced with \"The coder's\ - \ driver (task-1-1) accepts the pipeline id as a positional argv[1]; the shim\ - \ passes it that way.\" Concrete, no longer over-constrains.\n- **`time.time()`\ - \ timestamp (`test_bridge_flattened_round_trip.py`)** \u2014 replaced with `datetime.now(UTC).isoformat()`\ - \ at line 251 (matching the driver's `_now_iso()` format). Import updated at\ - \ line 67 (`from datetime import UTC, datetime`). Test fixture now mirrors the\ - \ driver's source-of-truth.\n- **Parametrize new roles (`test_rubric_loader.py:149-160`)**\ - \ \u2014 `test_loader_accepts_enum_and_string_role` now parametrizes all three\ - \ roles: `refiner` (regression), `reviewer_refine` (new), `reviewer_agent_design`\ - \ (new) \u2014 both enum and string shapes for each. Six test cases instead\ - \ of two; all pass.\n- **Path-traversal slice-fence pinning (`test_rubric_loader.py:209-228`)**\ - \ \u2014 assertion now verifies the path-traversal value resolves to the slice-fence\ - \ branch (which produces a \"this role is not part of the rollout\" diagnostic)\ - \ rather than the file-not-on-disk branch. The structural defence (`_RUBRIC_LANDED_ROLES`)\ - \ is what's load-bearing; the test now pins that.\n- **r2-verdict.json derivation\ - \ (`test_pretooluse_hook_nested.py`)** \u2014 verdict is now derived from the\ - \ dispatch outcome and written BEFORE the structured assertions. On regression\ - \ (dispatch fails to deny), the file records `{\"r2_verdict\": \"fail\", \"\ - reason\": ...}` so slice-5 sees the empirical outcome rather than a stale optimistic\ - \ value. The `_derive_r2_verdict` helper at lines 132-160 documents the pass/fail\ - \ decision logic.\n\n### Acceptance criteria check (final)\n\n- **TASK-1-3**\ - \ (`test_bridge_flattened_round_trip.py`): Test runs in <30s \u2713 (~0.5s wall-clock\ - \ for the round-trip case); does not require real Anthropic API \u2713 (MagicMock\ - \ substrate); AC bullet (a) \u2014 preflight question on first run \u2014 VERIFIED\ - \ via `decision1.get(\"question\") == _PREFLIGHT_QUESTION`; AC bullet (b) \u2014\ - \ refine-gate decision on second run \u2014 VERIFIED via `decision2.get(\"question\"\ - ) != _PREFLIGHT_QUESTION` plus `decision_type in {\"phase_gate\", \"choice\"\ - }`. **Acceptance met.**\n- **TASK-1-5** (`test_pretooluse_hook_nested.py`):\ - \ Test runs in <60s \u2713; uses TASK-1-9 fake \u2713; asserts hook denies child\ - \ write when parent role would allow \u2713; writes verdict to `.egg-state//r2-verdict.json`\ - \ \u2713 (now with outcome-derived pass/fail payload); docstring documents empirical-vs-test-fake\ - \ limitation \u2713. **Acceptance met.**\n- **TASK-1-7** (`test_rubric_loader.py`):\ - \ All four required cases + adversarial probing \u2713; runs cleanly \u2713\ - ; correct location (`shared/tests/`) \u2713. **Acceptance met.**\n\n### Non-blocking\ - \ (tester v2)\n\n- **`integration_tests/regression/test_bridge_flattened_round_trip.py:98-146`\ - \ \u2014 `_shim_source` still inline.** Carried over from v1 non-blocking; not\ - \ addressed. The inline string works fine now that the PYTHONPATH bug is fixed;\ - \ an extracted `_bridge_shim.py` helper would be a future polish item.\n- **`integration_tests/regression/test_pretooluse_hook_nested.py`\ - \ \u2014 `_derive_r2_verdict` writes the verdict before the structured assertions.**\ - \ This is the correct ordering for the downstream slice-5 handoff, but means\ - \ a flaky assertion (e.g., a test infrastructure bug) could record a misleading\ - \ \"pass\" on the .json. Worth a follow-up to add a `try/finally`-style \"verdict\ - \ from final outcome\" pattern. Non-blocking.\n\nNo new findings. The producer's\ - \ work meets the contract acceptance for all three test files and the cross-module\ - \ dependency on the coder's `bin/run_pipeline.py` and `_agent_tool_fake.py`\ - \ is exercised end-to-end.\n" - ack_version: 2 - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:36:35Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: fe90958e-0975-41 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:36:41Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: 3dad9998-7c15-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:41.779205+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:41Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 8b63a4a3-cd07-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:36:44Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 3619b18d-ec17-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:44.749608+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:45Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 013557eb-6382-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:35:29.939223+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:45Z] reviewer_code_holistic → tester (CONSENSUS_ACK): ACK from reviewer_code_holistic for tester - - -Holistic re-review of tester v2 (commit 601df904) against v1 (9ec45ddf). Diff scope: 100 added / 40 removed across the same three test files. Verified each delta against the four mandatory passes. - -**Pass 1 — End-to-end primary use case.** v2 fixes the blocking subprocess PYTHONPATH issue (reviewer_code's #1) by adding `/orchestrator` to the shim env, which is load-bearing for `orchestrator/models.py:16`'s bare-name `from slice_id_validation import SLICE_ID_PATTERN` import and `in_process.py:531-534`'s bare `from models import HITLDecision` fallback. Verified `orchestrator/slice_id_validation.py` exists and that `orchestrator/models.py:16` does the bare-name import. Without v2's fix the subprocess would crash with `ModuleNotFoundError` before the driver yielded its first HITL decision — so v1 wasn't actually exercising the use case end-to-end. v2 makes the primary refine round-trip walk-through real. ✓ - -**Pass 2 — Doc/code symmetry.** v2 updates the bridge test's "Driver invocation contract probed" docstring to drop the stale "either `EGG_PIPELINE_ID` env or positional `argv[1]`" hedge and accurately state that the driver accepts positional argv only (with the env still set so any downstream tool reading `EGG_PIPELINE_ID` sees the same id). Addresses my v1 non-blocking #1. The PYTHONPATH comment now enumerates each entry's purpose with concrete bare-import examples. ✓ - -**Pass 3 — Synthetic-key / sentinel coordination.** Three v2 improvements on the cross-module coordination front: -- `_write_answer` now writes `datetime.now(UTC).isoformat()` matching the driver's `_now_iso()` format at `run_pipeline.py:101-103`. The previous `str(time.time())` would have drifted from the driver's timestamp shape and forced any future schema validator to special-case the test fixture. v2 closes this drift. ✓ -- The R2 verdict file is now derived from the empirical dispatch outcome: when the structural assertions would fail (hook not denying, verdict dict not block-shaped, reason missing "tester"), v2 writes `{"r2_verdict": "fail", "reason": ""}` instead of optimistically writing `{"r2_verdict": "pass"}`. Slice-5's R15 migration task reads this file to decide whether to proceed — v1's optimistic constant would have lied to that downstream consumer; v2 reflects reality. Addresses my v1 non-blocking observation about empirical-vs-constant verdict. ✓ -- The verdict file is now written **before** the structural assertions, so a failed assertion still produces a record for the downstream slice-5 consumer instead of a missing-file silent gap. ✓ - -**Pass 4 — Silent-fallback hunt.** v2 strengthens two adversarial probes: -- `test_loader_accepts_enum_and_string_role` parametrize list extends from `{enum-refiner, str-refiner}` to also cover `{enum-reviewer_refine, str-reviewer_refine, enum-reviewer_agent_design, str-reviewer_agent_design}`. A future loader-normalisation regression that broke the str→enum path only for the **new** slice-1 roles would now be caught — the previous coverage only pinned the regression role. -- `test_loader_rejects_path_traversal_role_name` adds an assertion that the diagnostic must contain "not part of" / "rollout" / "rubric set" rather than "missing on disk" — pinning that the `_ROLE_RUBRIC_SLICES` allowlist **intercepts before** any `Path(...).is_file()` walk against attacker-controlled segments. This closes the information-leak existence-oracle vector I flagged in v1 non-blocking #6 and tightens the assertion considerably. ✓ - -**Net verdict:** v2 fixes one real blocker (the PYTHONPATH issue that prevented v1 tests from actually exercising the driver subprocess) and incorporates four of my v1 non-blocking observations into the code. All four holistic passes come back clean. Non-blocking items I did NOT see addressed in v2 are the missing regression tests I suggested (corrupt-contract silent reset, answer-without-status silent drop, NotImplementedError → status=completed) — these remain post-merge hygiene rather than slice blockers and were never a NACK in v1. - -### Non-blocking - -- v2 still doesn't add the regression tests I suggested for the coder's silent-fallback findings (`_read_contract` corrupt-JSON reset, status="answered" coordination, NotImplementedError fence). The AC didn't require them; flagging again as worthy post-merge follow-up rather than a blocker. - -- v2's r2-verdict "fail" payload writes a debug-shaped `reason` (`f"DispatchResult denied={...}; raw_decision={...}; reason={...}"`). When slice-5 R15 reads this, the structured fields would be easier to consume than a single repr-formatted string. Consider a richer payload — `{"denied": result.denied, "raw_decision": verdict, "reason": reason}` — under the failure path so the downstream consumer doesn't have to regex-parse a diagnostic blob. Non-blocking; the optimistic-pass-removal alone is the bigger win. - -ACKing. - - -````yaml -id: a4133543-536f-43 -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - - shared/tests/test_rubric_loader.py - reason: "\nHolistic re-review of tester v2 (commit 601df904) against v1 (9ec45ddf).\ - \ Diff scope: 100 added / 40 removed across the same three test files. Verified\ - \ each delta against the four mandatory passes.\n\n**Pass 1 \u2014 End-to-end\ - \ primary use case.** v2 fixes the blocking subprocess PYTHONPATH issue (reviewer_code's\ - \ #1) by adding `/orchestrator` to the shim env, which is load-bearing\ - \ for `orchestrator/models.py:16`'s bare-name `from slice_id_validation import\ - \ SLICE_ID_PATTERN` import and `in_process.py:531-534`'s bare `from models import\ - \ HITLDecision` fallback. Verified `orchestrator/slice_id_validation.py` exists\ - \ and that `orchestrator/models.py:16` does the bare-name import. Without v2's\ - \ fix the subprocess would crash with `ModuleNotFoundError` before the driver\ - \ yielded its first HITL decision \u2014 so v1 wasn't actually exercising the\ - \ use case end-to-end. v2 makes the primary refine round-trip walk-through real.\ - \ \u2713\n\n**Pass 2 \u2014 Doc/code symmetry.** v2 updates the bridge test's\ - \ \"Driver invocation contract probed\" docstring to drop the stale \"either\ - \ `EGG_PIPELINE_ID` env or positional `argv[1]`\" hedge and accurately state\ - \ that the driver accepts positional argv only (with the env still set so any\ - \ downstream tool reading `EGG_PIPELINE_ID` sees the same id). Addresses my\ - \ v1 non-blocking #1. The PYTHONPATH comment now enumerates each entry's purpose\ - \ with concrete bare-import examples. \u2713\n\n**Pass 3 \u2014 Synthetic-key\ - \ / sentinel coordination.** Three v2 improvements on the cross-module coordination\ - \ front:\n- `_write_answer` now writes `datetime.now(UTC).isoformat()` matching\ - \ the driver's `_now_iso()` format at `run_pipeline.py:101-103`. The previous\ - \ `str(time.time())` would have drifted from the driver's timestamp shape and\ - \ forced any future schema validator to special-case the test fixture. v2 closes\ - \ this drift. \u2713\n- The R2 verdict file is now derived from the empirical\ - \ dispatch outcome: when the structural assertions would fail (hook not denying,\ - \ verdict dict not block-shaped, reason missing \"tester\"), v2 writes `{\"\ - r2_verdict\": \"fail\", \"reason\": \"\"}` instead\ - \ of optimistically writing `{\"r2_verdict\": \"pass\"}`. Slice-5's R15 migration\ - \ task reads this file to decide whether to proceed \u2014 v1's optimistic constant\ - \ would have lied to that downstream consumer; v2 reflects reality. Addresses\ - \ my v1 non-blocking observation about empirical-vs-constant verdict. \u2713\ - \n- The verdict file is now written **before** the structural assertions, so\ - \ a failed assertion still produces a record for the downstream slice-5 consumer\ - \ instead of a missing-file silent gap. \u2713\n\n**Pass 4 \u2014 Silent-fallback\ - \ hunt.** v2 strengthens two adversarial probes:\n- `test_loader_accepts_enum_and_string_role`\ - \ parametrize list extends from `{enum-refiner, str-refiner}` to also cover\ - \ `{enum-reviewer_refine, str-reviewer_refine, enum-reviewer_agent_design, str-reviewer_agent_design}`.\ - \ A future loader-normalisation regression that broke the str\u2192enum path\ - \ only for the **new** slice-1 roles would now be caught \u2014 the previous\ - \ coverage only pinned the regression role.\n- `test_loader_rejects_path_traversal_role_name`\ - \ adds an assertion that the diagnostic must contain \"not part of\" / \"rollout\"\ - \ / \"rubric set\" rather than \"missing on disk\" \u2014 pinning that the `_ROLE_RUBRIC_SLICES`\ - \ allowlist **intercepts before** any `Path(...).is_file()` walk against attacker-controlled\ - \ segments. This closes the information-leak existence-oracle vector I flagged\ - \ in v1 non-blocking #6 and tightens the assertion considerably. \u2713\n\n\ - **Net verdict:** v2 fixes one real blocker (the PYTHONPATH issue that prevented\ - \ v1 tests from actually exercising the driver subprocess) and incorporates\ - \ four of my v1 non-blocking observations into the code. All four holistic passes\ - \ come back clean. Non-blocking items I did NOT see addressed in v2 are the\ - \ missing regression tests I suggested (corrupt-contract silent reset, answer-without-status\ - \ silent drop, NotImplementedError \u2192 status=completed) \u2014 these remain\ - \ post-merge hygiene rather than slice blockers and were never a NACK in v1.\n\ - \n### Non-blocking\n\n- v2 still doesn't add the regression tests I suggested\ - \ for the coder's silent-fallback findings (`_read_contract` corrupt-JSON reset,\ - \ status=\"answered\" coordination, NotImplementedError fence). The AC didn't\ - \ require them; flagging again as worthy post-merge follow-up rather than a\ - \ blocker.\n\n- v2's r2-verdict \"fail\" payload writes a debug-shaped `reason`\ - \ (`f\"DispatchResult denied={...}; raw_decision={...}; reason={...}\"`). When\ - \ slice-5 R15 reads this, the structured fields would be easier to consume than\ - \ a single repr-formatted string. Consider a richer payload \u2014 `{\"denied\"\ - : result.denied, \"raw_decision\": verdict, \"reason\": reason}` \u2014 under\ - \ the failure path so the downstream consumer doesn't have to regex-parse a\ - \ diagnostic blob. Non-blocking; the optimistic-pass-removal alone is the bigger\ - \ win.\n\nACKing.\n" - ack_version: 2 - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:36:48Z] reviewer_code (CONSENSUS_CONFIRMED): Confirmed by reviewer_code - -````yaml -id: 07dd4c4a-3203-4c -phase: implement -metadata: - consensus_reached: false - slice_id: slice-1 -```` - -### [2026-05-19T19:36:48Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 02b47dfd-da16-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:36:48Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 921cc5ae-4f18-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:36:52Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: 0e508f01-84fe-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:52.355972+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:52Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 209a5393-5a3f-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:52.354482+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:52Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: cca5ac65-4e87-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:36:52Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 521669d3-a09c-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:52.939357+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:53Z] reviewer_concurrency → tester (CONSENSUS_ACK): ACK from reviewer_concurrency for tester - - -**Concurrency-lens re-review of tester v2 (commit 601df9041)** - -Re-reviewed the v1→v2 delta with attention to concurrency invariants. No blocking concurrency findings; v2 does not introduce any new concurrency surface and the changes are well-contained. - -### v2 delta against v1 - -**1. `test_bridge_flattened_round_trip.py`** - -- **PYTHONPATH expansion** (`:166-189`): adds `/orchestrator` to the subprocess PYTHONPATH list. Same `os.pathsep.join(...)` construction, same per-subprocess env snapshot semantics. The new path enables bare-name imports inside the orchestrator tree (`orchestrator/models.py:16 from slice_id_validation import SLICE_ID_PATTERN`, `in_process.py:531-534 from models import HITLDecision` fallback). No new race surface — PYTHONPATH is read once at subprocess startup and never mutated thereafter. -- **Timestamp format swap** (`:246-252`): `str(time.time())` → `datetime.now(UTC).isoformat()`. `datetime.now(UTC)` returns a fresh immutable instance per call; thread-safe. The change aligns the test's `_write_answer` output with `run_pipeline.py:101-103`'s ISO-8601 envelope timestamp, eliminating a possible format-drift surface between the test fixture and the driver's source of truth. -- **Docstring clarification** (`:49-57`): purely documentation; no behavioural change. - -**2. `test_pretooluse_hook_nested.py`** - -- **r2-verdict write reordered to before assertions** (`:171-205`): the verdict file is now derived from the empirical `dispatch(...)` result and written *before* the `denied/decision/reason` assertions. Strictly sequential within the test function — no concurrency surface change. The reordering actually improves diagnostic value: if a future regression breaks one of the structured assertions, the verdict file still records the empirical failure (`{"r2_verdict": "fail", "reason": ...}`) for slice-5's R15 consumer rather than silently leaving a stale `pass` from a previous run. The fail-payload's `reason` field uses `repr()` on `verdict` which serialises any dict safely. -- **Verdict-deriving guard chain** (`:184-189`): uses `getattr(result, ..., None)` for all fields — safe defensive read, no AttributeError surface. The boolean chain is short-circuit-evaluated in declaration order; no side effects in any predicate. -- **Tightened non-empty dict assertion** (`:218-221`): `isinstance(verdict, dict) and verdict` adds an empty-dict catch. No concurrency relevance. - -**3. `test_rubric_loader.py`** - -- **Parametrization expansion** (`:148-164`): adds four new param cases for the new roles. Each parametrized test runs independently in pytest (and as separate test items under xdist). The loader reads distinct rubric files per role — no shared-state collision possible between workers. The `_load(role_input)` call is pure read-only file I/O. ✓ -- **Tightened path-traversal assertion** (`:226-240`): adds an assertion that the diagnostic specifically identifies the role as "not part of the rollout's rubric set" rather than "missing on disk". This pins the allowlist-fence-fires-first invariant, eliminating an existence-oracle vector via attacker-controlled `Path.is_file()` calls. Pure assertion change — no concurrency surface. - -### What v2 does NOT touch - -- The subprocess timeouts (`20s` in bridge test) are unchanged. -- The shim's `_HEARTBEAT_INTERVAL`/`_BRC_REVIEW_INTERVAL`/`_BUS_TICK_INTERVAL` overrides are unchanged (still 0.05s, still scoped to the subprocess only). -- The `sys.modules.pop` in the fake fixture is unchanged. -- The xdist-safe `tmp_path` isolation pattern is unchanged across all three files. -- BRC-protocol invariants: still not touched. - -### Carry-over of v1 non-blocking observations - -My v1 non-blockings (interval-knob defensive `hasattr`, module-cache widening, module-scoped `pytest.mark.timeout(30)`) remain unaddressed in v2 — none were prerequisites for ACK and v2's scope was explicitly the reviewer_code v1 NACK. They are still worth landing as a future polish pass. - -ACK v2 unconditionally on the concurrency lens. - - -````yaml -id: 42289b40-d041-49 -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_bridge_flattened_round_trip.py - - integration_tests/regression/test_pretooluse_hook_nested.py - - shared/tests/test_rubric_loader.py - reason: "\n**Concurrency-lens re-review of tester v2 (commit 601df9041)**\n\n\ - Re-reviewed the v1\u2192v2 delta with attention to concurrency invariants. No\ - \ blocking concurrency findings; v2 does not introduce any new concurrency surface\ - \ and the changes are well-contained.\n\n### v2 delta against v1\n\n**1. `test_bridge_flattened_round_trip.py`**\n\ - \n- **PYTHONPATH expansion** (`:166-189`): adds `/orchestrator` to the\ - \ subprocess PYTHONPATH list. Same `os.pathsep.join(...)` construction, same\ - \ per-subprocess env snapshot semantics. The new path enables bare-name imports\ - \ inside the orchestrator tree (`orchestrator/models.py:16 from slice_id_validation\ - \ import SLICE_ID_PATTERN`, `in_process.py:531-534 from models import HITLDecision`\ - \ fallback). No new race surface \u2014 PYTHONPATH is read once at subprocess\ - \ startup and never mutated thereafter.\n- **Timestamp format swap** (`:246-252`):\ - \ `str(time.time())` \u2192 `datetime.now(UTC).isoformat()`. `datetime.now(UTC)`\ - \ returns a fresh immutable instance per call; thread-safe. The change aligns\ - \ the test's `_write_answer` output with `run_pipeline.py:101-103`'s ISO-8601\ - \ envelope timestamp, eliminating a possible format-drift surface between the\ - \ test fixture and the driver's source of truth.\n- **Docstring clarification**\ - \ (`:49-57`): purely documentation; no behavioural change.\n\n**2. `test_pretooluse_hook_nested.py`**\n\ - \n- **r2-verdict write reordered to before assertions** (`:171-205`): the verdict\ - \ file is now derived from the empirical `dispatch(...)` result and written\ - \ *before* the `denied/decision/reason` assertions. Strictly sequential within\ - \ the test function \u2014 no concurrency surface change. The reordering actually\ - \ improves diagnostic value: if a future regression breaks one of the structured\ - \ assertions, the verdict file still records the empirical failure (`{\"r2_verdict\"\ - : \"fail\", \"reason\": ...}`) for slice-5's R15 consumer rather than silently\ - \ leaving a stale `pass` from a previous run. The fail-payload's `reason` field\ - \ uses `repr()` on `verdict` which serialises any dict safely.\n- **Verdict-deriving\ - \ guard chain** (`:184-189`): uses `getattr(result, ..., None)` for all fields\ - \ \u2014 safe defensive read, no AttributeError surface. The boolean chain is\ - \ short-circuit-evaluated in declaration order; no side effects in any predicate.\n\ - - **Tightened non-empty dict assertion** (`:218-221`): `isinstance(verdict,\ - \ dict) and verdict` adds an empty-dict catch. No concurrency relevance.\n\n\ - **3. `test_rubric_loader.py`**\n\n- **Parametrization expansion** (`:148-164`):\ - \ adds four new param cases for the new roles. Each parametrized test runs independently\ - \ in pytest (and as separate test items under xdist). The loader reads distinct\ - \ rubric files per role \u2014 no shared-state collision possible between workers.\ - \ The `_load(role_input)` call is pure read-only file I/O. \u2713\n- **Tightened\ - \ path-traversal assertion** (`:226-240`): adds an assertion that the diagnostic\ - \ specifically identifies the role as \"not part of the rollout's rubric set\"\ - \ rather than \"missing on disk\". This pins the allowlist-fence-fires-first\ - \ invariant, eliminating an existence-oracle vector via attacker-controlled\ - \ `Path.is_file()` calls. Pure assertion change \u2014 no concurrency surface.\n\ - \n### What v2 does NOT touch\n\n- The subprocess timeouts (`20s` in bridge test)\ - \ are unchanged.\n- The shim's `_HEARTBEAT_INTERVAL`/`_BRC_REVIEW_INTERVAL`/`_BUS_TICK_INTERVAL`\ - \ overrides are unchanged (still 0.05s, still scoped to the subprocess only).\n\ - - The `sys.modules.pop` in the fake fixture is unchanged.\n- The xdist-safe\ - \ `tmp_path` isolation pattern is unchanged across all three files.\n- BRC-protocol\ - \ invariants: still not touched.\n\n### Carry-over of v1 non-blocking observations\n\ - \nMy v1 non-blockings (interval-knob defensive `hasattr`, module-cache widening,\ - \ module-scoped `pytest.mark.timeout(30)`) remain unaddressed in v2 \u2014 none\ - \ were prerequisites for ACK and v2's scope was explicitly the reviewer_code\ - \ v1 NACK. They are still worth landing as a future polish pass.\n\nACK v2 unconditionally\ - \ on the concurrency lens.\n" - ack_version: 2 - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:36:53Z] orchestrator → tester (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 2) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: 67477ec2-0362-43 -phase: implement -metadata: - ready_to_confirm: true - version: 2 - slice_id: slice-1 -```` - -### [2026-05-19T19:36:59Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT,CONSENSUS_PROPOSE - -````yaml -id: 084491cb-b5da-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:36:59.018651+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:36:59Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 7928ea5e-5e99-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:37:00Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 12d05874-794c-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:37:00.068582+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:37:00Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: d999b500-5f1c-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:37:00.496179+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:37:00Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 755d0595-80f5-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:37:01Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: bb5f70a1-9575-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:37:01.328027+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:37:01Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f1b2fb7e-4310-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:37:03Z] tester (CONSENSUS_CONFIRMED): Confirmed by tester - -````yaml -id: bdb8bdd9-f544-4e -phase: implement -metadata: - consensus_reached: true - slice_id: slice-1 -```` - -### [2026-05-19T19:37:03Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 48996506-f063-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:37:03Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: dd6584ed-5c69-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:37:03Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: e1cd69f0-7aaf-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:37:07Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: eb84b080-aeea-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:37:07.259426+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:37:07Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 79f5ae08-565b-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` - -### [2026-05-19T19:37:08Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 483227c8-c965-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:37:08.010167+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:37:08Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 42c21fdf-3992-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:37:08.052053+00:00' - slice_id: slice-1 -```` - -### [2026-05-19T19:37:08Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ce6c5770-0884-40 -phase: implement -metadata: - state: WORKING - slice_id: slice-1 -```` diff --git a/.egg-state/brc-history/2717-implement-slice-2.json b/.egg-state/brc-history/2717-implement-slice-2.json deleted file mode 100644 index fd1c6e1564..0000000000 --- a/.egg-state/brc-history/2717-implement-slice-2.json +++ /dev/null @@ -1,8088 +0,0 @@ -[ - { - "id": "cb5e45c7-e6a0-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:39:02.257954+00:00", - "phase": "implement" - }, - { - "id": "b688d494-0f7c-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "reviewer_security ready, fetched origin and synced to slice-2 head; polling for CONSENSUS_PROPOSE from coder (already PROPOSED) and tester.", - "metadata": { - "state": "WAITING_FOR_EVENT", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:39:02.326662+00:00", - "phase": "implement" - }, - { - "id": "5dcff24e-8225-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:24.120054+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:39:24.165947+00:00", - "phase": "implement" - }, - { - "id": "874fa21d-052c-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:40:04.218631+00:00", - "phase": "implement" - }, - { - "id": "bef6f5c3-e49d-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:40:04.220295+00:00", - "phase": "implement" - }, - { - "id": "1fe3e739-6cf8-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:40:04.220814+00:00", - "phase": "implement" - }, - { - "id": "5f65c25f-d619-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:40:24.941857+00:00", - "phase": "implement" - }, - { - "id": "9de37278-5436-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:27.392669+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:40:27.436260+00:00", - "phase": "implement" - }, - { - "id": "be91b666-279e-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:51.121097+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:40:51.251404+00:00", - "phase": "implement" - }, - { - "id": "a52cda7a-a9e8-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:41:00.023618+00:00", - "phase": "implement" - }, - { - "id": "46f162fb-c572-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:41:04.321816+00:00", - "phase": "implement" - }, - { - "id": "bff4df5b-7d37-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:41:04.333597+00:00", - "phase": "implement" - }, - { - "id": "283f570f-17f7-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:41:25.011280+00:00", - "phase": "implement" - }, - { - "id": "b8c58a00-194c-40", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:27.392669+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:41:27.487956+00:00", - "phase": "implement" - }, - { - "id": "bc26e8ce-ef04-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:51.121097+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:41:51.313506+00:00", - "phase": "implement" - }, - { - "id": "2df0d298-f629-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:42:00.088700+00:00", - "phase": "implement" - }, - { - "id": "2bfe6081-9e2c-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:42:04.437812+00:00", - "phase": "implement" - }, - { - "id": "e7a6dac5-7045-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:42:04.450512+00:00", - "phase": "implement" - }, - { - "id": "84a135fa-b610-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:42:25.109402+00:00", - "phase": "implement" - }, - { - "id": "68207473-f343-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:27.392669+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:42:27.525352+00:00", - "phase": "implement" - }, - { - "id": "af33ffe3-9537-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:51.121097+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:42:51.406272+00:00", - "phase": "implement" - }, - { - "id": "9d4c1ee2-4661-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:43:00.143353+00:00", - "phase": "implement" - }, - { - "id": "32b6b7d1-808f-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:43:04.540207+00:00", - "phase": "implement" - }, - { - "id": "bd88de91-09ee-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:43:04.549378+00:00", - "phase": "implement" - }, - { - "id": "91f69dd9-a7df-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:43:25.143655+00:00", - "phase": "implement" - }, - { - "id": "67be32b0-c64a-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:51.121097+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:43:51.457227+00:00", - "phase": "implement" - }, - { - "id": "8fc1b8b7-0d0c-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:44:00.243679+00:00", - "phase": "implement" - }, - { - "id": "c37eaed5-f6ed-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:44:04.684897+00:00", - "phase": "implement" - }, - { - "id": "b1e55b0c-4149-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:44:04.692596+00:00", - "phase": "implement" - }, - { - "id": "3627d147-7325-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:44:25.202303+00:00", - "phase": "implement" - }, - { - "id": "fc216257-38c9-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:51.121097+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:44:51.502662+00:00", - "phase": "implement" - }, - { - "id": "e84fdf8b-1aed-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:45:00.388312+00:00", - "phase": "implement" - }, - { - "id": "eff3c61e-9940-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:45:04.764032+00:00", - "phase": "implement" - }, - { - "id": "0c10a0fa-30a0-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:45:04.788971+00:00", - "phase": "implement" - }, - { - "id": "bb3926ba-04d4-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:45:25.564037+00:00", - "phase": "implement" - }, - { - "id": "8c4e9010-03bb-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:51.121097+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:45:51.554182+00:00", - "phase": "implement" - }, - { - "id": "23753156-5d71-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:46:00.449319+00:00", - "phase": "implement" - }, - { - "id": "0ddc5e43-4f0f-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:46:04.815460+00:00", - "phase": "implement" - }, - { - "id": "67bc7d46-9a8d-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:46:04.866658+00:00", - "phase": "implement" - }, - { - "id": "2fd73284-1738-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:46:08.624715+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:46:08.670993+00:00", - "phase": "implement" - }, - { - "id": "74abad3c-3a18-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:46:25.625750+00:00", - "phase": "implement" - }, - { - "id": "7fb3ca8c-281e-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:39:44.960717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:07.600391+00:00", - "phase": "implement" - }, - { - "id": "ea5ce4f7-6b6e-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:38:59.274301+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:07.604439+00:00", - "phase": "implement" - }, - { - "id": "c5d2b7f3-8675-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:01.200359+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:07.605065+00:00", - "phase": "implement" - }, - { - "id": "dfeb0cc8-efe1-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:46:08.624715+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:08.799404+00:00", - "phase": "implement" - }, - { - "id": "97895310-e7a6-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:40:24.907002+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:25.657595+00:00", - "phase": "implement" - }, - { - "id": "71e86885-b8a1-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from documenter", - "body": "Slice-2 documenter v1: land plan-team rubrics + plan-phase SKILL.md updates.\n\ntask-2-3: Created four plan-team agent rubric files under plugins/egg-sdlc/skills/egg-sdlc/agents/ \u2014 architect.md, task_planner.md, risk_analyst.md, reviewer_plan.md \u2014 using underscore-style file names matching the loader at orchestrator/substrate/__init__.py:280-365. Each rubric has valid frontmatter (name, description) and a body that (a) mirrors the corresponding k3s-substrate prompt body from plugins/refine-plan/skills/refine-plan/agents/ (the substrate swap is structurally invisible to the role) and (b) follows the substrate-specific-notes shape from slice-1's reviewer_refine.md / reviewer_agent_design.md (worktree layout under EGG_WORKTREE_BASE, PreToolUse hook enforcement via build_agent_patterns, HITL-via-AskUserQuestion, concurrent peers in slice-2, output path stability). Each rubric explicitly references the substrate context (\"You are the **architect** running on the **Claude Code substrate** of egg's SDLC pipeline.\") and the role's specific outputs (architect \u2192 architecture analysis JSON; task_planner \u2192 slice DAG plan + YAML appendix; risk_analyst \u2192 risk register JSON; reviewer_plan \u2192 ACK/NACK on the three producer artifacts).\n\ntask-2-7: Updated plugins/egg-sdlc/skills/egg-sdlc/SKILL.md to document the plan phase as supported. Specifically: (1) bumped the rollout-status callout from \"slice 1 landed\" to \"slices 1 + 2 landed\" and enumerated both the refine and plan rosters; (2) replaced the \"Plan / implement / pr deferred\" bullet in \"What's NOT in this skill\" with a dedicated \"Plan phase\" subsection naming the four roles, their spawn order (architect solo first, then task_planner + risk_analyst concurrently, with reviewer_plan ACK/NACK on each producer edge), output paths, and the four plan-HITL gate options (approve / request_changes / change_approach / stop); (3) restructured the 8-step skill flow into 10 steps that walk the plan stage and re-position the phase fence on \"approve and continue to implement\" with a pointer to slice 3; (4) refreshed stale \"refine-only\" / \"refine-team subagents\" / artifact-path / failure-mode strings to cover both phases.\n\nBoth files are under the documenter's gateway-allowed pattern set (docs/, **/*.md, **/README.md). No tests were authored by this role \u2014 tester role owns integration_tests/regression/test_inprocess_plan_brc.py (task-2-4) and the in-process orchestrator's _run_plan stage / loader expansion are coder territory (task-2-1, task-2-2).", - "metadata": { - "payload": { - "summary": "Slice-2 documenter v1: land plan-team rubrics + plan-phase SKILL.md updates.\n\ntask-2-3: Created four plan-team agent rubric files under plugins/egg-sdlc/skills/egg-sdlc/agents/ \u2014 architect.md, task_planner.md, risk_analyst.md, reviewer_plan.md \u2014 using underscore-style file names matching the loader at orchestrator/substrate/__init__.py:280-365. Each rubric has valid frontmatter (name, description) and a body that (a) mirrors the corresponding k3s-substrate prompt body from plugins/refine-plan/skills/refine-plan/agents/ (the substrate swap is structurally invisible to the role) and (b) follows the substrate-specific-notes shape from slice-1's reviewer_refine.md / reviewer_agent_design.md (worktree layout under EGG_WORKTREE_BASE, PreToolUse hook enforcement via build_agent_patterns, HITL-via-AskUserQuestion, concurrent peers in slice-2, output path stability). Each rubric explicitly references the substrate context (\"You are the **architect** running on the **Claude Code substrate** of egg's SDLC pipeline.\") and the role's specific outputs (architect \u2192 architecture analysis JSON; task_planner \u2192 slice DAG plan + YAML appendix; risk_analyst \u2192 risk register JSON; reviewer_plan \u2192 ACK/NACK on the three producer artifacts).\n\ntask-2-7: Updated plugins/egg-sdlc/skills/egg-sdlc/SKILL.md to document the plan phase as supported. Specifically: (1) bumped the rollout-status callout from \"slice 1 landed\" to \"slices 1 + 2 landed\" and enumerated both the refine and plan rosters; (2) replaced the \"Plan / implement / pr deferred\" bullet in \"What's NOT in this skill\" with a dedicated \"Plan phase\" subsection naming the four roles, their spawn order (architect solo first, then task_planner + risk_analyst concurrently, with reviewer_plan ACK/NACK on each producer edge), output paths, and the four plan-HITL gate options (approve / request_changes / change_approach / stop); (3) restructured the 8-step skill flow into 10 steps that walk the plan stage and re-position the phase fence on \"approve and continue to implement\" with a pointer to slice 3; (4) refreshed stale \"refine-only\" / \"refine-team subagents\" / artifact-path / failure-mode strings to cover both phases.\n\nBoth files are under the documenter's gateway-allowed pattern set (docs/, **/*.md, **/README.md). No tests were authored by this role \u2014 tester role owns integration_tests/regression/test_inprocess_plan_brc.py (task-2-4) and the in-process orchestrator's _run_plan stage / loader expansion are coder territory (task-2-1, task-2-2).", - "attestation": {}, - "artifacts": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md" - ], - "risk_considered": "Risk: rubric file names use hyphens vs underscores. Mitigation: confirmed the loader at orchestrator/substrate/__init__.py:331 reads `agents/{role_name}.md` where role_name is the underscore-style enum value (e.g. task_planner, not task-planner), and _ROLE_RUBRIC_SLICES uses the underscore form. Files use underscores. Risk: rubric body might drift from k3s prompts. Mitigation: bodies were authored to mirror the corresponding refine-plan/skills/refine-plan/agents/{role}.md verbatim where possible (the substrate swap is structurally invisible per the slice's premise), with only the substrate-specific notes section replaced to match the slice-1 reviewer_refine.md / reviewer_agent_design.md shape. Risk: SKILL.md still references \"refine only\" in stale spots. Mitigation: grep-checked all \"refine\" mentions and updated the rollout callout, what-this-gets-you list, step flow, pending_hitl.result semantics, failure-modes section, and \"What's NOT in this skill\" header text.", - "commit_sha": "7122ca2d11d584068ae0f0c39b92c586e81c9d9e", - "files_changed": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md" - ], - "tests_run": [], - "tasks_satisfied": [ - "task-2-3", - "task-2-7" - ] - }, - "version": 1, - "commit_sha": "7122ca2d11d584068ae0f0c39b92c586e81c9d9e", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:26.029943+00:00", - "phase": "implement" - }, - { - "id": "a60805df-29e3-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:26.185893+00:00", - "phase": "implement" - }, - { - "id": "a7b21c91-8482-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:26.211293+00:00", - "phase": "implement" - }, - { - "id": "397e340b-149c-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:26.226172+00:00", - "phase": "implement" - }, - { - "id": "0eb7e144-b3fe-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:26.238312+00:00", - "phase": "implement" - }, - { - "id": "e518869b-3b4e-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:26.244274+00:00", - "phase": "implement" - }, - { - "id": "9394b2ec-3a0a-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:47.945375+00:00", - "phase": "implement" - }, - { - "id": "089aa004-5478-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_ON_ROLE", - "body": "", - "metadata": { - "state": "WAITING_ON_ROLE", - "waiting_on": "reviewer_code", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:47.960220+00:00", - "phase": "implement" - }, - { - "id": "2e2014f0-e529-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:48.339577+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:47:48.370859+00:00", - "phase": "implement" - }, - { - "id": "f8e183c3-d6c7-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:48:15.171337+00:00", - "phase": "implement" - }, - { - "id": "a5b442ea-a4a8-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:48:48.028724+00:00", - "phase": "implement" - }, - { - "id": "b41e4f83-9509-4f", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:48.339577+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:48:48.423024+00:00", - "phase": "implement" - }, - { - "id": "b49c7556-3233-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:48:54.937506+00:00", - "phase": "implement" - }, - { - "id": "d18bf9df-5a38-44", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_ON_ROLE", - "body": "tester slice-2: scaffolded test_inprocess_plan_brc.py, waiting on coder CONSENSUS_PROPOSE for task-2-1 (_run_plan) and task-2-2 (rubric loader expansion) to validate test assertions against the actual implementation.", - "metadata": { - "state": "WAITING_ON_ROLE", - "waiting_on": "coder", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:48:58.775331+00:00", - "phase": "implement" - }, - { - "id": "1fecbd1a-8226-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:49:15.265577+00:00", - "phase": "implement" - }, - { - "id": "0c055b2a-be46-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:17.619085+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:49:17.641550+00:00", - "phase": "implement" - }, - { - "id": "d76a4a3d-1cdc-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:49:48.108894+00:00", - "phase": "implement" - }, - { - "id": "35f0634d-8d17-4b", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:48.339577+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:49:48.493637+00:00", - "phase": "implement" - }, - { - "id": "07300379-8495-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:49.537771+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:49:49.587841+00:00", - "phase": "implement" - }, - { - "id": "9fd88587-8176-40", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:49:54.992852+00:00", - "phase": "implement" - }, - { - "id": "40d6f1c0-5b36-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:15.357238+00:00", - "phase": "implement" - }, - { - "id": "decba16e-2eb4-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:17.619085+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:17.719374+00:00", - "phase": "implement" - }, - { - "id": "1779f429-a65f-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:48.245586+00:00", - "phase": "implement" - }, - { - "id": "ca389f26-a98a-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:48.339577+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:48.560739+00:00", - "phase": "implement" - }, - { - "id": "1c73db17-bf37-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:49.537771+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:49.655980+00:00", - "phase": "implement" - }, - { - "id": "8e80e79c-e768-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "documenter", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for documenter", - "body": "\nReviewed 5 documentation files for task-2-3 (four plan-team rubrics) and task-2-7 (SKILL.md plan-phase section). Verified acceptance criteria for both tasks, body fidelity against the k3s-substrate counterparts in `plugins/refine-plan/skills/refine-plan/agents/`, link integrity, frontmatter validity, and loader-name alignment.\n\n### File-by-file analysis\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md** (new, 88 lines) \u2014 Frontmatter is informational-only per the slice-1 convention. Body opens with \"You are the **architect** running on the **Claude Code substrate**...\", explicitly meeting task-2-3's substrate-context requirement. The output JSON schema (`approach_summary`, `key_design_decisions`, `components_touched`, `ordering_constraints`, `open_questions_for_planner`) matches the k3s counterpart byte-for-byte. The \"What you do\" section adds the missing-from-k3s \"You run first, solo, before `task_planner` and `risk_analyst`\" sequencing clue, which is consistent with the SKILL.md narrative. The four substrate-specific notes (worktree, file-write restrictions, HITL, concurrent peers, output path stability) match the slice-1 pattern from `reviewer_refine.md` and `reviewer_agent_design.md`. Relative link `../../../../docs/architecture/claude-code-substrate.md` resolves correctly to `docs/architecture/claude-code-substrate.md`.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md** (new, 294 lines) \u2014 Body opens with \"You are the **task_planner** running on the **Claude Code substrate**...\". The full `[mode: ticket]` / `[mode: github_issue]` / `[mode: epic-fresh]` / `[mode: epic-reassess]` mode-switch block is preserved verbatim from the k3s version with light editorial trimming (Won't-Do comment template removed, Plan diff example reduced to a single sentence describing the cluster groups). The YAML appendix discipline section (block scalars, role mapping, `pr:` block requirements, DAG-is-a-forest rule) is intact. The output JSON schema (`plan_path`, `slice_count`, `task_count`, `roles_used`, `dag_shape_summary`, `critical_path_tasks`) matches the k3s counterpart. Substrate-specific notes match the slice-1 pattern.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md** (new, 99 lines) \u2014 Body opens with \"You are the **risk_analyst** running on the **Claude Code substrate**...\". The risk-record schema (`risks[].{name,category,likelihood,impact,evidence,mitigation,owns_task}` + `top_3_risks` + `blocking_concerns`) is preserved. The evidence and mitigation discipline sections are intact. The \"Concurrent peers in this slice\" note correctly explains the reviewer_plan reconciliation behavior when task_planner finalizes before risk_analyst's handoff is visible \u2014 useful guardrail.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md** (new, 102 lines) \u2014 Body opens with \"You are the **reviewer_plan** running on the **Claude Code substrate**...\". All eight review criteria (`alignment_with_analysis`, `task_breakdown`, `role_assignments`, `slice_dag_shape`, `test_strategy`, `rollback_plan`, `risk_coverage`, `pr_block`) match the k3s counterpart. Verdict JSON shape preserves `verdict`/`summary`/`analysis`/`suggestions`/`artifact_references`/`feedback`/`timestamp`. The \"Three review edges per cycle\" note correctly explains the open-NACK barrier in the in-process bus (the orchestrator rejects re-proposes with HTTP 409 once two-or-more *edges from this reviewer* across the three producers have NACKed). The verdict-path stability note explicitly addresses how the three per-edge verdicts get namespaced by producer role in the artifact handoff \u2014 this is a real coordination problem the rubric correctly flags.\n\nLoader alignment: all four files use underscored basenames (`architect.md`, `task_planner.md`, `risk_analyst.md`, `reviewer_plan.md`) matching `_load_egg_sdlc_role_rubric`'s `f\"{role_name}.md\"` pattern at `orchestrator/substrate/__init__.py:331` and the `_ROLE_RUBRIC_SLICES` entries for slice-2 at lines 250-253 \u2014 the loader expansion is task-2-2 (coder) and is not the documenter's responsibility, but the file basenames must match the keys, and they do.\n\n**plugins/egg-sdlc/skills/egg-sdlc/SKILL.md** (modified, +49/-10) \u2014 Task-2-7 acceptance criteria are satisfied:\n\n- The frontmatter description is updated to enumerate the plan-phase roster (\"plan-phase scope landed in slice 2 (architect + task_planner + risk_analyst + reviewer_plan)\") and to bump the bridge-gap-closed claim to \"refine + plan\".\n- The rollout-status callout is bumped from \"slice 1 of #2717 landed\" to \"slices 1 + 2 of #2717 landed\", with both rosters enumerated and the architect-first-then-task_planner+risk_analyst-concurrent ordering called out.\n- The \"Refine HITL gate\" step (step 7) is followed by a new \"Plan subagents run inside the next driver invocation\" step (8), a new \"Plan HITL gate\" step (9), and the phase fence is bumped to step 10 with its message updated to point past plan to slice 3 of the rollout.\n- The new \"Plan phase (landed in slice 2 of #2717)\" subsection (lines 235\u2013256) names the four roles, their spawn order, output paths, and the four standard plan-HITL gate options (approve / request_changes / change_approach / stop) \u2014 meeting the \"plan-HITL gate is named\" criterion.\n- The \"What's NOT in this skill\" section is updated: the \"Plan / implement / pr phases\" bullet is replaced with an \"Implement / pr phases\" bullet that points at slice 3 / slice 4 / slice 5 \u2014 meeting the \"plan-phase deferral no longer listed\" criterion.\n- Failure modes: the `NotImplementedError: claude-code substrate runs refine only` diagnostic is updated to `... refine + plan only` and re-aimed at \"tried to advance past the plan HITL gate\".\n\n### Non-blocking\n\n- **plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:14, 113, 286** \u2014 The \"slices 1 + 2 landed\" / \"closed for refine + plan\" / \"NotImplementedError: ... refine + plan only\" claims are forward-looking against the documenter's commit alone, since the coder's task-2-1 (plan stage in `_InProcessOrchestrator.run()`) and task-2-2 (rubric loader expansion) are still in flight. This is the normal BRC atomic-landing pattern (the slice converges before any of it lands), but it does mean a reader of the documenter's commit in isolation would see stale doc-vs-code state. No fix needed.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:53, 94, 105, 118, 126** \u2014 The k3s task-planner's epic-reassess Won't-Do comment template (\"Superseded by `` in the reassess of ``...\") and the full Plan diff example block were trimmed in the egg-sdlc copy. The substantive guidance (which `jira_action` to set, when to flag in-flight, the survivor-selection heuristic) is intact. Consider porting the Won't-Do template verbatim in a follow-up so the egg-sdlc task_planner emits the same comment shape the k3s task_planner does \u2014 keeps Won't-Do audit trails consistent across substrates. Not blocking because slice-2 is plan-team rubric setup, not epic-mode behavioral parity.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:27-34** \u2014 The \"Read all of these\" / inputs section lists five paths the Task context provides but does not enumerate `verdict_path` even though the body references it at line 59 (\"Also written to `verdict_path`\"). This mirrors slice-1's `reviewer_refine.md` pattern (which also references `verdict_path` only in the body, not in the inputs list) so consistency is preserved \u2014 but a clarifying bullet in inputs would help.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md:96** \u2014 The allow-list note for risk_analyst says only `.egg-state/agent-outputs/` (no `.egg-state/drafts/` since risk_analyst doesn't write the plan markdown). This is correct, but worth a note that the k3s patterns.py governs this and the substrate-specific note is informational; if `build_agent_patterns(role)` later expands the risk_analyst's allow-list, the rubric will fall out of sync.\n\nNo security, correctness, or robustness issues found. Documenter's submission ACKed.\n", - "metadata": { - "payload": { - "artifact_references": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md" - ], - "reason": "\nReviewed 5 documentation files for task-2-3 (four plan-team rubrics) and task-2-7 (SKILL.md plan-phase section). Verified acceptance criteria for both tasks, body fidelity against the k3s-substrate counterparts in `plugins/refine-plan/skills/refine-plan/agents/`, link integrity, frontmatter validity, and loader-name alignment.\n\n### File-by-file analysis\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md** (new, 88 lines) \u2014 Frontmatter is informational-only per the slice-1 convention. Body opens with \"You are the **architect** running on the **Claude Code substrate**...\", explicitly meeting task-2-3's substrate-context requirement. The output JSON schema (`approach_summary`, `key_design_decisions`, `components_touched`, `ordering_constraints`, `open_questions_for_planner`) matches the k3s counterpart byte-for-byte. The \"What you do\" section adds the missing-from-k3s \"You run first, solo, before `task_planner` and `risk_analyst`\" sequencing clue, which is consistent with the SKILL.md narrative. The four substrate-specific notes (worktree, file-write restrictions, HITL, concurrent peers, output path stability) match the slice-1 pattern from `reviewer_refine.md` and `reviewer_agent_design.md`. Relative link `../../../../docs/architecture/claude-code-substrate.md` resolves correctly to `docs/architecture/claude-code-substrate.md`.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md** (new, 294 lines) \u2014 Body opens with \"You are the **task_planner** running on the **Claude Code substrate**...\". The full `[mode: ticket]` / `[mode: github_issue]` / `[mode: epic-fresh]` / `[mode: epic-reassess]` mode-switch block is preserved verbatim from the k3s version with light editorial trimming (Won't-Do comment template removed, Plan diff example reduced to a single sentence describing the cluster groups). The YAML appendix discipline section (block scalars, role mapping, `pr:` block requirements, DAG-is-a-forest rule) is intact. The output JSON schema (`plan_path`, `slice_count`, `task_count`, `roles_used`, `dag_shape_summary`, `critical_path_tasks`) matches the k3s counterpart. Substrate-specific notes match the slice-1 pattern.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md** (new, 99 lines) \u2014 Body opens with \"You are the **risk_analyst** running on the **Claude Code substrate**...\". The risk-record schema (`risks[].{name,category,likelihood,impact,evidence,mitigation,owns_task}` + `top_3_risks` + `blocking_concerns`) is preserved. The evidence and mitigation discipline sections are intact. The \"Concurrent peers in this slice\" note correctly explains the reviewer_plan reconciliation behavior when task_planner finalizes before risk_analyst's handoff is visible \u2014 useful guardrail.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md** (new, 102 lines) \u2014 Body opens with \"You are the **reviewer_plan** running on the **Claude Code substrate**...\". All eight review criteria (`alignment_with_analysis`, `task_breakdown`, `role_assignments`, `slice_dag_shape`, `test_strategy`, `rollback_plan`, `risk_coverage`, `pr_block`) match the k3s counterpart. Verdict JSON shape preserves `verdict`/`summary`/`analysis`/`suggestions`/`artifact_references`/`feedback`/`timestamp`. The \"Three review edges per cycle\" note correctly explains the open-NACK barrier in the in-process bus (the orchestrator rejects re-proposes with HTTP 409 once two-or-more *edges from this reviewer* across the three producers have NACKed). The verdict-path stability note explicitly addresses how the three per-edge verdicts get namespaced by producer role in the artifact handoff \u2014 this is a real coordination problem the rubric correctly flags.\n\nLoader alignment: all four files use underscored basenames (`architect.md`, `task_planner.md`, `risk_analyst.md`, `reviewer_plan.md`) matching `_load_egg_sdlc_role_rubric`'s `f\"{role_name}.md\"` pattern at `orchestrator/substrate/__init__.py:331` and the `_ROLE_RUBRIC_SLICES` entries for slice-2 at lines 250-253 \u2014 the loader expansion is task-2-2 (coder) and is not the documenter's responsibility, but the file basenames must match the keys, and they do.\n\n**plugins/egg-sdlc/skills/egg-sdlc/SKILL.md** (modified, +49/-10) \u2014 Task-2-7 acceptance criteria are satisfied:\n\n- The frontmatter description is updated to enumerate the plan-phase roster (\"plan-phase scope landed in slice 2 (architect + task_planner + risk_analyst + reviewer_plan)\") and to bump the bridge-gap-closed claim to \"refine + plan\".\n- The rollout-status callout is bumped from \"slice 1 of #2717 landed\" to \"slices 1 + 2 of #2717 landed\", with both rosters enumerated and the architect-first-then-task_planner+risk_analyst-concurrent ordering called out.\n- The \"Refine HITL gate\" step (step 7) is followed by a new \"Plan subagents run inside the next driver invocation\" step (8), a new \"Plan HITL gate\" step (9), and the phase fence is bumped to step 10 with its message updated to point past plan to slice 3 of the rollout.\n- The new \"Plan phase (landed in slice 2 of #2717)\" subsection (lines 235\u2013256) names the four roles, their spawn order, output paths, and the four standard plan-HITL gate options (approve / request_changes / change_approach / stop) \u2014 meeting the \"plan-HITL gate is named\" criterion.\n- The \"What's NOT in this skill\" section is updated: the \"Plan / implement / pr phases\" bullet is replaced with an \"Implement / pr phases\" bullet that points at slice 3 / slice 4 / slice 5 \u2014 meeting the \"plan-phase deferral no longer listed\" criterion.\n- Failure modes: the `NotImplementedError: claude-code substrate runs refine only` diagnostic is updated to `... refine + plan only` and re-aimed at \"tried to advance past the plan HITL gate\".\n\n### Non-blocking\n\n- **plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:14, 113, 286** \u2014 The \"slices 1 + 2 landed\" / \"closed for refine + plan\" / \"NotImplementedError: ... refine + plan only\" claims are forward-looking against the documenter's commit alone, since the coder's task-2-1 (plan stage in `_InProcessOrchestrator.run()`) and task-2-2 (rubric loader expansion) are still in flight. This is the normal BRC atomic-landing pattern (the slice converges before any of it lands), but it does mean a reader of the documenter's commit in isolation would see stale doc-vs-code state. No fix needed.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:53, 94, 105, 118, 126** \u2014 The k3s task-planner's epic-reassess Won't-Do comment template (\"Superseded by `` in the reassess of ``...\") and the full Plan diff example block were trimmed in the egg-sdlc copy. The substantive guidance (which `jira_action` to set, when to flag in-flight, the survivor-selection heuristic) is intact. Consider porting the Won't-Do template verbatim in a follow-up so the egg-sdlc task_planner emits the same comment shape the k3s task_planner does \u2014 keeps Won't-Do audit trails consistent across substrates. Not blocking because slice-2 is plan-team rubric setup, not epic-mode behavioral parity.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:27-34** \u2014 The \"Read all of these\" / inputs section lists five paths the Task context provides but does not enumerate `verdict_path` even though the body references it at line 59 (\"Also written to `verdict_path`\"). This mirrors slice-1's `reviewer_refine.md` pattern (which also references `verdict_path` only in the body, not in the inputs list) so consistency is preserved \u2014 but a clarifying bullet in inputs would help.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md:96** \u2014 The allow-list note for risk_analyst says only `.egg-state/agent-outputs/` (no `.egg-state/drafts/` since risk_analyst doesn't write the plan markdown). This is correct, but worth a note that the k3s patterns.py governs this and the substrate-specific note is informational; if `build_agent_patterns(role)` later expands the risk_analyst's allow-list, the rubric will fall out of sync.\n\nNo security, correctness, or robustness issues found. Documenter's submission ACKed.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:54.743334+00:00", - "phase": "implement" - }, - { - "id": "7d5b6bee-164d-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:54.783913+00:00", - "phase": "implement" - }, - { - "id": "032629b3-ec28-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:50:55.045109+00:00", - "phase": "implement" - }, - { - "id": "18d3454d-d262-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:01.538453+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:01.614326+00:00", - "phase": "implement" - }, - { - "id": "90725577-e894-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by documenter (pending_acks)", - "body": "Agent documenter cannot confirm: producers ['tester', 'coder'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus.", - "metadata": { - "pending_acks": true, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:03.274930+00:00", - "phase": "implement" - }, - { - "id": "9ce76ac6-2fcb-49", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:14.455966+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:14.483481+00:00", - "phase": "implement" - }, - { - "id": "b2daffa6-f90a-4b", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:14.571271+00:00", - "phase": "implement" - }, - { - "id": "fbde1df4-2abc-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:15.426671+00:00", - "phase": "implement" - }, - { - "id": "f59f686d-f5e3-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:17.619085+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:17.785040+00:00", - "phase": "implement" - }, - { - "id": "4b910676-b140-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:22.922742+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:23.015436+00:00", - "phase": "implement" - }, - { - "id": "6adc06f8-c5ae-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:48.407562+00:00", - "phase": "implement" - }, - { - "id": "8d99d443-303a-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:49.537771+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:49.737644+00:00", - "phase": "implement" - }, - { - "id": "18f181bd-5547-40", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:51:55.147713+00:00", - "phase": "implement" - }, - { - "id": "16eb2347-36e8-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:01.538453+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:52:01.672409+00:00", - "phase": "implement" - }, - { - "id": "46874605-ce30-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:52:15.494243+00:00", - "phase": "implement" - }, - { - "id": "d43e23bc-e523-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:17.619085+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:52:17.859823+00:00", - "phase": "implement" - }, - { - "id": "647cbd0e-8192-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:22.922742+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:52:23.061592+00:00", - "phase": "implement" - }, - { - "id": "f63b2025-9dbb-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:52:48.760350+00:00", - "phase": "implement" - }, - { - "id": "709f5cb2-ba46-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:49.537771+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:52:49.835907+00:00", - "phase": "implement" - }, - { - "id": "62c11b0d-1b8f-4a", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:52:55.280264+00:00", - "phase": "implement" - }, - { - "id": "0dceb97c-16cf-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:01.538453+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:53:16.653773+00:00", - "phase": "implement" - }, - { - "id": "594ce0ea-847b-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:53:16.765712+00:00", - "phase": "implement" - }, - { - "id": "bf8d3f3b-10dc-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:17.619085+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:53:17.921673+00:00", - "phase": "implement" - }, - { - "id": "098472ac-e975-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:22.922742+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:53:41.980527+00:00", - "phase": "implement" - }, - { - "id": "77afe3d3-d596-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:54:07.033972+00:00", - "phase": "implement" - }, - { - "id": "2cf7d433-ff26-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:49.537771+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:54:07.064923+00:00", - "phase": "implement" - }, - { - "id": "329f3b64-43d0-4c", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:54:07.066286+00:00", - "phase": "implement" - }, - { - "id": "0de7b345-3289-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:01.538453+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:54:31.914597+00:00", - "phase": "implement" - }, - { - "id": "156f223d-0f92-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:54:31.915568+00:00", - "phase": "implement" - }, - { - "id": "e06e6505-99a1-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:17.619085+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:54:31.916548+00:00", - "phase": "implement" - }, - { - "id": "c079a4f8-9d74-4b", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:22.922742+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:54:38.156726+00:00", - "phase": "implement" - }, - { - "id": "1d686a2d-2672-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:55:23.382701+00:00", - "phase": "implement" - }, - { - "id": "685b986c-784e-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:49.537771+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:55:23.417081+00:00", - "phase": "implement" - }, - { - "id": "d6b2d098-86df-44", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:55:23.421116+00:00", - "phase": "implement" - }, - { - "id": "2bd0bd3c-6efe-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:01.538453+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:55:31.812902+00:00", - "phase": "implement" - }, - { - "id": "117a4a01-9961-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:15.136373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:55:31.849711+00:00", - "phase": "implement" - }, - { - "id": "9dc1aba5-6fcd-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:17.619085+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:55:31.959636+00:00", - "phase": "implement" - }, - { - "id": "ac56440d-50a3-4e", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:51:22.922742+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:55:38.214619+00:00", - "phase": "implement" - }, - { - "id": "6976d1dc-a3a1-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:47:36.510983+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:29.076516+00:00", - "phase": "implement" - }, - { - "id": "37798c1f-8589-4f", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:48:54.889725+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:29.082990+00:00", - "phase": "implement" - }, - { - "id": "69df766a-cb35-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:49:49.537771+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:29.083728+00:00", - "phase": "implement" - }, - { - "id": "18f645ea-a0b7-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from coder", - "body": "Slice-2 coder: wire plan-phase BRC stage (3 producers + 1 reviewer) on the in-process Claude Code substrate + extend the rubric loader to the plan team.\n\nTASK-2-1 (orchestrator/substrate/in_process.py) \u2014 added `_run_plan_phase` on `_InProcessOrchestrator`: after the refine HITL gate's `approve_continue` answer, a `ThreadPoolExecutor` spawns architect/task_planner/risk_analyst concurrently through the substrate bundle's `ClaudeCodeSpawner`, then reviewer_plan is dispatched once with the producer artifacts as its prompt input. `PeerConsensusTracker` (lazy-imported, pipeline-scoped) drives the BRC mechanics: `handle_propose` on each successful producer spawn, `handle_ack` for each reviewer\u2192producer edge, `handle_confirmed` per role. The stage then yields a plan-HITL gate (`HITLDecision` with `phase=\"plan\"`, options `approve_continue / request_changes / change_approach / stop`). `_maybe_fence`'s diagnostic now points at slice-3 / slice-4 of the #2717 rollout. The orchestrator drives the BRC transitions because the in-process spawner is synchronous (spawn-completion IS the signal that the subagent did its work) \u2014 production harness agents whose own emissions would be no-op duplicates and harness-faked tests both reach CONSENSUS_CONFIRMED on the same code path.\n\nTASK-2-2 (orchestrator/substrate/__init__.py) \u2014 extended `_RUBRIC_LANDED_ROLES` to include architect / task_planner / risk_analyst / reviewer_plan alongside the slice-1 refine roster. Implement-team roles continue to raise `ValueError` with the slice-3 pointer (structured-error contract preserved). The \"missing on disk\" fallback diagnostic now names both TASK-1-4 and TASK-2-3 so a future reviewer hitting the error knows which documenter task needs to land first.\n\nTASK-2-5 \u2014 closes as no-op: slice-1's `test_pretooluse_hook_denies_nested_child_write` already pinned the R2 verdict as pass (the hook denies a child write outside the child's role under nested dispatch). Per the task contract, no `sandbox/egg_agent_tools/handlers/restrictions.py` change is needed when R2 = pass.\n\nManual in-process smoke (harness-faked spawner, MagicMock subagents): preflight \u2192 refine gate \u2192 plan gate sequence yields the expected decisions; spawner is called exactly 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; `approve_continue` at the plan gate still raises `NotImplementedError` with the slice-3 / slice-4 pointer; a terminal answer at the refine gate (e.g. \"stop\") returns the refine artifact path without entering plan phase.", - "metadata": { - "payload": { - "summary": "Slice-2 coder: wire plan-phase BRC stage (3 producers + 1 reviewer) on the in-process Claude Code substrate + extend the rubric loader to the plan team.\n\nTASK-2-1 (orchestrator/substrate/in_process.py) \u2014 added `_run_plan_phase` on `_InProcessOrchestrator`: after the refine HITL gate's `approve_continue` answer, a `ThreadPoolExecutor` spawns architect/task_planner/risk_analyst concurrently through the substrate bundle's `ClaudeCodeSpawner`, then reviewer_plan is dispatched once with the producer artifacts as its prompt input. `PeerConsensusTracker` (lazy-imported, pipeline-scoped) drives the BRC mechanics: `handle_propose` on each successful producer spawn, `handle_ack` for each reviewer\u2192producer edge, `handle_confirmed` per role. The stage then yields a plan-HITL gate (`HITLDecision` with `phase=\"plan\"`, options `approve_continue / request_changes / change_approach / stop`). `_maybe_fence`'s diagnostic now points at slice-3 / slice-4 of the #2717 rollout. The orchestrator drives the BRC transitions because the in-process spawner is synchronous (spawn-completion IS the signal that the subagent did its work) \u2014 production harness agents whose own emissions would be no-op duplicates and harness-faked tests both reach CONSENSUS_CONFIRMED on the same code path.\n\nTASK-2-2 (orchestrator/substrate/__init__.py) \u2014 extended `_RUBRIC_LANDED_ROLES` to include architect / task_planner / risk_analyst / reviewer_plan alongside the slice-1 refine roster. Implement-team roles continue to raise `ValueError` with the slice-3 pointer (structured-error contract preserved). The \"missing on disk\" fallback diagnostic now names both TASK-1-4 and TASK-2-3 so a future reviewer hitting the error knows which documenter task needs to land first.\n\nTASK-2-5 \u2014 closes as no-op: slice-1's `test_pretooluse_hook_denies_nested_child_write` already pinned the R2 verdict as pass (the hook denies a child write outside the child's role under nested dispatch). Per the task contract, no `sandbox/egg_agent_tools/handlers/restrictions.py` change is needed when R2 = pass.\n\nManual in-process smoke (harness-faked spawner, MagicMock subagents): preflight \u2192 refine gate \u2192 plan gate sequence yields the expected decisions; spawner is called exactly 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; `approve_continue` at the plan gate still raises `NotImplementedError` with the slice-3 / slice-4 pointer; a terminal answer at the refine gate (e.g. \"stop\") returns the refine artifact path without entering plan phase.", - "attestation": { - "commit_sha": "3a466891e46661534c89f211a1d3d0a359dcb79e", - "commit_shas": [ - "3a466891e46661534c89f211a1d3d0a359dcb79e" - ], - "files_changed": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py" - ], - "checks_passed": [ - "manual_smoke_inprocess_plan_brc", - "manual_smoke_refine_terminal_stop", - "manual_smoke_plan_gate_fence", - "rubric_loader_regression", - "import_check" - ], - "tests_run": 5, - "no_test_changes_needed": true, - "no_test_changes_reason": "Coder role's allow-list excludes tests/ paths (TASK-2-4 / TASK-2-6 are tester-owned). The five manual smoke runs above exercise the same surface a pytest test would target; the slice-2 tester's `test_inprocess_plan_brc.py` lands the pytest equivalent under a separate role's commit." - }, - "artifacts": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py" - ], - "risk_considered": "Two non-trivial risks evaluated: (1) the orchestrator-side BRC bookkeeping (handle_propose/handle_ack/handle_confirmed called by `_run_plan_phase`) could double-count when a real harness agent later starts emitting its own BRC messages \u2014 acceptable because under the current cq-3 harness re-host model the subagents do NOT emit BRC messages (only the synchronous spawn-completion is the BRC signal), and a future Agent-tool dispatch migration (slice-5 R15) would be the natural place to swap to agent-emitted BRC. (2) The pipeline-scoped tracker reuses any existing tracker via `get_peer_consensus_tracker(self.pipeline_id)` \u2014 risk: a stale tracker from an aborted previous run could carry state across pipelines. Mitigated by the slice-1 spike's per-pipeline tracker key (`pipeline_id` only; no slice_id today) and by `cooldown_seconds=0` so AUTO_REPROPOSE debounce doesn't fire during the tight propose\u2192ack\u2192confirm sequence; the test path uses `remove_peer_consensus_tracker` between cycles, and production runs are pipeline-scoped per `select_substrate` lifecycle.", - "commit_sha": "3a466891e46661534c89f211a1d3d0a359dcb79e", - "files_changed": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py" - ], - "tests_run": [], - "tasks_satisfied": [ - "task-2-1", - "task-2-2", - "task-2-5" - ] - }, - "version": 1, - "commit_sha": "3a466891e46661534c89f211a1d3d0a359dcb79e", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:29.814590+00:00", - "phase": "implement" - }, - { - "id": "d8bcda3c-e2d9-40", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:30.027778+00:00", - "phase": "implement" - }, - { - "id": "bd6916e2-9e14-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:30.083179+00:00", - "phase": "implement" - }, - { - "id": "c68bf3e6-9857-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:30.092984+00:00", - "phase": "implement" - }, - { - "id": "93019684-b5e2-48", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:30.109077+00:00", - "phase": "implement" - }, - { - "id": "dd706814-85f7-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:30.118407+00:00", - "phase": "implement" - }, - { - "id": "5e6da642-b01f-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:30.127797+00:00", - "phase": "implement" - }, - { - "id": "71809853-f5db-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:30.138085+00:00", - "phase": "implement" - }, - { - "id": "d58782f6-1a2e-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:56:35.658120+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:54.020630+00:00", - "phase": "implement" - }, - { - "id": "70749f36-d5ef-40", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:56:36.073218+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:54.032430+00:00", - "phase": "implement" - }, - { - "id": "eb6c237b-cd1a-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:56:50.332240+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:54.131006+00:00", - "phase": "implement" - }, - { - "id": "0d9cdef9-34f1-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:54.217768+00:00", - "phase": "implement" - }, - { - "id": "19a7de8d-0b09-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:56.427670+00:00", - "phase": "implement" - }, - { - "id": "dee6d019-8ff3-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:56:56.434749+00:00", - "phase": "implement" - }, - { - "id": "7f60cf71-5d72-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:01.936285+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:57:01.965729+00:00", - "phase": "implement" - }, - { - "id": "809daca5-914c-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:57:02.812867+00:00", - "phase": "implement" - }, - { - "id": "24ea046c-dac1-45", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:01.936285+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:58:02.016755+00:00", - "phase": "implement" - }, - { - "id": "9accc37c-2c65-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:58:02.878719+00:00", - "phase": "implement" - }, - { - "id": "29b5dbc4-5058-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:01.936285+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:59:02.119119+00:00", - "phase": "implement" - }, - { - "id": "37ff7d1b-2da1-4e", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T19:59:02.948541+00:00", - "phase": "implement" - }, - { - "id": "f7349dff-b210-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:01.936285+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:00:02.217809+00:00", - "phase": "implement" - }, - { - "id": "7438ff63-7a0f-4e", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:00:03.049118+00:00", - "phase": "implement" - }, - { - "id": "07f37f09-26bc-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_security for coder", - "body": "\nReviewed the slice-2 coder diff at commit 3a466891e through the security lens. Read the orchestrator/substrate/__init__.py loader extension, the new _run_plan_phase / _spawn_plan_producer / _spawn_plan_reviewer / _format_plan_placeholder / _build_plan_gate_decision / _answer_continues_past_refine code paths in orchestrator/substrate/in_process.py, and cross-referenced the four plan-team rubric files landed by the documenter at commit 7122ca2d1. No security findings.\n\n### Lens checks\n1. **Cross-file allowlist mismatch (\u00a71):** `_RUBRIC_LANDED_ROLES` extension at __init__.py:272-286 (`architect`, `task_planner`, `risk_analyst`, `reviewer_plan`) matches the four `.md` files the documenter shipped under `plugins/egg-sdlc/skills/egg-sdlc/agents/` (ls -la confirms presence on the merged branch). `_ROLE_RUBRIC_SLICES` (line 244-263) lists each of the four under `slice-2`, consistent with the loader's \"supported but not landed\" \u2192 \"landed\" promotion path. The loader fence at __init__.py:348-360 raises with the correct slice pointer for any not-yet-landed role, and the path is only read after the role passes the allowlist gate \u2014 so the `f\"{role_name}.md\"` interpolation cannot traverse out of `agents/` even if `role_name` were attacker-controlled (which it isn't; it's an `AgentRole` enum value).\n\n2. **Handler-vs-validator path mismatch (\u00a72):** Not applicable \u2014 no new public/HTTP entrypoint is added. The plan phase runs in-process under the orchestrator's own generator; the only external surface (`_build_plan_gate_decision`'s `HITLDecision`) is read by the trusted operator via `AskUserQuestion`.\n\n3. **Information-disclosure / authz bypass (\u00a73):** `_format_plan_placeholder` writes per-producer diagnostics into `.egg-state/drafts/-plan.md`, including 500-char truncated stdout, `commit_sha`, and `Exception` repr. This mirrors the slice-1 refiner-placeholder pattern (also committed to drafts/) and the file is gateway-allowed under documenter-pattern writes \u2014 it is not a NEW exposure relative to slice-1's surface, and stdout from a worktree-isolated agent is not expected to carry orchestrator-side secrets. The plan-gate `HITLDecision` surfaces `blocking_agents` and `unresolved_nack_details` to the operator only, not over the network.\n\n4. **Uncommitted-artifact / symlink mismatch (\u00a74):** Every path-string the diff introduces (the four `agents/.md` rubrics, the four `_RUBRIC_LANDED_ROLES` entries) has a corresponding file committed by the documenter at 7122ca2d1 \u2014 `ls -la plugins/egg-sdlc/skills/egg-sdlc/agents/` shows all four present with non-zero sizes. No Dockerfile / packaging-manifest references to verify (the diff is Python + markdown only).\n\n5. **Credential-shim modifications (\u00a75):** No changes under `sandbox/scripts/`; the credential-routing invariant is untouched.\n\n6. **Secret leakage (\u00a76):** `spawn_env = {**self.env, \"EGG_PIPELINE_ID\": ..., \"EGG_AGENT_ROLE\": role.value, ...}` propagates the orchestrator's env to each producer subprocess \u2014 identical to the existing refiner spawn pattern. The producers run inside isolated worktrees under `` and each rubric explicitly fences their writes to `.egg-state/drafts/` and/or `.egg-state/agent-outputs/` via the PreToolUse hook; no new sink for secrets is introduced. The `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` constant is intentionally obvious in log output and carries no credential value.\n\n7. **Cross-file OWASP top-10 (\u00a77):** No SQL, no HTML rendering, no URL dereferencing, no deserialization of untrusted data is introduced. The `tracker.handle_propose` / `handle_ack` / `handle_confirmed` calls feed JSON-serializable Python dicts into an in-process tracker; the producer artifact paths in the ACK payload are orchestrator-derived from `drafts_dir / f\"{artifact_id}-plan.md\"`, not agent input.\n\n8. **Agent-supplied paths in read-only access (\u00a78):** All filesystem accesses in this diff use orchestrator-derived paths \u2014 `plan_artifact_path` is built from `drafts_dir` + `self.issue_number or self.pipeline_id`, `refine_artifact_path` is the prior-stage `_artifact_path`, and `producer_artifacts` is a Mapping built internally from the spawner's worktree allocations. No tool boundary in this diff accepts an external path and reads/stats it without a workspace-root check.\n\n### Non-blocking\n- **orchestrator/substrate/in_process.py:328-373** \u2014 three `_spawn_plan_producer` calls run concurrently in a `ThreadPoolExecutor` and each calls `_write_active_role_sentinel(role.value)` against the shared per-user `$HOME/.claude/egg-active-role.json` (sentinel docstring at :1195-1202 already names this as the \"single-valued, per-user file cannot disambiguate two concurrent sub-agents in different roles\" limitation). The race is constrained to roles whose allow-lists are all subsets of `.egg-state/` so there is no escape from the orchestrator's restriction set, and `EGG_AGENT_ROLE` is set in each subprocess's `spawn_env` so the sentinel is only consulted as a hook fallback. Surfacing here for the security-lens audit trail; the structural fix is reviewer_concurrency / R2-deferral scope, not a blocker for this slice.\n- **orchestrator/substrate/in_process.py:388-420** \u2014 `tracker.handle_ack` is recorded on every plan producer whose spawn returned exit_code==0 without inspecting the reviewer_plan verdict JSON in `.egg-state/agent-outputs/-reviewer_plan-output.json`. Not a security boundary (all in-process trusted code), but the orchestrator's \"ACK on the reviewer's behalf\" semantics deserve a reviewer_code look \u2014 if a future change made the reviewer's verdict load-bearing for downstream security policy, this would need to read the verdict file. Out of scope for the security lens today.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/__init__.py", - "orchestrator/substrate/in_process.py", - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md" - ], - "reason": "\nReviewed the slice-2 coder diff at commit 3a466891e through the security lens. Read the orchestrator/substrate/__init__.py loader extension, the new _run_plan_phase / _spawn_plan_producer / _spawn_plan_reviewer / _format_plan_placeholder / _build_plan_gate_decision / _answer_continues_past_refine code paths in orchestrator/substrate/in_process.py, and cross-referenced the four plan-team rubric files landed by the documenter at commit 7122ca2d1. No security findings.\n\n### Lens checks\n1. **Cross-file allowlist mismatch (\u00a71):** `_RUBRIC_LANDED_ROLES` extension at __init__.py:272-286 (`architect`, `task_planner`, `risk_analyst`, `reviewer_plan`) matches the four `.md` files the documenter shipped under `plugins/egg-sdlc/skills/egg-sdlc/agents/` (ls -la confirms presence on the merged branch). `_ROLE_RUBRIC_SLICES` (line 244-263) lists each of the four under `slice-2`, consistent with the loader's \"supported but not landed\" \u2192 \"landed\" promotion path. The loader fence at __init__.py:348-360 raises with the correct slice pointer for any not-yet-landed role, and the path is only read after the role passes the allowlist gate \u2014 so the `f\"{role_name}.md\"` interpolation cannot traverse out of `agents/` even if `role_name` were attacker-controlled (which it isn't; it's an `AgentRole` enum value).\n\n2. **Handler-vs-validator path mismatch (\u00a72):** Not applicable \u2014 no new public/HTTP entrypoint is added. The plan phase runs in-process under the orchestrator's own generator; the only external surface (`_build_plan_gate_decision`'s `HITLDecision`) is read by the trusted operator via `AskUserQuestion`.\n\n3. **Information-disclosure / authz bypass (\u00a73):** `_format_plan_placeholder` writes per-producer diagnostics into `.egg-state/drafts/-plan.md`, including 500-char truncated stdout, `commit_sha`, and `Exception` repr. This mirrors the slice-1 refiner-placeholder pattern (also committed to drafts/) and the file is gateway-allowed under documenter-pattern writes \u2014 it is not a NEW exposure relative to slice-1's surface, and stdout from a worktree-isolated agent is not expected to carry orchestrator-side secrets. The plan-gate `HITLDecision` surfaces `blocking_agents` and `unresolved_nack_details` to the operator only, not over the network.\n\n4. **Uncommitted-artifact / symlink mismatch (\u00a74):** Every path-string the diff introduces (the four `agents/.md` rubrics, the four `_RUBRIC_LANDED_ROLES` entries) has a corresponding file committed by the documenter at 7122ca2d1 \u2014 `ls -la plugins/egg-sdlc/skills/egg-sdlc/agents/` shows all four present with non-zero sizes. No Dockerfile / packaging-manifest references to verify (the diff is Python + markdown only).\n\n5. **Credential-shim modifications (\u00a75):** No changes under `sandbox/scripts/`; the credential-routing invariant is untouched.\n\n6. **Secret leakage (\u00a76):** `spawn_env = {**self.env, \"EGG_PIPELINE_ID\": ..., \"EGG_AGENT_ROLE\": role.value, ...}` propagates the orchestrator's env to each producer subprocess \u2014 identical to the existing refiner spawn pattern. The producers run inside isolated worktrees under `` and each rubric explicitly fences their writes to `.egg-state/drafts/` and/or `.egg-state/agent-outputs/` via the PreToolUse hook; no new sink for secrets is introduced. The `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` constant is intentionally obvious in log output and carries no credential value.\n\n7. **Cross-file OWASP top-10 (\u00a77):** No SQL, no HTML rendering, no URL dereferencing, no deserialization of untrusted data is introduced. The `tracker.handle_propose` / `handle_ack` / `handle_confirmed` calls feed JSON-serializable Python dicts into an in-process tracker; the producer artifact paths in the ACK payload are orchestrator-derived from `drafts_dir / f\"{artifact_id}-plan.md\"`, not agent input.\n\n8. **Agent-supplied paths in read-only access (\u00a78):** All filesystem accesses in this diff use orchestrator-derived paths \u2014 `plan_artifact_path` is built from `drafts_dir` + `self.issue_number or self.pipeline_id`, `refine_artifact_path` is the prior-stage `_artifact_path`, and `producer_artifacts` is a Mapping built internally from the spawner's worktree allocations. No tool boundary in this diff accepts an external path and reads/stats it without a workspace-root check.\n\n### Non-blocking\n- **orchestrator/substrate/in_process.py:328-373** \u2014 three `_spawn_plan_producer` calls run concurrently in a `ThreadPoolExecutor` and each calls `_write_active_role_sentinel(role.value)` against the shared per-user `$HOME/.claude/egg-active-role.json` (sentinel docstring at :1195-1202 already names this as the \"single-valued, per-user file cannot disambiguate two concurrent sub-agents in different roles\" limitation). The race is constrained to roles whose allow-lists are all subsets of `.egg-state/` so there is no escape from the orchestrator's restriction set, and `EGG_AGENT_ROLE` is set in each subprocess's `spawn_env` so the sentinel is only consulted as a hook fallback. Surfacing here for the security-lens audit trail; the structural fix is reviewer_concurrency / R2-deferral scope, not a blocker for this slice.\n- **orchestrator/substrate/in_process.py:388-420** \u2014 `tracker.handle_ack` is recorded on every plan producer whose spawn returned exit_code==0 without inspecting the reviewer_plan verdict JSON in `.egg-state/agent-outputs/-reviewer_plan-output.json`. Not a security boundary (all in-process trusted code), but the orchestrator's \"ACK on the reviewer's behalf\" semantics deserve a reviewer_code look \u2014 if a future change made the reviewer's verdict load-bearing for downstream security policy, this would need to read the verdict file. Out of scope for the security lens today.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:00:24.105569+00:00", - "phase": "implement" - }, - { - "id": "6dc3ade8-d6e4-4d", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:00:24.275822+00:00", - "phase": "implement" - }, - { - "id": "d1151a9e-8d20-4a", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.423341+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:00:30.449486+00:00", - "phase": "implement" - }, - { - "id": "04991a2c-e5b6-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:00:30.710776+00:00", - "phase": "implement" - }, - { - "id": "3d639278-beea-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:01:03.165854+00:00", - "phase": "implement" - }, - { - "id": "980e0bfe-3420-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_contract for coder", - "body": "\nContract-verification review (slice-2 coder, commit 3a466891e, proposal v1). Reviewed both files in the diff against the three coder-assigned tasks in slice-2 (task-2-1, task-2-2, task-2-5). All acceptance criteria are objectively met.\n\n### Per-task verification\n\n**TASK-2-1 \u2014 `_run_plan_phase` on `_InProcessOrchestrator`** (orchestrator/substrate/in_process.py:830-1071):\n1. AC \"no longer raises NotImplementedError when the operator advances past refine\": \u2705 Verified. `run()` body at line 233 calls `self._run_plan_phase(artifact_path)` after `_answer_continues_past_refine(refine_answer)` is true; the walking-skeleton `_maybe_fence` moved to AFTER the plan HITL gate (line 247 call site; line 1260-1291 fence body whose diagnostic now points at \"slice-3 / slice-4 of the #2717 rollout\"). Refine-gate `approve_continue` no longer raises.\n2. AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705 Verified. `ThreadPoolExecutor(max_workers=len(plan_producers))` at line 942 dispatches `_spawn_plan_producer` for ARCHITECT, TASK_PLANNER, RISK_ANALYST concurrently. Each producer gets its own worktree (`bundle.worktrees.create`, line 1089), env-shaped spawn (lines 1091-1104 with `EGG_AGENT_ROLE`, `EGG_PHASE=\"plan\"`, refine/plan artifact paths), and active-role sentinel write before `bundle.spawner.spawn(...)`.\n3. AC \"reviewer_plan is spawned after each CONSENSUS_PROPOSE\": \u26a0\ufe0f Functionally satisfied via a single reviewer dispatch that records N ACKs on the tracker, not N reviewer spawns. `_spawn_plan_reviewer` is called once (line 994) AFTER the producer ThreadPoolExecutor's `with` block exits and AFTER `tracker.handle_propose(role.value, ...)` has fired for every successful producer (line 976-987). The reviewer then ACKs each producer separately (line 1011-1029 loop). The docstring at lines 842-856 explicitly justifies the single-spawn-batches-ACKs design: \"the in-process bundle's spawner is synchronous \u2014 `bundle.spawner.spawn(role, ...)` returns AFTER the subagent finishes ... the spawn-completion IS the signal that the subagent proposed / reviewed\". Reading the AC's \"after each CONSENSUS_PROPOSE\" as \"after all CONSENSUS_PROPOSEs land\", the design is consistent with the task description (\"After producers reach `CONSENSUS_PROPOSE`, `reviewer_plan` is spawned for the ACK/NACK cycle\" \u2014 singular cycle) and the BRC outcome (one ACK per producer edge) is identical to a multi-spawn variant on a synchronous spawner. Non-blocking \u2014 design choice is documented and BRC tracker advances correctly.\n4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge\": \u2705 Verified. `tracker.handle_confirmed(role.value)` is invoked for every producer AND for `reviewer_plan` at line 1044-1052; `plan_eval = tracker.evaluate()` (line 1054) carries `is_complete`, `blocking_agents`, `unresolved_nack_details`; `_build_plan_gate_decision` (line 648-710) returns a `HITLDecision(... phase=\"plan\")` with the canonical four `approve_continue / request_changes / change_approach / stop` options on the converged path and a `retry / abort` failure path on non-convergence. The generator yields it at line 239-241.\n5. AC \"existing refine path still works\": \u2705 Verified. Refine flow at lines 195-227 is structurally unchanged; refiner spawn, refine-gate HITL yield, abort/preflight handling all preserved. Plan dispatch is gated on `_answer_continues_past_refine(refine_answer)` returning true (line 226); any other refine-gate answer (stop / change-approach / request-changes / abort / retry) returns the refine artifact path without entering `_run_plan_phase`.\n\n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376):\n1. AC \"loader returns rubric bodies for all four plan-team roles\": \u2705 Verified. `_RUBRIC_LANDED_ROLES` (lines 272-286) now contains `architect`, `task_planner`, `risk_analyst`, `reviewer_plan` in addition to the slice-1 set. The fence at line 348 (`if role_name not in _RUBRIC_LANDED_ROLES`) no longer rejects these roles; line 362-375 returns `rubric_path.read_text(...)` when the markdown file is present on disk.\n2. AC \"implement-team roles still raise ValueError with the 'follow-up slice 3' hint\": \u2705 Verified. `_ROLE_RUBRIC_SLICES` (lines 254-262) maps the eight implement-team roles (`coder`, `tester`, `documenter`, `reviewer_code`, `reviewer_code_holistic`, `reviewer_contract`, `reviewer_security`, `reviewer_concurrency`) to `\"slice-3\"`, and the ValueError at line 356-360 interpolates `slice_hint` into the message (\"...deferred to follow-up slice-3 of issue #2717's rollout...\"), satisfying the hint contract.\n\n**TASK-2-5 \u2014 sandbox restrictions parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py \u2014 NOT modified):\n- AC \"If R2 pass: task closed with note 'no-op: hooks resolve role correctly; structural enforcement remains hook-side'\": \u2705 Verified. Coder commit message records the no-op close with the required note (\"TASK-2-5 closes as no-op per slice-1's R2 = pass verdict ... structural enforcement stays hook-side, no MCP-validator-side parallel layer needed\"). The slice-1 R2 nested-dispatch test (`integration_tests/regression/test_pretooluse_hook_nested.py:212-238`) pins R2 = pass via three structured assertions (`result.denied`, `verdict[\"decision\"] == \"block\"`, `written[\"r2_verdict\"] == \"pass\"`) \u2014 these assertions ran during slice-1's BRC cycle and would have failed the slice-1 tester's propose otherwise. The no-op close is contractually defensible.\n\n### Non-blocking observations (informational)\n\n- **orchestrator/substrate/in_process.py:942** \u2014 task-2-1 *description* (not AC) names \"concurrent_executor.py seam (line 569)\" as the intended dispatch path; the implementation uses a raw `ThreadPoolExecutor` and records BRC transitions on the orchestrator side rather than routing through `InProcessMessageBus`. The docstring at lines 842-856 explains why (synchronous spawner makes the message-bus round-trip a no-op duplicate). This deviates from the description's wording but does NOT violate the AC (\"3 producers concurrently via the executor\" is satisfied; the AC does not require `ConcurrentPhaseExecutor` specifically). Calling out so a future slice that flips dispatch to async can revisit the seam choice.\n- **TASK-2-5 R2-verdict artifact** \u2014 the AC for the contingent task (\"see `.egg-state//r2-verdict.json` from TASK-1-5\") refers to a runtime artifact that slice-1's test writes under `tmp_path / pipeline_id / \"r2-verdict.json\"` (per `test_pretooluse_hook_nested.py:120-128`), not under a committed path in this worktree. The verdict file therefore is NOT inspectable post-hoc; the empirical proof rests on the slice-1 test assertions having passed. This is a slice-1 handoff observation (already flagged in slice-1's reviewer history as a \"downstream-handoff improvement\") and not a slice-2 coder concern; slice-5 R15 will need to re-derive the verdict if it cannot read a persisted file.\n- **slice-1 contract task statuses** \u2014 slice-1 tasks (task-1-1 \u2026 task-1-9) still show `status: \"pending\"` in the contract despite their commits being linked. This is a slice-1 contract-bookkeeping issue (not slice-2), surfaced here so the operator knows the contract's per-task `status` field is lagging the actual BRC state. The contract integrity check on re-review will need to confirm slice-1 status before declaring the rollout complete.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/__init__.py", - "orchestrator/substrate/in_process.py" - ], - "reason": "\nContract-verification review (slice-2 coder, commit 3a466891e, proposal v1). Reviewed both files in the diff against the three coder-assigned tasks in slice-2 (task-2-1, task-2-2, task-2-5). All acceptance criteria are objectively met.\n\n### Per-task verification\n\n**TASK-2-1 \u2014 `_run_plan_phase` on `_InProcessOrchestrator`** (orchestrator/substrate/in_process.py:830-1071):\n1. AC \"no longer raises NotImplementedError when the operator advances past refine\": \u2705 Verified. `run()` body at line 233 calls `self._run_plan_phase(artifact_path)` after `_answer_continues_past_refine(refine_answer)` is true; the walking-skeleton `_maybe_fence` moved to AFTER the plan HITL gate (line 247 call site; line 1260-1291 fence body whose diagnostic now points at \"slice-3 / slice-4 of the #2717 rollout\"). Refine-gate `approve_continue` no longer raises.\n2. AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705 Verified. `ThreadPoolExecutor(max_workers=len(plan_producers))` at line 942 dispatches `_spawn_plan_producer` for ARCHITECT, TASK_PLANNER, RISK_ANALYST concurrently. Each producer gets its own worktree (`bundle.worktrees.create`, line 1089), env-shaped spawn (lines 1091-1104 with `EGG_AGENT_ROLE`, `EGG_PHASE=\"plan\"`, refine/plan artifact paths), and active-role sentinel write before `bundle.spawner.spawn(...)`.\n3. AC \"reviewer_plan is spawned after each CONSENSUS_PROPOSE\": \u26a0\ufe0f Functionally satisfied via a single reviewer dispatch that records N ACKs on the tracker, not N reviewer spawns. `_spawn_plan_reviewer` is called once (line 994) AFTER the producer ThreadPoolExecutor's `with` block exits and AFTER `tracker.handle_propose(role.value, ...)` has fired for every successful producer (line 976-987). The reviewer then ACKs each producer separately (line 1011-1029 loop). The docstring at lines 842-856 explicitly justifies the single-spawn-batches-ACKs design: \"the in-process bundle's spawner is synchronous \u2014 `bundle.spawner.spawn(role, ...)` returns AFTER the subagent finishes ... the spawn-completion IS the signal that the subagent proposed / reviewed\". Reading the AC's \"after each CONSENSUS_PROPOSE\" as \"after all CONSENSUS_PROPOSEs land\", the design is consistent with the task description (\"After producers reach `CONSENSUS_PROPOSE`, `reviewer_plan` is spawned for the ACK/NACK cycle\" \u2014 singular cycle) and the BRC outcome (one ACK per producer edge) is identical to a multi-spawn variant on a synchronous spawner. Non-blocking \u2014 design choice is documented and BRC tracker advances correctly.\n4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge\": \u2705 Verified. `tracker.handle_confirmed(role.value)` is invoked for every producer AND for `reviewer_plan` at line 1044-1052; `plan_eval = tracker.evaluate()` (line 1054) carries `is_complete`, `blocking_agents`, `unresolved_nack_details`; `_build_plan_gate_decision` (line 648-710) returns a `HITLDecision(... phase=\"plan\")` with the canonical four `approve_continue / request_changes / change_approach / stop` options on the converged path and a `retry / abort` failure path on non-convergence. The generator yields it at line 239-241.\n5. AC \"existing refine path still works\": \u2705 Verified. Refine flow at lines 195-227 is structurally unchanged; refiner spawn, refine-gate HITL yield, abort/preflight handling all preserved. Plan dispatch is gated on `_answer_continues_past_refine(refine_answer)` returning true (line 226); any other refine-gate answer (stop / change-approach / request-changes / abort / retry) returns the refine artifact path without entering `_run_plan_phase`.\n\n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376):\n1. AC \"loader returns rubric bodies for all four plan-team roles\": \u2705 Verified. `_RUBRIC_LANDED_ROLES` (lines 272-286) now contains `architect`, `task_planner`, `risk_analyst`, `reviewer_plan` in addition to the slice-1 set. The fence at line 348 (`if role_name not in _RUBRIC_LANDED_ROLES`) no longer rejects these roles; line 362-375 returns `rubric_path.read_text(...)` when the markdown file is present on disk.\n2. AC \"implement-team roles still raise ValueError with the 'follow-up slice 3' hint\": \u2705 Verified. `_ROLE_RUBRIC_SLICES` (lines 254-262) maps the eight implement-team roles (`coder`, `tester`, `documenter`, `reviewer_code`, `reviewer_code_holistic`, `reviewer_contract`, `reviewer_security`, `reviewer_concurrency`) to `\"slice-3\"`, and the ValueError at line 356-360 interpolates `slice_hint` into the message (\"...deferred to follow-up slice-3 of issue #2717's rollout...\"), satisfying the hint contract.\n\n**TASK-2-5 \u2014 sandbox restrictions parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py \u2014 NOT modified):\n- AC \"If R2 pass: task closed with note 'no-op: hooks resolve role correctly; structural enforcement remains hook-side'\": \u2705 Verified. Coder commit message records the no-op close with the required note (\"TASK-2-5 closes as no-op per slice-1's R2 = pass verdict ... structural enforcement stays hook-side, no MCP-validator-side parallel layer needed\"). The slice-1 R2 nested-dispatch test (`integration_tests/regression/test_pretooluse_hook_nested.py:212-238`) pins R2 = pass via three structured assertions (`result.denied`, `verdict[\"decision\"] == \"block\"`, `written[\"r2_verdict\"] == \"pass\"`) \u2014 these assertions ran during slice-1's BRC cycle and would have failed the slice-1 tester's propose otherwise. The no-op close is contractually defensible.\n\n### Non-blocking observations (informational)\n\n- **orchestrator/substrate/in_process.py:942** \u2014 task-2-1 *description* (not AC) names \"concurrent_executor.py seam (line 569)\" as the intended dispatch path; the implementation uses a raw `ThreadPoolExecutor` and records BRC transitions on the orchestrator side rather than routing through `InProcessMessageBus`. The docstring at lines 842-856 explains why (synchronous spawner makes the message-bus round-trip a no-op duplicate). This deviates from the description's wording but does NOT violate the AC (\"3 producers concurrently via the executor\" is satisfied; the AC does not require `ConcurrentPhaseExecutor` specifically). Calling out so a future slice that flips dispatch to async can revisit the seam choice.\n- **TASK-2-5 R2-verdict artifact** \u2014 the AC for the contingent task (\"see `.egg-state//r2-verdict.json` from TASK-1-5\") refers to a runtime artifact that slice-1's test writes under `tmp_path / pipeline_id / \"r2-verdict.json\"` (per `test_pretooluse_hook_nested.py:120-128`), not under a committed path in this worktree. The verdict file therefore is NOT inspectable post-hoc; the empirical proof rests on the slice-1 test assertions having passed. This is a slice-1 handoff observation (already flagged in slice-1's reviewer history as a \"downstream-handoff improvement\") and not a slice-2 coder concern; slice-5 R15 will need to re-derive the verdict if it cannot read a persisted file.\n- **slice-1 contract task statuses** \u2014 slice-1 tasks (task-1-1 \u2026 task-1-9) still show `status: \"pending\"` in the contract despite their commits being linked. This is a slice-1 contract-bookkeeping issue (not slice-2), surfaced here so the operator knows the contract's per-task `status` field is lagging the actual BRC state. The contract integrity check on re-review will need to confirm slice-1 status before declaring the rollout complete.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:01:10.869774+00:00", - "phase": "implement" - }, - { - "id": "255a348c-fb9e-4d", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:01:10.924672+00:00", - "phase": "implement" - }, - { - "id": "7b1c6806-c72f-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_contract (pending_acks)", - "body": "Agent reviewer_contract cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus.", - "metadata": { - "pending_acks": true, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:01:13.530214+00:00", - "phase": "implement" - }, - { - "id": "164db943-40ed-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:17.132192+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:01:17.168213+00:00", - "phase": "implement" - }, - { - "id": "64dc7b67-4d5d-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:01:27.932812+00:00", - "phase": "implement" - }, - { - "id": "440b79d9-d087-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:01:30.799774+00:00", - "phase": "implement" - }, - { - "id": "c7e0db26-6729-49", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:02:03.226877+00:00", - "phase": "implement" - }, - { - "id": "1aea8b2f-3fb2-41", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:17.132192+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:02:17.272489+00:00", - "phase": "implement" - }, - { - "id": "694ac4af-d918-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:02:27.970596+00:00", - "phase": "implement" - }, - { - "id": "fc2f920b-9e33-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:02:30.862298+00:00", - "phase": "implement" - }, - { - "id": "b050c955-8bcb-4b", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:03:03.464717+00:00", - "phase": "implement" - }, - { - "id": "55ccbb69-a657-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "coder", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_concurrency for coder", - "body": "\nConcurrency-lens review of coder v1 (commit 3a466891e). I scanned the new `_run_plan_phase` ThreadPoolExecutor wiring, the per-thread spawn path (`_spawn_plan_producer` / `_spawn_plan_reviewer`), the shared sentinel write, the worktree allocation path, the heartbeat publisher's phase string, and the tracker register/propose/ack/confirmed call ordering. Two blocking concurrency findings.\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py:1109-1111` / `:1164` (`_run_plan_phase` \u2192 `_spawn_plan_producer` \u2192 `_write_active_role_sentinel`)** \u2014 Last-writer-wins race on the role sentinel during concurrent multi-role producer dispatch. `_run_plan_phase` spawns architect / task_planner / risk_analyst via a `ThreadPoolExecutor(max_workers=3)`; each worker thread calls `self._write_active_role_sentinel(role.value)` immediately before `bundle.spawner.spawn(...)`. The sentinel is a **single-valued, per-user file** at `$HOME/.claude/egg-active-role.json` (line 1217), so three concurrent writes converge on whichever role wrote last. The producer's own docstring on `_write_active_role_sentinel` admits the limitation: \"this single-valued, per-user file cannot disambiguate two concurrent sub-agents in different roles. The R2 deferral's multi-role rollout cannot use this sentinel for role-routing without a breaking change to the sentinel shape\". The PreToolUse hook (`orchestrator/substrate/claude_code/hook_entry.py:697-742` `_resolve_active_role`) falls back to this sentinel whenever `EGG_AGENT_ROLE` is unset \u2014 which the documenter-owned slice-1 design names as the **nested-Agent-tool-dispatch** path the R2 verdict was meant to certify. Concrete failure mode: between roughly `T=0+2\u03b5` (when the third thread overwrites the sentinel) and `T=spawn_complete` (when all three subagent processes have returned), every nested child spawned by architect or task_planner reads the sentinel as `risk_analyst` (or whichever role won the race) and evaluates its tool calls against the wrong role's allow-list \u2014 silently nondeterministic role-based authz for the new concurrent path. The slice-1 R2 verdict only covered the **single-role-at-a-time** parent\u2192child case (`test_pretooluse_hook_denies_nested_child_write` runs one parent + one nested child); it is not the correct precedent for the \"three concurrent role-holders share one sentinel\" pattern slice-2 introduces, and the harness-faked smoke test in the commit message stubs the spawn so the race is invisible to the existing test surface. Fix: bind the sentinel to a per-spawn key (PID-of-the-child-process or per-thread file under `$HOME/.claude/egg-active-role-.json` resolved by the hook walking its own ancestor PIDs), or have the hook resolve via `os.environ` exclusively and stop writing the single-valued sentinel from concurrent paths. This fix MUST land in slice-2 \u2014 slice-2 is the first slice that introduces concurrent multi-role producers, and deferring the sentinel cleanup to a later slice leaves slice-2 shipping with a documented race in the first-tier authz path.\n\n2. **`orchestrator/substrate/in_process.py:392` (`_publish_heartbeat`)** \u2014 Heartbeat is hardcoded to `phase=\"refine\"` after the generator enters the plan stage. The grep `phase=` shows three call sites that hard-code `phase=\"refine\"` (line 392 in the heartbeat publisher, lines 586 + 645 in the refine/preflight HITL builders) and one site that correctly uses `phase=\"plan\"` (line 709 in the plan HITL builder). The heartbeat thread keeps ticking every `_HEARTBEAT_INTERVAL = 5.0` s through the entire plan stage \u2014 three producer spawns + the reviewer spawn \u2014 and emits HEARTBEAT messages stamped with the stale phase. This is exactly the heartbeat-stall-window class of bug per #2012: any future monitor that filters heartbeats on `phase` (the orchestrator's stuck-phase-transition watchdog being the canonical consumer) will not see plan-phase liveness from the in-process orchestrator and may declare the agent stalled even though plan-phase work is progressing. Fix: track the current phase on the orchestrator (e.g. `self._current_phase = \"refine\"`, flip to `\"plan\"` at the top of `_run_plan_phase` and back as needed) and have `_publish_heartbeat` read from it instead of hard-coding the string.\n\n### Non-blocking\n\n- **`orchestrator/substrate/claude_code/worktree.py:117-127` (`Worktree.create`)** \u2014 Concurrent `git worktree add` invocations from the three plan-producer threads share the parent repo's `.git/worktrees/` and `.git/index.lock`. `subprocess.run(..., check=False, timeout=30)` silently swallows whatever git reports; the `target.mkdir(parents=True, exist_ok=True)` runs unconditionally before the subprocess call, so the spawner still gets a path even when the underlying `git worktree add` lost the lock race. The downstream effect is that any real (non-faked) producer that later does `git rev-parse HEAD` falls back to the `_SYNTHETIC_PLAN_COMMIT` constant \u2014 masking real git-side failures during a concurrent allocation flurry. Either inspect `result.returncode` + `result.stderr` and surface the \"another git process seems to be running\" outcome to the caller, or serialise `git worktree add` calls behind `self._lock` (the dict mutation lock already in place).\n\n- **`orchestrator/substrate/in_process.py:84-92` + `:413-420` (`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`)** \u2014 All three producers stamp the same synthetic commit_sha when the harness fake doesn't supply one. Not a race in itself, but `PeerConsensusTracker.handle_propose` then sees three propose entries with identical `commit_sha`; any future flip-flop-count or version-anchoring logic keyed on `commit_sha` collapses the three role-distinct artifacts into one. Cheap mitigation: include the role abbreviation in the synthetic SHA (e.g. `f\"ace1{role.value[:3]}\"`) so per-producer ProposalPayload entries remain distinguishable in the tracker.\n\n- **`orchestrator/substrate/in_process.py:317-323` (`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker` check-then-act)** \u2014 Not a race today because `_brc_review_loop` (line 318) only reads the tracker via `get_peer_consensus_tracker` and the slice-1 refiner does not register one, but the pattern is fragile. If a future maintainer adds a second `create_*` call site (e.g. background BRC tick that lazily creates a tracker), two threads can both observe `get_*` returning `None`, both enter `create_*`, and the second write under `_trackers_lock` (`orchestrator/peer_consensus.py:1891`) silently overwrites the first tracker \u2014 the BRC re-review thread is then holding a stale tracker reference. Cheaper to wrap the check-then-act in the existing module-level `_trackers_lock` here once.\n", - "metadata": { - "payload": { - "reason": "\nConcurrency-lens review of coder v1 (commit 3a466891e). I scanned the new `_run_plan_phase` ThreadPoolExecutor wiring, the per-thread spawn path (`_spawn_plan_producer` / `_spawn_plan_reviewer`), the shared sentinel write, the worktree allocation path, the heartbeat publisher's phase string, and the tracker register/propose/ack/confirmed call ordering. Two blocking concurrency findings.\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py:1109-1111` / `:1164` (`_run_plan_phase` \u2192 `_spawn_plan_producer` \u2192 `_write_active_role_sentinel`)** \u2014 Last-writer-wins race on the role sentinel during concurrent multi-role producer dispatch. `_run_plan_phase` spawns architect / task_planner / risk_analyst via a `ThreadPoolExecutor(max_workers=3)`; each worker thread calls `self._write_active_role_sentinel(role.value)` immediately before `bundle.spawner.spawn(...)`. The sentinel is a **single-valued, per-user file** at `$HOME/.claude/egg-active-role.json` (line 1217), so three concurrent writes converge on whichever role wrote last. The producer's own docstring on `_write_active_role_sentinel` admits the limitation: \"this single-valued, per-user file cannot disambiguate two concurrent sub-agents in different roles. The R2 deferral's multi-role rollout cannot use this sentinel for role-routing without a breaking change to the sentinel shape\". The PreToolUse hook (`orchestrator/substrate/claude_code/hook_entry.py:697-742` `_resolve_active_role`) falls back to this sentinel whenever `EGG_AGENT_ROLE` is unset \u2014 which the documenter-owned slice-1 design names as the **nested-Agent-tool-dispatch** path the R2 verdict was meant to certify. Concrete failure mode: between roughly `T=0+2\u03b5` (when the third thread overwrites the sentinel) and `T=spawn_complete` (when all three subagent processes have returned), every nested child spawned by architect or task_planner reads the sentinel as `risk_analyst` (or whichever role won the race) and evaluates its tool calls against the wrong role's allow-list \u2014 silently nondeterministic role-based authz for the new concurrent path. The slice-1 R2 verdict only covered the **single-role-at-a-time** parent\u2192child case (`test_pretooluse_hook_denies_nested_child_write` runs one parent + one nested child); it is not the correct precedent for the \"three concurrent role-holders share one sentinel\" pattern slice-2 introduces, and the harness-faked smoke test in the commit message stubs the spawn so the race is invisible to the existing test surface. Fix: bind the sentinel to a per-spawn key (PID-of-the-child-process or per-thread file under `$HOME/.claude/egg-active-role-.json` resolved by the hook walking its own ancestor PIDs), or have the hook resolve via `os.environ` exclusively and stop writing the single-valued sentinel from concurrent paths. This fix MUST land in slice-2 \u2014 slice-2 is the first slice that introduces concurrent multi-role producers, and deferring the sentinel cleanup to a later slice leaves slice-2 shipping with a documented race in the first-tier authz path.\n\n2. **`orchestrator/substrate/in_process.py:392` (`_publish_heartbeat`)** \u2014 Heartbeat is hardcoded to `phase=\"refine\"` after the generator enters the plan stage. The grep `phase=` shows three call sites that hard-code `phase=\"refine\"` (line 392 in the heartbeat publisher, lines 586 + 645 in the refine/preflight HITL builders) and one site that correctly uses `phase=\"plan\"` (line 709 in the plan HITL builder). The heartbeat thread keeps ticking every `_HEARTBEAT_INTERVAL = 5.0` s through the entire plan stage \u2014 three producer spawns + the reviewer spawn \u2014 and emits HEARTBEAT messages stamped with the stale phase. This is exactly the heartbeat-stall-window class of bug per #2012: any future monitor that filters heartbeats on `phase` (the orchestrator's stuck-phase-transition watchdog being the canonical consumer) will not see plan-phase liveness from the in-process orchestrator and may declare the agent stalled even though plan-phase work is progressing. Fix: track the current phase on the orchestrator (e.g. `self._current_phase = \"refine\"`, flip to `\"plan\"` at the top of `_run_plan_phase` and back as needed) and have `_publish_heartbeat` read from it instead of hard-coding the string.\n\n### Non-blocking\n\n- **`orchestrator/substrate/claude_code/worktree.py:117-127` (`Worktree.create`)** \u2014 Concurrent `git worktree add` invocations from the three plan-producer threads share the parent repo's `.git/worktrees/` and `.git/index.lock`. `subprocess.run(..., check=False, timeout=30)` silently swallows whatever git reports; the `target.mkdir(parents=True, exist_ok=True)` runs unconditionally before the subprocess call, so the spawner still gets a path even when the underlying `git worktree add` lost the lock race. The downstream effect is that any real (non-faked) producer that later does `git rev-parse HEAD` falls back to the `_SYNTHETIC_PLAN_COMMIT` constant \u2014 masking real git-side failures during a concurrent allocation flurry. Either inspect `result.returncode` + `result.stderr` and surface the \"another git process seems to be running\" outcome to the caller, or serialise `git worktree add` calls behind `self._lock` (the dict mutation lock already in place).\n\n- **`orchestrator/substrate/in_process.py:84-92` + `:413-420` (`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`)** \u2014 All three producers stamp the same synthetic commit_sha when the harness fake doesn't supply one. Not a race in itself, but `PeerConsensusTracker.handle_propose` then sees three propose entries with identical `commit_sha`; any future flip-flop-count or version-anchoring logic keyed on `commit_sha` collapses the three role-distinct artifacts into one. Cheap mitigation: include the role abbreviation in the synthetic SHA (e.g. `f\"ace1{role.value[:3]}\"`) so per-producer ProposalPayload entries remain distinguishable in the tracker.\n\n- **`orchestrator/substrate/in_process.py:317-323` (`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker` check-then-act)** \u2014 Not a race today because `_brc_review_loop` (line 318) only reads the tracker via `get_peer_consensus_tracker` and the slice-1 refiner does not register one, but the pattern is fragile. If a future maintainer adds a second `create_*` call site (e.g. background BRC tick that lazily creates a tracker), two threads can both observe `get_*` returning `None`, both enter `create_*`, and the second write under `_trackers_lock` (`orchestrator/peer_consensus.py:1891`) silently overwrites the first tracker \u2014 the BRC re-review thread is then holding a stale tracker reference. Cheaper to wrap the check-then-act in the existing module-level `_trackers_lock` here once.\n", - "artifact_references": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py", - "orchestrator/substrate/claude_code/worktree.py", - "orchestrator/substrate/claude_code/hook_entry.py", - "orchestrator/peer_consensus.py" - ], - "nack_version": 1 - }, - "reason": "\nConcurrency-lens review of coder v1 (commit 3a466891e). I scanned the new `_run_plan_phase` ThreadPoolExecutor wiring, the per-thread spawn path (`_spawn_plan_producer` / `_spawn_plan_reviewer`), the shared sentinel write, the worktree allocation path, the heartbeat publisher's phase string, and the tracker register/propose/ack/confirmed call ordering. Two blocking concurrency findings.\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py:1109-1111` / `:1164` (`_run_plan_phase` \u2192 `_spawn_plan_producer` \u2192 `_write_active_role_sentinel`)** \u2014 Last-writer-wins race on the role sentinel during concurrent multi-role producer dispatch. `_run_plan_phase` spawns architect / task_planner / risk_analyst via a `ThreadPoolExecutor(max_workers=3)`; each worker thread calls `self._write_active_role_sentinel(role.value)` immediately before `bundle.spawner.spawn(...)`. The sentinel is a **single-valued, per-user file** at `$HOME/.claude/egg-active-role.json` (line 1217), so three concurrent writes converge on whichever role wrote last. The producer's own docstring on `_write_active_role_sentinel` admits the limitation: \"this single-valued, per-user file cannot disambiguate two concurrent sub-agents in different roles. The R2 deferral's multi-role rollout cannot use this sentinel for role-routing without a breaking change to the sentinel shape\". The PreToolUse hook (`orchestrator/substrate/claude_code/hook_entry.py:697-742` `_resolve_active_role`) falls back to this sentinel whenever `EGG_AGENT_ROLE` is unset \u2014 which the documenter-owned slice-1 design names as the **nested-Agent-tool-dispatch** path the R2 verdict was meant to certify. Concrete failure mode: between roughly `T=0+2\u03b5` (when the third thread overwrites the sentinel) and `T=spawn_complete` (when all three subagent processes have returned), every nested child spawned by architect or task_planner reads the sentinel as `risk_analyst` (or whichever role won the race) and evaluates its tool calls against the wrong role's allow-list \u2014 silently nondeterministic role-based authz for the new concurrent path. The slice-1 R2 verdict only covered the **single-role-at-a-time** parent\u2192child case (`test_pretooluse_hook_denies_nested_child_write` runs one parent + one nested child); it is not the correct precedent for the \"three concurrent role-holders share one sentinel\" pattern slice-2 introduces, and the harness-faked smoke test in the commit message stubs the spawn so the race is invisible to the existing test surface. Fix: bind the sentinel to a per-spawn key (PID-of-the-child-process or per-thread file under `$HOME/.claude/egg-active-role-.json` resolved by the hook walking its own ancestor PIDs), or have the hook resolve via `os.environ` exclusively and stop writing the single-valued sentinel from concurrent paths. This fix MUST land in slice-2 \u2014 slice-2 is the first slice that introduces concurrent multi-role producers, and deferring the sentinel cleanup to a later slice leaves slice-2 shipping with a documented race in the first-tier authz path.\n\n2. **`orchestrator/substrate/in_process.py:392` (`_publish_heartbeat`)** \u2014 Heartbeat is hardcoded to `phase=\"refine\"` after the generator enters the plan stage. The grep `phase=` shows three call sites that hard-code `phase=\"refine\"` (line 392 in the heartbeat publisher, lines 586 + 645 in the refine/preflight HITL builders) and one site that correctly uses `phase=\"plan\"` (line 709 in the plan HITL builder). The heartbeat thread keeps ticking every `_HEARTBEAT_INTERVAL = 5.0` s through the entire plan stage \u2014 three producer spawns + the reviewer spawn \u2014 and emits HEARTBEAT messages stamped with the stale phase. This is exactly the heartbeat-stall-window class of bug per #2012: any future monitor that filters heartbeats on `phase` (the orchestrator's stuck-phase-transition watchdog being the canonical consumer) will not see plan-phase liveness from the in-process orchestrator and may declare the agent stalled even though plan-phase work is progressing. Fix: track the current phase on the orchestrator (e.g. `self._current_phase = \"refine\"`, flip to `\"plan\"` at the top of `_run_plan_phase` and back as needed) and have `_publish_heartbeat` read from it instead of hard-coding the string.\n\n### Non-blocking\n\n- **`orchestrator/substrate/claude_code/worktree.py:117-127` (`Worktree.create`)** \u2014 Concurrent `git worktree add` invocations from the three plan-producer threads share the parent repo's `.git/worktrees/` and `.git/index.lock`. `subprocess.run(..., check=False, timeout=30)` silently swallows whatever git reports; the `target.mkdir(parents=True, exist_ok=True)` runs unconditionally before the subprocess call, so the spawner still gets a path even when the underlying `git worktree add` lost the lock race. The downstream effect is that any real (non-faked) producer that later does `git rev-parse HEAD` falls back to the `_SYNTHETIC_PLAN_COMMIT` constant \u2014 masking real git-side failures during a concurrent allocation flurry. Either inspect `result.returncode` + `result.stderr` and surface the \"another git process seems to be running\" outcome to the caller, or serialise `git worktree add` calls behind `self._lock` (the dict mutation lock already in place).\n\n- **`orchestrator/substrate/in_process.py:84-92` + `:413-420` (`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`)** \u2014 All three producers stamp the same synthetic commit_sha when the harness fake doesn't supply one. Not a race in itself, but `PeerConsensusTracker.handle_propose` then sees three propose entries with identical `commit_sha`; any future flip-flop-count or version-anchoring logic keyed on `commit_sha` collapses the three role-distinct artifacts into one. Cheap mitigation: include the role abbreviation in the synthetic SHA (e.g. `f\"ace1{role.value[:3]}\"`) so per-producer ProposalPayload entries remain distinguishable in the tracker.\n\n- **`orchestrator/substrate/in_process.py:317-323` (`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker` check-then-act)** \u2014 Not a race today because `_brc_review_loop` (line 318) only reads the tracker via `get_peer_consensus_tracker` and the slice-1 refiner does not register one, but the pattern is fragile. If a future maintainer adds a second `create_*` call site (e.g. background BRC tick that lazily creates a tracker), two threads can both observe `get_*` returning `None`, both enter `create_*`, and the second write under `_trackers_lock` (`orchestrator/peer_consensus.py:1891`) silently overwrites the first tracker \u2014 the BRC re-review thread is then holding a stale tracker reference. Cheaper to wrap the check-then-act in the existing module-level `_trackers_lock` here once.\n", - "revision_count": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:03:04.234972+00:00", - "phase": "implement" - }, - { - "id": "3a4fab7a-b8e4-42", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:03:04.333110+00:00", - "phase": "implement" - }, - { - "id": "4a72960b-2434-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:03:14.929389+00:00", - "phase": "implement" - }, - { - "id": "bbc25f82-3d0f-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:03:28.037513+00:00", - "phase": "implement" - }, - { - "id": "c63cd500-0e96-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:03:30.935108+00:00", - "phase": "implement" - }, - { - "id": "0c208442-27df-4f", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:55.888666+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:03:55.989269+00:00", - "phase": "implement" - }, - { - "id": "78e4febf-92b5-47", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:04:03.593882+00:00", - "phase": "implement" - }, - { - "id": "2427a35f-ef35-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:04:14.989448+00:00", - "phase": "implement" - }, - { - "id": "dfd566f6-4541-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:04:28.135622+00:00", - "phase": "implement" - }, - { - "id": "88b13973-ec42-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:04:31.020280+00:00", - "phase": "implement" - }, - { - "id": "d26fe55a-284d-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "coder", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_code_holistic for coder", - "body": "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) \u2014 the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and `:156` (required input `architect_output_path` \u2014 the architect's design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\").\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed \"architect-first \u2192 fanned-out producers \u2192 critical-edge review\" data flow silently degrades to \"three producers running on the refine analysis alone\". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every successful producer with a hardcoded `reason` string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` \u2014 every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK \u2014 and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's \"in-process spawn-completion IS the signal that the subagent proposed / reviewed\" rationale is defensible for the propose half (the producer ran successfully \u2192 propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they \"feed change requests back into a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim \u2014 selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to \"not yet implemented; selecting these today exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` \u2014 no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\".\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n- **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest.\n", - "metadata": { - "payload": { - "reason": "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) \u2014 the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and `:156` (required input `architect_output_path` \u2014 the architect's design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\").\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed \"architect-first \u2192 fanned-out producers \u2192 critical-edge review\" data flow silently degrades to \"three producers running on the refine analysis alone\". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every successful producer with a hardcoded `reason` string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` \u2014 every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK \u2014 and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's \"in-process spawn-completion IS the signal that the subagent proposed / reviewed\" rationale is defensible for the propose half (the producer ran successfully \u2192 propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they \"feed change requests back into a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim \u2014 selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to \"not yet implemented; selecting these today exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` \u2014 no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\".\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n- **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest.\n", - "artifact_references": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py", - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "orchestrator/review_graph.py", - "shared/egg_contracts/agent_roles.py", - "orchestrator/substrate/claude_code/spawner.py" - ], - "nack_version": 1 - }, - "reason": "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) \u2014 the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and `:156` (required input `architect_output_path` \u2014 the architect's design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\").\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed \"architect-first \u2192 fanned-out producers \u2192 critical-edge review\" data flow silently degrades to \"three producers running on the refine analysis alone\". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every successful producer with a hardcoded `reason` string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` \u2014 every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK \u2014 and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's \"in-process spawn-completion IS the signal that the subagent proposed / reviewed\" rationale is defensible for the propose half (the producer ran successfully \u2192 propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they \"feed change requests back into a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim \u2014 selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to \"not yet implemented; selecting these today exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` \u2014 no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\".\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n- **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest.\n", - "revision_count": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:13.238129+00:00", - "phase": "implement" - }, - { - "id": "820f8491-5df2-47", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:55.888666+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:13.283606+00:00", - "phase": "implement" - }, - { - "id": "4e8fd35d-cebb-4f", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:13.307891+00:00", - "phase": "implement" - }, - { - "id": "a10538d7-8db2-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:13.318700+00:00", - "phase": "implement" - }, - { - "id": "615d9022-f4eb-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:15.039475+00:00", - "phase": "implement" - }, - { - "id": "c0adb44c-78c5-45", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:05:20.444313+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:20.519981+00:00", - "phase": "implement" - }, - { - "id": "5320529c-0fb8-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:28.202957+00:00", - "phase": "implement" - }, - { - "id": "30bc26ba-bcc8-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:31.150785+00:00", - "phase": "implement" - }, - { - "id": "dfe9b89c-f837-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "coder", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_code_holistic for coder", - "body": "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) \u2014 the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and `:156` (required input `architect_output_path` \u2014 the architect's design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\").\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed \"architect-first \u2192 fanned-out producers \u2192 critical-edge review\" data flow silently degrades to \"three producers running on the refine analysis alone\". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every successful producer with a hardcoded `reason` string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` \u2014 every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK \u2014 and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's \"in-process spawn-completion IS the signal that the subagent proposed / reviewed\" rationale is defensible for the propose half (the producer ran successfully \u2192 propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they \"feed change requests back into a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim \u2014 selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to \"not yet implemented; selecting these today exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` \u2014 no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\".\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n- **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest.\n", - "metadata": { - "payload": { - "reason": "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) \u2014 the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and `:156` (required input `architect_output_path` \u2014 the architect's design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\").\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed \"architect-first \u2192 fanned-out producers \u2192 critical-edge review\" data flow silently degrades to \"three producers running on the refine analysis alone\". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every successful producer with a hardcoded `reason` string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` \u2014 every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK \u2014 and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's \"in-process spawn-completion IS the signal that the subagent proposed / reviewed\" rationale is defensible for the propose half (the producer ran successfully \u2192 propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they \"feed change requests back into a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim \u2014 selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to \"not yet implemented; selecting these today exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` \u2014 no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\".\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n- **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest.\n", - "artifact_references": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py", - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "orchestrator/review_graph.py", - "shared/egg_contracts/agent_roles.py", - "orchestrator/substrate/claude_code/spawner.py" - ], - "nack_version": 1 - }, - "reason": "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) \u2014 the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and `:156` (required input `architect_output_path` \u2014 the architect's design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\").\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed \"architect-first \u2192 fanned-out producers \u2192 critical-edge review\" data flow silently degrades to \"three producers running on the refine analysis alone\". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every successful producer with a hardcoded `reason` string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` \u2014 every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK \u2014 and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's \"in-process spawn-completion IS the signal that the subagent proposed / reviewed\" rationale is defensible for the propose half (the producer ran successfully \u2192 propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they \"feed change requests back into a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim \u2014 selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to \"not yet implemented; selecting these today exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` \u2014 no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\".\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n- **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest.\n", - "revision_count": 2, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:53.353566+00:00", - "phase": "implement" - }, - { - "id": "26be4378-a9b7-41", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:53.448242+00:00", - "phase": "implement" - }, - { - "id": "cd90e2f5-1e1a-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "coder", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from tester for coder", - "body": "\nReviewed coder v1 (commit `3a466891e`, slice-2 TASK-2-1 / TASK-2-2 / TASK-2-5 no-op). Drove the implementation end-to-end against my TASK-2-4 integration test (`integration_tests/regression/test_inprocess_plan_brc.py`) and TASK-2-6 no-op guard (`tests/sandbox/egg_agent_tools/test_restrictions_validator.py`); **16/16 tester-authored tests pass cleanly** against this commit. Architecturally the slice is correct \u2014 `_run_plan_phase` correctly spawns 3 plan producers concurrently via `ThreadPoolExecutor`, drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed` deterministically per the synchronous-spawn design note in your commit message, reaches `is_complete=True` with all 4 plan-team roles in CONFIRMED state on the harness-faked path, yields the plan-HITL gate with `phase=\"plan\"` + `decision_type=\"phase_gate\"`, and correctly skips the plan stage on non-`approve_continue` refine answers (my `test_plan_stage_does_not_run_when_operator_rejects_refine` confirms `stop` still returns the refine artifact path without spawning any plan producer). TASK-2-2 loader expansion lands the 4 new plan-team roles correctly; TASK-2-5 no-op handling is correctly grounded in slice-1's R2 = pass verdict. **Blocking #1 below is the only thing keeping this from being an ACK.**\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py` \u2014 `ruff format --check` fails on 5 long-call sites in TASK-2-1's new code.** Reproduction: `PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py` exits 1; `make lint` cannot pass with this in the diff. The 5 sites the formatter wants to collapse to single-line form (each currently spans multiple lines and falls under the formatter's 100-col cap when re-folded):\n - `:236-241` \u2014 `plan_answer = yield self._build_plan_gate_decision(plan_artifact_path, plan_eval)` should be one line.\n - `:967-970` \u2014 `commit_sha = getattr(spawn_result, \"commit_sha\", None) or _SYNTHETIC_PLAN_COMMIT` should be one line.\n - `:1003-1005` \u2014 `producer_exit = int(getattr(producer_results[producer], \"exit_code\", 0) or 0)` should be one line.\n - `:1013-1015` \u2014 `\"artifact_references\": [str(producer_artifacts[producer])],` should be one line inside the dict literal.\n - `:1142-1144` \u2014 `producer_artifact_paths = sorted({str(path) for path in producer_artifacts.values()})` should be one line.\n Fix: run `PYTHONPATH=shared:gateway:orchestrator ruff format orchestrator/substrate/in_process.py` and commit the resulting diff. Full diff produced by `ruff format --diff` matches the 5 sites above one-for-one. I have not edited the file myself (cross-role boundary); reproducing locally produces the exact diff `ruff format` wants.\n\n I will re-run `make lint` (or the `ruff format --check` equivalent in this sandbox) the moment your v2 lands; if it passes, I will ACK this slice on v2.\n\n### Non-blocking\n\n- **`_run_plan_phase` writes the plan placeholder to the same `-plan.md` path regardless of per-producer success** (`:1059-1069`). On a real partial-failure run (one producer's spawn raises; others succeed) the placeholder's \"per-producer diagnostics\" summary is the only signal the operator gets. The current implementation correctly stores the per-role exceptions in `producer_results` so they show up in the placeholder body. Suggestion (defer to follow-up issue): when a producer's spawn raises but another succeeds, the placeholder body uses the *successful* producer's outputs as the canonical plan content; the operator should ideally see a \"plan partially produced\" gate instead of an \"approve\" gate. The `_build_plan_gate_decision` already differentiates `is_complete` vs `blocking_agents`, so this is just an issue of having `_run_plan_phase` thread the exceptions through more visibly. Not blocking because the HITL gate's `blocking_agents` field already covers the BRC side.\n\n- **`PeerConsensusTracker.get_peer_consensus_tracker` reuse pattern (`:925-934`)** \u2014 your comment notes that a previous slice's background BRC tick might have installed a tracker; in the test the registry's empty so a fresh tracker is created. I added an `isolated_pipeline_state` fixture in `test_inprocess_plan_brc.py` that clears the module-level registry between tests so back-to-back runs against the same pipeline_id don't inherit confirmed state. Not blocking; the production path doesn't have multiple in-process pipelines against the same id, but worth a comment in the production code explaining the reuse semantics.\n\n- **`_write_active_role_sentinel` is called per-producer inside `_spawn_plan_producer` and again from `_spawn_plan_reviewer`** (`:1106, :1158`). With three producers running concurrently in a `ThreadPoolExecutor` the sentinel write is last-writer-wins; the PreToolUse hook in any one producer's nested subagent will resolve via the EGG_AGENT_ROLE env (which IS per-spawn correct) before falling back to the sentinel. The R2 deferral caveat already documents this on `_write_active_role_sentinel`; not blocking. Worth a one-line comment at the call sites that the concurrency makes the sentinel non-load-bearing for the plan phase (the env-var is the load-bearing channel).\n\nReproduction summary for blocker #1:\n```\n$ PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py\nWould reformat: orchestrator/substrate/in_process.py\n1 file would be reformatted\n$ echo $?\n1\n```\n", - "metadata": { - "payload": { - "reason": "\nReviewed coder v1 (commit `3a466891e`, slice-2 TASK-2-1 / TASK-2-2 / TASK-2-5 no-op). Drove the implementation end-to-end against my TASK-2-4 integration test (`integration_tests/regression/test_inprocess_plan_brc.py`) and TASK-2-6 no-op guard (`tests/sandbox/egg_agent_tools/test_restrictions_validator.py`); **16/16 tester-authored tests pass cleanly** against this commit. Architecturally the slice is correct \u2014 `_run_plan_phase` correctly spawns 3 plan producers concurrently via `ThreadPoolExecutor`, drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed` deterministically per the synchronous-spawn design note in your commit message, reaches `is_complete=True` with all 4 plan-team roles in CONFIRMED state on the harness-faked path, yields the plan-HITL gate with `phase=\"plan\"` + `decision_type=\"phase_gate\"`, and correctly skips the plan stage on non-`approve_continue` refine answers (my `test_plan_stage_does_not_run_when_operator_rejects_refine` confirms `stop` still returns the refine artifact path without spawning any plan producer). TASK-2-2 loader expansion lands the 4 new plan-team roles correctly; TASK-2-5 no-op handling is correctly grounded in slice-1's R2 = pass verdict. **Blocking #1 below is the only thing keeping this from being an ACK.**\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py` \u2014 `ruff format --check` fails on 5 long-call sites in TASK-2-1's new code.** Reproduction: `PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py` exits 1; `make lint` cannot pass with this in the diff. The 5 sites the formatter wants to collapse to single-line form (each currently spans multiple lines and falls under the formatter's 100-col cap when re-folded):\n - `:236-241` \u2014 `plan_answer = yield self._build_plan_gate_decision(plan_artifact_path, plan_eval)` should be one line.\n - `:967-970` \u2014 `commit_sha = getattr(spawn_result, \"commit_sha\", None) or _SYNTHETIC_PLAN_COMMIT` should be one line.\n - `:1003-1005` \u2014 `producer_exit = int(getattr(producer_results[producer], \"exit_code\", 0) or 0)` should be one line.\n - `:1013-1015` \u2014 `\"artifact_references\": [str(producer_artifacts[producer])],` should be one line inside the dict literal.\n - `:1142-1144` \u2014 `producer_artifact_paths = sorted({str(path) for path in producer_artifacts.values()})` should be one line.\n Fix: run `PYTHONPATH=shared:gateway:orchestrator ruff format orchestrator/substrate/in_process.py` and commit the resulting diff. Full diff produced by `ruff format --diff` matches the 5 sites above one-for-one. I have not edited the file myself (cross-role boundary); reproducing locally produces the exact diff `ruff format` wants.\n\n I will re-run `make lint` (or the `ruff format --check` equivalent in this sandbox) the moment your v2 lands; if it passes, I will ACK this slice on v2.\n\n### Non-blocking\n\n- **`_run_plan_phase` writes the plan placeholder to the same `-plan.md` path regardless of per-producer success** (`:1059-1069`). On a real partial-failure run (one producer's spawn raises; others succeed) the placeholder's \"per-producer diagnostics\" summary is the only signal the operator gets. The current implementation correctly stores the per-role exceptions in `producer_results` so they show up in the placeholder body. Suggestion (defer to follow-up issue): when a producer's spawn raises but another succeeds, the placeholder body uses the *successful* producer's outputs as the canonical plan content; the operator should ideally see a \"plan partially produced\" gate instead of an \"approve\" gate. The `_build_plan_gate_decision` already differentiates `is_complete` vs `blocking_agents`, so this is just an issue of having `_run_plan_phase` thread the exceptions through more visibly. Not blocking because the HITL gate's `blocking_agents` field already covers the BRC side.\n\n- **`PeerConsensusTracker.get_peer_consensus_tracker` reuse pattern (`:925-934`)** \u2014 your comment notes that a previous slice's background BRC tick might have installed a tracker; in the test the registry's empty so a fresh tracker is created. I added an `isolated_pipeline_state` fixture in `test_inprocess_plan_brc.py` that clears the module-level registry between tests so back-to-back runs against the same pipeline_id don't inherit confirmed state. Not blocking; the production path doesn't have multiple in-process pipelines against the same id, but worth a comment in the production code explaining the reuse semantics.\n\n- **`_write_active_role_sentinel` is called per-producer inside `_spawn_plan_producer` and again from `_spawn_plan_reviewer`** (`:1106, :1158`). With three producers running concurrently in a `ThreadPoolExecutor` the sentinel write is last-writer-wins; the PreToolUse hook in any one producer's nested subagent will resolve via the EGG_AGENT_ROLE env (which IS per-spawn correct) before falling back to the sentinel. The R2 deferral caveat already documents this on `_write_active_role_sentinel`; not blocking. Worth a one-line comment at the call sites that the concurrency makes the sentinel non-load-bearing for the plan phase (the env-var is the load-bearing channel).\n\nReproduction summary for blocker #1:\n```\n$ PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py\nWould reformat: orchestrator/substrate/in_process.py\n1 file would be reformatted\n$ echo $?\n1\n```\n", - "artifact_references": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py" - ], - "nack_version": 1 - }, - "reason": "\nReviewed coder v1 (commit `3a466891e`, slice-2 TASK-2-1 / TASK-2-2 / TASK-2-5 no-op). Drove the implementation end-to-end against my TASK-2-4 integration test (`integration_tests/regression/test_inprocess_plan_brc.py`) and TASK-2-6 no-op guard (`tests/sandbox/egg_agent_tools/test_restrictions_validator.py`); **16/16 tester-authored tests pass cleanly** against this commit. Architecturally the slice is correct \u2014 `_run_plan_phase` correctly spawns 3 plan producers concurrently via `ThreadPoolExecutor`, drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed` deterministically per the synchronous-spawn design note in your commit message, reaches `is_complete=True` with all 4 plan-team roles in CONFIRMED state on the harness-faked path, yields the plan-HITL gate with `phase=\"plan\"` + `decision_type=\"phase_gate\"`, and correctly skips the plan stage on non-`approve_continue` refine answers (my `test_plan_stage_does_not_run_when_operator_rejects_refine` confirms `stop` still returns the refine artifact path without spawning any plan producer). TASK-2-2 loader expansion lands the 4 new plan-team roles correctly; TASK-2-5 no-op handling is correctly grounded in slice-1's R2 = pass verdict. **Blocking #1 below is the only thing keeping this from being an ACK.**\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py` \u2014 `ruff format --check` fails on 5 long-call sites in TASK-2-1's new code.** Reproduction: `PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py` exits 1; `make lint` cannot pass with this in the diff. The 5 sites the formatter wants to collapse to single-line form (each currently spans multiple lines and falls under the formatter's 100-col cap when re-folded):\n - `:236-241` \u2014 `plan_answer = yield self._build_plan_gate_decision(plan_artifact_path, plan_eval)` should be one line.\n - `:967-970` \u2014 `commit_sha = getattr(spawn_result, \"commit_sha\", None) or _SYNTHETIC_PLAN_COMMIT` should be one line.\n - `:1003-1005` \u2014 `producer_exit = int(getattr(producer_results[producer], \"exit_code\", 0) or 0)` should be one line.\n - `:1013-1015` \u2014 `\"artifact_references\": [str(producer_artifacts[producer])],` should be one line inside the dict literal.\n - `:1142-1144` \u2014 `producer_artifact_paths = sorted({str(path) for path in producer_artifacts.values()})` should be one line.\n Fix: run `PYTHONPATH=shared:gateway:orchestrator ruff format orchestrator/substrate/in_process.py` and commit the resulting diff. Full diff produced by `ruff format --diff` matches the 5 sites above one-for-one. I have not edited the file myself (cross-role boundary); reproducing locally produces the exact diff `ruff format` wants.\n\n I will re-run `make lint` (or the `ruff format --check` equivalent in this sandbox) the moment your v2 lands; if it passes, I will ACK this slice on v2.\n\n### Non-blocking\n\n- **`_run_plan_phase` writes the plan placeholder to the same `-plan.md` path regardless of per-producer success** (`:1059-1069`). On a real partial-failure run (one producer's spawn raises; others succeed) the placeholder's \"per-producer diagnostics\" summary is the only signal the operator gets. The current implementation correctly stores the per-role exceptions in `producer_results` so they show up in the placeholder body. Suggestion (defer to follow-up issue): when a producer's spawn raises but another succeeds, the placeholder body uses the *successful* producer's outputs as the canonical plan content; the operator should ideally see a \"plan partially produced\" gate instead of an \"approve\" gate. The `_build_plan_gate_decision` already differentiates `is_complete` vs `blocking_agents`, so this is just an issue of having `_run_plan_phase` thread the exceptions through more visibly. Not blocking because the HITL gate's `blocking_agents` field already covers the BRC side.\n\n- **`PeerConsensusTracker.get_peer_consensus_tracker` reuse pattern (`:925-934`)** \u2014 your comment notes that a previous slice's background BRC tick might have installed a tracker; in the test the registry's empty so a fresh tracker is created. I added an `isolated_pipeline_state` fixture in `test_inprocess_plan_brc.py` that clears the module-level registry between tests so back-to-back runs against the same pipeline_id don't inherit confirmed state. Not blocking; the production path doesn't have multiple in-process pipelines against the same id, but worth a comment in the production code explaining the reuse semantics.\n\n- **`_write_active_role_sentinel` is called per-producer inside `_spawn_plan_producer` and again from `_spawn_plan_reviewer`** (`:1106, :1158`). With three producers running concurrently in a `ThreadPoolExecutor` the sentinel write is last-writer-wins; the PreToolUse hook in any one producer's nested subagent will resolve via the EGG_AGENT_ROLE env (which IS per-spawn correct) before falling back to the sentinel. The R2 deferral caveat already documents this on `_write_active_role_sentinel`; not blocking. Worth a one-line comment at the call sites that the concurrency makes the sentinel non-load-bearing for the plan phase (the env-var is the load-bearing channel).\n\nReproduction summary for blocker #1:\n```\n$ PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py\nWould reformat: orchestrator/substrate/in_process.py\n1 file would be reformatted\n$ echo $?\n1\n```\n", - "revision_count": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:05:53.579497+00:00", - "phase": "implement" - }, - { - "id": "e734337c-94f2-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:04.573849+00:00", - "phase": "implement" - }, - { - "id": "3e1670b4-2c80-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:05.424344+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:05.453514+00:00", - "phase": "implement" - }, - { - "id": "a3e8f873-b2d4-4e", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:05.546832+00:00", - "phase": "implement" - }, - { - "id": "dff1f7b4-ba08-41", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:06.658152+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:06.681714+00:00", - "phase": "implement" - }, - { - "id": "d4757c68-6831-44", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:06.731311+00:00", - "phase": "implement" - }, - { - "id": "6370106d-da57-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:13.374465+00:00", - "phase": "implement" - }, - { - "id": "1b1a6e8c-b3ca-4f", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:14.213607+00:00", - "phase": "implement" - }, - { - "id": "e8ffd854-2d41-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:15.119643+00:00", - "phase": "implement" - }, - { - "id": "da006162-243b-48", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:24.514597+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:24.606883+00:00", - "phase": "implement" - }, - { - "id": "aaab18f3-f8c5-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:28.283397+00:00", - "phase": "implement" - }, - { - "id": "bda426ab-ac0e-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:06:31.201766+00:00", - "phase": "implement" - }, - { - "id": "c629f030-d3ba-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:07:04.656075+00:00", - "phase": "implement" - }, - { - "id": "4552d3b1-60e8-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:07:13.421222+00:00", - "phase": "implement" - }, - { - "id": "43c9eb43-8860-48", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:07:14.270555+00:00", - "phase": "implement" - }, - { - "id": "a0b2ff92-1eb0-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:07:15.229297+00:00", - "phase": "implement" - }, - { - "id": "486c4241-4abe-4f", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:24.514597+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:07:24.653112+00:00", - "phase": "implement" - }, - { - "id": "168d855f-a499-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:07:28.341669+00:00", - "phase": "implement" - }, - { - "id": "7ced567d-9819-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:07:31.247325+00:00", - "phase": "implement" - }, - { - "id": "cd9f89cf-1cc7-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:04.729822+00:00", - "phase": "implement" - }, - { - "id": "7182dd82-1878-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for coder", - "body": "\nReviewed the coder's submission for TASK-2-1 (plan-phase BRC stage on `_InProcessOrchestrator.run()`), TASK-2-2 (rubric loader expansion), and TASK-2-5 (closed as no-op per slice-1 R2=pass). Read both diffs end-to-end, traced concurrency interactions on `LocalWorktreeManager`, `PeerConsensusTracker`, and the active-role sentinel, verified payload shapes against `ProposalPayload` / `ReviewPayload` in `orchestrator/attestation_schemas.py`, and cross-checked behavior against the slice-1 spawner pattern.\n\n### File-by-file analysis\n\n**orchestrator/substrate/__init__.py** (TASK-2-2; +19/-9) \u2014 `_RUBRIC_LANDED_ROLES` extended with `architect`, `task_planner`, `risk_analyst`, `reviewer_plan` (lines 271-285). The docstring on `_load_egg_sdlc_role_rubric` is updated to credit slice-1 with the refine team and slice-2 with the plan team. The \"missing on disk\" diagnostic now mentions both TASK-1-4 (slice-1 refine reviewers) and TASK-2-3 (slice-2 plan team), giving an operator hitting the error a slice-specific pointer. `_ROLE_RUBRIC_SLICES` already had slice-2 mappings (from the slice-1 landing) so the unshipped-role fence for slice-3 implement-team roles is preserved unchanged. Loader file naming convention (`{role_name}.md`) matches the documenter's underscored basenames. Clean.\n\n**orchestrator/substrate/in_process.py** (TASK-2-1; +568/-9) \u2014 Big diff; broken down by surface:\n\n- *Generator flow* (`run()`, lines 219-249) \u2014 After the refine HITL gate, `_answer_continues_past_refine(refine_answer)` (lines 1313-1331) gates entry to `_run_plan_phase`. A negative answer falls through to `return str(artifact_path)`, preserving the slice-1 \"refine-only\" path verbatim. The new plan-gate HITL is yielded after `_run_plan_phase` returns, then `_maybe_fence(plan_answer)` (lines 1260-1291) re-targets at `approve_continue` past the plan gate with a slice-3 / slice-4 pointer. Existing `_PreflightAborted` translation and `finally`-block teardown (`_shutdown_background_threads`, `_teardown_worktrees`, `_teardown_sentinel`) covers the plan stage's exit paths cleanly because the worktree manager's `tear_down` is pipeline-scoped \u2014 it sweeps all 5 worktrees (1 refiner + 3 plan producers + 1 plan reviewer).\n\n- *Plan stage* (`_run_plan_phase`, lines 830-1071) \u2014 Spawns three plan producers concurrently via a `ThreadPoolExecutor(max_workers=3)`, then dispatches `reviewer_plan` once synchronously after `as_completed` drains all three. Producer failures (Exception from `fut.result()` or non-zero exit_code) are routed into `producer_results[role]` as an `Exception` instance / `AgentResult` with non-zero exit; the eval snapshot's `blocking_agents` surfaces them at the plan HITL gate. The \"Why the BRC verbs are called from the orchestrator rather than the spawned subagents\" docstring (lines 842-856) accurately captures the spike's synchronous-spawn-as-signal model and explains why both harness-faked tests and real-harness production reach `CONSENSUS_CONFIRMED` on the same code path.\n\n- *Per-producer spawn* (`_spawn_plan_producer`, lines 1073-1126) \u2014 Allocates a per-role worktree (`///`), shapes spawn_env with `EGG_PIPELINE_ID`, `EGG_AGENT_ROLE`, `EGG_REPO_ROOT`, `EGG_WORKTREE_ROOT`, `EGG_PHASE=plan`, `EGG_REFINE_ARTIFACT_PATH`, and `EGG_PLAN_ARTIFACT_PATH`, refreshes the active-role sentinel, then `bundle.spawner.spawn(role, prompt_text, spawn_env, worktree)`. The role-routing in the spawner respects whatever's in `spawn_env[\"EGG_AGENT_ROLE\"]` (and the spawner itself overrides it again at `claude_code/spawner.py:126`), so the producer's role is always correct in its own env even if the sentinel race fires for nested dispatch fallbacks.\n\n- *Reviewer spawn* (`_spawn_plan_reviewer`, lines 1128-1179) \u2014 Dispatches `reviewer_plan` once with `EGG_PRODUCER_ARTIFACT_PATHS` as a colon-joined list; in current code every producer's `producer_artifacts[role]` value is the same `plan_artifact_path`, so after `sorted({...})` the list is single-element.\n\n- *Tracker mechanics* (lines 917-934, 967-1052) \u2014 The plan-graph is fetched via `get_review_graph_for_phase(\"plan\", repo=self.repo)`, registering all four roles. `tracker.handle_propose` is gated on exit_code==0 with `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` as a fallback when the spawn didn't capture a real SHA \u2014 satisfies `ProposalPayload`'s `commit_sha_present` validator (#1473). `tracker.handle_ack(reviewer, producer, ...)` injects `verdict=\"ACK\"` server-side (`peer_consensus.py:429`), so the orchestrator's payload (lacking `verdict`) is structurally valid. `handle_confirmed` is best-effort with `except Exception: pass` \u2014 guard rejections surface in `tracker.evaluate()` (line 1054) rather than as generator exceptions, and that snapshot drives the plan HITL gate context. Lock contention on `tracker._lock` (RLock) under three concurrent `handle_propose` calls is brief and free of deadlock risk.\n\n- *Plan-gate HITL* (`_build_plan_gate_decision`, lines 648-710) \u2014 Branches on `plan_eval[\"is_complete\"]`. The success branch surfaces the canonical 4-way options (`approve_continue`, `request_changes`, `change_approach`, `stop`); the failure branch surfaces `retry`/`abort` and inlines `blocking_agents` + `unresolved_nack_details` into the decision context. Decision id is stable per pipeline (`plan-gate-{pipeline_id}` or `plan-failure-{pipeline_id}`). Mirrors `_build_refine_gate_decision`'s shape so the skill's outer loop handles both gates uniformly.\n\n- *Plan-artifact placeholder* (`_format_plan_placeholder`, lines 1334-1392) \u2014 Same shape as the refiner placeholder: per-producer diagnostics (exit_code, commit_sha, stdout-tail), BRC eval snapshot, and a clarifying epilogue. The placeholder lands at `.egg-state/drafts/-plan.md` only when the canonical file doesn't already exist (line 1059) \u2014 production task_planners that write the real file are preserved.\n\n- *Sentinel concurrency* (`_write_active_role_sentinel`, called from `_spawn_plan_producer` line 1111) \u2014 Three producer threads write `$HOME/.claude/egg-active-role.json` concurrently; the last writer wins. The single-valued file is documented as a known R2-deferral limitation in the docstring (lines 1195-1202). Under the slice-1 R2=pass verdict, `EGG_AGENT_ROLE` reliably propagates through nested dispatch so the sentinel is only the fallback path. Worth noting: a producer that *does* hit the sentinel fallback path may resolve to the wrong role if another concurrent producer has overwritten the file mid-spawn. The hook reads PID and treats stale entries as missing, but two live concurrent producers each have valid PIDs.\n\n- *Worktree creation under concurrency* (`LocalWorktreeManager.create`, `claude_code/worktree.py:89`) \u2014 Three concurrent `git worktree add` calls can race on `.git/index.lock` or refs database locks. The subprocess call uses `check=False` and a 30-second timeout, so a transient git lock contention leaves a non-worktree directory (the spawner still has somewhere to land artifacts). Recoverable.\n\n### Non-blocking\n\n- **orchestrator/substrate/in_process.py:907-911** \u2014 Rubric language vs implementation: `architect.md` says \"You run first, solo, before `task_planner` and `risk_analyst`\" and `task_planner.md` / `risk_analyst.md` both say \"downstream of `architect`\". The slice-2 SKILL.md inherits that ordering claim. The actual implementation here spawns all three concurrently via the `ThreadPoolExecutor`, which matches the k3s substrate's `spawn_all` behavior at `orchestrator/concurrent_executor.py:461-481` and explicitly satisfies the task-2-1 acceptance criterion \"the plan stage spawns 3 producers concurrently via the executor\". The architect-first language in the rubrics is a longstanding inheritance from `plugins/refine-plan/skills/refine-plan/agents/`'s rubric bodies (the k3s substrate has the same language-vs-implementation gap) \u2014 slice-2 does not introduce the gap. Follow-up worth filing to reconcile rubric language with actual concurrent dispatch, and to add an explicit \"architect's output JSON is read-on-best-effort by your peers\" note to task_planner / risk_analyst rubrics so the rubric language matches behavior.\n\n- **orchestrator/substrate/in_process.py:994-1034** \u2014 The orchestrator records `tracker.handle_ack(reviewer, producer, ...)` synthetically based on `reviewer_exit_code == 0`, **not** by parsing the reviewer's verdict JSON at `verdict_path`. A real reviewer that NACKs by writing `{\"verdict\": \"NACK\", ...}` to its verdict JSON but exits cleanly will have its NACK silently dropped \u2014 the orchestrator records ACK and the plan HITL gate fires with `is_complete=True`. The spike's harness-faked tests are insensitive to this because the fakes don't emit verdicts, but real-substrate usage of slice-2 today cannot rely on the reviewer NACK path. The commit message describes this as \"production (with real harness agents whose BRC emissions would be a no-op duplicate in this path)\" but the in-process substrate has no HTTP daemon for real agents' `egg-orch consensus propose` calls to land on \u2014 those emissions would error, not be duplicates. Slice-3 / 4 will need to wire verdict-JSON parsing or in-process BRC verb emission for the reviewer NACK path to actually work. Track in a follow-up issue.\n\n- **orchestrator/substrate/in_process.py:911 (\"reviewer_plan is spawned after each `CONSENSUS_PROPOSE`\")** \u2014 The task-2-1 acceptance criterion phrasing is ambiguous: it can be read as \"one reviewer spawn per producer propose\" (3 spawns) or as \"reviewer spawn is conditioned on at least one producer having proposed\" (1 spawn). Current code does the latter \u2014 one reviewer spawn after all three producers complete. The docstring at lines 1136-1139 documents the design choice (\"the synchronous spawn model means the producers' artifacts are on disk before the reviewer starts\"). Reasonable interpretation given the spike's spawn semantics, but reviewer_contract may want to verify this read. Either way the BRC tracker records per-producer ACKs (one tracker.handle_ack call per successful producer at lines 1011-1029), which satisfies the \"per-edge consensus\" spirit of the criterion.\n\n- **orchestrator/substrate/in_process.py:1128-1170** \u2014 The reviewer's spawn_env sets `EGG_PRODUCER_ARTIFACT_PATHS` but not the role-specific output paths the `reviewer_plan.md` rubric names (`analysis_path`, `architect_output_path`, `task_planner_output_path`, `risk_analyst_output_path`). After dedup, the producer-paths list collapses to a single entry (every producer's `producer_artifacts[role]` value is the same `plan_artifact_path`). The reviewer must infer the per-role JSON output paths from rubric convention. This matches the slice-1 pattern (the refiner also doesn't get `analysis_path` directly), but the rubric's input enumeration sets an expectation that slice-2's env shaping does not meet. Consider follow-up to surface role-specific paths in spawn_env so reviewer / task_planner / risk_analyst can read peer outputs deterministically rather than by convention-guessing.\n\n- **orchestrator/substrate/in_process.py:925-931** \u2014 The \"reuse existing tracker\" branch (`if tracker is None: create_peer_consensus_tracker(...)`) is dead code today \u2014 slice-1's `_spawn_refiner` does not register a tracker (the `create_peer_consensus_tracker` import is `noqa: F401`), and `_tick_brc_review` only reads. If a future slice registers a tracker with a different graph (e.g., refine graph during a future refine BRC cycle), the slice-2 plan phase would reuse that tracker with the wrong graph. Worth a guard that asserts the existing tracker's graph matches the plan graph before reuse, or just always-create (the spike's tight propose\u2192ack\u2192confirm sequence has no need to reuse).\n\n- **orchestrator/substrate/in_process.py:934** \u2014 `self._plan_tracker = tracker` is set but never read elsewhere in the module. If the intent was to expose the tracker for tests / observability, document the surface; otherwise drop the assignment.\n\n- **orchestrator/substrate/in_process.py:91 (`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`)** \u2014 Synthetic 7-hex constant for the test path. Real-substrate spawns capture `git rev-parse HEAD` post-commit, so the fallback only fires for harness fakes that don't write a commit. The constant is intentionally obviously-synthetic in log output. Worth a docstring note on `_SYNTHETIC_PLAN_COMMIT` mentioning that any caller hex-validating `commit_sha` (e.g. a gateway-style policy check) accepts this as a 7-char hex \u2014 non-issue today because the in-process bus doesn't gateway-validate, but a slice-5 hardening pass may want to swap to a clearly-non-hex sentinel (e.g. `\"synthetic-plan-commit\"`) if any consumer becomes hex-strict.\n\n- **orchestrator/substrate/in_process.py:1011-1029** \u2014 The `try / except Exception: pass` around `tracker.handle_ack` silently swallows malformed-payload errors (e.g., a ReviewPayload validation failure). The eval snapshot's `unresolved_nack_details` surfaces the unconfirmed edge, so the failure is visible at the HITL gate, but the operator sees no specific error message. Acceptable for the spike but worth flagging if the consumed payload shape changes (e.g., #2142's `ack_version` plumbing extends the required fields).\n\nNo security, correctness, or robustness issues that block the slice's atomic landing. Coder's submission ACKed.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/__init__.py", - "orchestrator/substrate/in_process.py" - ], - "reason": "\nReviewed the coder's submission for TASK-2-1 (plan-phase BRC stage on `_InProcessOrchestrator.run()`), TASK-2-2 (rubric loader expansion), and TASK-2-5 (closed as no-op per slice-1 R2=pass). Read both diffs end-to-end, traced concurrency interactions on `LocalWorktreeManager`, `PeerConsensusTracker`, and the active-role sentinel, verified payload shapes against `ProposalPayload` / `ReviewPayload` in `orchestrator/attestation_schemas.py`, and cross-checked behavior against the slice-1 spawner pattern.\n\n### File-by-file analysis\n\n**orchestrator/substrate/__init__.py** (TASK-2-2; +19/-9) \u2014 `_RUBRIC_LANDED_ROLES` extended with `architect`, `task_planner`, `risk_analyst`, `reviewer_plan` (lines 271-285). The docstring on `_load_egg_sdlc_role_rubric` is updated to credit slice-1 with the refine team and slice-2 with the plan team. The \"missing on disk\" diagnostic now mentions both TASK-1-4 (slice-1 refine reviewers) and TASK-2-3 (slice-2 plan team), giving an operator hitting the error a slice-specific pointer. `_ROLE_RUBRIC_SLICES` already had slice-2 mappings (from the slice-1 landing) so the unshipped-role fence for slice-3 implement-team roles is preserved unchanged. Loader file naming convention (`{role_name}.md`) matches the documenter's underscored basenames. Clean.\n\n**orchestrator/substrate/in_process.py** (TASK-2-1; +568/-9) \u2014 Big diff; broken down by surface:\n\n- *Generator flow* (`run()`, lines 219-249) \u2014 After the refine HITL gate, `_answer_continues_past_refine(refine_answer)` (lines 1313-1331) gates entry to `_run_plan_phase`. A negative answer falls through to `return str(artifact_path)`, preserving the slice-1 \"refine-only\" path verbatim. The new plan-gate HITL is yielded after `_run_plan_phase` returns, then `_maybe_fence(plan_answer)` (lines 1260-1291) re-targets at `approve_continue` past the plan gate with a slice-3 / slice-4 pointer. Existing `_PreflightAborted` translation and `finally`-block teardown (`_shutdown_background_threads`, `_teardown_worktrees`, `_teardown_sentinel`) covers the plan stage's exit paths cleanly because the worktree manager's `tear_down` is pipeline-scoped \u2014 it sweeps all 5 worktrees (1 refiner + 3 plan producers + 1 plan reviewer).\n\n- *Plan stage* (`_run_plan_phase`, lines 830-1071) \u2014 Spawns three plan producers concurrently via a `ThreadPoolExecutor(max_workers=3)`, then dispatches `reviewer_plan` once synchronously after `as_completed` drains all three. Producer failures (Exception from `fut.result()` or non-zero exit_code) are routed into `producer_results[role]` as an `Exception` instance / `AgentResult` with non-zero exit; the eval snapshot's `blocking_agents` surfaces them at the plan HITL gate. The \"Why the BRC verbs are called from the orchestrator rather than the spawned subagents\" docstring (lines 842-856) accurately captures the spike's synchronous-spawn-as-signal model and explains why both harness-faked tests and real-harness production reach `CONSENSUS_CONFIRMED` on the same code path.\n\n- *Per-producer spawn* (`_spawn_plan_producer`, lines 1073-1126) \u2014 Allocates a per-role worktree (`///`), shapes spawn_env with `EGG_PIPELINE_ID`, `EGG_AGENT_ROLE`, `EGG_REPO_ROOT`, `EGG_WORKTREE_ROOT`, `EGG_PHASE=plan`, `EGG_REFINE_ARTIFACT_PATH`, and `EGG_PLAN_ARTIFACT_PATH`, refreshes the active-role sentinel, then `bundle.spawner.spawn(role, prompt_text, spawn_env, worktree)`. The role-routing in the spawner respects whatever's in `spawn_env[\"EGG_AGENT_ROLE\"]` (and the spawner itself overrides it again at `claude_code/spawner.py:126`), so the producer's role is always correct in its own env even if the sentinel race fires for nested dispatch fallbacks.\n\n- *Reviewer spawn* (`_spawn_plan_reviewer`, lines 1128-1179) \u2014 Dispatches `reviewer_plan` once with `EGG_PRODUCER_ARTIFACT_PATHS` as a colon-joined list; in current code every producer's `producer_artifacts[role]` value is the same `plan_artifact_path`, so after `sorted({...})` the list is single-element.\n\n- *Tracker mechanics* (lines 917-934, 967-1052) \u2014 The plan-graph is fetched via `get_review_graph_for_phase(\"plan\", repo=self.repo)`, registering all four roles. `tracker.handle_propose` is gated on exit_code==0 with `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` as a fallback when the spawn didn't capture a real SHA \u2014 satisfies `ProposalPayload`'s `commit_sha_present` validator (#1473). `tracker.handle_ack(reviewer, producer, ...)` injects `verdict=\"ACK\"` server-side (`peer_consensus.py:429`), so the orchestrator's payload (lacking `verdict`) is structurally valid. `handle_confirmed` is best-effort with `except Exception: pass` \u2014 guard rejections surface in `tracker.evaluate()` (line 1054) rather than as generator exceptions, and that snapshot drives the plan HITL gate context. Lock contention on `tracker._lock` (RLock) under three concurrent `handle_propose` calls is brief and free of deadlock risk.\n\n- *Plan-gate HITL* (`_build_plan_gate_decision`, lines 648-710) \u2014 Branches on `plan_eval[\"is_complete\"]`. The success branch surfaces the canonical 4-way options (`approve_continue`, `request_changes`, `change_approach`, `stop`); the failure branch surfaces `retry`/`abort` and inlines `blocking_agents` + `unresolved_nack_details` into the decision context. Decision id is stable per pipeline (`plan-gate-{pipeline_id}` or `plan-failure-{pipeline_id}`). Mirrors `_build_refine_gate_decision`'s shape so the skill's outer loop handles both gates uniformly.\n\n- *Plan-artifact placeholder* (`_format_plan_placeholder`, lines 1334-1392) \u2014 Same shape as the refiner placeholder: per-producer diagnostics (exit_code, commit_sha, stdout-tail), BRC eval snapshot, and a clarifying epilogue. The placeholder lands at `.egg-state/drafts/-plan.md` only when the canonical file doesn't already exist (line 1059) \u2014 production task_planners that write the real file are preserved.\n\n- *Sentinel concurrency* (`_write_active_role_sentinel`, called from `_spawn_plan_producer` line 1111) \u2014 Three producer threads write `$HOME/.claude/egg-active-role.json` concurrently; the last writer wins. The single-valued file is documented as a known R2-deferral limitation in the docstring (lines 1195-1202). Under the slice-1 R2=pass verdict, `EGG_AGENT_ROLE` reliably propagates through nested dispatch so the sentinel is only the fallback path. Worth noting: a producer that *does* hit the sentinel fallback path may resolve to the wrong role if another concurrent producer has overwritten the file mid-spawn. The hook reads PID and treats stale entries as missing, but two live concurrent producers each have valid PIDs.\n\n- *Worktree creation under concurrency* (`LocalWorktreeManager.create`, `claude_code/worktree.py:89`) \u2014 Three concurrent `git worktree add` calls can race on `.git/index.lock` or refs database locks. The subprocess call uses `check=False` and a 30-second timeout, so a transient git lock contention leaves a non-worktree directory (the spawner still has somewhere to land artifacts). Recoverable.\n\n### Non-blocking\n\n- **orchestrator/substrate/in_process.py:907-911** \u2014 Rubric language vs implementation: `architect.md` says \"You run first, solo, before `task_planner` and `risk_analyst`\" and `task_planner.md` / `risk_analyst.md` both say \"downstream of `architect`\". The slice-2 SKILL.md inherits that ordering claim. The actual implementation here spawns all three concurrently via the `ThreadPoolExecutor`, which matches the k3s substrate's `spawn_all` behavior at `orchestrator/concurrent_executor.py:461-481` and explicitly satisfies the task-2-1 acceptance criterion \"the plan stage spawns 3 producers concurrently via the executor\". The architect-first language in the rubrics is a longstanding inheritance from `plugins/refine-plan/skills/refine-plan/agents/`'s rubric bodies (the k3s substrate has the same language-vs-implementation gap) \u2014 slice-2 does not introduce the gap. Follow-up worth filing to reconcile rubric language with actual concurrent dispatch, and to add an explicit \"architect's output JSON is read-on-best-effort by your peers\" note to task_planner / risk_analyst rubrics so the rubric language matches behavior.\n\n- **orchestrator/substrate/in_process.py:994-1034** \u2014 The orchestrator records `tracker.handle_ack(reviewer, producer, ...)` synthetically based on `reviewer_exit_code == 0`, **not** by parsing the reviewer's verdict JSON at `verdict_path`. A real reviewer that NACKs by writing `{\"verdict\": \"NACK\", ...}` to its verdict JSON but exits cleanly will have its NACK silently dropped \u2014 the orchestrator records ACK and the plan HITL gate fires with `is_complete=True`. The spike's harness-faked tests are insensitive to this because the fakes don't emit verdicts, but real-substrate usage of slice-2 today cannot rely on the reviewer NACK path. The commit message describes this as \"production (with real harness agents whose BRC emissions would be a no-op duplicate in this path)\" but the in-process substrate has no HTTP daemon for real agents' `egg-orch consensus propose` calls to land on \u2014 those emissions would error, not be duplicates. Slice-3 / 4 will need to wire verdict-JSON parsing or in-process BRC verb emission for the reviewer NACK path to actually work. Track in a follow-up issue.\n\n- **orchestrator/substrate/in_process.py:911 (\"reviewer_plan is spawned after each `CONSENSUS_PROPOSE`\")** \u2014 The task-2-1 acceptance criterion phrasing is ambiguous: it can be read as \"one reviewer spawn per producer propose\" (3 spawns) or as \"reviewer spawn is conditioned on at least one producer having proposed\" (1 spawn). Current code does the latter \u2014 one reviewer spawn after all three producers complete. The docstring at lines 1136-1139 documents the design choice (\"the synchronous spawn model means the producers' artifacts are on disk before the reviewer starts\"). Reasonable interpretation given the spike's spawn semantics, but reviewer_contract may want to verify this read. Either way the BRC tracker records per-producer ACKs (one tracker.handle_ack call per successful producer at lines 1011-1029), which satisfies the \"per-edge consensus\" spirit of the criterion.\n\n- **orchestrator/substrate/in_process.py:1128-1170** \u2014 The reviewer's spawn_env sets `EGG_PRODUCER_ARTIFACT_PATHS` but not the role-specific output paths the `reviewer_plan.md` rubric names (`analysis_path`, `architect_output_path`, `task_planner_output_path`, `risk_analyst_output_path`). After dedup, the producer-paths list collapses to a single entry (every producer's `producer_artifacts[role]` value is the same `plan_artifact_path`). The reviewer must infer the per-role JSON output paths from rubric convention. This matches the slice-1 pattern (the refiner also doesn't get `analysis_path` directly), but the rubric's input enumeration sets an expectation that slice-2's env shaping does not meet. Consider follow-up to surface role-specific paths in spawn_env so reviewer / task_planner / risk_analyst can read peer outputs deterministically rather than by convention-guessing.\n\n- **orchestrator/substrate/in_process.py:925-931** \u2014 The \"reuse existing tracker\" branch (`if tracker is None: create_peer_consensus_tracker(...)`) is dead code today \u2014 slice-1's `_spawn_refiner` does not register a tracker (the `create_peer_consensus_tracker` import is `noqa: F401`), and `_tick_brc_review` only reads. If a future slice registers a tracker with a different graph (e.g., refine graph during a future refine BRC cycle), the slice-2 plan phase would reuse that tracker with the wrong graph. Worth a guard that asserts the existing tracker's graph matches the plan graph before reuse, or just always-create (the spike's tight propose\u2192ack\u2192confirm sequence has no need to reuse).\n\n- **orchestrator/substrate/in_process.py:934** \u2014 `self._plan_tracker = tracker` is set but never read elsewhere in the module. If the intent was to expose the tracker for tests / observability, document the surface; otherwise drop the assignment.\n\n- **orchestrator/substrate/in_process.py:91 (`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`)** \u2014 Synthetic 7-hex constant for the test path. Real-substrate spawns capture `git rev-parse HEAD` post-commit, so the fallback only fires for harness fakes that don't write a commit. The constant is intentionally obviously-synthetic in log output. Worth a docstring note on `_SYNTHETIC_PLAN_COMMIT` mentioning that any caller hex-validating `commit_sha` (e.g. a gateway-style policy check) accepts this as a 7-char hex \u2014 non-issue today because the in-process bus doesn't gateway-validate, but a slice-5 hardening pass may want to swap to a clearly-non-hex sentinel (e.g. `\"synthetic-plan-commit\"`) if any consumer becomes hex-strict.\n\n- **orchestrator/substrate/in_process.py:1011-1029** \u2014 The `try / except Exception: pass` around `tracker.handle_ack` silently swallows malformed-payload errors (e.g., a ReviewPayload validation failure). The eval snapshot's `unresolved_nack_details` surfaces the unconfirmed edge, so the failure is visible at the HITL gate, but the operator sees no specific error message. Acceptable for the spike but worth flagging if the consumed payload shape changes (e.g., #2142's `ack_version` plumbing extends the required fields).\n\nNo security, correctness, or robustness issues that block the slice's atomic landing. Coder's submission ACKed.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:11.169900+00:00", - "phase": "implement" - }, - { - "id": "62235097-bcbe-45", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:11.227683+00:00", - "phase": "implement" - }, - { - "id": "7e7402ee-e699-44", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:13.471903+00:00", - "phase": "implement" - }, - { - "id": "9978420e-1a19-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:14.558629+00:00", - "phase": "implement" - }, - { - "id": "a714377a-2e1a-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:15.282300+00:00", - "phase": "implement" - }, - { - "id": "3b9a37d8-4f9e-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:19.146212+00:00", - "phase": "implement" - }, - { - "id": "612b60d5-840f-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:28.543783+00:00", - "phase": "implement" - }, - { - "id": "ebb6ce83-2229-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:08:31.295238+00:00", - "phase": "implement" - }, - { - "id": "f05b0f7b-a72d-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:09:04.846518+00:00", - "phase": "implement" - }, - { - "id": "4a9b1fb0-7a25-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:09:13.524285+00:00", - "phase": "implement" - }, - { - "id": "18878ca2-61a6-4c", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:09:14.651372+00:00", - "phase": "implement" - }, - { - "id": "c81b3bf1-ace0-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:09:15.335492+00:00", - "phase": "implement" - }, - { - "id": "a8292db9-f6df-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:09:19.200615+00:00", - "phase": "implement" - }, - { - "id": "48a27f87-b194-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:09:28.592596+00:00", - "phase": "implement" - }, - { - "id": "a797b721-7b11-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:09:31.344568+00:00", - "phase": "implement" - }, - { - "id": "2e41f757-6e7b-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:10:04.953097+00:00", - "phase": "implement" - }, - { - "id": "456d0a9c-abfd-46", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:10:13.567698+00:00", - "phase": "implement" - }, - { - "id": "77d45174-8043-44", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:10:14.741418+00:00", - "phase": "implement" - }, - { - "id": "a8bbccbf-8102-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:10:15.415885+00:00", - "phase": "implement" - }, - { - "id": "94843ce9-9215-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:10:19.248896+00:00", - "phase": "implement" - }, - { - "id": "c5a40c4f-9aa2-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:10:28.666434+00:00", - "phase": "implement" - }, - { - "id": "753031e7-7d68-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:10:31.401679+00:00", - "phase": "implement" - }, - { - "id": "a5775d43-b6c8-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:11:05.135225+00:00", - "phase": "implement" - }, - { - "id": "5229cdbe-98c7-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:11:13.630360+00:00", - "phase": "implement" - }, - { - "id": "0fb0f488-d631-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:11:14.803169+00:00", - "phase": "implement" - }, - { - "id": "2819bc2b-c8c6-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:11:15.488410+00:00", - "phase": "implement" - }, - { - "id": "165f84e2-dd81-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:11:19.300490+00:00", - "phase": "implement" - }, - { - "id": "a06323c6-629a-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:11:28.768647+00:00", - "phase": "implement" - }, - { - "id": "48fb0128-9530-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:11:31.520616+00:00", - "phase": "implement" - }, - { - "id": "5603cbab-395f-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:12:05.437471+00:00", - "phase": "implement" - }, - { - "id": "81e4a032-dfcc-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:12:32.033107+00:00", - "phase": "implement" - }, - { - "id": "4bf0540a-44fe-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:12:32.035342+00:00", - "phase": "implement" - }, - { - "id": "7803d64b-3b96-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:12:32.035742+00:00", - "phase": "implement" - }, - { - "id": "0cd6cbbb-e955-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:12:32.036566+00:00", - "phase": "implement" - }, - { - "id": "be7aad66-d364-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:12:32.101456+00:00", - "phase": "implement" - }, - { - "id": "f475df61-15c5-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:12:32.291161+00:00", - "phase": "implement" - }, - { - "id": "19516057-7a15-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:13:22.388839+00:00", - "phase": "implement" - }, - { - "id": "b830ec4c-3dd1-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:13:47.746529+00:00", - "phase": "implement" - }, - { - "id": "edf929e2-745b-42", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:13:47.758759+00:00", - "phase": "implement" - }, - { - "id": "b5a8a3b8-3d8c-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:13:47.773694+00:00", - "phase": "implement" - }, - { - "id": "76550f6b-98c3-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:13:47.780694+00:00", - "phase": "implement" - }, - { - "id": "c4e567f7-4ab9-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:13:47.793385+00:00", - "phase": "implement" - }, - { - "id": "f4632fa6-6f9e-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:13:47.810040+00:00", - "phase": "implement" - }, - { - "id": "7b06de42-e051-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:14:38.689262+00:00", - "phase": "implement" - }, - { - "id": "8abaa996-7c72-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:03.729445+00:00", - "phase": "implement" - }, - { - "id": "cc88e713-2dae-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:03.732929+00:00", - "phase": "implement" - }, - { - "id": "a3ae0f8a-d86d-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:03.734335+00:00", - "phase": "implement" - }, - { - "id": "14e2769d-ea79-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:03.737178+00:00", - "phase": "implement" - }, - { - "id": "dd9ec42b-bcae-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:03.739606+00:00", - "phase": "implement" - }, - { - "id": "afc0fbea-4a3e-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:03.740765+00:00", - "phase": "implement" - }, - { - "id": "9221a2b1-22fe-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:53.952918+00:00", - "phase": "implement" - }, - { - "id": "6a5fdee9-21e4-4e", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:58.731766+00:00", - "phase": "implement" - }, - { - "id": "a4b198b5-9c07-42", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:15:59.885174+00:00", - "phase": "implement" - }, - { - "id": "79af6064-abf7-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:16:00.616976+00:00", - "phase": "implement" - }, - { - "id": "248f514f-4dd3-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:16:02.122103+00:00", - "phase": "implement" - }, - { - "id": "9544ab9c-1d91-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:16:02.178445+00:00", - "phase": "implement" - }, - { - "id": "3df786d4-814f-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:16:02.387791+00:00", - "phase": "implement" - }, - { - "id": "7a4f207f-2804-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:16:50.569295+00:00", - "phase": "implement" - }, - { - "id": "3efe176d-1730-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:16:58.818830+00:00", - "phase": "implement" - }, - { - "id": "f8fb2abd-bdc5-4a", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:16:59.934007+00:00", - "phase": "implement" - }, - { - "id": "03b1697f-08e8-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:17:00.678212+00:00", - "phase": "implement" - }, - { - "id": "f4d8d1c5-6daf-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:17:02.294079+00:00", - "phase": "implement" - }, - { - "id": "c574da09-30fb-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:17:02.327275+00:00", - "phase": "implement" - }, - { - "id": "928c506c-a57d-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:17:02.550330+00:00", - "phase": "implement" - }, - { - "id": "f53884a6-dc1b-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:04.546467+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:17:50.620136+00:00", - "phase": "implement" - }, - { - "id": "e3f8d095-99ad-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T19:57:02.754451+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:17:58.874054+00:00", - "phase": "implement" - }, - { - "id": "4b9bf7e2-9e26-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:06:14.154836+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:17:59.999563+00:00", - "phase": "implement" - }, - { - "id": "318ef50c-16a4-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:03:14.869540+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:00.728083+00:00", - "phase": "implement" - }, - { - "id": "51a3ce17-a90a-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:02.374144+00:00", - "phase": "implement" - }, - { - "id": "c42e6072-f260-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:01:27.865295+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:02.418513+00:00", - "phase": "implement" - }, - { - "id": "8fe2cb00-4ee5-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:02.604619+00:00", - "phase": "implement" - }, - { - "id": "53b27b73-79bf-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:29.341005+00:00", - "phase": "implement" - }, - { - "id": "d213cd2d-a1a3-4e", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:29.423861+00:00", - "phase": "implement" - }, - { - "id": "618c9efa-c2e3-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:29.431282+00:00", - "phase": "implement" - }, - { - "id": "8e8e9b74-e2b4-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:29.445262+00:00", - "phase": "implement" - }, - { - "id": "4c65e384-a58d-45", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:29.447873+00:00", - "phase": "implement" - }, - { - "id": "8304d51c-51e3-40", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.560221+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:35.587823+00:00", - "phase": "implement" - }, - { - "id": "0f513240-5f86-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.796116+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:35.845177+00:00", - "phase": "implement" - }, - { - "id": "85aa7f78-a081-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:37.123503+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:37.190491+00:00", - "phase": "implement" - }, - { - "id": "ecbc4e4c-e58a-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:44.228261+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:18:44.262996+00:00", - "phase": "implement" - }, - { - "id": "3ef00e30-2702-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:19:02.408844+00:00", - "phase": "implement" - }, - { - "id": "58eca11a-aa71-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:19:02.632116+00:00", - "phase": "implement" - }, - { - "id": "925ad237-836d-46", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.560221+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:19:35.670238+00:00", - "phase": "implement" - }, - { - "id": "1a6e08d9-ffe0-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.796116+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:19:35.915899+00:00", - "phase": "implement" - }, - { - "id": "33cd2ffe-c4a5-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:37.123503+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:19:37.242127+00:00", - "phase": "implement" - }, - { - "id": "c3de86a9-2565-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:44.228261+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:19:44.309464+00:00", - "phase": "implement" - }, - { - "id": "6d5d0dc6-2081-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:02.461168+00:00", - "phase": "implement" - }, - { - "id": "d83cb088-f9d6-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:02.677590+00:00", - "phase": "implement" - }, - { - "id": "acd98786-e10e-4a", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_ON_ROLE", - "body": "tester slice-2: Tests written and locally verified (16/16 pass). NACKed coder v1 at 20:09 on ruff format failure in orchestrator/substrate/in_process.py (5 long-call sites need re-formatting). Cannot propose with `lint` missing from checks_passed (spawn-prompt rule: \"Only propose consensus once every configured check passes literally\"). Awaiting coder v2 push with `ruff format orchestrator/substrate/in_process.py` applied. HANDOFF already sent. Will re-run lint + propose immediately on coder v2.", - "metadata": { - "state": "WAITING_ON_ROLE", - "waiting_on": "coder", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:21.622206+00:00", - "phase": "implement" - }, - { - "id": "ba9962fd-5fd7-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:21.887327+00:00", - "phase": "implement" - }, - { - "id": "02d7d340-1042-43", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:25.948718+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:25.988491+00:00", - "phase": "implement" - }, - { - "id": "b84e18ce-bc6e-40", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:26.077573+00:00", - "phase": "implement" - }, - { - "id": "68707824-a93c-4b", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:26.961327+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:27.009551+00:00", - "phase": "implement" - }, - { - "id": "c0796578-bf27-4c", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:33.111050+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:33.152657+00:00", - "phase": "implement" - }, - { - "id": "5390cd40-2d47-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.796116+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:35.985559+00:00", - "phase": "implement" - }, - { - "id": "f5afdb2f-1aa9-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:37.123503+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:37.431157+00:00", - "phase": "implement" - }, - { - "id": "d687e34d-bed5-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:44.228261+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:20:44.375043+00:00", - "phase": "implement" - }, - { - "id": "1676e8e4-8ba1-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:21:02.533357+00:00", - "phase": "implement" - }, - { - "id": "4de9f23c-b051-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:21:02.741676+00:00", - "phase": "implement" - }, - { - "id": "7c963c51-847a-4c", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:26.961327+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:21:27.121493+00:00", - "phase": "implement" - }, - { - "id": "f774b32c-f726-4b", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:33.111050+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:21:33.217600+00:00", - "phase": "implement" - }, - { - "id": "292a13a5-cbf8-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.796116+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:21:36.047564+00:00", - "phase": "implement" - }, - { - "id": "a2172991-2f63-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:37.123503+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:21:37.493934+00:00", - "phase": "implement" - }, - { - "id": "6a04aa4a-561e-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:44.228261+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:21:44.438824+00:00", - "phase": "implement" - }, - { - "id": "30468c2b-a09e-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:22:02.598401+00:00", - "phase": "implement" - }, - { - "id": "6ba6e61e-98d1-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:22:02.797990+00:00", - "phase": "implement" - }, - { - "id": "57d40719-2249-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:26.961327+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:22:27.293236+00:00", - "phase": "implement" - }, - { - "id": "0725cbdb-4bb9-40", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:33.111050+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:22:33.415627+00:00", - "phase": "implement" - }, - { - "id": "befe1e47-f210-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:37.123503+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:22:55.105122+00:00", - "phase": "implement" - }, - { - "id": "8f1b188a-9d4c-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:44.228261+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:22:55.106135+00:00", - "phase": "implement" - }, - { - "id": "cedac962-9e02-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.796116+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:22:55.113690+00:00", - "phase": "implement" - }, - { - "id": "b9ca7f87-ba02-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:23:02.698598+00:00", - "phase": "implement" - }, - { - "id": "68ca789e-dafa-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:23:02.844682+00:00", - "phase": "implement" - }, - { - "id": "df3d672c-b0eb-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:26.961327+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:23:40.504599+00:00", - "phase": "implement" - }, - { - "id": "f40fbe35-774b-42", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:33.111050+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:23:40.558752+00:00", - "phase": "implement" - }, - { - "id": "0d021087-fb5d-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:35.796116+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:23:51.123869+00:00", - "phase": "implement" - }, - { - "id": "1e79587d-2f41-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:37.123503+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:23:52.554162+00:00", - "phase": "implement" - }, - { - "id": "5c3272e4-0d39-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:18:44.228261+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:23:55.220299+00:00", - "phase": "implement" - }, - { - "id": "8eb4e5e9-afce-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:08:19.102487+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:02.761859+00:00", - "phase": "implement" - }, - { - "id": "11c6314e-cb5f-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:00:30.663856+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:02.898969+00:00", - "phase": "implement" - }, - { - "id": "fcd357c5-3799-4a", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:26.961327+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.128830+00:00", - "phase": "implement" - }, - { - "id": "9c270128-214f-46", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:20:33.111050+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.138448+00:00", - "phase": "implement" - }, - { - "id": "3a56b578-a013-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.283926+00:00", - "phase": "implement" - }, - { - "id": "1948ca7b-5a21-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.306397+00:00", - "phase": "implement" - }, - { - "id": "6406ada8-23d9-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.311218+00:00", - "phase": "implement" - }, - { - "id": "6c240c32-aa01-4a", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.326460+00:00", - "phase": "implement" - }, - { - "id": "128db3b2-e0da-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from coder", - "body": "Slice-2 coder v2 \u2014 addresses NACK blockers from reviewer_concurrency (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format). Commit d7b2a6152.\n\nBlocker fixes:\n\nreviewer_concurrency C1: `_spawn_plan_producer` no longer writes `$HOME/.claude/egg-active-role.json` (the 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. `_spawn_plan_reviewer` (single dispatch) keeps the sentinel write.\n\nreviewer_concurrency C2: added `self._current_phase = \"refine\"` field on `_InProcessOrchestrator.__init__`; flipped to \"plan\" at the top of `_run_plan_phase`. `_publish_heartbeat` reads from it, so HEARTBEAT messages carry the correct `phase` string for stuck-phase-transition watchdogs (was hardcoded \"refine\").\n\nreviewer_code_holistic H1: `_run_plan_phase_inner` now spawns architect synchronously first, records its CONSENSUS_PROPOSE on the tracker, then fans out task_planner + risk_analyst concurrently via `ThreadPoolExecutor(max_workers=2)`. The architect's per-role output path threads into each downstream producer's spawn env (`EGG_ARCHITECT_OUTPUT_PATH`) and prompt_text. Matches `shared/egg_contracts/agent_roles.py:398/422` role-dependency declarations and the rubric bodies.\n\nreviewer_code_holistic H2: new `read_plan_reviewer_verdicts` parses `.egg-state/agent-outputs/-reviewer_plan-output.json`; `_apply_reviewer_verdicts` drives per-edge ACK / NACK on the tracker based on the reviewer's actual verdicts. Fail-closed (NACK every edge) when the verdict file is missing AND the reviewer's spawn returned exit_code != 0. Optimistic ACK only on the harness-faked path (missing file + exit 0), with the discrepancy surfaced in the placeholder body. `_spawn_plan_reviewer` env shaping now uses per-role `EGG__OUTPUT_PATH` + `EGG_REVIEWER_VERDICT_PATH` (rubric-input shape) instead of the dead `EGG_PRODUCER_ARTIFACT_PATHS`.\n\ntester T1: ran `ruff format` on both modified files. Lint + format gates pass.\n\nNon-blocker polish landed alongside the blockers (every non-blocker from the three NACKs):\n\nreviewer_concurrency NB2 (synthetic commit): `synthetic_commit_for(role)` derives per-role hex SHAs via SHA-1 of the role name; ProposalPayload entries now distinguishable per producer.\n\nreviewer_concurrency NB1 + reviewer_code_holistic NB4 (silent except): all `tracker.handle_*` guard-rejection branches now log via `logging.getLogger(\"orchestrator.substrate.in_process\").warning` rather than silent `except Exception: pass`. The eval snapshot remains the authoritative source for the HITL gate; logging is a debug surface.\n\nreviewer_code_holistic NB3 (dead env vars): `EGG_PRODUCER_ARTIFACT_PATHS` is gone. `EGG_REFINE_ARTIFACT_PATH` / `EGG_PLAN_ARTIFACT_PATH` stay because the rubrics' Task-context block names them; `EGG_PRODUCER_OUTPUT_PATH` (new) and `EGG_ARCHITECT_OUTPUT_PATH` (new) match the per-rubric input enumeration.\n\nreviewer_code_holistic NB (reviewer in placeholder): `format_plan_placeholder` renders reviewer_plan diagnostics + verdict-parsing status alongside producer diagnostics so the operator at the plan HITL gate sees end-to-end visibility.\n\nreviewer_code_holistic NB (synthetic commit escape): `_SYNTHETIC_PLAN_COMMIT` docstring now explicitly states it must never escape the in-process driver and points at the production-consumer hex-validate risk.\n\nFile decomposition:\nruff format expanded v1 from 1411 \u2192 1879 lines, breaching the 1500-line hard cap. Extracted plan-phase body to new `orchestrator/substrate/_plan_phase.py` as module-level functions that take the orchestrator instance as their first arg. The class methods (`_run_plan_phase`, `_spawn_plan_producer`, `_spawn_plan_reviewer`, `_plan_producer_output_path`, `_read_plan_reviewer_verdicts`) stay as thin delegates so tester's 16 passing v1 tests keep the same method-call surface. `in_process.py` lands at 1093 lines; `_plan_phase.py` at 680. `python3 scripts/check-file-sizes.py` passes (no errors). Decomposition rationale in `_plan_phase.py`'s module docstring.\n\nManual in-process smoke (harness fakes, MagicMock subagents) covers four new behavioural scenarios: happy path (5 spawns in architect-first order, tracker is_complete=True), refine-stop (refine artifact returned, no plan spawns), mixed verdict ACK/NACK (verdict-driven NACK on task_planner\u2192reviewer_plan), fail-closed (no verdict + reviewer exit 1 \u2192 every critical edge NACK'd). All ruff format + lint + file-size checks pass.", - "metadata": { - "payload": { - "summary": "Slice-2 coder v2 \u2014 addresses NACK blockers from reviewer_concurrency (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format). Commit d7b2a6152.\n\nBlocker fixes:\n\nreviewer_concurrency C1: `_spawn_plan_producer` no longer writes `$HOME/.claude/egg-active-role.json` (the 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. `_spawn_plan_reviewer` (single dispatch) keeps the sentinel write.\n\nreviewer_concurrency C2: added `self._current_phase = \"refine\"` field on `_InProcessOrchestrator.__init__`; flipped to \"plan\" at the top of `_run_plan_phase`. `_publish_heartbeat` reads from it, so HEARTBEAT messages carry the correct `phase` string for stuck-phase-transition watchdogs (was hardcoded \"refine\").\n\nreviewer_code_holistic H1: `_run_plan_phase_inner` now spawns architect synchronously first, records its CONSENSUS_PROPOSE on the tracker, then fans out task_planner + risk_analyst concurrently via `ThreadPoolExecutor(max_workers=2)`. The architect's per-role output path threads into each downstream producer's spawn env (`EGG_ARCHITECT_OUTPUT_PATH`) and prompt_text. Matches `shared/egg_contracts/agent_roles.py:398/422` role-dependency declarations and the rubric bodies.\n\nreviewer_code_holistic H2: new `read_plan_reviewer_verdicts` parses `.egg-state/agent-outputs/-reviewer_plan-output.json`; `_apply_reviewer_verdicts` drives per-edge ACK / NACK on the tracker based on the reviewer's actual verdicts. Fail-closed (NACK every edge) when the verdict file is missing AND the reviewer's spawn returned exit_code != 0. Optimistic ACK only on the harness-faked path (missing file + exit 0), with the discrepancy surfaced in the placeholder body. `_spawn_plan_reviewer` env shaping now uses per-role `EGG__OUTPUT_PATH` + `EGG_REVIEWER_VERDICT_PATH` (rubric-input shape) instead of the dead `EGG_PRODUCER_ARTIFACT_PATHS`.\n\ntester T1: ran `ruff format` on both modified files. Lint + format gates pass.\n\nNon-blocker polish landed alongside the blockers (every non-blocker from the three NACKs):\n\nreviewer_concurrency NB2 (synthetic commit): `synthetic_commit_for(role)` derives per-role hex SHAs via SHA-1 of the role name; ProposalPayload entries now distinguishable per producer.\n\nreviewer_concurrency NB1 + reviewer_code_holistic NB4 (silent except): all `tracker.handle_*` guard-rejection branches now log via `logging.getLogger(\"orchestrator.substrate.in_process\").warning` rather than silent `except Exception: pass`. The eval snapshot remains the authoritative source for the HITL gate; logging is a debug surface.\n\nreviewer_code_holistic NB3 (dead env vars): `EGG_PRODUCER_ARTIFACT_PATHS` is gone. `EGG_REFINE_ARTIFACT_PATH` / `EGG_PLAN_ARTIFACT_PATH` stay because the rubrics' Task-context block names them; `EGG_PRODUCER_OUTPUT_PATH` (new) and `EGG_ARCHITECT_OUTPUT_PATH` (new) match the per-rubric input enumeration.\n\nreviewer_code_holistic NB (reviewer in placeholder): `format_plan_placeholder` renders reviewer_plan diagnostics + verdict-parsing status alongside producer diagnostics so the operator at the plan HITL gate sees end-to-end visibility.\n\nreviewer_code_holistic NB (synthetic commit escape): `_SYNTHETIC_PLAN_COMMIT` docstring now explicitly states it must never escape the in-process driver and points at the production-consumer hex-validate risk.\n\nFile decomposition:\nruff format expanded v1 from 1411 \u2192 1879 lines, breaching the 1500-line hard cap. Extracted plan-phase body to new `orchestrator/substrate/_plan_phase.py` as module-level functions that take the orchestrator instance as their first arg. The class methods (`_run_plan_phase`, `_spawn_plan_producer`, `_spawn_plan_reviewer`, `_plan_producer_output_path`, `_read_plan_reviewer_verdicts`) stay as thin delegates so tester's 16 passing v1 tests keep the same method-call surface. `in_process.py` lands at 1093 lines; `_plan_phase.py` at 680. `python3 scripts/check-file-sizes.py` passes (no errors). Decomposition rationale in `_plan_phase.py`'s module docstring.\n\nManual in-process smoke (harness fakes, MagicMock subagents) covers four new behavioural scenarios: happy path (5 spawns in architect-first order, tracker is_complete=True), refine-stop (refine artifact returned, no plan spawns), mixed verdict ACK/NACK (verdict-driven NACK on task_planner\u2192reviewer_plan), fail-closed (no verdict + reviewer exit 1 \u2192 every critical edge NACK'd). All ruff format + lint + file-size checks pass.", - "attestation": { - "commit_sha": "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2", - "commit_shas": [ - "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2" - ], - "files_changed": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py" - ], - "checks_passed": [ - "ruff_format_check", - "ruff_check_lint", - "file_size_lint", - "manual_smoke_happy_path", - "manual_smoke_refine_stop", - "manual_smoke_mixed_verdict", - "manual_smoke_fail_closed", - "import_check" - ], - "tests_run": 8, - "no_test_changes_needed": true, - "no_test_changes_reason": "Coder role's allow-list excludes tests/. Class methods stayed as thin delegates so tester's existing 16 passing tests keep working. Eight manual smoke runs cover the new behaviour surface." - }, - "artifacts": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py" - ], - "risk_considered": "v2 risks: (1) Decomposition risk \u2014 extracting ~700 lines to `_plan_phase.py` could break tester's 16 v1 tests if they call the class methods. Mitigated by keeping the class methods as thin delegates with identical signatures. (2) Verdict-JSON parsing risk \u2014 fail-closed-on-missing-verdict could break harness-faked tests that rely on optimistic ACK. Mitigated by only fail-closing when reviewer exit_code is non-zero; the missing-file + exit-0 path keeps the optimistic ACK behaviour with a placeholder-body discrepancy surface so the operator sees it at the HITL gate. (3) Phase-flip atomicity \u2014 `self._current_phase` is written from `_run_plan_phase` and read from `_publish_heartbeat`; Python GIL makes the single assignment atomic, and the reader doesn't compose with other state in a way that observes inconsistent snapshots.", - "commit_sha": "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2", - "files_changed": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py" - ], - "tests_run": [], - "tasks_satisfied": [ - "task-2-1", - "task-2-2", - "task-2-5" - ] - }, - "version": 2, - "commit_sha": "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.633607+00:00", - "phase": "implement" - }, - { - "id": "3c09a940-b384-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.726423+00:00", - "phase": "implement" - }, - { - "id": "fbc571b4-b883-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.735614+00:00", - "phase": "implement" - }, - { - "id": "d7d900ad-4d91-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:24:43.741057+00:00", - "phase": "implement" - }, - { - "id": "148f0318-dc03-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:24:48.589646+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:11.264240+00:00", - "phase": "implement" - }, - { - "id": "12830da3-33f9-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:24:49.288373+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:11.272392+00:00", - "phase": "implement" - }, - { - "id": "71a2c747-2391-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:24:49.774794+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:11.281471+00:00", - "phase": "implement" - }, - { - "id": "ed745c64-5c37-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:24:50.772614+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:11.306564+00:00", - "phase": "implement" - }, - { - "id": "ac89609b-98da-42", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:11.486511+00:00", - "phase": "implement" - }, - { - "id": "f0244c46-9f4e-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:11.509630+00:00", - "phase": "implement" - }, - { - "id": "da2a62b9-2f0b-40", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:25:29.414853+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:29.480803+00:00", - "phase": "implement" - }, - { - "id": "140eb5ea-5a9e-4d", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from coder", - "body": "Slice-2 coder v2 \u2014 addresses NACK blockers from reviewer_concurrency (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format). Commit d7b2a6152. Also folds in every non-blocker noted in the three NACKs (synthetic commit per role, log warnings on tracker guard rejections, reviewer in placeholder, dead env vars dropped, _SYNTHETIC_PLAN_COMMIT docstring caveat). ruff format expanded the v1 diff to 1879 lines; extracted plan-phase body to new `orchestrator/substrate/_plan_phase.py` (680 lines) as module functions taking the orchestrator instance as their first arg. Class methods stay as thin delegates so tester's 16 passing v1 tests keep working. `in_process.py` now 1093 lines. `python3 scripts/check-file-sizes.py` passes. Manual smoke: happy path (5 spawns architect-first, is_complete=True), refine-stop (refine artifact returned, no plan dispatch), mixed verdict (verdict-driven NACK on task_planner\u2192reviewer_plan), fail-closed (no verdict + reviewer exit 1 \u2192 critical edges NACK'd, advisory edge confirms).", - "metadata": { - "payload": { - "summary": "Slice-2 coder v2 \u2014 addresses NACK blockers from reviewer_concurrency (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format). Commit d7b2a6152. Also folds in every non-blocker noted in the three NACKs (synthetic commit per role, log warnings on tracker guard rejections, reviewer in placeholder, dead env vars dropped, _SYNTHETIC_PLAN_COMMIT docstring caveat). ruff format expanded the v1 diff to 1879 lines; extracted plan-phase body to new `orchestrator/substrate/_plan_phase.py` (680 lines) as module functions taking the orchestrator instance as their first arg. Class methods stay as thin delegates so tester's 16 passing v1 tests keep working. `in_process.py` now 1093 lines. `python3 scripts/check-file-sizes.py` passes. Manual smoke: happy path (5 spawns architect-first, is_complete=True), refine-stop (refine artifact returned, no plan dispatch), mixed verdict (verdict-driven NACK on task_planner\u2192reviewer_plan), fail-closed (no verdict + reviewer exit 1 \u2192 critical edges NACK'd, advisory edge confirms).", - "attestation": { - "commit_sha": "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2", - "commit_shas": [ - "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2" - ], - "files_changed": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py" - ], - "checks_passed": [ - "ruff_format_check", - "ruff_check_lint", - "file_size_lint", - "manual_smoke_happy_path", - "manual_smoke_refine_stop", - "manual_smoke_mixed_verdict", - "manual_smoke_fail_closed", - "import_check" - ], - "tests_run": 8, - "no_test_changes_needed": true, - "no_test_changes_reason": "Coder role's allow-list excludes tests/. Class methods stayed as thin delegates so tester's existing 16 passing v1 tests keep working. Eight manual smoke runs cover the new behaviour surface." - }, - "artifacts": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py" - ], - "risk_considered": "Decomposition could break tester's v1 tests if they called methods directly; mitigated by keeping class methods as thin delegates with identical signatures. Verdict-JSON fail-closed could break harness-faked tests reliant on optimistic ACK; mitigated by only fail-closing when reviewer exit_code != 0 \u2014 the missing-file + exit-0 path preserves optimistic ACK with a placeholder-body discrepancy surface. Phase-flip atomicity is fine per Python GIL on a single assignment.", - "commit_sha": "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2", - "files_changed": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py" - ], - "tests_run": [], - "tasks_satisfied": [ - "task-2-1", - "task-2-2", - "task-2-5" - ] - }, - "version": 3, - "commit_sha": "d7b2a615268c0ac37a7ed872f2b16c48ade4abe2", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:36.551327+00:00", - "phase": "implement" - }, - { - "id": "f43e4335-f839-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:36.690683+00:00", - "phase": "implement" - }, - { - "id": "642c4c16-30c9-48", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:36.708087+00:00", - "phase": "implement" - }, - { - "id": "923e5053-e688-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:36.719678+00:00", - "phase": "implement" - }, - { - "id": "7ce5e59a-924e-4f", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:25:45.362708+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:58.518260+00:00", - "phase": "implement" - }, - { - "id": "9fd88793-01bc-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:25:51.626425+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:58.622440+00:00", - "phase": "implement" - }, - { - "id": "352b9725-d257-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:25:58.688034+00:00", - "phase": "implement" - }, - { - "id": "5012afde-5a5d-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:26:03.178962+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:26:23.796323+00:00", - "phase": "implement" - }, - { - "id": "263de540-03b5-43", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:25:45.362708+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:26:58.573268+00:00", - "phase": "implement" - }, - { - "id": "d141bf25-afe5-45", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from tester for coder", - "body": "\nReviewed coder v2 (commit `d7b2a6152`, slice-2 TASK-2-1 / TASK-2-2 / TASK-2-5 no-op). \n\n**v1 blocker T1 fully resolved.** `PYTHONPATH=shared:gateway:orchestrator ruff format --check .` now exits 0 across all 962 files; the 5 long-call sites in `_run_plan_phase` v1 are reformatted (and the decomposition into `orchestrator/substrate/_plan_phase.py` moved the affected blocks to a standalone module that also passes `ruff format --check`). `ruff check` and `mypy gateway shared sandbox` are both green. \n\n**16/16 tester-authored tests still pass against v2** (`integration_tests/regression/test_inprocess_plan_brc.py` 7/7; `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` 9/9) with no test edits \u2014 the architectural changes (architect-first synchronous spawn then fanout via 2-worker pool for task_planner + risk_analyst; verdict-JSON ACK/NACK parsing; sentinel removal from concurrent producer path) preserve every observable invariant the tests pin:\n- 3 plan producers + 1 reviewer spawn through the substrate (sequence is now architect \u2192 task_planner \u2225 risk_analyst \u2192 reviewer_plan, which my `test_plan_stage_spawns_three_producers_and_one_reviewer` still passes because it asserts on the *set* of spawned roles, not ordering);\n- BRC reaches `is_complete=True` with all 4 plan-team roles confirmed (verdict-JSON parsing falls back to optimistic-ACK on the harness-faked path where `agent_outputs/-reviewer_plan-output.json` is absent and reviewer exit_code==0, exactly as your H2 narrative describes);\n- plan-HITL decision shape unchanged (`phase=\"plan\"`, `decision_type=\"phase_gate\"`);\n- non-`approve_continue` refine answers still short-circuit before the plan stage (no plan-producer spawn);\n- implement-phase roles still not spawned;\n- refiner spawned exactly once;\n- every plan-phase spawn carries `EGG_PHASE=plan`.\n\n### v2 deltas spot-checked\n\n1. **`_plan_phase.py` decomposition** \u2014 clean module boundary (lazy imports for `PeerConsensusTracker` / `get_review_graph_for_phase` / `AgentRole` at use site; the underscore prefix matches the project's submodule pattern from `docs/guides/decomposition-pattern.md`). `_run_plan_phase` in `in_process.py:233` now delegates to `_plan_phase._run_plan_phase_inner(self, refine_artifact_path)`; the runner instance's attributes (`self._bundle`, `self._plan_tracker`, `self._current_phase`) carry the state across the call boundary. Module-level surface is correctly minimal (`run_plan_phase` is the only public symbol; the helpers are private).\n\n2. **C1 fix \u2014 sentinel removed from concurrent producer path** (`_plan_phase.py` \u2014 no `_write_active_role_sentinel` call inside `_spawn_plan_producer_inner`). The reviewer path retains it (`_spawn_plan_reviewer_inner`). The R2-deferral docstring on `_write_active_role_sentinel` previously documented the last-writer-wins limitation; this fix actively avoids hitting it for the concurrent fanout. The `EGG_AGENT_ROLE` env var remains the load-bearing role-resolution channel per spawn. Architecturally correct.\n\n3. **C2 fix \u2014 `_current_phase` state** (`in_process.py:189` set to `\"refine\"`; flipped to `\"plan\"` at the top of `_run_plan_phase`). `_publish_heartbeat` (`in_process.py:373`) reads from it. Stuck-phase-transition watchdogs filtering by `phase` now see the right phase across the transition. Sound.\n\n4. **H1 fix \u2014 architect-first then fanout** (`_plan_phase.py:_run_plan_phase_inner`). Architect synchronously spawns first; `EGG_ARCHITECT_OUTPUT_PATH` is threaded into the env + prompt of `task_planner` and `risk_analyst`. The order matches `shared/egg_contracts/agent_roles.py:398,422` (`TASK_PLANNER_ROLE.dependencies = [AgentRole.ARCHITECT]`, `RISK_ANALYST_ROLE.dependencies = [AgentRole.ARCHITECT]`). Matches the rubric semantics shipped by the documenter in `architect.md` / `task_planner.md` / `risk_analyst.md`.\n\n5. **H2 fix \u2014 reviewer_plan verdict-JSON parsing** (`_plan_phase.py:read_plan_reviewer_verdicts` / `_apply_reviewer_verdicts`). The fail-closed branch (verdict file missing AND reviewer exit_code != 0 \u2192 NACK every edge) is the right default; the optimistic-ACK branch (verdict file missing AND reviewer exit_code == 0) preserves the harness-faked test path I depend on while surfacing `verdict-not-parsed` in the placeholder body for the operator. The valid-JSON branch correctly applies per-edge ACK/NACK based on the reviewer's declared verdict. Good defensive shape.\n\n### Non-blocking (carry to follow-up)\n\n- **Verdict-JSON schema is documented in the reviewer_plan rubric body but not in a typed validator.** `read_plan_reviewer_verdicts` does a loose dict probe (`isinstance(v, dict)`, `.get(\"verdict\")`). A malformed reviewer output (e.g. `{\"verdicts\": \"approved\"}` \u2014 bare string instead of per-producer dict) would silently degrade to the empty-verdicts case. Adding a `pydantic` / `dataclass`-backed schema (or a JSON Schema check) would surface that as a structured error rather than a soft fallback. Not blocking because the placeholder body surfaces `verdict-not-parsed` so the operator's HITL gate sees the discrepancy; just worth a follow-up.\n\n- **`_current_phase` is set as a bare string field with no enum**. Setting it to an unknown phase string would silently produce a misleading heartbeat. The existing `phase` argument on `Message` is also loosely typed; if a future slice introduces a `PipelinePhase` enum that's the right tightening point. Not blocking.\n\n- **`_spawn_plan_reviewer_inner` retains the sentinel write** (`_plan_phase.py`). Correct per your C1 narrative (the reviewer's spawn never overlaps another spawn), but a one-line comment at the call site documenting WHY this single survives the C1 removal would help a future reader who scans both paths. Not blocking; the C1 commit message already documents it.\n\n- **`EGG_REFINE_ARTIFACT_PATH` is set as a per-producer env arg but not on the reviewer's env** (`_plan_phase.py:_spawn_plan_reviewer_inner`). Since the reviewer's prompt names the producer artifact paths explicitly (`producer_artifact_paths` repr), the refine artifact isn't strictly needed \u2014 but for consistency with the production prompt's \"Refine artifact: ...\" preamble, threading it through the reviewer's env wouldn't hurt. Not blocking.\n\nAll ACs in the contract task-2-1 / task-2-2 / task-2-5 are satisfied; lint is green; my 16/16 tests pass against v2 with no edits. ACK.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "\nReviewed coder v2 (commit `d7b2a6152`, slice-2 TASK-2-1 / TASK-2-2 / TASK-2-5 no-op). \n\n**v1 blocker T1 fully resolved.** `PYTHONPATH=shared:gateway:orchestrator ruff format --check .` now exits 0 across all 962 files; the 5 long-call sites in `_run_plan_phase` v1 are reformatted (and the decomposition into `orchestrator/substrate/_plan_phase.py` moved the affected blocks to a standalone module that also passes `ruff format --check`). `ruff check` and `mypy gateway shared sandbox` are both green. \n\n**16/16 tester-authored tests still pass against v2** (`integration_tests/regression/test_inprocess_plan_brc.py` 7/7; `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` 9/9) with no test edits \u2014 the architectural changes (architect-first synchronous spawn then fanout via 2-worker pool for task_planner + risk_analyst; verdict-JSON ACK/NACK parsing; sentinel removal from concurrent producer path) preserve every observable invariant the tests pin:\n- 3 plan producers + 1 reviewer spawn through the substrate (sequence is now architect \u2192 task_planner \u2225 risk_analyst \u2192 reviewer_plan, which my `test_plan_stage_spawns_three_producers_and_one_reviewer` still passes because it asserts on the *set* of spawned roles, not ordering);\n- BRC reaches `is_complete=True` with all 4 plan-team roles confirmed (verdict-JSON parsing falls back to optimistic-ACK on the harness-faked path where `agent_outputs/-reviewer_plan-output.json` is absent and reviewer exit_code==0, exactly as your H2 narrative describes);\n- plan-HITL decision shape unchanged (`phase=\"plan\"`, `decision_type=\"phase_gate\"`);\n- non-`approve_continue` refine answers still short-circuit before the plan stage (no plan-producer spawn);\n- implement-phase roles still not spawned;\n- refiner spawned exactly once;\n- every plan-phase spawn carries `EGG_PHASE=plan`.\n\n### v2 deltas spot-checked\n\n1. **`_plan_phase.py` decomposition** \u2014 clean module boundary (lazy imports for `PeerConsensusTracker` / `get_review_graph_for_phase` / `AgentRole` at use site; the underscore prefix matches the project's submodule pattern from `docs/guides/decomposition-pattern.md`). `_run_plan_phase` in `in_process.py:233` now delegates to `_plan_phase._run_plan_phase_inner(self, refine_artifact_path)`; the runner instance's attributes (`self._bundle`, `self._plan_tracker`, `self._current_phase`) carry the state across the call boundary. Module-level surface is correctly minimal (`run_plan_phase` is the only public symbol; the helpers are private).\n\n2. **C1 fix \u2014 sentinel removed from concurrent producer path** (`_plan_phase.py` \u2014 no `_write_active_role_sentinel` call inside `_spawn_plan_producer_inner`). The reviewer path retains it (`_spawn_plan_reviewer_inner`). The R2-deferral docstring on `_write_active_role_sentinel` previously documented the last-writer-wins limitation; this fix actively avoids hitting it for the concurrent fanout. The `EGG_AGENT_ROLE` env var remains the load-bearing role-resolution channel per spawn. Architecturally correct.\n\n3. **C2 fix \u2014 `_current_phase` state** (`in_process.py:189` set to `\"refine\"`; flipped to `\"plan\"` at the top of `_run_plan_phase`). `_publish_heartbeat` (`in_process.py:373`) reads from it. Stuck-phase-transition watchdogs filtering by `phase` now see the right phase across the transition. Sound.\n\n4. **H1 fix \u2014 architect-first then fanout** (`_plan_phase.py:_run_plan_phase_inner`). Architect synchronously spawns first; `EGG_ARCHITECT_OUTPUT_PATH` is threaded into the env + prompt of `task_planner` and `risk_analyst`. The order matches `shared/egg_contracts/agent_roles.py:398,422` (`TASK_PLANNER_ROLE.dependencies = [AgentRole.ARCHITECT]`, `RISK_ANALYST_ROLE.dependencies = [AgentRole.ARCHITECT]`). Matches the rubric semantics shipped by the documenter in `architect.md` / `task_planner.md` / `risk_analyst.md`.\n\n5. **H2 fix \u2014 reviewer_plan verdict-JSON parsing** (`_plan_phase.py:read_plan_reviewer_verdicts` / `_apply_reviewer_verdicts`). The fail-closed branch (verdict file missing AND reviewer exit_code != 0 \u2192 NACK every edge) is the right default; the optimistic-ACK branch (verdict file missing AND reviewer exit_code == 0) preserves the harness-faked test path I depend on while surfacing `verdict-not-parsed` in the placeholder body for the operator. The valid-JSON branch correctly applies per-edge ACK/NACK based on the reviewer's declared verdict. Good defensive shape.\n\n### Non-blocking (carry to follow-up)\n\n- **Verdict-JSON schema is documented in the reviewer_plan rubric body but not in a typed validator.** `read_plan_reviewer_verdicts` does a loose dict probe (`isinstance(v, dict)`, `.get(\"verdict\")`). A malformed reviewer output (e.g. `{\"verdicts\": \"approved\"}` \u2014 bare string instead of per-producer dict) would silently degrade to the empty-verdicts case. Adding a `pydantic` / `dataclass`-backed schema (or a JSON Schema check) would surface that as a structured error rather than a soft fallback. Not blocking because the placeholder body surfaces `verdict-not-parsed` so the operator's HITL gate sees the discrepancy; just worth a follow-up.\n\n- **`_current_phase` is set as a bare string field with no enum**. Setting it to an unknown phase string would silently produce a misleading heartbeat. The existing `phase` argument on `Message` is also loosely typed; if a future slice introduces a `PipelinePhase` enum that's the right tightening point. Not blocking.\n\n- **`_spawn_plan_reviewer_inner` retains the sentinel write** (`_plan_phase.py`). Correct per your C1 narrative (the reviewer's spawn never overlaps another spawn), but a one-line comment at the call site documenting WHY this single survives the C1 removal would help a future reader who scans both paths. Not blocking; the C1 commit message already documents it.\n\n- **`EGG_REFINE_ARTIFACT_PATH` is set as a per-producer env arg but not on the reviewer's env** (`_plan_phase.py:_spawn_plan_reviewer_inner`). Since the reviewer's prompt names the producer artifact paths explicitly (`producer_artifact_paths` repr), the refine artifact isn't strictly needed \u2014 but for consistency with the production prompt's \"Refine artifact: ...\" preamble, threading it through the reviewer's env wouldn't hurt. Not blocking.\n\nAll ACs in the contract task-2-1 / task-2-2 / task-2-5 are satisfied; lint is green; my 16/16 tests pass against v2 with no edits. ACK.\n", - "ack_version": 3 - }, - "version": 3, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:27:07.727310+00:00", - "phase": "implement" - }, - { - "id": "652bcc98-6c8e-46", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:27:07.801488+00:00", - "phase": "implement" - }, - { - "id": "44820bd9-5ea0-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:27:13.630131+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:27:13.658692+00:00", - "phase": "implement" - }, - { - "id": "6321c79a-c654-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_contract for coder", - "body": "\nRe-review of coder v3 (same commit_sha d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 \u2014 v2 \u2192 v3 is a re-propose at the same artifact). Contract-verification focus held over from my v2 ACK; the underlying commit's task-2-1/2-2/2-5 verification is unchanged.\n\n### Per-task verification (v3)\n\n**TASK-2-1 \u2014 `_run_plan_phase`** (orchestrator/substrate/in_process.py:850-921 thin delegates + orchestrator/substrate/_plan_phase.py:36-210 body):\n1. AC \"no longer raises NotImplementedError when the operator advances past refine\": \u2705 `run()` at in_process.py:246 calls `self._run_plan_phase(...)` which delegates to `_plan_phase.run_plan_phase`. The walking-skeleton fence (`_maybe_fence`) now fires only on the plan HITL gate's `approve_continue`, with a diagnostic pointing at slice-3 / slice-4 of the #2717 rollout.\n2. AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705 Spirit-of-AC satisfied. v3 implements architect-first synchronous spawn (_plan_phase.py:124-135) followed by `task_planner + risk_analyst` concurrent fan-out through `ThreadPoolExecutor(max_workers=2)` (lines 137-161). The intentional deviation from \"3 concurrent\" honours the role-dependency contract: `shared/egg_contracts/agent_roles.py` declares `TASK_PLANNER_ROLE` / `RISK_ANALYST_ROLE` with `dependencies=[ARCHITECT]`, and architect's per-role output path flows downstream via `EGG_ARCHITECT_OUTPUT_PATH` (line 470 + prompt at line 484). Required by reviewer_code_holistic v1 H1 NACK.\n3. AC \"reviewer_plan is spawned after each CONSENSUS_PROPOSE\": \u2705 Single dispatch (line 491-551) followed by verdict-JSON-driven per-edge ACK/NACK via `read_plan_reviewer_verdicts` (line 251-286) and `_apply_reviewer_verdicts` (line 289-371). Fail-closed branch NACKs every edge when the verdict file is missing AND reviewer exit_code != 0 (lines 310, 322-336); harness-fake branch ACKs with a \"verdict not parsed\" diagnostic when verdict missing + reviewer exit 0. Each producer edge receives its own tracker verdict tagged by `(reviewer_plan, producer)`.\n4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge\": \u2705 `tracker.handle_confirmed(role.value)` is invoked at _plan_phase.py:188-192 for every plan producer AND reviewer_plan; `evaluate()` at line 194 produces the snapshot; `_build_plan_gate_decision` (in_process.py:660-720) yields `HITLDecision(phase=\"plan\")` with the canonical 4-way options on convergence, retry/abort on non-convergence.\n5. AC \"existing refine path still works\": \u2705 Refine flow at in_process.py:213-240 is structurally unchanged; `self._current_phase` is initialised to `\"refine\"` (line 202) so heartbeats during refine continue to carry the right phase string before flipping to \"plan\" inside `_plan_phase.run_plan_phase` (line 67).\n\n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376):\n1. AC \"loader returns rubric bodies for all four plan-team roles\": \u2705 `_RUBRIC_LANDED_ROLES` (lines 272-286) includes architect, task_planner, risk_analyst, reviewer_plan. The fence at line 348 no longer rejects these; line 362-375 returns `rubric_path.read_text(...)` when the markdown is on disk.\n2. AC \"implement-team roles still raise ValueError with the 'follow-up slice 3' hint\": \u2705 `_ROLE_RUBRIC_SLICES` (lines 254-262) maps the eight implement-team roles to `\"slice-3\"`; the ValueError at line 356-360 interpolates `slice_hint` into the message (\"deferred to follow-up slice-3 of issue #2717's rollout\"). Structured-error contract preserved.\n\n**TASK-2-5 \u2014 sandbox restrictions parallel validator**: \u2705 Closed as no-op per slice-1 R2 = pass verdict (pinned by `integration_tests/regression/test_pretooluse_hook_nested.py:212-238`). No changes to `sandbox/egg_agent_tools/handlers/restrictions.py` in this proposal. Coder commit message records the required close-with-note (\"no-op: hooks resolve role correctly; structural enforcement remains hook-side\").\n\n### File-decomposition delta (informational)\n\nThe ruff format pass expanded the v1 diff past the 1500-line hard cap (`scripts/file-size-allowlist.yaml`), so the coder extracted ~680 lines of plan-phase body into `orchestrator/substrate/_plan_phase.py`. Class methods `_run_plan_phase`/`_spawn_plan_producer`/`_spawn_plan_reviewer`/`_plan_producer_output_path`/`_read_plan_reviewer_verdicts` stay as thin delegates (in_process.py:850-921). `in_process.py` is 1093 lines, `_plan_phase.py` is 680 lines \u2014 both under the cap. Decomposition is invisible to AC-level verification (same public method names; same call surface).\n\n### Non-blocking observations\n\n- Slice-1 contract bookkeeping: tasks task-1-1 \u2026 task-1-9 still show `status: \"pending\"` despite their commits being linked. Not a slice-2 coder issue; operator should reconcile before declaring the rollout complete.\n- The `synthetic_commit_for(role)` SHA prefix at _plan_phase.py:644-656 emits `ace1<3-hex>` \u2014 fine for 3 producers (collision impossible) and obviously synthetic in logs.\n- Fail-closed reason string (\"reviewer_plan verdict file missing / unparseable AND reviewer exit_code=\u2026\") surfaces in the placeholder body; if a future regression test wants to pin the operator-facing wording, the `_verdict_diagnostics` dict on the runner is the structured surface to assert against.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "\nRe-review of coder v3 (same commit_sha d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 \u2014 v2 \u2192 v3 is a re-propose at the same artifact). Contract-verification focus held over from my v2 ACK; the underlying commit's task-2-1/2-2/2-5 verification is unchanged.\n\n### Per-task verification (v3)\n\n**TASK-2-1 \u2014 `_run_plan_phase`** (orchestrator/substrate/in_process.py:850-921 thin delegates + orchestrator/substrate/_plan_phase.py:36-210 body):\n1. AC \"no longer raises NotImplementedError when the operator advances past refine\": \u2705 `run()` at in_process.py:246 calls `self._run_plan_phase(...)` which delegates to `_plan_phase.run_plan_phase`. The walking-skeleton fence (`_maybe_fence`) now fires only on the plan HITL gate's `approve_continue`, with a diagnostic pointing at slice-3 / slice-4 of the #2717 rollout.\n2. AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705 Spirit-of-AC satisfied. v3 implements architect-first synchronous spawn (_plan_phase.py:124-135) followed by `task_planner + risk_analyst` concurrent fan-out through `ThreadPoolExecutor(max_workers=2)` (lines 137-161). The intentional deviation from \"3 concurrent\" honours the role-dependency contract: `shared/egg_contracts/agent_roles.py` declares `TASK_PLANNER_ROLE` / `RISK_ANALYST_ROLE` with `dependencies=[ARCHITECT]`, and architect's per-role output path flows downstream via `EGG_ARCHITECT_OUTPUT_PATH` (line 470 + prompt at line 484). Required by reviewer_code_holistic v1 H1 NACK.\n3. AC \"reviewer_plan is spawned after each CONSENSUS_PROPOSE\": \u2705 Single dispatch (line 491-551) followed by verdict-JSON-driven per-edge ACK/NACK via `read_plan_reviewer_verdicts` (line 251-286) and `_apply_reviewer_verdicts` (line 289-371). Fail-closed branch NACKs every edge when the verdict file is missing AND reviewer exit_code != 0 (lines 310, 322-336); harness-fake branch ACKs with a \"verdict not parsed\" diagnostic when verdict missing + reviewer exit 0. Each producer edge receives its own tracker verdict tagged by `(reviewer_plan, producer)`.\n4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge\": \u2705 `tracker.handle_confirmed(role.value)` is invoked at _plan_phase.py:188-192 for every plan producer AND reviewer_plan; `evaluate()` at line 194 produces the snapshot; `_build_plan_gate_decision` (in_process.py:660-720) yields `HITLDecision(phase=\"plan\")` with the canonical 4-way options on convergence, retry/abort on non-convergence.\n5. AC \"existing refine path still works\": \u2705 Refine flow at in_process.py:213-240 is structurally unchanged; `self._current_phase` is initialised to `\"refine\"` (line 202) so heartbeats during refine continue to carry the right phase string before flipping to \"plan\" inside `_plan_phase.run_plan_phase` (line 67).\n\n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376):\n1. AC \"loader returns rubric bodies for all four plan-team roles\": \u2705 `_RUBRIC_LANDED_ROLES` (lines 272-286) includes architect, task_planner, risk_analyst, reviewer_plan. The fence at line 348 no longer rejects these; line 362-375 returns `rubric_path.read_text(...)` when the markdown is on disk.\n2. AC \"implement-team roles still raise ValueError with the 'follow-up slice 3' hint\": \u2705 `_ROLE_RUBRIC_SLICES` (lines 254-262) maps the eight implement-team roles to `\"slice-3\"`; the ValueError at line 356-360 interpolates `slice_hint` into the message (\"deferred to follow-up slice-3 of issue #2717's rollout\"). Structured-error contract preserved.\n\n**TASK-2-5 \u2014 sandbox restrictions parallel validator**: \u2705 Closed as no-op per slice-1 R2 = pass verdict (pinned by `integration_tests/regression/test_pretooluse_hook_nested.py:212-238`). No changes to `sandbox/egg_agent_tools/handlers/restrictions.py` in this proposal. Coder commit message records the required close-with-note (\"no-op: hooks resolve role correctly; structural enforcement remains hook-side\").\n\n### File-decomposition delta (informational)\n\nThe ruff format pass expanded the v1 diff past the 1500-line hard cap (`scripts/file-size-allowlist.yaml`), so the coder extracted ~680 lines of plan-phase body into `orchestrator/substrate/_plan_phase.py`. Class methods `_run_plan_phase`/`_spawn_plan_producer`/`_spawn_plan_reviewer`/`_plan_producer_output_path`/`_read_plan_reviewer_verdicts` stay as thin delegates (in_process.py:850-921). `in_process.py` is 1093 lines, `_plan_phase.py` is 680 lines \u2014 both under the cap. Decomposition is invisible to AC-level verification (same public method names; same call surface).\n\n### Non-blocking observations\n\n- Slice-1 contract bookkeeping: tasks task-1-1 \u2026 task-1-9 still show `status: \"pending\"` despite their commits being linked. Not a slice-2 coder issue; operator should reconcile before declaring the rollout complete.\n- The `synthetic_commit_for(role)` SHA prefix at _plan_phase.py:644-656 emits `ace1<3-hex>` \u2014 fine for 3 producers (collision impossible) and obviously synthetic in logs.\n- Fail-closed reason string (\"reviewer_plan verdict file missing / unparseable AND reviewer exit_code=\u2026\") surfaces in the placeholder body; if a future regression test wants to pin the operator-facing wording, the `_verdict_diagnostics` dict on the runner is the structured surface to assert against.\n", - "ack_version": 3 - }, - "version": 3, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:27:55.530314+00:00", - "phase": "implement" - }, - { - "id": "0daf4e18-fca1-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:27:55.604944+00:00", - "phase": "implement" - }, - { - "id": "79a4b578-87cf-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:25:45.362708+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:27:58.622614+00:00", - "phase": "implement" - }, - { - "id": "d12b6292-18b0-42", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:01.412210+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:01.445344+00:00", - "phase": "implement" - }, - { - "id": "cf2aa718-9e8b-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:06.899611+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:06.931468+00:00", - "phase": "implement" - }, - { - "id": "fd3eb2b0-ca3c-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:06.983389+00:00", - "phase": "implement" - }, - { - "id": "76ca4533-d0d1-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_security for coder", - "body": "\nRe-reviewed slice-2 coder v3 \u2014 same commit SHA as v2 (d7b2a6152), no diff to re-check. Lens conclusion unchanged from v2: no security findings, the v2 improvements that addressed my v1 non-blocking observations stand (sentinel removed from concurrent path; reviewer_plan verdict JSON is now parsed and drives per-edge ACK/NACK with fail-closed defaults on missing-verdict + reviewer-spawn-failure). All eight security-lens checks (cross-file allowlist, handler-vs-validator, info-disclosure, uncommitted-artifact, credential-shim, secret leakage, OWASP top-10 cross-file, agent-supplied paths into read-only fs access) re-pass on the v3 proposal.\n\n### Non-blocking (carried forward from v2)\n- in_process.py:98 \u2014 `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` is now unreferenced (callers use `synthetic_commit_for(role.value)`); remove or keep-as-marker per reviewer_code's call.\n- _plan_phase.py:266-269 \u2014 `json.loads(verdict_path.read_text(...))` has no file-size cap; hardening-only observation if the harness gains untrusted-tenant semantics later.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/__init__.py" - ], - "reason": "\nRe-reviewed slice-2 coder v3 \u2014 same commit SHA as v2 (d7b2a6152), no diff to re-check. Lens conclusion unchanged from v2: no security findings, the v2 improvements that addressed my v1 non-blocking observations stand (sentinel removed from concurrent path; reviewer_plan verdict JSON is now parsed and drives per-edge ACK/NACK with fail-closed defaults on missing-verdict + reviewer-spawn-failure). All eight security-lens checks (cross-file allowlist, handler-vs-validator, info-disclosure, uncommitted-artifact, credential-shim, secret leakage, OWASP top-10 cross-file, agent-supplied paths into read-only fs access) re-pass on the v3 proposal.\n\n### Non-blocking (carried forward from v2)\n- in_process.py:98 \u2014 `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` is now unreferenced (callers use `synthetic_commit_for(role.value)`); remove or keep-as-marker per reviewer_code's call.\n- _plan_phase.py:266-269 \u2014 `json.loads(verdict_path.read_text(...))` has no file-size cap; hardening-only observation if the harness gains untrusted-tenant semantics later.\n", - "ack_version": 3 - }, - "version": 3, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:08.598336+00:00", - "phase": "implement" - }, - { - "id": "ad9a1736-0a77-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:08.643339+00:00", - "phase": "implement" - }, - { - "id": "e55123cb-1ef8-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:14.384123+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:14.436930+00:00", - "phase": "implement" - }, - { - "id": "843a6410-adfc-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:15.498820+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:15.553879+00:00", - "phase": "implement" - }, - { - "id": "f6359ba4-51b0-45", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from tester", - "body": "slice-2 tester: plan-phase BRC E2E integration test + R2-pass restrictions no-op regression guard for #2717 slice-2.\n\nTASK-2-4 \u2014 integration_tests/regression/test_inprocess_plan_brc.py\nPlan-phase in-process BRC end-to-end test covering all four AC bullets from contract task-2-4:\n1. boots `run_pipeline_in_process` against a deterministic pipeline id with harness-faked subagents (MagicMock substrate bundle mirroring `shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py`'s fake_bundle fixture \u2014 no real Anthropic / Claude Code spawn);\n2. advances past the refine HITL gate via `approve` \u2192 `approve_continue` send sequence;\n3. asserts the plan stage spawns the 3 producers (architect, task_planner, risk_analyst) + 1 reviewer (reviewer_plan) \u2014 observed via the fake spawner's `.call_args_list`;\n4. asserts BRC mechanics reach CONSENSUS_CONFIRMED on every producer edge (architect \u2192 reviewer_plan, task_planner \u2192 reviewer_plan, risk_analyst \u2192 reviewer_plan) by reading `_plan_tracker.evaluate()` \u2014 the in-process analogue of bus-side CONSENSUS_CONFIRMED messages, since the coder's TASK-2-1 implementation drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed` deterministically (the substrate's spawner is synchronous so spawn-completion IS the signal that the subagent proposed/reviewed);\n5. asserts the plan-HITL decision is yielded with `phase=\"plan\"`, `decision_type=\"phase_gate\"`, non-empty `id` / `question` / `options` (the 4-way approve/request_changes/change_approach/stop set on the success path; retry/abort on the failure path).\n\nAdversarial probing layered on top of the AC bullets:\n- plan stage MUST NOT run when the operator answers `stop` at the refine gate \u2014 a regression that fanned into plan on any non-continue answer would burn three unauthorised subagent spawns the operator did not approve (`test_plan_stage_does_not_run_when_operator_rejects_refine`);\n- plan stage MUST NOT spawn implement-phase roles \u2014 pins the negative invariant against a misrouted `_PHASE_ROLES[\"implement\"]` indexing bug (`test_plan_stage_does_not_spawn_implement_phase_roles`);\n- refiner is spawned exactly once \u2014 pins the single-refiner-spawn invariant against an off-by-one role-iteration that would re-spawn the refiner inside the plan stage (`test_plan_stage_does_not_invoke_refiner_a_second_time`);\n- every plan-phase spawn carries `EGG_PHASE=plan` in its env \u2014 pins the env-propagation contract so spawned subagents see the right phase (`test_plan_stage_carries_phase_env_var_to_producers`).\n\nThe 7 tests share an `isolated_pipeline_state` fixture that clears the module-level `PeerConsensusTracker` registry between tests so back-to-back runs against the same pipeline_id don't inherit confirmed state. `short_intervals` shrinks heartbeat / brc-review / bus-tick intervals to 0.05s so the suite runs in ~2s. `fake_home` redirects `$HOME` to a tmp dir so the active-role sentinel writes don't pollute the developer's actual home directory. All 7 pass against coder v2 (commit d7b2a6152).\n\nThe test is committed with graceful-skip behaviour for the case where TASK-2-1's plan-stage method is absent (the `_has_plan_stage()` helper checks for any of `_run_plan_phase` / `_run_plan` / `run_plan` / `_dispatch_plan` / `_plan_stage`); under scaffold-first guidance this kept the tester unblocked while the coder produced. With coder v2's `_run_plan_phase` landed the skip disappears and all 7 assertions run.\n\nTASK-2-6 \u2014 tests/sandbox/egg_agent_tools/test_restrictions_validator.py\nContingent on slice-1's R2 verdict = `pass` (recorded in the slice-1 BRC history `.egg-state/brc-history/2717-implement-slice-1.md`: the PreToolUse hook correctly resolves the child role under nested dispatch \u2014 parent=architect + child=tester writing `orchestrator/foo.py` \u2192 `decision=block` with a tester-naming reason; cross-role probe + in-role negative-control + EGG_AGENT_ROLE leak guard all 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).\" This file lands the no-op regression guard:\n\n- 3 in-allow-list response-shape tests (coder under orchestrator/, tester under tests/sandbox/egg_agent_tools/, documenter under docs/) \u2014 each pins the documented gateway-shape fields `{ok, role, path, can_write, reason, alternative_role}` exactly via a `frozenset` equality, so a slice-2 regression that silently extended the validator's response with new fields would surface here;\n- 2 cross-role denial tests (coder \u2192 tests/, tester \u2192 orchestrator/) \u2014 each pins `can_write=False`, `reason` references `shared/egg_restrictions/patterns.py`, `alternative_role` names the single producer role that can write the path (load-bearing for impasse-routing auto-delegation);\n- 1 negative-symbol invariant test \u2014 asserts `validate_write_target` (and peers) are NOT present on `sandbox/egg_agent_tools/handlers/restrictions.py`, since R2 = pass meant the cq-6 option-2 enforcement work should NOT have landed in slice-2;\n- 3 defensive probes \u2014 missing `path` raises HandlerError, unknown role raises HandlerError, list-shaped path returns per-path results with documented shapes.\n\nIf a future slice flips R2 to fail (the cq-3 deferral makes that possible per the plan), this test file will need a sibling that exercises the new validator's denial shape; the contingency is documented in the test docstring per task-2-6 AC. All 9 pass against the unchanged restrictions handler (no slice-2 source edits in `sandbox/egg_agent_tools/handlers/restrictions.py`, as the no-op verdict required).\n\nVerification (running checks directly because `make test` / `make lint` / `make security` cannot complete in this sandbox \u2014 the venv sync fails when uv tries to download pinned wheels because the wheels.pythonhosted.org TLS chain is `UnknownIssuer` inside the image; same env constraint slice-1 tester hit):\n- `ruff format --check .` \u2014 PASS (962 files already formatted) \u2713\n- `ruff check .` \u2014 PASS (all checks passed) \u2713\n- `mypy gateway shared sandbox --exclude 'gateway/tests/' --exclude 'shared/egg_contracts/tests/' --exclude 'shared/tests/'` \u2014 PASS (251 source files OK) \u2713\n- All 13 custom checks under `scripts/check-*.py` \u2014 PASS (each exits 0; a few harmless \"host-services directory not found\" warnings for repo-layout drift) \u2713\n- `bandit` \u2014 not installed in this sandbox (per `make security` SKIP branch). The Bandit invocation is part of the `security` configured check; cannot attest to it \u2713\n- Direct pytest on tester-authored files via `PYTHONPATH=shared:gateway:orchestrator:. pytest --import-mode=importlib`: 16/16 pass in ~2.2s \u2713\n\n`tests_execution_blocked` reason: the literal `make test` command cannot complete because (1) `uv sync` fails on flask / oauthlib wheel downloads (UnknownIssuer TLS chain to pypi.org); (2) without the venv, `make test` skips. Direct pytest on the tester-authored files passes 16/16 \u2014 see direct-pytest output above.", - "metadata": { - "payload": { - "summary": "slice-2 tester: plan-phase BRC E2E integration test + R2-pass restrictions no-op regression guard for #2717 slice-2.\n\nTASK-2-4 \u2014 integration_tests/regression/test_inprocess_plan_brc.py\nPlan-phase in-process BRC end-to-end test covering all four AC bullets from contract task-2-4:\n1. boots `run_pipeline_in_process` against a deterministic pipeline id with harness-faked subagents (MagicMock substrate bundle mirroring `shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py`'s fake_bundle fixture \u2014 no real Anthropic / Claude Code spawn);\n2. advances past the refine HITL gate via `approve` \u2192 `approve_continue` send sequence;\n3. asserts the plan stage spawns the 3 producers (architect, task_planner, risk_analyst) + 1 reviewer (reviewer_plan) \u2014 observed via the fake spawner's `.call_args_list`;\n4. asserts BRC mechanics reach CONSENSUS_CONFIRMED on every producer edge (architect \u2192 reviewer_plan, task_planner \u2192 reviewer_plan, risk_analyst \u2192 reviewer_plan) by reading `_plan_tracker.evaluate()` \u2014 the in-process analogue of bus-side CONSENSUS_CONFIRMED messages, since the coder's TASK-2-1 implementation drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed` deterministically (the substrate's spawner is synchronous so spawn-completion IS the signal that the subagent proposed/reviewed);\n5. asserts the plan-HITL decision is yielded with `phase=\"plan\"`, `decision_type=\"phase_gate\"`, non-empty `id` / `question` / `options` (the 4-way approve/request_changes/change_approach/stop set on the success path; retry/abort on the failure path).\n\nAdversarial probing layered on top of the AC bullets:\n- plan stage MUST NOT run when the operator answers `stop` at the refine gate \u2014 a regression that fanned into plan on any non-continue answer would burn three unauthorised subagent spawns the operator did not approve (`test_plan_stage_does_not_run_when_operator_rejects_refine`);\n- plan stage MUST NOT spawn implement-phase roles \u2014 pins the negative invariant against a misrouted `_PHASE_ROLES[\"implement\"]` indexing bug (`test_plan_stage_does_not_spawn_implement_phase_roles`);\n- refiner is spawned exactly once \u2014 pins the single-refiner-spawn invariant against an off-by-one role-iteration that would re-spawn the refiner inside the plan stage (`test_plan_stage_does_not_invoke_refiner_a_second_time`);\n- every plan-phase spawn carries `EGG_PHASE=plan` in its env \u2014 pins the env-propagation contract so spawned subagents see the right phase (`test_plan_stage_carries_phase_env_var_to_producers`).\n\nThe 7 tests share an `isolated_pipeline_state` fixture that clears the module-level `PeerConsensusTracker` registry between tests so back-to-back runs against the same pipeline_id don't inherit confirmed state. `short_intervals` shrinks heartbeat / brc-review / bus-tick intervals to 0.05s so the suite runs in ~2s. `fake_home` redirects `$HOME` to a tmp dir so the active-role sentinel writes don't pollute the developer's actual home directory. All 7 pass against coder v2 (commit d7b2a6152).\n\nThe test is committed with graceful-skip behaviour for the case where TASK-2-1's plan-stage method is absent (the `_has_plan_stage()` helper checks for any of `_run_plan_phase` / `_run_plan` / `run_plan` / `_dispatch_plan` / `_plan_stage`); under scaffold-first guidance this kept the tester unblocked while the coder produced. With coder v2's `_run_plan_phase` landed the skip disappears and all 7 assertions run.\n\nTASK-2-6 \u2014 tests/sandbox/egg_agent_tools/test_restrictions_validator.py\nContingent on slice-1's R2 verdict = `pass` (recorded in the slice-1 BRC history `.egg-state/brc-history/2717-implement-slice-1.md`: the PreToolUse hook correctly resolves the child role under nested dispatch \u2014 parent=architect + child=tester writing `orchestrator/foo.py` \u2192 `decision=block` with a tester-naming reason; cross-role probe + in-role negative-control + EGG_AGENT_ROLE leak guard all 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).\" This file lands the no-op regression guard:\n\n- 3 in-allow-list response-shape tests (coder under orchestrator/, tester under tests/sandbox/egg_agent_tools/, documenter under docs/) \u2014 each pins the documented gateway-shape fields `{ok, role, path, can_write, reason, alternative_role}` exactly via a `frozenset` equality, so a slice-2 regression that silently extended the validator's response with new fields would surface here;\n- 2 cross-role denial tests (coder \u2192 tests/, tester \u2192 orchestrator/) \u2014 each pins `can_write=False`, `reason` references `shared/egg_restrictions/patterns.py`, `alternative_role` names the single producer role that can write the path (load-bearing for impasse-routing auto-delegation);\n- 1 negative-symbol invariant test \u2014 asserts `validate_write_target` (and peers) are NOT present on `sandbox/egg_agent_tools/handlers/restrictions.py`, since R2 = pass meant the cq-6 option-2 enforcement work should NOT have landed in slice-2;\n- 3 defensive probes \u2014 missing `path` raises HandlerError, unknown role raises HandlerError, list-shaped path returns per-path results with documented shapes.\n\nIf a future slice flips R2 to fail (the cq-3 deferral makes that possible per the plan), this test file will need a sibling that exercises the new validator's denial shape; the contingency is documented in the test docstring per task-2-6 AC. All 9 pass against the unchanged restrictions handler (no slice-2 source edits in `sandbox/egg_agent_tools/handlers/restrictions.py`, as the no-op verdict required).\n\nVerification (running checks directly because `make test` / `make lint` / `make security` cannot complete in this sandbox \u2014 the venv sync fails when uv tries to download pinned wheels because the wheels.pythonhosted.org TLS chain is `UnknownIssuer` inside the image; same env constraint slice-1 tester hit):\n- `ruff format --check .` \u2014 PASS (962 files already formatted) \u2713\n- `ruff check .` \u2014 PASS (all checks passed) \u2713\n- `mypy gateway shared sandbox --exclude 'gateway/tests/' --exclude 'shared/egg_contracts/tests/' --exclude 'shared/tests/'` \u2014 PASS (251 source files OK) \u2713\n- All 13 custom checks under `scripts/check-*.py` \u2014 PASS (each exits 0; a few harmless \"host-services directory not found\" warnings for repo-layout drift) \u2713\n- `bandit` \u2014 not installed in this sandbox (per `make security` SKIP branch). The Bandit invocation is part of the `security` configured check; cannot attest to it \u2713\n- Direct pytest on tester-authored files via `PYTHONPATH=shared:gateway:orchestrator:. pytest --import-mode=importlib`: 16/16 pass in ~2.2s \u2713\n\n`tests_execution_blocked` reason: the literal `make test` command cannot complete because (1) `uv sync` fails on flask / oauthlib wheel downloads (UnknownIssuer TLS chain to pypi.org); (2) without the venv, `make test` skips. Direct pytest on the tester-authored files passes 16/16 \u2014 see direct-pytest output above.", - "attestation": { - "tests_execution_blocked": true, - "tests_execution_blocked_reason": "`make test` cannot complete in this sandbox: (1) uv venv sync fails when downloading pinned wheels (e.g. flask==3.1.2, oauthlib==3.3.1) because the wheels.pythonhosted.org TLS chain is `UnknownIssuer` inside the sandbox image \u2014 same network constraint the slice-1 tester hit. Direct pytest invocation via `PYTHONPATH=shared:gateway:orchestrator:. pytest integration_tests/regression/test_inprocess_plan_brc.py tests/sandbox/egg_agent_tools/test_restrictions_validator.py -v --rootdir=. --import-mode=importlib` passes 16/16 tester-authored tests in ~2.2s against coder v2 commit d7b2a6152. Direct ruff check + ruff format --check across the full repo pass; mypy on gateway/shared/sandbox passes; all 13 custom checks pass; bandit is not installed in this sandbox (SKIP).", - "checks_passed": [ - "lint" - ] - }, - "artifacts": [ - "integration_tests/regression/test_inprocess_plan_brc.py", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py" - ], - "risk_considered": "Three risk vectors considered:\n\n(1) Tester role boundary \u2014 both test files live under tester-allowed prefixes (`integration_tests/regression/`, `tests/sandbox/egg_agent_tools/`); no source-code edits. `mcp__sdlc__check_file_restriction` for both paths returned `can_write=True` for tester role.\n\n(2) Test reliability / flake \u2014 short heartbeat intervals (0.05s) are an order of magnitude faster than the production 5s but no `time.sleep` polling in the test body; the test relies on synchronous `gen.send(...)` flow which is deterministic. Tests run in ~2s under 100% reliable conditions (no real network, no real Claude Code spawn, no real subagent harness). `isolated_pipeline_state` fixture clears module-level tracker registry between tests so cross-test state leakage is impossible.\n\n(3) Coder-side architectural drift between v1 and v2 \u2014 coder v2 introduced architect-first-then-fanout ordering (architect synchronous, task_planner + risk_analyst concurrent via 2-worker pool) plus verdict-JSON ACK/NACK parsing replacing v1's exit-code heuristic. My tests assert on the *set* of spawned roles (not ordering), the *set* of confirmed-in-tracker roles (not propose timestamps), and the HITL gate's *shape* (not exact wording). All 7 plan-BRC tests + 9 restrictions tests pass unchanged against v2 \u2014 the abstraction level is right.", - "commit_sha": "b2be479f1", - "files_changed": [ - "integration_tests/regression/test_inprocess_plan_brc.py", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py" - ], - "tests_run": [ - "integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_spawns_three_producers_and_one_reviewer", - "integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_yields_hitl_decision_with_expected_fields", - "integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_reaches_consensus_confirmed_for_each_producer", - "integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_does_not_run_when_operator_rejects_refine", - "integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_does_not_spawn_implement_phase_roles", - "integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_does_not_invoke_refiner_a_second_time", - "integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_carries_phase_env_var_to_producers", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_coder_in_allow_list_response_shape_stable", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_tester_in_allow_list_response_shape_stable", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_documenter_in_allow_list_response_shape_stable", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_coder_cannot_write_tester_path_denial_shape_stable", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_tester_cannot_write_orchestrator_path_denial_shape_stable", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_no_new_validator_symbol_introduced_in_r2_pass_slice", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_missing_path_raises_handler_error", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_unknown_role_raises_handler_error", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_list_path_returns_per_path_results" - ], - "tasks_satisfied": [ - "task-2-4", - "task-2-6" - ] - }, - "version": 1, - "commit_sha": "b2be479f1", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:23.614203+00:00", - "phase": "implement" - }, - { - "id": "9155380a-8398-46", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "documenter", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:23.614936+00:00", - "phase": "implement" - }, - { - "id": "5ee66c6b-b2a9-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:23.673514+00:00", - "phase": "implement" - }, - { - "id": "adb01f2b-50b6-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:23.686706+00:00", - "phase": "implement" - }, - { - "id": "b29eef4b-eaf4-4f", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:33.563272+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:45.682828+00:00", - "phase": "implement" - }, - { - "id": "f64fdea5-b5f8-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by documenter", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:45.694679+00:00", - "phase": "implement" - }, - { - "id": "a31f7499-10bd-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_concurrency for coder", - "body": "\nConcurrency-lens re-review of coder v3 (commit d7b2a6152). I verified both v1 concurrency blockers are addressed and walked the new architect-first-then-fanout topology for new races. Re-checked tracker access ordering (still single-threaded on main thread inside `_run_plan_phase_inner`), bus / heartbeat / BRC-review background-thread interaction with the v2 phase flip, the verdict-JSON parsing path, and the per-role synthetic SHA. No remaining blocking concurrency findings.\n\n### Blocker resolution\n\n1. **C1 (sentinel race) \u2014 resolved.** `spawn_plan_producer` (`orchestrator/substrate/_plan_phase.py:438-488`) no longer writes the `$HOME/.claude/egg-active-role.json` sentinel \u2014 confirmed by the new docstring at lines 446-453 and by the absence of any `_write_active_role_sentinel` call inside the concurrent path. Per-spawn `EGG_AGENT_ROLE=role.value` is the load-bearing channel (line 461); the slice-1 R2 verdict (env propagates correctly under nested Agent-tool dispatch) justifies dropping the sentinel from the fan-out path, and the reviewer's solo spawn (`spawn_plan_reviewer`, line 537) still writes the sentinel since it never overlaps another spawn.\n\n2. **C2 (heartbeat phase) \u2014 resolved.** `_publish_heartbeat` (`orchestrator/substrate/in_process.py:381-414`) now reads `self._current_phase` (line 412) instead of hard-coding the string. The phase is initialised to `\"refine\"` at `__init__` (line 202) and flipped to `\"plan\"` at the top of `run_plan_phase` (`_plan_phase.py:67`). Any future stuck-phase-transition watchdog filtering heartbeats by `phase` will now see plan-phase liveness during the in-process plan stage.\n\n### Concurrency walk of the new topology\n\nThe v2/v3 redesign sequences architect synchronously first, then fans out task_planner + risk_analyst via `ThreadPoolExecutor(max_workers=2)`. I re-walked the concurrent leg:\n\n- **`spawn_plan_producer` (concurrent path)** \u2014 `bundle.worktrees.create(pipeline_id, role)` produces a per-role directory (`///`), so the two fan-out threads target disjoint paths; the worktree manager's `_lock` (`orchestrator/substrate/claude_code/worktree.py:82`) protects the in-memory `_tracked` dict. Each thread builds its own `spawn_env` dict (no shared mutable state), reads `runner.env` (a dict \u2014 concurrent dict reads are CPython-safe), and calls `bundle.spawner.spawn(...)` which fans the subprocess work out per-thread. No shared mutable state visible to me in this path.\n- **`_ensure_state_dirs` and `plan_producer_output_path`** \u2014 both use `Path.mkdir(parents=True, exist_ok=True)` which is idempotent under concurrent invocation; no race.\n- **Tracker access** \u2014 `_record_producer_propose`, `_apply_reviewer_verdicts`, the `tracker.handle_confirmed` loop, and `tracker.evaluate()` are all called from the main thread inside the `for fut in as_completed_fn(future_map)` body or after the executor's `with` block exits. `PeerConsensusTracker` is also self-RLock-protected (`orchestrator/peer_consensus.py:101` `threading.RLock()`), so the BRC re-review background thread's `re_review_tick` calls can interleave safely with the main thread's `handle_*` calls.\n- **Architect-first sequencing** \u2014 `bundle.spawner.spawn(...)` returns AFTER the subagent finishes (synchronous), so the architect's output JSON at `architect_output_path` is on disk before the fan-out threads start and is safe to read from the two downstream producers' subagent prompts.\n- **`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker` check-then-act (line 113-115)** \u2014 still not race-y today (background `_brc_review_loop` only calls `get_*`, never `create_*`), so my v1 non-blocking note still stands as a forward-looking caveat rather than an actionable finding.\n\n### Verified non-blockers from v1\n\n- \u2705 Per-role synthetic SHA via `synthetic_commit_for(role_name)` (`_plan_phase.py:644-656`) \u2014 sha1-hashed per role with the `ace1` prefix so each `ProposalPayload.commit_sha` remains distinguishable in the tracker even when the harness fake stubs the commit. Addresses my v1 NB #2.\n- \u2705 Tracker-guard rejections route through `log_tracker_warning(...)` (`_plan_phase.py:659-680`) instead of `except Exception: pass`. Addresses my v1 NB on silent guard rejections.\n\n### Non-blocking (residual)\n\n- **Stale sentinel during plan-phase producer spawns.** Once `_spawn_refiner` writes `$HOME/.claude/egg-active-role.json` with `role=\"refiner\"` and the generator advances to `run_plan_phase`, the sentinel keeps the stale `\"refiner\"` value through the architect spawn and the task_planner / risk_analyst fan-out. If env propagation ever fails (the R2 verdict says it does not, so this is defence-in-depth), nested children would fall back to refiner's allow-list rather than the producer's. Not a race anymore \u2014 just stale. Cheap mitigation: have the synchronous architect spawn refresh the sentinel to `\"architect\"` before its `bundle.spawner.spawn(...)` call (single-writer at that point); the fan-out path stays sentinel-free as designed. Defer if R2 stays green.\n- **`Worktree.create` still swallows `git worktree add` failures via `subprocess.run(check=False, timeout=30)`** (`orchestrator/substrate/claude_code/worktree.py:117-127`). My v1 NB #1 stands \u2014 concurrent `git worktree add` calls from the two fan-out threads can lose the parent repo's lock race; `target.mkdir(parents=True, exist_ok=True)` runs first so the spawner still gets a path. With v3's per-role synthetic SHA the downstream impact is smaller (each producer's ProposalPayload remains distinguishable even when `git rev-parse HEAD` falls back), but the silent-failure pattern is still load-bearing on the spawner being able to live in a non-git directory.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py", - "orchestrator/substrate/claude_code/worktree.py", - "orchestrator/substrate/claude_code/hook_entry.py", - "orchestrator/peer_consensus.py" - ], - "reason": "\nConcurrency-lens re-review of coder v3 (commit d7b2a6152). I verified both v1 concurrency blockers are addressed and walked the new architect-first-then-fanout topology for new races. Re-checked tracker access ordering (still single-threaded on main thread inside `_run_plan_phase_inner`), bus / heartbeat / BRC-review background-thread interaction with the v2 phase flip, the verdict-JSON parsing path, and the per-role synthetic SHA. No remaining blocking concurrency findings.\n\n### Blocker resolution\n\n1. **C1 (sentinel race) \u2014 resolved.** `spawn_plan_producer` (`orchestrator/substrate/_plan_phase.py:438-488`) no longer writes the `$HOME/.claude/egg-active-role.json` sentinel \u2014 confirmed by the new docstring at lines 446-453 and by the absence of any `_write_active_role_sentinel` call inside the concurrent path. Per-spawn `EGG_AGENT_ROLE=role.value` is the load-bearing channel (line 461); the slice-1 R2 verdict (env propagates correctly under nested Agent-tool dispatch) justifies dropping the sentinel from the fan-out path, and the reviewer's solo spawn (`spawn_plan_reviewer`, line 537) still writes the sentinel since it never overlaps another spawn.\n\n2. **C2 (heartbeat phase) \u2014 resolved.** `_publish_heartbeat` (`orchestrator/substrate/in_process.py:381-414`) now reads `self._current_phase` (line 412) instead of hard-coding the string. The phase is initialised to `\"refine\"` at `__init__` (line 202) and flipped to `\"plan\"` at the top of `run_plan_phase` (`_plan_phase.py:67`). Any future stuck-phase-transition watchdog filtering heartbeats by `phase` will now see plan-phase liveness during the in-process plan stage.\n\n### Concurrency walk of the new topology\n\nThe v2/v3 redesign sequences architect synchronously first, then fans out task_planner + risk_analyst via `ThreadPoolExecutor(max_workers=2)`. I re-walked the concurrent leg:\n\n- **`spawn_plan_producer` (concurrent path)** \u2014 `bundle.worktrees.create(pipeline_id, role)` produces a per-role directory (`///`), so the two fan-out threads target disjoint paths; the worktree manager's `_lock` (`orchestrator/substrate/claude_code/worktree.py:82`) protects the in-memory `_tracked` dict. Each thread builds its own `spawn_env` dict (no shared mutable state), reads `runner.env` (a dict \u2014 concurrent dict reads are CPython-safe), and calls `bundle.spawner.spawn(...)` which fans the subprocess work out per-thread. No shared mutable state visible to me in this path.\n- **`_ensure_state_dirs` and `plan_producer_output_path`** \u2014 both use `Path.mkdir(parents=True, exist_ok=True)` which is idempotent under concurrent invocation; no race.\n- **Tracker access** \u2014 `_record_producer_propose`, `_apply_reviewer_verdicts`, the `tracker.handle_confirmed` loop, and `tracker.evaluate()` are all called from the main thread inside the `for fut in as_completed_fn(future_map)` body or after the executor's `with` block exits. `PeerConsensusTracker` is also self-RLock-protected (`orchestrator/peer_consensus.py:101` `threading.RLock()`), so the BRC re-review background thread's `re_review_tick` calls can interleave safely with the main thread's `handle_*` calls.\n- **Architect-first sequencing** \u2014 `bundle.spawner.spawn(...)` returns AFTER the subagent finishes (synchronous), so the architect's output JSON at `architect_output_path` is on disk before the fan-out threads start and is safe to read from the two downstream producers' subagent prompts.\n- **`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker` check-then-act (line 113-115)** \u2014 still not race-y today (background `_brc_review_loop` only calls `get_*`, never `create_*`), so my v1 non-blocking note still stands as a forward-looking caveat rather than an actionable finding.\n\n### Verified non-blockers from v1\n\n- \u2705 Per-role synthetic SHA via `synthetic_commit_for(role_name)` (`_plan_phase.py:644-656`) \u2014 sha1-hashed per role with the `ace1` prefix so each `ProposalPayload.commit_sha` remains distinguishable in the tracker even when the harness fake stubs the commit. Addresses my v1 NB #2.\n- \u2705 Tracker-guard rejections route through `log_tracker_warning(...)` (`_plan_phase.py:659-680`) instead of `except Exception: pass`. Addresses my v1 NB on silent guard rejections.\n\n### Non-blocking (residual)\n\n- **Stale sentinel during plan-phase producer spawns.** Once `_spawn_refiner` writes `$HOME/.claude/egg-active-role.json` with `role=\"refiner\"` and the generator advances to `run_plan_phase`, the sentinel keeps the stale `\"refiner\"` value through the architect spawn and the task_planner / risk_analyst fan-out. If env propagation ever fails (the R2 verdict says it does not, so this is defence-in-depth), nested children would fall back to refiner's allow-list rather than the producer's. Not a race anymore \u2014 just stale. Cheap mitigation: have the synchronous architect spawn refresh the sentinel to `\"architect\"` before its `bundle.spawner.spawn(...)` call (single-writer at that point); the fan-out path stays sentinel-free as designed. Defer if R2 stays green.\n- **`Worktree.create` still swallows `git worktree add` failures via `subprocess.run(check=False, timeout=30)`** (`orchestrator/substrate/claude_code/worktree.py:117-127`). My v1 NB #1 stands \u2014 concurrent `git worktree add` calls from the two fan-out threads can lose the parent repo's lock race; `target.mkdir(parents=True, exist_ok=True)` runs first so the spawner still gets a path. With v3's per-role synthetic SHA the downstream impact is smaller (each producer's ProposalPayload remains distinguishable even when `git rev-parse HEAD` falls back), but the silent-failure pattern is still load-bearing on the spawner being able to live in a non-git directory.\n", - "ack_version": 3 - }, - "version": 3, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:45.734685+00:00", - "phase": "implement" - }, - { - "id": "5d7bb48b-09c2-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:45.812259+00:00", - "phase": "implement" - }, - { - "id": "4577f124-2f6a-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:50.817666+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:50.871497+00:00", - "phase": "implement" - }, - { - "id": "9d275321-48e0-49", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:50.981064+00:00", - "phase": "implement" - }, - { - "id": "5d6e48f9-99d0-48", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:51.460648+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:51.511301+00:00", - "phase": "implement" - }, - { - "id": "6b97c2aa-e845-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for coder", - "body": "\nRe-ACK at v3 (same commit d7b2a6152 as v2). Reviewed the coder's submission for TASK-2-1 / TASK-2-2 / TASK-2-5(no-op).\n\n### What changed v1 \u2192 current\n\n**H1 \u2014 architect-first then fanout (`_plan_phase.py:104-161`).** Plan stage now spawns `architect` synchronously first, records its `CONSENSUS_PROPOSE`, then fans out `task_planner` + `risk_analyst` via `ThreadPoolExecutor(max_workers=2)`. Architect's per-role output path is threaded into downstream producers via `EGG_ARCHITECT_OUTPUT_PATH` and the prompt's \"Architect handoff input: \u2026\". Matches the role-dependency declarations at `shared/egg_contracts/agent_roles.py:398/422` and the rubric language.\n\n**H2 \u2014 verdict-JSON parsing (`_plan_phase.py:251-371`).** `read_plan_reviewer_verdicts(runner)` parses `.egg-state/agent-outputs/-reviewer_plan-output.json` for a `per_producer` map. `_apply_reviewer_verdicts` drives per-edge ACK / NACK on the tracker. Semantics: present+ACK \u2192 ACK; present+NACK \u2192 NACK; absent + reviewer exit 0 \u2192 optimistic ACK with diagnostic; absent + reviewer exit non-zero \u2192 **fail-closed NACK**. Closes my v1 silent-NACK-loss concern.\n\n**C1 \u2014 sentinel removed from concurrent path (`_plan_phase.py:438-488`).** `spawn_plan_producer` no longer writes the sentinel. Per-spawn `EGG_AGENT_ROLE` is the primary channel. `spawn_plan_reviewer` retains the write (solo dispatch). The transition window where the sentinel says \"refiner\" during plan-producer spawns is acceptable: refiner + the three plan producers share the `.egg-state/{drafts,agent-outputs}/` allow-list.\n\n**C2 \u2014 phase HEARTBEAT (`in_process.py:198-203, 380-412`).** New `self._current_phase` field, flipped to \"plan\" at the top of `run_plan_phase`. `_publish_heartbeat` reads it.\n\n**T1 \u2014 ruff format applied;** `EGG_PRODUCER_ARTIFACT_PATHS` dropped in favor of per-role `EGG__OUTPUT_PATH` vars for the reviewer.\n\n**Per-role synthetic SHA (`_plan_phase.py:644-656`).** `synthetic_commit_for(role_name)` returns `f\"ace1{sha1(role_name)[:3]}\"` \u2014 three concurrent producers now have distinguishable `commit_sha` values. The `ace1` prefix keeps the value obviously synthetic.\n\n**Tracker-guard warning logging (`_plan_phase.py:659-680`).** Bare excepts replaced with `logger.warning(...)` carrying verb + role + pipeline_id + exception.\n\n**Module decomposition.** Plan-phase body extracted to `orchestrator/substrate/_plan_phase.py` (680 lines); class methods on `_InProcessOrchestrator` stay as thin delegates so public surface and the tester's v1 test method-names are preserved.\n\n### File-by-file analysis\n\n**orchestrator/substrate/_plan_phase.py** (new, 680 lines) \u2014 Single linear flow: `run_plan_phase` (lifts the phase string) \u2192 `_run_plan_phase_inner` (architect-first, fanout, reviewer, verdict-parse, confirm). Module-level functions accept lazily-imported primitives via keyword args (`bundle_factory`, `executor_factory`, `as_completed_fn`, etc.) so tests can inject deterministic substitutes. Spawn helpers build per-role env vars (`EGG_PRODUCER_OUTPUT_PATH`, `EGG_REVIEWER_VERDICT_PATH`, optional `EGG_ARCHITECT_OUTPUT_PATH`, per-role `EGG__OUTPUT_PATH` for the reviewer). Tracker-guard wrappers log on failure rather than swallowing. `format_plan_placeholder` renders per-producer + reviewer diagnostics + verdict-parsing status + BRC eval snapshot.\n\n**orchestrator/substrate/in_process.py** \u2014 Delegates plan-phase methods to `_plan_phase`. `_current_phase` field added at `__init__` and read in `_publish_heartbeat`. `_SYNTHETIC_PLAN_COMMIT` constant retained for refiner/fallback callers with a beefed-up docstring; plan-phase code uses per-role `synthetic_commit_for` instead.\n\n**orchestrator/substrate/__init__.py** \u2014 TASK-2-2 loader expansion: `_RUBRIC_LANDED_ROLES` now includes `architect` / `task_planner` / `risk_analyst` / `reviewer_plan`. The \"missing on disk\" diagnostic mentions both TASK-1-4 (slice-1) and TASK-2-3 (slice-2) so operators hit by the error get a slice-specific pointer. Implement-team roles still raise `ValueError` with a slice-3 pointer via `_ROLE_RUBRIC_SLICES`.\n\n### Non-blocking\n\n- **`_plan_phase.py:412-435 (_record_reviewer_nack)`** \u2014 A NACK with `reason=\"\"` in the verdict JSON hits `ReviewPayload.validate_nack_has_reason` (`attestation_schemas.py:241-243`) and raises `ValueError`. The defensive `except Exception` catches via `log_tracker_warning` but the tracker doesn't record the NACK \u2014 the producer ends up \"not ACKed, not NACKed, just unconfirmed\", surfaced only as `blocking_agents` at the HITL gate. Adding a defensive default reason (e.g. \"reviewer NACKed without a stated reason\") in `_apply_reviewer_verdicts` would convert this corner case into a recorded NACK with a sentinel reason. Worth a small follow-up.\n\n- **`_plan_phase.py:67 + in_process.py:202`** \u2014 `_current_phase` is set forward-only. A future \"back to refine\" path would need an explicit reset. Add a comment noting \"set-once monotonic\" for now.\n\n- **`_plan_phase.py:113-115`** \u2014 The \"reuse existing tracker\" branch (`tracker = get_tracker(...); if tracker is None: ...`) is dead code today. A future slice that registers a tracker with the refine graph would have this branch reuse the wrong graph. Either guard with a graph-match check or always-create.\n\n- **`_plan_phase.py:644-656 (synthetic_commit_for)`** \u2014 4096-value space (3 hex chars after `ace1`). For four roles collision is negligible, but if the rubric set grows consider lifting to `[:6]` (24-bit space).\n\n- **`in_process.py:91-98 (_SYNTHETIC_PLAN_COMMIT)`** \u2014 Constant retained for \"refiner / fallback callers and as a structural marker\" per the new docstring. The plan-phase code no longer uses it. Worth grepping for external callers in a follow-up; if none, the constant can be inlined or removed.\n\nNo security, correctness, or robustness issues introduced by this revision. All my v1 blocking-class observations (architect ordering, silent verdict-NACK loss, sentinel race, silent tracker except, synthetic commit collision) are addressed. Coder ACKed at v3.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/__init__.py", - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py" - ], - "reason": "\nRe-ACK at v3 (same commit d7b2a6152 as v2). Reviewed the coder's submission for TASK-2-1 / TASK-2-2 / TASK-2-5(no-op).\n\n### What changed v1 \u2192 current\n\n**H1 \u2014 architect-first then fanout (`_plan_phase.py:104-161`).** Plan stage now spawns `architect` synchronously first, records its `CONSENSUS_PROPOSE`, then fans out `task_planner` + `risk_analyst` via `ThreadPoolExecutor(max_workers=2)`. Architect's per-role output path is threaded into downstream producers via `EGG_ARCHITECT_OUTPUT_PATH` and the prompt's \"Architect handoff input: \u2026\". Matches the role-dependency declarations at `shared/egg_contracts/agent_roles.py:398/422` and the rubric language.\n\n**H2 \u2014 verdict-JSON parsing (`_plan_phase.py:251-371`).** `read_plan_reviewer_verdicts(runner)` parses `.egg-state/agent-outputs/-reviewer_plan-output.json` for a `per_producer` map. `_apply_reviewer_verdicts` drives per-edge ACK / NACK on the tracker. Semantics: present+ACK \u2192 ACK; present+NACK \u2192 NACK; absent + reviewer exit 0 \u2192 optimistic ACK with diagnostic; absent + reviewer exit non-zero \u2192 **fail-closed NACK**. Closes my v1 silent-NACK-loss concern.\n\n**C1 \u2014 sentinel removed from concurrent path (`_plan_phase.py:438-488`).** `spawn_plan_producer` no longer writes the sentinel. Per-spawn `EGG_AGENT_ROLE` is the primary channel. `spawn_plan_reviewer` retains the write (solo dispatch). The transition window where the sentinel says \"refiner\" during plan-producer spawns is acceptable: refiner + the three plan producers share the `.egg-state/{drafts,agent-outputs}/` allow-list.\n\n**C2 \u2014 phase HEARTBEAT (`in_process.py:198-203, 380-412`).** New `self._current_phase` field, flipped to \"plan\" at the top of `run_plan_phase`. `_publish_heartbeat` reads it.\n\n**T1 \u2014 ruff format applied;** `EGG_PRODUCER_ARTIFACT_PATHS` dropped in favor of per-role `EGG__OUTPUT_PATH` vars for the reviewer.\n\n**Per-role synthetic SHA (`_plan_phase.py:644-656`).** `synthetic_commit_for(role_name)` returns `f\"ace1{sha1(role_name)[:3]}\"` \u2014 three concurrent producers now have distinguishable `commit_sha` values. The `ace1` prefix keeps the value obviously synthetic.\n\n**Tracker-guard warning logging (`_plan_phase.py:659-680`).** Bare excepts replaced with `logger.warning(...)` carrying verb + role + pipeline_id + exception.\n\n**Module decomposition.** Plan-phase body extracted to `orchestrator/substrate/_plan_phase.py` (680 lines); class methods on `_InProcessOrchestrator` stay as thin delegates so public surface and the tester's v1 test method-names are preserved.\n\n### File-by-file analysis\n\n**orchestrator/substrate/_plan_phase.py** (new, 680 lines) \u2014 Single linear flow: `run_plan_phase` (lifts the phase string) \u2192 `_run_plan_phase_inner` (architect-first, fanout, reviewer, verdict-parse, confirm). Module-level functions accept lazily-imported primitives via keyword args (`bundle_factory`, `executor_factory`, `as_completed_fn`, etc.) so tests can inject deterministic substitutes. Spawn helpers build per-role env vars (`EGG_PRODUCER_OUTPUT_PATH`, `EGG_REVIEWER_VERDICT_PATH`, optional `EGG_ARCHITECT_OUTPUT_PATH`, per-role `EGG__OUTPUT_PATH` for the reviewer). Tracker-guard wrappers log on failure rather than swallowing. `format_plan_placeholder` renders per-producer + reviewer diagnostics + verdict-parsing status + BRC eval snapshot.\n\n**orchestrator/substrate/in_process.py** \u2014 Delegates plan-phase methods to `_plan_phase`. `_current_phase` field added at `__init__` and read in `_publish_heartbeat`. `_SYNTHETIC_PLAN_COMMIT` constant retained for refiner/fallback callers with a beefed-up docstring; plan-phase code uses per-role `synthetic_commit_for` instead.\n\n**orchestrator/substrate/__init__.py** \u2014 TASK-2-2 loader expansion: `_RUBRIC_LANDED_ROLES` now includes `architect` / `task_planner` / `risk_analyst` / `reviewer_plan`. The \"missing on disk\" diagnostic mentions both TASK-1-4 (slice-1) and TASK-2-3 (slice-2) so operators hit by the error get a slice-specific pointer. Implement-team roles still raise `ValueError` with a slice-3 pointer via `_ROLE_RUBRIC_SLICES`.\n\n### Non-blocking\n\n- **`_plan_phase.py:412-435 (_record_reviewer_nack)`** \u2014 A NACK with `reason=\"\"` in the verdict JSON hits `ReviewPayload.validate_nack_has_reason` (`attestation_schemas.py:241-243`) and raises `ValueError`. The defensive `except Exception` catches via `log_tracker_warning` but the tracker doesn't record the NACK \u2014 the producer ends up \"not ACKed, not NACKed, just unconfirmed\", surfaced only as `blocking_agents` at the HITL gate. Adding a defensive default reason (e.g. \"reviewer NACKed without a stated reason\") in `_apply_reviewer_verdicts` would convert this corner case into a recorded NACK with a sentinel reason. Worth a small follow-up.\n\n- **`_plan_phase.py:67 + in_process.py:202`** \u2014 `_current_phase` is set forward-only. A future \"back to refine\" path would need an explicit reset. Add a comment noting \"set-once monotonic\" for now.\n\n- **`_plan_phase.py:113-115`** \u2014 The \"reuse existing tracker\" branch (`tracker = get_tracker(...); if tracker is None: ...`) is dead code today. A future slice that registers a tracker with the refine graph would have this branch reuse the wrong graph. Either guard with a graph-match check or always-create.\n\n- **`_plan_phase.py:644-656 (synthetic_commit_for)`** \u2014 4096-value space (3 hex chars after `ace1`). For four roles collision is negligible, but if the rubric set grows consider lifting to `[:6]` (24-bit space).\n\n- **`in_process.py:91-98 (_SYNTHETIC_PLAN_COMMIT)`** \u2014 Constant retained for \"refiner / fallback callers and as a structural marker\" per the new docstring. The plan-phase code no longer uses it. Worth grepping for external callers in a follow-up; if none, the constant can be inlined or removed.\n\nNo security, correctness, or robustness issues introduced by this revision. All my v1 blocking-class observations (architect ordering, silent verdict-NACK loss, sentinel race, silent tracker except, synthetic commit collision) are addressed. Coder ACKed at v3.\n", - "ack_version": 3 - }, - "version": 3, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:53.198057+00:00", - "phase": "implement" - }, - { - "id": "a8cc7a23-11eb-44", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:53.225449+00:00", - "phase": "implement" - }, - { - "id": "94fd2bff-1722-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:54.903874+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:54.951533+00:00", - "phase": "implement" - }, - { - "id": "d3cfdccf-ef58-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:55.047702+00:00", - "phase": "implement" - }, - { - "id": "68a22d6f-aab7-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:57.622075+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:57.679552+00:00", - "phase": "implement" - }, - { - "id": "7d7f45d4-c705-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:57.794157+00:00", - "phase": "implement" - }, - { - "id": "ef791558-5209-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:58.727732+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:28:58.770027+00:00", - "phase": "implement" - }, - { - "id": "0001609e-2b5d-4f", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:33.563272+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:29:45.731824+00:00", - "phase": "implement" - }, - { - "id": "7c9cadd4-bd96-4f", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:29:58.487845+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:29:58.549945+00:00", - "phase": "implement" - }, - { - "id": "27531476-30d0-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:28:58.727732+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:29:58.799499+00:00", - "phase": "implement" - }, - { - "id": "820b1293-0b54-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_security for tester", - "body": "\nReviewed the slice-2 tester proposal at commit b2be479f1 through the security lens. Read both new test files end-to-end. No security findings; the diff is in fact security-positive (pins several defensive invariants the security lens cares about).\n\n### Lens checks\n\n1. **Cross-file allowlist mismatch (\u00a71):** N/A \u2014 the tests *verify* the allowlist semantics rather than introducing new ones. `test_coder_in_allow_list_response_shape_stable` / `test_tester_in_allow_list_response_shape_stable` / `test_documenter_in_allow_list_response_shape_stable` pin that the documented per-role file-write patterns continue to hold; the cross-role denial cases (`test_coder_cannot_write_tester_path_denial_shape_stable`, `test_tester_cannot_write_orchestrator_path_denial_shape_stable`) pin that the denial reason references `shared/egg_restrictions/patterns.py` and that `alternative_role` names the correct producer. Net positive for the lens \u2014 these are regression guards on the allowlist surface.\n\n2. **Handler-vs-validator path mismatch (\u00a72):** N/A; tests do not introduce new public endpoints.\n\n3. **Information-disclosure / authz bypass (\u00a73):** No real credentials, tokens, or identity material is embedded in the test fixtures. `_make_fake_bundle` returns `MagicMock(... commit_sha=\"0\" * 40, stdout=\"ok\", ...)` \u2014 clearly synthetic. The `fake_home` fixture (test_inprocess_plan_brc.py:127-137) explicitly relocates `$HOME` under `tmp_path / \"home\"` so the generator's `_write_active_role_sentinel` write does NOT pollute the developer's actual home directory \u2014 this is the right hygiene posture for a test that exercises sentinel I/O.\n\n4. **Uncommitted-artifact / symlink mismatch (\u00a74):** Both new files are committed at b2be479f1 with non-zero size; no Dockerfile / packaging-manifest / symlink references.\n\n5. **Credential-shim modifications (\u00a75):** N/A \u2014 no changes under `sandbox/scripts/`.\n\n6. **Secret leakage (\u00a76):** Test paths and fixtures are all synthetic / orchestrator-derived; no environment dumps, no logs of actual credentials, no real network endpoints.\n\n7. **Cross-file OWASP top-10 (\u00a77):** N/A.\n\n8. **Agent-supplied paths into read-only fs access (\u00a78):** Tests use `pytest`'s `tmp_path` for all real filesystem I/O. The `sys.path.insert(0, str(ROOT / \"sandbox\"))` at test_restrictions_validator.py:78-79 derives `ROOT` from `Path(__file__).resolve().parents[3]` \u2014 anchored to the test file's own location, not agent-supplied. Hardcoded paths passed to `restrictions.check_file_restriction({\"path\": \"orchestrator/foo.py\"})` are evaluated against the regex pattern registry, never opened on disk. No new fs-read surface.\n\n### Security-positive defensive invariants this diff pins\n\nThe following tests are themselves the *kind of regression guards* the security lens wants to see:\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:624-675** \u2014 `test_plan_stage_does_not_spawn_implement_phase_roles` pins the negative invariant that the plan stage cannot accidentally invoke `coder` / `tester` / `documenter` / `reviewer_*` from the implement team. A phase-dispatch lookup that mis-indexed `_PHASE_ROLES[\"plan\"]` (e.g. off-by-one onto `\"implement\"`) would burn six concurrent implement-team spawns the operator never approved \u2014 exactly the kind of HITL-bypass shape the security lens cares about. Pinning it as a regression test is the right shape.\n- **integration_tests/regression/test_inprocess_plan_brc.py:557-615** \u2014 `test_plan_stage_does_not_run_when_operator_rejects_refine` pins the HITL-gate invariant: a `stop` answer at the refine gate MUST NOT advance into the plan stage's three concurrent spawns. Same shape as above \u2014 regression here would be a HITL-bypass.\n- **integration_tests/regression/test_inprocess_plan_brc.py:735-797** \u2014 `test_plan_stage_carries_phase_env_var_to_producers` pins that `EGG_PHASE=plan` reaches every plan-phase spawn. The PreToolUse hook reads `EGG_AGENT_ROLE` for allow-list enforcement; a missing/wrong `EGG_PHASE` would not directly bypass that, but the env-propagation contract is a load-bearing piece of the substrate's trust-boundary story and pinning it pre-empts a class of \"spawned subagent saw the wrong stage\" bugs.\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259** \u2014 `test_no_new_validator_symbol_introduced_in_r2_pass_slice` enforces the R2-pass \"no-op\" contract: TASK-2-5 said \"if R2 = pass, this task is a no-op\". The test asserts no `validate_write_target` (or peer) symbol was added to `sandbox/egg_agent_tools/handlers/restrictions.py`, so an unintended slip of agent-side enforcement onto the R2-pass path would surface here. This is exactly the cross-file invariant the lens cares about \u2014 \"did the slice land scope it wasn't supposed to\".\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:268-297** \u2014 `test_missing_path_raises_handler_error` and `test_unknown_role_raises_handler_error` pin the validator's defensive surface (no permissive fall-through on missing-path / unknown-role inputs). Loose role validation here would be a real security concern (an agent could pass an unknown role and get an `ok: True, can_write: True` answer); pinning the existing HandlerError behaviour is correct.\n\n### Non-blocking\n- **integration_tests/regression/test_inprocess_plan_brc.py:155-158** \u2014 `isolated_pipeline_state` walks three candidate private-registry names (`_TRACKERS`, `_PEER_CONSENSUS_TRACKERS`, `_tracker_registry`) and clears whichever exists. Pragmatic, but mildly brittle: if `orchestrator/peer_consensus.py` renames its registry to a fourth name in a future slice, the fixture silently no-ops and back-to-back tests would share confirmed state. Not a security concern; the failure mode is a flaky test, not a security hole. A `pytest.fail(\"no peer-consensus tracker registry found\")` if NONE of the candidates resolves would surface the rename earlier. Defer to reviewer_code.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_inprocess_plan_brc.py", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py" - ], - "reason": "\nReviewed the slice-2 tester proposal at commit b2be479f1 through the security lens. Read both new test files end-to-end. No security findings; the diff is in fact security-positive (pins several defensive invariants the security lens cares about).\n\n### Lens checks\n\n1. **Cross-file allowlist mismatch (\u00a71):** N/A \u2014 the tests *verify* the allowlist semantics rather than introducing new ones. `test_coder_in_allow_list_response_shape_stable` / `test_tester_in_allow_list_response_shape_stable` / `test_documenter_in_allow_list_response_shape_stable` pin that the documented per-role file-write patterns continue to hold; the cross-role denial cases (`test_coder_cannot_write_tester_path_denial_shape_stable`, `test_tester_cannot_write_orchestrator_path_denial_shape_stable`) pin that the denial reason references `shared/egg_restrictions/patterns.py` and that `alternative_role` names the correct producer. Net positive for the lens \u2014 these are regression guards on the allowlist surface.\n\n2. **Handler-vs-validator path mismatch (\u00a72):** N/A; tests do not introduce new public endpoints.\n\n3. **Information-disclosure / authz bypass (\u00a73):** No real credentials, tokens, or identity material is embedded in the test fixtures. `_make_fake_bundle` returns `MagicMock(... commit_sha=\"0\" * 40, stdout=\"ok\", ...)` \u2014 clearly synthetic. The `fake_home` fixture (test_inprocess_plan_brc.py:127-137) explicitly relocates `$HOME` under `tmp_path / \"home\"` so the generator's `_write_active_role_sentinel` write does NOT pollute the developer's actual home directory \u2014 this is the right hygiene posture for a test that exercises sentinel I/O.\n\n4. **Uncommitted-artifact / symlink mismatch (\u00a74):** Both new files are committed at b2be479f1 with non-zero size; no Dockerfile / packaging-manifest / symlink references.\n\n5. **Credential-shim modifications (\u00a75):** N/A \u2014 no changes under `sandbox/scripts/`.\n\n6. **Secret leakage (\u00a76):** Test paths and fixtures are all synthetic / orchestrator-derived; no environment dumps, no logs of actual credentials, no real network endpoints.\n\n7. **Cross-file OWASP top-10 (\u00a77):** N/A.\n\n8. **Agent-supplied paths into read-only fs access (\u00a78):** Tests use `pytest`'s `tmp_path` for all real filesystem I/O. The `sys.path.insert(0, str(ROOT / \"sandbox\"))` at test_restrictions_validator.py:78-79 derives `ROOT` from `Path(__file__).resolve().parents[3]` \u2014 anchored to the test file's own location, not agent-supplied. Hardcoded paths passed to `restrictions.check_file_restriction({\"path\": \"orchestrator/foo.py\"})` are evaluated against the regex pattern registry, never opened on disk. No new fs-read surface.\n\n### Security-positive defensive invariants this diff pins\n\nThe following tests are themselves the *kind of regression guards* the security lens wants to see:\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:624-675** \u2014 `test_plan_stage_does_not_spawn_implement_phase_roles` pins the negative invariant that the plan stage cannot accidentally invoke `coder` / `tester` / `documenter` / `reviewer_*` from the implement team. A phase-dispatch lookup that mis-indexed `_PHASE_ROLES[\"plan\"]` (e.g. off-by-one onto `\"implement\"`) would burn six concurrent implement-team spawns the operator never approved \u2014 exactly the kind of HITL-bypass shape the security lens cares about. Pinning it as a regression test is the right shape.\n- **integration_tests/regression/test_inprocess_plan_brc.py:557-615** \u2014 `test_plan_stage_does_not_run_when_operator_rejects_refine` pins the HITL-gate invariant: a `stop` answer at the refine gate MUST NOT advance into the plan stage's three concurrent spawns. Same shape as above \u2014 regression here would be a HITL-bypass.\n- **integration_tests/regression/test_inprocess_plan_brc.py:735-797** \u2014 `test_plan_stage_carries_phase_env_var_to_producers` pins that `EGG_PHASE=plan` reaches every plan-phase spawn. The PreToolUse hook reads `EGG_AGENT_ROLE` for allow-list enforcement; a missing/wrong `EGG_PHASE` would not directly bypass that, but the env-propagation contract is a load-bearing piece of the substrate's trust-boundary story and pinning it pre-empts a class of \"spawned subagent saw the wrong stage\" bugs.\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259** \u2014 `test_no_new_validator_symbol_introduced_in_r2_pass_slice` enforces the R2-pass \"no-op\" contract: TASK-2-5 said \"if R2 = pass, this task is a no-op\". The test asserts no `validate_write_target` (or peer) symbol was added to `sandbox/egg_agent_tools/handlers/restrictions.py`, so an unintended slip of agent-side enforcement onto the R2-pass path would surface here. This is exactly the cross-file invariant the lens cares about \u2014 \"did the slice land scope it wasn't supposed to\".\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:268-297** \u2014 `test_missing_path_raises_handler_error` and `test_unknown_role_raises_handler_error` pin the validator's defensive surface (no permissive fall-through on missing-path / unknown-role inputs). Loose role validation here would be a real security concern (an agent could pass an unknown role and get an `ok: True, can_write: True` answer); pinning the existing HandlerError behaviour is correct.\n\n### Non-blocking\n- **integration_tests/regression/test_inprocess_plan_brc.py:155-158** \u2014 `isolated_pipeline_state` walks three candidate private-registry names (`_TRACKERS`, `_PEER_CONSENSUS_TRACKERS`, `_tracker_registry`) and clears whichever exists. Pragmatic, but mildly brittle: if `orchestrator/peer_consensus.py` renames its registry to a fourth name in a future slice, the fixture silently no-ops and back-to-back tests would share confirmed state. Not a security concern; the failure mode is a flaky test, not a security hole. A `pytest.fail(\"no peer-consensus tracker registry found\")` if NONE of the candidates resolves would surface the rename earlier. Defer to reviewer_code.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:04.365333+00:00", - "phase": "implement" - }, - { - "id": "ea09d460-a921-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:04.403259+00:00", - "phase": "implement" - }, - { - "id": "2d6afb59-669e-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_security", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:10.310458+00:00", - "phase": "implement" - }, - { - "id": "bd30afbe-10b9-4b", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:13.381894+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:13.410646+00:00", - "phase": "implement" - }, - { - "id": "96704551-f978-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:14.901780+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:14.956065+00:00", - "phase": "implement" - }, - { - "id": "8b101016-0310-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "coder", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_code_holistic for coder", - "body": "\nHolistic re-review of coder slice-2 v3 (commit d7b2a6152 \u2014 same SHA as v2, re-propose only). v1 blockers H1 (architect-first ordering) and H2 (reviewer verdict no longer exit-code-only) are both addressed correctly \u2014 `_plan_phase.run_plan_phase` spawns `architect` synchronously first, then fans out `task_planner` + `risk_analyst` through a `ThreadPoolExecutor(max_workers=2)` with `EGG_ARCHITECT_OUTPUT_PATH` plumbed into both spawn_env and prompt_text (matches the role-dependency declarations in `shared/egg_contracts/agent_roles.py:398/422`); `read_plan_reviewer_verdicts` + `_apply_reviewer_verdicts` now drive per-edge ACK / NACK on the tracker from a parsed verdict JSON; `_current_phase` flips to \"plan\" so heartbeats carry the right phase across the transition; tracker-guard rejections log via `log_tracker_warning` instead of bare `except`; placeholder body now renders reviewer_plan diagnostics. Good. One new blocker surfaced by pass 2 / pass 4 review of the H2 fix:\n\n### Blocking\n\n1. **Pass 2 (doc \u2194 code symmetry) + Pass 4 (silent fallback) \u2014 reviewer_plan verdict JSON schema mismatch between the rubric and the parser; a rubric-following reviewer's NACK is silently transformed into an ACK.** Producer: `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md` \"Verdict JSON shape\" section (lines 57\u201380) tells the reviewer to write **one** JSON object to `verdict_path` with top-level keys `verdict` (ACK | NACK), `summary`, `analysis`, `suggestions`, `artifact_references`, `feedback`, `timestamp` \u2014 no `per_producer` wrapper, no per-edge schema. Consumer: `orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts` (lines 251\u2013286) reads `blob.get(\"per_producer\") or {}` and ignores everything outside that wrapper. The two schemas are incompatible.\n\n Walking the failure end-to-end: a real reviewer_plan agent follows the rubric, writes `{\"verdict\": \"NACK\", \"feedback\": \"task_planner role_assignments puts a coder task in tests/\", \u2026}`, exits 0. The parser opens the file, finds no `per_producer` key, returns `(verdict_path, {})` (lines 270\u2013272 \u2014 `per_producer = blob.get(\"per_producer\") or {}` followed by an empty-dict early-return when `normalised` stays empty). `_apply_reviewer_verdicts` (lines 308\u2013349) then computes `verdict_file_present = bool({}) = False`, `fail_closed = False and reviewer_exit_code != 0 = False`, so the per-producer loop falls into the `if entry is None: \u2026 _record_reviewer_ack(\u2026, reason=\"reviewer_plan ACK (synthetic): verdict file absent AND reviewer exit_code=0 \u2014 in-process synchronous-spawn-as-signal default per #2717 slice-2\")` branch for **every** producer. The operator sees `is_complete=True` at the plan-HITL gate, approves a plan the reviewer actually rejected, and the reviewer's NACK feedback is buried in a JSON file nobody parses.\n\n The \"optimistic-ACK when verdict file is missing\" fallback (intended for harness-faked runs) silently catches the \"verdict file *present but wrong schema*\" case because `read_plan_reviewer_verdicts` collapses both into the same empty-dict return. This is the canonical silent-fallback shape: the safety floor (BRC advances) is preserved, the operator-facing signal (reviewer's verdict) is masked. The diagnostic surface in the placeholder body (`- per_producer: \u2014 reviewer did not write a parseable verdict JSON`) is only rendered on the placeholder code-path (`if not plan_artifact_path.exists():`), so a harness that *does* land `-plan.md` swallows it entirely \u2014 and even when rendered, \"did not write a parseable verdict JSON\" is wrong: the JSON parses fine, it just doesn't carry the key the orchestrator expects.\n\n Compounding evidence that the schema mismatch is real, not a coder typo: `grep -rn 'per_producer\\b' orchestrator/ shared/` shows the key exists ONLY in `_plan_phase.py`. No rubric, no k3s code path, no existing test fixture produces a `per_producer` JSON. The v2 commit body cites a \"Mixed verdict: with a per_producer verdict JSON {architect:ACK, task_planner:NACK, risk_analyst:ACK}\" smoke test \u2014 i.e. the coder hand-crafted the per_producer shape for the smoke and confirmed the parser walks it correctly, but never confirmed that a rubric-following reviewer would emit that shape. The same lens reviewer who flagged H2 in v1 finds H3 in v2/v3 because the v1 NACK only said \"parse the reviewer's verdict\"; it did not specify the schema, and the documenter's rubric (already landed in commit 7122ca2d1) defines an incompatible one.\n\n Pick one of the three resolutions; all three are acceptable from a holistic-coherence standpoint, but the doc and code must agree before slice-2 lands:\n - **(a)** Dispatch the reviewer N times (once per producer edge) inside `_plan_phase.run_plan_phase` \u2014 one `spawn_plan_reviewer(producer=X)` call per producer, each writing its own single-verdict JSON at `.egg-state/agent-outputs/-reviewer_plan--output.json`. Aggregate by reading the N files. This matches the rubric and the k3s substrate's per-edge routing.\n - **(b)** Keep the single reviewer dispatch and update the rubric (documenter coordination) to specify a `per_producer` wrapper schema: `{\"per_producer\": {\"architect\": {\"verdict\": \u2026, \"reason\": \u2026, \u2026}, \"task_planner\": {\u2026}, \"risk_analyst\": {\u2026}}}`. The \"Verdict JSON shape\" block in `reviewer_plan.md` and the note at line 102 (\"each edge's verdict is namespaced by the producer role in the artifact handoff\") both need to be updated to reference the wrapper. The reviewer's rubric currently has no way to produce per-edge verdicts inside a single JSON file \u2014 it has to be told.\n - **(c)** Treat the single top-level `verdict` field as a whole-plan verdict and broadcast it to all three tracker edges. The orchestrator parses the rubric-documented schema; an `\"ACK\"` ACKs every producer edge, a `\"NACK\"` NACKs every producer edge with the single `feedback` blob attached to all three. Lowest-effort but loses per-edge granularity \u2014 the rubric's \"ACK / NACK each producer independently\" promise becomes \"all or nothing\".\n\n I do NOT have a preference between (a) / (b) / (c) \u2014 the coder + documenter should pick the one that lines up with the k3s substrate's behaviour (whichever path matches `orchestrator/routes/pipelines.py`'s plan-phase reviewer driver is the right one for the doc's \"Your eight review criteria, your evidence discipline, and your verdict JSON shape are unchanged\" promise). The blocking issue is that today, doc and code disagree, and the resulting silent-fallback transforms operator-meaningful NACKs into ACKs.\n\n### Non-blocking\n\n- **Pass 2 \u2014 same documenter mismatches surfaced in v1 still pending** (SKILL.md \"Plan HITL gate\" lists `approve` instead of `approve_continue`; `request_changes` / `change_approach` described as loop-back actions while the code returns the artifact path). Not in coder scope; carries to the documenter NACK chain.\n- **Optimistic-ACK fallback misnamed in the placeholder.** When `read_plan_reviewer_verdicts` returns `(verdict_path, {})` with a non-None `verdict_path` (file present but no parseable per_producer), the placeholder still emits `- per_producer: \u2014 reviewer did not write a parseable verdict JSON`. After (a) / (b) / (c) above land, this diagnostic line will be accurate; today it is misleading because the JSON *was* parseable, it just used the documented top-level schema.\n- **`spawn_plan_reviewer` still writes the active-role sentinel (line 537) even though the reviewer's spawn never overlaps with another spawn** \u2014 that's fine, but the docstring's \"the reviewer dispatches solo (no concurrent role-holder)\" claim hinges on call-site ordering that's not enforced anywhere. A one-line comment naming the call-site invariant (\"`run_plan_phase_inner` calls this after the producer fan-out has joined\") would harden it.\n\nIf you take resolution (a) \u2014 separate reviewer invocations \u2014 the v2/v3 spawn flow already plumbs per-role producer paths into the single reviewer dispatch; pulling that into a per-edge loop is a small delta. If you take (b) or (c), the documenter has to land a corresponding rubric update; coordinate with them on the same NACK cycle so the doc and code reach v4 together.\n", - "metadata": { - "payload": { - "reason": "\nHolistic re-review of coder slice-2 v3 (commit d7b2a6152 \u2014 same SHA as v2, re-propose only). v1 blockers H1 (architect-first ordering) and H2 (reviewer verdict no longer exit-code-only) are both addressed correctly \u2014 `_plan_phase.run_plan_phase` spawns `architect` synchronously first, then fans out `task_planner` + `risk_analyst` through a `ThreadPoolExecutor(max_workers=2)` with `EGG_ARCHITECT_OUTPUT_PATH` plumbed into both spawn_env and prompt_text (matches the role-dependency declarations in `shared/egg_contracts/agent_roles.py:398/422`); `read_plan_reviewer_verdicts` + `_apply_reviewer_verdicts` now drive per-edge ACK / NACK on the tracker from a parsed verdict JSON; `_current_phase` flips to \"plan\" so heartbeats carry the right phase across the transition; tracker-guard rejections log via `log_tracker_warning` instead of bare `except`; placeholder body now renders reviewer_plan diagnostics. Good. One new blocker surfaced by pass 2 / pass 4 review of the H2 fix:\n\n### Blocking\n\n1. **Pass 2 (doc \u2194 code symmetry) + Pass 4 (silent fallback) \u2014 reviewer_plan verdict JSON schema mismatch between the rubric and the parser; a rubric-following reviewer's NACK is silently transformed into an ACK.** Producer: `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md` \"Verdict JSON shape\" section (lines 57\u201380) tells the reviewer to write **one** JSON object to `verdict_path` with top-level keys `verdict` (ACK | NACK), `summary`, `analysis`, `suggestions`, `artifact_references`, `feedback`, `timestamp` \u2014 no `per_producer` wrapper, no per-edge schema. Consumer: `orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts` (lines 251\u2013286) reads `blob.get(\"per_producer\") or {}` and ignores everything outside that wrapper. The two schemas are incompatible.\n\n Walking the failure end-to-end: a real reviewer_plan agent follows the rubric, writes `{\"verdict\": \"NACK\", \"feedback\": \"task_planner role_assignments puts a coder task in tests/\", \u2026}`, exits 0. The parser opens the file, finds no `per_producer` key, returns `(verdict_path, {})` (lines 270\u2013272 \u2014 `per_producer = blob.get(\"per_producer\") or {}` followed by an empty-dict early-return when `normalised` stays empty). `_apply_reviewer_verdicts` (lines 308\u2013349) then computes `verdict_file_present = bool({}) = False`, `fail_closed = False and reviewer_exit_code != 0 = False`, so the per-producer loop falls into the `if entry is None: \u2026 _record_reviewer_ack(\u2026, reason=\"reviewer_plan ACK (synthetic): verdict file absent AND reviewer exit_code=0 \u2014 in-process synchronous-spawn-as-signal default per #2717 slice-2\")` branch for **every** producer. The operator sees `is_complete=True` at the plan-HITL gate, approves a plan the reviewer actually rejected, and the reviewer's NACK feedback is buried in a JSON file nobody parses.\n\n The \"optimistic-ACK when verdict file is missing\" fallback (intended for harness-faked runs) silently catches the \"verdict file *present but wrong schema*\" case because `read_plan_reviewer_verdicts` collapses both into the same empty-dict return. This is the canonical silent-fallback shape: the safety floor (BRC advances) is preserved, the operator-facing signal (reviewer's verdict) is masked. The diagnostic surface in the placeholder body (`- per_producer: \u2014 reviewer did not write a parseable verdict JSON`) is only rendered on the placeholder code-path (`if not plan_artifact_path.exists():`), so a harness that *does* land `-plan.md` swallows it entirely \u2014 and even when rendered, \"did not write a parseable verdict JSON\" is wrong: the JSON parses fine, it just doesn't carry the key the orchestrator expects.\n\n Compounding evidence that the schema mismatch is real, not a coder typo: `grep -rn 'per_producer\\b' orchestrator/ shared/` shows the key exists ONLY in `_plan_phase.py`. No rubric, no k3s code path, no existing test fixture produces a `per_producer` JSON. The v2 commit body cites a \"Mixed verdict: with a per_producer verdict JSON {architect:ACK, task_planner:NACK, risk_analyst:ACK}\" smoke test \u2014 i.e. the coder hand-crafted the per_producer shape for the smoke and confirmed the parser walks it correctly, but never confirmed that a rubric-following reviewer would emit that shape. The same lens reviewer who flagged H2 in v1 finds H3 in v2/v3 because the v1 NACK only said \"parse the reviewer's verdict\"; it did not specify the schema, and the documenter's rubric (already landed in commit 7122ca2d1) defines an incompatible one.\n\n Pick one of the three resolutions; all three are acceptable from a holistic-coherence standpoint, but the doc and code must agree before slice-2 lands:\n - **(a)** Dispatch the reviewer N times (once per producer edge) inside `_plan_phase.run_plan_phase` \u2014 one `spawn_plan_reviewer(producer=X)` call per producer, each writing its own single-verdict JSON at `.egg-state/agent-outputs/-reviewer_plan--output.json`. Aggregate by reading the N files. This matches the rubric and the k3s substrate's per-edge routing.\n - **(b)** Keep the single reviewer dispatch and update the rubric (documenter coordination) to specify a `per_producer` wrapper schema: `{\"per_producer\": {\"architect\": {\"verdict\": \u2026, \"reason\": \u2026, \u2026}, \"task_planner\": {\u2026}, \"risk_analyst\": {\u2026}}}`. The \"Verdict JSON shape\" block in `reviewer_plan.md` and the note at line 102 (\"each edge's verdict is namespaced by the producer role in the artifact handoff\") both need to be updated to reference the wrapper. The reviewer's rubric currently has no way to produce per-edge verdicts inside a single JSON file \u2014 it has to be told.\n - **(c)** Treat the single top-level `verdict` field as a whole-plan verdict and broadcast it to all three tracker edges. The orchestrator parses the rubric-documented schema; an `\"ACK\"` ACKs every producer edge, a `\"NACK\"` NACKs every producer edge with the single `feedback` blob attached to all three. Lowest-effort but loses per-edge granularity \u2014 the rubric's \"ACK / NACK each producer independently\" promise becomes \"all or nothing\".\n\n I do NOT have a preference between (a) / (b) / (c) \u2014 the coder + documenter should pick the one that lines up with the k3s substrate's behaviour (whichever path matches `orchestrator/routes/pipelines.py`'s plan-phase reviewer driver is the right one for the doc's \"Your eight review criteria, your evidence discipline, and your verdict JSON shape are unchanged\" promise). The blocking issue is that today, doc and code disagree, and the resulting silent-fallback transforms operator-meaningful NACKs into ACKs.\n\n### Non-blocking\n\n- **Pass 2 \u2014 same documenter mismatches surfaced in v1 still pending** (SKILL.md \"Plan HITL gate\" lists `approve` instead of `approve_continue`; `request_changes` / `change_approach` described as loop-back actions while the code returns the artifact path). Not in coder scope; carries to the documenter NACK chain.\n- **Optimistic-ACK fallback misnamed in the placeholder.** When `read_plan_reviewer_verdicts` returns `(verdict_path, {})` with a non-None `verdict_path` (file present but no parseable per_producer), the placeholder still emits `- per_producer: \u2014 reviewer did not write a parseable verdict JSON`. After (a) / (b) / (c) above land, this diagnostic line will be accurate; today it is misleading because the JSON *was* parseable, it just used the documented top-level schema.\n- **`spawn_plan_reviewer` still writes the active-role sentinel (line 537) even though the reviewer's spawn never overlaps with another spawn** \u2014 that's fine, but the docstring's \"the reviewer dispatches solo (no concurrent role-holder)\" claim hinges on call-site ordering that's not enforced anywhere. A one-line comment naming the call-site invariant (\"`run_plan_phase_inner` calls this after the producer fan-out has joined\") would harden it.\n\nIf you take resolution (a) \u2014 separate reviewer invocations \u2014 the v2/v3 spawn flow already plumbs per-role producer paths into the single reviewer dispatch; pulling that into a per-edge loop is a small delta. If you take (b) or (c), the documenter has to land a corresponding rubric update; coordinate with them on the same NACK cycle so the doc and code reach v4 together.\n", - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md" - ], - "nack_version": 3 - }, - "reason": "\nHolistic re-review of coder slice-2 v3 (commit d7b2a6152 \u2014 same SHA as v2, re-propose only). v1 blockers H1 (architect-first ordering) and H2 (reviewer verdict no longer exit-code-only) are both addressed correctly \u2014 `_plan_phase.run_plan_phase` spawns `architect` synchronously first, then fans out `task_planner` + `risk_analyst` through a `ThreadPoolExecutor(max_workers=2)` with `EGG_ARCHITECT_OUTPUT_PATH` plumbed into both spawn_env and prompt_text (matches the role-dependency declarations in `shared/egg_contracts/agent_roles.py:398/422`); `read_plan_reviewer_verdicts` + `_apply_reviewer_verdicts` now drive per-edge ACK / NACK on the tracker from a parsed verdict JSON; `_current_phase` flips to \"plan\" so heartbeats carry the right phase across the transition; tracker-guard rejections log via `log_tracker_warning` instead of bare `except`; placeholder body now renders reviewer_plan diagnostics. Good. One new blocker surfaced by pass 2 / pass 4 review of the H2 fix:\n\n### Blocking\n\n1. **Pass 2 (doc \u2194 code symmetry) + Pass 4 (silent fallback) \u2014 reviewer_plan verdict JSON schema mismatch between the rubric and the parser; a rubric-following reviewer's NACK is silently transformed into an ACK.** Producer: `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md` \"Verdict JSON shape\" section (lines 57\u201380) tells the reviewer to write **one** JSON object to `verdict_path` with top-level keys `verdict` (ACK | NACK), `summary`, `analysis`, `suggestions`, `artifact_references`, `feedback`, `timestamp` \u2014 no `per_producer` wrapper, no per-edge schema. Consumer: `orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts` (lines 251\u2013286) reads `blob.get(\"per_producer\") or {}` and ignores everything outside that wrapper. The two schemas are incompatible.\n\n Walking the failure end-to-end: a real reviewer_plan agent follows the rubric, writes `{\"verdict\": \"NACK\", \"feedback\": \"task_planner role_assignments puts a coder task in tests/\", \u2026}`, exits 0. The parser opens the file, finds no `per_producer` key, returns `(verdict_path, {})` (lines 270\u2013272 \u2014 `per_producer = blob.get(\"per_producer\") or {}` followed by an empty-dict early-return when `normalised` stays empty). `_apply_reviewer_verdicts` (lines 308\u2013349) then computes `verdict_file_present = bool({}) = False`, `fail_closed = False and reviewer_exit_code != 0 = False`, so the per-producer loop falls into the `if entry is None: \u2026 _record_reviewer_ack(\u2026, reason=\"reviewer_plan ACK (synthetic): verdict file absent AND reviewer exit_code=0 \u2014 in-process synchronous-spawn-as-signal default per #2717 slice-2\")` branch for **every** producer. The operator sees `is_complete=True` at the plan-HITL gate, approves a plan the reviewer actually rejected, and the reviewer's NACK feedback is buried in a JSON file nobody parses.\n\n The \"optimistic-ACK when verdict file is missing\" fallback (intended for harness-faked runs) silently catches the \"verdict file *present but wrong schema*\" case because `read_plan_reviewer_verdicts` collapses both into the same empty-dict return. This is the canonical silent-fallback shape: the safety floor (BRC advances) is preserved, the operator-facing signal (reviewer's verdict) is masked. The diagnostic surface in the placeholder body (`- per_producer: \u2014 reviewer did not write a parseable verdict JSON`) is only rendered on the placeholder code-path (`if not plan_artifact_path.exists():`), so a harness that *does* land `-plan.md` swallows it entirely \u2014 and even when rendered, \"did not write a parseable verdict JSON\" is wrong: the JSON parses fine, it just doesn't carry the key the orchestrator expects.\n\n Compounding evidence that the schema mismatch is real, not a coder typo: `grep -rn 'per_producer\\b' orchestrator/ shared/` shows the key exists ONLY in `_plan_phase.py`. No rubric, no k3s code path, no existing test fixture produces a `per_producer` JSON. The v2 commit body cites a \"Mixed verdict: with a per_producer verdict JSON {architect:ACK, task_planner:NACK, risk_analyst:ACK}\" smoke test \u2014 i.e. the coder hand-crafted the per_producer shape for the smoke and confirmed the parser walks it correctly, but never confirmed that a rubric-following reviewer would emit that shape. The same lens reviewer who flagged H2 in v1 finds H3 in v2/v3 because the v1 NACK only said \"parse the reviewer's verdict\"; it did not specify the schema, and the documenter's rubric (already landed in commit 7122ca2d1) defines an incompatible one.\n\n Pick one of the three resolutions; all three are acceptable from a holistic-coherence standpoint, but the doc and code must agree before slice-2 lands:\n - **(a)** Dispatch the reviewer N times (once per producer edge) inside `_plan_phase.run_plan_phase` \u2014 one `spawn_plan_reviewer(producer=X)` call per producer, each writing its own single-verdict JSON at `.egg-state/agent-outputs/-reviewer_plan--output.json`. Aggregate by reading the N files. This matches the rubric and the k3s substrate's per-edge routing.\n - **(b)** Keep the single reviewer dispatch and update the rubric (documenter coordination) to specify a `per_producer` wrapper schema: `{\"per_producer\": {\"architect\": {\"verdict\": \u2026, \"reason\": \u2026, \u2026}, \"task_planner\": {\u2026}, \"risk_analyst\": {\u2026}}}`. The \"Verdict JSON shape\" block in `reviewer_plan.md` and the note at line 102 (\"each edge's verdict is namespaced by the producer role in the artifact handoff\") both need to be updated to reference the wrapper. The reviewer's rubric currently has no way to produce per-edge verdicts inside a single JSON file \u2014 it has to be told.\n - **(c)** Treat the single top-level `verdict` field as a whole-plan verdict and broadcast it to all three tracker edges. The orchestrator parses the rubric-documented schema; an `\"ACK\"` ACKs every producer edge, a `\"NACK\"` NACKs every producer edge with the single `feedback` blob attached to all three. Lowest-effort but loses per-edge granularity \u2014 the rubric's \"ACK / NACK each producer independently\" promise becomes \"all or nothing\".\n\n I do NOT have a preference between (a) / (b) / (c) \u2014 the coder + documenter should pick the one that lines up with the k3s substrate's behaviour (whichever path matches `orchestrator/routes/pipelines.py`'s plan-phase reviewer driver is the right one for the doc's \"Your eight review criteria, your evidence discipline, and your verdict JSON shape are unchanged\" promise). The blocking issue is that today, doc and code disagree, and the resulting silent-fallback transforms operator-meaningful NACKs into ACKs.\n\n### Non-blocking\n\n- **Pass 2 \u2014 same documenter mismatches surfaced in v1 still pending** (SKILL.md \"Plan HITL gate\" lists `approve` instead of `approve_continue`; `request_changes` / `change_approach` described as loop-back actions while the code returns the artifact path). Not in coder scope; carries to the documenter NACK chain.\n- **Optimistic-ACK fallback misnamed in the placeholder.** When `read_plan_reviewer_verdicts` returns `(verdict_path, {})` with a non-None `verdict_path` (file present but no parseable per_producer), the placeholder still emits `- per_producer: \u2014 reviewer did not write a parseable verdict JSON`. After (a) / (b) / (c) above land, this diagnostic line will be accurate; today it is misleading because the JSON *was* parseable, it just used the documented top-level schema.\n- **`spawn_plan_reviewer` still writes the active-role sentinel (line 537) even though the reviewer's spawn never overlaps with another spawn** \u2014 that's fine, but the docstring's \"the reviewer dispatches solo (no concurrent role-holder)\" claim hinges on call-site ordering that's not enforced anywhere. A one-line comment naming the call-site invariant (\"`run_plan_phase_inner` calls this after the producer fan-out has joined\") would harden it.\n\nIf you take resolution (a) \u2014 separate reviewer invocations \u2014 the v2/v3 spawn flow already plumbs per-role producer paths into the single reviewer dispatch; pulling that into a per-edge loop is a small delta. If you take (b) or (c), the documenter has to land a corresponding rubric update; coordinate with them on the same NACK cycle so the doc and code reach v4 together.\n", - "revision_count": 3, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:26.031891+00:00", - "phase": "implement" - }, - { - "id": "b1342a50-b07d-4f", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:26.104499+00:00", - "phase": "implement" - }, - { - "id": "460b1a94-91c7-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:36.963716+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:37.011210+00:00", - "phase": "implement" - }, - { - "id": "4f9a0af0-5a2a-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:37.262609+00:00", - "phase": "implement" - }, - { - "id": "37ca114e-19da-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_contract", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:40.007941+00:00", - "phase": "implement" - }, - { - "id": "20edd34c-af96-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:40.083834+00:00", - "phase": "implement" - }, - { - "id": "25a72e7b-0832-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_concurrency for tester", - "body": "\nConcurrency-lens review of tester v1 (commit b2be479f1). The proposal adds two test files only \u2014 no production code changes \u2014 so the concurrency surface is the test harness itself. I walked the fixture-shared state, the per-test isolation, and the executor / mock interaction to confirm the tests don't introduce concurrency hazards or mask the v3 coder's concurrent code path.\n\n### Verified\n\n- **Shared-state isolation between tests.** The `isolated_pipeline_state` fixture (`test_inprocess_plan_brc.py:230-248`) clears the module-level `_TRACKERS` / `_PEER_CONSENSUS_TRACKERS` / `_tracker_registry` registry between tests so back-to-back runs don't inherit a previously-confirmed tracker. `fake_home` (`:216-227`) isolates `$HOME` per test so sentinel writes can't bleed across tests. Each test uses a unique pipeline id (`pipeline-plan-brc-spawns`, `pipeline-plan-brc-hitl`, etc.) so two tests in the same worker cannot collide on the registry key even before the explicit `.clear()`. Under `pytest-xdist` the worker-process boundary takes care of the rest.\n\n- **Exercising the real concurrent path.** `_make_fake_bundle` returns a `MagicMock`-backed spawner whose `.spawn(...)` is recorded into `.call_args_list`. `_run_plan_phase_inner` (coder v3, `orchestrator/substrate/_plan_phase.py:138-150`) submits task_planner + risk_analyst to a real `ThreadPoolExecutor(max_workers=2)`, so both threads concurrently invoke `bundle.spawner.spawn(...)`. CPython's GIL makes `list.append` (the recording mechanism inside `_mock_call`) atomic, so the spawn-call ledger does not tear under the concurrent invocation; the `_EXPECTED_PRODUCERS - plan_spawned` assertion at `test_inprocess_plan_brc.py:428-433` therefore reliably catches a missing-role regression even when the executor fans out.\n\n- **Background-thread teardown.** Every test's `finally` block calls `gen.close()` then `time.sleep(0.2)` / `0.3` to let the heartbeat / brc-review / bus-tick daemons unwind. The generator's `_shutdown_background_threads` already joins with a 2.0 s timeout, so the sleep is a courtesy flush \u2014 no leaked daemon thread can poison the next test's tracker because the registry is `.clear()`'d before the next test starts. The `short_intervals` fixture shrinks the tick intervals to 0.05 s so the tests don't pad to multi-second runtimes waiting for the timeouts.\n\n- **No retry storms or off-protocol bus emissions.** The fake bundle binds `bundle.bus = InProcessMessageBus()` (`:280-282`) rather than a `MagicMock`, so the heartbeat publisher's `bus.add_message(...)` lands on a real bus and does not silently swallow type errors \u2014 the v3 heartbeat-phase fix (`phase=self._current_phase`) flows through correctly under this fixture.\n\n- **Adversarial probes target the right concurrency-adjacent invariants.** `test_plan_stage_does_not_run_when_operator_rejects_refine` (`:647-705`) pins the safety invariant that an unauthorised concurrent producer dispatch cannot fire on a non-`approve_continue` answer. `test_plan_stage_carries_phase_env_var_to_producers` (`:825-887`) confirms `EGG_PHASE=plan` is on every plan-producer spawn env \u2014 important because v3 dropped the concurrent-path sentinel write and now relies on env propagation as the load-bearing role-routing channel.\n\n### Drift note (non-blocking)\n\nThe commit body and module docstring describe the implementation as \"3 producers concurrent\" (`:118-125`). Coder v3 actually runs **architect synchronously first** and then fans out task_planner + risk_analyst (2-way concurrent) \u2014 the producer count is still 3 but only 2 are concurrent. The test's spawn-set assertion (`_EXPECTED_PRODUCERS - plan_spawned`) doesn't pin ordering and still passes against v3's architect-first sequencing, but the docstring text would mislead a future maintainer reading the comments. Drop a sentence noting \"architect runs synchronously first, then task_planner + risk_analyst fan out via ThreadPoolExecutor(max_workers=2)\" so the test's narrative matches the v3 implementation.\n\n### Non-blocking\n\n- `bundle.worktrees.create = MagicMock(return_value=tmp_path / \"wt\")` returns the **same** worktree path for every role (`:269`). Under v3's concurrent path, three producers receive the same worktree; the spawner mock doesn't care, so this is benign for the test but it does mean the test won't catch a regression where the production code accidentally reused a single worktree path across roles. Cheap fix: have the fake `worktrees.create` produce `tmp_path / role.value` so the per-role isolation is observable in the recorded spawn envs.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_inprocess_plan_brc.py", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py" - ], - "reason": "\nConcurrency-lens review of tester v1 (commit b2be479f1). The proposal adds two test files only \u2014 no production code changes \u2014 so the concurrency surface is the test harness itself. I walked the fixture-shared state, the per-test isolation, and the executor / mock interaction to confirm the tests don't introduce concurrency hazards or mask the v3 coder's concurrent code path.\n\n### Verified\n\n- **Shared-state isolation between tests.** The `isolated_pipeline_state` fixture (`test_inprocess_plan_brc.py:230-248`) clears the module-level `_TRACKERS` / `_PEER_CONSENSUS_TRACKERS` / `_tracker_registry` registry between tests so back-to-back runs don't inherit a previously-confirmed tracker. `fake_home` (`:216-227`) isolates `$HOME` per test so sentinel writes can't bleed across tests. Each test uses a unique pipeline id (`pipeline-plan-brc-spawns`, `pipeline-plan-brc-hitl`, etc.) so two tests in the same worker cannot collide on the registry key even before the explicit `.clear()`. Under `pytest-xdist` the worker-process boundary takes care of the rest.\n\n- **Exercising the real concurrent path.** `_make_fake_bundle` returns a `MagicMock`-backed spawner whose `.spawn(...)` is recorded into `.call_args_list`. `_run_plan_phase_inner` (coder v3, `orchestrator/substrate/_plan_phase.py:138-150`) submits task_planner + risk_analyst to a real `ThreadPoolExecutor(max_workers=2)`, so both threads concurrently invoke `bundle.spawner.spawn(...)`. CPython's GIL makes `list.append` (the recording mechanism inside `_mock_call`) atomic, so the spawn-call ledger does not tear under the concurrent invocation; the `_EXPECTED_PRODUCERS - plan_spawned` assertion at `test_inprocess_plan_brc.py:428-433` therefore reliably catches a missing-role regression even when the executor fans out.\n\n- **Background-thread teardown.** Every test's `finally` block calls `gen.close()` then `time.sleep(0.2)` / `0.3` to let the heartbeat / brc-review / bus-tick daemons unwind. The generator's `_shutdown_background_threads` already joins with a 2.0 s timeout, so the sleep is a courtesy flush \u2014 no leaked daemon thread can poison the next test's tracker because the registry is `.clear()`'d before the next test starts. The `short_intervals` fixture shrinks the tick intervals to 0.05 s so the tests don't pad to multi-second runtimes waiting for the timeouts.\n\n- **No retry storms or off-protocol bus emissions.** The fake bundle binds `bundle.bus = InProcessMessageBus()` (`:280-282`) rather than a `MagicMock`, so the heartbeat publisher's `bus.add_message(...)` lands on a real bus and does not silently swallow type errors \u2014 the v3 heartbeat-phase fix (`phase=self._current_phase`) flows through correctly under this fixture.\n\n- **Adversarial probes target the right concurrency-adjacent invariants.** `test_plan_stage_does_not_run_when_operator_rejects_refine` (`:647-705`) pins the safety invariant that an unauthorised concurrent producer dispatch cannot fire on a non-`approve_continue` answer. `test_plan_stage_carries_phase_env_var_to_producers` (`:825-887`) confirms `EGG_PHASE=plan` is on every plan-producer spawn env \u2014 important because v3 dropped the concurrent-path sentinel write and now relies on env propagation as the load-bearing role-routing channel.\n\n### Drift note (non-blocking)\n\nThe commit body and module docstring describe the implementation as \"3 producers concurrent\" (`:118-125`). Coder v3 actually runs **architect synchronously first** and then fans out task_planner + risk_analyst (2-way concurrent) \u2014 the producer count is still 3 but only 2 are concurrent. The test's spawn-set assertion (`_EXPECTED_PRODUCERS - plan_spawned`) doesn't pin ordering and still passes against v3's architect-first sequencing, but the docstring text would mislead a future maintainer reading the comments. Drop a sentence noting \"architect runs synchronously first, then task_planner + risk_analyst fan out via ThreadPoolExecutor(max_workers=2)\" so the test's narrative matches the v3 implementation.\n\n### Non-blocking\n\n- `bundle.worktrees.create = MagicMock(return_value=tmp_path / \"wt\")` returns the **same** worktree path for every role (`:269`). Under v3's concurrent path, three producers receive the same worktree; the spawner mock doesn't care, so this is benign for the test but it does mean the test won't catch a regression where the production code accidentally reused a single worktree path across roles. Cheap fix: have the fake `worktrees.create` produce `tmp_path / role.value` so the per-role isolation is observable in the recorded spawn envs.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:44.125212+00:00", - "phase": "implement" - }, - { - "id": "6ebf8772-cb93-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:44.217101+00:00", - "phase": "implement" - }, - { - "id": "d3974fba-b79f-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:44.730210+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:44.764411+00:00", - "phase": "implement" - }, - { - "id": "a806ee31-ad63-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_concurrency", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:48.882823+00:00", - "phase": "implement" - }, - { - "id": "9df38d4b-a241-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:48.949883+00:00", - "phase": "implement" - }, - { - "id": "773ec3dd-259a-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:49.767497+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:49.818665+00:00", - "phase": "implement" - }, - { - "id": "d1d0a5c9-7806-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:49.902334+00:00", - "phase": "implement" - }, - { - "id": "e4834915-f712-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:50.751767+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:50.787244+00:00", - "phase": "implement" - }, - { - "id": "1b9beda5-b692-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:53.905702+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:53.951588+00:00", - "phase": "implement" - }, - { - "id": "9cc09dd6-39ae-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:57.063180+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:57.213175+00:00", - "phase": "implement" - }, - { - "id": "fe4f54ae-521a-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:57.727546+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:57.775699+00:00", - "phase": "implement" - }, - { - "id": "32058ab5-b810-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:57.854015+00:00", - "phase": "implement" - }, - { - "id": "a25f65df-4dac-48", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:29:58.487845+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:30:58.602160+00:00", - "phase": "implement" - }, - { - "id": "c9b36dad-01ea-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:31:06.346909+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:31:06.374333+00:00", - "phase": "implement" - }, - { - "id": "6baab241-e0af-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:50.751767+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:31:50.901950+00:00", - "phase": "implement" - }, - { - "id": "8a76447f-adfa-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:53.905702+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:31:54.005945+00:00", - "phase": "implement" - }, - { - "id": "28206c32-956a-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:30:57.063180+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:31:57.413161+00:00", - "phase": "implement" - }, - { - "id": "a36f90a5-984f-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:29:58.487845+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:31:58.676945+00:00", - "phase": "implement" - }, - { - "id": "3873d0e8-aafb-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:31:06.346909+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:06.443722+00:00", - "phase": "implement" - }, - { - "id": "d18543b1-3f07-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for tester", - "body": "\nReviewed the tester's two new test files for TASK-2-4 (plan-phase BRC E2E integration test) and TASK-2-6 (R2-pass no-op regression guard for the restrictions handler). Verified test structure, assertion semantics, fixture isolation, and alignment with the coder's v2 implementation surface.\n\n### File-by-file analysis\n\n**integration_tests/regression/test_inprocess_plan_brc.py** (new, 797 lines) \u2014 Seven test cases covering task-2-4's four acceptance bullets plus adversarial probing:\n\n1. *`test_plan_stage_spawns_three_producers_and_one_reviewer`* (lines 286-349) \u2014 Drives the generator past the refine gate via `_drive_past_refine_gate`, then walks `bundle.spawner.spawn.call_args_list` to assert architect / task_planner / risk_analyst / reviewer_plan were all dispatched. Excludes the refiner spawn before comparing against the plan-team frozenset (`_EXPECTED_PRODUCERS` / `_EXPECTED_REVIEWERS` at lines 107-110). Bullet 3 satisfied.\n\n2. *`test_plan_stage_yields_hitl_decision_with_expected_fields`* (lines 358-448) \u2014 Asserts the yielded `HITLDecision` has non-empty `id`, `question`, `options`, `decision_type \u2208 {phase_gate, choice}`, and `phase == \"plan\"`. Tolerates both attribute and dict access shapes and both enum and string forms for `phase`. The coder's `_build_plan_gate_decision` (`in_process.py:660-720`) returns `phase=\"plan\"` and `decision_type=\"phase_gate\"`, so the assertions hold. Bullet 4 satisfied.\n\n3. *`test_plan_stage_reaches_consensus_confirmed_for_each_producer`* (lines 457-543) \u2014 Drives past the refine gate, pulls the `_InProcessOrchestrator` runner out of the live generator's frame (via `_runner_from_gen`), reads `runner._plan_tracker.evaluate()`, and asserts every plan-team role (architect / task_planner / risk_analyst / reviewer_plan) has `confirmed=True` in the `agents` map AND `is_complete=True` on the snapshot. The coder's v2 sets `runner._plan_tracker = tracker` at `_plan_phase.py:118` and the evaluate-shape matches `peer_consensus.py:1590-1601` (`agents[role][\"confirmed\"]` + top-level `is_complete`). Bullet 2 satisfied.\n\n4. *`test_plan_stage_does_not_run_when_operator_rejects_refine`* (lines 557-615) \u2014 Adversarial probe: sending `\"stop\"` to the refine gate terminates the generator with `StopIteration(value=str)` (the artifact path) and no plan-team roles are spawned. Guards against a regression that fans into plan on any non-continue answer. The coder's check at `in_process.py:226-227` (`_answer_continues_past_refine` returns False for \"stop\") satisfies this \u2014 `return str(artifact_path)` fires before `_run_plan_phase` is called.\n\n5. *`test_plan_stage_does_not_spawn_implement_phase_roles`* (lines 624-675) \u2014 Adversarial probe: a misrouted `_PHASE_ROLES[\"implement\"]` lookup would spawn coder / tester / documenter / reviewer_* roles. The forbidden set covers all eight implement-team roles. Negative invariant \u2014 implement-team roles must NOT appear in `bundle.spawner.spawn.call_args_list` after the plan stage runs. Good defense against phase-dispatch off-by-one.\n\n6. *`test_plan_stage_does_not_invoke_refiner_a_second_time`* (lines 684-726) \u2014 Adversarial probe: counts refiner spawn invocations and asserts exactly one (the refine-stage spawn). Guards against a regression that re-includes REFINER in the plan-phase producer set. Sensible single-refiner-spawn invariant.\n\n7. *`test_plan_stage_carries_phase_env_var_to_producers`* (lines 735-797) \u2014 Adversarial probe: every plan-phase spawn's env must set `EGG_PHASE=plan`. Walks the spawn calls (excluding REFINER), pulls the env arg (positional `args[2]` or `kwargs[\"env\"]`), and asserts `env[\"EGG_PHASE\"] == \"plan\"`. The coder's v2 sets this at `_plan_phase.py:464` (producers) and `:527` (reviewer). Good env-propagation contract guard.\n\n**Fixtures** (lines 117-196):\n\n- *`short_intervals`* \u2014 Shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL` / `_BUS_TICK_INTERVAL` to 0.05s so the background-thread loops don't drag the test wall-clock. Module-level constant monkeypatch \u2014 correct technique.\n- *`fake_home`* \u2014 Redirects `$HOME` to a tmp dir so the sentinel write at `_write_active_role_sentinel` (still called from `_spawn_refiner` and `_spawn_plan_reviewer`) doesn't pollute the developer's actual `~/.claude/`. Good test hygiene.\n- *`isolated_pipeline_state`* \u2014 Clears `peer_consensus._TRACKERS` (or sibling names) between tests so back-to-back tests with the same pipeline id don't inherit confirmed state. Defensive against the module-level singleton at `peer_consensus.py:create_peer_consensus_tracker`.\n- *`_make_fake_bundle`* \u2014 MagicMock spawner returning `MagicMock(exit_code=0, commit_sha=\"0\"*40, stdout=\"ok\")` for every spawn. Backs `bundle.bus` with a real `InProcessMessageBus` so the heartbeat / bus-tick background loops don't trip on MagicMock-returned garbage.\n\nThe `_runner_from_gen` helper at lines 261-270 reaches into `gen.gi_frame.f_locals['self']` to access the runner. Generator-frame introspection is brittle but justified \u2014 it's the only way to read `_plan_tracker.evaluate()` for the consensus assertion without adding a leaky public accessor. Acceptable test technique with a clear docstring.\n\nThe skip guards (lines 278-284, 351-356, 451-456, 551-555, 618-622, 678-683, 729-734) all use `_has_plan_stage()` which checks for `_run_plan_phase` or peer names. The coder's v2 has `_run_plan_phase` so the skip never fires.\n\n**tests/sandbox/egg_agent_tools/test_restrictions_validator.py** (new, 324 lines) \u2014 TASK-2-6's R2-pass no-op regression guard. Verifies the slice-2 work did NOT silently extend the in-sandbox handler with R2-fail-only enforcement logic and accidentally change the response shape for the R2-pass path:\n\n1. *`test_coder_in_allow_list_response_shape_stable`* (lines 100-139) \u2014 Coder writing `orchestrator/foo.py` \u2192 `can_write=True`, response shape equals `_SINGLE_PATH_FIELDS = {\"ok\", \"role\", \"path\", \"can_write\", \"reason\", \"alternative_role\"}`. `alternative_role=None` on the allowed path.\n\n2. *`test_tester_in_allow_list_response_shape_stable`* (lines 142-160) \u2014 Tester under `tests/sandbox/egg_agent_tools/test_x.py` \u2192 `can_write=True`.\n\n3. *`test_documenter_in_allow_list_response_shape_stable`* (lines 163-173) \u2014 Documenter writing `docs/foo.md` \u2192 `can_write=True`.\n\n4. *`test_coder_cannot_write_tester_path_denial_shape_stable`* (lines 182-206) \u2014 Cross-role denial: coder writing `tests/sandbox/...` \u2192 `can_write=False`, `reason` references `shared/egg_restrictions/patterns.py`, `alternative_role=\"tester\"`. Pins the denial-shape contract that impasse-routing relies on.\n\n5. *`test_tester_cannot_write_orchestrator_path_denial_shape_stable`* (lines 209-224) \u2014 Cross-role denial: tester writing `orchestrator/foo.py` \u2192 `can_write=False`, `alternative_role=\"coder\"`.\n\n6. *`test_no_new_validator_symbol_introduced_in_r2_pass_slice`* (lines 233-259) \u2014 Asserts `validate_write_target` (and similar) is NOT in `restrictions` namespace. The R2-pass no-op invariant from TASK-2-5's contingent description. I verified via `git diff origin/main...slice-2 -- sandbox/egg_agent_tools/handlers/restrictions.py` that the slice-2 diff did NOT modify that file \u2014 the test holds.\n\n7. *`test_missing_path_raises_handler_error`* (lines 268-279) \u2014 Defensive surface: calling without `path` raises `HandlerError` with `'path' is required` message.\n\n8. *`test_unknown_role_raises_handler_error`* (lines 282-297) \u2014 Defensive surface: unknown role \u2192 `HandlerError`, not a permissive `can_write=True`.\n\n9. *`test_list_path_returns_per_path_results`* (lines 300-324) \u2014 Bulk-check surface: list `path` returns `results` array with per-path entries.\n\nThe R2-pass no-op claim is well-supported: TASK-2-5's contract description (\"If R2 = pass, this task is a no-op (close with note)\") is verified through both shape stability (existing behaviour preserved) and the negative invariant (no new validator symbol). The fail-loop case (R2 = pass + a sneaky new symbol) is the right place to catch a documentation-vs-code drift.\n\n### Strict-mode tester attestation\n\nThe tester role under BRC strict-mode requires `attestation.tests_run > 0 AND checks_passed non-empty` OR `tests_execution_blocked` OR `no_test_changes_needed` (#2431). The orchestrator accepted the propose at v1, so the attestation passed validation gateway-side. The commit message describes manual test execution intent but doesn't reproduce the attestation payload \u2014 I'm trusting the gateway's validation rather than inspecting the attestation directly (the `delphi_redacted=true` flag in the message metadata strips the attestation from peer-visible reads).\n\n### Non-blocking\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:519-526** \u2014 `agents.get(role, {}) or {}` defaults missing roles to `{}`, then `.get(\"confirmed\", False)` defaults missing flag to `False`. A regression where a role is missing entirely from `agents` would surface as `not_confirmed=[role]` \u2014 clear failure mode. Worth a small clarifying assertion that the role *is* in `agents` separately from the `confirmed` flag check (avoids ambiguity between \"role missing\" and \"role present but not confirmed\"), but the current shape is sufficiently diagnostic.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:399-446** \u2014 The test tolerates both attribute and dict access (`getattr(plan_hitl, \"id\", None) or plan_hitl.get(\"id\", None)`). The current `HITLDecision` is a dataclass with attributes; if the type ever migrates to a dict envelope the test still works. Reasonable forward-compat, but worth noting that the chained `or` swallows the empty-string case (an `id=\"\"` attribute would fall through to the dict branch and fail). Not blocking \u2014 empty-string `id` would itself be a bug.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:261-270 (`_runner_from_gen`)** \u2014 Reaches into `gen.gi_frame.f_locals[\"self\"]`. CPython-specific; PyPy or other interpreters with different generator-frame layouts could break this. Acceptable for now since the rest of the codebase pins CPython, but a follow-up could expose `_plan_tracker` via a public test hook.\n\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259** \u2014 `test_no_new_validator_symbol_introduced_in_r2_pass_slice` only checks `validate_write_target`. If TASK-2-5 had landed with a different symbol name (e.g., `enforce_write_policy` or `check_write_target`), this test would pass silently. Consider expanding the `forbidden` set to a more comprehensive list of likely names, or \u2014 better \u2014 a wildcard check on any new public symbol added to the module relative to `origin/main`. Not blocking because the actual codebase has no such symbol and the slice-2 diff doesn't touch restrictions.py.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py** \u2014 No test exercises the **NACK path** explicitly. With the v2 coder's verdict-JSON parsing (`_apply_reviewer_verdicts`), a verdict file with `per_producer: {task_planner: {verdict: \"NACK\", reason: \"...\"}}` should drive the tracker to NACK that producer. The current tests only exercise the optimistic-ACK path (no verdict file, exit_code=0). A follow-up test that writes a fake verdict JSON before the reviewer's \"spawn\" completes would close the NACK-path regression gap. Not blocking for slice-2 because the acceptance criteria don't name this, but worth filing for slice-3 / hardening.\n\nNo security, correctness, or robustness issues. Tester ACKed.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_inprocess_plan_brc.py", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py" - ], - "reason": "\nReviewed the tester's two new test files for TASK-2-4 (plan-phase BRC E2E integration test) and TASK-2-6 (R2-pass no-op regression guard for the restrictions handler). Verified test structure, assertion semantics, fixture isolation, and alignment with the coder's v2 implementation surface.\n\n### File-by-file analysis\n\n**integration_tests/regression/test_inprocess_plan_brc.py** (new, 797 lines) \u2014 Seven test cases covering task-2-4's four acceptance bullets plus adversarial probing:\n\n1. *`test_plan_stage_spawns_three_producers_and_one_reviewer`* (lines 286-349) \u2014 Drives the generator past the refine gate via `_drive_past_refine_gate`, then walks `bundle.spawner.spawn.call_args_list` to assert architect / task_planner / risk_analyst / reviewer_plan were all dispatched. Excludes the refiner spawn before comparing against the plan-team frozenset (`_EXPECTED_PRODUCERS` / `_EXPECTED_REVIEWERS` at lines 107-110). Bullet 3 satisfied.\n\n2. *`test_plan_stage_yields_hitl_decision_with_expected_fields`* (lines 358-448) \u2014 Asserts the yielded `HITLDecision` has non-empty `id`, `question`, `options`, `decision_type \u2208 {phase_gate, choice}`, and `phase == \"plan\"`. Tolerates both attribute and dict access shapes and both enum and string forms for `phase`. The coder's `_build_plan_gate_decision` (`in_process.py:660-720`) returns `phase=\"plan\"` and `decision_type=\"phase_gate\"`, so the assertions hold. Bullet 4 satisfied.\n\n3. *`test_plan_stage_reaches_consensus_confirmed_for_each_producer`* (lines 457-543) \u2014 Drives past the refine gate, pulls the `_InProcessOrchestrator` runner out of the live generator's frame (via `_runner_from_gen`), reads `runner._plan_tracker.evaluate()`, and asserts every plan-team role (architect / task_planner / risk_analyst / reviewer_plan) has `confirmed=True` in the `agents` map AND `is_complete=True` on the snapshot. The coder's v2 sets `runner._plan_tracker = tracker` at `_plan_phase.py:118` and the evaluate-shape matches `peer_consensus.py:1590-1601` (`agents[role][\"confirmed\"]` + top-level `is_complete`). Bullet 2 satisfied.\n\n4. *`test_plan_stage_does_not_run_when_operator_rejects_refine`* (lines 557-615) \u2014 Adversarial probe: sending `\"stop\"` to the refine gate terminates the generator with `StopIteration(value=str)` (the artifact path) and no plan-team roles are spawned. Guards against a regression that fans into plan on any non-continue answer. The coder's check at `in_process.py:226-227` (`_answer_continues_past_refine` returns False for \"stop\") satisfies this \u2014 `return str(artifact_path)` fires before `_run_plan_phase` is called.\n\n5. *`test_plan_stage_does_not_spawn_implement_phase_roles`* (lines 624-675) \u2014 Adversarial probe: a misrouted `_PHASE_ROLES[\"implement\"]` lookup would spawn coder / tester / documenter / reviewer_* roles. The forbidden set covers all eight implement-team roles. Negative invariant \u2014 implement-team roles must NOT appear in `bundle.spawner.spawn.call_args_list` after the plan stage runs. Good defense against phase-dispatch off-by-one.\n\n6. *`test_plan_stage_does_not_invoke_refiner_a_second_time`* (lines 684-726) \u2014 Adversarial probe: counts refiner spawn invocations and asserts exactly one (the refine-stage spawn). Guards against a regression that re-includes REFINER in the plan-phase producer set. Sensible single-refiner-spawn invariant.\n\n7. *`test_plan_stage_carries_phase_env_var_to_producers`* (lines 735-797) \u2014 Adversarial probe: every plan-phase spawn's env must set `EGG_PHASE=plan`. Walks the spawn calls (excluding REFINER), pulls the env arg (positional `args[2]` or `kwargs[\"env\"]`), and asserts `env[\"EGG_PHASE\"] == \"plan\"`. The coder's v2 sets this at `_plan_phase.py:464` (producers) and `:527` (reviewer). Good env-propagation contract guard.\n\n**Fixtures** (lines 117-196):\n\n- *`short_intervals`* \u2014 Shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL` / `_BUS_TICK_INTERVAL` to 0.05s so the background-thread loops don't drag the test wall-clock. Module-level constant monkeypatch \u2014 correct technique.\n- *`fake_home`* \u2014 Redirects `$HOME` to a tmp dir so the sentinel write at `_write_active_role_sentinel` (still called from `_spawn_refiner` and `_spawn_plan_reviewer`) doesn't pollute the developer's actual `~/.claude/`. Good test hygiene.\n- *`isolated_pipeline_state`* \u2014 Clears `peer_consensus._TRACKERS` (or sibling names) between tests so back-to-back tests with the same pipeline id don't inherit confirmed state. Defensive against the module-level singleton at `peer_consensus.py:create_peer_consensus_tracker`.\n- *`_make_fake_bundle`* \u2014 MagicMock spawner returning `MagicMock(exit_code=0, commit_sha=\"0\"*40, stdout=\"ok\")` for every spawn. Backs `bundle.bus` with a real `InProcessMessageBus` so the heartbeat / bus-tick background loops don't trip on MagicMock-returned garbage.\n\nThe `_runner_from_gen` helper at lines 261-270 reaches into `gen.gi_frame.f_locals['self']` to access the runner. Generator-frame introspection is brittle but justified \u2014 it's the only way to read `_plan_tracker.evaluate()` for the consensus assertion without adding a leaky public accessor. Acceptable test technique with a clear docstring.\n\nThe skip guards (lines 278-284, 351-356, 451-456, 551-555, 618-622, 678-683, 729-734) all use `_has_plan_stage()` which checks for `_run_plan_phase` or peer names. The coder's v2 has `_run_plan_phase` so the skip never fires.\n\n**tests/sandbox/egg_agent_tools/test_restrictions_validator.py** (new, 324 lines) \u2014 TASK-2-6's R2-pass no-op regression guard. Verifies the slice-2 work did NOT silently extend the in-sandbox handler with R2-fail-only enforcement logic and accidentally change the response shape for the R2-pass path:\n\n1. *`test_coder_in_allow_list_response_shape_stable`* (lines 100-139) \u2014 Coder writing `orchestrator/foo.py` \u2192 `can_write=True`, response shape equals `_SINGLE_PATH_FIELDS = {\"ok\", \"role\", \"path\", \"can_write\", \"reason\", \"alternative_role\"}`. `alternative_role=None` on the allowed path.\n\n2. *`test_tester_in_allow_list_response_shape_stable`* (lines 142-160) \u2014 Tester under `tests/sandbox/egg_agent_tools/test_x.py` \u2192 `can_write=True`.\n\n3. *`test_documenter_in_allow_list_response_shape_stable`* (lines 163-173) \u2014 Documenter writing `docs/foo.md` \u2192 `can_write=True`.\n\n4. *`test_coder_cannot_write_tester_path_denial_shape_stable`* (lines 182-206) \u2014 Cross-role denial: coder writing `tests/sandbox/...` \u2192 `can_write=False`, `reason` references `shared/egg_restrictions/patterns.py`, `alternative_role=\"tester\"`. Pins the denial-shape contract that impasse-routing relies on.\n\n5. *`test_tester_cannot_write_orchestrator_path_denial_shape_stable`* (lines 209-224) \u2014 Cross-role denial: tester writing `orchestrator/foo.py` \u2192 `can_write=False`, `alternative_role=\"coder\"`.\n\n6. *`test_no_new_validator_symbol_introduced_in_r2_pass_slice`* (lines 233-259) \u2014 Asserts `validate_write_target` (and similar) is NOT in `restrictions` namespace. The R2-pass no-op invariant from TASK-2-5's contingent description. I verified via `git diff origin/main...slice-2 -- sandbox/egg_agent_tools/handlers/restrictions.py` that the slice-2 diff did NOT modify that file \u2014 the test holds.\n\n7. *`test_missing_path_raises_handler_error`* (lines 268-279) \u2014 Defensive surface: calling without `path` raises `HandlerError` with `'path' is required` message.\n\n8. *`test_unknown_role_raises_handler_error`* (lines 282-297) \u2014 Defensive surface: unknown role \u2192 `HandlerError`, not a permissive `can_write=True`.\n\n9. *`test_list_path_returns_per_path_results`* (lines 300-324) \u2014 Bulk-check surface: list `path` returns `results` array with per-path entries.\n\nThe R2-pass no-op claim is well-supported: TASK-2-5's contract description (\"If R2 = pass, this task is a no-op (close with note)\") is verified through both shape stability (existing behaviour preserved) and the negative invariant (no new validator symbol). The fail-loop case (R2 = pass + a sneaky new symbol) is the right place to catch a documentation-vs-code drift.\n\n### Strict-mode tester attestation\n\nThe tester role under BRC strict-mode requires `attestation.tests_run > 0 AND checks_passed non-empty` OR `tests_execution_blocked` OR `no_test_changes_needed` (#2431). The orchestrator accepted the propose at v1, so the attestation passed validation gateway-side. The commit message describes manual test execution intent but doesn't reproduce the attestation payload \u2014 I'm trusting the gateway's validation rather than inspecting the attestation directly (the `delphi_redacted=true` flag in the message metadata strips the attestation from peer-visible reads).\n\n### Non-blocking\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:519-526** \u2014 `agents.get(role, {}) or {}` defaults missing roles to `{}`, then `.get(\"confirmed\", False)` defaults missing flag to `False`. A regression where a role is missing entirely from `agents` would surface as `not_confirmed=[role]` \u2014 clear failure mode. Worth a small clarifying assertion that the role *is* in `agents` separately from the `confirmed` flag check (avoids ambiguity between \"role missing\" and \"role present but not confirmed\"), but the current shape is sufficiently diagnostic.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:399-446** \u2014 The test tolerates both attribute and dict access (`getattr(plan_hitl, \"id\", None) or plan_hitl.get(\"id\", None)`). The current `HITLDecision` is a dataclass with attributes; if the type ever migrates to a dict envelope the test still works. Reasonable forward-compat, but worth noting that the chained `or` swallows the empty-string case (an `id=\"\"` attribute would fall through to the dict branch and fail). Not blocking \u2014 empty-string `id` would itself be a bug.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:261-270 (`_runner_from_gen`)** \u2014 Reaches into `gen.gi_frame.f_locals[\"self\"]`. CPython-specific; PyPy or other interpreters with different generator-frame layouts could break this. Acceptable for now since the rest of the codebase pins CPython, but a follow-up could expose `_plan_tracker` via a public test hook.\n\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259** \u2014 `test_no_new_validator_symbol_introduced_in_r2_pass_slice` only checks `validate_write_target`. If TASK-2-5 had landed with a different symbol name (e.g., `enforce_write_policy` or `check_write_target`), this test would pass silently. Consider expanding the `forbidden` set to a more comprehensive list of likely names, or \u2014 better \u2014 a wildcard check on any new public symbol added to the module relative to `origin/main`. Not blocking because the actual codebase has no such symbol and the slice-2 diff doesn't touch restrictions.py.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py** \u2014 No test exercises the **NACK path** explicitly. With the v2 coder's verdict-JSON parsing (`_apply_reviewer_verdicts`), a verdict file with `per_producer: {task_planner: {verdict: \"NACK\", reason: \"...\"}}` should drive the tracker to NACK that producer. The current tests only exercise the optimistic-ACK path (no verdict file, exit_code=0). A follow-up test that writes a fake verdict JSON before the reviewer's \"spawn\" completes would close the NACK-path regression gap. Not blocking for slice-2 because the acceptance criteria don't name this, but worth filing for slice-3 / hardening.\n\nNo security, correctness, or robustness issues. Tester ACKed.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:29.368265+00:00", - "phase": "implement" - }, - { - "id": "4db07212-8946-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:29.517465+00:00", - "phase": "implement" - }, - { - "id": "e56ddae1-811f-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_code", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:34.410681+00:00", - "phase": "implement" - }, - { - "id": "273852cd-cab7-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:34.616462+00:00", - "phase": "implement" - }, - { - "id": "a04fe866-1607-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:34.634548+00:00", - "phase": "implement" - }, - { - "id": "45f10d71-5314-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:34.644236+00:00", - "phase": "implement" - }, - { - "id": "07997f3a-3e59-4b", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:32:35.790039+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:35.841545+00:00", - "phase": "implement" - }, - { - "id": "95b907f9-1d90-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:32:41.073146+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:41.309738+00:00", - "phase": "implement" - }, - { - "id": "b275cd75-03ea-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:32:41.110634+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:41.338087+00:00", - "phase": "implement" - }, - { - "id": "3a7394fc-e955-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:32:41.695415+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:41.754661+00:00", - "phase": "implement" - }, - { - "id": "a4d4626c-4b7b-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:32:42.397892+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:42.428274+00:00", - "phase": "implement" - }, - { - "id": "3f4f4b2d-f534-4e", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:29:58.487845+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:32:58.735375+00:00", - "phase": "implement" - }, - { - "id": "7ab0d7a1-ed51-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "tester", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code_holistic for tester", - "body": "\nHolistic ACK on tester slice-2 v1 (commit b2be479f1). Ran all four mandatory passes against the new test surface; nothing blocking \u2014 the tests cover the four AC bullets for TASK-2-4 (3-producer + 1-reviewer spawn, plan-HITL yield with phase=plan, BRC consensus per edge, no-plan-when-operator-stops) plus four useful adversarial regressions (no implement-phase role leak, refiner spawned exactly once, EGG_PHASE=plan propagation, R2-pass restrictions-handler shape stability). TASK-2-6 lands as the documented no-op regression guard pinning the gateway-shape fields exactly so a future cq-6 option-2 fallback can't quietly mutate the response shape without flipping this suite. Holistic asymmetries I checked:\n\n- **Pass 1 (end-to-end primary use case):** `_drive_past_refine_gate` walks preflight \u2192 refiner \u2192 refine HITL gate \u2192 plan-phase BRC \u2192 plan-HITL yield, the actual user path. The fake spawner observes every spawn so the assertion is on the real call graph, not a mock surrogate. Good.\n- **Pass 2 (doc \u2194 code symmetry):** `test_plan_stage_carries_phase_env_var_to_producers` pins the rubric-promised `EGG_PHASE=plan` env-propagation contract (architect / task_planner / risk_analyst rubrics all reference plan-phase substrate context). `test_plan_stage_yields_hitl_decision_with_expected_fields` pins `phase=\"plan\"` and `decision_type \u2208 {phase_gate, choice}` to match the documenter's HITL surface promise.\n- **Pass 3 (synthetic key / sentinel):** No new sentinels introduced. The `fake_home` fixture isolates the `_write_active_role_sentinel` writes per-test so the developer's actual `$HOME/.claude/egg-active-role.json` is not polluted \u2014 good defence on a sentinel I flagged on the coder side. The `isolated_pipeline_state` fixture clears the module-level `PeerConsensusTracker` registry so back-to-back tests don't inherit confirmed state \u2014 necessary correctness given how `get_peer_consensus_tracker` caches per-pipeline trackers.\n- **Pass 4 (silent fallback):** `test_plan_stage_does_not_run_when_operator_rejects_refine` and `test_plan_stage_does_not_spawn_implement_phase_roles` pin the negative invariants. Both are exactly the regressions a future reviewer would miss in a single-file diff. `test_plan_stage_does_not_invoke_refiner_a_second_time` closes the off-by-one role-iteration loophole I would have asked for explicitly.\n\n### Non-blocking\n\n- **Architect-first ordering not pinned.** The docstring on `test_plan_stage_spawns_three_producers_and_one_reviewer` (lines 297\u2013299) reads \"The producer ordering is not pinned \u2014 they run concurrently via `ThreadPoolExecutor`\". That's correct for coder v1 but stale after coder v2/v3 (architect spawns synchronously first; `task_planner` + `risk_analyst` fan out in `ThreadPoolExecutor(max_workers=2)`). When the coder reaches v4, add a test that pins the new invariant: build a fake spawner that records each call's wall-clock timestamp (or a deterministic counter), drive the generator past the refine gate, and assert `architect`'s call_args index < min(task_planner_index, risk_analyst_index). Otherwise a future regression that flips back to all-concurrent silently passes this suite (my v1 NACK to the coder hinged on exactly that, and the rubrics in `agents/architect.md:23` + `agents/task_planner.md:23` make architect-first part of the doc-claimed contract).\n- **Reviewer verdict-JSON parsing path not exercised.** `_make_fake_bundle` returns a fixed `exit_code=0` AgentResult for every role and never writes the verdict JSON the coder's `_apply_reviewer_verdicts` parses. Per my open v3 NACK on the coder, the documented rubric schema (single-verdict JSON) and the coder's parser schema (`per_producer` wrapper) currently disagree \u2014 a tester-side fixture that writes the per_producer JSON shape (or whichever schema the coder + documenter converge on in v4) and asserts the tracker reaches the expected ACK / NACK / mixed verdict states would have caught that asymmetry on first contact. Worth adding to slice-2 tester v2.\n- **No test for the v3 `_current_phase` heartbeat-phase invariant.** The coder's v2/v3 fix flips `runner._current_phase = \"plan\"` so HEARTBEAT messages carry the right phase across the refine \u2192 plan transition. A test that drives the generator into the plan stage, then reads the bus messages and asserts at least one HEARTBEAT with `phase=\"plan\"` lands, would pin that contract. Today the only proof is the coder's commit body, not a regression guard.\n- **`_has_plan_stage()` accepts five candidate method names** (line 215\u2013225). That makes the test resilient to a coder rename but lets a downstream slice rename the method without anyone noticing. Once the dust settles on v4+, pin the canonical name (`_run_plan_phase`) and drop the wildcard.\n\nThe four mandatory holistic passes returned no blocking findings against the test surface. ACKing v1 so the tester can re-propose v2 once coder v4 lands with the verdict-schema fix.\n", - "metadata": { - "payload": { - "artifact_references": [ - "integration_tests/regression/test_inprocess_plan_brc.py", - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py" - ], - "reason": "\nHolistic ACK on tester slice-2 v1 (commit b2be479f1). Ran all four mandatory passes against the new test surface; nothing blocking \u2014 the tests cover the four AC bullets for TASK-2-4 (3-producer + 1-reviewer spawn, plan-HITL yield with phase=plan, BRC consensus per edge, no-plan-when-operator-stops) plus four useful adversarial regressions (no implement-phase role leak, refiner spawned exactly once, EGG_PHASE=plan propagation, R2-pass restrictions-handler shape stability). TASK-2-6 lands as the documented no-op regression guard pinning the gateway-shape fields exactly so a future cq-6 option-2 fallback can't quietly mutate the response shape without flipping this suite. Holistic asymmetries I checked:\n\n- **Pass 1 (end-to-end primary use case):** `_drive_past_refine_gate` walks preflight \u2192 refiner \u2192 refine HITL gate \u2192 plan-phase BRC \u2192 plan-HITL yield, the actual user path. The fake spawner observes every spawn so the assertion is on the real call graph, not a mock surrogate. Good.\n- **Pass 2 (doc \u2194 code symmetry):** `test_plan_stage_carries_phase_env_var_to_producers` pins the rubric-promised `EGG_PHASE=plan` env-propagation contract (architect / task_planner / risk_analyst rubrics all reference plan-phase substrate context). `test_plan_stage_yields_hitl_decision_with_expected_fields` pins `phase=\"plan\"` and `decision_type \u2208 {phase_gate, choice}` to match the documenter's HITL surface promise.\n- **Pass 3 (synthetic key / sentinel):** No new sentinels introduced. The `fake_home` fixture isolates the `_write_active_role_sentinel` writes per-test so the developer's actual `$HOME/.claude/egg-active-role.json` is not polluted \u2014 good defence on a sentinel I flagged on the coder side. The `isolated_pipeline_state` fixture clears the module-level `PeerConsensusTracker` registry so back-to-back tests don't inherit confirmed state \u2014 necessary correctness given how `get_peer_consensus_tracker` caches per-pipeline trackers.\n- **Pass 4 (silent fallback):** `test_plan_stage_does_not_run_when_operator_rejects_refine` and `test_plan_stage_does_not_spawn_implement_phase_roles` pin the negative invariants. Both are exactly the regressions a future reviewer would miss in a single-file diff. `test_plan_stage_does_not_invoke_refiner_a_second_time` closes the off-by-one role-iteration loophole I would have asked for explicitly.\n\n### Non-blocking\n\n- **Architect-first ordering not pinned.** The docstring on `test_plan_stage_spawns_three_producers_and_one_reviewer` (lines 297\u2013299) reads \"The producer ordering is not pinned \u2014 they run concurrently via `ThreadPoolExecutor`\". That's correct for coder v1 but stale after coder v2/v3 (architect spawns synchronously first; `task_planner` + `risk_analyst` fan out in `ThreadPoolExecutor(max_workers=2)`). When the coder reaches v4, add a test that pins the new invariant: build a fake spawner that records each call's wall-clock timestamp (or a deterministic counter), drive the generator past the refine gate, and assert `architect`'s call_args index < min(task_planner_index, risk_analyst_index). Otherwise a future regression that flips back to all-concurrent silently passes this suite (my v1 NACK to the coder hinged on exactly that, and the rubrics in `agents/architect.md:23` + `agents/task_planner.md:23` make architect-first part of the doc-claimed contract).\n- **Reviewer verdict-JSON parsing path not exercised.** `_make_fake_bundle` returns a fixed `exit_code=0` AgentResult for every role and never writes the verdict JSON the coder's `_apply_reviewer_verdicts` parses. Per my open v3 NACK on the coder, the documented rubric schema (single-verdict JSON) and the coder's parser schema (`per_producer` wrapper) currently disagree \u2014 a tester-side fixture that writes the per_producer JSON shape (or whichever schema the coder + documenter converge on in v4) and asserts the tracker reaches the expected ACK / NACK / mixed verdict states would have caught that asymmetry on first contact. Worth adding to slice-2 tester v2.\n- **No test for the v3 `_current_phase` heartbeat-phase invariant.** The coder's v2/v3 fix flips `runner._current_phase = \"plan\"` so HEARTBEAT messages carry the right phase across the refine \u2192 plan transition. A test that drives the generator into the plan stage, then reads the bus messages and asserts at least one HEARTBEAT with `phase=\"plan\"` lands, would pin that contract. Today the only proof is the coder's commit body, not a regression guard.\n- **`_has_plan_stage()` accepts five candidate method names** (line 215\u2013225). That makes the test resilient to a coder rename but lets a downstream slice rename the method without anyone noticing. Once the dust settles on v4+, pin the canonical name (`_run_plan_phase`) and drop the wildcard.\n\nThe four mandatory holistic passes returned no blocking findings against the test surface. ACKing v1 so the tester can re-propose v2 once coder v4 lands with the verdict-schema fix.\n", - "ack_version": 1 - }, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:10.730470+00:00", - "phase": "implement" - }, - { - "id": "ffe9fc42-7041-4f", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "tester", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:10.730531+00:00", - "phase": "implement" - }, - { - "id": "ab55bb15-bc01-48", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:10.792057+00:00", - "phase": "implement" - }, - { - "id": "316d896c-1da8-42", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by tester", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:14.728503+00:00", - "phase": "implement" - }, - { - "id": "5b4dcdbe-3fa9-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:14.936632+00:00", - "phase": "implement" - }, - { - "id": "56e5e4e4-23f2-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:14.993475+00:00", - "phase": "implement" - }, - { - "id": "977ae1e7-9e54-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:14.998070+00:00", - "phase": "implement" - }, - { - "id": "d09ccadb-d24a-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:15.007574+00:00", - "phase": "implement" - }, - { - "id": "0570e233-193c-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_code_holistic (pending_acks)", - "body": "Reviewer reviewer_code_holistic cannot confirm: unresolved NACKs. Wait for these producers to re-propose before confirming: ['coder']", - "metadata": { - "pending_acks": true, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:19.103669+00:00", - "phase": "implement" - }, - { - "id": "798d1efc-43cd-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:19.617388+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:19.636874+00:00", - "phase": "implement" - }, - { - "id": "cef36e5b-60cb-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:19.661083+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:19.699069+00:00", - "phase": "implement" - }, - { - "id": "c40e7f11-9f23-41", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:19.711458+00:00", - "phase": "implement" - }, - { - "id": "32c58e2a-3689-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:19.784333+00:00", - "phase": "implement" - }, - { - "id": "df817afd-fa65-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:20.190449+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:20.220668+00:00", - "phase": "implement" - }, - { - "id": "e08fdba4-ddcd-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:20.290271+00:00", - "phase": "implement" - }, - { - "id": "0d641223-2d6d-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:20.580514+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:20.695460+00:00", - "phase": "implement" - }, - { - "id": "e3ce6884-a9a9-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:20.810613+00:00", - "phase": "implement" - }, - { - "id": "35b96164-9686-4b", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:23.705367+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:23.731552+00:00", - "phase": "implement" - }, - { - "id": "1843a281-b946-4e", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:23.870774+00:00", - "phase": "implement" - }, - { - "id": "8f3f3a9d-a1b3-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:24.404584+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:24.460006+00:00", - "phase": "implement" - }, - { - "id": "dd7ad53d-5980-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:24.546883+00:00", - "phase": "implement" - }, - { - "id": "541b9c3c-2430-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:25.103637+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:25.159588+00:00", - "phase": "implement" - }, - { - "id": "27bd0f66-027b-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:26.649587+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:26.683228+00:00", - "phase": "implement" - }, - { - "id": "5963cac2-1194-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:27.941195+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:27.961891+00:00", - "phase": "implement" - }, - { - "id": "f60feb50-d820-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:28.076954+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:28.107091+00:00", - "phase": "implement" - }, - { - "id": "5ea103bb-a31e-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:33:30.025166+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:51.171572+00:00", - "phase": "implement" - }, - { - "id": "5321e04b-7a8e-4d", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:29:58.487845+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:33:58.790247+00:00", - "phase": "implement" - }, - { - "id": "2cf80827-429a-42", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from coder", - "body": "Slice-2 coder v4 \u2014 addresses reviewer_code_holistic v3 NACK blocker H3 (rubric/code verdict-JSON schema mismatch). Commit ecd8336b7.\n\nH3 fix: `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default single-verdict shape AND the per_producer wrapper. When the verdict JSON's top-level `verdict` is \"ACK\" or \"NACK\" (the rubric-documented shape \u2014 plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:57-80), the verdict is broadcast to every plan producer edge. ACK acks all three, NACK nacks all three with the rubric's top-level `feedback` blob propagated as the per-edge reason (synthetic placeholder fires if feedback is empty so the tracker's NACK guard doesn't reject the payload). The per_producer wrapper still takes precedence when present and well-formed \u2014 reviewers that want explicit edge granularity opt in. The function takes an optional `plan_producers` kwarg so the orchestrator can broadcast across the right role set; the `_read_plan_reviewer_verdicts` class method delegate propagates the kwarg unchanged so tester-side tests keep their access pattern.\n\nWhy broadcast (option (c)) rather than option (a) per-edge spawn: the rubric's \"Verdict JSON shape\" section documents a single top-level verdict object as the canonical shape. The \"ACK only if every criterion passes; NACK if any criterion fails\" rubric rule is a whole-plan verdict semantic, so the broadcast preserves it. Per-edge granularity is available via the per_producer extension wrapper for reviewers that want it. No documenter coordination needed; the rubric stays as-shipped.\n\nEnd-to-end smoke (manual, in-process, MagicMock subagents) on v4:\n- Rubric-default single-verdict NACK: tracker NACKs critical edges (architect, task_planner), advisory edge (risk_analyst) confirms, reviewer_plan blocks consensus. is_complete=False; blocking_agents=['architect', 'task_planner', 'reviewer_plan'].\n- Rubric-default single-verdict ACK: every edge confirmed; is_complete=True.\n- per_producer wrapper: mixed ACK/NACK applied per-edge (existing behaviour).\n- Harness-fake path (no verdict file, reviewer exit 0): optimistic ACK preserved so tester's 16 existing tests keep working.\n- Fail-closed (no verdict file, reviewer exit non-zero): critical edges NACK'd (existing behaviour).\n\nLint + format + file-size checks all green. `_plan_phase.py` is 747 lines; `in_process.py` 1095 lines.\n\nCarries forward every v1/v2/v3 blocker fix (C1 sentinel race in concurrent producers, C2 heartbeat phase, H1 architect-first ordering, H2 reviewer verdict parsing, T1 ruff format).", - "metadata": { - "payload": { - "summary": "Slice-2 coder v4 \u2014 addresses reviewer_code_holistic v3 NACK blocker H3 (rubric/code verdict-JSON schema mismatch). Commit ecd8336b7.\n\nH3 fix: `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default single-verdict shape AND the per_producer wrapper. When the verdict JSON's top-level `verdict` is \"ACK\" or \"NACK\" (the rubric-documented shape \u2014 plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:57-80), the verdict is broadcast to every plan producer edge. ACK acks all three, NACK nacks all three with the rubric's top-level `feedback` blob propagated as the per-edge reason (synthetic placeholder fires if feedback is empty so the tracker's NACK guard doesn't reject the payload). The per_producer wrapper still takes precedence when present and well-formed \u2014 reviewers that want explicit edge granularity opt in. The function takes an optional `plan_producers` kwarg so the orchestrator can broadcast across the right role set; the `_read_plan_reviewer_verdicts` class method delegate propagates the kwarg unchanged so tester-side tests keep their access pattern.\n\nWhy broadcast (option (c)) rather than option (a) per-edge spawn: the rubric's \"Verdict JSON shape\" section documents a single top-level verdict object as the canonical shape. The \"ACK only if every criterion passes; NACK if any criterion fails\" rubric rule is a whole-plan verdict semantic, so the broadcast preserves it. Per-edge granularity is available via the per_producer extension wrapper for reviewers that want it. No documenter coordination needed; the rubric stays as-shipped.\n\nEnd-to-end smoke (manual, in-process, MagicMock subagents) on v4:\n- Rubric-default single-verdict NACK: tracker NACKs critical edges (architect, task_planner), advisory edge (risk_analyst) confirms, reviewer_plan blocks consensus. is_complete=False; blocking_agents=['architect', 'task_planner', 'reviewer_plan'].\n- Rubric-default single-verdict ACK: every edge confirmed; is_complete=True.\n- per_producer wrapper: mixed ACK/NACK applied per-edge (existing behaviour).\n- Harness-fake path (no verdict file, reviewer exit 0): optimistic ACK preserved so tester's 16 existing tests keep working.\n- Fail-closed (no verdict file, reviewer exit non-zero): critical edges NACK'd (existing behaviour).\n\nLint + format + file-size checks all green. `_plan_phase.py` is 747 lines; `in_process.py` 1095 lines.\n\nCarries forward every v1/v2/v3 blocker fix (C1 sentinel race in concurrent producers, C2 heartbeat phase, H1 architect-first ordering, H2 reviewer verdict parsing, T1 ruff format).", - "attestation": { - "commit_sha": "ecd8336b706e7f6df51fa1f4c37211c330efcf54", - "commit_shas": [ - "ecd8336b706e7f6df51fa1f4c37211c330efcf54" - ], - "files_changed": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py" - ], - "checks_passed": [ - "ruff_format_check", - "ruff_check_lint", - "file_size_lint", - "manual_smoke_rubric_default_nack", - "manual_smoke_rubric_default_ack", - "manual_smoke_per_producer_wrapper", - "manual_smoke_harness_fake_optimistic_ack", - "manual_smoke_fail_closed" - ], - "tests_run": 5, - "no_test_changes_needed": true, - "no_test_changes_reason": "Coder role's allow-list excludes tests/. Class methods stay as thin delegates (the new `plan_producers` kwarg on `_read_plan_reviewer_verdicts` is keyword-only with a None default, so existing callers' access pattern is unchanged). Five new manual smoke runs cover the dual-schema parser behaviour." - }, - "artifacts": [ - "orchestrator/substrate/in_process.py", - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/__init__.py" - ], - "risk_considered": "v4 risks: (1) Broadcast vs per-edge ambiguity \u2014 a future rubric extension that wants per-edge verdicts under a non-`per_producer` key would not be honoured. Mitigated by documenting both schemas in `read_plan_reviewer_verdicts` and treating the per_producer wrapper as the explicit per-edge opt-in. (2) NACK guard rejection on empty feedback \u2014 ReviewPayload validators reject NACKs without a reason. Mitigated by synthesising a placeholder reason when feedback is empty so the tracker records the NACK rather than discarding via `log_tracker_warning`. (3) Test compatibility \u2014 tester's 16 passing v2/v3 tests rely on the harness-fake path (no verdict file, reviewer exit 0 \u2192 optimistic ACK). v4 preserves that path verbatim; only the \"verdict file present but rubric-default shape\" case changes from \"fall-through to empty-verdicts\" to \"broadcast top-level verdict\".", - "commit_sha": "ecd8336b706e7f6df51fa1f4c37211c330efcf54", - "files_changed": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py" - ], - "tests_run": [], - "tasks_satisfied": [ - "task-2-1", - "task-2-2", - "task-2-5" - ] - }, - "version": 4, - "commit_sha": "ecd8336b706e7f6df51fa1f4c37211c330efcf54", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.396717+00:00", - "phase": "implement" - }, - { - "id": "8242e968-4bbc-48", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "reviewer_code", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: coder submitted new proposal v4", - "body": "Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "coder", - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.396831+00:00", - "phase": "implement" - }, - { - "id": "460d44eb-4172-40", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "reviewer_contract", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: coder submitted new proposal v4", - "body": "Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "coder", - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.396852+00:00", - "phase": "implement" - }, - { - "id": "bd765861-11d5-4c", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "tester", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: coder submitted new proposal v4", - "body": "Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "coder", - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.396871+00:00", - "phase": "implement" - }, - { - "id": "28beffe3-b2c0-48", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "reviewer_security", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: coder submitted new proposal v4", - "body": "Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "coder", - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.396888+00:00", - "phase": "implement" - }, - { - "id": "1c595b54-c3c3-4a", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "reviewer_concurrency", - "message_type": "CONSENSUS_RE_REVIEW", - "subject": "Re-review required: coder submitted new proposal v4", - "body": "Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal.", - "metadata": { - "producer_role": "coder", - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.396904+00:00", - "phase": "implement" - }, - { - "id": "a9b74b5d-f955-4b", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.610906+00:00", - "phase": "implement" - }, - { - "id": "fc080014-d5cc-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.644484+00:00", - "phase": "implement" - }, - { - "id": "eb5806c1-2dcc-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.708269+00:00", - "phase": "implement" - }, - { - "id": "f8f241ac-290d-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.723865+00:00", - "phase": "implement" - }, - { - "id": "566ac92c-d605-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.732711+00:00", - "phase": "implement" - }, - { - "id": "c407a6f5-89d6-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:00.741172+00:00", - "phase": "implement" - }, - { - "id": "d8528bdf-532f-44", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:34:10.517501+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:22.348323+00:00", - "phase": "implement" - }, - { - "id": "25899c4c-90f1-4a", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:34:10.049489+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:34:22.386149+00:00", - "phase": "implement" - }, - { - "id": "7794f311-e994-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_concurrency for coder", - "body": "\nConcurrency-lens re-review of coder v4 (commit ecd8336b7). The v3\u2192v4 delta is purely in the verdict-JSON parser (`orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts`) \u2014 single-verdict-broadcast schema added alongside the per-producer wrapper, plus a `plan_producers` kwarg passed through from `_run_plan_phase_inner` and the `_read_plan_reviewer_verdicts` class-method delegate. Zero changes to the ThreadPoolExecutor wiring, the spawner call shape, the sentinel write, the heartbeat phase plumbing, the tracker register/propose/ack/confirmed sequencing, or the worktree allocation path. My v3 ACK still applies \u2014 re-confirming.\n\n### What I checked\n\n- **`read_plan_reviewer_verdicts` runs on the main thread** (`_plan_phase.py:170`, post-`as_completed_fn` block), so adding `plan_producers` as a kwarg and iterating it inside the function body has no thread-safety surface to disturb. No new shared mutable state, no new locks needed.\n- **`{role.value: broadcast for role in plan_producers}`** (`:347`) constructs a fresh dict from the list passed in \u2014 no aliasing of the list-of-producers across threads.\n- **No new background-thread state** \u2014 `_publish_heartbeat` still reads `self._current_phase`; `_run_plan_phase` still sets it to `\"plan\"`; v4 does not regress C2.\n- **No new sentinel writes** in the concurrent path \u2014 `spawn_plan_producer` is unchanged from v3, still sentinel-free; v4 does not regress C1.\n- **`_read_plan_reviewer_verdicts` class-method delegate** (`in_process.py:914-922`) simply forwards the kwarg; called from the same main-thread path as before.\n\nNo new concurrency findings. ACKing v4 against version 4.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py" - ], - "reason": "\nConcurrency-lens re-review of coder v4 (commit ecd8336b7). The v3\u2192v4 delta is purely in the verdict-JSON parser (`orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts`) \u2014 single-verdict-broadcast schema added alongside the per-producer wrapper, plus a `plan_producers` kwarg passed through from `_run_plan_phase_inner` and the `_read_plan_reviewer_verdicts` class-method delegate. Zero changes to the ThreadPoolExecutor wiring, the spawner call shape, the sentinel write, the heartbeat phase plumbing, the tracker register/propose/ack/confirmed sequencing, or the worktree allocation path. My v3 ACK still applies \u2014 re-confirming.\n\n### What I checked\n\n- **`read_plan_reviewer_verdicts` runs on the main thread** (`_plan_phase.py:170`, post-`as_completed_fn` block), so adding `plan_producers` as a kwarg and iterating it inside the function body has no thread-safety surface to disturb. No new shared mutable state, no new locks needed.\n- **`{role.value: broadcast for role in plan_producers}`** (`:347`) constructs a fresh dict from the list passed in \u2014 no aliasing of the list-of-producers across threads.\n- **No new background-thread state** \u2014 `_publish_heartbeat` still reads `self._current_phase`; `_run_plan_phase` still sets it to `\"plan\"`; v4 does not regress C2.\n- **No new sentinel writes** in the concurrent path \u2014 `spawn_plan_producer` is unchanged from v3, still sentinel-free; v4 does not regress C1.\n- **`_read_plan_reviewer_verdicts` class-method delegate** (`in_process.py:914-922`) simply forwards the kwarg; called from the same main-thread path as before.\n\nNo new concurrency findings. ACKing v4 against version 4.\n", - "ack_version": 4 - }, - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:05.619787+00:00", - "phase": "implement" - }, - { - "id": "b21d22f7-6147-45", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:05.667862+00:00", - "phase": "implement" - }, - { - "id": "478b422d-018e-4e", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:10.950600+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:10.980529+00:00", - "phase": "implement" - }, - { - "id": "60324dbb-1aa6-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code for coder", - "body": "\nRe-reviewed v4 (commit ecd8336b7) targeting the reviewer_code_holistic v3 NACK blocker H3 \u2014 the documenter-shipped `reviewer_plan.md` rubric documents a single top-level verdict object, but v2/v3's `read_plan_reviewer_verdicts` only parsed a `per_producer` wrapper, so a rubric-following reviewer's NACK would have been silently lost into the optimistic-ACK fallback path.\n\n### What changed v3 \u2192 v4\n\n**Two-schema verdict parser (`_plan_phase.py:251-353`).** `read_plan_reviewer_verdicts` now accepts both shapes:\n\n1. **Rubric-default single-verdict (broadcast).** Top-level `verdict \u2208 {ACK, NACK}` \u2192 broadcast to every plan producer edge. NACK propagates the top-level `feedback` blob as the per-edge `reason`; ACK propagates `artifact_references` and `pre_merge_condition`. If the broadcast verdict is NACK and `feedback` is empty, a synthetic placeholder fires (`f\"reviewer_plan broadcast {top_verdict}: top-level verdict without a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed analysis.\"`) so `ReviewPayload.validate_nack_has_reason` doesn't reject the payload server-side.\n\n2. **Per-producer extension (per-edge).** Existing `per_producer: {role: {verdict, reason, ...}}` wrapper takes precedence when present AND well-formed (at least one entry survives validation). Reviewers that want explicit edge granularity (ACK architect + NACK task_planner) opt into the wrapper; the rubric's default shape stays broadcast-compatible.\n\n**Precedence rule**: per_producer wrapper > top-level broadcast > empty (fail-closed / optimistic-ACK fallback in `_apply_reviewer_verdicts`).\n\n**`plan_producers` kwarg threading.** New `plan_producers: list[Any] | None = None` kwarg on `read_plan_reviewer_verdicts` (lines 252-254). The orchestrator caller passes the producer list (`_run_plan_phase_inner` line 170) so the broadcast knows which producer roles to target. The class-method delegate at `in_process.py:914-923` propagates the kwarg so tester-side tests that call `runner._read_plan_reviewer_verdicts(plan_producers=[...])` retain their access pattern.\n\n### File-by-file analysis\n\n**orchestrator/substrate/_plan_phase.py** (+82/-25) \u2014 Single-function change in `read_plan_reviewer_verdicts`; the rest of `_run_plan_phase_inner` / `_apply_reviewer_verdicts` / spawn helpers is unchanged. The broadcast construction at lines 348-353 is a dict-comprehension keyed by `role.value` so the resulting `{role: entry, ...}` matches the per_producer wrapper's shape \u2014 `_apply_reviewer_verdicts` consumes either path uniformly without changes. The `if not plan_producers: return verdict_path, {}` guard at lines 332-334 keeps legacy callers (any test or future caller that didn't pass `plan_producers`) safe \u2014 they fall through to the fail-closed / optimistic-ACK heuristic rather than crashing.\n\n**orchestrator/substrate/in_process.py** (+3/-1) \u2014 `_read_plan_reviewer_verdicts` delegate updated with the same `plan_producers` kwarg. Surface-preserving for the tester's tests.\n\n### Edge-case behaviour\n\n- **per_producer wrapper present but all entries invalid (e.g., `verdict` field missing or unrecognized).** The filter loop produces an empty `normalised` dict; `if normalised:` is False; falls through to single-verdict broadcast (if top-level `verdict` is set) or empty (fail-closed/optimistic heuristic). Reasonable.\n\n- **Both per_producer wrapper AND top-level verdict present.** per_producer wins when it has at least one valid entry. Reviewers that want to override the broadcast for specific edges set the wrapper explicitly. Sensible precedence.\n\n- **Top-level verdict is invalid (e.g., \"MAYBE\").** `top_verdict in {\"ACK\", \"NACK\"}` returns False; falls through to `return verdict_path, {}` \u2014 empty verdicts, fail-closed/optimistic heuristic applies. Good.\n\n- **Broadcast NACK with empty feedback.** Synthetic placeholder reason fires (lines 339-345). `_record_reviewer_nack` records the NACK successfully. Closes my v3 non-blocking #1.\n\n- **`plan_producers=None` (legacy caller path).** Returns empty verdicts; `_apply_reviewer_verdicts` falls back to fail-closed / optimistic-ACK based on reviewer exit_code. Safe default.\n\n### Compatibility with tester's existing tests\n\nThe tester's `test_inprocess_plan_brc.py` uses a MagicMock spawner that never writes a verdict JSON. The new schema parser still returns `(None, {})` when the file is missing, so `_apply_reviewer_verdicts` continues to take the optimistic-ACK path for harness-faked runs \u2014 the tester's 7 existing plan-phase tests remain green. The v4 docstring smoke notes confirm this.\n\n### Non-blocking\n\n- **orchestrator/substrate/_plan_phase.py:333-334** \u2014 When `plan_producers=None` and the verdict file has a top-level single verdict, the function silently returns `(verdict_path, {})`. The fail-closed/optimistic heuristic downstream may then misinterpret a real NACK as an optimistic ACK (if reviewer exit_code=0). The current orchestrator always passes `plan_producers`, so this is a latent footgun for legacy callers. Worth a `logger.warning` when this branch fires noting \"single-verdict shape detected but no plan_producers kwarg \u2014 broadcast suppressed\".\n\n- **orchestrator/substrate/_plan_phase.py:286-313** \u2014 When `per_producer` is a well-formed dict that has entries but ALL entries are filtered out (every entry has `verdict` \u2209 {ACK, NACK} or is not a dict), the code falls through to single-verdict broadcast. This is reasonable, but combined with the comment \"per-producer extension wrapper takes precedence if it's a well-formed dict\" the fall-through behavior may surprise a reviewer who tried to write a per_producer wrapper but typo'd every verdict. A `logger.warning` on \"per_producer wrapper present but no valid entries\" would help debugging.\n\n- **orchestrator/substrate/_plan_phase.py:341-345** \u2014 The synthetic broadcast-NACK placeholder text references \"the criteria-keyed analysis\" but doesn't embed the actual verdict JSON path. An operator reading the NACK reason at the HITL gate gets a pointer to \"see verdict JSON\" without knowing where it lives. Consider templating `{verdict_path}` into the placeholder so the operator can `cat` the file directly. Not blocking \u2014 the verdict_path is already surfaced in `runner._verdict_diagnostics` (`_plan_phase.py:171-175`) and the plan-placeholder body.\n\nNo security, correctness, or robustness issues introduced by v4. The change is surgical and well-scoped \u2014 addresses H3 (rubric-default verdict schema) without disturbing the v2/v3 per_producer extension semantics. Coder v4 ACKed.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py" - ], - "reason": "\nRe-reviewed v4 (commit ecd8336b7) targeting the reviewer_code_holistic v3 NACK blocker H3 \u2014 the documenter-shipped `reviewer_plan.md` rubric documents a single top-level verdict object, but v2/v3's `read_plan_reviewer_verdicts` only parsed a `per_producer` wrapper, so a rubric-following reviewer's NACK would have been silently lost into the optimistic-ACK fallback path.\n\n### What changed v3 \u2192 v4\n\n**Two-schema verdict parser (`_plan_phase.py:251-353`).** `read_plan_reviewer_verdicts` now accepts both shapes:\n\n1. **Rubric-default single-verdict (broadcast).** Top-level `verdict \u2208 {ACK, NACK}` \u2192 broadcast to every plan producer edge. NACK propagates the top-level `feedback` blob as the per-edge `reason`; ACK propagates `artifact_references` and `pre_merge_condition`. If the broadcast verdict is NACK and `feedback` is empty, a synthetic placeholder fires (`f\"reviewer_plan broadcast {top_verdict}: top-level verdict without a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed analysis.\"`) so `ReviewPayload.validate_nack_has_reason` doesn't reject the payload server-side.\n\n2. **Per-producer extension (per-edge).** Existing `per_producer: {role: {verdict, reason, ...}}` wrapper takes precedence when present AND well-formed (at least one entry survives validation). Reviewers that want explicit edge granularity (ACK architect + NACK task_planner) opt into the wrapper; the rubric's default shape stays broadcast-compatible.\n\n**Precedence rule**: per_producer wrapper > top-level broadcast > empty (fail-closed / optimistic-ACK fallback in `_apply_reviewer_verdicts`).\n\n**`plan_producers` kwarg threading.** New `plan_producers: list[Any] | None = None` kwarg on `read_plan_reviewer_verdicts` (lines 252-254). The orchestrator caller passes the producer list (`_run_plan_phase_inner` line 170) so the broadcast knows which producer roles to target. The class-method delegate at `in_process.py:914-923` propagates the kwarg so tester-side tests that call `runner._read_plan_reviewer_verdicts(plan_producers=[...])` retain their access pattern.\n\n### File-by-file analysis\n\n**orchestrator/substrate/_plan_phase.py** (+82/-25) \u2014 Single-function change in `read_plan_reviewer_verdicts`; the rest of `_run_plan_phase_inner` / `_apply_reviewer_verdicts` / spawn helpers is unchanged. The broadcast construction at lines 348-353 is a dict-comprehension keyed by `role.value` so the resulting `{role: entry, ...}` matches the per_producer wrapper's shape \u2014 `_apply_reviewer_verdicts` consumes either path uniformly without changes. The `if not plan_producers: return verdict_path, {}` guard at lines 332-334 keeps legacy callers (any test or future caller that didn't pass `plan_producers`) safe \u2014 they fall through to the fail-closed / optimistic-ACK heuristic rather than crashing.\n\n**orchestrator/substrate/in_process.py** (+3/-1) \u2014 `_read_plan_reviewer_verdicts` delegate updated with the same `plan_producers` kwarg. Surface-preserving for the tester's tests.\n\n### Edge-case behaviour\n\n- **per_producer wrapper present but all entries invalid (e.g., `verdict` field missing or unrecognized).** The filter loop produces an empty `normalised` dict; `if normalised:` is False; falls through to single-verdict broadcast (if top-level `verdict` is set) or empty (fail-closed/optimistic heuristic). Reasonable.\n\n- **Both per_producer wrapper AND top-level verdict present.** per_producer wins when it has at least one valid entry. Reviewers that want to override the broadcast for specific edges set the wrapper explicitly. Sensible precedence.\n\n- **Top-level verdict is invalid (e.g., \"MAYBE\").** `top_verdict in {\"ACK\", \"NACK\"}` returns False; falls through to `return verdict_path, {}` \u2014 empty verdicts, fail-closed/optimistic heuristic applies. Good.\n\n- **Broadcast NACK with empty feedback.** Synthetic placeholder reason fires (lines 339-345). `_record_reviewer_nack` records the NACK successfully. Closes my v3 non-blocking #1.\n\n- **`plan_producers=None` (legacy caller path).** Returns empty verdicts; `_apply_reviewer_verdicts` falls back to fail-closed / optimistic-ACK based on reviewer exit_code. Safe default.\n\n### Compatibility with tester's existing tests\n\nThe tester's `test_inprocess_plan_brc.py` uses a MagicMock spawner that never writes a verdict JSON. The new schema parser still returns `(None, {})` when the file is missing, so `_apply_reviewer_verdicts` continues to take the optimistic-ACK path for harness-faked runs \u2014 the tester's 7 existing plan-phase tests remain green. The v4 docstring smoke notes confirm this.\n\n### Non-blocking\n\n- **orchestrator/substrate/_plan_phase.py:333-334** \u2014 When `plan_producers=None` and the verdict file has a top-level single verdict, the function silently returns `(verdict_path, {})`. The fail-closed/optimistic heuristic downstream may then misinterpret a real NACK as an optimistic ACK (if reviewer exit_code=0). The current orchestrator always passes `plan_producers`, so this is a latent footgun for legacy callers. Worth a `logger.warning` when this branch fires noting \"single-verdict shape detected but no plan_producers kwarg \u2014 broadcast suppressed\".\n\n- **orchestrator/substrate/_plan_phase.py:286-313** \u2014 When `per_producer` is a well-formed dict that has entries but ALL entries are filtered out (every entry has `verdict` \u2209 {ACK, NACK} or is not a dict), the code falls through to single-verdict broadcast. This is reasonable, but combined with the comment \"per-producer extension wrapper takes precedence if it's a well-formed dict\" the fall-through behavior may surprise a reviewer who tried to write a per_producer wrapper but typo'd every verdict. A `logger.warning` on \"per_producer wrapper present but no valid entries\" would help debugging.\n\n- **orchestrator/substrate/_plan_phase.py:341-345** \u2014 The synthetic broadcast-NACK placeholder text references \"the criteria-keyed analysis\" but doesn't embed the actual verdict JSON path. An operator reading the NACK reason at the HITL gate gets a pointer to \"see verdict JSON\" without knowing where it lives. Consider templating `{verdict_path}` into the placeholder so the operator can `cat` the file directly. Not blocking \u2014 the verdict_path is already surfaced in `runner._verdict_diagnostics` (`_plan_phase.py:171-175`) and the plan-placeholder body.\n\nNo security, correctness, or robustness issues introduced by v4. The change is surgical and well-scoped \u2014 addresses H3 (rubric-default verdict schema) without disturbing the v2/v3 per_producer extension semantics. Coder v4 ACKed.\n", - "ack_version": 4 - }, - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:12.717496+00:00", - "phase": "implement" - }, - { - "id": "a620e741-9d78-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:12.758873+00:00", - "phase": "implement" - }, - { - "id": "852dd96b-1ddb-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:13.364302+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:13.424717+00:00", - "phase": "implement" - }, - { - "id": "b489a258-f267-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_security for coder", - "body": "\nRe-reviewed slice-2 coder v4 at commit ecd8336b7 through the security lens. The v3\u2192v4 delta is bounded to `read_plan_reviewer_verdicts` (now accepts both the rubric-default single-verdict schema AND the per_producer wrapper) plus the matching kwarg propagation on the class delegate. No new security findings; the dual-schema parser is well-bounded.\n\n### Lens checks against the v3\u2192v4 delta\n\n1. **Cross-file allowlist mismatch (\u00a71):** Unchanged. The newly-supported schema 1 matches the documenter's rubric at plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md lines 57-80 (top-level `verdict`, `feedback`, `analysis`, `artifact_references`) \u2014 closes a *real* cross-file mismatch between the documenter-shipped reviewer rubric and the v2/v3 parser, where a rubric-conformant reviewer NACK would have been silently swallowed into the \"verdict file present but no parseable per_producer entries\" branch. v4 explicitly preserves the \"ACK only if every criterion passes\" semantic by broadcasting an ACK / NACK to every producer edge.\n\n2. **Handler-vs-validator path mismatch (\u00a72):** N/A \u2014 no new entrypoint.\n\n3. **Information-disclosure (\u00a73):** The reviewer's `feedback` blob now broadcasts to every producer edge as the per-edge `reason`. The `feedback` originates from the reviewer's own output in a worktree-bounded write, flows into the BRC tracker payload (in-process state) and is repr-truncated to 200 chars in `format_plan_placeholder`'s \"reviewer_plan verdict parsing\" subsection \u2014 same disclosure surface as v3, just propagated to three edges instead of zero when the rubric-default shape is used. No NEW sink.\n\n4. **Path-traversal / agent-supplied paths (\u00a78):** `verdict_path` construction is unchanged (`outputs_dir / f\"{artifact_id}-reviewer_plan-output.json\"`); still orchestrator-derived from trusted `state_root` + `issue_number/pipeline_id`. The new schema-1 parser preserves strict input sanitisation:\n - `isinstance(blob, dict)` gate before any `.get` access.\n - `top_verdict in {\"ACK\", \"NACK\"}` whitelist before any tracker emission.\n - `if not plan_producers: return verdict_path, {}` fail-safe: a caller that doesn't supply the producer list cannot drive a broadcast.\n - All string fields cast through `str()`, list fields through `list()`, dict comprehension builds typed entries.\n - Empty-`feedback` NACK is given a deterministic placeholder string so `ReviewPayload.validate_nack_has_reason` cannot reject the payload and silently lose the NACK \u2014 closes a class of \"reviewer NACK disappears\" bugs the v3 parser had if the rubric was followed literally.\n\n5. **Uncommitted-artifact / symlink mismatch (\u00a74):** N/A.\n\n6. **Credential-shim modifications (\u00a75):** N/A.\n\n7. **Secret leakage (\u00a76):** Unchanged sinks. The reviewer's `pre_merge_condition` string is also broadcast to every producer edge via the shared `broadcast` dict (`{role.value: broadcast for role in plan_producers}`); pre_merge_condition is a documented bare-string field on `ReviewPayload`, not a credential carrier.\n\n8. **Cross-file OWASP top-10 (\u00a77):** No new sources or sinks. The dict-comprehension shares one `broadcast` dict reference across all producer keys, but `_apply_reviewer_verdicts` only reads from those entries; no downstream mutation that would couple per-edge state. Pure code-quality concern, not security.\n\n### Non-blocking (carried forward where relevant)\n- in_process.py:98 \u2014 `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` remains unreferenced; defer to reviewer_code.\n- _plan_phase.py:288-294 \u2014 `json.loads(verdict_path.read_text(...))` still has no file-size cap; hardening-only observation.\n- _plan_phase.py:343-348 \u2014 the shared `broadcast` dict reference across all producer keys means any future mutation in `_apply_reviewer_verdicts` would silently couple per-edge state. Today's downstream is read-only so this is latent; a follow-up could `copy.deepcopy(broadcast)` per role if mutation becomes warranted. Code-quality / future-proofing only; defer to reviewer_code.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py" - ], - "reason": "\nRe-reviewed slice-2 coder v4 at commit ecd8336b7 through the security lens. The v3\u2192v4 delta is bounded to `read_plan_reviewer_verdicts` (now accepts both the rubric-default single-verdict schema AND the per_producer wrapper) plus the matching kwarg propagation on the class delegate. No new security findings; the dual-schema parser is well-bounded.\n\n### Lens checks against the v3\u2192v4 delta\n\n1. **Cross-file allowlist mismatch (\u00a71):** Unchanged. The newly-supported schema 1 matches the documenter's rubric at plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md lines 57-80 (top-level `verdict`, `feedback`, `analysis`, `artifact_references`) \u2014 closes a *real* cross-file mismatch between the documenter-shipped reviewer rubric and the v2/v3 parser, where a rubric-conformant reviewer NACK would have been silently swallowed into the \"verdict file present but no parseable per_producer entries\" branch. v4 explicitly preserves the \"ACK only if every criterion passes\" semantic by broadcasting an ACK / NACK to every producer edge.\n\n2. **Handler-vs-validator path mismatch (\u00a72):** N/A \u2014 no new entrypoint.\n\n3. **Information-disclosure (\u00a73):** The reviewer's `feedback` blob now broadcasts to every producer edge as the per-edge `reason`. The `feedback` originates from the reviewer's own output in a worktree-bounded write, flows into the BRC tracker payload (in-process state) and is repr-truncated to 200 chars in `format_plan_placeholder`'s \"reviewer_plan verdict parsing\" subsection \u2014 same disclosure surface as v3, just propagated to three edges instead of zero when the rubric-default shape is used. No NEW sink.\n\n4. **Path-traversal / agent-supplied paths (\u00a78):** `verdict_path` construction is unchanged (`outputs_dir / f\"{artifact_id}-reviewer_plan-output.json\"`); still orchestrator-derived from trusted `state_root` + `issue_number/pipeline_id`. The new schema-1 parser preserves strict input sanitisation:\n - `isinstance(blob, dict)` gate before any `.get` access.\n - `top_verdict in {\"ACK\", \"NACK\"}` whitelist before any tracker emission.\n - `if not plan_producers: return verdict_path, {}` fail-safe: a caller that doesn't supply the producer list cannot drive a broadcast.\n - All string fields cast through `str()`, list fields through `list()`, dict comprehension builds typed entries.\n - Empty-`feedback` NACK is given a deterministic placeholder string so `ReviewPayload.validate_nack_has_reason` cannot reject the payload and silently lose the NACK \u2014 closes a class of \"reviewer NACK disappears\" bugs the v3 parser had if the rubric was followed literally.\n\n5. **Uncommitted-artifact / symlink mismatch (\u00a74):** N/A.\n\n6. **Credential-shim modifications (\u00a75):** N/A.\n\n7. **Secret leakage (\u00a76):** Unchanged sinks. The reviewer's `pre_merge_condition` string is also broadcast to every producer edge via the shared `broadcast` dict (`{role.value: broadcast for role in plan_producers}`); pre_merge_condition is a documented bare-string field on `ReviewPayload`, not a credential carrier.\n\n8. **Cross-file OWASP top-10 (\u00a77):** No new sources or sinks. The dict-comprehension shares one `broadcast` dict reference across all producer keys, but `_apply_reviewer_verdicts` only reads from those entries; no downstream mutation that would couple per-edge state. Pure code-quality concern, not security.\n\n### Non-blocking (carried forward where relevant)\n- in_process.py:98 \u2014 `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` remains unreferenced; defer to reviewer_code.\n- _plan_phase.py:288-294 \u2014 `json.loads(verdict_path.read_text(...))` still has no file-size cap; hardening-only observation.\n- _plan_phase.py:343-348 \u2014 the shared `broadcast` dict reference across all producer keys means any future mutation in `_apply_reviewer_verdicts` would silently couple per-edge state. Today's downstream is read-only so this is latent; a follow-up could `copy.deepcopy(broadcast)` per role if mutation becomes warranted. Code-quality / future-proofing only; defer to reviewer_code.\n", - "ack_version": 4 - }, - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:16.177823+00:00", - "phase": "implement" - }, - { - "id": "9cf0bf20-3a20-4c", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:17.972008+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:18.017941+00:00", - "phase": "implement" - }, - { - "id": "5722f951-2bca-4d", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:18.107781+00:00", - "phase": "implement" - }, - { - "id": "d1586d49-1cb2-4d", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from tester for coder", - "body": "\nRe-reviewed coder v4 (commit `ecd8336b7`) after v3 ACK was invalidated by the new proposal. The delta is contained to `_plan_phase.py:read_plan_reviewer_verdicts` (now ~107 lines wider than v3 in the function body + helpers) plus a 4-line ripple in `in_process.py` passing the new `plan_producers` kwarg through. \n\n**My 16/16 tester-authored tests still pass against v4 unchanged.** `ruff format --check .` passes across 962 files; `ruff check` is clean. mypy is green on gateway/shared/sandbox. The schema-acceptance addition is purely additive \u2014 the per_producer wrapper (which my harness-fake path doesn't write) still takes precedence when present, and the rubric-default single-verdict path (which my path doesn't trigger either) is only exercised when the JSON file is actually present on disk. The harness-faked optimistic-ACK fallback I depend on (no verdict file + reviewer exit 0) is preserved at the same call site so my test_plan_stage_reaches_consensus_confirmed_for_each_producer continues to see all 4 plan-team roles in confirmed=True.\n\n### v4 deltas spot-checked\n\n1. **Dual-schema verdict parsing in `read_plan_reviewer_verdicts`**. The function now accepts an optional `plan_producers` kwarg (default `None`). When the top-level JSON has `verdict \u2208 {\"ACK\", \"NACK\"}` it's broadcast to every entry in `plan_producers`; when it has a `per_producer` dict, the per-edge granularity wins. The precedence rule (per_producer takes precedence over a top-level verdict if both present and well-formed) is the right default \u2014 explicit edge granularity should override a broadcast.\n\n2. **`_apply_reviewer_verdicts` propagates `plan_producers`**. The class-level delegate at the call site in `_run_plan_phase_inner` passes the producer role list correctly. Sound.\n\n3. **`feedback` field propagation**. When a top-level NACK has an empty/missing `feedback` field, a synthetic placeholder fires so the tracker's NACK guard doesn't reject the payload. Good defensive shape \u2014 without it a sparse NACK verdict would be silently dropped.\n\n4. **Backward-compat with v3's per_producer schema** confirmed: a reviewer that wrote `{\"per_producer\": {\"architect\": {\"verdict\": \"ACK\"}, ...}}` continues to produce per-edge ACKs. The v4 change is a strict superset.\n\n5. **Rubric alignment**. The rubric the documenter shipped (`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md` \"Verdict JSON shape\", lines 57-80) documents the single top-level `verdict` shape with the 8-criterion analysis block. v4 now accepts the rubric's documented shape; this was the rubric-vs-code drift reviewer_code_holistic v3 flagged. Resolved.\n\n### Non-blocking (carry to follow-up)\n\n- **Schema parsing is loose**. `read_plan_reviewer_verdicts` does isinstance probes on dict / string values; a malformed `feedback` field (e.g., a list of strings instead of a single string) would fall into the synthetic-placeholder branch. A pydantic / dataclass schema check (or a JSON Schema) would surface that as a structured error rather than a silent placeholder substitution. Not blocking because the placeholder body surfaces \"verdict-not-parsed\" on the HITL gate, but worth a follow-up.\n\n- **The rubric body cites the 8 criteria but the parser doesn't verify the analysis block matches the documented criteria set**. A reviewer that wrote `{\"verdict\": \"ACK\", \"analysis\": {\"foo\": true}}` would land as a broadcast-ACK with the analysis blob silently retained. Again: HITL gate sees the placeholder body so the operator catches the discrepancy; not blocking.\n\nAll ACs in the contract task-2-1 / task-2-2 / task-2-5 are satisfied; lint is green; my 16/16 tests pass against v4 with no edits; rubric \u2194 parser symmetry is resolved. ACK.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py" - ], - "reason": "\nRe-reviewed coder v4 (commit `ecd8336b7`) after v3 ACK was invalidated by the new proposal. The delta is contained to `_plan_phase.py:read_plan_reviewer_verdicts` (now ~107 lines wider than v3 in the function body + helpers) plus a 4-line ripple in `in_process.py` passing the new `plan_producers` kwarg through. \n\n**My 16/16 tester-authored tests still pass against v4 unchanged.** `ruff format --check .` passes across 962 files; `ruff check` is clean. mypy is green on gateway/shared/sandbox. The schema-acceptance addition is purely additive \u2014 the per_producer wrapper (which my harness-fake path doesn't write) still takes precedence when present, and the rubric-default single-verdict path (which my path doesn't trigger either) is only exercised when the JSON file is actually present on disk. The harness-faked optimistic-ACK fallback I depend on (no verdict file + reviewer exit 0) is preserved at the same call site so my test_plan_stage_reaches_consensus_confirmed_for_each_producer continues to see all 4 plan-team roles in confirmed=True.\n\n### v4 deltas spot-checked\n\n1. **Dual-schema verdict parsing in `read_plan_reviewer_verdicts`**. The function now accepts an optional `plan_producers` kwarg (default `None`). When the top-level JSON has `verdict \u2208 {\"ACK\", \"NACK\"}` it's broadcast to every entry in `plan_producers`; when it has a `per_producer` dict, the per-edge granularity wins. The precedence rule (per_producer takes precedence over a top-level verdict if both present and well-formed) is the right default \u2014 explicit edge granularity should override a broadcast.\n\n2. **`_apply_reviewer_verdicts` propagates `plan_producers`**. The class-level delegate at the call site in `_run_plan_phase_inner` passes the producer role list correctly. Sound.\n\n3. **`feedback` field propagation**. When a top-level NACK has an empty/missing `feedback` field, a synthetic placeholder fires so the tracker's NACK guard doesn't reject the payload. Good defensive shape \u2014 without it a sparse NACK verdict would be silently dropped.\n\n4. **Backward-compat with v3's per_producer schema** confirmed: a reviewer that wrote `{\"per_producer\": {\"architect\": {\"verdict\": \"ACK\"}, ...}}` continues to produce per-edge ACKs. The v4 change is a strict superset.\n\n5. **Rubric alignment**. The rubric the documenter shipped (`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md` \"Verdict JSON shape\", lines 57-80) documents the single top-level `verdict` shape with the 8-criterion analysis block. v4 now accepts the rubric's documented shape; this was the rubric-vs-code drift reviewer_code_holistic v3 flagged. Resolved.\n\n### Non-blocking (carry to follow-up)\n\n- **Schema parsing is loose**. `read_plan_reviewer_verdicts` does isinstance probes on dict / string values; a malformed `feedback` field (e.g., a list of strings instead of a single string) would fall into the synthetic-placeholder branch. A pydantic / dataclass schema check (or a JSON Schema) would surface that as a structured error rather than a silent placeholder substitution. Not blocking because the placeholder body surfaces \"verdict-not-parsed\" on the HITL gate, but worth a follow-up.\n\n- **The rubric body cites the 8 criteria but the parser doesn't verify the analysis block matches the documented criteria set**. A reviewer that wrote `{\"verdict\": \"ACK\", \"analysis\": {\"foo\": true}}` would land as a broadcast-ACK with the analysis blob silently retained. Again: HITL gate sees the placeholder body so the operator catches the discrepancy; not blocking.\n\nAll ACs in the contract task-2-1 / task-2-2 / task-2-5 are satisfied; lint is green; my 16/16 tests pass against v4 with no edits; rubric \u2194 parser symmetry is resolved. ACK.\n", - "ack_version": 4 - }, - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:18.280849+00:00", - "phase": "implement" - }, - { - "id": "bfa55c15-5bbd-4d", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "tester", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:18.280975+00:00", - "phase": "implement" - }, - { - "id": "9d6aec21-06bd-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:20.636469+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:20.665723+00:00", - "phase": "implement" - }, - { - "id": "d8bd6b97-563a-45", - "pipeline_id": "issue-2717", - "from_role": "documenter", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:34:10.517501+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:22.438744+00:00", - "phase": "implement" - }, - { - "id": "b64f54d3-0cee-4e", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:23.248175+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:23.288034+00:00", - "phase": "implement" - }, - { - "id": "cb00829d-389d-47", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:23.365660+00:00", - "phase": "implement" - }, - { - "id": "822431cb-dbf6-4c", - "pipeline_id": "issue-2717", - "from_role": "tester", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_NACK,CONSENSUS_ACK,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:25.979157+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:26.074663+00:00", - "phase": "implement" - }, - { - "id": "033d3b5a-8d22-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_contract for coder", - "body": "\nRe-review of coder v4 (commit ecd8336b7) after CONSENSUS_RE_REVIEW signal. v4 is a focused, additive change to `read_plan_reviewer_verdicts` that resolves reviewer_code_holistic v3 blocker H3 \u2014 the reviewer_plan rubric ships a single-verdict top-level JSON shape, but v2/v3's parser only understood the `per_producer` wrapper, so a rubric-following NACK would silently miss the tracker. v4 broadens the parser to accept BOTH schemas with `per_producer` taking precedence when present.\n\nRe-checked all three coder-owned task ACs against the v4 commit: nothing in v4 regresses any AC.\n\n### Per-task verification (v4)\n\n**TASK-2-1 \u2014 `_run_plan_phase` end-to-end** (orchestrator/substrate/in_process.py + orchestrator/substrate/_plan_phase.py):\n\n1. AC \"no longer raises NotImplementedError when the operator advances past refine\": \u2705 Unchanged in v4. `run()` at in_process.py:246 still calls `self._run_plan_phase(...)`; the walking-skeleton fence still fires only on the plan HITL gate's `approve_continue` (slice-3 / slice-4 pointer intact).\n\n2. AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705 Unchanged in v4. Architect-first synchronous spawn \u2192 `task_planner + risk_analyst` concurrent fan-out via `ThreadPoolExecutor(max_workers=2)` (_plan_phase.py:124-161, unchanged in v4). Role-dependency-driven deviation from literal \"3 concurrent\" is still grounded in `shared/egg_contracts/agent_roles.py` declaring ARCHITECT as the sole dependency of TASK_PLANNER / RISK_ANALYST.\n\n3. AC \"reviewer_plan is spawned after each CONSENSUS_PROPOSE\": \u2705 Materially strengthened in v4. Reviewer dispatch and tracker advancement structure unchanged; the verdict-parsing layer now correctly recognises the rubric-default shape. A rubric-following `verdict: \"NACK\"` no longer falls into the optimistic-ACK fallback that masked NACKs from the operator at the plan HITL gate (v3 silent bug). The NACK now broadcasts to every producer edge with `feedback` propagated as each edge's `reason` (_plan_phase.py:325-351) and an explicit synthetic placeholder when `feedback` is empty to avoid hitting `ReviewPayload.validate_nack_has_reason`. Per-edge ACK / NACK still drives `tracker.handle_ack` / `tracker.handle_nack` per producer.\n\n4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge\": \u2705 Unchanged in v4. `tracker.handle_confirmed` for each role; `evaluate()` snapshot; `_build_plan_gate_decision` yields HITLDecision with `phase=\"plan\"`. The schema-broadening at the parsing layer cannot regress the CONSENSUS_CONFIRMED path because (a) ACK still acks all three on the broadcast path \u2192 CONSENSUS_CONFIRMED reachable; (b) NACK paths surface in the eval snapshot's `blocking_agents` exactly as before \u2014 the only difference is they now surface for rubric-default JSON shapes too, which is a correctness improvement.\n\n5. AC \"existing refine path still works\": \u2705 Unchanged. Refine flow at in_process.py:213-240 untouched in v4.\n\n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py): unchanged in v4. ACs remain met.\n\n**TASK-2-5 \u2014 sandbox restrictions parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py): unchanged in v4 (still no edits). The \"R2 = pass \u2192 no-op\" close remains the operative decision; coder commit message lineage preserves the required close-with-note.\n\n### v4 surface-area assessment (informational)\n\n- The `read_plan_reviewer_verdicts(runner, *, plan_producers=None)` signature change is backward-compatible (kwarg with `None` default), and the `_read_plan_reviewer_verdicts` class-method delegate on `_InProcessOrchestrator` propagates the new kwarg with the same default. The tester's `test_inprocess_plan_brc.py` does not call this method directly (it inspects `_plan_tracker.evaluate()` after the stage runs), so the existing 16 passing test cases remain intact.\n- Legacy-caller safety: when `plan_producers=None` and the JSON is single-verdict, the function returns `({}, verdict_path)` and the orchestrator's fail-closed / optimistic-ACK heuristic in `_apply_reviewer_verdicts` applies \u2014 preserves the historical behaviour for any out-of-tree caller.\n- Schema 2 (per_producer wrapper) still takes precedence when present and well-formed (_plan_phase.py:301-317), so an explicit per-edge verdict reviewer is not surprised by silently-broadcast behaviour.\n\n### Non-blocking observations carried forward from v3 review\n\n- Slice-1 contract bookkeeping (`task-1-1` \u2026 `task-1-9` show `status: \"pending\"` despite linked commits) \u2014 informational; operator reconcile before declaring the rollout complete.\n- `synthetic_commit_for(role)` SHA-1-derived prefix at _plan_phase.py:644-656 is unchanged; per-role distinguishability holds.\n- Schema-1 NACK reason placeholder (\"reviewer_plan broadcast NACK: top-level verdict without a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed analysis\") is operator-readable and explicit; if a future regression test wants to pin the exact substring, the runner's `_verdict_diagnostics` dict is the structured surface to assert against.\n\nMarking v4 ACKed.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py" - ], - "reason": "\nRe-review of coder v4 (commit ecd8336b7) after CONSENSUS_RE_REVIEW signal. v4 is a focused, additive change to `read_plan_reviewer_verdicts` that resolves reviewer_code_holistic v3 blocker H3 \u2014 the reviewer_plan rubric ships a single-verdict top-level JSON shape, but v2/v3's parser only understood the `per_producer` wrapper, so a rubric-following NACK would silently miss the tracker. v4 broadens the parser to accept BOTH schemas with `per_producer` taking precedence when present.\n\nRe-checked all three coder-owned task ACs against the v4 commit: nothing in v4 regresses any AC.\n\n### Per-task verification (v4)\n\n**TASK-2-1 \u2014 `_run_plan_phase` end-to-end** (orchestrator/substrate/in_process.py + orchestrator/substrate/_plan_phase.py):\n\n1. AC \"no longer raises NotImplementedError when the operator advances past refine\": \u2705 Unchanged in v4. `run()` at in_process.py:246 still calls `self._run_plan_phase(...)`; the walking-skeleton fence still fires only on the plan HITL gate's `approve_continue` (slice-3 / slice-4 pointer intact).\n\n2. AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705 Unchanged in v4. Architect-first synchronous spawn \u2192 `task_planner + risk_analyst` concurrent fan-out via `ThreadPoolExecutor(max_workers=2)` (_plan_phase.py:124-161, unchanged in v4). Role-dependency-driven deviation from literal \"3 concurrent\" is still grounded in `shared/egg_contracts/agent_roles.py` declaring ARCHITECT as the sole dependency of TASK_PLANNER / RISK_ANALYST.\n\n3. AC \"reviewer_plan is spawned after each CONSENSUS_PROPOSE\": \u2705 Materially strengthened in v4. Reviewer dispatch and tracker advancement structure unchanged; the verdict-parsing layer now correctly recognises the rubric-default shape. A rubric-following `verdict: \"NACK\"` no longer falls into the optimistic-ACK fallback that masked NACKs from the operator at the plan HITL gate (v3 silent bug). The NACK now broadcasts to every producer edge with `feedback` propagated as each edge's `reason` (_plan_phase.py:325-351) and an explicit synthetic placeholder when `feedback` is empty to avoid hitting `ReviewPayload.validate_nack_has_reason`. Per-edge ACK / NACK still drives `tracker.handle_ack` / `tracker.handle_nack` per producer.\n\n4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge\": \u2705 Unchanged in v4. `tracker.handle_confirmed` for each role; `evaluate()` snapshot; `_build_plan_gate_decision` yields HITLDecision with `phase=\"plan\"`. The schema-broadening at the parsing layer cannot regress the CONSENSUS_CONFIRMED path because (a) ACK still acks all three on the broadcast path \u2192 CONSENSUS_CONFIRMED reachable; (b) NACK paths surface in the eval snapshot's `blocking_agents` exactly as before \u2014 the only difference is they now surface for rubric-default JSON shapes too, which is a correctness improvement.\n\n5. AC \"existing refine path still works\": \u2705 Unchanged. Refine flow at in_process.py:213-240 untouched in v4.\n\n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py): unchanged in v4. ACs remain met.\n\n**TASK-2-5 \u2014 sandbox restrictions parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py): unchanged in v4 (still no edits). The \"R2 = pass \u2192 no-op\" close remains the operative decision; coder commit message lineage preserves the required close-with-note.\n\n### v4 surface-area assessment (informational)\n\n- The `read_plan_reviewer_verdicts(runner, *, plan_producers=None)` signature change is backward-compatible (kwarg with `None` default), and the `_read_plan_reviewer_verdicts` class-method delegate on `_InProcessOrchestrator` propagates the new kwarg with the same default. The tester's `test_inprocess_plan_brc.py` does not call this method directly (it inspects `_plan_tracker.evaluate()` after the stage runs), so the existing 16 passing test cases remain intact.\n- Legacy-caller safety: when `plan_producers=None` and the JSON is single-verdict, the function returns `({}, verdict_path)` and the orchestrator's fail-closed / optimistic-ACK heuristic in `_apply_reviewer_verdicts` applies \u2014 preserves the historical behaviour for any out-of-tree caller.\n- Schema 2 (per_producer wrapper) still takes precedence when present and well-formed (_plan_phase.py:301-317), so an explicit per-edge verdict reviewer is not surprised by silently-broadcast behaviour.\n\n### Non-blocking observations carried forward from v3 review\n\n- Slice-1 contract bookkeeping (`task-1-1` \u2026 `task-1-9` show `status: \"pending\"` despite linked commits) \u2014 informational; operator reconcile before declaring the rollout complete.\n- `synthetic_commit_for(role)` SHA-1-derived prefix at _plan_phase.py:644-656 is unchanged; per-role distinguishability holds.\n- Schema-1 NACK reason placeholder (\"reviewer_plan broadcast NACK: top-level verdict without a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed analysis\") is operator-readable and explicit; if a future regression test wants to pin the exact substring, the runner's `_verdict_diagnostics` dict is the structured surface to assert against.\n\nMarking v4 ACKed.\n", - "ack_version": 4 - }, - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:27.520897+00:00", - "phase": "implement" - }, - { - "id": "d2e24485-f3c6-40", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:28.011717+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:28.213822+00:00", - "phase": "implement" - }, - { - "id": "ea2a3991-3c23-49", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:28.520793+00:00", - "phase": "implement" - }, - { - "id": "ed925f8e-7bf1-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:30.366283+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:30.427560+00:00", - "phase": "implement" - }, - { - "id": "089173ae-c460-45", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:34.058965+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:34.130279+00:00", - "phase": "implement" - }, - { - "id": "af593ea0-034b-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T20:35:39.094304+00:00", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:39.189139+00:00", - "phase": "implement" - }, - { - "id": "8d8ac0a2-46ec-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "coder", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_code_holistic for coder", - "body": "\nHolistic ACK on coder slice-2 v4 (commit ecd8336b7). v3 blocker H3 is resolved: `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default single-verdict schema (broadcast to every producer edge) AND the per-producer wrapper (per-edge granularity), with the per-producer wrapper taking precedence when present. This is \"Option (c)\" from my v3 NACK with the per-producer wrapper retained as the documented extension shape \u2014 both code-paths reach the same `normalised` dict shape, so `_apply_reviewer_verdicts` and the rest of the BRC flow are unchanged.\n\nRe-ran all four mandatory passes against v4:\n\n- **Pass 1 (end-to-end primary use case):** Walked the path \u2014 refiner \u2192 \"approve_continue\" \u2192 architect synchronously \u2192 task_planner + risk_analyst fan-out \u2192 reviewer_plan \u2192 verdict-JSON parse \u2192 tracker per-edge ACK/NACK \u2192 plan-HITL yield. A rubric-following reviewer's NACK now propagates to the tracker. The `if not plan_producers: return verdict_path, {}` legacy-caller guard (line for the broadcast path) means tester tests that call `_read_plan_reviewer_verdicts()` without the kwarg still get the empty-dict + optimistic-ACK fallback \u2014 backwards compatible with the tester's existing assertions.\n- **Pass 2 (doc \u2194 code symmetry):** The rubric's documented JSON shape (`{\"verdict\": \"ACK\"|\"NACK\", \"analysis\": {...}, \"feedback\": ...}`) is now the primary parsed schema. The rubric's claim that the reviewer \"ACK / NACK each producer independently\" is weakened to \"ACK / NACK all producers as a unit, with optional per-producer extension\"; the per_producer wrapper is the explicit opt-in for the original per-edge granularity. The documenter may want to add a short note in `reviewer_plan.md` that the per_producer wrapper is the per-edge form (non-blocking; ACK does not depend on the documenter coordinating this turn).\n- **Pass 3 (synthetic key / sentinel):** No new synthetic keys. The empty-feedback synthetic-placeholder NACK reason (lines: `f\"reviewer_plan broadcast {top_verdict}: top-level verdict without a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed analysis.\"`) plugs the gap where `ReviewPayload.validate_nack_has_reason` would otherwise reject a NACK with empty feedback \u2014 i.e. a real \"doc-claimed schema \u2192 tracker rejection \u2192 silent fallback\" path that v4 prevents.\n- **Pass 4 (silent fallback):** Verdict-typo silently falls back to optimistic-ACK if reviewer exit 0 (e.g. `{\"verdict\": \"OK\"}` returns empty verdicts \u2192 optimistic-ACK fires). Borderline; the placeholder body's verdict_path line surfaces the source file so an operator hitting the issue can `cat` the file and see the typo. Not worth blocking \u2014 and would be caught by the tester's planned v2 fixture exercising the new parser anyway.\n\n### Non-blocking\n\n- **Rubric needs a one-line note that `per_producer` is the per-edge extension.** Documenter-side. The reviewer rubric currently documents only the single-verdict shape; the per_producer wrapper is supported by code but undocumented for the reviewer-agent reader. A future reviewer might never know per-edge granularity is available.\n- **`if not plan_producers: return verdict_path, {}` swallows a legitimate single-verdict NACK** when callers omit the kwarg. Today the only caller that omits it is the tester's existing class-method call site, which feeds the empty-verdicts result into the harness-fake path anyway. Worth a one-line docstring note that omitting `plan_producers` disables single-verdict broadcasting so a future caller doesn't mistakenly omit the kwarg and lose the reviewer's NACK.\n- **Verdict-typo (e.g. `{\"verdict\": \"OK\"}`) silently optimistic-ACKs on reviewer exit 0.** Surfaced in the placeholder, but a `log_tracker_warning(verb=\"unknown_verdict\", role_label=top_verdict, ...)`-equivalent on the unrecognised-verdict path would make the typo show up in the orchestrator log too. Low-priority polish.\n\nH1 (architect-first ordering), H2 (parsed verdict not exit-code-only), H3 (rubric-schema parser): all three blockers resolved. Non-blocking items above are polish; ACKing so the BRC barrier opens.\n", - "metadata": { - "payload": { - "artifact_references": [ - "orchestrator/substrate/_plan_phase.py", - "orchestrator/substrate/in_process.py", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md" - ], - "reason": "\nHolistic ACK on coder slice-2 v4 (commit ecd8336b7). v3 blocker H3 is resolved: `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default single-verdict schema (broadcast to every producer edge) AND the per-producer wrapper (per-edge granularity), with the per-producer wrapper taking precedence when present. This is \"Option (c)\" from my v3 NACK with the per-producer wrapper retained as the documented extension shape \u2014 both code-paths reach the same `normalised` dict shape, so `_apply_reviewer_verdicts` and the rest of the BRC flow are unchanged.\n\nRe-ran all four mandatory passes against v4:\n\n- **Pass 1 (end-to-end primary use case):** Walked the path \u2014 refiner \u2192 \"approve_continue\" \u2192 architect synchronously \u2192 task_planner + risk_analyst fan-out \u2192 reviewer_plan \u2192 verdict-JSON parse \u2192 tracker per-edge ACK/NACK \u2192 plan-HITL yield. A rubric-following reviewer's NACK now propagates to the tracker. The `if not plan_producers: return verdict_path, {}` legacy-caller guard (line for the broadcast path) means tester tests that call `_read_plan_reviewer_verdicts()` without the kwarg still get the empty-dict + optimistic-ACK fallback \u2014 backwards compatible with the tester's existing assertions.\n- **Pass 2 (doc \u2194 code symmetry):** The rubric's documented JSON shape (`{\"verdict\": \"ACK\"|\"NACK\", \"analysis\": {...}, \"feedback\": ...}`) is now the primary parsed schema. The rubric's claim that the reviewer \"ACK / NACK each producer independently\" is weakened to \"ACK / NACK all producers as a unit, with optional per-producer extension\"; the per_producer wrapper is the explicit opt-in for the original per-edge granularity. The documenter may want to add a short note in `reviewer_plan.md` that the per_producer wrapper is the per-edge form (non-blocking; ACK does not depend on the documenter coordinating this turn).\n- **Pass 3 (synthetic key / sentinel):** No new synthetic keys. The empty-feedback synthetic-placeholder NACK reason (lines: `f\"reviewer_plan broadcast {top_verdict}: top-level verdict without a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed analysis.\"`) plugs the gap where `ReviewPayload.validate_nack_has_reason` would otherwise reject a NACK with empty feedback \u2014 i.e. a real \"doc-claimed schema \u2192 tracker rejection \u2192 silent fallback\" path that v4 prevents.\n- **Pass 4 (silent fallback):** Verdict-typo silently falls back to optimistic-ACK if reviewer exit 0 (e.g. `{\"verdict\": \"OK\"}` returns empty verdicts \u2192 optimistic-ACK fires). Borderline; the placeholder body's verdict_path line surfaces the source file so an operator hitting the issue can `cat` the file and see the typo. Not worth blocking \u2014 and would be caught by the tester's planned v2 fixture exercising the new parser anyway.\n\n### Non-blocking\n\n- **Rubric needs a one-line note that `per_producer` is the per-edge extension.** Documenter-side. The reviewer rubric currently documents only the single-verdict shape; the per_producer wrapper is supported by code but undocumented for the reviewer-agent reader. A future reviewer might never know per-edge granularity is available.\n- **`if not plan_producers: return verdict_path, {}` swallows a legitimate single-verdict NACK** when callers omit the kwarg. Today the only caller that omits it is the tester's existing class-method call site, which feeds the empty-verdicts result into the harness-fake path anyway. Worth a one-line docstring note that omitting `plan_producers` disables single-verdict broadcasting so a future caller doesn't mistakenly omit the kwarg and lose the reviewer's NACK.\n- **Verdict-typo (e.g. `{\"verdict\": \"OK\"}`) silently optimistic-ACKs on reviewer exit 0.** Surfaced in the placeholder, but a `log_tracker_warning(verb=\"unknown_verdict\", role_label=top_verdict, ...)`-equivalent on the unrecognised-verdict path would make the typo show up in the orchestrator log too. Low-priority polish.\n\nH1 (architect-first ordering), H2 (parsed verdict not exit-code-only), H3 (rubric-schema parser): all three blockers resolved. Non-blocking items above are polish; ACKing so the BRC barrier opens.\n", - "ack_version": 4 - }, - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:52.749382+00:00", - "phase": "implement" - }, - { - "id": "c6ddeae7-7aae-4b", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "coder", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 4) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 4, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:52.749604+00:00", - "phase": "implement" - }, - { - "id": "6a7417b9-dd51-48", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:52.808077+00:00", - "phase": "implement" - }, - { - "id": "4cfd7c34-845d-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code_holistic", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_code_holistic", - "body": "", - "metadata": { - "consensus_reached": false, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:55.098498+00:00", - "phase": "implement" - }, - { - "id": "1b9b7bcd-fa04-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_concurrency", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:55.294352+00:00", - "phase": "implement" - }, - { - "id": "3197cf38-84f9-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_code", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:55.313665+00:00", - "phase": "implement" - }, - { - "id": "033abcb5-9cc6-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_security", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:55.316989+00:00", - "phase": "implement" - }, - { - "id": "68fe0ba0-4e88-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_contract", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING", - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:55.325633+00:00", - "phase": "implement" - }, - { - "id": "f1ccbcf4-eaa6-43", - "pipeline_id": "issue-2717", - "from_role": "coder", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by coder", - "body": "", - "metadata": { - "consensus_reached": true, - "slice_id": "slice-2" - }, - "timestamp": "2026-05-19T20:35:56.587952+00:00", - "phase": "implement" - } -] \ No newline at end of file diff --git a/.egg-state/brc-history/2717-implement-slice-2.md b/.egg-state/brc-history/2717-implement-slice-2.md deleted file mode 100644 index 0849379ef7..0000000000 --- a/.egg-state/brc-history/2717-implement-slice-2.md +++ /dev/null @@ -1,10030 +0,0 @@ -# BRC Consensus History — implement phase, slice-2 - -Generated: 2026-05-19T20:35:56Z -Pipeline: issue-2717 -Slice: slice-2 - -### [2026-05-19T19:39:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: cb5e45c7-e6a0-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:39:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -reviewer_security ready, fetched origin and synced to slice-2 head; polling for CONSENSUS_PROPOSE from coder (already PROPOSED) and tester. - -````yaml -id: b688d494-0f7c-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - slice_id: slice-2 -```` - -### [2026-05-19T19:39:24Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 5dcff24e-8225-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:24.120054+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:40:04Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 874fa21d-052c-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:40:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bef6f5c3-e49d-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:40:04Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1fe3e739-6cf8-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:40:24Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 5f65c25f-d619-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:40:27Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9de37278-5436-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:27.392669+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:40:51Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: be91b666-279e-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:51.121097+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:41:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: a52cda7a-a9e8-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:41:04Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 46f162fb-c572-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:41:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bff4df5b-7d37-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:41:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 283f570f-17f7-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:41:27Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b8c58a00-194c-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:27.392669+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:41:51Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bc26e8ce-ef04-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:51.121097+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:42:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2df0d298-f629-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:42:04Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2bfe6081-9e2c-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:42:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e7a6dac5-7045-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:42:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 84a135fa-b610-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:42:27Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 68207473-f343-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:27.392669+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:42:51Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: af33ffe3-9537-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:51.121097+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:43:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9d4c1ee2-4661-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:43:04Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 32b6b7d1-808f-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:43:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bd88de91-09ee-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:43:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 91f69dd9-a7df-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:43:51Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 67be32b0-c64a-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:51.121097+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:44:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8fc1b8b7-0d0c-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:44:04Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c37eaed5-f6ed-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:44:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b1e55b0c-4149-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:44:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3627d147-7325-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:44:51Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: fc216257-38c9-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:51.121097+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:45:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e84fdf8b-1aed-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:45:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: eff3c61e-9940-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:45:04Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0c10a0fa-30a0-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:45:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bb3926ba-04d4-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:45:51Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8c4e9010-03bb-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:51.121097+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:46:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 23753156-5d71-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:46:04Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0ddc5e43-4f0f-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:46:04Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 67bc7d46-9a8d-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:46:08Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 2fd73284-1738-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:46:08.624715+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:46:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 74abad3c-3a18-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:47:07Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7fb3ca8c-281e-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:39:44.960717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:47:07Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: ea5ce4f7-6b6e-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:38:59.274301+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:47:07Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c5d2b7f3-8675-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:01.200359+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:47:08Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: dfeb0cc8-efe1-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:46:08.624715+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:47:25Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 97895310-e7a6-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:40:24.907002+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:47:26Z] documenter (CONSENSUS_PROPOSE): Proposal from documenter - -Slice-2 documenter v1: land plan-team rubrics + plan-phase SKILL.md updates. - -task-2-3: Created four plan-team agent rubric files under plugins/egg-sdlc/skills/egg-sdlc/agents/ — architect.md, task_planner.md, risk_analyst.md, reviewer_plan.md — using underscore-style file names matching the loader at orchestrator/substrate/__init__.py:280-365. Each rubric has valid frontmatter (name, description) and a body that (a) mirrors the corresponding k3s-substrate prompt body from plugins/refine-plan/skills/refine-plan/agents/ (the substrate swap is structurally invisible to the role) and (b) follows the substrate-specific-notes shape from slice-1's reviewer_refine.md / reviewer_agent_design.md (worktree layout under EGG_WORKTREE_BASE, PreToolUse hook enforcement via build_agent_patterns, HITL-via-AskUserQuestion, concurrent peers in slice-2, output path stability). Each rubric explicitly references the substrate context ("You are the **architect** running on the **Claude Code substrate** of egg's SDLC pipeline.") and the role's specific outputs (architect → architecture analysis JSON; task_planner → slice DAG plan + YAML appendix; risk_analyst → risk register JSON; reviewer_plan → ACK/NACK on the three producer artifacts). - -task-2-7: Updated plugins/egg-sdlc/skills/egg-sdlc/SKILL.md to document the plan phase as supported. Specifically: (1) bumped the rollout-status callout from "slice 1 landed" to "slices 1 + 2 landed" and enumerated both the refine and plan rosters; (2) replaced the "Plan / implement / pr deferred" bullet in "What's NOT in this skill" with a dedicated "Plan phase" subsection naming the four roles, their spawn order (architect solo first, then task_planner + risk_analyst concurrently, with reviewer_plan ACK/NACK on each producer edge), output paths, and the four plan-HITL gate options (approve / request_changes / change_approach / stop); (3) restructured the 8-step skill flow into 10 steps that walk the plan stage and re-position the phase fence on "approve and continue to implement" with a pointer to slice 3; (4) refreshed stale "refine-only" / "refine-team subagents" / artifact-path / failure-mode strings to cover both phases. - -Both files are under the documenter's gateway-allowed pattern set (docs/, **/*.md, **/README.md). No tests were authored by this role — tester role owns integration_tests/regression/test_inprocess_plan_brc.py (task-2-4) and the in-process orchestrator's _run_plan stage / loader expansion are coder territory (task-2-1, task-2-2). - -````yaml -id: 71e86885-b8a1-4d -phase: implement -metadata: - payload: - summary: "Slice-2 documenter v1: land plan-team rubrics + plan-phase SKILL.md\ - \ updates.\n\ntask-2-3: Created four plan-team agent rubric files under plugins/egg-sdlc/skills/egg-sdlc/agents/\ - \ \u2014 architect.md, task_planner.md, risk_analyst.md, reviewer_plan.md \u2014\ - \ using underscore-style file names matching the loader at orchestrator/substrate/__init__.py:280-365.\ - \ Each rubric has valid frontmatter (name, description) and a body that (a)\ - \ mirrors the corresponding k3s-substrate prompt body from plugins/refine-plan/skills/refine-plan/agents/\ - \ (the substrate swap is structurally invisible to the role) and (b) follows\ - \ the substrate-specific-notes shape from slice-1's reviewer_refine.md / reviewer_agent_design.md\ - \ (worktree layout under EGG_WORKTREE_BASE, PreToolUse hook enforcement via\ - \ build_agent_patterns, HITL-via-AskUserQuestion, concurrent peers in slice-2,\ - \ output path stability). Each rubric explicitly references the substrate context\ - \ (\"You are the **architect** running on the **Claude Code substrate** of egg's\ - \ SDLC pipeline.\") and the role's specific outputs (architect \u2192 architecture\ - \ analysis JSON; task_planner \u2192 slice DAG plan + YAML appendix; risk_analyst\ - \ \u2192 risk register JSON; reviewer_plan \u2192 ACK/NACK on the three producer\ - \ artifacts).\n\ntask-2-7: Updated plugins/egg-sdlc/skills/egg-sdlc/SKILL.md\ - \ to document the plan phase as supported. Specifically: (1) bumped the rollout-status\ - \ callout from \"slice 1 landed\" to \"slices 1 + 2 landed\" and enumerated\ - \ both the refine and plan rosters; (2) replaced the \"Plan / implement / pr\ - \ deferred\" bullet in \"What's NOT in this skill\" with a dedicated \"Plan\ - \ phase\" subsection naming the four roles, their spawn order (architect solo\ - \ first, then task_planner + risk_analyst concurrently, with reviewer_plan ACK/NACK\ - \ on each producer edge), output paths, and the four plan-HITL gate options\ - \ (approve / request_changes / change_approach / stop); (3) restructured the\ - \ 8-step skill flow into 10 steps that walk the plan stage and re-position the\ - \ phase fence on \"approve and continue to implement\" with a pointer to slice\ - \ 3; (4) refreshed stale \"refine-only\" / \"refine-team subagents\" / artifact-path\ - \ / failure-mode strings to cover both phases.\n\nBoth files are under the documenter's\ - \ gateway-allowed pattern set (docs/, **/*.md, **/README.md). No tests were\ - \ authored by this role \u2014 tester role owns integration_tests/regression/test_inprocess_plan_brc.py\ - \ (task-2-4) and the in-process orchestrator's _run_plan stage / loader expansion\ - \ are coder territory (task-2-1, task-2-2)." - attestation: {} - artifacts: - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - risk_considered: 'Risk: rubric file names use hyphens vs underscores. Mitigation: - confirmed the loader at orchestrator/substrate/__init__.py:331 reads `agents/{role_name}.md` - where role_name is the underscore-style enum value (e.g. task_planner, not task-planner), - and _ROLE_RUBRIC_SLICES uses the underscore form. Files use underscores. Risk: - rubric body might drift from k3s prompts. Mitigation: bodies were authored to - mirror the corresponding refine-plan/skills/refine-plan/agents/{role}.md verbatim - where possible (the substrate swap is structurally invisible per the slice''s - premise), with only the substrate-specific notes section replaced to match the - slice-1 reviewer_refine.md / reviewer_agent_design.md shape. Risk: SKILL.md - still references "refine only" in stale spots. Mitigation: grep-checked all - "refine" mentions and updated the rollout callout, what-this-gets-you list, - step flow, pending_hitl.result semantics, failure-modes section, and "What''s - NOT in this skill" header text.' - commit_sha: 7122ca2d11d584068ae0f0c39b92c586e81c9d9e - files_changed: - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - tests_run: [] - tasks_satisfied: - - task-2-3 - - task-2-7 - version: 1 - commit_sha: 7122ca2d11d584068ae0f0c39b92c586e81c9d9e - slice_id: slice-2 -```` - -### [2026-05-19T19:47:26Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a60805df-29e3-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:47:26Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a7b21c91-8482-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:47:26Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 397e340b-149c-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:47:26Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 0eb7e144-b3fe-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:47:26Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: e518869b-3b4e-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:47:47Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9394b2ec-3a0a-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:47:47Z] documenter (HEARTBEAT): heartbeat: WAITING_ON_ROLE - -````yaml -id: 089aa004-5478-4c -phase: implement -metadata: - state: WAITING_ON_ROLE - waiting_on: reviewer_code - slice_id: slice-2 -```` - -### [2026-05-19T19:47:48Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 2e2014f0-e529-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:48.339577+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:48:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f8e183c3-d6c7-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:48:48Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: a5b442ea-a4a8-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:48:48Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b41e4f83-9509-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:48.339577+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:48:54Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: b49c7556-3233-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:48:58Z] tester (HEARTBEAT): heartbeat: WAITING_ON_ROLE - -tester slice-2: scaffolded test_inprocess_plan_brc.py, waiting on coder CONSENSUS_PROPOSE for task-2-1 (_run_plan) and task-2-2 (rubric loader expansion) to validate test assertions against the actual implementation. - -````yaml -id: d18bf9df-5a38-44 -phase: implement -metadata: - state: WAITING_ON_ROLE - waiting_on: coder - slice_id: slice-2 -```` - -### [2026-05-19T19:49:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1fecbd1a-8226-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:49:17Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0c055b2a-be46-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:17.619085+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:49:48Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d76a4a3d-1cdc-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:49:48Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 35f0634d-8d17-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:48.339577+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:49:49Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 07300379-8495-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:49.537771+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:49:54Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 9fd88587-8176-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:50:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 40d6f1c0-5b36-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:50:17Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: decba16e-2eb4-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:17.619085+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:50:48Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1779f429-a65f-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:50:48Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ca389f26-a98a-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:48.339577+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:50:49Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1c73db17-bf37-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:49.537771+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:50:54Z] reviewer_code → documenter (CONSENSUS_ACK): ACK from reviewer_code for documenter - - -Reviewed 5 documentation files for task-2-3 (four plan-team rubrics) and task-2-7 (SKILL.md plan-phase section). Verified acceptance criteria for both tasks, body fidelity against the k3s-substrate counterparts in `plugins/refine-plan/skills/refine-plan/agents/`, link integrity, frontmatter validity, and loader-name alignment. - -### File-by-file analysis - -**plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md** (new, 88 lines) — Frontmatter is informational-only per the slice-1 convention. Body opens with "You are the **architect** running on the **Claude Code substrate**...", explicitly meeting task-2-3's substrate-context requirement. The output JSON schema (`approach_summary`, `key_design_decisions`, `components_touched`, `ordering_constraints`, `open_questions_for_planner`) matches the k3s counterpart byte-for-byte. The "What you do" section adds the missing-from-k3s "You run first, solo, before `task_planner` and `risk_analyst`" sequencing clue, which is consistent with the SKILL.md narrative. The four substrate-specific notes (worktree, file-write restrictions, HITL, concurrent peers, output path stability) match the slice-1 pattern from `reviewer_refine.md` and `reviewer_agent_design.md`. Relative link `../../../../docs/architecture/claude-code-substrate.md` resolves correctly to `docs/architecture/claude-code-substrate.md`. - -**plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md** (new, 294 lines) — Body opens with "You are the **task_planner** running on the **Claude Code substrate**...". The full `[mode: ticket]` / `[mode: github_issue]` / `[mode: epic-fresh]` / `[mode: epic-reassess]` mode-switch block is preserved verbatim from the k3s version with light editorial trimming (Won't-Do comment template removed, Plan diff example reduced to a single sentence describing the cluster groups). The YAML appendix discipline section (block scalars, role mapping, `pr:` block requirements, DAG-is-a-forest rule) is intact. The output JSON schema (`plan_path`, `slice_count`, `task_count`, `roles_used`, `dag_shape_summary`, `critical_path_tasks`) matches the k3s counterpart. Substrate-specific notes match the slice-1 pattern. - -**plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md** (new, 99 lines) — Body opens with "You are the **risk_analyst** running on the **Claude Code substrate**...". The risk-record schema (`risks[].{name,category,likelihood,impact,evidence,mitigation,owns_task}` + `top_3_risks` + `blocking_concerns`) is preserved. The evidence and mitigation discipline sections are intact. The "Concurrent peers in this slice" note correctly explains the reviewer_plan reconciliation behavior when task_planner finalizes before risk_analyst's handoff is visible — useful guardrail. - -**plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md** (new, 102 lines) — Body opens with "You are the **reviewer_plan** running on the **Claude Code substrate**...". All eight review criteria (`alignment_with_analysis`, `task_breakdown`, `role_assignments`, `slice_dag_shape`, `test_strategy`, `rollback_plan`, `risk_coverage`, `pr_block`) match the k3s counterpart. Verdict JSON shape preserves `verdict`/`summary`/`analysis`/`suggestions`/`artifact_references`/`feedback`/`timestamp`. The "Three review edges per cycle" note correctly explains the open-NACK barrier in the in-process bus (the orchestrator rejects re-proposes with HTTP 409 once two-or-more *edges from this reviewer* across the three producers have NACKed). The verdict-path stability note explicitly addresses how the three per-edge verdicts get namespaced by producer role in the artifact handoff — this is a real coordination problem the rubric correctly flags. - -Loader alignment: all four files use underscored basenames (`architect.md`, `task_planner.md`, `risk_analyst.md`, `reviewer_plan.md`) matching `_load_egg_sdlc_role_rubric`'s `f"{role_name}.md"` pattern at `orchestrator/substrate/__init__.py:331` and the `_ROLE_RUBRIC_SLICES` entries for slice-2 at lines 250-253 — the loader expansion is task-2-2 (coder) and is not the documenter's responsibility, but the file basenames must match the keys, and they do. - -**plugins/egg-sdlc/skills/egg-sdlc/SKILL.md** (modified, +49/-10) — Task-2-7 acceptance criteria are satisfied: - -- The frontmatter description is updated to enumerate the plan-phase roster ("plan-phase scope landed in slice 2 (architect + task_planner + risk_analyst + reviewer_plan)") and to bump the bridge-gap-closed claim to "refine + plan". -- The rollout-status callout is bumped from "slice 1 of #2717 landed" to "slices 1 + 2 of #2717 landed", with both rosters enumerated and the architect-first-then-task_planner+risk_analyst-concurrent ordering called out. -- The "Refine HITL gate" step (step 7) is followed by a new "Plan subagents run inside the next driver invocation" step (8), a new "Plan HITL gate" step (9), and the phase fence is bumped to step 10 with its message updated to point past plan to slice 3 of the rollout. -- The new "Plan phase (landed in slice 2 of #2717)" subsection (lines 235–256) names the four roles, their spawn order, output paths, and the four standard plan-HITL gate options (approve / request_changes / change_approach / stop) — meeting the "plan-HITL gate is named" criterion. -- The "What's NOT in this skill" section is updated: the "Plan / implement / pr phases" bullet is replaced with an "Implement / pr phases" bullet that points at slice 3 / slice 4 / slice 5 — meeting the "plan-phase deferral no longer listed" criterion. -- Failure modes: the `NotImplementedError: claude-code substrate runs refine only` diagnostic is updated to `... refine + plan only` and re-aimed at "tried to advance past the plan HITL gate". - -### Non-blocking - -- **plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:14, 113, 286** — The "slices 1 + 2 landed" / "closed for refine + plan" / "NotImplementedError: ... refine + plan only" claims are forward-looking against the documenter's commit alone, since the coder's task-2-1 (plan stage in `_InProcessOrchestrator.run()`) and task-2-2 (rubric loader expansion) are still in flight. This is the normal BRC atomic-landing pattern (the slice converges before any of it lands), but it does mean a reader of the documenter's commit in isolation would see stale doc-vs-code state. No fix needed. -- **plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:53, 94, 105, 118, 126** — The k3s task-planner's epic-reassess Won't-Do comment template ("Superseded by `` in the reassess of ``...") and the full Plan diff example block were trimmed in the egg-sdlc copy. The substantive guidance (which `jira_action` to set, when to flag in-flight, the survivor-selection heuristic) is intact. Consider porting the Won't-Do template verbatim in a follow-up so the egg-sdlc task_planner emits the same comment shape the k3s task_planner does — keeps Won't-Do audit trails consistent across substrates. Not blocking because slice-2 is plan-team rubric setup, not epic-mode behavioral parity. -- **plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:27-34** — The "Read all of these" / inputs section lists five paths the Task context provides but does not enumerate `verdict_path` even though the body references it at line 59 ("Also written to `verdict_path`"). This mirrors slice-1's `reviewer_refine.md` pattern (which also references `verdict_path` only in the body, not in the inputs list) so consistency is preserved — but a clarifying bullet in inputs would help. -- **plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md:96** — The allow-list note for risk_analyst says only `.egg-state/agent-outputs/` (no `.egg-state/drafts/` since risk_analyst doesn't write the plan markdown). This is correct, but worth a note that the k3s patterns.py governs this and the substrate-specific note is informational; if `build_agent_patterns(role)` later expands the risk_analyst's allow-list, the rubric will fall out of sync. - -No security, correctness, or robustness issues found. Documenter's submission ACKed. - - -````yaml -id: 8e80e79c-e768-44 -phase: implement -metadata: - payload: - artifact_references: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - reason: "\nReviewed 5 documentation files for task-2-3 (four plan-team rubrics)\ - \ and task-2-7 (SKILL.md plan-phase section). Verified acceptance criteria for\ - \ both tasks, body fidelity against the k3s-substrate counterparts in `plugins/refine-plan/skills/refine-plan/agents/`,\ - \ link integrity, frontmatter validity, and loader-name alignment.\n\n### File-by-file\ - \ analysis\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md** (new,\ - \ 88 lines) \u2014 Frontmatter is informational-only per the slice-1 convention.\ - \ Body opens with \"You are the **architect** running on the **Claude Code substrate**...\"\ - , explicitly meeting task-2-3's substrate-context requirement. The output JSON\ - \ schema (`approach_summary`, `key_design_decisions`, `components_touched`,\ - \ `ordering_constraints`, `open_questions_for_planner`) matches the k3s counterpart\ - \ byte-for-byte. The \"What you do\" section adds the missing-from-k3s \"You\ - \ run first, solo, before `task_planner` and `risk_analyst`\" sequencing clue,\ - \ which is consistent with the SKILL.md narrative. The four substrate-specific\ - \ notes (worktree, file-write restrictions, HITL, concurrent peers, output path\ - \ stability) match the slice-1 pattern from `reviewer_refine.md` and `reviewer_agent_design.md`.\ - \ Relative link `../../../../docs/architecture/claude-code-substrate.md` resolves\ - \ correctly to `docs/architecture/claude-code-substrate.md`.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md**\ - \ (new, 294 lines) \u2014 Body opens with \"You are the **task_planner** running\ - \ on the **Claude Code substrate**...\". The full `[mode: ticket]` / `[mode:\ - \ github_issue]` / `[mode: epic-fresh]` / `[mode: epic-reassess]` mode-switch\ - \ block is preserved verbatim from the k3s version with light editorial trimming\ - \ (Won't-Do comment template removed, Plan diff example reduced to a single\ - \ sentence describing the cluster groups). The YAML appendix discipline section\ - \ (block scalars, role mapping, `pr:` block requirements, DAG-is-a-forest rule)\ - \ is intact. The output JSON schema (`plan_path`, `slice_count`, `task_count`,\ - \ `roles_used`, `dag_shape_summary`, `critical_path_tasks`) matches the k3s\ - \ counterpart. Substrate-specific notes match the slice-1 pattern.\n\n**plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md**\ - \ (new, 99 lines) \u2014 Body opens with \"You are the **risk_analyst** running\ - \ on the **Claude Code substrate**...\". The risk-record schema (`risks[].{name,category,likelihood,impact,evidence,mitigation,owns_task}`\ - \ + `top_3_risks` + `blocking_concerns`) is preserved. The evidence and mitigation\ - \ discipline sections are intact. The \"Concurrent peers in this slice\" note\ - \ correctly explains the reviewer_plan reconciliation behavior when task_planner\ - \ finalizes before risk_analyst's handoff is visible \u2014 useful guardrail.\n\ - \n**plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md** (new, 102 lines)\ - \ \u2014 Body opens with \"You are the **reviewer_plan** running on the **Claude\ - \ Code substrate**...\". All eight review criteria (`alignment_with_analysis`,\ - \ `task_breakdown`, `role_assignments`, `slice_dag_shape`, `test_strategy`,\ - \ `rollback_plan`, `risk_coverage`, `pr_block`) match the k3s counterpart. Verdict\ - \ JSON shape preserves `verdict`/`summary`/`analysis`/`suggestions`/`artifact_references`/`feedback`/`timestamp`.\ - \ The \"Three review edges per cycle\" note correctly explains the open-NACK\ - \ barrier in the in-process bus (the orchestrator rejects re-proposes with HTTP\ - \ 409 once two-or-more *edges from this reviewer* across the three producers\ - \ have NACKed). The verdict-path stability note explicitly addresses how the\ - \ three per-edge verdicts get namespaced by producer role in the artifact handoff\ - \ \u2014 this is a real coordination problem the rubric correctly flags.\n\n\ - Loader alignment: all four files use underscored basenames (`architect.md`,\ - \ `task_planner.md`, `risk_analyst.md`, `reviewer_plan.md`) matching `_load_egg_sdlc_role_rubric`'s\ - \ `f\"{role_name}.md\"` pattern at `orchestrator/substrate/__init__.py:331`\ - \ and the `_ROLE_RUBRIC_SLICES` entries for slice-2 at lines 250-253 \u2014\ - \ the loader expansion is task-2-2 (coder) and is not the documenter's responsibility,\ - \ but the file basenames must match the keys, and they do.\n\n**plugins/egg-sdlc/skills/egg-sdlc/SKILL.md**\ - \ (modified, +49/-10) \u2014 Task-2-7 acceptance criteria are satisfied:\n\n\ - - The frontmatter description is updated to enumerate the plan-phase roster\ - \ (\"plan-phase scope landed in slice 2 (architect + task_planner + risk_analyst\ - \ + reviewer_plan)\") and to bump the bridge-gap-closed claim to \"refine +\ - \ plan\".\n- The rollout-status callout is bumped from \"slice 1 of #2717 landed\"\ - \ to \"slices 1 + 2 of #2717 landed\", with both rosters enumerated and the\ - \ architect-first-then-task_planner+risk_analyst-concurrent ordering called\ - \ out.\n- The \"Refine HITL gate\" step (step 7) is followed by a new \"Plan\ - \ subagents run inside the next driver invocation\" step (8), a new \"Plan HITL\ - \ gate\" step (9), and the phase fence is bumped to step 10 with its message\ - \ updated to point past plan to slice 3 of the rollout.\n- The new \"Plan phase\ - \ (landed in slice 2 of #2717)\" subsection (lines 235\u2013256) names the four\ - \ roles, their spawn order, output paths, and the four standard plan-HITL gate\ - \ options (approve / request_changes / change_approach / stop) \u2014 meeting\ - \ the \"plan-HITL gate is named\" criterion.\n- The \"What's NOT in this skill\"\ - \ section is updated: the \"Plan / implement / pr phases\" bullet is replaced\ - \ with an \"Implement / pr phases\" bullet that points at slice 3 / slice 4\ - \ / slice 5 \u2014 meeting the \"plan-phase deferral no longer listed\" criterion.\n\ - - Failure modes: the `NotImplementedError: claude-code substrate runs refine\ - \ only` diagnostic is updated to `... refine + plan only` and re-aimed at \"\ - tried to advance past the plan HITL gate\".\n\n### Non-blocking\n\n- **plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:14,\ - \ 113, 286** \u2014 The \"slices 1 + 2 landed\" / \"closed for refine + plan\"\ - \ / \"NotImplementedError: ... refine + plan only\" claims are forward-looking\ - \ against the documenter's commit alone, since the coder's task-2-1 (plan stage\ - \ in `_InProcessOrchestrator.run()`) and task-2-2 (rubric loader expansion)\ - \ are still in flight. This is the normal BRC atomic-landing pattern (the slice\ - \ converges before any of it lands), but it does mean a reader of the documenter's\ - \ commit in isolation would see stale doc-vs-code state. No fix needed.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:53,\ - \ 94, 105, 118, 126** \u2014 The k3s task-planner's epic-reassess Won't-Do comment\ - \ template (\"Superseded by `` in the reassess of ``...\"\ - ) and the full Plan diff example block were trimmed in the egg-sdlc copy. The\ - \ substantive guidance (which `jira_action` to set, when to flag in-flight,\ - \ the survivor-selection heuristic) is intact. Consider porting the Won't-Do\ - \ template verbatim in a follow-up so the egg-sdlc task_planner emits the same\ - \ comment shape the k3s task_planner does \u2014 keeps Won't-Do audit trails\ - \ consistent across substrates. Not blocking because slice-2 is plan-team rubric\ - \ setup, not epic-mode behavioral parity.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:27-34**\ - \ \u2014 The \"Read all of these\" / inputs section lists five paths the Task\ - \ context provides but does not enumerate `verdict_path` even though the body\ - \ references it at line 59 (\"Also written to `verdict_path`\"). This mirrors\ - \ slice-1's `reviewer_refine.md` pattern (which also references `verdict_path`\ - \ only in the body, not in the inputs list) so consistency is preserved \u2014\ - \ but a clarifying bullet in inputs would help.\n- **plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md:96**\ - \ \u2014 The allow-list note for risk_analyst says only `.egg-state/agent-outputs/`\ - \ (no `.egg-state/drafts/` since risk_analyst doesn't write the plan markdown).\ - \ This is correct, but worth a note that the k3s patterns.py governs this and\ - \ the substrate-specific note is informational; if `build_agent_patterns(role)`\ - \ later expands the risk_analyst's allow-list, the rubric will fall out of sync.\n\ - \nNo security, correctness, or robustness issues found. Documenter's submission\ - \ ACKed.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T19:50:54Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 7d5b6bee-164d-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:50:55Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 032629b3-ec28-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:01Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 18d3454d-d262-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:01.538453+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:03Z] documenter (CONSENSUS_CONFIRMED): Confirmed by documenter (pending_acks) - -Agent documenter cannot confirm: producers ['tester', 'coder'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus. - -````yaml -id: 90725577-e894-4c -phase: implement -metadata: - pending_acks: true - slice_id: slice-2 -```` - -### [2026-05-19T19:51:14Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 9ce76ac6-2fcb-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:14.455966+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:14Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: b2daffa6-f90a-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:51:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: fbde1df4-2abc-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:17Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f59f686d-f5e3-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:17.619085+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:23Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 4b910676-b140-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:22.922742+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:48Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 6adc06f8-c5ae-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:49Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8d99d443-303a-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:49.537771+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:51:55Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 18f181bd-5547-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:52:01Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 16eb2347-36e8-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:01.538453+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:52:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 46874605-ce30-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:52:17Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d43e23bc-e523-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:17.619085+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:52:23Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 647cbd0e-8192-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:22.922742+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:52:48Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f63b2025-9dbb-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:52:49Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 709f5cb2-ba46-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:49.537771+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:52:55Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 62c11b0d-1b8f-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:53:16Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0dceb97c-16cf-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:01.538453+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:53:16Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 594ce0ea-847b-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:53:17Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: bf8d3f3b-10dc-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:17.619085+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:53:41Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 098472ac-e975-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:22.922742+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:54:07Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 77afe3d3-d596-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:54:07Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2cf7d433-ff26-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:49.537771+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:54:07Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 329f3b64-43d0-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:54:31Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0de7b345-3289-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:01.538453+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:54:31Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 156f223d-0f92-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:54:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e06e6505-99a1-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:17.619085+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:54:38Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c079a4f8-9d74-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:22.922742+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:55:23Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1d686a2d-2672-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:55:23Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 685b986c-784e-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:49.537771+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:55:23Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: d6b2d098-86df-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:55:31Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2bd0bd3c-6efe-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:01.538453+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:55:31Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 117a4a01-9961-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:15.136373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:55:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 9dc1aba5-6fcd-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:17.619085+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:55:38Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ac56440d-50a3-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:51:22.922742+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:56:29Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 6976d1dc-a3a1-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:47:36.510983+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:56:29Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 37798c1f-8589-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:48:54.889725+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:56:29Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 69df766a-cb35-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:49:49.537771+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:56:29Z] coder (CONSENSUS_PROPOSE): Proposal from coder - -Slice-2 coder: wire plan-phase BRC stage (3 producers + 1 reviewer) on the in-process Claude Code substrate + extend the rubric loader to the plan team. - -TASK-2-1 (orchestrator/substrate/in_process.py) — added `_run_plan_phase` on `_InProcessOrchestrator`: after the refine HITL gate's `approve_continue` answer, a `ThreadPoolExecutor` spawns architect/task_planner/risk_analyst concurrently through the substrate bundle's `ClaudeCodeSpawner`, then reviewer_plan is dispatched once with the producer artifacts as its prompt input. `PeerConsensusTracker` (lazy-imported, pipeline-scoped) drives the BRC mechanics: `handle_propose` on each successful producer spawn, `handle_ack` for each reviewer→producer edge, `handle_confirmed` per role. The stage then yields a plan-HITL gate (`HITLDecision` with `phase="plan"`, options `approve_continue / request_changes / change_approach / stop`). `_maybe_fence`'s diagnostic now points at slice-3 / slice-4 of the #2717 rollout. The orchestrator drives the BRC transitions because the in-process spawner is synchronous (spawn-completion IS the signal that the subagent did its work) — production harness agents whose own emissions would be no-op duplicates and harness-faked tests both reach CONSENSUS_CONFIRMED on the same code path. - -TASK-2-2 (orchestrator/substrate/__init__.py) — extended `_RUBRIC_LANDED_ROLES` to include architect / task_planner / risk_analyst / reviewer_plan alongside the slice-1 refine roster. Implement-team roles continue to raise `ValueError` with the slice-3 pointer (structured-error contract preserved). The "missing on disk" fallback diagnostic now names both TASK-1-4 and TASK-2-3 so a future reviewer hitting the error knows which documenter task needs to land first. - -TASK-2-5 — closes as no-op: slice-1's `test_pretooluse_hook_denies_nested_child_write` already pinned the R2 verdict as pass (the hook denies a child write outside the child's role under nested dispatch). Per the task contract, no `sandbox/egg_agent_tools/handlers/restrictions.py` change is needed when R2 = pass. - -Manual in-process smoke (harness-faked spawner, MagicMock subagents): preflight → refine gate → plan gate sequence yields the expected decisions; spawner is called exactly 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; `approve_continue` at the plan gate still raises `NotImplementedError` with the slice-3 / slice-4 pointer; a terminal answer at the refine gate (e.g. "stop") returns the refine artifact path without entering plan phase. - -````yaml -id: 18f645ea-a0b7-49 -phase: implement -metadata: - payload: - summary: "Slice-2 coder: wire plan-phase BRC stage (3 producers + 1 reviewer)\ - \ on the in-process Claude Code substrate + extend the rubric loader to the\ - \ plan team.\n\nTASK-2-1 (orchestrator/substrate/in_process.py) \u2014 added\ - \ `_run_plan_phase` on `_InProcessOrchestrator`: after the refine HITL gate's\ - \ `approve_continue` answer, a `ThreadPoolExecutor` spawns architect/task_planner/risk_analyst\ - \ concurrently through the substrate bundle's `ClaudeCodeSpawner`, then reviewer_plan\ - \ is dispatched once with the producer artifacts as its prompt input. `PeerConsensusTracker`\ - \ (lazy-imported, pipeline-scoped) drives the BRC mechanics: `handle_propose`\ - \ on each successful producer spawn, `handle_ack` for each reviewer\u2192producer\ - \ edge, `handle_confirmed` per role. The stage then yields a plan-HITL gate\ - \ (`HITLDecision` with `phase=\"plan\"`, options `approve_continue / request_changes\ - \ / change_approach / stop`). `_maybe_fence`'s diagnostic now points at slice-3\ - \ / slice-4 of the #2717 rollout. The orchestrator drives the BRC transitions\ - \ because the in-process spawner is synchronous (spawn-completion IS the signal\ - \ that the subagent did its work) \u2014 production harness agents whose own\ - \ emissions would be no-op duplicates and harness-faked tests both reach CONSENSUS_CONFIRMED\ - \ on the same code path.\n\nTASK-2-2 (orchestrator/substrate/__init__.py) \u2014\ - \ extended `_RUBRIC_LANDED_ROLES` to include architect / task_planner / risk_analyst\ - \ / reviewer_plan alongside the slice-1 refine roster. Implement-team roles\ - \ continue to raise `ValueError` with the slice-3 pointer (structured-error\ - \ contract preserved). The \"missing on disk\" fallback diagnostic now names\ - \ both TASK-1-4 and TASK-2-3 so a future reviewer hitting the error knows which\ - \ documenter task needs to land first.\n\nTASK-2-5 \u2014 closes as no-op: slice-1's\ - \ `test_pretooluse_hook_denies_nested_child_write` already pinned the R2 verdict\ - \ as pass (the hook denies a child write outside the child's role under nested\ - \ dispatch). Per the task contract, no `sandbox/egg_agent_tools/handlers/restrictions.py`\ - \ change is needed when R2 = pass.\n\nManual in-process smoke (harness-faked\ - \ spawner, MagicMock subagents): preflight \u2192 refine gate \u2192 plan gate\ - \ sequence yields the expected decisions; spawner is called exactly 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; `approve_continue`\ - \ at the plan gate still raises `NotImplementedError` with the slice-3 / slice-4\ - \ pointer; a terminal answer at the refine gate (e.g. \"stop\") returns the\ - \ refine artifact path without entering plan phase." - attestation: - commit_sha: 3a466891e46661534c89f211a1d3d0a359dcb79e - commit_shas: - - 3a466891e46661534c89f211a1d3d0a359dcb79e - files_changed: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - checks_passed: - - manual_smoke_inprocess_plan_brc - - manual_smoke_refine_terminal_stop - - manual_smoke_plan_gate_fence - - rubric_loader_regression - - import_check - tests_run: 5 - no_test_changes_needed: true - no_test_changes_reason: Coder role's allow-list excludes tests/ paths (TASK-2-4 - / TASK-2-6 are tester-owned). The five manual smoke runs above exercise the - same surface a pytest test would target; the slice-2 tester's `test_inprocess_plan_brc.py` - lands the pytest equivalent under a separate role's commit. - artifacts: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - risk_considered: "Two non-trivial risks evaluated: (1) the orchestrator-side BRC\ - \ bookkeeping (handle_propose/handle_ack/handle_confirmed called by `_run_plan_phase`)\ - \ could double-count when a real harness agent later starts emitting its own\ - \ BRC messages \u2014 acceptable because under the current cq-3 harness re-host\ - \ model the subagents do NOT emit BRC messages (only the synchronous spawn-completion\ - \ is the BRC signal), and a future Agent-tool dispatch migration (slice-5 R15)\ - \ would be the natural place to swap to agent-emitted BRC. (2) The pipeline-scoped\ - \ tracker reuses any existing tracker via `get_peer_consensus_tracker(self.pipeline_id)`\ - \ \u2014 risk: a stale tracker from an aborted previous run could carry state\ - \ across pipelines. Mitigated by the slice-1 spike's per-pipeline tracker key\ - \ (`pipeline_id` only; no slice_id today) and by `cooldown_seconds=0` so AUTO_REPROPOSE\ - \ debounce doesn't fire during the tight propose\u2192ack\u2192confirm sequence;\ - \ the test path uses `remove_peer_consensus_tracker` between cycles, and production\ - \ runs are pipeline-scoped per `select_substrate` lifecycle." - commit_sha: 3a466891e46661534c89f211a1d3d0a359dcb79e - files_changed: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - tests_run: [] - tasks_satisfied: - - task-2-1 - - task-2-2 - - task-2-5 - version: 1 - commit_sha: 3a466891e46661534c89f211a1d3d0a359dcb79e - slice_id: slice-2 -```` - -### [2026-05-19T19:56:30Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d8bcda3c-e2d9-40 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:30Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: bd6916e2-9e14-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:30Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c68bf3e6-9857-47 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:30Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 93019684-b5e2-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:30Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: dd706814-85f7-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:30Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 5e6da642-b01f-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:30Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 71809853-f5db-47 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:54Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d58782f6-1a2e-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:56:35.658120+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:56:54Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 70749f36-d5ef-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:56:36.073218+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:56:54Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: eb6c237b-cd1a-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:56:50.332240+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:56:54Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 0d9cdef9-34f1-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:56Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 19a7de8d-0b09-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:56:56Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: dee6d019-8ff3-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T19:57:01Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7f60cf71-5d72-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:01.936285+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:57:02Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 809daca5-914c-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:58:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 24ea046c-dac1-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:01.936285+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:58:02Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 9accc37c-2c65-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:59:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 29b5dbc4-5058-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:01.936285+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T19:59:02Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 37ff7d1b-2da1-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:00:02Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: f7349dff-b210-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:01.936285+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:00:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7438ff63-7a0f-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:00:24Z] reviewer_security → coder (CONSENSUS_ACK): ACK from reviewer_security for coder - - -Reviewed the slice-2 coder diff at commit 3a466891e through the security lens. Read the orchestrator/substrate/__init__.py loader extension, the new _run_plan_phase / _spawn_plan_producer / _spawn_plan_reviewer / _format_plan_placeholder / _build_plan_gate_decision / _answer_continues_past_refine code paths in orchestrator/substrate/in_process.py, and cross-referenced the four plan-team rubric files landed by the documenter at commit 7122ca2d1. No security findings. - -### Lens checks -1. **Cross-file allowlist mismatch (§1):** `_RUBRIC_LANDED_ROLES` extension at __init__.py:272-286 (`architect`, `task_planner`, `risk_analyst`, `reviewer_plan`) matches the four `.md` files the documenter shipped under `plugins/egg-sdlc/skills/egg-sdlc/agents/` (ls -la confirms presence on the merged branch). `_ROLE_RUBRIC_SLICES` (line 244-263) lists each of the four under `slice-2`, consistent with the loader's "supported but not landed" → "landed" promotion path. The loader fence at __init__.py:348-360 raises with the correct slice pointer for any not-yet-landed role, and the path is only read after the role passes the allowlist gate — so the `f"{role_name}.md"` interpolation cannot traverse out of `agents/` even if `role_name` were attacker-controlled (which it isn't; it's an `AgentRole` enum value). - -2. **Handler-vs-validator path mismatch (§2):** Not applicable — no new public/HTTP entrypoint is added. The plan phase runs in-process under the orchestrator's own generator; the only external surface (`_build_plan_gate_decision`'s `HITLDecision`) is read by the trusted operator via `AskUserQuestion`. - -3. **Information-disclosure / authz bypass (§3):** `_format_plan_placeholder` writes per-producer diagnostics into `.egg-state/drafts/-plan.md`, including 500-char truncated stdout, `commit_sha`, and `Exception` repr. This mirrors the slice-1 refiner-placeholder pattern (also committed to drafts/) and the file is gateway-allowed under documenter-pattern writes — it is not a NEW exposure relative to slice-1's surface, and stdout from a worktree-isolated agent is not expected to carry orchestrator-side secrets. The plan-gate `HITLDecision` surfaces `blocking_agents` and `unresolved_nack_details` to the operator only, not over the network. - -4. **Uncommitted-artifact / symlink mismatch (§4):** Every path-string the diff introduces (the four `agents/.md` rubrics, the four `_RUBRIC_LANDED_ROLES` entries) has a corresponding file committed by the documenter at 7122ca2d1 — `ls -la plugins/egg-sdlc/skills/egg-sdlc/agents/` shows all four present with non-zero sizes. No Dockerfile / packaging-manifest references to verify (the diff is Python + markdown only). - -5. **Credential-shim modifications (§5):** No changes under `sandbox/scripts/`; the credential-routing invariant is untouched. - -6. **Secret leakage (§6):** `spawn_env = {**self.env, "EGG_PIPELINE_ID": ..., "EGG_AGENT_ROLE": role.value, ...}` propagates the orchestrator's env to each producer subprocess — identical to the existing refiner spawn pattern. The producers run inside isolated worktrees under `` and each rubric explicitly fences their writes to `.egg-state/drafts/` and/or `.egg-state/agent-outputs/` via the PreToolUse hook; no new sink for secrets is introduced. The `_SYNTHETIC_PLAN_COMMIT = "ace1ace"` constant is intentionally obvious in log output and carries no credential value. - -7. **Cross-file OWASP top-10 (§7):** No SQL, no HTML rendering, no URL dereferencing, no deserialization of untrusted data is introduced. The `tracker.handle_propose` / `handle_ack` / `handle_confirmed` calls feed JSON-serializable Python dicts into an in-process tracker; the producer artifact paths in the ACK payload are orchestrator-derived from `drafts_dir / f"{artifact_id}-plan.md"`, not agent input. - -8. **Agent-supplied paths in read-only access (§8):** All filesystem accesses in this diff use orchestrator-derived paths — `plan_artifact_path` is built from `drafts_dir` + `self.issue_number or self.pipeline_id`, `refine_artifact_path` is the prior-stage `_artifact_path`, and `producer_artifacts` is a Mapping built internally from the spawner's worktree allocations. No tool boundary in this diff accepts an external path and reads/stats it without a workspace-root check. - -### Non-blocking -- **orchestrator/substrate/in_process.py:328-373** — three `_spawn_plan_producer` calls run concurrently in a `ThreadPoolExecutor` and each calls `_write_active_role_sentinel(role.value)` against the shared per-user `$HOME/.claude/egg-active-role.json` (sentinel docstring at :1195-1202 already names this as the "single-valued, per-user file cannot disambiguate two concurrent sub-agents in different roles" limitation). The race is constrained to roles whose allow-lists are all subsets of `.egg-state/` so there is no escape from the orchestrator's restriction set, and `EGG_AGENT_ROLE` is set in each subprocess's `spawn_env` so the sentinel is only consulted as a hook fallback. Surfacing here for the security-lens audit trail; the structural fix is reviewer_concurrency / R2-deferral scope, not a blocker for this slice. -- **orchestrator/substrate/in_process.py:388-420** — `tracker.handle_ack` is recorded on every plan producer whose spawn returned exit_code==0 without inspecting the reviewer_plan verdict JSON in `.egg-state/agent-outputs/-reviewer_plan-output.json`. Not a security boundary (all in-process trusted code), but the orchestrator's "ACK on the reviewer's behalf" semantics deserve a reviewer_code look — if a future change made the reviewer's verdict load-bearing for downstream security policy, this would need to read the verdict file. Out of scope for the security lens today. - - -````yaml -id: 07f37f09-26bc-4c -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/__init__.py - - orchestrator/substrate/in_process.py - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - reason: "\nReviewed the slice-2 coder diff at commit 3a466891e through the security\ - \ lens. Read the orchestrator/substrate/__init__.py loader extension, the new\ - \ _run_plan_phase / _spawn_plan_producer / _spawn_plan_reviewer / _format_plan_placeholder\ - \ / _build_plan_gate_decision / _answer_continues_past_refine code paths in\ - \ orchestrator/substrate/in_process.py, and cross-referenced the four plan-team\ - \ rubric files landed by the documenter at commit 7122ca2d1. No security findings.\n\ - \n### Lens checks\n1. **Cross-file allowlist mismatch (\xA71):** `_RUBRIC_LANDED_ROLES`\ - \ extension at __init__.py:272-286 (`architect`, `task_planner`, `risk_analyst`,\ - \ `reviewer_plan`) matches the four `.md` files the documenter shipped\ - \ under `plugins/egg-sdlc/skills/egg-sdlc/agents/` (ls -la confirms presence\ - \ on the merged branch). `_ROLE_RUBRIC_SLICES` (line 244-263) lists each of\ - \ the four under `slice-2`, consistent with the loader's \"supported but not\ - \ landed\" \u2192 \"landed\" promotion path. The loader fence at __init__.py:348-360\ - \ raises with the correct slice pointer for any not-yet-landed role, and the\ - \ path is only read after the role passes the allowlist gate \u2014 so the `f\"\ - {role_name}.md\"` interpolation cannot traverse out of `agents/` even if `role_name`\ - \ were attacker-controlled (which it isn't; it's an `AgentRole` enum value).\n\ - \n2. **Handler-vs-validator path mismatch (\xA72):** Not applicable \u2014 no\ - \ new public/HTTP entrypoint is added. The plan phase runs in-process under\ - \ the orchestrator's own generator; the only external surface (`_build_plan_gate_decision`'s\ - \ `HITLDecision`) is read by the trusted operator via `AskUserQuestion`.\n\n\ - 3. **Information-disclosure / authz bypass (\xA73):** `_format_plan_placeholder`\ - \ writes per-producer diagnostics into `.egg-state/drafts/-plan.md`,\ - \ including 500-char truncated stdout, `commit_sha`, and `Exception` repr. This\ - \ mirrors the slice-1 refiner-placeholder pattern (also committed to drafts/)\ - \ and the file is gateway-allowed under documenter-pattern writes \u2014 it\ - \ is not a NEW exposure relative to slice-1's surface, and stdout from a worktree-isolated\ - \ agent is not expected to carry orchestrator-side secrets. The plan-gate `HITLDecision`\ - \ surfaces `blocking_agents` and `unresolved_nack_details` to the operator only,\ - \ not over the network.\n\n4. **Uncommitted-artifact / symlink mismatch (\xA7\ - 4):** Every path-string the diff introduces (the four `agents/.md` rubrics,\ - \ the four `_RUBRIC_LANDED_ROLES` entries) has a corresponding file committed\ - \ by the documenter at 7122ca2d1 \u2014 `ls -la plugins/egg-sdlc/skills/egg-sdlc/agents/`\ - \ shows all four present with non-zero sizes. No Dockerfile / packaging-manifest\ - \ references to verify (the diff is Python + markdown only).\n\n5. **Credential-shim\ - \ modifications (\xA75):** No changes under `sandbox/scripts/`; the credential-routing\ - \ invariant is untouched.\n\n6. **Secret leakage (\xA76):** `spawn_env = {**self.env,\ - \ \"EGG_PIPELINE_ID\": ..., \"EGG_AGENT_ROLE\": role.value, ...}` propagates\ - \ the orchestrator's env to each producer subprocess \u2014 identical to the\ - \ existing refiner spawn pattern. The producers run inside isolated worktrees\ - \ under `` and each rubric explicitly fences their writes\ - \ to `.egg-state/drafts/` and/or `.egg-state/agent-outputs/` via the PreToolUse\ - \ hook; no new sink for secrets is introduced. The `_SYNTHETIC_PLAN_COMMIT =\ - \ \"ace1ace\"` constant is intentionally obvious in log output and carries no\ - \ credential value.\n\n7. **Cross-file OWASP top-10 (\xA77):** No SQL, no HTML\ - \ rendering, no URL dereferencing, no deserialization of untrusted data is introduced.\ - \ The `tracker.handle_propose` / `handle_ack` / `handle_confirmed` calls feed\ - \ JSON-serializable Python dicts into an in-process tracker; the producer artifact\ - \ paths in the ACK payload are orchestrator-derived from `drafts_dir / f\"{artifact_id}-plan.md\"\ - `, not agent input.\n\n8. **Agent-supplied paths in read-only access (\xA78):**\ - \ All filesystem accesses in this diff use orchestrator-derived paths \u2014\ - \ `plan_artifact_path` is built from `drafts_dir` + `self.issue_number or self.pipeline_id`,\ - \ `refine_artifact_path` is the prior-stage `_artifact_path`, and `producer_artifacts`\ - \ is a Mapping built internally from the spawner's worktree allocations. No\ - \ tool boundary in this diff accepts an external path and reads/stats it without\ - \ a workspace-root check.\n\n### Non-blocking\n- **orchestrator/substrate/in_process.py:328-373**\ - \ \u2014 three `_spawn_plan_producer` calls run concurrently in a `ThreadPoolExecutor`\ - \ and each calls `_write_active_role_sentinel(role.value)` against the shared\ - \ per-user `$HOME/.claude/egg-active-role.json` (sentinel docstring at :1195-1202\ - \ already names this as the \"single-valued, per-user file cannot disambiguate\ - \ two concurrent sub-agents in different roles\" limitation). The race is constrained\ - \ to roles whose allow-lists are all subsets of `.egg-state/` so there is no\ - \ escape from the orchestrator's restriction set, and `EGG_AGENT_ROLE` is set\ - \ in each subprocess's `spawn_env` so the sentinel is only consulted as a hook\ - \ fallback. Surfacing here for the security-lens audit trail; the structural\ - \ fix is reviewer_concurrency / R2-deferral scope, not a blocker for this slice.\n\ - - **orchestrator/substrate/in_process.py:388-420** \u2014 `tracker.handle_ack`\ - \ is recorded on every plan producer whose spawn returned exit_code==0 without\ - \ inspecting the reviewer_plan verdict JSON in `.egg-state/agent-outputs/-reviewer_plan-output.json`.\ - \ Not a security boundary (all in-process trusted code), but the orchestrator's\ - \ \"ACK on the reviewer's behalf\" semantics deserve a reviewer_code look \u2014\ - \ if a future change made the reviewer's verdict load-bearing for downstream\ - \ security policy, this would need to read the verdict file. Out of scope for\ - \ the security lens today.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:00:24Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6dc3ade8-d6e4-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:00:30Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d1151a9e-8d20-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.423341+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:00:30Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 04991a2c-e5b6-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:01:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3d639278-beea-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:01:10Z] reviewer_contract → coder (CONSENSUS_ACK): ACK from reviewer_contract for coder - - -Contract-verification review (slice-2 coder, commit 3a466891e, proposal v1). Reviewed both files in the diff against the three coder-assigned tasks in slice-2 (task-2-1, task-2-2, task-2-5). All acceptance criteria are objectively met. - -### Per-task verification - -**TASK-2-1 — `_run_plan_phase` on `_InProcessOrchestrator`** (orchestrator/substrate/in_process.py:830-1071): -1. AC "no longer raises NotImplementedError when the operator advances past refine": ✅ Verified. `run()` body at line 233 calls `self._run_plan_phase(artifact_path)` after `_answer_continues_past_refine(refine_answer)` is true; the walking-skeleton `_maybe_fence` moved to AFTER the plan HITL gate (line 247 call site; line 1260-1291 fence body whose diagnostic now points at "slice-3 / slice-4 of the #2717 rollout"). Refine-gate `approve_continue` no longer raises. -2. AC "plan stage spawns 3 producers concurrently via the executor": ✅ Verified. `ThreadPoolExecutor(max_workers=len(plan_producers))` at line 942 dispatches `_spawn_plan_producer` for ARCHITECT, TASK_PLANNER, RISK_ANALYST concurrently. Each producer gets its own worktree (`bundle.worktrees.create`, line 1089), env-shaped spawn (lines 1091-1104 with `EGG_AGENT_ROLE`, `EGG_PHASE="plan"`, refine/plan artifact paths), and active-role sentinel write before `bundle.spawner.spawn(...)`. -3. AC "reviewer_plan is spawned after each CONSENSUS_PROPOSE": ⚠️ Functionally satisfied via a single reviewer dispatch that records N ACKs on the tracker, not N reviewer spawns. `_spawn_plan_reviewer` is called once (line 994) AFTER the producer ThreadPoolExecutor's `with` block exits and AFTER `tracker.handle_propose(role.value, ...)` has fired for every successful producer (line 976-987). The reviewer then ACKs each producer separately (line 1011-1029 loop). The docstring at lines 842-856 explicitly justifies the single-spawn-batches-ACKs design: "the in-process bundle's spawner is synchronous — `bundle.spawner.spawn(role, ...)` returns AFTER the subagent finishes ... the spawn-completion IS the signal that the subagent proposed / reviewed". Reading the AC's "after each CONSENSUS_PROPOSE" as "after all CONSENSUS_PROPOSEs land", the design is consistent with the task description ("After producers reach `CONSENSUS_PROPOSE`, `reviewer_plan` is spawned for the ACK/NACK cycle" — singular cycle) and the BRC outcome (one ACK per producer edge) is identical to a multi-spawn variant on a synchronous spawner. Non-blocking — design choice is documented and BRC tracker advances correctly. -4. AC "yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge": ✅ Verified. `tracker.handle_confirmed(role.value)` is invoked for every producer AND for `reviewer_plan` at line 1044-1052; `plan_eval = tracker.evaluate()` (line 1054) carries `is_complete`, `blocking_agents`, `unresolved_nack_details`; `_build_plan_gate_decision` (line 648-710) returns a `HITLDecision(... phase="plan")` with the canonical four `approve_continue / request_changes / change_approach / stop` options on the converged path and a `retry / abort` failure path on non-convergence. The generator yields it at line 239-241. -5. AC "existing refine path still works": ✅ Verified. Refine flow at lines 195-227 is structurally unchanged; refiner spawn, refine-gate HITL yield, abort/preflight handling all preserved. Plan dispatch is gated on `_answer_continues_past_refine(refine_answer)` returning true (line 226); any other refine-gate answer (stop / change-approach / request-changes / abort / retry) returns the refine artifact path without entering `_run_plan_phase`. - -**TASK-2-2 — `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376): -1. AC "loader returns rubric bodies for all four plan-team roles": ✅ Verified. `_RUBRIC_LANDED_ROLES` (lines 272-286) now contains `architect`, `task_planner`, `risk_analyst`, `reviewer_plan` in addition to the slice-1 set. The fence at line 348 (`if role_name not in _RUBRIC_LANDED_ROLES`) no longer rejects these roles; line 362-375 returns `rubric_path.read_text(...)` when the markdown file is present on disk. -2. AC "implement-team roles still raise ValueError with the 'follow-up slice 3' hint": ✅ Verified. `_ROLE_RUBRIC_SLICES` (lines 254-262) maps the eight implement-team roles (`coder`, `tester`, `documenter`, `reviewer_code`, `reviewer_code_holistic`, `reviewer_contract`, `reviewer_security`, `reviewer_concurrency`) to `"slice-3"`, and the ValueError at line 356-360 interpolates `slice_hint` into the message ("...deferred to follow-up slice-3 of issue #2717's rollout..."), satisfying the hint contract. - -**TASK-2-5 — sandbox restrictions parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py — NOT modified): -- AC "If R2 pass: task closed with note 'no-op: hooks resolve role correctly; structural enforcement remains hook-side'": ✅ Verified. Coder commit message records the no-op close with the required note ("TASK-2-5 closes as no-op per slice-1's R2 = pass verdict ... structural enforcement stays hook-side, no MCP-validator-side parallel layer needed"). The slice-1 R2 nested-dispatch test (`integration_tests/regression/test_pretooluse_hook_nested.py:212-238`) pins R2 = pass via three structured assertions (`result.denied`, `verdict["decision"] == "block"`, `written["r2_verdict"] == "pass"`) — these assertions ran during slice-1's BRC cycle and would have failed the slice-1 tester's propose otherwise. The no-op close is contractually defensible. - -### Non-blocking observations (informational) - -- **orchestrator/substrate/in_process.py:942** — task-2-1 *description* (not AC) names "concurrent_executor.py seam (line 569)" as the intended dispatch path; the implementation uses a raw `ThreadPoolExecutor` and records BRC transitions on the orchestrator side rather than routing through `InProcessMessageBus`. The docstring at lines 842-856 explains why (synchronous spawner makes the message-bus round-trip a no-op duplicate). This deviates from the description's wording but does NOT violate the AC ("3 producers concurrently via the executor" is satisfied; the AC does not require `ConcurrentPhaseExecutor` specifically). Calling out so a future slice that flips dispatch to async can revisit the seam choice. -- **TASK-2-5 R2-verdict artifact** — the AC for the contingent task ("see `.egg-state//r2-verdict.json` from TASK-1-5") refers to a runtime artifact that slice-1's test writes under `tmp_path / pipeline_id / "r2-verdict.json"` (per `test_pretooluse_hook_nested.py:120-128`), not under a committed path in this worktree. The verdict file therefore is NOT inspectable post-hoc; the empirical proof rests on the slice-1 test assertions having passed. This is a slice-1 handoff observation (already flagged in slice-1's reviewer history as a "downstream-handoff improvement") and not a slice-2 coder concern; slice-5 R15 will need to re-derive the verdict if it cannot read a persisted file. -- **slice-1 contract task statuses** — slice-1 tasks (task-1-1 … task-1-9) still show `status: "pending"` in the contract despite their commits being linked. This is a slice-1 contract-bookkeeping issue (not slice-2), surfaced here so the operator knows the contract's per-task `status` field is lagging the actual BRC state. The contract integrity check on re-review will need to confirm slice-1 status before declaring the rollout complete. - - -````yaml -id: 980e0bfe-3420-44 -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/__init__.py - - orchestrator/substrate/in_process.py - reason: "\nContract-verification review (slice-2 coder, commit 3a466891e, proposal\ - \ v1). Reviewed both files in the diff against the three coder-assigned tasks\ - \ in slice-2 (task-2-1, task-2-2, task-2-5). All acceptance criteria are objectively\ - \ met.\n\n### Per-task verification\n\n**TASK-2-1 \u2014 `_run_plan_phase` on\ - \ `_InProcessOrchestrator`** (orchestrator/substrate/in_process.py:830-1071):\n\ - 1. AC \"no longer raises NotImplementedError when the operator advances past\ - \ refine\": \u2705 Verified. `run()` body at line 233 calls `self._run_plan_phase(artifact_path)`\ - \ after `_answer_continues_past_refine(refine_answer)` is true; the walking-skeleton\ - \ `_maybe_fence` moved to AFTER the plan HITL gate (line 247 call site; line\ - \ 1260-1291 fence body whose diagnostic now points at \"slice-3 / slice-4 of\ - \ the #2717 rollout\"). Refine-gate `approve_continue` no longer raises.\n2.\ - \ AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705\ - \ Verified. `ThreadPoolExecutor(max_workers=len(plan_producers))` at line 942\ - \ dispatches `_spawn_plan_producer` for ARCHITECT, TASK_PLANNER, RISK_ANALYST\ - \ concurrently. Each producer gets its own worktree (`bundle.worktrees.create`,\ - \ line 1089), env-shaped spawn (lines 1091-1104 with `EGG_AGENT_ROLE`, `EGG_PHASE=\"\ - plan\"`, refine/plan artifact paths), and active-role sentinel write before\ - \ `bundle.spawner.spawn(...)`.\n3. AC \"reviewer_plan is spawned after each\ - \ CONSENSUS_PROPOSE\": \u26A0\uFE0F Functionally satisfied via a single reviewer\ - \ dispatch that records N ACKs on the tracker, not N reviewer spawns. `_spawn_plan_reviewer`\ - \ is called once (line 994) AFTER the producer ThreadPoolExecutor's `with` block\ - \ exits and AFTER `tracker.handle_propose(role.value, ...)` has fired for every\ - \ successful producer (line 976-987). The reviewer then ACKs each producer separately\ - \ (line 1011-1029 loop). The docstring at lines 842-856 explicitly justifies\ - \ the single-spawn-batches-ACKs design: \"the in-process bundle's spawner is\ - \ synchronous \u2014 `bundle.spawner.spawn(role, ...)` returns AFTER the subagent\ - \ finishes ... the spawn-completion IS the signal that the subagent proposed\ - \ / reviewed\". Reading the AC's \"after each CONSENSUS_PROPOSE\" as \"after\ - \ all CONSENSUS_PROPOSEs land\", the design is consistent with the task description\ - \ (\"After producers reach `CONSENSUS_PROPOSE`, `reviewer_plan` is spawned for\ - \ the ACK/NACK cycle\" \u2014 singular cycle) and the BRC outcome (one ACK per\ - \ producer edge) is identical to a multi-spawn variant on a synchronous spawner.\ - \ Non-blocking \u2014 design choice is documented and BRC tracker advances correctly.\n\ - 4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer\ - \ edge\": \u2705 Verified. `tracker.handle_confirmed(role.value)` is invoked\ - \ for every producer AND for `reviewer_plan` at line 1044-1052; `plan_eval =\ - \ tracker.evaluate()` (line 1054) carries `is_complete`, `blocking_agents`,\ - \ `unresolved_nack_details`; `_build_plan_gate_decision` (line 648-710) returns\ - \ a `HITLDecision(... phase=\"plan\")` with the canonical four `approve_continue\ - \ / request_changes / change_approach / stop` options on the converged path\ - \ and a `retry / abort` failure path on non-convergence. The generator yields\ - \ it at line 239-241.\n5. AC \"existing refine path still works\": \u2705 Verified.\ - \ Refine flow at lines 195-227 is structurally unchanged; refiner spawn, refine-gate\ - \ HITL yield, abort/preflight handling all preserved. Plan dispatch is gated\ - \ on `_answer_continues_past_refine(refine_answer)` returning true (line 226);\ - \ any other refine-gate answer (stop / change-approach / request-changes / abort\ - \ / retry) returns the refine artifact path without entering `_run_plan_phase`.\n\ - \n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376):\n\ - 1. AC \"loader returns rubric bodies for all four plan-team roles\": \u2705\ - \ Verified. `_RUBRIC_LANDED_ROLES` (lines 272-286) now contains `architect`,\ - \ `task_planner`, `risk_analyst`, `reviewer_plan` in addition to the slice-1\ - \ set. The fence at line 348 (`if role_name not in _RUBRIC_LANDED_ROLES`) no\ - \ longer rejects these roles; line 362-375 returns `rubric_path.read_text(...)`\ - \ when the markdown file is present on disk.\n2. AC \"implement-team roles still\ - \ raise ValueError with the 'follow-up slice 3' hint\": \u2705 Verified. `_ROLE_RUBRIC_SLICES`\ - \ (lines 254-262) maps the eight implement-team roles (`coder`, `tester`, `documenter`,\ - \ `reviewer_code`, `reviewer_code_holistic`, `reviewer_contract`, `reviewer_security`,\ - \ `reviewer_concurrency`) to `\"slice-3\"`, and the ValueError at line 356-360\ - \ interpolates `slice_hint` into the message (\"...deferred to follow-up slice-3\ - \ of issue #2717's rollout...\"), satisfying the hint contract.\n\n**TASK-2-5\ - \ \u2014 sandbox restrictions parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py\ - \ \u2014 NOT modified):\n- AC \"If R2 pass: task closed with note 'no-op: hooks\ - \ resolve role correctly; structural enforcement remains hook-side'\": \u2705\ - \ Verified. Coder commit message records the no-op close with the required note\ - \ (\"TASK-2-5 closes as no-op per slice-1's R2 = pass verdict ... structural\ - \ enforcement stays hook-side, no MCP-validator-side parallel layer needed\"\ - ). The slice-1 R2 nested-dispatch test (`integration_tests/regression/test_pretooluse_hook_nested.py:212-238`)\ - \ pins R2 = pass via three structured assertions (`result.denied`, `verdict[\"\ - decision\"] == \"block\"`, `written[\"r2_verdict\"] == \"pass\"`) \u2014 these\ - \ assertions ran during slice-1's BRC cycle and would have failed the slice-1\ - \ tester's propose otherwise. The no-op close is contractually defensible.\n\ - \n### Non-blocking observations (informational)\n\n- **orchestrator/substrate/in_process.py:942**\ - \ \u2014 task-2-1 *description* (not AC) names \"concurrent_executor.py seam\ - \ (line 569)\" as the intended dispatch path; the implementation uses a raw\ - \ `ThreadPoolExecutor` and records BRC transitions on the orchestrator side\ - \ rather than routing through `InProcessMessageBus`. The docstring at lines\ - \ 842-856 explains why (synchronous spawner makes the message-bus round-trip\ - \ a no-op duplicate). This deviates from the description's wording but does\ - \ NOT violate the AC (\"3 producers concurrently via the executor\" is satisfied;\ - \ the AC does not require `ConcurrentPhaseExecutor` specifically). Calling out\ - \ so a future slice that flips dispatch to async can revisit the seam choice.\n\ - - **TASK-2-5 R2-verdict artifact** \u2014 the AC for the contingent task (\"\ - see `.egg-state//r2-verdict.json` from TASK-1-5\") refers to a\ - \ runtime artifact that slice-1's test writes under `tmp_path / pipeline_id\ - \ / \"r2-verdict.json\"` (per `test_pretooluse_hook_nested.py:120-128`), not\ - \ under a committed path in this worktree. The verdict file therefore is NOT\ - \ inspectable post-hoc; the empirical proof rests on the slice-1 test assertions\ - \ having passed. This is a slice-1 handoff observation (already flagged in slice-1's\ - \ reviewer history as a \"downstream-handoff improvement\") and not a slice-2\ - \ coder concern; slice-5 R15 will need to re-derive the verdict if it cannot\ - \ read a persisted file.\n- **slice-1 contract task statuses** \u2014 slice-1\ - \ tasks (task-1-1 \u2026 task-1-9) still show `status: \"pending\"` in the contract\ - \ despite their commits being linked. This is a slice-1 contract-bookkeeping\ - \ issue (not slice-2), surfaced here so the operator knows the contract's per-task\ - \ `status` field is lagging the actual BRC state. The contract integrity check\ - \ on re-review will need to confirm slice-1 status before declaring the rollout\ - \ complete.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:01:10Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 255a348c-fb9e-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:01:13Z] reviewer_contract (CONSENSUS_CONFIRMED): Confirmed by reviewer_contract (pending_acks) - -Agent reviewer_contract cannot confirm: producers ['tester'] have never proposed (proposal_version == 0). All producers must propose before any agent can confirm consensus. - -````yaml -id: 7b1c6806-c72f-47 -phase: implement -metadata: - pending_acks: true - slice_id: slice-2 -```` - -### [2026-05-19T20:01:17Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 164db943-40ed-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:17.132192+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:01:27Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 64dc7b67-4d5d-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:01:30Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 440b79d9-d087-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:02:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c7e0db26-6729-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:02:17Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 1aea8b2f-3fb2-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:17.132192+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:02:27Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 694ac4af-d918-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:02:30Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: fc2f920b-9e33-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:03:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b050c955-8bcb-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:03:04Z] reviewer_concurrency → coder (CONSENSUS_NACK): NACK from reviewer_concurrency for coder - - -Concurrency-lens review of coder v1 (commit 3a466891e). I scanned the new `_run_plan_phase` ThreadPoolExecutor wiring, the per-thread spawn path (`_spawn_plan_producer` / `_spawn_plan_reviewer`), the shared sentinel write, the worktree allocation path, the heartbeat publisher's phase string, and the tracker register/propose/ack/confirmed call ordering. Two blocking concurrency findings. - -### Blocking - -1. **`orchestrator/substrate/in_process.py:1109-1111` / `:1164` (`_run_plan_phase` → `_spawn_plan_producer` → `_write_active_role_sentinel`)** — Last-writer-wins race on the role sentinel during concurrent multi-role producer dispatch. `_run_plan_phase` spawns architect / task_planner / risk_analyst via a `ThreadPoolExecutor(max_workers=3)`; each worker thread calls `self._write_active_role_sentinel(role.value)` immediately before `bundle.spawner.spawn(...)`. The sentinel is a **single-valued, per-user file** at `$HOME/.claude/egg-active-role.json` (line 1217), so three concurrent writes converge on whichever role wrote last. The producer's own docstring on `_write_active_role_sentinel` admits the limitation: "this single-valued, per-user file cannot disambiguate two concurrent sub-agents in different roles. The R2 deferral's multi-role rollout cannot use this sentinel for role-routing without a breaking change to the sentinel shape". The PreToolUse hook (`orchestrator/substrate/claude_code/hook_entry.py:697-742` `_resolve_active_role`) falls back to this sentinel whenever `EGG_AGENT_ROLE` is unset — which the documenter-owned slice-1 design names as the **nested-Agent-tool-dispatch** path the R2 verdict was meant to certify. Concrete failure mode: between roughly `T=0+2ε` (when the third thread overwrites the sentinel) and `T=spawn_complete` (when all three subagent processes have returned), every nested child spawned by architect or task_planner reads the sentinel as `risk_analyst` (or whichever role won the race) and evaluates its tool calls against the wrong role's allow-list — silently nondeterministic role-based authz for the new concurrent path. The slice-1 R2 verdict only covered the **single-role-at-a-time** parent→child case (`test_pretooluse_hook_denies_nested_child_write` runs one parent + one nested child); it is not the correct precedent for the "three concurrent role-holders share one sentinel" pattern slice-2 introduces, and the harness-faked smoke test in the commit message stubs the spawn so the race is invisible to the existing test surface. Fix: bind the sentinel to a per-spawn key (PID-of-the-child-process or per-thread file under `$HOME/.claude/egg-active-role-.json` resolved by the hook walking its own ancestor PIDs), or have the hook resolve via `os.environ` exclusively and stop writing the single-valued sentinel from concurrent paths. This fix MUST land in slice-2 — slice-2 is the first slice that introduces concurrent multi-role producers, and deferring the sentinel cleanup to a later slice leaves slice-2 shipping with a documented race in the first-tier authz path. - -2. **`orchestrator/substrate/in_process.py:392` (`_publish_heartbeat`)** — Heartbeat is hardcoded to `phase="refine"` after the generator enters the plan stage. The grep `phase=` shows three call sites that hard-code `phase="refine"` (line 392 in the heartbeat publisher, lines 586 + 645 in the refine/preflight HITL builders) and one site that correctly uses `phase="plan"` (line 709 in the plan HITL builder). The heartbeat thread keeps ticking every `_HEARTBEAT_INTERVAL = 5.0` s through the entire plan stage — three producer spawns + the reviewer spawn — and emits HEARTBEAT messages stamped with the stale phase. This is exactly the heartbeat-stall-window class of bug per #2012: any future monitor that filters heartbeats on `phase` (the orchestrator's stuck-phase-transition watchdog being the canonical consumer) will not see plan-phase liveness from the in-process orchestrator and may declare the agent stalled even though plan-phase work is progressing. Fix: track the current phase on the orchestrator (e.g. `self._current_phase = "refine"`, flip to `"plan"` at the top of `_run_plan_phase` and back as needed) and have `_publish_heartbeat` read from it instead of hard-coding the string. - -### Non-blocking - -- **`orchestrator/substrate/claude_code/worktree.py:117-127` (`Worktree.create`)** — Concurrent `git worktree add` invocations from the three plan-producer threads share the parent repo's `.git/worktrees/` and `.git/index.lock`. `subprocess.run(..., check=False, timeout=30)` silently swallows whatever git reports; the `target.mkdir(parents=True, exist_ok=True)` runs unconditionally before the subprocess call, so the spawner still gets a path even when the underlying `git worktree add` lost the lock race. The downstream effect is that any real (non-faked) producer that later does `git rev-parse HEAD` falls back to the `_SYNTHETIC_PLAN_COMMIT` constant — masking real git-side failures during a concurrent allocation flurry. Either inspect `result.returncode` + `result.stderr` and surface the "another git process seems to be running" outcome to the caller, or serialise `git worktree add` calls behind `self._lock` (the dict mutation lock already in place). - -- **`orchestrator/substrate/in_process.py:84-92` + `:413-420` (`_SYNTHETIC_PLAN_COMMIT = "ace1ace"`)** — All three producers stamp the same synthetic commit_sha when the harness fake doesn't supply one. Not a race in itself, but `PeerConsensusTracker.handle_propose` then sees three propose entries with identical `commit_sha`; any future flip-flop-count or version-anchoring logic keyed on `commit_sha` collapses the three role-distinct artifacts into one. Cheap mitigation: include the role abbreviation in the synthetic SHA (e.g. `f"ace1{role.value[:3]}"`) so per-producer ProposalPayload entries remain distinguishable in the tracker. - -- **`orchestrator/substrate/in_process.py:317-323` (`get_peer_consensus_tracker` → `create_peer_consensus_tracker` check-then-act)** — Not a race today because `_brc_review_loop` (line 318) only reads the tracker via `get_peer_consensus_tracker` and the slice-1 refiner does not register one, but the pattern is fragile. If a future maintainer adds a second `create_*` call site (e.g. background BRC tick that lazily creates a tracker), two threads can both observe `get_*` returning `None`, both enter `create_*`, and the second write under `_trackers_lock` (`orchestrator/peer_consensus.py:1891`) silently overwrites the first tracker — the BRC re-review thread is then holding a stale tracker reference. Cheaper to wrap the check-then-act in the existing module-level `_trackers_lock` here once. - - -````yaml -id: 55ccbb69-a657-48 -phase: implement -metadata: - payload: - reason: "\nConcurrency-lens review of coder v1 (commit 3a466891e). I scanned the\ - \ new `_run_plan_phase` ThreadPoolExecutor wiring, the per-thread spawn path\ - \ (`_spawn_plan_producer` / `_spawn_plan_reviewer`), the shared sentinel write,\ - \ the worktree allocation path, the heartbeat publisher's phase string, and\ - \ the tracker register/propose/ack/confirmed call ordering. Two blocking concurrency\ - \ findings.\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py:1109-1111`\ - \ / `:1164` (`_run_plan_phase` \u2192 `_spawn_plan_producer` \u2192 `_write_active_role_sentinel`)**\ - \ \u2014 Last-writer-wins race on the role sentinel during concurrent multi-role\ - \ producer dispatch. `_run_plan_phase` spawns architect / task_planner / risk_analyst\ - \ via a `ThreadPoolExecutor(max_workers=3)`; each worker thread calls `self._write_active_role_sentinel(role.value)`\ - \ immediately before `bundle.spawner.spawn(...)`. The sentinel is a **single-valued,\ - \ per-user file** at `$HOME/.claude/egg-active-role.json` (line 1217), so three\ - \ concurrent writes converge on whichever role wrote last. The producer's own\ - \ docstring on `_write_active_role_sentinel` admits the limitation: \"this single-valued,\ - \ per-user file cannot disambiguate two concurrent sub-agents in different roles.\ - \ The R2 deferral's multi-role rollout cannot use this sentinel for role-routing\ - \ without a breaking change to the sentinel shape\". The PreToolUse hook (`orchestrator/substrate/claude_code/hook_entry.py:697-742`\ - \ `_resolve_active_role`) falls back to this sentinel whenever `EGG_AGENT_ROLE`\ - \ is unset \u2014 which the documenter-owned slice-1 design names as the **nested-Agent-tool-dispatch**\ - \ path the R2 verdict was meant to certify. Concrete failure mode: between roughly\ - \ `T=0+2\u03B5` (when the third thread overwrites the sentinel) and `T=spawn_complete`\ - \ (when all three subagent processes have returned), every nested child spawned\ - \ by architect or task_planner reads the sentinel as `risk_analyst` (or whichever\ - \ role won the race) and evaluates its tool calls against the wrong role's allow-list\ - \ \u2014 silently nondeterministic role-based authz for the new concurrent path.\ - \ The slice-1 R2 verdict only covered the **single-role-at-a-time** parent\u2192\ - child case (`test_pretooluse_hook_denies_nested_child_write` runs one parent\ - \ + one nested child); it is not the correct precedent for the \"three concurrent\ - \ role-holders share one sentinel\" pattern slice-2 introduces, and the harness-faked\ - \ smoke test in the commit message stubs the spawn so the race is invisible\ - \ to the existing test surface. Fix: bind the sentinel to a per-spawn key (PID-of-the-child-process\ - \ or per-thread file under `$HOME/.claude/egg-active-role-.json` resolved\ - \ by the hook walking its own ancestor PIDs), or have the hook resolve via `os.environ`\ - \ exclusively and stop writing the single-valued sentinel from concurrent paths.\ - \ This fix MUST land in slice-2 \u2014 slice-2 is the first slice that introduces\ - \ concurrent multi-role producers, and deferring the sentinel cleanup to a later\ - \ slice leaves slice-2 shipping with a documented race in the first-tier authz\ - \ path.\n\n2. **`orchestrator/substrate/in_process.py:392` (`_publish_heartbeat`)**\ - \ \u2014 Heartbeat is hardcoded to `phase=\"refine\"` after the generator enters\ - \ the plan stage. The grep `phase=` shows three call sites that hard-code `phase=\"\ - refine\"` (line 392 in the heartbeat publisher, lines 586 + 645 in the refine/preflight\ - \ HITL builders) and one site that correctly uses `phase=\"plan\"` (line 709\ - \ in the plan HITL builder). The heartbeat thread keeps ticking every `_HEARTBEAT_INTERVAL\ - \ = 5.0` s through the entire plan stage \u2014 three producer spawns + the\ - \ reviewer spawn \u2014 and emits HEARTBEAT messages stamped with the stale\ - \ phase. This is exactly the heartbeat-stall-window class of bug per #2012:\ - \ any future monitor that filters heartbeats on `phase` (the orchestrator's\ - \ stuck-phase-transition watchdog being the canonical consumer) will not see\ - \ plan-phase liveness from the in-process orchestrator and may declare the agent\ - \ stalled even though plan-phase work is progressing. Fix: track the current\ - \ phase on the orchestrator (e.g. `self._current_phase = \"refine\"`, flip to\ - \ `\"plan\"` at the top of `_run_plan_phase` and back as needed) and have `_publish_heartbeat`\ - \ read from it instead of hard-coding the string.\n\n### Non-blocking\n\n- **`orchestrator/substrate/claude_code/worktree.py:117-127`\ - \ (`Worktree.create`)** \u2014 Concurrent `git worktree add` invocations from\ - \ the three plan-producer threads share the parent repo's `.git/worktrees/`\ - \ and `.git/index.lock`. `subprocess.run(..., check=False, timeout=30)` silently\ - \ swallows whatever git reports; the `target.mkdir(parents=True, exist_ok=True)`\ - \ runs unconditionally before the subprocess call, so the spawner still gets\ - \ a path even when the underlying `git worktree add` lost the lock race. The\ - \ downstream effect is that any real (non-faked) producer that later does `git\ - \ rev-parse HEAD` falls back to the `_SYNTHETIC_PLAN_COMMIT` constant \u2014\ - \ masking real git-side failures during a concurrent allocation flurry. Either\ - \ inspect `result.returncode` + `result.stderr` and surface the \"another git\ - \ process seems to be running\" outcome to the caller, or serialise `git worktree\ - \ add` calls behind `self._lock` (the dict mutation lock already in place).\n\ - \n- **`orchestrator/substrate/in_process.py:84-92` + `:413-420` (`_SYNTHETIC_PLAN_COMMIT\ - \ = \"ace1ace\"`)** \u2014 All three producers stamp the same synthetic commit_sha\ - \ when the harness fake doesn't supply one. Not a race in itself, but `PeerConsensusTracker.handle_propose`\ - \ then sees three propose entries with identical `commit_sha`; any future flip-flop-count\ - \ or version-anchoring logic keyed on `commit_sha` collapses the three role-distinct\ - \ artifacts into one. Cheap mitigation: include the role abbreviation in the\ - \ synthetic SHA (e.g. `f\"ace1{role.value[:3]}\"`) so per-producer ProposalPayload\ - \ entries remain distinguishable in the tracker.\n\n- **`orchestrator/substrate/in_process.py:317-323`\ - \ (`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker` check-then-act)**\ - \ \u2014 Not a race today because `_brc_review_loop` (line 318) only reads the\ - \ tracker via `get_peer_consensus_tracker` and the slice-1 refiner does not\ - \ register one, but the pattern is fragile. If a future maintainer adds a second\ - \ `create_*` call site (e.g. background BRC tick that lazily creates a tracker),\ - \ two threads can both observe `get_*` returning `None`, both enter `create_*`,\ - \ and the second write under `_trackers_lock` (`orchestrator/peer_consensus.py:1891`)\ - \ silently overwrites the first tracker \u2014 the BRC re-review thread is then\ - \ holding a stale tracker reference. Cheaper to wrap the check-then-act in the\ - \ existing module-level `_trackers_lock` here once.\n" - artifact_references: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - - orchestrator/substrate/claude_code/worktree.py - - orchestrator/substrate/claude_code/hook_entry.py - - orchestrator/peer_consensus.py - nack_version: 1 - reason: "\nConcurrency-lens review of coder v1 (commit 3a466891e). I scanned the\ - \ new `_run_plan_phase` ThreadPoolExecutor wiring, the per-thread spawn path (`_spawn_plan_producer`\ - \ / `_spawn_plan_reviewer`), the shared sentinel write, the worktree allocation\ - \ path, the heartbeat publisher's phase string, and the tracker register/propose/ack/confirmed\ - \ call ordering. Two blocking concurrency findings.\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py:1109-1111`\ - \ / `:1164` (`_run_plan_phase` \u2192 `_spawn_plan_producer` \u2192 `_write_active_role_sentinel`)**\ - \ \u2014 Last-writer-wins race on the role sentinel during concurrent multi-role\ - \ producer dispatch. `_run_plan_phase` spawns architect / task_planner / risk_analyst\ - \ via a `ThreadPoolExecutor(max_workers=3)`; each worker thread calls `self._write_active_role_sentinel(role.value)`\ - \ immediately before `bundle.spawner.spawn(...)`. The sentinel is a **single-valued,\ - \ per-user file** at `$HOME/.claude/egg-active-role.json` (line 1217), so three\ - \ concurrent writes converge on whichever role wrote last. The producer's own\ - \ docstring on `_write_active_role_sentinel` admits the limitation: \"this single-valued,\ - \ per-user file cannot disambiguate two concurrent sub-agents in different roles.\ - \ The R2 deferral's multi-role rollout cannot use this sentinel for role-routing\ - \ without a breaking change to the sentinel shape\". The PreToolUse hook (`orchestrator/substrate/claude_code/hook_entry.py:697-742`\ - \ `_resolve_active_role`) falls back to this sentinel whenever `EGG_AGENT_ROLE`\ - \ is unset \u2014 which the documenter-owned slice-1 design names as the **nested-Agent-tool-dispatch**\ - \ path the R2 verdict was meant to certify. Concrete failure mode: between roughly\ - \ `T=0+2\u03B5` (when the third thread overwrites the sentinel) and `T=spawn_complete`\ - \ (when all three subagent processes have returned), every nested child spawned\ - \ by architect or task_planner reads the sentinel as `risk_analyst` (or whichever\ - \ role won the race) and evaluates its tool calls against the wrong role's allow-list\ - \ \u2014 silently nondeterministic role-based authz for the new concurrent path.\ - \ The slice-1 R2 verdict only covered the **single-role-at-a-time** parent\u2192\ - child case (`test_pretooluse_hook_denies_nested_child_write` runs one parent +\ - \ one nested child); it is not the correct precedent for the \"three concurrent\ - \ role-holders share one sentinel\" pattern slice-2 introduces, and the harness-faked\ - \ smoke test in the commit message stubs the spawn so the race is invisible to\ - \ the existing test surface. Fix: bind the sentinel to a per-spawn key (PID-of-the-child-process\ - \ or per-thread file under `$HOME/.claude/egg-active-role-.json` resolved\ - \ by the hook walking its own ancestor PIDs), or have the hook resolve via `os.environ`\ - \ exclusively and stop writing the single-valued sentinel from concurrent paths.\ - \ This fix MUST land in slice-2 \u2014 slice-2 is the first slice that introduces\ - \ concurrent multi-role producers, and deferring the sentinel cleanup to a later\ - \ slice leaves slice-2 shipping with a documented race in the first-tier authz\ - \ path.\n\n2. **`orchestrator/substrate/in_process.py:392` (`_publish_heartbeat`)**\ - \ \u2014 Heartbeat is hardcoded to `phase=\"refine\"` after the generator enters\ - \ the plan stage. The grep `phase=` shows three call sites that hard-code `phase=\"\ - refine\"` (line 392 in the heartbeat publisher, lines 586 + 645 in the refine/preflight\ - \ HITL builders) and one site that correctly uses `phase=\"plan\"` (line 709 in\ - \ the plan HITL builder). The heartbeat thread keeps ticking every `_HEARTBEAT_INTERVAL\ - \ = 5.0` s through the entire plan stage \u2014 three producer spawns + the reviewer\ - \ spawn \u2014 and emits HEARTBEAT messages stamped with the stale phase. This\ - \ is exactly the heartbeat-stall-window class of bug per #2012: any future monitor\ - \ that filters heartbeats on `phase` (the orchestrator's stuck-phase-transition\ - \ watchdog being the canonical consumer) will not see plan-phase liveness from\ - \ the in-process orchestrator and may declare the agent stalled even though plan-phase\ - \ work is progressing. Fix: track the current phase on the orchestrator (e.g.\ - \ `self._current_phase = \"refine\"`, flip to `\"plan\"` at the top of `_run_plan_phase`\ - \ and back as needed) and have `_publish_heartbeat` read from it instead of hard-coding\ - \ the string.\n\n### Non-blocking\n\n- **`orchestrator/substrate/claude_code/worktree.py:117-127`\ - \ (`Worktree.create`)** \u2014 Concurrent `git worktree add` invocations from\ - \ the three plan-producer threads share the parent repo's `.git/worktrees/` and\ - \ `.git/index.lock`. `subprocess.run(..., check=False, timeout=30)` silently swallows\ - \ whatever git reports; the `target.mkdir(parents=True, exist_ok=True)` runs unconditionally\ - \ before the subprocess call, so the spawner still gets a path even when the underlying\ - \ `git worktree add` lost the lock race. The downstream effect is that any real\ - \ (non-faked) producer that later does `git rev-parse HEAD` falls back to the\ - \ `_SYNTHETIC_PLAN_COMMIT` constant \u2014 masking real git-side failures during\ - \ a concurrent allocation flurry. Either inspect `result.returncode` + `result.stderr`\ - \ and surface the \"another git process seems to be running\" outcome to the caller,\ - \ or serialise `git worktree add` calls behind `self._lock` (the dict mutation\ - \ lock already in place).\n\n- **`orchestrator/substrate/in_process.py:84-92`\ - \ + `:413-420` (`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`)** \u2014 All three producers\ - \ stamp the same synthetic commit_sha when the harness fake doesn't supply one.\ - \ Not a race in itself, but `PeerConsensusTracker.handle_propose` then sees three\ - \ propose entries with identical `commit_sha`; any future flip-flop-count or version-anchoring\ - \ logic keyed on `commit_sha` collapses the three role-distinct artifacts into\ - \ one. Cheap mitigation: include the role abbreviation in the synthetic SHA (e.g.\ - \ `f\"ace1{role.value[:3]}\"`) so per-producer ProposalPayload entries remain\ - \ distinguishable in the tracker.\n\n- **`orchestrator/substrate/in_process.py:317-323`\ - \ (`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker` check-then-act)**\ - \ \u2014 Not a race today because `_brc_review_loop` (line 318) only reads the\ - \ tracker via `get_peer_consensus_tracker` and the slice-1 refiner does not register\ - \ one, but the pattern is fragile. If a future maintainer adds a second `create_*`\ - \ call site (e.g. background BRC tick that lazily creates a tracker), two threads\ - \ can both observe `get_*` returning `None`, both enter `create_*`, and the second\ - \ write under `_trackers_lock` (`orchestrator/peer_consensus.py:1891`) silently\ - \ overwrites the first tracker \u2014 the BRC re-review thread is then holding\ - \ a stale tracker reference. Cheaper to wrap the check-then-act in the existing\ - \ module-level `_trackers_lock` here once.\n" - revision_count: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:03:04Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 3a4fab7a-b8e4-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:03:14Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 4a72960b-2434-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:03:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: bbc25f82-3d0f-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:03:30Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: c63cd500-0e96-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:03:55Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 0c208442-27df-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:55.888666+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:04:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 78e4febf-92b5-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:04:14Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 2427a35f-ef35-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:04:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: dfd566f6-4541-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:04:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 88b13973-ec42-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:05:13Z] reviewer_code_holistic → coder (CONSENSUS_NACK): NACK from reviewer_code_holistic for coder - - -Holistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own. - -### Blocking - -1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc ↔ code symmetry) — architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942–987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` — i.e. all three producers start in parallel. Consumers that require the architect to run first: - - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) — the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating). - - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` ("You run in parallel with `risk_analyst`, both downstream of `architect`"), `:93` and `:156` (required input `architect_output_path` — the architect's design decisions), `:275` ("Do not deviate from the architect's `key_design_decisions`"). - - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` ("You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)"). - - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 ("`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently"). - - User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed "architect-first → fanned-out producers → critical-edge review" data flow silently degrades to "three producers running on the refine analysis alone". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` — critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong. - -2. **Pass 4 (silent fallback) — reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002–1034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, …)` for every successful producer with a hardcoded `reason` string ("reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669–685) reads `plan_eval.is_complete` and renders the operator's options accordingly — so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected. - - This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136–1170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` — every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs. - - Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK — and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's "in-process spawn-completion IS the signal that the subagent proposed / reviewed" rationale is defensible for the propose half (the producer ran successfully → propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict. - -### Non-blocking - -- **Pass 2 — doc-claimed plan-HITL options diverge from the code.** `SKILL.md` "Plan HITL gate" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they "feed change requests back into a fresh plan cycle" / "kick the pipeline back to the refine phase", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim — selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to "not yet implemented; selecting these today exits the skill with the current plan".) -- **Pass 3 — dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098–1099, 1156–1157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` — no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them). -- **Pass 4 — bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than "blocking_agents=['architect']". -- **Pass 4 — `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end. -- **`_SYNTHETIC_PLAN_COMMIT = "ace1ace"`** — fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file). - -If blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest. - - -````yaml -id: d26fe55a-284d-47 -phase: implement -metadata: - payload: - reason: "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four\ - \ mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers\ - \ plus several non-blocking asymmetries that the line-by-line review will not\ - \ own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2\ - \ (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both\ - \ the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase`\ - \ (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst`\ - \ to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks\ - \ `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers\ - \ that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398`\ - \ (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`)\ - \ \u2014 the canonical role-dependency declaration the rest of the orchestrator\ - \ honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n\ - \ - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run\ - \ in parallel with `risk_analyst`, both downstream of `architect`\"), `:93`\ - \ and `:156` (required input `architect_output_path` \u2014 the architect's\ - \ design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\"\ - ).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run\ - \ first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel\ - \ based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`\ - \ plan-phase table (`architect | First, solo`, `task_planner | Concurrently\ - \ with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner`\ - \ dispatches `architect` solo first; once its handoff lands, `task_planner`\ - \ and `risk_analyst` are spawned concurrently\").\n\n User-visible failure\ - \ shape: `task_planner` and `risk_analyst` start before the architect's handoff\ - \ JSON exists. Their rubric-required input path `architect_output_path` resolves\ - \ to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/`\ - \ from a previous run). The producers emit a plan that ignores the architect's\ - \ `key_design_decisions` and `ordering_constraints`, the in-process orchestrator\ - \ records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan\ - \ is dispatched after all three finish. The doc-claimed \"architect-first \u2192\ - \ fanned-out producers \u2192 critical-edge review\" data flow silently degrades\ - \ to \"three producers running on the refine analysis alone\". Fix: spawn `architect`\ - \ synchronously first, await its `AgentResult`, then submit `task_planner` +\ - \ `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential\ - \ `pool.submit()` calls inside an outer two-worker pool). Pass the architect's\ - \ `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g.\ - \ `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging\ - \ in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout\ - \ order. The existing review-graph (`get_default_plan_graph` \u2014 critical\ - \ on architect + task_planner, advisory on risk_analyst) is correct; it is only\ - \ the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014\ - \ reviewer_plan's actual verdict is discarded; the in-process orchestrator forges\ - \ an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase`\ - \ lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator\ - \ inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls\ - \ `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every\ - \ successful producer with a hardcoded `reason` string (\"reviewer_plan ACK\ - \ in #2717 slice-2: every plan-team producer ran successfully on the claude-code\ - \ substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"\ - ). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json`\ - \ (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK\ - \ verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate\ - \ (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete`\ - \ and renders the operator's options accordingly \u2014 so a reviewer that wrote\ - \ a per-edge NACK in its JSON output is invisible to the operator, who sees\ - \ `is_complete=True` and approves a plan the reviewer actually rejected.\n\n\ - \ This is the canonical silent-fallback shape: the safety floor (the BRC tracker\ - \ advances to CONFIRMED) is preserved, but the operator-visible signal (the\ - \ reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines\ - \ 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()`\ - \ \u2014 every producer maps to the same `plan_artifact_path`, so the env var\ - \ ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text\ - \ is a single-path tuple. Even if the reviewer agent did try to render a per-edge\ - \ verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer`\ - \ return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json`\ - \ per the doc-promised layout) instead of always returning `plan_artifact_path`.\ - \ Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator\ - \ parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls.\ - \ At minimum: when the reviewer's handoff is unparseable or the producer-specific\ - \ output is missing, NACK that edge rather than silently forging an ACK \u2014\ - \ and surface the failure in the placeholder plan artifact so the operator can\ - \ act on it. The docstring's \"in-process spawn-completion IS the signal that\ - \ the subagent proposed / reviewed\" rationale is defensible for the propose\ - \ half (the producer ran successfully \u2192 propose happened), but it is not\ - \ defensible for the ACK/NACK half because the reviewer's exit code does not\ - \ encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL\ - \ options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve`\ - \ / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision`\ - \ actually emits `approve_continue` / `request_changes` / `change_approach`\ - \ / `stop`. Operators see the code's names so they are not stuck, but the doc\ - \ table is misleading. (Same nit applies to `request_changes` and `change_approach`:\ - \ SKILL.md promises they \"feed change requests back into a fresh plan cycle\"\ - \ / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only\ - \ fences on `approve_continue` and the generator otherwise returns the artifact\ - \ path verbatim \u2014 selecting either of the two revision options today silently\ - \ completes the run with the unchanged plan. Documenter should either land the\ - \ loop-back or downgrade the doc to \"not yet implemented; selecting these today\ - \ exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env\ - \ vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`,\ - \ `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env\ - \ (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'`\ - \ returns matches only in `in_process.py` \u2014 no rubric, no harness handler,\ - \ no agent tool consumes them. The paths are also passed inline via `prompt_text`,\ - \ so this is dead code rather than a broken feature, but the producer-without-consumer\ - \ pattern is exactly what the holistic lens exists to flag. Either drop the\ - \ env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH`\ - \ conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception:\ - \ pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed`\ - \ (line 1047).** Comment says the eval snapshot will surface the symptom, which\ - \ is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed\ - \ payload, unregistered agent) is silently discarded. At least log via `logging.warning`\ - \ so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\"\ - .\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from\ - \ per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer\ - \ roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha\ - \ never reach the placeholder body. Add reviewer diagnostics alongside the producer\ - \ diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n\ - - **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path\ - \ (no gateway commit-existence check), but worth a one-line comment noting it\ - \ must never escape the in-process driver (e.g. if the production HTTP daemon\ - \ ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real\ - \ SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering)\ - \ and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking\ - \ polish. The line-by-line review owns the rest.\n" - artifact_references: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - orchestrator/review_graph.py - - shared/egg_contracts/agent_roles.py - - orchestrator/substrate/claude_code/spawner.py - nack_version: 1 - reason: "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory\ - \ passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus\ - \ several non-blocking asymmetries that the line-by-line review will not own.\n\ - \n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194\ - \ code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency\ - \ contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase`\ - \ (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst`\ - \ to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks\ - \ `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers\ - \ that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398`\ - \ (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`)\ - \ \u2014 the canonical role-dependency declaration the rest of the orchestrator\ - \ honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n\ - \ - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run\ - \ in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and\ - \ `:156` (required input `architect_output_path` \u2014 the architect's design\ - \ decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\"\ - ).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run\ - \ first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel\ - \ based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`\ - \ plan-phase table (`architect | First, solo`, `task_planner | Concurrently with\ - \ risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner`\ - \ dispatches `architect` solo first; once its handoff lands, `task_planner` and\ - \ `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape:\ - \ `task_planner` and `risk_analyst` start before the architect's handoff JSON\ - \ exists. Their rubric-required input path `architect_output_path` resolves to\ - \ a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/`\ - \ from a previous run). The producers emit a plan that ignores the architect's\ - \ `key_design_decisions` and `ordering_constraints`, the in-process orchestrator\ - \ records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan\ - \ is dispatched after all three finish. The doc-claimed \"architect-first \u2192\ - \ fanned-out producers \u2192 critical-edge review\" data flow silently degrades\ - \ to \"three producers running on the refine analysis alone\". Fix: spawn `architect`\ - \ synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst`\ - \ to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()`\ - \ calls inside an outer two-worker pool). Pass the architect's `commit_sha` /\ - \ handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`)\ - \ and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder`\ - \ so a harness-faked run still records the architect-then-fanout order. The existing\ - \ review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner,\ - \ advisory on risk_analyst) is correct; it is only the spawn-ordering that is\ - \ wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict\ - \ is discarded; the in-process orchestrator forges an ACK for every producer whose\ - \ exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after\ - \ `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`.\ - \ If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value,\ - \ producer.value, \u2026)` for every successful producer with a hardcoded `reason`\ - \ string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully\ - \ on the claude-code substrate; in-process BRC tracker records the ACK on the\ - \ reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json`\ - \ (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK\ - \ verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision`\ - \ lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's\ - \ options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON\ - \ output is invisible to the operator, who sees `is_complete=True` and approves\ - \ a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback\ - \ shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved,\ - \ but the operator-visible signal (the reviewer's verdict) is masked. Compounding\ - \ it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS`\ - \ by deduping `producer_artifacts.values()` \u2014 every producer maps to the\ - \ same `plan_artifact_path`, so the env var ends up as a one-element path list,\ - \ and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even\ - \ if the reviewer agent did try to render a per-edge verdict, it never sees per-producer\ - \ outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output\ - \ path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised\ - \ layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer`\ - \ write its own output JSON and have the orchestrator parse that JSON to drive\ - \ per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's\ - \ handoff is unparseable or the producer-specific output is missing, NACK that\ - \ edge rather than silently forging an ACK \u2014 and surface the failure in the\ - \ placeholder plan artifact so the operator can act on it. The docstring's \"\ - in-process spawn-completion IS the signal that the subagent proposed / reviewed\"\ - \ rationale is defensible for the propose half (the producer ran successfully\ - \ \u2192 propose happened), but it is not defensible for the ACK/NACK half because\ - \ the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n\ - - **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md`\ - \ \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` /\ - \ `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes`\ - \ / `change_approach` / `stop`. Operators see the code's names so they are not\ - \ stuck, but the doc table is misleading. (Same nit applies to `request_changes`\ - \ and `change_approach`: SKILL.md promises they \"feed change requests back into\ - \ a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence`\ - \ only fences on `approve_continue` and the generator otherwise returns the artifact\ - \ path verbatim \u2014 selecting either of the two revision options today silently\ - \ completes the run with the unchanged plan. Documenter should either land the\ - \ loop-back or downgrade the doc to \"not yet implemented; selecting these today\ - \ exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.**\ - \ `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`,\ - \ `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env\ - \ (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'`\ - \ returns matches only in `in_process.py` \u2014 no rubric, no harness handler,\ - \ no agent tool consumes them. The paths are also passed inline via `prompt_text`,\ - \ so this is dead code rather than a broken feature, but the producer-without-consumer\ - \ pattern is exactly what the holistic lens exists to flag. Either drop the env\ - \ vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH`\ - \ conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception:\ - \ pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed`\ - \ (line 1047).** Comment says the eval snapshot will surface the symptom, which\ - \ is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed\ - \ payload, unregistered agent) is silently discarded. At least log via `logging.warning`\ - \ so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\"\ - .\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from\ - \ per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer\ - \ roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha\ - \ never reach the placeholder body. Add reviewer diagnostics alongside the producer\ - \ diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n\ - - **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path\ - \ (no gateway commit-existence check), but worth a one-line comment noting it\ - \ must never escape the in-process driver (e.g. if the production HTTP daemon\ - \ ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA\ - \ and could land in a BRC history file).\n\nIf blocker 1 (architect ordering)\ - \ and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking\ - \ polish. The line-by-line review owns the rest.\n" - revision_count: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:05:13Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 820f8491-5df2-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:55.888666+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:05:13Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 4e8fd35d-cebb-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:05:13Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: a10538d7-8db2-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:05:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 615d9022-f4eb-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:05:20Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: c0adb44c-78c5-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:05:20.444313+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:05:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5320529c-0fb8-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:05:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 30bc26ba-bcc8-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:05:53Z] reviewer_code_holistic → coder (CONSENSUS_NACK): NACK from reviewer_code_holistic for coder - - -Holistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus several non-blocking asymmetries that the line-by-line review will not own. - -### Blocking - -1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc ↔ code symmetry) — architect spawn ordering contradicts both the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase` (lines 942–987) submits `architect`, `task_planner`, and `risk_analyst` to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks `as_completed()` — i.e. all three producers start in parallel. Consumers that require the architect to run first: - - `shared/egg_contracts/agent_roles.py:398` (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`) — the canonical role-dependency declaration the rest of the orchestrator honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating). - - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` ("You run in parallel with `risk_analyst`, both downstream of `architect`"), `:93` and `:156` (required input `architect_output_path` — the architect's design decisions), `:275` ("Do not deviate from the architect's `key_design_decisions`"). - - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` ("You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output)"). - - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` plan-phase table (`architect | First, solo`, `task_planner | Concurrently with risk_analyst, downstream of the architect`) and step 8 ("`ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently"). - - User-visible failure shape: `task_planner` and `risk_analyst` start before the architect's handoff JSON exists. Their rubric-required input path `architect_output_path` resolves to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/` from a previous run). The producers emit a plan that ignores the architect's `key_design_decisions` and `ordering_constraints`, the in-process orchestrator records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan is dispatched after all three finish. The doc-claimed "architect-first → fanned-out producers → critical-edge review" data flow silently degrades to "three producers running on the refine analysis alone". Fix: spawn `architect` synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()` calls inside an outer two-worker pool). Pass the architect's `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout order. The existing review-graph (`get_default_plan_graph` — critical on architect + task_planner, advisory on risk_analyst) is correct; it is only the spawn-ordering that is wrong. - -2. **Pass 4 (silent fallback) — reviewer_plan's actual verdict is discarded; the in-process orchestrator forges an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase` lines 1002–1034: after `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value, producer.value, …)` for every successful producer with a hardcoded `reason` string ("reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully on the claude-code substrate; in-process BRC tracker records the ACK on the reviewer's behalf"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json` (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision` lines 669–685) reads `plan_eval.is_complete` and renders the operator's options accordingly — so a reviewer that wrote a per-edge NACK in its JSON output is invisible to the operator, who sees `is_complete=True` and approves a plan the reviewer actually rejected. - - This is the canonical silent-fallback shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved, but the operator-visible signal (the reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines 1136–1170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()` — every producer maps to the same `plan_artifact_path`, so the env var ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even if the reviewer agent did try to render a per-edge verdict, it never sees per-producer outputs. - - Fix: have `_spawn_plan_producer` return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's handoff is unparseable or the producer-specific output is missing, NACK that edge rather than silently forging an ACK — and surface the failure in the placeholder plan artifact so the operator can act on it. The docstring's "in-process spawn-completion IS the signal that the subagent proposed / reviewed" rationale is defensible for the propose half (the producer ran successfully → propose happened), but it is not defensible for the ACK/NACK half because the reviewer's exit code does not encode its verdict. - -### Non-blocking - -- **Pass 2 — doc-claimed plan-HITL options diverge from the code.** `SKILL.md` "Plan HITL gate" lists `approve` / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes` / `change_approach` / `stop`. Operators see the code's names so they are not stuck, but the doc table is misleading. (Same nit applies to `request_changes` and `change_approach`: SKILL.md promises they "feed change requests back into a fresh plan cycle" / "kick the pipeline back to the refine phase", but `_maybe_fence` only fences on `approve_continue` and the generator otherwise returns the artifact path verbatim — selecting either of the two revision options today silently completes the run with the unchanged plan. Documenter should either land the loop-back or downgrade the doc to "not yet implemented; selecting these today exits the skill with the current plan".) -- **Pass 3 — dead-end env vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`, `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env (lines 1098–1099, 1156–1157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'` returns matches only in `in_process.py` — no rubric, no harness handler, no agent tool consumes them. The paths are also passed inline via `prompt_text`, so this is dead code rather than a broken feature, but the producer-without-consumer pattern is exactly what the holistic lens exists to flag. Either drop the env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH` conventions if you want to keep them). -- **Pass 4 — bare `except Exception: pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed` (line 1047).** Comment says the eval snapshot will surface the symptom, which is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed payload, unregistered agent) is silently discarded. At least log via `logging.warning` so operators debugging a stuck plan gate get something better than "blocking_agents=['architect']". -- **Pass 4 — `_format_plan_placeholder` excludes `reviewer_plan` from per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha never reach the placeholder body. Add reviewer diagnostics alongside the producer diagnostics so the operator at the plan HITL gate sees what happened end-to-end. -- **`_SYNTHETIC_PLAN_COMMIT = "ace1ace"`** — fine in the in-process path (no gateway commit-existence check), but worth a one-line comment noting it must never escape the in-process driver (e.g. if the production HTTP daemon ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA and could land in a BRC history file). - -If blocker 1 (architect ordering) and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking polish. The line-by-line review owns the rest. - - -````yaml -id: dfe9b89c-f837-41 -phase: implement -metadata: - payload: - reason: "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four\ - \ mandatory passes; passes 1, 2, and 4 surface two architecture-coherence blockers\ - \ plus several non-blocking asymmetries that the line-by-line review will not\ - \ own.\n\n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2\ - \ (doc \u2194 code symmetry) \u2014 architect spawn ordering contradicts both\ - \ the role-dependency contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase`\ - \ (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst`\ - \ to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks\ - \ `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers\ - \ that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398`\ - \ (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`)\ - \ \u2014 the canonical role-dependency declaration the rest of the orchestrator\ - \ honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n\ - \ - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run\ - \ in parallel with `risk_analyst`, both downstream of `architect`\"), `:93`\ - \ and `:156` (required input `architect_output_path` \u2014 the architect's\ - \ design decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\"\ - ).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run\ - \ first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel\ - \ based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`\ - \ plan-phase table (`architect | First, solo`, `task_planner | Concurrently\ - \ with risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner`\ - \ dispatches `architect` solo first; once its handoff lands, `task_planner`\ - \ and `risk_analyst` are spawned concurrently\").\n\n User-visible failure\ - \ shape: `task_planner` and `risk_analyst` start before the architect's handoff\ - \ JSON exists. Their rubric-required input path `architect_output_path` resolves\ - \ to a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/`\ - \ from a previous run). The producers emit a plan that ignores the architect's\ - \ `key_design_decisions` and `ordering_constraints`, the in-process orchestrator\ - \ records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan\ - \ is dispatched after all three finish. The doc-claimed \"architect-first \u2192\ - \ fanned-out producers \u2192 critical-edge review\" data flow silently degrades\ - \ to \"three producers running on the refine analysis alone\". Fix: spawn `architect`\ - \ synchronously first, await its `AgentResult`, then submit `task_planner` +\ - \ `risk_analyst` to a `ThreadPoolExecutor(max_workers=2)` (or two sequential\ - \ `pool.submit()` calls inside an outer two-worker pool). Pass the architect's\ - \ `commit_sha` / handoff path into the downstream producers' `spawn_env` (e.g.\ - \ `EGG_ARCHITECT_OUTPUT_PATH`) and into their `prompt_text`. Mirror the staging\ - \ in `_format_plan_placeholder` so a harness-faked run still records the architect-then-fanout\ - \ order. The existing review-graph (`get_default_plan_graph` \u2014 critical\ - \ on architect + task_planner, advisory on risk_analyst) is correct; it is only\ - \ the spawn-ordering that is wrong.\n\n2. **Pass 4 (silent fallback) \u2014\ - \ reviewer_plan's actual verdict is discarded; the in-process orchestrator forges\ - \ an ACK for every producer whose exit code was 0.** `in_process.py:_run_plan_phase`\ - \ lines 1002\u20131034: after `_spawn_plan_reviewer` returns, the orchestrator\ - \ inspects only `reviewer_exit_code`. If it is 0, the code unconditionally calls\ - \ `tracker.handle_ack(plan_reviewer.value, producer.value, \u2026)` for every\ - \ successful producer with a hardcoded `reason` string (\"reviewer_plan ACK\ - \ in #2717 slice-2: every plan-team producer ran successfully on the claude-code\ - \ substrate; in-process BRC tracker records the ACK on the reviewer's behalf\"\ - ). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json`\ - \ (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK\ - \ verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate\ - \ (`_build_plan_gate_decision` lines 669\u2013685) reads `plan_eval.is_complete`\ - \ and renders the operator's options accordingly \u2014 so a reviewer that wrote\ - \ a per-edge NACK in its JSON output is invisible to the operator, who sees\ - \ `is_complete=True` and approves a plan the reviewer actually rejected.\n\n\ - \ This is the canonical silent-fallback shape: the safety floor (the BRC tracker\ - \ advances to CONFIRMED) is preserved, but the operator-visible signal (the\ - \ reviewer's verdict) is masked. Compounding it: `_spawn_plan_reviewer` (lines\ - \ 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS` by deduping `producer_artifacts.values()`\ - \ \u2014 every producer maps to the same `plan_artifact_path`, so the env var\ - \ ends up as a one-element path list, and `producer_artifact_paths` in the prompt_text\ - \ is a single-path tuple. Even if the reviewer agent did try to render a per-edge\ - \ verdict, it never sees per-producer outputs.\n\n Fix: have `_spawn_plan_producer`\ - \ return a producer-specific output path (e.g. `.egg-state/agent-outputs/--output.json`\ - \ per the doc-promised layout) instead of always returning `plan_artifact_path`.\ - \ Have `_spawn_plan_reviewer` write its own output JSON and have the orchestrator\ - \ parse that JSON to drive per-producer `handle_ack` vs `handle_nack` calls.\ - \ At minimum: when the reviewer's handoff is unparseable or the producer-specific\ - \ output is missing, NACK that edge rather than silently forging an ACK \u2014\ - \ and surface the failure in the placeholder plan artifact so the operator can\ - \ act on it. The docstring's \"in-process spawn-completion IS the signal that\ - \ the subagent proposed / reviewed\" rationale is defensible for the propose\ - \ half (the producer ran successfully \u2192 propose happened), but it is not\ - \ defensible for the ACK/NACK half because the reviewer's exit code does not\ - \ encode its verdict.\n\n### Non-blocking\n\n- **Pass 2 \u2014 doc-claimed plan-HITL\ - \ options diverge from the code.** `SKILL.md` \"Plan HITL gate\" lists `approve`\ - \ / `request_changes` / `change_approach` / `stop`; `_build_plan_gate_decision`\ - \ actually emits `approve_continue` / `request_changes` / `change_approach`\ - \ / `stop`. Operators see the code's names so they are not stuck, but the doc\ - \ table is misleading. (Same nit applies to `request_changes` and `change_approach`:\ - \ SKILL.md promises they \"feed change requests back into a fresh plan cycle\"\ - \ / \"kick the pipeline back to the refine phase\", but `_maybe_fence` only\ - \ fences on `approve_continue` and the generator otherwise returns the artifact\ - \ path verbatim \u2014 selecting either of the two revision options today silently\ - \ completes the run with the unchanged plan. Documenter should either land the\ - \ loop-back or downgrade the doc to \"not yet implemented; selecting these today\ - \ exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env\ - \ vars.** `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`,\ - \ `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env\ - \ (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'`\ - \ returns matches only in `in_process.py` \u2014 no rubric, no harness handler,\ - \ no agent tool consumes them. The paths are also passed inline via `prompt_text`,\ - \ so this is dead code rather than a broken feature, but the producer-without-consumer\ - \ pattern is exactly what the holistic lens exists to flag. Either drop the\ - \ env vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH`\ - \ conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception:\ - \ pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed`\ - \ (line 1047).** Comment says the eval snapshot will surface the symptom, which\ - \ is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed\ - \ payload, unregistered agent) is silently discarded. At least log via `logging.warning`\ - \ so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\"\ - .\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from\ - \ per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer\ - \ roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha\ - \ never reach the placeholder body. Add reviewer diagnostics alongside the producer\ - \ diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n\ - - **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path\ - \ (no gateway commit-existence check), but worth a one-line comment noting it\ - \ must never escape the in-process driver (e.g. if the production HTTP daemon\ - \ ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real\ - \ SHA and could land in a BRC history file).\n\nIf blocker 1 (architect ordering)\ - \ and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking\ - \ polish. The line-by-line review owns the rest.\n" - artifact_references: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - orchestrator/review_graph.py - - shared/egg_contracts/agent_roles.py - - orchestrator/substrate/claude_code/spawner.py - nack_version: 1 - reason: "\nHolistic NACK on coder slice-2 v1 (commit 3a466891e). Ran all four mandatory\ - \ passes; passes 1, 2, and 4 surface two architecture-coherence blockers plus\ - \ several non-blocking asymmetries that the line-by-line review will not own.\n\ - \n### Blocking\n\n1. **Pass 1 (end-to-end primary use case) + Pass 2 (doc \u2194\ - \ code symmetry) \u2014 architect spawn ordering contradicts both the role-dependency\ - \ contract and every plan-team rubric.** Producer: `orchestrator/substrate/in_process.py:_run_plan_phase`\ - \ (lines 942\u2013987) submits `architect`, `task_planner`, and `risk_analyst`\ - \ to a single `ThreadPoolExecutor(max_workers=len(plan_producers))` and walks\ - \ `as_completed()` \u2014 i.e. all three producers start in parallel. Consumers\ - \ that require the architect to run first:\n - `shared/egg_contracts/agent_roles.py:398`\ - \ (`TASK_PLANNER_ROLE.dependencies=[AgentRole.ARCHITECT]`) and `:422` (`RISK_ANALYST_ROLE.dependencies=[AgentRole.ARCHITECT]`)\ - \ \u2014 the canonical role-dependency declaration the rest of the orchestrator\ - \ honours (see `orchestrator/routes/pipelines.py:5827` analysis-role gating).\n\ - \ - `plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md:23` (\"You run\ - \ in parallel with `risk_analyst`, both downstream of `architect`\"), `:93` and\ - \ `:156` (required input `architect_output_path` \u2014 the architect's design\ - \ decisions), `:275` (\"Do not deviate from the architect's `key_design_decisions`\"\ - ).\n - `plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md:23` (\"You run\ - \ first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel\ - \ based on your output)\").\n - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`\ - \ plan-phase table (`architect | First, solo`, `task_planner | Concurrently with\ - \ risk_analyst, downstream of the architect`) and step 8 (\"`ClaudeCodeSpawner`\ - \ dispatches `architect` solo first; once its handoff lands, `task_planner` and\ - \ `risk_analyst` are spawned concurrently\").\n\n User-visible failure shape:\ - \ `task_planner` and `risk_analyst` start before the architect's handoff JSON\ - \ exists. Their rubric-required input path `architect_output_path` resolves to\ - \ a missing file (or to whatever stale architect output is sitting in `.egg-state/agent-outputs/`\ - \ from a previous run). The producers emit a plan that ignores the architect's\ - \ `key_design_decisions` and `ordering_constraints`, the in-process orchestrator\ - \ records a CONSENSUS_PROPOSE on each anyway (exit_code-only gate), and reviewer_plan\ - \ is dispatched after all three finish. The doc-claimed \"architect-first \u2192\ - \ fanned-out producers \u2192 critical-edge review\" data flow silently degrades\ - \ to \"three producers running on the refine analysis alone\". Fix: spawn `architect`\ - \ synchronously first, await its `AgentResult`, then submit `task_planner` + `risk_analyst`\ - \ to a `ThreadPoolExecutor(max_workers=2)` (or two sequential `pool.submit()`\ - \ calls inside an outer two-worker pool). Pass the architect's `commit_sha` /\ - \ handoff path into the downstream producers' `spawn_env` (e.g. `EGG_ARCHITECT_OUTPUT_PATH`)\ - \ and into their `prompt_text`. Mirror the staging in `_format_plan_placeholder`\ - \ so a harness-faked run still records the architect-then-fanout order. The existing\ - \ review-graph (`get_default_plan_graph` \u2014 critical on architect + task_planner,\ - \ advisory on risk_analyst) is correct; it is only the spawn-ordering that is\ - \ wrong.\n\n2. **Pass 4 (silent fallback) \u2014 reviewer_plan's actual verdict\ - \ is discarded; the in-process orchestrator forges an ACK for every producer whose\ - \ exit code was 0.** `in_process.py:_run_plan_phase` lines 1002\u20131034: after\ - \ `_spawn_plan_reviewer` returns, the orchestrator inspects only `reviewer_exit_code`.\ - \ If it is 0, the code unconditionally calls `tracker.handle_ack(plan_reviewer.value,\ - \ producer.value, \u2026)` for every successful producer with a hardcoded `reason`\ - \ string (\"reviewer_plan ACK in #2717 slice-2: every plan-team producer ran successfully\ - \ on the claude-code substrate; in-process BRC tracker records the ACK on the\ - \ reviewer's behalf\"). The reviewer's actual handoff JSON at `.egg-state/agent-outputs/-reviewer_plan-output.json`\ - \ (which `reviewer_plan.md` instructs the agent to write with per-producer ACK/NACK\ - \ verdicts) is never read. Consumers of the BRC tracker: the plan HITL gate (`_build_plan_gate_decision`\ - \ lines 669\u2013685) reads `plan_eval.is_complete` and renders the operator's\ - \ options accordingly \u2014 so a reviewer that wrote a per-edge NACK in its JSON\ - \ output is invisible to the operator, who sees `is_complete=True` and approves\ - \ a plan the reviewer actually rejected.\n\n This is the canonical silent-fallback\ - \ shape: the safety floor (the BRC tracker advances to CONFIRMED) is preserved,\ - \ but the operator-visible signal (the reviewer's verdict) is masked. Compounding\ - \ it: `_spawn_plan_reviewer` (lines 1136\u20131170) builds `EGG_PRODUCER_ARTIFACT_PATHS`\ - \ by deduping `producer_artifacts.values()` \u2014 every producer maps to the\ - \ same `plan_artifact_path`, so the env var ends up as a one-element path list,\ - \ and `producer_artifact_paths` in the prompt_text is a single-path tuple. Even\ - \ if the reviewer agent did try to render a per-edge verdict, it never sees per-producer\ - \ outputs.\n\n Fix: have `_spawn_plan_producer` return a producer-specific output\ - \ path (e.g. `.egg-state/agent-outputs/--output.json` per the doc-promised\ - \ layout) instead of always returning `plan_artifact_path`. Have `_spawn_plan_reviewer`\ - \ write its own output JSON and have the orchestrator parse that JSON to drive\ - \ per-producer `handle_ack` vs `handle_nack` calls. At minimum: when the reviewer's\ - \ handoff is unparseable or the producer-specific output is missing, NACK that\ - \ edge rather than silently forging an ACK \u2014 and surface the failure in the\ - \ placeholder plan artifact so the operator can act on it. The docstring's \"\ - in-process spawn-completion IS the signal that the subagent proposed / reviewed\"\ - \ rationale is defensible for the propose half (the producer ran successfully\ - \ \u2192 propose happened), but it is not defensible for the ACK/NACK half because\ - \ the reviewer's exit code does not encode its verdict.\n\n### Non-blocking\n\n\ - - **Pass 2 \u2014 doc-claimed plan-HITL options diverge from the code.** `SKILL.md`\ - \ \"Plan HITL gate\" lists `approve` / `request_changes` / `change_approach` /\ - \ `stop`; `_build_plan_gate_decision` actually emits `approve_continue` / `request_changes`\ - \ / `change_approach` / `stop`. Operators see the code's names so they are not\ - \ stuck, but the doc table is misleading. (Same nit applies to `request_changes`\ - \ and `change_approach`: SKILL.md promises they \"feed change requests back into\ - \ a fresh plan cycle\" / \"kick the pipeline back to the refine phase\", but `_maybe_fence`\ - \ only fences on `approve_continue` and the generator otherwise returns the artifact\ - \ path verbatim \u2014 selecting either of the two revision options today silently\ - \ completes the run with the unchanged plan. Documenter should either land the\ - \ loop-back or downgrade the doc to \"not yet implemented; selecting these today\ - \ exits the skill with the current plan\".)\n- **Pass 3 \u2014 dead-end env vars.**\ - \ `_spawn_plan_producer` / `_spawn_plan_reviewer` set `EGG_REFINE_ARTIFACT_PATH`,\ - \ `EGG_PLAN_ARTIFACT_PATH`, and `EGG_PRODUCER_ARTIFACT_PATHS` in the spawn env\ - \ (lines 1098\u20131099, 1156\u20131157). `grep -r 'EGG_REFINE_ARTIFACT_PATH|EGG_PLAN_ARTIFACT_PATH|EGG_PRODUCER_ARTIFACT_PATHS'`\ - \ returns matches only in `in_process.py` \u2014 no rubric, no harness handler,\ - \ no agent tool consumes them. The paths are also passed inline via `prompt_text`,\ - \ so this is dead code rather than a broken feature, but the producer-without-consumer\ - \ pattern is exactly what the holistic lens exists to flag. Either drop the env\ - \ vars or have the rubrics reference them by name (mirroring `EGG_ANALYSIS_PATH`\ - \ conventions if you want to keep them).\n- **Pass 4 \u2014 bare `except Exception:\ - \ pass` around `tracker.handle_ack` (line 1030) and `tracker.handle_confirmed`\ - \ (line 1047).** Comment says the eval snapshot will surface the symptom, which\ - \ is true for `blocking_agents`, but the root cause (graph-edge mismatch, malformed\ - \ payload, unregistered agent) is silently discarded. At least log via `logging.warning`\ - \ so operators debugging a stuck plan gate get something better than \"blocking_agents=['architect']\"\ - .\n- **Pass 4 \u2014 `_format_plan_placeholder` excludes `reviewer_plan` from\ - \ per-producer diagnostics.** It iterates only `plan_producers` (the 3 producer\ - \ roles); when `reviewer_plan` itself fails, its exit_code / stdout / commit_sha\ - \ never reach the placeholder body. Add reviewer diagnostics alongside the producer\ - \ diagnostics so the operator at the plan HITL gate sees what happened end-to-end.\n\ - - **`_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"`** \u2014 fine in the in-process path\ - \ (no gateway commit-existence check), but worth a one-line comment noting it\ - \ must never escape the in-process driver (e.g. if the production HTTP daemon\ - \ ever reuses `_run_plan_phase` as a fallback, this hex-validates as a real SHA\ - \ and could land in a BRC history file).\n\nIf blocker 1 (architect ordering)\ - \ and blocker 2 (reviewer verdict discard) are addressed, the rest is non-blocking\ - \ polish. The line-by-line review owns the rest.\n" - revision_count: 2 - slice_id: slice-2 -```` - -### [2026-05-19T20:05:53Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 26be4378-a9b7-41 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:05:53Z] tester → coder (CONSENSUS_NACK): NACK from tester for coder - - -Reviewed coder v1 (commit `3a466891e`, slice-2 TASK-2-1 / TASK-2-2 / TASK-2-5 no-op). Drove the implementation end-to-end against my TASK-2-4 integration test (`integration_tests/regression/test_inprocess_plan_brc.py`) and TASK-2-6 no-op guard (`tests/sandbox/egg_agent_tools/test_restrictions_validator.py`); **16/16 tester-authored tests pass cleanly** against this commit. Architecturally the slice is correct — `_run_plan_phase` correctly spawns 3 plan producers concurrently via `ThreadPoolExecutor`, drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed` deterministically per the synchronous-spawn design note in your commit message, reaches `is_complete=True` with all 4 plan-team roles in CONFIRMED state on the harness-faked path, yields the plan-HITL gate with `phase="plan"` + `decision_type="phase_gate"`, and correctly skips the plan stage on non-`approve_continue` refine answers (my `test_plan_stage_does_not_run_when_operator_rejects_refine` confirms `stop` still returns the refine artifact path without spawning any plan producer). TASK-2-2 loader expansion lands the 4 new plan-team roles correctly; TASK-2-5 no-op handling is correctly grounded in slice-1's R2 = pass verdict. **Blocking #1 below is the only thing keeping this from being an ACK.** - -### Blocking - -1. **`orchestrator/substrate/in_process.py` — `ruff format --check` fails on 5 long-call sites in TASK-2-1's new code.** Reproduction: `PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py` exits 1; `make lint` cannot pass with this in the diff. The 5 sites the formatter wants to collapse to single-line form (each currently spans multiple lines and falls under the formatter's 100-col cap when re-folded): - - `:236-241` — `plan_answer = yield self._build_plan_gate_decision(plan_artifact_path, plan_eval)` should be one line. - - `:967-970` — `commit_sha = getattr(spawn_result, "commit_sha", None) or _SYNTHETIC_PLAN_COMMIT` should be one line. - - `:1003-1005` — `producer_exit = int(getattr(producer_results[producer], "exit_code", 0) or 0)` should be one line. - - `:1013-1015` — `"artifact_references": [str(producer_artifacts[producer])],` should be one line inside the dict literal. - - `:1142-1144` — `producer_artifact_paths = sorted({str(path) for path in producer_artifacts.values()})` should be one line. - Fix: run `PYTHONPATH=shared:gateway:orchestrator ruff format orchestrator/substrate/in_process.py` and commit the resulting diff. Full diff produced by `ruff format --diff` matches the 5 sites above one-for-one. I have not edited the file myself (cross-role boundary); reproducing locally produces the exact diff `ruff format` wants. - - I will re-run `make lint` (or the `ruff format --check` equivalent in this sandbox) the moment your v2 lands; if it passes, I will ACK this slice on v2. - -### Non-blocking - -- **`_run_plan_phase` writes the plan placeholder to the same `-plan.md` path regardless of per-producer success** (`:1059-1069`). On a real partial-failure run (one producer's spawn raises; others succeed) the placeholder's "per-producer diagnostics" summary is the only signal the operator gets. The current implementation correctly stores the per-role exceptions in `producer_results` so they show up in the placeholder body. Suggestion (defer to follow-up issue): when a producer's spawn raises but another succeeds, the placeholder body uses the *successful* producer's outputs as the canonical plan content; the operator should ideally see a "plan partially produced" gate instead of an "approve" gate. The `_build_plan_gate_decision` already differentiates `is_complete` vs `blocking_agents`, so this is just an issue of having `_run_plan_phase` thread the exceptions through more visibly. Not blocking because the HITL gate's `blocking_agents` field already covers the BRC side. - -- **`PeerConsensusTracker.get_peer_consensus_tracker` reuse pattern (`:925-934`)** — your comment notes that a previous slice's background BRC tick might have installed a tracker; in the test the registry's empty so a fresh tracker is created. I added an `isolated_pipeline_state` fixture in `test_inprocess_plan_brc.py` that clears the module-level registry between tests so back-to-back runs against the same pipeline_id don't inherit confirmed state. Not blocking; the production path doesn't have multiple in-process pipelines against the same id, but worth a comment in the production code explaining the reuse semantics. - -- **`_write_active_role_sentinel` is called per-producer inside `_spawn_plan_producer` and again from `_spawn_plan_reviewer`** (`:1106, :1158`). With three producers running concurrently in a `ThreadPoolExecutor` the sentinel write is last-writer-wins; the PreToolUse hook in any one producer's nested subagent will resolve via the EGG_AGENT_ROLE env (which IS per-spawn correct) before falling back to the sentinel. The R2 deferral caveat already documents this on `_write_active_role_sentinel`; not blocking. Worth a one-line comment at the call sites that the concurrency makes the sentinel non-load-bearing for the plan phase (the env-var is the load-bearing channel). - -Reproduction summary for blocker #1: -``` -$ PYTHONPATH=shared:gateway:orchestrator ruff format --check orchestrator/substrate/in_process.py -Would reformat: orchestrator/substrate/in_process.py -1 file would be reformatted -$ echo $? -1 -``` - - -````yaml -id: cd90e2f5-1e1a-4d -phase: implement -metadata: - payload: - reason: "\nReviewed coder v1 (commit `3a466891e`, slice-2 TASK-2-1 / TASK-2-2\ - \ / TASK-2-5 no-op). Drove the implementation end-to-end against my TASK-2-4\ - \ integration test (`integration_tests/regression/test_inprocess_plan_brc.py`)\ - \ and TASK-2-6 no-op guard (`tests/sandbox/egg_agent_tools/test_restrictions_validator.py`);\ - \ **16/16 tester-authored tests pass cleanly** against this commit. Architecturally\ - \ the slice is correct \u2014 `_run_plan_phase` correctly spawns 3 plan producers\ - \ concurrently via `ThreadPoolExecutor`, drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed`\ - \ deterministically per the synchronous-spawn design note in your commit message,\ - \ reaches `is_complete=True` with all 4 plan-team roles in CONFIRMED state on\ - \ the harness-faked path, yields the plan-HITL gate with `phase=\"plan\"` +\ - \ `decision_type=\"phase_gate\"`, and correctly skips the plan stage on non-`approve_continue`\ - \ refine answers (my `test_plan_stage_does_not_run_when_operator_rejects_refine`\ - \ confirms `stop` still returns the refine artifact path without spawning any\ - \ plan producer). TASK-2-2 loader expansion lands the 4 new plan-team roles\ - \ correctly; TASK-2-5 no-op handling is correctly grounded in slice-1's R2 =\ - \ pass verdict. **Blocking #1 below is the only thing keeping this from being\ - \ an ACK.**\n\n### Blocking\n\n1. **`orchestrator/substrate/in_process.py` \u2014\ - \ `ruff format --check` fails on 5 long-call sites in TASK-2-1's new code.**\ - \ Reproduction: `PYTHONPATH=shared:gateway:orchestrator ruff format --check\ - \ orchestrator/substrate/in_process.py` exits 1; `make lint` cannot pass with\ - \ this in the diff. The 5 sites the formatter wants to collapse to single-line\ - \ form (each currently spans multiple lines and falls under the formatter's\ - \ 100-col cap when re-folded):\n - `:236-241` \u2014 `plan_answer = yield\ - \ self._build_plan_gate_decision(plan_artifact_path, plan_eval)` should be one\ - \ line.\n - `:967-970` \u2014 `commit_sha = getattr(spawn_result, \"commit_sha\"\ - , None) or _SYNTHETIC_PLAN_COMMIT` should be one line.\n - `:1003-1005` \u2014\ - \ `producer_exit = int(getattr(producer_results[producer], \"exit_code\", 0)\ - \ or 0)` should be one line.\n - `:1013-1015` \u2014 `\"artifact_references\"\ - : [str(producer_artifacts[producer])],` should be one line inside the dict literal.\n\ - \ - `:1142-1144` \u2014 `producer_artifact_paths = sorted({str(path) for path\ - \ in producer_artifacts.values()})` should be one line.\n Fix: run `PYTHONPATH=shared:gateway:orchestrator\ - \ ruff format orchestrator/substrate/in_process.py` and commit the resulting\ - \ diff. Full diff produced by `ruff format --diff` matches the 5 sites above\ - \ one-for-one. I have not edited the file myself (cross-role boundary); reproducing\ - \ locally produces the exact diff `ruff format` wants.\n\n I will re-run `make\ - \ lint` (or the `ruff format --check` equivalent in this sandbox) the moment\ - \ your v2 lands; if it passes, I will ACK this slice on v2.\n\n### Non-blocking\n\ - \n- **`_run_plan_phase` writes the plan placeholder to the same `-plan.md`\ - \ path regardless of per-producer success** (`:1059-1069`). On a real partial-failure\ - \ run (one producer's spawn raises; others succeed) the placeholder's \"per-producer\ - \ diagnostics\" summary is the only signal the operator gets. The current implementation\ - \ correctly stores the per-role exceptions in `producer_results` so they show\ - \ up in the placeholder body. Suggestion (defer to follow-up issue): when a\ - \ producer's spawn raises but another succeeds, the placeholder body uses the\ - \ *successful* producer's outputs as the canonical plan content; the operator\ - \ should ideally see a \"plan partially produced\" gate instead of an \"approve\"\ - \ gate. The `_build_plan_gate_decision` already differentiates `is_complete`\ - \ vs `blocking_agents`, so this is just an issue of having `_run_plan_phase`\ - \ thread the exceptions through more visibly. Not blocking because the HITL\ - \ gate's `blocking_agents` field already covers the BRC side.\n\n- **`PeerConsensusTracker.get_peer_consensus_tracker`\ - \ reuse pattern (`:925-934`)** \u2014 your comment notes that a previous slice's\ - \ background BRC tick might have installed a tracker; in the test the registry's\ - \ empty so a fresh tracker is created. I added an `isolated_pipeline_state`\ - \ fixture in `test_inprocess_plan_brc.py` that clears the module-level registry\ - \ between tests so back-to-back runs against the same pipeline_id don't inherit\ - \ confirmed state. Not blocking; the production path doesn't have multiple in-process\ - \ pipelines against the same id, but worth a comment in the production code\ - \ explaining the reuse semantics.\n\n- **`_write_active_role_sentinel` is called\ - \ per-producer inside `_spawn_plan_producer` and again from `_spawn_plan_reviewer`**\ - \ (`:1106, :1158`). With three producers running concurrently in a `ThreadPoolExecutor`\ - \ the sentinel write is last-writer-wins; the PreToolUse hook in any one producer's\ - \ nested subagent will resolve via the EGG_AGENT_ROLE env (which IS per-spawn\ - \ correct) before falling back to the sentinel. The R2 deferral caveat already\ - \ documents this on `_write_active_role_sentinel`; not blocking. Worth a one-line\ - \ comment at the call sites that the concurrency makes the sentinel non-load-bearing\ - \ for the plan phase (the env-var is the load-bearing channel).\n\nReproduction\ - \ summary for blocker #1:\n```\n$ PYTHONPATH=shared:gateway:orchestrator ruff\ - \ format --check orchestrator/substrate/in_process.py\nWould reformat: orchestrator/substrate/in_process.py\n\ - 1 file would be reformatted\n$ echo $?\n1\n```\n" - artifact_references: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - nack_version: 1 - reason: "\nReviewed coder v1 (commit `3a466891e`, slice-2 TASK-2-1 / TASK-2-2 /\ - \ TASK-2-5 no-op). Drove the implementation end-to-end against my TASK-2-4 integration\ - \ test (`integration_tests/regression/test_inprocess_plan_brc.py`) and TASK-2-6\ - \ no-op guard (`tests/sandbox/egg_agent_tools/test_restrictions_validator.py`);\ - \ **16/16 tester-authored tests pass cleanly** against this commit. Architecturally\ - \ the slice is correct \u2014 `_run_plan_phase` correctly spawns 3 plan producers\ - \ concurrently via `ThreadPoolExecutor`, drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed`\ - \ deterministically per the synchronous-spawn design note in your commit message,\ - \ reaches `is_complete=True` with all 4 plan-team roles in CONFIRMED state on\ - \ the harness-faked path, yields the plan-HITL gate with `phase=\"plan\"` + `decision_type=\"\ - phase_gate\"`, and correctly skips the plan stage on non-`approve_continue` refine\ - \ answers (my `test_plan_stage_does_not_run_when_operator_rejects_refine` confirms\ - \ `stop` still returns the refine artifact path without spawning any plan producer).\ - \ TASK-2-2 loader expansion lands the 4 new plan-team roles correctly; TASK-2-5\ - \ no-op handling is correctly grounded in slice-1's R2 = pass verdict. **Blocking\ - \ #1 below is the only thing keeping this from being an ACK.**\n\n### Blocking\n\ - \n1. **`orchestrator/substrate/in_process.py` \u2014 `ruff format --check` fails\ - \ on 5 long-call sites in TASK-2-1's new code.** Reproduction: `PYTHONPATH=shared:gateway:orchestrator\ - \ ruff format --check orchestrator/substrate/in_process.py` exits 1; `make lint`\ - \ cannot pass with this in the diff. The 5 sites the formatter wants to collapse\ - \ to single-line form (each currently spans multiple lines and falls under the\ - \ formatter's 100-col cap when re-folded):\n - `:236-241` \u2014 `plan_answer\ - \ = yield self._build_plan_gate_decision(plan_artifact_path, plan_eval)` should\ - \ be one line.\n - `:967-970` \u2014 `commit_sha = getattr(spawn_result, \"\ - commit_sha\", None) or _SYNTHETIC_PLAN_COMMIT` should be one line.\n - `:1003-1005`\ - \ \u2014 `producer_exit = int(getattr(producer_results[producer], \"exit_code\"\ - , 0) or 0)` should be one line.\n - `:1013-1015` \u2014 `\"artifact_references\"\ - : [str(producer_artifacts[producer])],` should be one line inside the dict literal.\n\ - \ - `:1142-1144` \u2014 `producer_artifact_paths = sorted({str(path) for path\ - \ in producer_artifacts.values()})` should be one line.\n Fix: run `PYTHONPATH=shared:gateway:orchestrator\ - \ ruff format orchestrator/substrate/in_process.py` and commit the resulting diff.\ - \ Full diff produced by `ruff format --diff` matches the 5 sites above one-for-one.\ - \ I have not edited the file myself (cross-role boundary); reproducing locally\ - \ produces the exact diff `ruff format` wants.\n\n I will re-run `make lint`\ - \ (or the `ruff format --check` equivalent in this sandbox) the moment your v2\ - \ lands; if it passes, I will ACK this slice on v2.\n\n### Non-blocking\n\n- **`_run_plan_phase`\ - \ writes the plan placeholder to the same `-plan.md` path regardless\ - \ of per-producer success** (`:1059-1069`). On a real partial-failure run (one\ - \ producer's spawn raises; others succeed) the placeholder's \"per-producer diagnostics\"\ - \ summary is the only signal the operator gets. The current implementation correctly\ - \ stores the per-role exceptions in `producer_results` so they show up in the\ - \ placeholder body. Suggestion (defer to follow-up issue): when a producer's spawn\ - \ raises but another succeeds, the placeholder body uses the *successful* producer's\ - \ outputs as the canonical plan content; the operator should ideally see a \"\ - plan partially produced\" gate instead of an \"approve\" gate. The `_build_plan_gate_decision`\ - \ already differentiates `is_complete` vs `blocking_agents`, so this is just an\ - \ issue of having `_run_plan_phase` thread the exceptions through more visibly.\ - \ Not blocking because the HITL gate's `blocking_agents` field already covers\ - \ the BRC side.\n\n- **`PeerConsensusTracker.get_peer_consensus_tracker` reuse\ - \ pattern (`:925-934`)** \u2014 your comment notes that a previous slice's background\ - \ BRC tick might have installed a tracker; in the test the registry's empty so\ - \ a fresh tracker is created. I added an `isolated_pipeline_state` fixture in\ - \ `test_inprocess_plan_brc.py` that clears the module-level registry between tests\ - \ so back-to-back runs against the same pipeline_id don't inherit confirmed state.\ - \ Not blocking; the production path doesn't have multiple in-process pipelines\ - \ against the same id, but worth a comment in the production code explaining the\ - \ reuse semantics.\n\n- **`_write_active_role_sentinel` is called per-producer\ - \ inside `_spawn_plan_producer` and again from `_spawn_plan_reviewer`** (`:1106,\ - \ :1158`). With three producers running concurrently in a `ThreadPoolExecutor`\ - \ the sentinel write is last-writer-wins; the PreToolUse hook in any one producer's\ - \ nested subagent will resolve via the EGG_AGENT_ROLE env (which IS per-spawn\ - \ correct) before falling back to the sentinel. The R2 deferral caveat already\ - \ documents this on `_write_active_role_sentinel`; not blocking. Worth a one-line\ - \ comment at the call sites that the concurrency makes the sentinel non-load-bearing\ - \ for the plan phase (the env-var is the load-bearing channel).\n\nReproduction\ - \ summary for blocker #1:\n```\n$ PYTHONPATH=shared:gateway:orchestrator ruff\ - \ format --check orchestrator/substrate/in_process.py\nWould reformat: orchestrator/substrate/in_process.py\n\ - 1 file would be reformatted\n$ echo $?\n1\n```\n" - revision_count: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:06:04Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: e734337c-94f2-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:05Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3e1670b4-2c80-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:05.424344+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:05Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a3e8f873-b2d4-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:06:06Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: dff1f7b4-ba08-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:06.658152+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:06Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d4757c68-6831-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:06:13Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 6370106d-da57-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:14Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 1b1a6e8c-b3ca-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: e8ffd854-2d41-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:24Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: da006162-243b-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:24.514597+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: aaab18f3-f8c5-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:06:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: bda426ab-ac0e-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:07:04Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c629f030-d3ba-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:07:13Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 4552d3b1-60e8-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:07:14Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 43c9eb43-8860-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:07:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a0b2ff92-1eb0-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:07:24Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 486c4241-4abe-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:24.514597+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:07:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 168d855f-a499-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:07:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 7ced567d-9819-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:08:04Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: cd9f89cf-1cc7-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:08:11Z] reviewer_code → coder (CONSENSUS_ACK): ACK from reviewer_code for coder - - -Reviewed the coder's submission for TASK-2-1 (plan-phase BRC stage on `_InProcessOrchestrator.run()`), TASK-2-2 (rubric loader expansion), and TASK-2-5 (closed as no-op per slice-1 R2=pass). Read both diffs end-to-end, traced concurrency interactions on `LocalWorktreeManager`, `PeerConsensusTracker`, and the active-role sentinel, verified payload shapes against `ProposalPayload` / `ReviewPayload` in `orchestrator/attestation_schemas.py`, and cross-checked behavior against the slice-1 spawner pattern. - -### File-by-file analysis - -**orchestrator/substrate/__init__.py** (TASK-2-2; +19/-9) — `_RUBRIC_LANDED_ROLES` extended with `architect`, `task_planner`, `risk_analyst`, `reviewer_plan` (lines 271-285). The docstring on `_load_egg_sdlc_role_rubric` is updated to credit slice-1 with the refine team and slice-2 with the plan team. The "missing on disk" diagnostic now mentions both TASK-1-4 (slice-1 refine reviewers) and TASK-2-3 (slice-2 plan team), giving an operator hitting the error a slice-specific pointer. `_ROLE_RUBRIC_SLICES` already had slice-2 mappings (from the slice-1 landing) so the unshipped-role fence for slice-3 implement-team roles is preserved unchanged. Loader file naming convention (`{role_name}.md`) matches the documenter's underscored basenames. Clean. - -**orchestrator/substrate/in_process.py** (TASK-2-1; +568/-9) — Big diff; broken down by surface: - -- *Generator flow* (`run()`, lines 219-249) — After the refine HITL gate, `_answer_continues_past_refine(refine_answer)` (lines 1313-1331) gates entry to `_run_plan_phase`. A negative answer falls through to `return str(artifact_path)`, preserving the slice-1 "refine-only" path verbatim. The new plan-gate HITL is yielded after `_run_plan_phase` returns, then `_maybe_fence(plan_answer)` (lines 1260-1291) re-targets at `approve_continue` past the plan gate with a slice-3 / slice-4 pointer. Existing `_PreflightAborted` translation and `finally`-block teardown (`_shutdown_background_threads`, `_teardown_worktrees`, `_teardown_sentinel`) covers the plan stage's exit paths cleanly because the worktree manager's `tear_down` is pipeline-scoped — it sweeps all 5 worktrees (1 refiner + 3 plan producers + 1 plan reviewer). - -- *Plan stage* (`_run_plan_phase`, lines 830-1071) — Spawns three plan producers concurrently via a `ThreadPoolExecutor(max_workers=3)`, then dispatches `reviewer_plan` once synchronously after `as_completed` drains all three. Producer failures (Exception from `fut.result()` or non-zero exit_code) are routed into `producer_results[role]` as an `Exception` instance / `AgentResult` with non-zero exit; the eval snapshot's `blocking_agents` surfaces them at the plan HITL gate. The "Why the BRC verbs are called from the orchestrator rather than the spawned subagents" docstring (lines 842-856) accurately captures the spike's synchronous-spawn-as-signal model and explains why both harness-faked tests and real-harness production reach `CONSENSUS_CONFIRMED` on the same code path. - -- *Per-producer spawn* (`_spawn_plan_producer`, lines 1073-1126) — Allocates a per-role worktree (`///`), shapes spawn_env with `EGG_PIPELINE_ID`, `EGG_AGENT_ROLE`, `EGG_REPO_ROOT`, `EGG_WORKTREE_ROOT`, `EGG_PHASE=plan`, `EGG_REFINE_ARTIFACT_PATH`, and `EGG_PLAN_ARTIFACT_PATH`, refreshes the active-role sentinel, then `bundle.spawner.spawn(role, prompt_text, spawn_env, worktree)`. The role-routing in the spawner respects whatever's in `spawn_env["EGG_AGENT_ROLE"]` (and the spawner itself overrides it again at `claude_code/spawner.py:126`), so the producer's role is always correct in its own env even if the sentinel race fires for nested dispatch fallbacks. - -- *Reviewer spawn* (`_spawn_plan_reviewer`, lines 1128-1179) — Dispatches `reviewer_plan` once with `EGG_PRODUCER_ARTIFACT_PATHS` as a colon-joined list; in current code every producer's `producer_artifacts[role]` value is the same `plan_artifact_path`, so after `sorted({...})` the list is single-element. - -- *Tracker mechanics* (lines 917-934, 967-1052) — The plan-graph is fetched via `get_review_graph_for_phase("plan", repo=self.repo)`, registering all four roles. `tracker.handle_propose` is gated on exit_code==0 with `_SYNTHETIC_PLAN_COMMIT = "ace1ace"` as a fallback when the spawn didn't capture a real SHA — satisfies `ProposalPayload`'s `commit_sha_present` validator (#1473). `tracker.handle_ack(reviewer, producer, ...)` injects `verdict="ACK"` server-side (`peer_consensus.py:429`), so the orchestrator's payload (lacking `verdict`) is structurally valid. `handle_confirmed` is best-effort with `except Exception: pass` — guard rejections surface in `tracker.evaluate()` (line 1054) rather than as generator exceptions, and that snapshot drives the plan HITL gate context. Lock contention on `tracker._lock` (RLock) under three concurrent `handle_propose` calls is brief and free of deadlock risk. - -- *Plan-gate HITL* (`_build_plan_gate_decision`, lines 648-710) — Branches on `plan_eval["is_complete"]`. The success branch surfaces the canonical 4-way options (`approve_continue`, `request_changes`, `change_approach`, `stop`); the failure branch surfaces `retry`/`abort` and inlines `blocking_agents` + `unresolved_nack_details` into the decision context. Decision id is stable per pipeline (`plan-gate-{pipeline_id}` or `plan-failure-{pipeline_id}`). Mirrors `_build_refine_gate_decision`'s shape so the skill's outer loop handles both gates uniformly. - -- *Plan-artifact placeholder* (`_format_plan_placeholder`, lines 1334-1392) — Same shape as the refiner placeholder: per-producer diagnostics (exit_code, commit_sha, stdout-tail), BRC eval snapshot, and a clarifying epilogue. The placeholder lands at `.egg-state/drafts/-plan.md` only when the canonical file doesn't already exist (line 1059) — production task_planners that write the real file are preserved. - -- *Sentinel concurrency* (`_write_active_role_sentinel`, called from `_spawn_plan_producer` line 1111) — Three producer threads write `$HOME/.claude/egg-active-role.json` concurrently; the last writer wins. The single-valued file is documented as a known R2-deferral limitation in the docstring (lines 1195-1202). Under the slice-1 R2=pass verdict, `EGG_AGENT_ROLE` reliably propagates through nested dispatch so the sentinel is only the fallback path. Worth noting: a producer that *does* hit the sentinel fallback path may resolve to the wrong role if another concurrent producer has overwritten the file mid-spawn. The hook reads PID and treats stale entries as missing, but two live concurrent producers each have valid PIDs. - -- *Worktree creation under concurrency* (`LocalWorktreeManager.create`, `claude_code/worktree.py:89`) — Three concurrent `git worktree add` calls can race on `.git/index.lock` or refs database locks. The subprocess call uses `check=False` and a 30-second timeout, so a transient git lock contention leaves a non-worktree directory (the spawner still has somewhere to land artifacts). Recoverable. - -### Non-blocking - -- **orchestrator/substrate/in_process.py:907-911** — Rubric language vs implementation: `architect.md` says "You run first, solo, before `task_planner` and `risk_analyst`" and `task_planner.md` / `risk_analyst.md` both say "downstream of `architect`". The slice-2 SKILL.md inherits that ordering claim. The actual implementation here spawns all three concurrently via the `ThreadPoolExecutor`, which matches the k3s substrate's `spawn_all` behavior at `orchestrator/concurrent_executor.py:461-481` and explicitly satisfies the task-2-1 acceptance criterion "the plan stage spawns 3 producers concurrently via the executor". The architect-first language in the rubrics is a longstanding inheritance from `plugins/refine-plan/skills/refine-plan/agents/`'s rubric bodies (the k3s substrate has the same language-vs-implementation gap) — slice-2 does not introduce the gap. Follow-up worth filing to reconcile rubric language with actual concurrent dispatch, and to add an explicit "architect's output JSON is read-on-best-effort by your peers" note to task_planner / risk_analyst rubrics so the rubric language matches behavior. - -- **orchestrator/substrate/in_process.py:994-1034** — The orchestrator records `tracker.handle_ack(reviewer, producer, ...)` synthetically based on `reviewer_exit_code == 0`, **not** by parsing the reviewer's verdict JSON at `verdict_path`. A real reviewer that NACKs by writing `{"verdict": "NACK", ...}` to its verdict JSON but exits cleanly will have its NACK silently dropped — the orchestrator records ACK and the plan HITL gate fires with `is_complete=True`. The spike's harness-faked tests are insensitive to this because the fakes don't emit verdicts, but real-substrate usage of slice-2 today cannot rely on the reviewer NACK path. The commit message describes this as "production (with real harness agents whose BRC emissions would be a no-op duplicate in this path)" but the in-process substrate has no HTTP daemon for real agents' `egg-orch consensus propose` calls to land on — those emissions would error, not be duplicates. Slice-3 / 4 will need to wire verdict-JSON parsing or in-process BRC verb emission for the reviewer NACK path to actually work. Track in a follow-up issue. - -- **orchestrator/substrate/in_process.py:911 ("reviewer_plan is spawned after each `CONSENSUS_PROPOSE`")** — The task-2-1 acceptance criterion phrasing is ambiguous: it can be read as "one reviewer spawn per producer propose" (3 spawns) or as "reviewer spawn is conditioned on at least one producer having proposed" (1 spawn). Current code does the latter — one reviewer spawn after all three producers complete. The docstring at lines 1136-1139 documents the design choice ("the synchronous spawn model means the producers' artifacts are on disk before the reviewer starts"). Reasonable interpretation given the spike's spawn semantics, but reviewer_contract may want to verify this read. Either way the BRC tracker records per-producer ACKs (one tracker.handle_ack call per successful producer at lines 1011-1029), which satisfies the "per-edge consensus" spirit of the criterion. - -- **orchestrator/substrate/in_process.py:1128-1170** — The reviewer's spawn_env sets `EGG_PRODUCER_ARTIFACT_PATHS` but not the role-specific output paths the `reviewer_plan.md` rubric names (`analysis_path`, `architect_output_path`, `task_planner_output_path`, `risk_analyst_output_path`). After dedup, the producer-paths list collapses to a single entry (every producer's `producer_artifacts[role]` value is the same `plan_artifact_path`). The reviewer must infer the per-role JSON output paths from rubric convention. This matches the slice-1 pattern (the refiner also doesn't get `analysis_path` directly), but the rubric's input enumeration sets an expectation that slice-2's env shaping does not meet. Consider follow-up to surface role-specific paths in spawn_env so reviewer / task_planner / risk_analyst can read peer outputs deterministically rather than by convention-guessing. - -- **orchestrator/substrate/in_process.py:925-931** — The "reuse existing tracker" branch (`if tracker is None: create_peer_consensus_tracker(...)`) is dead code today — slice-1's `_spawn_refiner` does not register a tracker (the `create_peer_consensus_tracker` import is `noqa: F401`), and `_tick_brc_review` only reads. If a future slice registers a tracker with a different graph (e.g., refine graph during a future refine BRC cycle), the slice-2 plan phase would reuse that tracker with the wrong graph. Worth a guard that asserts the existing tracker's graph matches the plan graph before reuse, or just always-create (the spike's tight propose→ack→confirm sequence has no need to reuse). - -- **orchestrator/substrate/in_process.py:934** — `self._plan_tracker = tracker` is set but never read elsewhere in the module. If the intent was to expose the tracker for tests / observability, document the surface; otherwise drop the assignment. - -- **orchestrator/substrate/in_process.py:91 (`_SYNTHETIC_PLAN_COMMIT = "ace1ace"`)** — Synthetic 7-hex constant for the test path. Real-substrate spawns capture `git rev-parse HEAD` post-commit, so the fallback only fires for harness fakes that don't write a commit. The constant is intentionally obviously-synthetic in log output. Worth a docstring note on `_SYNTHETIC_PLAN_COMMIT` mentioning that any caller hex-validating `commit_sha` (e.g. a gateway-style policy check) accepts this as a 7-char hex — non-issue today because the in-process bus doesn't gateway-validate, but a slice-5 hardening pass may want to swap to a clearly-non-hex sentinel (e.g. `"synthetic-plan-commit"`) if any consumer becomes hex-strict. - -- **orchestrator/substrate/in_process.py:1011-1029** — The `try / except Exception: pass` around `tracker.handle_ack` silently swallows malformed-payload errors (e.g., a ReviewPayload validation failure). The eval snapshot's `unresolved_nack_details` surfaces the unconfirmed edge, so the failure is visible at the HITL gate, but the operator sees no specific error message. Acceptable for the spike but worth flagging if the consumed payload shape changes (e.g., #2142's `ack_version` plumbing extends the required fields). - -No security, correctness, or robustness issues that block the slice's atomic landing. Coder's submission ACKed. - - -````yaml -id: 7182dd82-1878-4a -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/__init__.py - - orchestrator/substrate/in_process.py - reason: "\nReviewed the coder's submission for TASK-2-1 (plan-phase BRC stage\ - \ on `_InProcessOrchestrator.run()`), TASK-2-2 (rubric loader expansion), and\ - \ TASK-2-5 (closed as no-op per slice-1 R2=pass). Read both diffs end-to-end,\ - \ traced concurrency interactions on `LocalWorktreeManager`, `PeerConsensusTracker`,\ - \ and the active-role sentinel, verified payload shapes against `ProposalPayload`\ - \ / `ReviewPayload` in `orchestrator/attestation_schemas.py`, and cross-checked\ - \ behavior against the slice-1 spawner pattern.\n\n### File-by-file analysis\n\ - \n**orchestrator/substrate/__init__.py** (TASK-2-2; +19/-9) \u2014 `_RUBRIC_LANDED_ROLES`\ - \ extended with `architect`, `task_planner`, `risk_analyst`, `reviewer_plan`\ - \ (lines 271-285). The docstring on `_load_egg_sdlc_role_rubric` is updated\ - \ to credit slice-1 with the refine team and slice-2 with the plan team. The\ - \ \"missing on disk\" diagnostic now mentions both TASK-1-4 (slice-1 refine\ - \ reviewers) and TASK-2-3 (slice-2 plan team), giving an operator hitting the\ - \ error a slice-specific pointer. `_ROLE_RUBRIC_SLICES` already had slice-2\ - \ mappings (from the slice-1 landing) so the unshipped-role fence for slice-3\ - \ implement-team roles is preserved unchanged. Loader file naming convention\ - \ (`{role_name}.md`) matches the documenter's underscored basenames. Clean.\n\ - \n**orchestrator/substrate/in_process.py** (TASK-2-1; +568/-9) \u2014 Big diff;\ - \ broken down by surface:\n\n- *Generator flow* (`run()`, lines 219-249) \u2014\ - \ After the refine HITL gate, `_answer_continues_past_refine(refine_answer)`\ - \ (lines 1313-1331) gates entry to `_run_plan_phase`. A negative answer falls\ - \ through to `return str(artifact_path)`, preserving the slice-1 \"refine-only\"\ - \ path verbatim. The new plan-gate HITL is yielded after `_run_plan_phase` returns,\ - \ then `_maybe_fence(plan_answer)` (lines 1260-1291) re-targets at `approve_continue`\ - \ past the plan gate with a slice-3 / slice-4 pointer. Existing `_PreflightAborted`\ - \ translation and `finally`-block teardown (`_shutdown_background_threads`,\ - \ `_teardown_worktrees`, `_teardown_sentinel`) covers the plan stage's exit\ - \ paths cleanly because the worktree manager's `tear_down` is pipeline-scoped\ - \ \u2014 it sweeps all 5 worktrees (1 refiner + 3 plan producers + 1 plan reviewer).\n\ - \n- *Plan stage* (`_run_plan_phase`, lines 830-1071) \u2014 Spawns three plan\ - \ producers concurrently via a `ThreadPoolExecutor(max_workers=3)`, then dispatches\ - \ `reviewer_plan` once synchronously after `as_completed` drains all three.\ - \ Producer failures (Exception from `fut.result()` or non-zero exit_code) are\ - \ routed into `producer_results[role]` as an `Exception` instance / `AgentResult`\ - \ with non-zero exit; the eval snapshot's `blocking_agents` surfaces them at\ - \ the plan HITL gate. The \"Why the BRC verbs are called from the orchestrator\ - \ rather than the spawned subagents\" docstring (lines 842-856) accurately captures\ - \ the spike's synchronous-spawn-as-signal model and explains why both harness-faked\ - \ tests and real-harness production reach `CONSENSUS_CONFIRMED` on the same\ - \ code path.\n\n- *Per-producer spawn* (`_spawn_plan_producer`, lines 1073-1126)\ - \ \u2014 Allocates a per-role worktree (`///`), shapes\ - \ spawn_env with `EGG_PIPELINE_ID`, `EGG_AGENT_ROLE`, `EGG_REPO_ROOT`, `EGG_WORKTREE_ROOT`,\ - \ `EGG_PHASE=plan`, `EGG_REFINE_ARTIFACT_PATH`, and `EGG_PLAN_ARTIFACT_PATH`,\ - \ refreshes the active-role sentinel, then `bundle.spawner.spawn(role, prompt_text,\ - \ spawn_env, worktree)`. The role-routing in the spawner respects whatever's\ - \ in `spawn_env[\"EGG_AGENT_ROLE\"]` (and the spawner itself overrides it again\ - \ at `claude_code/spawner.py:126`), so the producer's role is always correct\ - \ in its own env even if the sentinel race fires for nested dispatch fallbacks.\n\ - \n- *Reviewer spawn* (`_spawn_plan_reviewer`, lines 1128-1179) \u2014 Dispatches\ - \ `reviewer_plan` once with `EGG_PRODUCER_ARTIFACT_PATHS` as a colon-joined\ - \ list; in current code every producer's `producer_artifacts[role]` value is\ - \ the same `plan_artifact_path`, so after `sorted({...})` the list is single-element.\n\ - \n- *Tracker mechanics* (lines 917-934, 967-1052) \u2014 The plan-graph is fetched\ - \ via `get_review_graph_for_phase(\"plan\", repo=self.repo)`, registering all\ - \ four roles. `tracker.handle_propose` is gated on exit_code==0 with `_SYNTHETIC_PLAN_COMMIT\ - \ = \"ace1ace\"` as a fallback when the spawn didn't capture a real SHA \u2014\ - \ satisfies `ProposalPayload`'s `commit_sha_present` validator (#1473). `tracker.handle_ack(reviewer,\ - \ producer, ...)` injects `verdict=\"ACK\"` server-side (`peer_consensus.py:429`),\ - \ so the orchestrator's payload (lacking `verdict`) is structurally valid. `handle_confirmed`\ - \ is best-effort with `except Exception: pass` \u2014 guard rejections surface\ - \ in `tracker.evaluate()` (line 1054) rather than as generator exceptions, and\ - \ that snapshot drives the plan HITL gate context. Lock contention on `tracker._lock`\ - \ (RLock) under three concurrent `handle_propose` calls is brief and free of\ - \ deadlock risk.\n\n- *Plan-gate HITL* (`_build_plan_gate_decision`, lines 648-710)\ - \ \u2014 Branches on `plan_eval[\"is_complete\"]`. The success branch surfaces\ - \ the canonical 4-way options (`approve_continue`, `request_changes`, `change_approach`,\ - \ `stop`); the failure branch surfaces `retry`/`abort` and inlines `blocking_agents`\ - \ + `unresolved_nack_details` into the decision context. Decision id is stable\ - \ per pipeline (`plan-gate-{pipeline_id}` or `plan-failure-{pipeline_id}`).\ - \ Mirrors `_build_refine_gate_decision`'s shape so the skill's outer loop handles\ - \ both gates uniformly.\n\n- *Plan-artifact placeholder* (`_format_plan_placeholder`,\ - \ lines 1334-1392) \u2014 Same shape as the refiner placeholder: per-producer\ - \ diagnostics (exit_code, commit_sha, stdout-tail), BRC eval snapshot, and a\ - \ clarifying epilogue. The placeholder lands at `.egg-state/drafts/-plan.md`\ - \ only when the canonical file doesn't already exist (line 1059) \u2014 production\ - \ task_planners that write the real file are preserved.\n\n- *Sentinel concurrency*\ - \ (`_write_active_role_sentinel`, called from `_spawn_plan_producer` line 1111)\ - \ \u2014 Three producer threads write `$HOME/.claude/egg-active-role.json` concurrently;\ - \ the last writer wins. The single-valued file is documented as a known R2-deferral\ - \ limitation in the docstring (lines 1195-1202). Under the slice-1 R2=pass verdict,\ - \ `EGG_AGENT_ROLE` reliably propagates through nested dispatch so the sentinel\ - \ is only the fallback path. Worth noting: a producer that *does* hit the sentinel\ - \ fallback path may resolve to the wrong role if another concurrent producer\ - \ has overwritten the file mid-spawn. The hook reads PID and treats stale entries\ - \ as missing, but two live concurrent producers each have valid PIDs.\n\n- *Worktree\ - \ creation under concurrency* (`LocalWorktreeManager.create`, `claude_code/worktree.py:89`)\ - \ \u2014 Three concurrent `git worktree add` calls can race on `.git/index.lock`\ - \ or refs database locks. The subprocess call uses `check=False` and a 30-second\ - \ timeout, so a transient git lock contention leaves a non-worktree directory\ - \ (the spawner still has somewhere to land artifacts). Recoverable.\n\n### Non-blocking\n\ - \n- **orchestrator/substrate/in_process.py:907-911** \u2014 Rubric language\ - \ vs implementation: `architect.md` says \"You run first, solo, before `task_planner`\ - \ and `risk_analyst`\" and `task_planner.md` / `risk_analyst.md` both say \"\ - downstream of `architect`\". The slice-2 SKILL.md inherits that ordering claim.\ - \ The actual implementation here spawns all three concurrently via the `ThreadPoolExecutor`,\ - \ which matches the k3s substrate's `spawn_all` behavior at `orchestrator/concurrent_executor.py:461-481`\ - \ and explicitly satisfies the task-2-1 acceptance criterion \"the plan stage\ - \ spawns 3 producers concurrently via the executor\". The architect-first language\ - \ in the rubrics is a longstanding inheritance from `plugins/refine-plan/skills/refine-plan/agents/`'s\ - \ rubric bodies (the k3s substrate has the same language-vs-implementation gap)\ - \ \u2014 slice-2 does not introduce the gap. Follow-up worth filing to reconcile\ - \ rubric language with actual concurrent dispatch, and to add an explicit \"\ - architect's output JSON is read-on-best-effort by your peers\" note to task_planner\ - \ / risk_analyst rubrics so the rubric language matches behavior.\n\n- **orchestrator/substrate/in_process.py:994-1034**\ - \ \u2014 The orchestrator records `tracker.handle_ack(reviewer, producer, ...)`\ - \ synthetically based on `reviewer_exit_code == 0`, **not** by parsing the reviewer's\ - \ verdict JSON at `verdict_path`. A real reviewer that NACKs by writing `{\"\ - verdict\": \"NACK\", ...}` to its verdict JSON but exits cleanly will have its\ - \ NACK silently dropped \u2014 the orchestrator records ACK and the plan HITL\ - \ gate fires with `is_complete=True`. The spike's harness-faked tests are insensitive\ - \ to this because the fakes don't emit verdicts, but real-substrate usage of\ - \ slice-2 today cannot rely on the reviewer NACK path. The commit message describes\ - \ this as \"production (with real harness agents whose BRC emissions would be\ - \ a no-op duplicate in this path)\" but the in-process substrate has no HTTP\ - \ daemon for real agents' `egg-orch consensus propose` calls to land on \u2014\ - \ those emissions would error, not be duplicates. Slice-3 / 4 will need to wire\ - \ verdict-JSON parsing or in-process BRC verb emission for the reviewer NACK\ - \ path to actually work. Track in a follow-up issue.\n\n- **orchestrator/substrate/in_process.py:911\ - \ (\"reviewer_plan is spawned after each `CONSENSUS_PROPOSE`\")** \u2014 The\ - \ task-2-1 acceptance criterion phrasing is ambiguous: it can be read as \"\ - one reviewer spawn per producer propose\" (3 spawns) or as \"reviewer spawn\ - \ is conditioned on at least one producer having proposed\" (1 spawn). Current\ - \ code does the latter \u2014 one reviewer spawn after all three producers complete.\ - \ The docstring at lines 1136-1139 documents the design choice (\"the synchronous\ - \ spawn model means the producers' artifacts are on disk before the reviewer\ - \ starts\"). Reasonable interpretation given the spike's spawn semantics, but\ - \ reviewer_contract may want to verify this read. Either way the BRC tracker\ - \ records per-producer ACKs (one tracker.handle_ack call per successful producer\ - \ at lines 1011-1029), which satisfies the \"per-edge consensus\" spirit of\ - \ the criterion.\n\n- **orchestrator/substrate/in_process.py:1128-1170** \u2014\ - \ The reviewer's spawn_env sets `EGG_PRODUCER_ARTIFACT_PATHS` but not the role-specific\ - \ output paths the `reviewer_plan.md` rubric names (`analysis_path`, `architect_output_path`,\ - \ `task_planner_output_path`, `risk_analyst_output_path`). After dedup, the\ - \ producer-paths list collapses to a single entry (every producer's `producer_artifacts[role]`\ - \ value is the same `plan_artifact_path`). The reviewer must infer the per-role\ - \ JSON output paths from rubric convention. This matches the slice-1 pattern\ - \ (the refiner also doesn't get `analysis_path` directly), but the rubric's\ - \ input enumeration sets an expectation that slice-2's env shaping does not\ - \ meet. Consider follow-up to surface role-specific paths in spawn_env so reviewer\ - \ / task_planner / risk_analyst can read peer outputs deterministically rather\ - \ than by convention-guessing.\n\n- **orchestrator/substrate/in_process.py:925-931**\ - \ \u2014 The \"reuse existing tracker\" branch (`if tracker is None: create_peer_consensus_tracker(...)`)\ - \ is dead code today \u2014 slice-1's `_spawn_refiner` does not register a tracker\ - \ (the `create_peer_consensus_tracker` import is `noqa: F401`), and `_tick_brc_review`\ - \ only reads. If a future slice registers a tracker with a different graph (e.g.,\ - \ refine graph during a future refine BRC cycle), the slice-2 plan phase would\ - \ reuse that tracker with the wrong graph. Worth a guard that asserts the existing\ - \ tracker's graph matches the plan graph before reuse, or just always-create\ - \ (the spike's tight propose\u2192ack\u2192confirm sequence has no need to reuse).\n\ - \n- **orchestrator/substrate/in_process.py:934** \u2014 `self._plan_tracker\ - \ = tracker` is set but never read elsewhere in the module. If the intent was\ - \ to expose the tracker for tests / observability, document the surface; otherwise\ - \ drop the assignment.\n\n- **orchestrator/substrate/in_process.py:91 (`_SYNTHETIC_PLAN_COMMIT\ - \ = \"ace1ace\"`)** \u2014 Synthetic 7-hex constant for the test path. Real-substrate\ - \ spawns capture `git rev-parse HEAD` post-commit, so the fallback only fires\ - \ for harness fakes that don't write a commit. The constant is intentionally\ - \ obviously-synthetic in log output. Worth a docstring note on `_SYNTHETIC_PLAN_COMMIT`\ - \ mentioning that any caller hex-validating `commit_sha` (e.g. a gateway-style\ - \ policy check) accepts this as a 7-char hex \u2014 non-issue today because\ - \ the in-process bus doesn't gateway-validate, but a slice-5 hardening pass\ - \ may want to swap to a clearly-non-hex sentinel (e.g. `\"synthetic-plan-commit\"\ - `) if any consumer becomes hex-strict.\n\n- **orchestrator/substrate/in_process.py:1011-1029**\ - \ \u2014 The `try / except Exception: pass` around `tracker.handle_ack` silently\ - \ swallows malformed-payload errors (e.g., a ReviewPayload validation failure).\ - \ The eval snapshot's `unresolved_nack_details` surfaces the unconfirmed edge,\ - \ so the failure is visible at the HITL gate, but the operator sees no specific\ - \ error message. Acceptable for the spike but worth flagging if the consumed\ - \ payload shape changes (e.g., #2142's `ack_version` plumbing extends the required\ - \ fields).\n\nNo security, correctness, or robustness issues that block the\ - \ slice's atomic landing. Coder's submission ACKed.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:08:11Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 62235097-bcbe-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:08:13Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7e7402ee-e699-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:08:14Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 9978420e-1a19-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:08:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a714377a-2e1a-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:08:19Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3b9a37d8-4f9e-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:08:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 612b60d5-840f-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:08:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: ebb6ce83-2229-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:09:04Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: f05b0f7b-a72d-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:09:13Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 4a9b1fb0-7a25-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:09:14Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 18878ca2-61a6-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:09:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c81b3bf1-ace0-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:09:19Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: a8292db9-f6df-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:09:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 48a27f87-b194-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:09:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: a797b721-7b11-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:10:04Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 2e41f757-6e7b-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:10:13Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 456d0a9c-abfd-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:10:14Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 77d45174-8043-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:10:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a8bbccbf-8102-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:10:19Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 94843ce9-9215-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:10:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c5a40c4f-9aa2-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:10:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 753031e7-7d68-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:11:05Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a5775d43-b6c8-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:11:13Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5229cdbe-98c7-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:11:14Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 0fb0f488-d631-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:11:15Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 2819bc2b-c8c6-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:11:19Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 165f84e2-dd81-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:11:28Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a06323c6-629a-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:11:31Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 48fb0128-9530-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:12:05Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5603cbab-395f-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:12:32Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 81e4a032-dfcc-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:12:32Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 4bf0540a-44fe-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:12:32Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7803d64b-3b96-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:12:32Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 0cd6cbbb-e955-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:12:32Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: be7aad66-d364-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:12:32Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: f475df61-15c5-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:13:22Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 19516057-7a15-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:13:47Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b830ec4c-3dd1-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:13:47Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: edf929e2-745b-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:13:47Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: b5a8a3b8-3d8c-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:13:47Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 76550f6b-98c3-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:13:47Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c4e567f7-4ab9-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:13:47Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: f4632fa6-6f9e-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:14:38Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 7b06de42-e051-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:03Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8abaa996-7c72-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:03Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: cc88e713-2dae-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:03Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a3ae0f8a-d86d-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:03Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 14e2769d-ea79-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:03Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: dd9ec42b-bcae-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:03Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: afc0fbea-4a3e-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:53Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 9221a2b1-22fe-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 6a5fdee9-21e4-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:15:59Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: a4b198b5-9c07-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:16:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 79af6064-abf7-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:16:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 248f514f-4dd3-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:16:02Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 9544ab9c-1d91-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:16:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 3df786d4-814f-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:16:50Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 7a4f207f-2804-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:16:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3efe176d-1730-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:16:59Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: f8fb2abd-bdc5-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:17:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 03b1697f-08e8-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:17:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: f4d8d1c5-6daf-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:17:02Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c574da09-30fb-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:17:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 928c506c-a57d-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:17:50Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: f53884a6-dc1b-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:04.546467+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:17:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e3f8d095-99ad-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T19:57:02.754451+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:17:59Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 4b9bf7e2-9e26-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:06:14.154836+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 318ef50c-16a4-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:03:14.869540+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 51a3ce17-a90a-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:02Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c42e6072-f260-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:01:27.865295+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 8fe2cb00-4ee5-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:29Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 53b27b73-79bf-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:18:29Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d213cd2d-a1a3-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:18:29Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 618c9efa-c2e3-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:18:29Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 8e8e9b74-e2b4-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:18:29Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 4c65e384-a58d-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:18:35Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8304d51c-51e3-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.560221+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 0f513240-5f86-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.796116+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:37Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 85aa7f78-a081-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:37.123503+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:18:44Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: ecbc4e4c-e58a-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:44.228261+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:19:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3ef00e30-2702-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:19:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 58eca11a-aa71-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:19:35Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 925ad237-836d-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.560221+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:19:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 1a6e08d9-ffe0-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.796116+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:19:37Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 33cd2ffe-c4a5-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:37.123503+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:19:44Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: c3de86a9-2565-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:44.228261+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 6d5d0dc6-2081-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: d83cb088-f9d6-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:21Z] tester (HEARTBEAT): heartbeat: WAITING_ON_ROLE - -tester slice-2: Tests written and locally verified (16/16 pass). NACKed coder v1 at 20:09 on ruff format failure in orchestrator/substrate/in_process.py (5 long-call sites need re-formatting). Cannot propose with `lint` missing from checks_passed (spawn-prompt rule: "Only propose consensus once every configured check passes literally"). Awaiting coder v2 push with `ruff format orchestrator/substrate/in_process.py` applied. HANDOFF already sent. Will re-run lint + propose immediately on coder v2. - -````yaml -id: acd98786-e10e-4a -phase: implement -metadata: - state: WAITING_ON_ROLE - waiting_on: coder - slice_id: slice-2 -```` - -### [2026-05-19T20:20:21Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ba9962fd-5fd7-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:20:25Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,STATUS,OVERSEER_ALERT - -````yaml -id: 02d7d340-1042-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:25.948718+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:26Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: b84e18ce-bc6e-40 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:20:27Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 68707824-a93c-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:26.961327+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:33Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT - -````yaml -id: c0796578-bf27-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:33.111050+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:35Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5390cd40-2d47-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.796116+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:37Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: f5afdb2f-1aa9-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:37.123503+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:20:44Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: d687e34d-bed5-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:44.228261+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:21:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 1676e8e4-8ba1-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:21:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 4de9f23c-b051-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:21:27Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7c963c51-847a-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:26.961327+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:21:33Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT - -````yaml -id: f774b32c-f726-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:33.111050+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:21:36Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 292a13a5-cbf8-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.796116+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:21:37Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a2172991-2f63-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:37.123503+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:21:44Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 6a04aa4a-561e-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:44.228261+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:22:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 30468c2b-a09e-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:22:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 6ba6e61e-98d1-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:22:27Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 57d40719-2249-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:26.961327+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:22:33Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT - -````yaml -id: 0725cbdb-4bb9-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:33.111050+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:22:55Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: befe1e47-f210-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:37.123503+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:22:55Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 8f1b188a-9d4c-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:44.228261+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:22:55Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: cedac962-9e02-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.796116+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:23:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b9ca7f87-ba02-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:23:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 68ca789e-dafa-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:23:40Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: df3d672c-b0eb-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:26.961327+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:23:40Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT - -````yaml -id: f40fbe35-774b-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:33.111050+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:23:51Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 0d021087-fb5d-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:35.796116+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:23:52Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 1e79587d-2f41-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:37.123503+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:23:55Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 5c3272e4-0d39-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:18:44.228261+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:24:02Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8eb4e5e9-afce-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:08:19.102487+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:24:02Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW - -````yaml -id: 11c6314e-cb5f-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:00:30.663856+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: fcd357c5-3799-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:26.961327+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,OVERSEER_ALERT - -````yaml -id: 9c270128-214f-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:20:33.111050+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 3a56b578-a013-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 1948ca7b-5a21-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6406ada8-23d9-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6c240c32-aa01-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] coder (CONSENSUS_PROPOSE): Proposal from coder - -Slice-2 coder v2 — addresses NACK blockers from reviewer_concurrency (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format). Commit d7b2a6152. - -Blocker fixes: - -reviewer_concurrency C1: `_spawn_plan_producer` no longer writes `$HOME/.claude/egg-active-role.json` (the 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. `_spawn_plan_reviewer` (single dispatch) keeps the sentinel write. - -reviewer_concurrency C2: added `self._current_phase = "refine"` field on `_InProcessOrchestrator.__init__`; flipped to "plan" at the top of `_run_plan_phase`. `_publish_heartbeat` reads from it, so HEARTBEAT messages carry the correct `phase` string for stuck-phase-transition watchdogs (was hardcoded "refine"). - -reviewer_code_holistic H1: `_run_plan_phase_inner` now spawns architect synchronously first, records its CONSENSUS_PROPOSE on the tracker, then fans out task_planner + risk_analyst concurrently via `ThreadPoolExecutor(max_workers=2)`. The architect's per-role output path threads into each downstream producer's spawn env (`EGG_ARCHITECT_OUTPUT_PATH`) and prompt_text. Matches `shared/egg_contracts/agent_roles.py:398/422` role-dependency declarations and the rubric bodies. - -reviewer_code_holistic H2: new `read_plan_reviewer_verdicts` parses `.egg-state/agent-outputs/-reviewer_plan-output.json`; `_apply_reviewer_verdicts` drives per-edge ACK / NACK on the tracker based on the reviewer's actual verdicts. Fail-closed (NACK every edge) when the verdict file is missing AND the reviewer's spawn returned exit_code != 0. Optimistic ACK only on the harness-faked path (missing file + exit 0), with the discrepancy surfaced in the placeholder body. `_spawn_plan_reviewer` env shaping now uses per-role `EGG__OUTPUT_PATH` + `EGG_REVIEWER_VERDICT_PATH` (rubric-input shape) instead of the dead `EGG_PRODUCER_ARTIFACT_PATHS`. - -tester T1: ran `ruff format` on both modified files. Lint + format gates pass. - -Non-blocker polish landed alongside the blockers (every non-blocker from the three NACKs): - -reviewer_concurrency NB2 (synthetic commit): `synthetic_commit_for(role)` derives per-role hex SHAs via SHA-1 of the role name; ProposalPayload entries now distinguishable per producer. - -reviewer_concurrency NB1 + reviewer_code_holistic NB4 (silent except): all `tracker.handle_*` guard-rejection branches now log via `logging.getLogger("orchestrator.substrate.in_process").warning` rather than silent `except Exception: pass`. The eval snapshot remains the authoritative source for the HITL gate; logging is a debug surface. - -reviewer_code_holistic NB3 (dead env vars): `EGG_PRODUCER_ARTIFACT_PATHS` is gone. `EGG_REFINE_ARTIFACT_PATH` / `EGG_PLAN_ARTIFACT_PATH` stay because the rubrics' Task-context block names them; `EGG_PRODUCER_OUTPUT_PATH` (new) and `EGG_ARCHITECT_OUTPUT_PATH` (new) match the per-rubric input enumeration. - -reviewer_code_holistic NB (reviewer in placeholder): `format_plan_placeholder` renders reviewer_plan diagnostics + verdict-parsing status alongside producer diagnostics so the operator at the plan HITL gate sees end-to-end visibility. - -reviewer_code_holistic NB (synthetic commit escape): `_SYNTHETIC_PLAN_COMMIT` docstring now explicitly states it must never escape the in-process driver and points at the production-consumer hex-validate risk. - -File decomposition: -ruff format expanded v1 from 1411 → 1879 lines, breaching the 1500-line hard cap. Extracted plan-phase body to new `orchestrator/substrate/_plan_phase.py` as module-level functions that take the orchestrator instance as their first arg. The class methods (`_run_plan_phase`, `_spawn_plan_producer`, `_spawn_plan_reviewer`, `_plan_producer_output_path`, `_read_plan_reviewer_verdicts`) stay as thin delegates so tester's 16 passing v1 tests keep the same method-call surface. `in_process.py` lands at 1093 lines; `_plan_phase.py` at 680. `python3 scripts/check-file-sizes.py` passes (no errors). Decomposition rationale in `_plan_phase.py`'s module docstring. - -Manual in-process smoke (harness fakes, MagicMock subagents) covers four new behavioural scenarios: happy path (5 spawns in architect-first order, tracker is_complete=True), refine-stop (refine artifact returned, no plan spawns), mixed verdict ACK/NACK (verdict-driven NACK on task_planner→reviewer_plan), fail-closed (no verdict + reviewer exit 1 → every critical edge NACK'd). All ruff format + lint + file-size checks pass. - -````yaml -id: 128db3b2-e0da-44 -phase: implement -metadata: - payload: - summary: "Slice-2 coder v2 \u2014 addresses NACK blockers from reviewer_concurrency\ - \ (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first\ - \ ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format).\ - \ Commit d7b2a6152.\n\nBlocker fixes:\n\nreviewer_concurrency C1: `_spawn_plan_producer`\ - \ no longer writes `$HOME/.claude/egg-active-role.json` (the 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. `_spawn_plan_reviewer`\ - \ (single dispatch) keeps the sentinel write.\n\nreviewer_concurrency C2: added\ - \ `self._current_phase = \"refine\"` field on `_InProcessOrchestrator.__init__`;\ - \ flipped to \"plan\" at the top of `_run_plan_phase`. `_publish_heartbeat`\ - \ reads from it, so HEARTBEAT messages carry the correct `phase` string for\ - \ stuck-phase-transition watchdogs (was hardcoded \"refine\").\n\nreviewer_code_holistic\ - \ H1: `_run_plan_phase_inner` now spawns architect synchronously first, records\ - \ its CONSENSUS_PROPOSE on the tracker, then fans out task_planner + risk_analyst\ - \ concurrently via `ThreadPoolExecutor(max_workers=2)`. The architect's per-role\ - \ output path threads into each downstream producer's spawn env (`EGG_ARCHITECT_OUTPUT_PATH`)\ - \ and prompt_text. Matches `shared/egg_contracts/agent_roles.py:398/422` role-dependency\ - \ declarations and the rubric bodies.\n\nreviewer_code_holistic H2: new `read_plan_reviewer_verdicts`\ - \ parses `.egg-state/agent-outputs/-reviewer_plan-output.json`; `_apply_reviewer_verdicts`\ - \ drives per-edge ACK / NACK on the tracker based on the reviewer's actual verdicts.\ - \ Fail-closed (NACK every edge) when the verdict file is missing AND the reviewer's\ - \ spawn returned exit_code != 0. Optimistic ACK only on the harness-faked path\ - \ (missing file + exit 0), with the discrepancy surfaced in the placeholder\ - \ body. `_spawn_plan_reviewer` env shaping now uses per-role `EGG__OUTPUT_PATH`\ - \ + `EGG_REVIEWER_VERDICT_PATH` (rubric-input shape) instead of the dead `EGG_PRODUCER_ARTIFACT_PATHS`.\n\ - \ntester T1: ran `ruff format` on both modified files. Lint + format gates pass.\n\ - \nNon-blocker polish landed alongside the blockers (every non-blocker from the\ - \ three NACKs):\n\nreviewer_concurrency NB2 (synthetic commit): `synthetic_commit_for(role)`\ - \ derives per-role hex SHAs via SHA-1 of the role name; ProposalPayload entries\ - \ now distinguishable per producer.\n\nreviewer_concurrency NB1 + reviewer_code_holistic\ - \ NB4 (silent except): all `tracker.handle_*` guard-rejection branches now log\ - \ via `logging.getLogger(\"orchestrator.substrate.in_process\").warning` rather\ - \ than silent `except Exception: pass`. The eval snapshot remains the authoritative\ - \ source for the HITL gate; logging is a debug surface.\n\nreviewer_code_holistic\ - \ NB3 (dead env vars): `EGG_PRODUCER_ARTIFACT_PATHS` is gone. `EGG_REFINE_ARTIFACT_PATH`\ - \ / `EGG_PLAN_ARTIFACT_PATH` stay because the rubrics' Task-context block names\ - \ them; `EGG_PRODUCER_OUTPUT_PATH` (new) and `EGG_ARCHITECT_OUTPUT_PATH` (new)\ - \ match the per-rubric input enumeration.\n\nreviewer_code_holistic NB (reviewer\ - \ in placeholder): `format_plan_placeholder` renders reviewer_plan diagnostics\ - \ + verdict-parsing status alongside producer diagnostics so the operator at\ - \ the plan HITL gate sees end-to-end visibility.\n\nreviewer_code_holistic NB\ - \ (synthetic commit escape): `_SYNTHETIC_PLAN_COMMIT` docstring now explicitly\ - \ states it must never escape the in-process driver and points at the production-consumer\ - \ hex-validate risk.\n\nFile decomposition:\nruff format expanded v1 from 1411\ - \ \u2192 1879 lines, breaching the 1500-line hard cap. Extracted plan-phase\ - \ body to new `orchestrator/substrate/_plan_phase.py` as module-level functions\ - \ that take the orchestrator instance as their first arg. The class methods\ - \ (`_run_plan_phase`, `_spawn_plan_producer`, `_spawn_plan_reviewer`, `_plan_producer_output_path`,\ - \ `_read_plan_reviewer_verdicts`) stay as thin delegates so tester's 16 passing\ - \ v1 tests keep the same method-call surface. `in_process.py` lands at 1093\ - \ lines; `_plan_phase.py` at 680. `python3 scripts/check-file-sizes.py` passes\ - \ (no errors). Decomposition rationale in `_plan_phase.py`'s module docstring.\n\ - \nManual in-process smoke (harness fakes, MagicMock subagents) covers four new\ - \ behavioural scenarios: happy path (5 spawns in architect-first order, tracker\ - \ is_complete=True), refine-stop (refine artifact returned, no plan spawns),\ - \ mixed verdict ACK/NACK (verdict-driven NACK on task_planner\u2192reviewer_plan),\ - \ fail-closed (no verdict + reviewer exit 1 \u2192 every critical edge NACK'd).\ - \ All ruff format + lint + file-size checks pass." - attestation: - commit_sha: d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - commit_shas: - - d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - files_changed: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - checks_passed: - - ruff_format_check - - ruff_check_lint - - file_size_lint - - manual_smoke_happy_path - - manual_smoke_refine_stop - - manual_smoke_mixed_verdict - - manual_smoke_fail_closed - - import_check - tests_run: 8 - no_test_changes_needed: true - no_test_changes_reason: Coder role's allow-list excludes tests/. Class methods - stayed as thin delegates so tester's existing 16 passing tests keep working. - Eight manual smoke runs cover the new behaviour surface. - artifacts: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - risk_considered: "v2 risks: (1) Decomposition risk \u2014 extracting ~700 lines\ - \ to `_plan_phase.py` could break tester's 16 v1 tests if they call the class\ - \ methods. Mitigated by keeping the class methods as thin delegates with identical\ - \ signatures. (2) Verdict-JSON parsing risk \u2014 fail-closed-on-missing-verdict\ - \ could break harness-faked tests that rely on optimistic ACK. Mitigated by\ - \ only fail-closing when reviewer exit_code is non-zero; the missing-file +\ - \ exit-0 path keeps the optimistic ACK behaviour with a placeholder-body discrepancy\ - \ surface so the operator sees it at the HITL gate. (3) Phase-flip atomicity\ - \ \u2014 `self._current_phase` is written from `_run_plan_phase` and read from\ - \ `_publish_heartbeat`; Python GIL makes the single assignment atomic, and the\ - \ reader doesn't compose with other state in a way that observes inconsistent\ - \ snapshots." - commit_sha: d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - files_changed: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - tests_run: [] - tasks_satisfied: - - task-2-1 - - task-2-2 - - task-2-5 - version: 2 - commit_sha: d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 3c09a940-b384-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: fbc571b4-b883-47 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:24:43Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d7d900ad-4d91-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:25:11Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 148f0318-dc03-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:24:48.589646+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:25:11Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 12830da3-33f9-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:24:49.288373+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:25:11Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 71a2c747-2391-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:24:49.774794+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:25:11Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: ed745c64-5c37-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:24:50.772614+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:25:11Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ac89609b-98da-42 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:25:11Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f0244c46-9f4e-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:25:29Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: da2a62b9-2f0b-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:25:29.414853+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:25:36Z] coder (CONSENSUS_PROPOSE): Proposal from coder - -Slice-2 coder v2 — addresses NACK blockers from reviewer_concurrency (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format). Commit d7b2a6152. Also folds in every non-blocker noted in the three NACKs (synthetic commit per role, log warnings on tracker guard rejections, reviewer in placeholder, dead env vars dropped, _SYNTHETIC_PLAN_COMMIT docstring caveat). ruff format expanded the v1 diff to 1879 lines; extracted plan-phase body to new `orchestrator/substrate/_plan_phase.py` (680 lines) as module functions taking the orchestrator instance as their first arg. Class methods stay as thin delegates so tester's 16 passing v1 tests keep working. `in_process.py` now 1093 lines. `python3 scripts/check-file-sizes.py` passes. Manual smoke: happy path (5 spawns architect-first, is_complete=True), refine-stop (refine artifact returned, no plan dispatch), mixed verdict (verdict-driven NACK on task_planner→reviewer_plan), fail-closed (no verdict + reviewer exit 1 → critical edges NACK'd, advisory edge confirms). - -````yaml -id: 140eb5ea-5a9e-4d -phase: implement -metadata: - payload: - summary: "Slice-2 coder v2 \u2014 addresses NACK blockers from reviewer_concurrency\ - \ (C1 sentinel race + C2 heartbeat phase), reviewer_code_holistic (H1 architect-first\ - \ ordering + H2 reviewer-verdict-JSON parsing), and tester (T1 ruff format).\ - \ Commit d7b2a6152. Also folds in every non-blocker noted in the three NACKs\ - \ (synthetic commit per role, log warnings on tracker guard rejections, reviewer\ - \ in placeholder, dead env vars dropped, _SYNTHETIC_PLAN_COMMIT docstring caveat).\ - \ ruff format expanded the v1 diff to 1879 lines; extracted plan-phase body\ - \ to new `orchestrator/substrate/_plan_phase.py` (680 lines) as module functions\ - \ taking the orchestrator instance as their first arg. Class methods stay as\ - \ thin delegates so tester's 16 passing v1 tests keep working. `in_process.py`\ - \ now 1093 lines. `python3 scripts/check-file-sizes.py` passes. Manual smoke:\ - \ happy path (5 spawns architect-first, is_complete=True), refine-stop (refine\ - \ artifact returned, no plan dispatch), mixed verdict (verdict-driven NACK on\ - \ task_planner\u2192reviewer_plan), fail-closed (no verdict + reviewer exit\ - \ 1 \u2192 critical edges NACK'd, advisory edge confirms)." - attestation: - commit_sha: d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - commit_shas: - - d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - files_changed: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - checks_passed: - - ruff_format_check - - ruff_check_lint - - file_size_lint - - manual_smoke_happy_path - - manual_smoke_refine_stop - - manual_smoke_mixed_verdict - - manual_smoke_fail_closed - - import_check - tests_run: 8 - no_test_changes_needed: true - no_test_changes_reason: Coder role's allow-list excludes tests/. Class methods - stayed as thin delegates so tester's existing 16 passing v1 tests keep working. - Eight manual smoke runs cover the new behaviour surface. - artifacts: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - risk_considered: "Decomposition could break tester's v1 tests if they called methods\ - \ directly; mitigated by keeping class methods as thin delegates with identical\ - \ signatures. Verdict-JSON fail-closed could break harness-faked tests reliant\ - \ on optimistic ACK; mitigated by only fail-closing when reviewer exit_code\ - \ != 0 \u2014 the missing-file + exit-0 path preserves optimistic ACK with a\ - \ placeholder-body discrepancy surface. Phase-flip atomicity is fine per Python\ - \ GIL on a single assignment." - commit_sha: d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - files_changed: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - tests_run: [] - tasks_satisfied: - - task-2-1 - - task-2-2 - - task-2-5 - version: 3 - commit_sha: d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 - slice_id: slice-2 -```` - -### [2026-05-19T20:25:36Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f43e4335-f839-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:25:36Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 642c4c16-30c9-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:25:36Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 923e5053-e688-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:25:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7ce5e59a-924e-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:25:45.362708+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:25:58Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 9fd88793-01bc-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:25:51.626425+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:25:58Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 352b9725-d257-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:26:23Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5012afde-5a5d-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:26:03.178962+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:26:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 263de540-03b5-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:25:45.362708+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:27:07Z] tester → coder (CONSENSUS_ACK): ACK from tester for coder - - -Reviewed coder v2 (commit `d7b2a6152`, slice-2 TASK-2-1 / TASK-2-2 / TASK-2-5 no-op). - -**v1 blocker T1 fully resolved.** `PYTHONPATH=shared:gateway:orchestrator ruff format --check .` now exits 0 across all 962 files; the 5 long-call sites in `_run_plan_phase` v1 are reformatted (and the decomposition into `orchestrator/substrate/_plan_phase.py` moved the affected blocks to a standalone module that also passes `ruff format --check`). `ruff check` and `mypy gateway shared sandbox` are both green. - -**16/16 tester-authored tests still pass against v2** (`integration_tests/regression/test_inprocess_plan_brc.py` 7/7; `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` 9/9) with no test edits — the architectural changes (architect-first synchronous spawn then fanout via 2-worker pool for task_planner + risk_analyst; verdict-JSON ACK/NACK parsing; sentinel removal from concurrent producer path) preserve every observable invariant the tests pin: -- 3 plan producers + 1 reviewer spawn through the substrate (sequence is now architect → task_planner ∥ risk_analyst → reviewer_plan, which my `test_plan_stage_spawns_three_producers_and_one_reviewer` still passes because it asserts on the *set* of spawned roles, not ordering); -- BRC reaches `is_complete=True` with all 4 plan-team roles confirmed (verdict-JSON parsing falls back to optimistic-ACK on the harness-faked path where `agent_outputs/-reviewer_plan-output.json` is absent and reviewer exit_code==0, exactly as your H2 narrative describes); -- plan-HITL decision shape unchanged (`phase="plan"`, `decision_type="phase_gate"`); -- non-`approve_continue` refine answers still short-circuit before the plan stage (no plan-producer spawn); -- implement-phase roles still not spawned; -- refiner spawned exactly once; -- every plan-phase spawn carries `EGG_PHASE=plan`. - -### v2 deltas spot-checked - -1. **`_plan_phase.py` decomposition** — clean module boundary (lazy imports for `PeerConsensusTracker` / `get_review_graph_for_phase` / `AgentRole` at use site; the underscore prefix matches the project's submodule pattern from `docs/guides/decomposition-pattern.md`). `_run_plan_phase` in `in_process.py:233` now delegates to `_plan_phase._run_plan_phase_inner(self, refine_artifact_path)`; the runner instance's attributes (`self._bundle`, `self._plan_tracker`, `self._current_phase`) carry the state across the call boundary. Module-level surface is correctly minimal (`run_plan_phase` is the only public symbol; the helpers are private). - -2. **C1 fix — sentinel removed from concurrent producer path** (`_plan_phase.py` — no `_write_active_role_sentinel` call inside `_spawn_plan_producer_inner`). The reviewer path retains it (`_spawn_plan_reviewer_inner`). The R2-deferral docstring on `_write_active_role_sentinel` previously documented the last-writer-wins limitation; this fix actively avoids hitting it for the concurrent fanout. The `EGG_AGENT_ROLE` env var remains the load-bearing role-resolution channel per spawn. Architecturally correct. - -3. **C2 fix — `_current_phase` state** (`in_process.py:189` set to `"refine"`; flipped to `"plan"` at the top of `_run_plan_phase`). `_publish_heartbeat` (`in_process.py:373`) reads from it. Stuck-phase-transition watchdogs filtering by `phase` now see the right phase across the transition. Sound. - -4. **H1 fix — architect-first then fanout** (`_plan_phase.py:_run_plan_phase_inner`). Architect synchronously spawns first; `EGG_ARCHITECT_OUTPUT_PATH` is threaded into the env + prompt of `task_planner` and `risk_analyst`. The order matches `shared/egg_contracts/agent_roles.py:398,422` (`TASK_PLANNER_ROLE.dependencies = [AgentRole.ARCHITECT]`, `RISK_ANALYST_ROLE.dependencies = [AgentRole.ARCHITECT]`). Matches the rubric semantics shipped by the documenter in `architect.md` / `task_planner.md` / `risk_analyst.md`. - -5. **H2 fix — reviewer_plan verdict-JSON parsing** (`_plan_phase.py:read_plan_reviewer_verdicts` / `_apply_reviewer_verdicts`). The fail-closed branch (verdict file missing AND reviewer exit_code != 0 → NACK every edge) is the right default; the optimistic-ACK branch (verdict file missing AND reviewer exit_code == 0) preserves the harness-faked test path I depend on while surfacing `verdict-not-parsed` in the placeholder body for the operator. The valid-JSON branch correctly applies per-edge ACK/NACK based on the reviewer's declared verdict. Good defensive shape. - -### Non-blocking (carry to follow-up) - -- **Verdict-JSON schema is documented in the reviewer_plan rubric body but not in a typed validator.** `read_plan_reviewer_verdicts` does a loose dict probe (`isinstance(v, dict)`, `.get("verdict")`). A malformed reviewer output (e.g. `{"verdicts": "approved"}` — bare string instead of per-producer dict) would silently degrade to the empty-verdicts case. Adding a `pydantic` / `dataclass`-backed schema (or a JSON Schema check) would surface that as a structured error rather than a soft fallback. Not blocking because the placeholder body surfaces `verdict-not-parsed` so the operator's HITL gate sees the discrepancy; just worth a follow-up. - -- **`_current_phase` is set as a bare string field with no enum**. Setting it to an unknown phase string would silently produce a misleading heartbeat. The existing `phase` argument on `Message` is also loosely typed; if a future slice introduces a `PipelinePhase` enum that's the right tightening point. Not blocking. - -- **`_spawn_plan_reviewer_inner` retains the sentinel write** (`_plan_phase.py`). Correct per your C1 narrative (the reviewer's spawn never overlaps another spawn), but a one-line comment at the call site documenting WHY this single survives the C1 removal would help a future reader who scans both paths. Not blocking; the C1 commit message already documents it. - -- **`EGG_REFINE_ARTIFACT_PATH` is set as a per-producer env arg but not on the reviewer's env** (`_plan_phase.py:_spawn_plan_reviewer_inner`). Since the reviewer's prompt names the producer artifact paths explicitly (`producer_artifact_paths` repr), the refine artifact isn't strictly needed — but for consistency with the production prompt's "Refine artifact: ..." preamble, threading it through the reviewer's env wouldn't hurt. Not blocking. - -All ACs in the contract task-2-1 / task-2-2 / task-2-5 are satisfied; lint is green; my 16/16 tests pass against v2 with no edits. ACK. - - -````yaml -id: d141bf25-afe5-45 -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - reason: "\nReviewed coder v2 (commit `d7b2a6152`, slice-2 TASK-2-1 / TASK-2-2\ - \ / TASK-2-5 no-op). \n\n**v1 blocker T1 fully resolved.** `PYTHONPATH=shared:gateway:orchestrator\ - \ ruff format --check .` now exits 0 across all 962 files; the 5 long-call sites\ - \ in `_run_plan_phase` v1 are reformatted (and the decomposition into `orchestrator/substrate/_plan_phase.py`\ - \ moved the affected blocks to a standalone module that also passes `ruff format\ - \ --check`). `ruff check` and `mypy gateway shared sandbox` are both green.\ - \ \n\n**16/16 tester-authored tests still pass against v2** (`integration_tests/regression/test_inprocess_plan_brc.py`\ - \ 7/7; `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` 9/9) with\ - \ no test edits \u2014 the architectural changes (architect-first synchronous\ - \ spawn then fanout via 2-worker pool for task_planner + risk_analyst; verdict-JSON\ - \ ACK/NACK parsing; sentinel removal from concurrent producer path) preserve\ - \ every observable invariant the tests pin:\n- 3 plan producers + 1 reviewer\ - \ spawn through the substrate (sequence is now architect \u2192 task_planner\ - \ \u2225 risk_analyst \u2192 reviewer_plan, which my `test_plan_stage_spawns_three_producers_and_one_reviewer`\ - \ still passes because it asserts on the *set* of spawned roles, not ordering);\n\ - - BRC reaches `is_complete=True` with all 4 plan-team roles confirmed (verdict-JSON\ - \ parsing falls back to optimistic-ACK on the harness-faked path where `agent_outputs/-reviewer_plan-output.json`\ - \ is absent and reviewer exit_code==0, exactly as your H2 narrative describes);\n\ - - plan-HITL decision shape unchanged (`phase=\"plan\"`, `decision_type=\"phase_gate\"\ - `);\n- non-`approve_continue` refine answers still short-circuit before the\ - \ plan stage (no plan-producer spawn);\n- implement-phase roles still not spawned;\n\ - - refiner spawned exactly once;\n- every plan-phase spawn carries `EGG_PHASE=plan`.\n\ - \n### v2 deltas spot-checked\n\n1. **`_plan_phase.py` decomposition** \u2014\ - \ clean module boundary (lazy imports for `PeerConsensusTracker` / `get_review_graph_for_phase`\ - \ / `AgentRole` at use site; the underscore prefix matches the project's submodule\ - \ pattern from `docs/guides/decomposition-pattern.md`). `_run_plan_phase` in\ - \ `in_process.py:233` now delegates to `_plan_phase._run_plan_phase_inner(self,\ - \ refine_artifact_path)`; the runner instance's attributes (`self._bundle`,\ - \ `self._plan_tracker`, `self._current_phase`) carry the state across the call\ - \ boundary. Module-level surface is correctly minimal (`run_plan_phase` is the\ - \ only public symbol; the helpers are private).\n\n2. **C1 fix \u2014 sentinel\ - \ removed from concurrent producer path** (`_plan_phase.py` \u2014 no `_write_active_role_sentinel`\ - \ call inside `_spawn_plan_producer_inner`). The reviewer path retains it (`_spawn_plan_reviewer_inner`).\ - \ The R2-deferral docstring on `_write_active_role_sentinel` previously documented\ - \ the last-writer-wins limitation; this fix actively avoids hitting it for the\ - \ concurrent fanout. The `EGG_AGENT_ROLE` env var remains the load-bearing role-resolution\ - \ channel per spawn. Architecturally correct.\n\n3. **C2 fix \u2014 `_current_phase`\ - \ state** (`in_process.py:189` set to `\"refine\"`; flipped to `\"plan\"` at\ - \ the top of `_run_plan_phase`). `_publish_heartbeat` (`in_process.py:373`)\ - \ reads from it. Stuck-phase-transition watchdogs filtering by `phase` now see\ - \ the right phase across the transition. Sound.\n\n4. **H1 fix \u2014 architect-first\ - \ then fanout** (`_plan_phase.py:_run_plan_phase_inner`). Architect synchronously\ - \ spawns first; `EGG_ARCHITECT_OUTPUT_PATH` is threaded into the env + prompt\ - \ of `task_planner` and `risk_analyst`. The order matches `shared/egg_contracts/agent_roles.py:398,422`\ - \ (`TASK_PLANNER_ROLE.dependencies = [AgentRole.ARCHITECT]`, `RISK_ANALYST_ROLE.dependencies\ - \ = [AgentRole.ARCHITECT]`). Matches the rubric semantics shipped by the documenter\ - \ in `architect.md` / `task_planner.md` / `risk_analyst.md`.\n\n5. **H2 fix\ - \ \u2014 reviewer_plan verdict-JSON parsing** (`_plan_phase.py:read_plan_reviewer_verdicts`\ - \ / `_apply_reviewer_verdicts`). The fail-closed branch (verdict file missing\ - \ AND reviewer exit_code != 0 \u2192 NACK every edge) is the right default;\ - \ the optimistic-ACK branch (verdict file missing AND reviewer exit_code ==\ - \ 0) preserves the harness-faked test path I depend on while surfacing `verdict-not-parsed`\ - \ in the placeholder body for the operator. The valid-JSON branch correctly\ - \ applies per-edge ACK/NACK based on the reviewer's declared verdict. Good defensive\ - \ shape.\n\n### Non-blocking (carry to follow-up)\n\n- **Verdict-JSON schema\ - \ is documented in the reviewer_plan rubric body but not in a typed validator.**\ - \ `read_plan_reviewer_verdicts` does a loose dict probe (`isinstance(v, dict)`,\ - \ `.get(\"verdict\")`). A malformed reviewer output (e.g. `{\"verdicts\": \"\ - approved\"}` \u2014 bare string instead of per-producer dict) would silently\ - \ degrade to the empty-verdicts case. Adding a `pydantic` / `dataclass`-backed\ - \ schema (or a JSON Schema check) would surface that as a structured error rather\ - \ than a soft fallback. Not blocking because the placeholder body surfaces `verdict-not-parsed`\ - \ so the operator's HITL gate sees the discrepancy; just worth a follow-up.\n\ - \n- **`_current_phase` is set as a bare string field with no enum**. Setting\ - \ it to an unknown phase string would silently produce a misleading heartbeat.\ - \ The existing `phase` argument on `Message` is also loosely typed; if a future\ - \ slice introduces a `PipelinePhase` enum that's the right tightening point.\ - \ Not blocking.\n\n- **`_spawn_plan_reviewer_inner` retains the sentinel write**\ - \ (`_plan_phase.py`). Correct per your C1 narrative (the reviewer's spawn never\ - \ overlaps another spawn), but a one-line comment at the call site documenting\ - \ WHY this single survives the C1 removal would help a future reader who scans\ - \ both paths. Not blocking; the C1 commit message already documents it.\n\n\ - - **`EGG_REFINE_ARTIFACT_PATH` is set as a per-producer env arg but not on the\ - \ reviewer's env** (`_plan_phase.py:_spawn_plan_reviewer_inner`). Since the\ - \ reviewer's prompt names the producer artifact paths explicitly (`producer_artifact_paths`\ - \ repr), the refine artifact isn't strictly needed \u2014 but for consistency\ - \ with the production prompt's \"Refine artifact: ...\" preamble, threading\ - \ it through the reviewer's env wouldn't hurt. Not blocking.\n\nAll ACs in the\ - \ contract task-2-1 / task-2-2 / task-2-5 are satisfied; lint is green; my 16/16\ - \ tests pass against v2 with no edits. ACK.\n" - ack_version: 3 - version: 3 - slice_id: slice-2 -```` - -### [2026-05-19T20:27:07Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 652bcc98-6c8e-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:27:13Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 44820bd9-5ea0-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:27:13.630131+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:27:55Z] reviewer_contract → coder (CONSENSUS_ACK): ACK from reviewer_contract for coder - - -Re-review of coder v3 (same commit_sha d7b2a615268c0ac37a7ed872f2b16c48ade4abe2 — v2 → v3 is a re-propose at the same artifact). Contract-verification focus held over from my v2 ACK; the underlying commit's task-2-1/2-2/2-5 verification is unchanged. - -### Per-task verification (v3) - -**TASK-2-1 — `_run_plan_phase`** (orchestrator/substrate/in_process.py:850-921 thin delegates + orchestrator/substrate/_plan_phase.py:36-210 body): -1. AC "no longer raises NotImplementedError when the operator advances past refine": ✅ `run()` at in_process.py:246 calls `self._run_plan_phase(...)` which delegates to `_plan_phase.run_plan_phase`. The walking-skeleton fence (`_maybe_fence`) now fires only on the plan HITL gate's `approve_continue`, with a diagnostic pointing at slice-3 / slice-4 of the #2717 rollout. -2. AC "plan stage spawns 3 producers concurrently via the executor": ✅ Spirit-of-AC satisfied. v3 implements architect-first synchronous spawn (_plan_phase.py:124-135) followed by `task_planner + risk_analyst` concurrent fan-out through `ThreadPoolExecutor(max_workers=2)` (lines 137-161). The intentional deviation from "3 concurrent" honours the role-dependency contract: `shared/egg_contracts/agent_roles.py` declares `TASK_PLANNER_ROLE` / `RISK_ANALYST_ROLE` with `dependencies=[ARCHITECT]`, and architect's per-role output path flows downstream via `EGG_ARCHITECT_OUTPUT_PATH` (line 470 + prompt at line 484). Required by reviewer_code_holistic v1 H1 NACK. -3. AC "reviewer_plan is spawned after each CONSENSUS_PROPOSE": ✅ Single dispatch (line 491-551) followed by verdict-JSON-driven per-edge ACK/NACK via `read_plan_reviewer_verdicts` (line 251-286) and `_apply_reviewer_verdicts` (line 289-371). Fail-closed branch NACKs every edge when the verdict file is missing AND reviewer exit_code != 0 (lines 310, 322-336); harness-fake branch ACKs with a "verdict not parsed" diagnostic when verdict missing + reviewer exit 0. Each producer edge receives its own tracker verdict tagged by `(reviewer_plan, producer)`. -4. AC "yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge": ✅ `tracker.handle_confirmed(role.value)` is invoked at _plan_phase.py:188-192 for every plan producer AND reviewer_plan; `evaluate()` at line 194 produces the snapshot; `_build_plan_gate_decision` (in_process.py:660-720) yields `HITLDecision(phase="plan")` with the canonical 4-way options on convergence, retry/abort on non-convergence. -5. AC "existing refine path still works": ✅ Refine flow at in_process.py:213-240 is structurally unchanged; `self._current_phase` is initialised to `"refine"` (line 202) so heartbeats during refine continue to carry the right phase string before flipping to "plan" inside `_plan_phase.run_plan_phase` (line 67). - -**TASK-2-2 — `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376): -1. AC "loader returns rubric bodies for all four plan-team roles": ✅ `_RUBRIC_LANDED_ROLES` (lines 272-286) includes architect, task_planner, risk_analyst, reviewer_plan. The fence at line 348 no longer rejects these; line 362-375 returns `rubric_path.read_text(...)` when the markdown is on disk. -2. AC "implement-team roles still raise ValueError with the 'follow-up slice 3' hint": ✅ `_ROLE_RUBRIC_SLICES` (lines 254-262) maps the eight implement-team roles to `"slice-3"`; the ValueError at line 356-360 interpolates `slice_hint` into the message ("deferred to follow-up slice-3 of issue #2717's rollout"). Structured-error contract preserved. - -**TASK-2-5 — sandbox restrictions parallel validator**: ✅ Closed as no-op per slice-1 R2 = pass verdict (pinned by `integration_tests/regression/test_pretooluse_hook_nested.py:212-238`). No changes to `sandbox/egg_agent_tools/handlers/restrictions.py` in this proposal. Coder commit message records the required close-with-note ("no-op: hooks resolve role correctly; structural enforcement remains hook-side"). - -### File-decomposition delta (informational) - -The ruff format pass expanded the v1 diff past the 1500-line hard cap (`scripts/file-size-allowlist.yaml`), so the coder extracted ~680 lines of plan-phase body into `orchestrator/substrate/_plan_phase.py`. Class methods `_run_plan_phase`/`_spawn_plan_producer`/`_spawn_plan_reviewer`/`_plan_producer_output_path`/`_read_plan_reviewer_verdicts` stay as thin delegates (in_process.py:850-921). `in_process.py` is 1093 lines, `_plan_phase.py` is 680 lines — both under the cap. Decomposition is invisible to AC-level verification (same public method names; same call surface). - -### Non-blocking observations - -- Slice-1 contract bookkeeping: tasks task-1-1 … task-1-9 still show `status: "pending"` despite their commits being linked. Not a slice-2 coder issue; operator should reconcile before declaring the rollout complete. -- The `synthetic_commit_for(role)` SHA prefix at _plan_phase.py:644-656 emits `ace1<3-hex>` — fine for 3 producers (collision impossible) and obviously synthetic in logs. -- Fail-closed reason string ("reviewer_plan verdict file missing / unparseable AND reviewer exit_code=…") surfaces in the placeholder body; if a future regression test wants to pin the operator-facing wording, the `_verdict_diagnostics` dict on the runner is the structured surface to assert against. - - -````yaml -id: 6321c79a-c654-4e -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - reason: "\nRe-review of coder v3 (same commit_sha d7b2a615268c0ac37a7ed872f2b16c48ade4abe2\ - \ \u2014 v2 \u2192 v3 is a re-propose at the same artifact). Contract-verification\ - \ focus held over from my v2 ACK; the underlying commit's task-2-1/2-2/2-5 verification\ - \ is unchanged.\n\n### Per-task verification (v3)\n\n**TASK-2-1 \u2014 `_run_plan_phase`**\ - \ (orchestrator/substrate/in_process.py:850-921 thin delegates + orchestrator/substrate/_plan_phase.py:36-210\ - \ body):\n1. AC \"no longer raises NotImplementedError when the operator advances\ - \ past refine\": \u2705 `run()` at in_process.py:246 calls `self._run_plan_phase(...)`\ - \ which delegates to `_plan_phase.run_plan_phase`. The walking-skeleton fence\ - \ (`_maybe_fence`) now fires only on the plan HITL gate's `approve_continue`,\ - \ with a diagnostic pointing at slice-3 / slice-4 of the #2717 rollout.\n2.\ - \ AC \"plan stage spawns 3 producers concurrently via the executor\": \u2705\ - \ Spirit-of-AC satisfied. v3 implements architect-first synchronous spawn (_plan_phase.py:124-135)\ - \ followed by `task_planner + risk_analyst` concurrent fan-out through `ThreadPoolExecutor(max_workers=2)`\ - \ (lines 137-161). The intentional deviation from \"3 concurrent\" honours the\ - \ role-dependency contract: `shared/egg_contracts/agent_roles.py` declares `TASK_PLANNER_ROLE`\ - \ / `RISK_ANALYST_ROLE` with `dependencies=[ARCHITECT]`, and architect's per-role\ - \ output path flows downstream via `EGG_ARCHITECT_OUTPUT_PATH` (line 470 + prompt\ - \ at line 484). Required by reviewer_code_holistic v1 H1 NACK.\n3. AC \"reviewer_plan\ - \ is spawned after each CONSENSUS_PROPOSE\": \u2705 Single dispatch (line 491-551)\ - \ followed by verdict-JSON-driven per-edge ACK/NACK via `read_plan_reviewer_verdicts`\ - \ (line 251-286) and `_apply_reviewer_verdicts` (line 289-371). Fail-closed\ - \ branch NACKs every edge when the verdict file is missing AND reviewer exit_code\ - \ != 0 (lines 310, 322-336); harness-fake branch ACKs with a \"verdict not parsed\"\ - \ diagnostic when verdict missing + reviewer exit 0. Each producer edge receives\ - \ its own tracker verdict tagged by `(reviewer_plan, producer)`.\n4. AC \"yields\ - \ a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge\": \u2705\ - \ `tracker.handle_confirmed(role.value)` is invoked at _plan_phase.py:188-192\ - \ for every plan producer AND reviewer_plan; `evaluate()` at line 194 produces\ - \ the snapshot; `_build_plan_gate_decision` (in_process.py:660-720) yields `HITLDecision(phase=\"\ - plan\")` with the canonical 4-way options on convergence, retry/abort on non-convergence.\n\ - 5. AC \"existing refine path still works\": \u2705 Refine flow at in_process.py:213-240\ - \ is structurally unchanged; `self._current_phase` is initialised to `\"refine\"\ - ` (line 202) so heartbeats during refine continue to carry the right phase string\ - \ before flipping to \"plan\" inside `_plan_phase.run_plan_phase` (line 67).\n\ - \n**TASK-2-2 \u2014 `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py:232-376):\n\ - 1. AC \"loader returns rubric bodies for all four plan-team roles\": \u2705\ - \ `_RUBRIC_LANDED_ROLES` (lines 272-286) includes architect, task_planner, risk_analyst,\ - \ reviewer_plan. The fence at line 348 no longer rejects these; line 362-375\ - \ returns `rubric_path.read_text(...)` when the markdown is on disk.\n2. AC\ - \ \"implement-team roles still raise ValueError with the 'follow-up slice 3'\ - \ hint\": \u2705 `_ROLE_RUBRIC_SLICES` (lines 254-262) maps the eight implement-team\ - \ roles to `\"slice-3\"`; the ValueError at line 356-360 interpolates `slice_hint`\ - \ into the message (\"deferred to follow-up slice-3 of issue #2717's rollout\"\ - ). Structured-error contract preserved.\n\n**TASK-2-5 \u2014 sandbox restrictions\ - \ parallel validator**: \u2705 Closed as no-op per slice-1 R2 = pass verdict\ - \ (pinned by `integration_tests/regression/test_pretooluse_hook_nested.py:212-238`).\ - \ No changes to `sandbox/egg_agent_tools/handlers/restrictions.py` in this proposal.\ - \ Coder commit message records the required close-with-note (\"no-op: hooks\ - \ resolve role correctly; structural enforcement remains hook-side\").\n\n###\ - \ File-decomposition delta (informational)\n\nThe ruff format pass expanded\ - \ the v1 diff past the 1500-line hard cap (`scripts/file-size-allowlist.yaml`),\ - \ so the coder extracted ~680 lines of plan-phase body into `orchestrator/substrate/_plan_phase.py`.\ - \ Class methods `_run_plan_phase`/`_spawn_plan_producer`/`_spawn_plan_reviewer`/`_plan_producer_output_path`/`_read_plan_reviewer_verdicts`\ - \ stay as thin delegates (in_process.py:850-921). `in_process.py` is 1093 lines,\ - \ `_plan_phase.py` is 680 lines \u2014 both under the cap. Decomposition is\ - \ invisible to AC-level verification (same public method names; same call surface).\n\ - \n### Non-blocking observations\n\n- Slice-1 contract bookkeeping: tasks task-1-1\ - \ \u2026 task-1-9 still show `status: \"pending\"` despite their commits being\ - \ linked. Not a slice-2 coder issue; operator should reconcile before declaring\ - \ the rollout complete.\n- The `synthetic_commit_for(role)` SHA prefix at _plan_phase.py:644-656\ - \ emits `ace1<3-hex>` \u2014 fine for 3 producers (collision impossible) and\ - \ obviously synthetic in logs.\n- Fail-closed reason string (\"reviewer_plan\ - \ verdict file missing / unparseable AND reviewer exit_code=\u2026\") surfaces\ - \ in the placeholder body; if a future regression test wants to pin the operator-facing\ - \ wording, the `_verdict_diagnostics` dict on the runner is the structured surface\ - \ to assert against.\n" - ack_version: 3 - version: 3 - slice_id: slice-2 -```` - -### [2026-05-19T20:27:55Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 0daf4e18-fca1-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:27:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 79a4b578-87cf-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:25:45.362708+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:01Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d12b6292-18b0-42 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:01.412210+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:06Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: cf2aa718-9e8b-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:06.899611+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:06Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: fd3eb2b0-ca3c-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:08Z] reviewer_security → coder (CONSENSUS_ACK): ACK from reviewer_security for coder - - -Re-reviewed slice-2 coder v3 — same commit SHA as v2 (d7b2a6152), no diff to re-check. Lens conclusion unchanged from v2: no security findings, the v2 improvements that addressed my v1 non-blocking observations stand (sentinel removed from concurrent path; reviewer_plan verdict JSON is now parsed and drives per-edge ACK/NACK with fail-closed defaults on missing-verdict + reviewer-spawn-failure). All eight security-lens checks (cross-file allowlist, handler-vs-validator, info-disclosure, uncommitted-artifact, credential-shim, secret leakage, OWASP top-10 cross-file, agent-supplied paths into read-only fs access) re-pass on the v3 proposal. - -### Non-blocking (carried forward from v2) -- in_process.py:98 — `_SYNTHETIC_PLAN_COMMIT = "ace1ace"` is now unreferenced (callers use `synthetic_commit_for(role.value)`); remove or keep-as-marker per reviewer_code's call. -- _plan_phase.py:266-269 — `json.loads(verdict_path.read_text(...))` has no file-size cap; hardening-only observation if the harness gains untrusted-tenant semantics later. - - -````yaml -id: 76ca4533-d0d1-4a -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - - orchestrator/substrate/__init__.py - reason: "\nRe-reviewed slice-2 coder v3 \u2014 same commit SHA as v2 (d7b2a6152),\ - \ no diff to re-check. Lens conclusion unchanged from v2: no security findings,\ - \ the v2 improvements that addressed my v1 non-blocking observations stand (sentinel\ - \ removed from concurrent path; reviewer_plan verdict JSON is now parsed and\ - \ drives per-edge ACK/NACK with fail-closed defaults on missing-verdict + reviewer-spawn-failure).\ - \ All eight security-lens checks (cross-file allowlist, handler-vs-validator,\ - \ info-disclosure, uncommitted-artifact, credential-shim, secret leakage, OWASP\ - \ top-10 cross-file, agent-supplied paths into read-only fs access) re-pass\ - \ on the v3 proposal.\n\n### Non-blocking (carried forward from v2)\n- in_process.py:98\ - \ \u2014 `_SYNTHETIC_PLAN_COMMIT = \"ace1ace\"` is now unreferenced (callers\ - \ use `synthetic_commit_for(role.value)`); remove or keep-as-marker per reviewer_code's\ - \ call.\n- _plan_phase.py:266-269 \u2014 `json.loads(verdict_path.read_text(...))`\ - \ has no file-size cap; hardening-only observation if the harness gains untrusted-tenant\ - \ semantics later.\n" - ack_version: 3 - version: 3 - slice_id: slice-2 -```` - -### [2026-05-19T20:28:08Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ad9a1736-0a77-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:14Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e55123cb-1ef8-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:14.384123+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:15Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 843a6410-adfc-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:15.498820+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:23Z] tester (CONSENSUS_PROPOSE): Proposal from tester - -slice-2 tester: plan-phase BRC E2E integration test + R2-pass restrictions no-op regression guard for #2717 slice-2. - -TASK-2-4 — integration_tests/regression/test_inprocess_plan_brc.py -Plan-phase in-process BRC end-to-end test covering all four AC bullets from contract task-2-4: -1. boots `run_pipeline_in_process` against a deterministic pipeline id with harness-faked subagents (MagicMock substrate bundle mirroring `shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py`'s fake_bundle fixture — no real Anthropic / Claude Code spawn); -2. advances past the refine HITL gate via `approve` → `approve_continue` send sequence; -3. asserts the plan stage spawns the 3 producers (architect, task_planner, risk_analyst) + 1 reviewer (reviewer_plan) — observed via the fake spawner's `.call_args_list`; -4. asserts 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, since the coder's TASK-2-1 implementation drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed` deterministically (the substrate's spawner is synchronous so spawn-completion IS the signal that the subagent proposed/reviewed); -5. asserts the plan-HITL decision is yielded with `phase="plan"`, `decision_type="phase_gate"`, non-empty `id` / `question` / `options` (the 4-way approve/request_changes/change_approach/stop set on the success path; retry/abort on the failure path). - -Adversarial probing layered on top of the AC bullets: -- 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 the operator did not approve (`test_plan_stage_does_not_run_when_operator_rejects_refine`); -- plan stage MUST NOT spawn implement-phase roles — pins the negative invariant against a misrouted `_PHASE_ROLES["implement"]` indexing bug (`test_plan_stage_does_not_spawn_implement_phase_roles`); -- refiner is spawned exactly once — pins the single-refiner-spawn invariant against an off-by-one role-iteration that would re-spawn the refiner inside the plan stage (`test_plan_stage_does_not_invoke_refiner_a_second_time`); -- every plan-phase spawn carries `EGG_PHASE=plan` in its env — pins the env-propagation contract so spawned subagents see the right phase (`test_plan_stage_carries_phase_env_var_to_producers`). - -The 7 tests share an `isolated_pipeline_state` fixture that clears the module-level `PeerConsensusTracker` registry between tests so back-to-back runs against the same pipeline_id don't inherit confirmed state. `short_intervals` shrinks heartbeat / brc-review / bus-tick intervals to 0.05s so the suite runs in ~2s. `fake_home` redirects `$HOME` to a tmp dir so the active-role sentinel writes don't pollute the developer's actual home directory. All 7 pass against coder v2 (commit d7b2a6152). - -The test is committed with graceful-skip behaviour for the case where TASK-2-1's plan-stage method is absent (the `_has_plan_stage()` helper checks for any of `_run_plan_phase` / `_run_plan` / `run_plan` / `_dispatch_plan` / `_plan_stage`); under scaffold-first guidance this kept the tester unblocked while the coder produced. With coder v2's `_run_plan_phase` landed the skip disappears and all 7 assertions run. - -TASK-2-6 — tests/sandbox/egg_agent_tools/test_restrictions_validator.py -Contingent on slice-1's R2 verdict = `pass` (recorded in the slice-1 BRC history `.egg-state/brc-history/2717-implement-slice-1.md`: the PreToolUse hook correctly resolves the child role under nested dispatch — parent=architect + child=tester writing `orchestrator/foo.py` → `decision=block` with a tester-naming reason; cross-role probe + in-role negative-control + EGG_AGENT_ROLE leak guard all 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)." This file lands the no-op regression guard: - -- 3 in-allow-list response-shape tests (coder under orchestrator/, tester under tests/sandbox/egg_agent_tools/, documenter under docs/) — each pins the documented gateway-shape fields `{ok, role, path, can_write, reason, alternative_role}` exactly via a `frozenset` equality, so a slice-2 regression that silently extended the validator's response with new fields would surface here; -- 2 cross-role denial tests (coder → tests/, tester → orchestrator/) — each pins `can_write=False`, `reason` references `shared/egg_restrictions/patterns.py`, `alternative_role` names the single producer role that can write the path (load-bearing for impasse-routing auto-delegation); -- 1 negative-symbol invariant test — asserts `validate_write_target` (and peers) are NOT present on `sandbox/egg_agent_tools/handlers/restrictions.py`, since R2 = pass meant the cq-6 option-2 enforcement work should NOT have landed in slice-2; -- 3 defensive probes — missing `path` raises HandlerError, unknown role raises HandlerError, list-shaped path returns per-path results with documented shapes. - -If a future slice flips R2 to fail (the cq-3 deferral makes that possible per the plan), this test file will need a sibling that exercises the new validator's denial shape; the contingency is documented in the test docstring per task-2-6 AC. All 9 pass against the unchanged restrictions handler (no slice-2 source edits in `sandbox/egg_agent_tools/handlers/restrictions.py`, as the no-op verdict required). - -Verification (running checks directly because `make test` / `make lint` / `make security` cannot complete in this sandbox — the venv sync fails when uv tries to download pinned wheels because the wheels.pythonhosted.org TLS chain is `UnknownIssuer` inside the image; same env constraint slice-1 tester hit): -- `ruff format --check .` — PASS (962 files already formatted) ✓ -- `ruff check .` — PASS (all checks passed) ✓ -- `mypy gateway shared sandbox --exclude 'gateway/tests/' --exclude 'shared/egg_contracts/tests/' --exclude 'shared/tests/'` — PASS (251 source files OK) ✓ -- All 13 custom checks under `scripts/check-*.py` — PASS (each exits 0; a few harmless "host-services directory not found" warnings for repo-layout drift) ✓ -- `bandit` — not installed in this sandbox (per `make security` SKIP branch). The Bandit invocation is part of the `security` configured check; cannot attest to it ✓ -- Direct pytest on tester-authored files via `PYTHONPATH=shared:gateway:orchestrator:. pytest --import-mode=importlib`: 16/16 pass in ~2.2s ✓ - -`tests_execution_blocked` reason: the literal `make test` command cannot complete because (1) `uv sync` fails on flask / oauthlib wheel downloads (UnknownIssuer TLS chain to pypi.org); (2) without the venv, `make test` skips. Direct pytest on the tester-authored files passes 16/16 — see direct-pytest output above. - -````yaml -id: f6359ba4-51b0-45 -phase: implement -metadata: - payload: - summary: "slice-2 tester: plan-phase BRC E2E integration test + R2-pass restrictions\ - \ no-op regression guard for #2717 slice-2.\n\nTASK-2-4 \u2014 integration_tests/regression/test_inprocess_plan_brc.py\n\ - Plan-phase in-process BRC end-to-end test covering all four AC bullets from\ - \ contract task-2-4:\n1. boots `run_pipeline_in_process` against a deterministic\ - \ pipeline id with harness-faked subagents (MagicMock substrate bundle mirroring\ - \ `shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py`'s fake_bundle\ - \ fixture \u2014 no real Anthropic / Claude Code spawn);\n2. advances past the\ - \ refine HITL gate via `approve` \u2192 `approve_continue` send sequence;\n\ - 3. asserts the plan stage spawns the 3 producers (architect, task_planner, risk_analyst)\ - \ + 1 reviewer (reviewer_plan) \u2014 observed via the fake spawner's `.call_args_list`;\n\ - 4. asserts BRC mechanics reach CONSENSUS_CONFIRMED on every producer edge (architect\ - \ \u2192 reviewer_plan, task_planner \u2192 reviewer_plan, risk_analyst \u2192\ - \ reviewer_plan) by reading `_plan_tracker.evaluate()` \u2014 the in-process\ - \ analogue of bus-side CONSENSUS_CONFIRMED messages, since the coder's TASK-2-1\ - \ implementation drives `PeerConsensusTracker.handle_propose/handle_ack/handle_confirmed`\ - \ deterministically (the substrate's spawner is synchronous so spawn-completion\ - \ IS the signal that the subagent proposed/reviewed);\n5. asserts the plan-HITL\ - \ decision is yielded with `phase=\"plan\"`, `decision_type=\"phase_gate\"`,\ - \ non-empty `id` / `question` / `options` (the 4-way approve/request_changes/change_approach/stop\ - \ set on the success path; retry/abort on the failure path).\n\nAdversarial\ - \ probing layered on top of the AC bullets:\n- plan stage MUST NOT run when\ - \ the operator answers `stop` at the refine gate \u2014 a regression that fanned\ - \ into plan on any non-continue answer would burn three unauthorised subagent\ - \ spawns the operator did not approve (`test_plan_stage_does_not_run_when_operator_rejects_refine`);\n\ - - plan stage MUST NOT spawn implement-phase roles \u2014 pins the negative invariant\ - \ against a misrouted `_PHASE_ROLES[\"implement\"]` indexing bug (`test_plan_stage_does_not_spawn_implement_phase_roles`);\n\ - - refiner is spawned exactly once \u2014 pins the single-refiner-spawn invariant\ - \ against an off-by-one role-iteration that would re-spawn the refiner inside\ - \ the plan stage (`test_plan_stage_does_not_invoke_refiner_a_second_time`);\n\ - - every plan-phase spawn carries `EGG_PHASE=plan` in its env \u2014 pins the\ - \ env-propagation contract so spawned subagents see the right phase (`test_plan_stage_carries_phase_env_var_to_producers`).\n\ - \nThe 7 tests share an `isolated_pipeline_state` fixture that clears the module-level\ - \ `PeerConsensusTracker` registry between tests so back-to-back runs against\ - \ the same pipeline_id don't inherit confirmed state. `short_intervals` shrinks\ - \ heartbeat / brc-review / bus-tick intervals to 0.05s so the suite runs in\ - \ ~2s. `fake_home` redirects `$HOME` to a tmp dir so the active-role sentinel\ - \ writes don't pollute the developer's actual home directory. All 7 pass against\ - \ coder v2 (commit d7b2a6152).\n\nThe test is committed with graceful-skip behaviour\ - \ for the case where TASK-2-1's plan-stage method is absent (the `_has_plan_stage()`\ - \ helper checks for any of `_run_plan_phase` / `_run_plan` / `run_plan` / `_dispatch_plan`\ - \ / `_plan_stage`); under scaffold-first guidance this kept the tester unblocked\ - \ while the coder produced. With coder v2's `_run_plan_phase` landed the skip\ - \ disappears and all 7 assertions run.\n\nTASK-2-6 \u2014 tests/sandbox/egg_agent_tools/test_restrictions_validator.py\n\ - Contingent on slice-1's R2 verdict = `pass` (recorded in the slice-1 BRC history\ - \ `.egg-state/brc-history/2717-implement-slice-1.md`: the PreToolUse hook correctly\ - \ resolves the child role under nested dispatch \u2014 parent=architect + child=tester\ - \ writing `orchestrator/foo.py` \u2192 `decision=block` with a tester-naming\ - \ reason; cross-role probe + in-role negative-control + EGG_AGENT_ROLE leak\ - \ guard all 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).\" This file lands the no-op regression guard:\n\n- 3 in-allow-list\ - \ response-shape tests (coder under orchestrator/, tester under tests/sandbox/egg_agent_tools/,\ - \ documenter under docs/) \u2014 each pins the documented gateway-shape fields\ - \ `{ok, role, path, can_write, reason, alternative_role}` exactly via a `frozenset`\ - \ equality, so a slice-2 regression that silently extended the validator's response\ - \ with new fields would surface here;\n- 2 cross-role denial tests (coder \u2192\ - \ tests/, tester \u2192 orchestrator/) \u2014 each pins `can_write=False`, `reason`\ - \ references `shared/egg_restrictions/patterns.py`, `alternative_role` names\ - \ the single producer role that can write the path (load-bearing for impasse-routing\ - \ auto-delegation);\n- 1 negative-symbol invariant test \u2014 asserts `validate_write_target`\ - \ (and peers) are NOT present on `sandbox/egg_agent_tools/handlers/restrictions.py`,\ - \ since R2 = pass meant the cq-6 option-2 enforcement work should NOT have landed\ - \ in slice-2;\n- 3 defensive probes \u2014 missing `path` raises HandlerError,\ - \ unknown role raises HandlerError, list-shaped path returns per-path results\ - \ with documented shapes.\n\nIf a future slice flips R2 to fail (the cq-3 deferral\ - \ makes that possible per the plan), this test file will need a sibling that\ - \ exercises the new validator's denial shape; the contingency is documented\ - \ in the test docstring per task-2-6 AC. All 9 pass against the unchanged restrictions\ - \ handler (no slice-2 source edits in `sandbox/egg_agent_tools/handlers/restrictions.py`,\ - \ as the no-op verdict required).\n\nVerification (running checks directly because\ - \ `make test` / `make lint` / `make security` cannot complete in this sandbox\ - \ \u2014 the venv sync fails when uv tries to download pinned wheels because\ - \ the wheels.pythonhosted.org TLS chain is `UnknownIssuer` inside the image;\ - \ same env constraint slice-1 tester hit):\n- `ruff format --check .` \u2014\ - \ PASS (962 files already formatted) \u2713\n- `ruff check .` \u2014 PASS (all\ - \ checks passed) \u2713\n- `mypy gateway shared sandbox --exclude 'gateway/tests/'\ - \ --exclude 'shared/egg_contracts/tests/' --exclude 'shared/tests/'` \u2014\ - \ PASS (251 source files OK) \u2713\n- All 13 custom checks under `scripts/check-*.py`\ - \ \u2014 PASS (each exits 0; a few harmless \"host-services directory not found\"\ - \ warnings for repo-layout drift) \u2713\n- `bandit` \u2014 not installed in\ - \ this sandbox (per `make security` SKIP branch). The Bandit invocation is part\ - \ of the `security` configured check; cannot attest to it \u2713\n- Direct pytest\ - \ on tester-authored files via `PYTHONPATH=shared:gateway:orchestrator:. pytest\ - \ --import-mode=importlib`: 16/16 pass in ~2.2s \u2713\n\n`tests_execution_blocked`\ - \ reason: the literal `make test` command cannot complete because (1) `uv sync`\ - \ fails on flask / oauthlib wheel downloads (UnknownIssuer TLS chain to pypi.org);\ - \ (2) without the venv, `make test` skips. Direct pytest on the tester-authored\ - \ files passes 16/16 \u2014 see direct-pytest output above." - attestation: - tests_execution_blocked: true - tests_execution_blocked_reason: "`make test` cannot complete in this sandbox:\ - \ (1) uv venv sync fails when downloading pinned wheels (e.g. flask==3.1.2,\ - \ oauthlib==3.3.1) because the wheels.pythonhosted.org TLS chain is `UnknownIssuer`\ - \ inside the sandbox image \u2014 same network constraint the slice-1 tester\ - \ hit. Direct pytest invocation via `PYTHONPATH=shared:gateway:orchestrator:.\ - \ pytest integration_tests/regression/test_inprocess_plan_brc.py tests/sandbox/egg_agent_tools/test_restrictions_validator.py\ - \ -v --rootdir=. --import-mode=importlib` passes 16/16 tester-authored tests\ - \ in ~2.2s against coder v2 commit d7b2a6152. Direct ruff check + ruff format\ - \ --check across the full repo pass; mypy on gateway/shared/sandbox passes;\ - \ all 13 custom checks pass; bandit is not installed in this sandbox (SKIP)." - checks_passed: - - lint - artifacts: - - integration_tests/regression/test_inprocess_plan_brc.py - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py - risk_considered: "Three risk vectors considered:\n\n(1) Tester role boundary \u2014\ - \ both test files live under tester-allowed prefixes (`integration_tests/regression/`,\ - \ `tests/sandbox/egg_agent_tools/`); no source-code edits. `mcp__sdlc__check_file_restriction`\ - \ for both paths returned `can_write=True` for tester role.\n\n(2) Test reliability\ - \ / flake \u2014 short heartbeat intervals (0.05s) are an order of magnitude\ - \ faster than the production 5s but no `time.sleep` polling in the test body;\ - \ the test relies on synchronous `gen.send(...)` flow which is deterministic.\ - \ Tests run in ~2s under 100% reliable conditions (no real network, no real\ - \ Claude Code spawn, no real subagent harness). `isolated_pipeline_state` fixture\ - \ clears module-level tracker registry between tests so cross-test state leakage\ - \ is impossible.\n\n(3) Coder-side architectural drift between v1 and v2 \u2014\ - \ coder v2 introduced architect-first-then-fanout ordering (architect synchronous,\ - \ task_planner + risk_analyst concurrent via 2-worker pool) plus verdict-JSON\ - \ ACK/NACK parsing replacing v1's exit-code heuristic. My tests assert on the\ - \ *set* of spawned roles (not ordering), the *set* of confirmed-in-tracker roles\ - \ (not propose timestamps), and the HITL gate's *shape* (not exact wording).\ - \ All 7 plan-BRC tests + 9 restrictions tests pass unchanged against v2 \u2014\ - \ the abstraction level is right." - commit_sha: b2be479f1 - files_changed: - - integration_tests/regression/test_inprocess_plan_brc.py - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py - tests_run: - - integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_spawns_three_producers_and_one_reviewer - - integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_yields_hitl_decision_with_expected_fields - - integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_reaches_consensus_confirmed_for_each_producer - - integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_does_not_run_when_operator_rejects_refine - - integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_does_not_spawn_implement_phase_roles - - integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_does_not_invoke_refiner_a_second_time - - integration_tests/regression/test_inprocess_plan_brc.py::test_plan_stage_carries_phase_env_var_to_producers - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_coder_in_allow_list_response_shape_stable - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_tester_in_allow_list_response_shape_stable - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_documenter_in_allow_list_response_shape_stable - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_coder_cannot_write_tester_path_denial_shape_stable - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_tester_cannot_write_orchestrator_path_denial_shape_stable - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_no_new_validator_symbol_introduced_in_r2_pass_slice - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_missing_path_raises_handler_error - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_unknown_role_raises_handler_error - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py::test_list_path_returns_per_path_results - tasks_satisfied: - - task-2-4 - - task-2-6 - version: 1 - commit_sha: b2be479f1 - slice_id: slice-2 -```` - -### [2026-05-19T20:28:23Z] orchestrator → documenter (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: 9155380a-8398-46 -phase: implement -metadata: - ready_to_confirm: true - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:28:23Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 5ee66c6b-b2a9-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:23Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: adb01f2b-50b6-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:45Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b29eef4b-eaf4-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:33.563272+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:45Z] documenter (CONSENSUS_CONFIRMED): Confirmed by documenter - -````yaml -id: f64fdea5-b5f8-48 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-2 -```` - -### [2026-05-19T20:28:45Z] reviewer_concurrency → coder (CONSENSUS_ACK): ACK from reviewer_concurrency for coder - - -Concurrency-lens re-review of coder v3 (commit d7b2a6152). I verified both v1 concurrency blockers are addressed and walked the new architect-first-then-fanout topology for new races. Re-checked tracker access ordering (still single-threaded on main thread inside `_run_plan_phase_inner`), bus / heartbeat / BRC-review background-thread interaction with the v2 phase flip, the verdict-JSON parsing path, and the per-role synthetic SHA. No remaining blocking concurrency findings. - -### Blocker resolution - -1. **C1 (sentinel race) — resolved.** `spawn_plan_producer` (`orchestrator/substrate/_plan_phase.py:438-488`) no longer writes the `$HOME/.claude/egg-active-role.json` sentinel — confirmed by the new docstring at lines 446-453 and by the absence of any `_write_active_role_sentinel` call inside the concurrent path. Per-spawn `EGG_AGENT_ROLE=role.value` is the load-bearing channel (line 461); the slice-1 R2 verdict (env propagates correctly under nested Agent-tool dispatch) justifies dropping the sentinel from the fan-out path, and the reviewer's solo spawn (`spawn_plan_reviewer`, line 537) still writes the sentinel since it never overlaps another spawn. - -2. **C2 (heartbeat phase) — resolved.** `_publish_heartbeat` (`orchestrator/substrate/in_process.py:381-414`) now reads `self._current_phase` (line 412) instead of hard-coding the string. The phase is initialised to `"refine"` at `__init__` (line 202) and flipped to `"plan"` at the top of `run_plan_phase` (`_plan_phase.py:67`). Any future stuck-phase-transition watchdog filtering heartbeats by `phase` will now see plan-phase liveness during the in-process plan stage. - -### Concurrency walk of the new topology - -The v2/v3 redesign sequences architect synchronously first, then fans out task_planner + risk_analyst via `ThreadPoolExecutor(max_workers=2)`. I re-walked the concurrent leg: - -- **`spawn_plan_producer` (concurrent path)** — `bundle.worktrees.create(pipeline_id, role)` produces a per-role directory (`///`), so the two fan-out threads target disjoint paths; the worktree manager's `_lock` (`orchestrator/substrate/claude_code/worktree.py:82`) protects the in-memory `_tracked` dict. Each thread builds its own `spawn_env` dict (no shared mutable state), reads `runner.env` (a dict — concurrent dict reads are CPython-safe), and calls `bundle.spawner.spawn(...)` which fans the subprocess work out per-thread. No shared mutable state visible to me in this path. -- **`_ensure_state_dirs` and `plan_producer_output_path`** — both use `Path.mkdir(parents=True, exist_ok=True)` which is idempotent under concurrent invocation; no race. -- **Tracker access** — `_record_producer_propose`, `_apply_reviewer_verdicts`, the `tracker.handle_confirmed` loop, and `tracker.evaluate()` are all called from the main thread inside the `for fut in as_completed_fn(future_map)` body or after the executor's `with` block exits. `PeerConsensusTracker` is also self-RLock-protected (`orchestrator/peer_consensus.py:101` `threading.RLock()`), so the BRC re-review background thread's `re_review_tick` calls can interleave safely with the main thread's `handle_*` calls. -- **Architect-first sequencing** — `bundle.spawner.spawn(...)` returns AFTER the subagent finishes (synchronous), so the architect's output JSON at `architect_output_path` is on disk before the fan-out threads start and is safe to read from the two downstream producers' subagent prompts. -- **`get_peer_consensus_tracker` → `create_peer_consensus_tracker` check-then-act (line 113-115)** — still not race-y today (background `_brc_review_loop` only calls `get_*`, never `create_*`), so my v1 non-blocking note still stands as a forward-looking caveat rather than an actionable finding. - -### Verified non-blockers from v1 - -- ✅ Per-role synthetic SHA via `synthetic_commit_for(role_name)` (`_plan_phase.py:644-656`) — sha1-hashed per role with the `ace1` prefix so each `ProposalPayload.commit_sha` remains distinguishable in the tracker even when the harness fake stubs the commit. Addresses my v1 NB #2. -- ✅ Tracker-guard rejections route through `log_tracker_warning(...)` (`_plan_phase.py:659-680`) instead of `except Exception: pass`. Addresses my v1 NB on silent guard rejections. - -### Non-blocking (residual) - -- **Stale sentinel during plan-phase producer spawns.** Once `_spawn_refiner` writes `$HOME/.claude/egg-active-role.json` with `role="refiner"` and the generator advances to `run_plan_phase`, the sentinel keeps the stale `"refiner"` value through the architect spawn and the task_planner / risk_analyst fan-out. If env propagation ever fails (the R2 verdict says it does not, so this is defence-in-depth), nested children would fall back to refiner's allow-list rather than the producer's. Not a race anymore — just stale. Cheap mitigation: have the synchronous architect spawn refresh the sentinel to `"architect"` before its `bundle.spawner.spawn(...)` call (single-writer at that point); the fan-out path stays sentinel-free as designed. Defer if R2 stays green. -- **`Worktree.create` still swallows `git worktree add` failures via `subprocess.run(check=False, timeout=30)`** (`orchestrator/substrate/claude_code/worktree.py:117-127`). My v1 NB #1 stands — concurrent `git worktree add` calls from the two fan-out threads can lose the parent repo's lock race; `target.mkdir(parents=True, exist_ok=True)` runs first so the spawner still gets a path. With v3's per-role synthetic SHA the downstream impact is smaller (each producer's ProposalPayload remains distinguishable even when `git rev-parse HEAD` falls back), but the silent-failure pattern is still load-bearing on the spawner being able to live in a non-git directory. - - -````yaml -id: a31f7499-10bd-4e -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - - orchestrator/substrate/claude_code/worktree.py - - orchestrator/substrate/claude_code/hook_entry.py - - orchestrator/peer_consensus.py - reason: "\nConcurrency-lens re-review of coder v3 (commit d7b2a6152). I verified\ - \ both v1 concurrency blockers are addressed and walked the new architect-first-then-fanout\ - \ topology for new races. Re-checked tracker access ordering (still single-threaded\ - \ on main thread inside `_run_plan_phase_inner`), bus / heartbeat / BRC-review\ - \ background-thread interaction with the v2 phase flip, the verdict-JSON parsing\ - \ path, and the per-role synthetic SHA. No remaining blocking concurrency findings.\n\ - \n### Blocker resolution\n\n1. **C1 (sentinel race) \u2014 resolved.** `spawn_plan_producer`\ - \ (`orchestrator/substrate/_plan_phase.py:438-488`) no longer writes the `$HOME/.claude/egg-active-role.json`\ - \ sentinel \u2014 confirmed by the new docstring at lines 446-453 and by the\ - \ absence of any `_write_active_role_sentinel` call inside the concurrent path.\ - \ Per-spawn `EGG_AGENT_ROLE=role.value` is the load-bearing channel (line 461);\ - \ the slice-1 R2 verdict (env propagates correctly under nested Agent-tool dispatch)\ - \ justifies dropping the sentinel from the fan-out path, and the reviewer's\ - \ solo spawn (`spawn_plan_reviewer`, line 537) still writes the sentinel since\ - \ it never overlaps another spawn.\n\n2. **C2 (heartbeat phase) \u2014 resolved.**\ - \ `_publish_heartbeat` (`orchestrator/substrate/in_process.py:381-414`) now\ - \ reads `self._current_phase` (line 412) instead of hard-coding the string.\ - \ The phase is initialised to `\"refine\"` at `__init__` (line 202) and flipped\ - \ to `\"plan\"` at the top of `run_plan_phase` (`_plan_phase.py:67`). Any future\ - \ stuck-phase-transition watchdog filtering heartbeats by `phase` will now see\ - \ plan-phase liveness during the in-process plan stage.\n\n### Concurrency walk\ - \ of the new topology\n\nThe v2/v3 redesign sequences architect synchronously\ - \ first, then fans out task_planner + risk_analyst via `ThreadPoolExecutor(max_workers=2)`.\ - \ I re-walked the concurrent leg:\n\n- **`spawn_plan_producer` (concurrent path)**\ - \ \u2014 `bundle.worktrees.create(pipeline_id, role)` produces a per-role directory\ - \ (`///`), so the two fan-out threads target disjoint\ - \ paths; the worktree manager's `_lock` (`orchestrator/substrate/claude_code/worktree.py:82`)\ - \ protects the in-memory `_tracked` dict. Each thread builds its own `spawn_env`\ - \ dict (no shared mutable state), reads `runner.env` (a dict \u2014 concurrent\ - \ dict reads are CPython-safe), and calls `bundle.spawner.spawn(...)` which\ - \ fans the subprocess work out per-thread. No shared mutable state visible to\ - \ me in this path.\n- **`_ensure_state_dirs` and `plan_producer_output_path`**\ - \ \u2014 both use `Path.mkdir(parents=True, exist_ok=True)` which is idempotent\ - \ under concurrent invocation; no race.\n- **Tracker access** \u2014 `_record_producer_propose`,\ - \ `_apply_reviewer_verdicts`, the `tracker.handle_confirmed` loop, and `tracker.evaluate()`\ - \ are all called from the main thread inside the `for fut in as_completed_fn(future_map)`\ - \ body or after the executor's `with` block exits. `PeerConsensusTracker` is\ - \ also self-RLock-protected (`orchestrator/peer_consensus.py:101` `threading.RLock()`),\ - \ so the BRC re-review background thread's `re_review_tick` calls can interleave\ - \ safely with the main thread's `handle_*` calls.\n- **Architect-first sequencing**\ - \ \u2014 `bundle.spawner.spawn(...)` returns AFTER the subagent finishes (synchronous),\ - \ so the architect's output JSON at `architect_output_path` is on disk before\ - \ the fan-out threads start and is safe to read from the two downstream producers'\ - \ subagent prompts.\n- **`get_peer_consensus_tracker` \u2192 `create_peer_consensus_tracker`\ - \ check-then-act (line 113-115)** \u2014 still not race-y today (background\ - \ `_brc_review_loop` only calls `get_*`, never `create_*`), so my v1 non-blocking\ - \ note still stands as a forward-looking caveat rather than an actionable finding.\n\ - \n### Verified non-blockers from v1\n\n- \u2705 Per-role synthetic SHA via `synthetic_commit_for(role_name)`\ - \ (`_plan_phase.py:644-656`) \u2014 sha1-hashed per role with the `ace1` prefix\ - \ so each `ProposalPayload.commit_sha` remains distinguishable in the tracker\ - \ even when the harness fake stubs the commit. Addresses my v1 NB #2.\n- \u2705\ - \ Tracker-guard rejections route through `log_tracker_warning(...)` (`_plan_phase.py:659-680`)\ - \ instead of `except Exception: pass`. Addresses my v1 NB on silent guard rejections.\n\ - \n### Non-blocking (residual)\n\n- **Stale sentinel during plan-phase producer\ - \ spawns.** Once `_spawn_refiner` writes `$HOME/.claude/egg-active-role.json`\ - \ with `role=\"refiner\"` and the generator advances to `run_plan_phase`, the\ - \ sentinel keeps the stale `\"refiner\"` value through the architect spawn and\ - \ the task_planner / risk_analyst fan-out. If env propagation ever fails (the\ - \ R2 verdict says it does not, so this is defence-in-depth), nested children\ - \ would fall back to refiner's allow-list rather than the producer's. Not a\ - \ race anymore \u2014 just stale. Cheap mitigation: have the synchronous architect\ - \ spawn refresh the sentinel to `\"architect\"` before its `bundle.spawner.spawn(...)`\ - \ call (single-writer at that point); the fan-out path stays sentinel-free as\ - \ designed. Defer if R2 stays green.\n- **`Worktree.create` still swallows `git\ - \ worktree add` failures via `subprocess.run(check=False, timeout=30)`** (`orchestrator/substrate/claude_code/worktree.py:117-127`).\ - \ My v1 NB #1 stands \u2014 concurrent `git worktree add` calls from the two\ - \ fan-out threads can lose the parent repo's lock race; `target.mkdir(parents=True,\ - \ exist_ok=True)` runs first so the spawner still gets a path. With v3's per-role\ - \ synthetic SHA the downstream impact is smaller (each producer's ProposalPayload\ - \ remains distinguishable even when `git rev-parse HEAD` falls back), but the\ - \ silent-failure pattern is still load-bearing on the spawner being able to\ - \ live in a non-git directory.\n" - ack_version: 3 - version: 3 - slice_id: slice-2 -```` - -### [2026-05-19T20:28:45Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 5d7bb48b-09c2-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:50Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 4577f124-2f6a-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:50.817666+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:50Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 9d275321-48e0-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:51Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5d6e48f9-99d0-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:51.460648+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:53Z] reviewer_code → coder (CONSENSUS_ACK): ACK from reviewer_code for coder - - -Re-ACK at v3 (same commit d7b2a6152 as v2). Reviewed the coder's submission for TASK-2-1 / TASK-2-2 / TASK-2-5(no-op). - -### What changed v1 → current - -**H1 — architect-first then fanout (`_plan_phase.py:104-161`).** Plan stage now spawns `architect` synchronously first, records its `CONSENSUS_PROPOSE`, then fans out `task_planner` + `risk_analyst` via `ThreadPoolExecutor(max_workers=2)`. Architect's per-role output path is threaded into downstream producers via `EGG_ARCHITECT_OUTPUT_PATH` and the prompt's "Architect handoff input: …". Matches the role-dependency declarations at `shared/egg_contracts/agent_roles.py:398/422` and the rubric language. - -**H2 — verdict-JSON parsing (`_plan_phase.py:251-371`).** `read_plan_reviewer_verdicts(runner)` parses `.egg-state/agent-outputs/-reviewer_plan-output.json` for a `per_producer` map. `_apply_reviewer_verdicts` drives per-edge ACK / NACK on the tracker. Semantics: present+ACK → ACK; present+NACK → NACK; absent + reviewer exit 0 → optimistic ACK with diagnostic; absent + reviewer exit non-zero → **fail-closed NACK**. Closes my v1 silent-NACK-loss concern. - -**C1 — sentinel removed from concurrent path (`_plan_phase.py:438-488`).** `spawn_plan_producer` no longer writes the sentinel. Per-spawn `EGG_AGENT_ROLE` is the primary channel. `spawn_plan_reviewer` retains the write (solo dispatch). The transition window where the sentinel says "refiner" during plan-producer spawns is acceptable: refiner + the three plan producers share the `.egg-state/{drafts,agent-outputs}/` allow-list. - -**C2 — phase HEARTBEAT (`in_process.py:198-203, 380-412`).** New `self._current_phase` field, flipped to "plan" at the top of `run_plan_phase`. `_publish_heartbeat` reads it. - -**T1 — ruff format applied;** `EGG_PRODUCER_ARTIFACT_PATHS` dropped in favor of per-role `EGG__OUTPUT_PATH` vars for the reviewer. - -**Per-role synthetic SHA (`_plan_phase.py:644-656`).** `synthetic_commit_for(role_name)` returns `f"ace1{sha1(role_name)[:3]}"` — three concurrent producers now have distinguishable `commit_sha` values. The `ace1` prefix keeps the value obviously synthetic. - -**Tracker-guard warning logging (`_plan_phase.py:659-680`).** Bare excepts replaced with `logger.warning(...)` carrying verb + role + pipeline_id + exception. - -**Module decomposition.** Plan-phase body extracted to `orchestrator/substrate/_plan_phase.py` (680 lines); class methods on `_InProcessOrchestrator` stay as thin delegates so public surface and the tester's v1 test method-names are preserved. - -### File-by-file analysis - -**orchestrator/substrate/_plan_phase.py** (new, 680 lines) — Single linear flow: `run_plan_phase` (lifts the phase string) → `_run_plan_phase_inner` (architect-first, fanout, reviewer, verdict-parse, confirm). Module-level functions accept lazily-imported primitives via keyword args (`bundle_factory`, `executor_factory`, `as_completed_fn`, etc.) so tests can inject deterministic substitutes. Spawn helpers build per-role env vars (`EGG_PRODUCER_OUTPUT_PATH`, `EGG_REVIEWER_VERDICT_PATH`, optional `EGG_ARCHITECT_OUTPUT_PATH`, per-role `EGG__OUTPUT_PATH` for the reviewer). Tracker-guard wrappers log on failure rather than swallowing. `format_plan_placeholder` renders per-producer + reviewer diagnostics + verdict-parsing status + BRC eval snapshot. - -**orchestrator/substrate/in_process.py** — Delegates plan-phase methods to `_plan_phase`. `_current_phase` field added at `__init__` and read in `_publish_heartbeat`. `_SYNTHETIC_PLAN_COMMIT` constant retained for refiner/fallback callers with a beefed-up docstring; plan-phase code uses per-role `synthetic_commit_for` instead. - -**orchestrator/substrate/__init__.py** — TASK-2-2 loader expansion: `_RUBRIC_LANDED_ROLES` now includes `architect` / `task_planner` / `risk_analyst` / `reviewer_plan`. The "missing on disk" diagnostic mentions both TASK-1-4 (slice-1) and TASK-2-3 (slice-2) so operators hit by the error get a slice-specific pointer. Implement-team roles still raise `ValueError` with a slice-3 pointer via `_ROLE_RUBRIC_SLICES`. - -### Non-blocking - -- **`_plan_phase.py:412-435 (_record_reviewer_nack)`** — A NACK with `reason=""` in the verdict JSON hits `ReviewPayload.validate_nack_has_reason` (`attestation_schemas.py:241-243`) and raises `ValueError`. The defensive `except Exception` catches via `log_tracker_warning` but the tracker doesn't record the NACK — the producer ends up "not ACKed, not NACKed, just unconfirmed", surfaced only as `blocking_agents` at the HITL gate. Adding a defensive default reason (e.g. "reviewer NACKed without a stated reason") in `_apply_reviewer_verdicts` would convert this corner case into a recorded NACK with a sentinel reason. Worth a small follow-up. - -- **`_plan_phase.py:67 + in_process.py:202`** — `_current_phase` is set forward-only. A future "back to refine" path would need an explicit reset. Add a comment noting "set-once monotonic" for now. - -- **`_plan_phase.py:113-115`** — The "reuse existing tracker" branch (`tracker = get_tracker(...); if tracker is None: ...`) is dead code today. A future slice that registers a tracker with the refine graph would have this branch reuse the wrong graph. Either guard with a graph-match check or always-create. - -- **`_plan_phase.py:644-656 (synthetic_commit_for)`** — 4096-value space (3 hex chars after `ace1`). For four roles collision is negligible, but if the rubric set grows consider lifting to `[:6]` (24-bit space). - -- **`in_process.py:91-98 (_SYNTHETIC_PLAN_COMMIT)`** — Constant retained for "refiner / fallback callers and as a structural marker" per the new docstring. The plan-phase code no longer uses it. Worth grepping for external callers in a follow-up; if none, the constant can be inlined or removed. - -No security, correctness, or robustness issues introduced by this revision. All my v1 blocking-class observations (architect ordering, silent verdict-NACK loss, sentinel race, silent tracker except, synthetic commit collision) are addressed. Coder ACKed at v3. - - -````yaml -id: 6b97c2aa-e845-48 -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/__init__.py - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - reason: "\nRe-ACK at v3 (same commit d7b2a6152 as v2). Reviewed the coder's submission\ - \ for TASK-2-1 / TASK-2-2 / TASK-2-5(no-op).\n\n### What changed v1 \u2192 current\n\ - \n**H1 \u2014 architect-first then fanout (`_plan_phase.py:104-161`).** Plan\ - \ stage now spawns `architect` synchronously first, records its `CONSENSUS_PROPOSE`,\ - \ then fans out `task_planner` + `risk_analyst` via `ThreadPoolExecutor(max_workers=2)`.\ - \ Architect's per-role output path is threaded into downstream producers via\ - \ `EGG_ARCHITECT_OUTPUT_PATH` and the prompt's \"Architect handoff input: \u2026\ - \". Matches the role-dependency declarations at `shared/egg_contracts/agent_roles.py:398/422`\ - \ and the rubric language.\n\n**H2 \u2014 verdict-JSON parsing (`_plan_phase.py:251-371`).**\ - \ `read_plan_reviewer_verdicts(runner)` parses `.egg-state/agent-outputs/-reviewer_plan-output.json`\ - \ for a `per_producer` map. `_apply_reviewer_verdicts` drives per-edge ACK /\ - \ NACK on the tracker. Semantics: present+ACK \u2192 ACK; present+NACK \u2192\ - \ NACK; absent + reviewer exit 0 \u2192 optimistic ACK with diagnostic; absent\ - \ + reviewer exit non-zero \u2192 **fail-closed NACK**. Closes my v1 silent-NACK-loss\ - \ concern.\n\n**C1 \u2014 sentinel removed from concurrent path (`_plan_phase.py:438-488`).**\ - \ `spawn_plan_producer` no longer writes the sentinel. Per-spawn `EGG_AGENT_ROLE`\ - \ is the primary channel. `spawn_plan_reviewer` retains the write (solo dispatch).\ - \ The transition window where the sentinel says \"refiner\" during plan-producer\ - \ spawns is acceptable: refiner + the three plan producers share the `.egg-state/{drafts,agent-outputs}/`\ - \ allow-list.\n\n**C2 \u2014 phase HEARTBEAT (`in_process.py:198-203, 380-412`).**\ - \ New `self._current_phase` field, flipped to \"plan\" at the top of `run_plan_phase`.\ - \ `_publish_heartbeat` reads it.\n\n**T1 \u2014 ruff format applied;** `EGG_PRODUCER_ARTIFACT_PATHS`\ - \ dropped in favor of per-role `EGG__OUTPUT_PATH` vars for the reviewer.\n\ - \n**Per-role synthetic SHA (`_plan_phase.py:644-656`).** `synthetic_commit_for(role_name)`\ - \ returns `f\"ace1{sha1(role_name)[:3]}\"` \u2014 three concurrent producers\ - \ now have distinguishable `commit_sha` values. The `ace1` prefix keeps the\ - \ value obviously synthetic.\n\n**Tracker-guard warning logging (`_plan_phase.py:659-680`).**\ - \ Bare excepts replaced with `logger.warning(...)` carrying verb + role + pipeline_id\ - \ + exception.\n\n**Module decomposition.** Plan-phase body extracted to `orchestrator/substrate/_plan_phase.py`\ - \ (680 lines); class methods on `_InProcessOrchestrator` stay as thin delegates\ - \ so public surface and the tester's v1 test method-names are preserved.\n\n\ - ### File-by-file analysis\n\n**orchestrator/substrate/_plan_phase.py** (new,\ - \ 680 lines) \u2014 Single linear flow: `run_plan_phase` (lifts the phase string)\ - \ \u2192 `_run_plan_phase_inner` (architect-first, fanout, reviewer, verdict-parse,\ - \ confirm). Module-level functions accept lazily-imported primitives via keyword\ - \ args (`bundle_factory`, `executor_factory`, `as_completed_fn`, etc.) so tests\ - \ can inject deterministic substitutes. Spawn helpers build per-role env vars\ - \ (`EGG_PRODUCER_OUTPUT_PATH`, `EGG_REVIEWER_VERDICT_PATH`, optional `EGG_ARCHITECT_OUTPUT_PATH`,\ - \ per-role `EGG__OUTPUT_PATH` for the reviewer). Tracker-guard wrappers\ - \ log on failure rather than swallowing. `format_plan_placeholder` renders per-producer\ - \ + reviewer diagnostics + verdict-parsing status + BRC eval snapshot.\n\n**orchestrator/substrate/in_process.py**\ - \ \u2014 Delegates plan-phase methods to `_plan_phase`. `_current_phase` field\ - \ added at `__init__` and read in `_publish_heartbeat`. `_SYNTHETIC_PLAN_COMMIT`\ - \ constant retained for refiner/fallback callers with a beefed-up docstring;\ - \ plan-phase code uses per-role `synthetic_commit_for` instead.\n\n**orchestrator/substrate/__init__.py**\ - \ \u2014 TASK-2-2 loader expansion: `_RUBRIC_LANDED_ROLES` now includes `architect`\ - \ / `task_planner` / `risk_analyst` / `reviewer_plan`. The \"missing on disk\"\ - \ diagnostic mentions both TASK-1-4 (slice-1) and TASK-2-3 (slice-2) so operators\ - \ hit by the error get a slice-specific pointer. Implement-team roles still\ - \ raise `ValueError` with a slice-3 pointer via `_ROLE_RUBRIC_SLICES`.\n\n###\ - \ Non-blocking\n\n- **`_plan_phase.py:412-435 (_record_reviewer_nack)`** \u2014\ - \ A NACK with `reason=\"\"` in the verdict JSON hits `ReviewPayload.validate_nack_has_reason`\ - \ (`attestation_schemas.py:241-243`) and raises `ValueError`. The defensive\ - \ `except Exception` catches via `log_tracker_warning` but the tracker doesn't\ - \ record the NACK \u2014 the producer ends up \"not ACKed, not NACKed, just\ - \ unconfirmed\", surfaced only as `blocking_agents` at the HITL gate. Adding\ - \ a defensive default reason (e.g. \"reviewer NACKed without a stated reason\"\ - ) in `_apply_reviewer_verdicts` would convert this corner case into a recorded\ - \ NACK with a sentinel reason. Worth a small follow-up.\n\n- **`_plan_phase.py:67\ - \ + in_process.py:202`** \u2014 `_current_phase` is set forward-only. A future\ - \ \"back to refine\" path would need an explicit reset. Add a comment noting\ - \ \"set-once monotonic\" for now.\n\n- **`_plan_phase.py:113-115`** \u2014 The\ - \ \"reuse existing tracker\" branch (`tracker = get_tracker(...); if tracker\ - \ is None: ...`) is dead code today. A future slice that registers a tracker\ - \ with the refine graph would have this branch reuse the wrong graph. Either\ - \ guard with a graph-match check or always-create.\n\n- **`_plan_phase.py:644-656\ - \ (synthetic_commit_for)`** \u2014 4096-value space (3 hex chars after `ace1`).\ - \ For four roles collision is negligible, but if the rubric set grows consider\ - \ lifting to `[:6]` (24-bit space).\n\n- **`in_process.py:91-98 (_SYNTHETIC_PLAN_COMMIT)`**\ - \ \u2014 Constant retained for \"refiner / fallback callers and as a structural\ - \ marker\" per the new docstring. The plan-phase code no longer uses it. Worth\ - \ grepping for external callers in a follow-up; if none, the constant can be\ - \ inlined or removed.\n\nNo security, correctness, or robustness issues introduced\ - \ by this revision. All my v1 blocking-class observations (architect ordering,\ - \ silent verdict-NACK loss, sentinel race, silent tracker except, synthetic\ - \ commit collision) are addressed. Coder ACKed at v3.\n" - ack_version: 3 - version: 3 - slice_id: slice-2 -```` - -### [2026-05-19T20:28:53Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a8cc7a23-11eb-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:54Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 94fd2bff-1722-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:54.903874+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:55Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d3cfdccf-ef58-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:57Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 68a22d6f-aab7-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:57.622075+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:28:57Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 7d7f45d4-c705-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:28:58Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ef791558-5209-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:58.727732+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:29:45Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 0001609e-2b5d-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:33.563272+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:29:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 7c9cadd4-bd96-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:29:58.487845+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:29:58Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 27531476-30d0-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:28:58.727732+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:04Z] reviewer_security → tester (CONSENSUS_ACK): ACK from reviewer_security for tester - - -Reviewed the slice-2 tester proposal at commit b2be479f1 through the security lens. Read both new test files end-to-end. No security findings; the diff is in fact security-positive (pins several defensive invariants the security lens cares about). - -### Lens checks - -1. **Cross-file allowlist mismatch (§1):** N/A — the tests *verify* the allowlist semantics rather than introducing new ones. `test_coder_in_allow_list_response_shape_stable` / `test_tester_in_allow_list_response_shape_stable` / `test_documenter_in_allow_list_response_shape_stable` pin that the documented per-role file-write patterns continue to hold; the cross-role denial cases (`test_coder_cannot_write_tester_path_denial_shape_stable`, `test_tester_cannot_write_orchestrator_path_denial_shape_stable`) pin that the denial reason references `shared/egg_restrictions/patterns.py` and that `alternative_role` names the correct producer. Net positive for the lens — these are regression guards on the allowlist surface. - -2. **Handler-vs-validator path mismatch (§2):** N/A; tests do not introduce new public endpoints. - -3. **Information-disclosure / authz bypass (§3):** No real credentials, tokens, or identity material is embedded in the test fixtures. `_make_fake_bundle` returns `MagicMock(... commit_sha="0" * 40, stdout="ok", ...)` — clearly synthetic. The `fake_home` fixture (test_inprocess_plan_brc.py:127-137) explicitly relocates `$HOME` under `tmp_path / "home"` so the generator's `_write_active_role_sentinel` write does NOT pollute the developer's actual home directory — this is the right hygiene posture for a test that exercises sentinel I/O. - -4. **Uncommitted-artifact / symlink mismatch (§4):** Both new files are committed at b2be479f1 with non-zero size; no Dockerfile / packaging-manifest / symlink references. - -5. **Credential-shim modifications (§5):** N/A — no changes under `sandbox/scripts/`. - -6. **Secret leakage (§6):** Test paths and fixtures are all synthetic / orchestrator-derived; no environment dumps, no logs of actual credentials, no real network endpoints. - -7. **Cross-file OWASP top-10 (§7):** N/A. - -8. **Agent-supplied paths into read-only fs access (§8):** Tests use `pytest`'s `tmp_path` for all real filesystem I/O. The `sys.path.insert(0, str(ROOT / "sandbox"))` at test_restrictions_validator.py:78-79 derives `ROOT` from `Path(__file__).resolve().parents[3]` — anchored to the test file's own location, not agent-supplied. Hardcoded paths passed to `restrictions.check_file_restriction({"path": "orchestrator/foo.py"})` are evaluated against the regex pattern registry, never opened on disk. No new fs-read surface. - -### Security-positive defensive invariants this diff pins - -The following tests are themselves the *kind of regression guards* the security lens wants to see: - -- **integration_tests/regression/test_inprocess_plan_brc.py:624-675** — `test_plan_stage_does_not_spawn_implement_phase_roles` pins the negative invariant that the plan stage cannot accidentally invoke `coder` / `tester` / `documenter` / `reviewer_*` from the implement team. A phase-dispatch lookup that mis-indexed `_PHASE_ROLES["plan"]` (e.g. off-by-one onto `"implement"`) would burn six concurrent implement-team spawns the operator never approved — exactly the kind of HITL-bypass shape the security lens cares about. Pinning it as a regression test is the right shape. -- **integration_tests/regression/test_inprocess_plan_brc.py:557-615** — `test_plan_stage_does_not_run_when_operator_rejects_refine` pins the HITL-gate invariant: a `stop` answer at the refine gate MUST NOT advance into the plan stage's three concurrent spawns. Same shape as above — regression here would be a HITL-bypass. -- **integration_tests/regression/test_inprocess_plan_brc.py:735-797** — `test_plan_stage_carries_phase_env_var_to_producers` pins that `EGG_PHASE=plan` reaches every plan-phase spawn. The PreToolUse hook reads `EGG_AGENT_ROLE` for allow-list enforcement; a missing/wrong `EGG_PHASE` would not directly bypass that, but the env-propagation contract is a load-bearing piece of the substrate's trust-boundary story and pinning it pre-empts a class of "spawned subagent saw the wrong stage" bugs. -- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259** — `test_no_new_validator_symbol_introduced_in_r2_pass_slice` enforces the R2-pass "no-op" contract: TASK-2-5 said "if R2 = pass, this task is a no-op". The test asserts no `validate_write_target` (or peer) symbol was added to `sandbox/egg_agent_tools/handlers/restrictions.py`, so an unintended slip of agent-side enforcement onto the R2-pass path would surface here. This is exactly the cross-file invariant the lens cares about — "did the slice land scope it wasn't supposed to". -- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:268-297** — `test_missing_path_raises_handler_error` and `test_unknown_role_raises_handler_error` pin the validator's defensive surface (no permissive fall-through on missing-path / unknown-role inputs). Loose role validation here would be a real security concern (an agent could pass an unknown role and get an `ok: True, can_write: True` answer); pinning the existing HandlerError behaviour is correct. - -### Non-blocking -- **integration_tests/regression/test_inprocess_plan_brc.py:155-158** — `isolated_pipeline_state` walks three candidate private-registry names (`_TRACKERS`, `_PEER_CONSENSUS_TRACKERS`, `_tracker_registry`) and clears whichever exists. Pragmatic, but mildly brittle: if `orchestrator/peer_consensus.py` renames its registry to a fourth name in a future slice, the fixture silently no-ops and back-to-back tests would share confirmed state. Not a security concern; the failure mode is a flaky test, not a security hole. A `pytest.fail("no peer-consensus tracker registry found")` if NONE of the candidates resolves would surface the rename earlier. Defer to reviewer_code. - - -````yaml -id: 820b1293-0b54-46 -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_inprocess_plan_brc.py - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py - reason: "\nReviewed the slice-2 tester proposal at commit b2be479f1 through the\ - \ security lens. Read both new test files end-to-end. No security findings;\ - \ the diff is in fact security-positive (pins several defensive invariants the\ - \ security lens cares about).\n\n### Lens checks\n\n1. **Cross-file allowlist\ - \ mismatch (\xA71):** N/A \u2014 the tests *verify* the allowlist semantics\ - \ rather than introducing new ones. `test_coder_in_allow_list_response_shape_stable`\ - \ / `test_tester_in_allow_list_response_shape_stable` / `test_documenter_in_allow_list_response_shape_stable`\ - \ pin that the documented per-role file-write patterns continue to hold; the\ - \ cross-role denial cases (`test_coder_cannot_write_tester_path_denial_shape_stable`,\ - \ `test_tester_cannot_write_orchestrator_path_denial_shape_stable`) pin that\ - \ the denial reason references `shared/egg_restrictions/patterns.py` and that\ - \ `alternative_role` names the correct producer. Net positive for the lens \u2014\ - \ these are regression guards on the allowlist surface.\n\n2. **Handler-vs-validator\ - \ path mismatch (\xA72):** N/A; tests do not introduce new public endpoints.\n\ - \n3. **Information-disclosure / authz bypass (\xA73):** No real credentials,\ - \ tokens, or identity material is embedded in the test fixtures. `_make_fake_bundle`\ - \ returns `MagicMock(... commit_sha=\"0\" * 40, stdout=\"ok\", ...)` \u2014\ - \ clearly synthetic. The `fake_home` fixture (test_inprocess_plan_brc.py:127-137)\ - \ explicitly relocates `$HOME` under `tmp_path / \"home\"` so the generator's\ - \ `_write_active_role_sentinel` write does NOT pollute the developer's actual\ - \ home directory \u2014 this is the right hygiene posture for a test that exercises\ - \ sentinel I/O.\n\n4. **Uncommitted-artifact / symlink mismatch (\xA74):** Both\ - \ new files are committed at b2be479f1 with non-zero size; no Dockerfile / packaging-manifest\ - \ / symlink references.\n\n5. **Credential-shim modifications (\xA75):** N/A\ - \ \u2014 no changes under `sandbox/scripts/`.\n\n6. **Secret leakage (\xA76):**\ - \ Test paths and fixtures are all synthetic / orchestrator-derived; no environment\ - \ dumps, no logs of actual credentials, no real network endpoints.\n\n7. **Cross-file\ - \ OWASP top-10 (\xA77):** N/A.\n\n8. **Agent-supplied paths into read-only fs\ - \ access (\xA78):** Tests use `pytest`'s `tmp_path` for all real filesystem\ - \ I/O. The `sys.path.insert(0, str(ROOT / \"sandbox\"))` at test_restrictions_validator.py:78-79\ - \ derives `ROOT` from `Path(__file__).resolve().parents[3]` \u2014 anchored\ - \ to the test file's own location, not agent-supplied. Hardcoded paths passed\ - \ to `restrictions.check_file_restriction({\"path\": \"orchestrator/foo.py\"\ - })` are evaluated against the regex pattern registry, never opened on disk.\ - \ No new fs-read surface.\n\n### Security-positive defensive invariants this\ - \ diff pins\n\nThe following tests are themselves the *kind of regression guards*\ - \ the security lens wants to see:\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:624-675**\ - \ \u2014 `test_plan_stage_does_not_spawn_implement_phase_roles` pins the negative\ - \ invariant that the plan stage cannot accidentally invoke `coder` / `tester`\ - \ / `documenter` / `reviewer_*` from the implement team. A phase-dispatch lookup\ - \ that mis-indexed `_PHASE_ROLES[\"plan\"]` (e.g. off-by-one onto `\"implement\"\ - `) would burn six concurrent implement-team spawns the operator never approved\ - \ \u2014 exactly the kind of HITL-bypass shape the security lens cares about.\ - \ Pinning it as a regression test is the right shape.\n- **integration_tests/regression/test_inprocess_plan_brc.py:557-615**\ - \ \u2014 `test_plan_stage_does_not_run_when_operator_rejects_refine` pins the\ - \ HITL-gate invariant: a `stop` answer at the refine gate MUST NOT advance into\ - \ the plan stage's three concurrent spawns. Same shape as above \u2014 regression\ - \ here would be a HITL-bypass.\n- **integration_tests/regression/test_inprocess_plan_brc.py:735-797**\ - \ \u2014 `test_plan_stage_carries_phase_env_var_to_producers` pins that `EGG_PHASE=plan`\ - \ reaches every plan-phase spawn. The PreToolUse hook reads `EGG_AGENT_ROLE`\ - \ for allow-list enforcement; a missing/wrong `EGG_PHASE` would not directly\ - \ bypass that, but the env-propagation contract is a load-bearing piece of the\ - \ substrate's trust-boundary story and pinning it pre-empts a class of \"spawned\ - \ subagent saw the wrong stage\" bugs.\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259**\ - \ \u2014 `test_no_new_validator_symbol_introduced_in_r2_pass_slice` enforces\ - \ the R2-pass \"no-op\" contract: TASK-2-5 said \"if R2 = pass, this task is\ - \ a no-op\". The test asserts no `validate_write_target` (or peer) symbol was\ - \ added to `sandbox/egg_agent_tools/handlers/restrictions.py`, so an unintended\ - \ slip of agent-side enforcement onto the R2-pass path would surface here. This\ - \ is exactly the cross-file invariant the lens cares about \u2014 \"did the\ - \ slice land scope it wasn't supposed to\".\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:268-297**\ - \ \u2014 `test_missing_path_raises_handler_error` and `test_unknown_role_raises_handler_error`\ - \ pin the validator's defensive surface (no permissive fall-through on missing-path\ - \ / unknown-role inputs). Loose role validation here would be a real security\ - \ concern (an agent could pass an unknown role and get an `ok: True, can_write:\ - \ True` answer); pinning the existing HandlerError behaviour is correct.\n\n\ - ### Non-blocking\n- **integration_tests/regression/test_inprocess_plan_brc.py:155-158**\ - \ \u2014 `isolated_pipeline_state` walks three candidate private-registry names\ - \ (`_TRACKERS`, `_PEER_CONSENSUS_TRACKERS`, `_tracker_registry`) and clears\ - \ whichever exists. Pragmatic, but mildly brittle: if `orchestrator/peer_consensus.py`\ - \ renames its registry to a fourth name in a future slice, the fixture silently\ - \ no-ops and back-to-back tests would share confirmed state. Not a security\ - \ concern; the failure mode is a flaky test, not a security hole. A `pytest.fail(\"\ - no peer-consensus tracker registry found\")` if NONE of the candidates resolves\ - \ would surface the rename earlier. Defer to reviewer_code.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:30:04Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ea09d460-a921-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:10Z] reviewer_security (CONSENSUS_CONFIRMED): Confirmed by reviewer_security - -````yaml -id: 2d6afb59-669e-4f -phase: implement -metadata: - consensus_reached: false - slice_id: slice-2 -```` - -### [2026-05-19T20:30:13Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: bd30afbe-10b9-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:13.381894+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:14Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 96704551-f978-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:14.901780+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:26Z] reviewer_code_holistic → coder (CONSENSUS_NACK): NACK from reviewer_code_holistic for coder - - -Holistic re-review of coder slice-2 v3 (commit d7b2a6152 — same SHA as v2, re-propose only). v1 blockers H1 (architect-first ordering) and H2 (reviewer verdict no longer exit-code-only) are both addressed correctly — `_plan_phase.run_plan_phase` spawns `architect` synchronously first, then fans out `task_planner` + `risk_analyst` through a `ThreadPoolExecutor(max_workers=2)` with `EGG_ARCHITECT_OUTPUT_PATH` plumbed into both spawn_env and prompt_text (matches the role-dependency declarations in `shared/egg_contracts/agent_roles.py:398/422`); `read_plan_reviewer_verdicts` + `_apply_reviewer_verdicts` now drive per-edge ACK / NACK on the tracker from a parsed verdict JSON; `_current_phase` flips to "plan" so heartbeats carry the right phase across the transition; tracker-guard rejections log via `log_tracker_warning` instead of bare `except`; placeholder body now renders reviewer_plan diagnostics. Good. One new blocker surfaced by pass 2 / pass 4 review of the H2 fix: - -### Blocking - -1. **Pass 2 (doc ↔ code symmetry) + Pass 4 (silent fallback) — reviewer_plan verdict JSON schema mismatch between the rubric and the parser; a rubric-following reviewer's NACK is silently transformed into an ACK.** Producer: `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md` "Verdict JSON shape" section (lines 57–80) tells the reviewer to write **one** JSON object to `verdict_path` with top-level keys `verdict` (ACK | NACK), `summary`, `analysis`, `suggestions`, `artifact_references`, `feedback`, `timestamp` — no `per_producer` wrapper, no per-edge schema. Consumer: `orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts` (lines 251–286) reads `blob.get("per_producer") or {}` and ignores everything outside that wrapper. The two schemas are incompatible. - - Walking the failure end-to-end: a real reviewer_plan agent follows the rubric, writes `{"verdict": "NACK", "feedback": "task_planner role_assignments puts a coder task in tests/", …}`, exits 0. The parser opens the file, finds no `per_producer` key, returns `(verdict_path, {})` (lines 270–272 — `per_producer = blob.get("per_producer") or {}` followed by an empty-dict early-return when `normalised` stays empty). `_apply_reviewer_verdicts` (lines 308–349) then computes `verdict_file_present = bool({}) = False`, `fail_closed = False and reviewer_exit_code != 0 = False`, so the per-producer loop falls into the `if entry is None: … _record_reviewer_ack(…, reason="reviewer_plan ACK (synthetic): verdict file absent AND reviewer exit_code=0 — in-process synchronous-spawn-as-signal default per #2717 slice-2")` branch for **every** producer. The operator sees `is_complete=True` at the plan-HITL gate, approves a plan the reviewer actually rejected, and the reviewer's NACK feedback is buried in a JSON file nobody parses. - - The "optimistic-ACK when verdict file is missing" fallback (intended for harness-faked runs) silently catches the "verdict file *present but wrong schema*" case because `read_plan_reviewer_verdicts` collapses both into the same empty-dict return. This is the canonical silent-fallback shape: the safety floor (BRC advances) is preserved, the operator-facing signal (reviewer's verdict) is masked. The diagnostic surface in the placeholder body (`- per_producer: — reviewer did not write a parseable verdict JSON`) is only rendered on the placeholder code-path (`if not plan_artifact_path.exists():`), so a harness that *does* land `-plan.md` swallows it entirely — and even when rendered, "did not write a parseable verdict JSON" is wrong: the JSON parses fine, it just doesn't carry the key the orchestrator expects. - - Compounding evidence that the schema mismatch is real, not a coder typo: `grep -rn 'per_producer\b' orchestrator/ shared/` shows the key exists ONLY in `_plan_phase.py`. No rubric, no k3s code path, no existing test fixture produces a `per_producer` JSON. The v2 commit body cites a "Mixed verdict: with a per_producer verdict JSON {architect:ACK, task_planner:NACK, risk_analyst:ACK}" smoke test — i.e. the coder hand-crafted the per_producer shape for the smoke and confirmed the parser walks it correctly, but never confirmed that a rubric-following reviewer would emit that shape. The same lens reviewer who flagged H2 in v1 finds H3 in v2/v3 because the v1 NACK only said "parse the reviewer's verdict"; it did not specify the schema, and the documenter's rubric (already landed in commit 7122ca2d1) defines an incompatible one. - - Pick one of the three resolutions; all three are acceptable from a holistic-coherence standpoint, but the doc and code must agree before slice-2 lands: - - **(a)** Dispatch the reviewer N times (once per producer edge) inside `_plan_phase.run_plan_phase` — one `spawn_plan_reviewer(producer=X)` call per producer, each writing its own single-verdict JSON at `.egg-state/agent-outputs/-reviewer_plan--output.json`. Aggregate by reading the N files. This matches the rubric and the k3s substrate's per-edge routing. - - **(b)** Keep the single reviewer dispatch and update the rubric (documenter coordination) to specify a `per_producer` wrapper schema: `{"per_producer": {"architect": {"verdict": …, "reason": …, …}, "task_planner": {…}, "risk_analyst": {…}}}`. The "Verdict JSON shape" block in `reviewer_plan.md` and the note at line 102 ("each edge's verdict is namespaced by the producer role in the artifact handoff") both need to be updated to reference the wrapper. The reviewer's rubric currently has no way to produce per-edge verdicts inside a single JSON file — it has to be told. - - **(c)** Treat the single top-level `verdict` field as a whole-plan verdict and broadcast it to all three tracker edges. The orchestrator parses the rubric-documented schema; an `"ACK"` ACKs every producer edge, a `"NACK"` NACKs every producer edge with the single `feedback` blob attached to all three. Lowest-effort but loses per-edge granularity — the rubric's "ACK / NACK each producer independently" promise becomes "all or nothing". - - I do NOT have a preference between (a) / (b) / (c) — the coder + documenter should pick the one that lines up with the k3s substrate's behaviour (whichever path matches `orchestrator/routes/pipelines.py`'s plan-phase reviewer driver is the right one for the doc's "Your eight review criteria, your evidence discipline, and your verdict JSON shape are unchanged" promise). The blocking issue is that today, doc and code disagree, and the resulting silent-fallback transforms operator-meaningful NACKs into ACKs. - -### Non-blocking - -- **Pass 2 — same documenter mismatches surfaced in v1 still pending** (SKILL.md "Plan HITL gate" lists `approve` instead of `approve_continue`; `request_changes` / `change_approach` described as loop-back actions while the code returns the artifact path). Not in coder scope; carries to the documenter NACK chain. -- **Optimistic-ACK fallback misnamed in the placeholder.** When `read_plan_reviewer_verdicts` returns `(verdict_path, {})` with a non-None `verdict_path` (file present but no parseable per_producer), the placeholder still emits `- per_producer: — reviewer did not write a parseable verdict JSON`. After (a) / (b) / (c) above land, this diagnostic line will be accurate; today it is misleading because the JSON *was* parseable, it just used the documented top-level schema. -- **`spawn_plan_reviewer` still writes the active-role sentinel (line 537) even though the reviewer's spawn never overlaps with another spawn** — that's fine, but the docstring's "the reviewer dispatches solo (no concurrent role-holder)" claim hinges on call-site ordering that's not enforced anywhere. A one-line comment naming the call-site invariant ("`run_plan_phase_inner` calls this after the producer fan-out has joined") would harden it. - -If you take resolution (a) — separate reviewer invocations — the v2/v3 spawn flow already plumbs per-role producer paths into the single reviewer dispatch; pulling that into a per-edge loop is a small delta. If you take (b) or (c), the documenter has to land a corresponding rubric update; coordinate with them on the same NACK cycle so the doc and code reach v4 together. - - -````yaml -id: 8b101016-0310-41 -phase: implement -metadata: - payload: - reason: "\nHolistic re-review of coder slice-2 v3 (commit d7b2a6152 \u2014 same\ - \ SHA as v2, re-propose only). v1 blockers H1 (architect-first ordering) and\ - \ H2 (reviewer verdict no longer exit-code-only) are both addressed correctly\ - \ \u2014 `_plan_phase.run_plan_phase` spawns `architect` synchronously first,\ - \ then fans out `task_planner` + `risk_analyst` through a `ThreadPoolExecutor(max_workers=2)`\ - \ with `EGG_ARCHITECT_OUTPUT_PATH` plumbed into both spawn_env and prompt_text\ - \ (matches the role-dependency declarations in `shared/egg_contracts/agent_roles.py:398/422`);\ - \ `read_plan_reviewer_verdicts` + `_apply_reviewer_verdicts` now drive per-edge\ - \ ACK / NACK on the tracker from a parsed verdict JSON; `_current_phase` flips\ - \ to \"plan\" so heartbeats carry the right phase across the transition; tracker-guard\ - \ rejections log via `log_tracker_warning` instead of bare `except`; placeholder\ - \ body now renders reviewer_plan diagnostics. Good. One new blocker surfaced\ - \ by pass 2 / pass 4 review of the H2 fix:\n\n### Blocking\n\n1. **Pass 2 (doc\ - \ \u2194 code symmetry) + Pass 4 (silent fallback) \u2014 reviewer_plan verdict\ - \ JSON schema mismatch between the rubric and the parser; a rubric-following\ - \ reviewer's NACK is silently transformed into an ACK.** Producer: `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md`\ - \ \"Verdict JSON shape\" section (lines 57\u201380) tells the reviewer to write\ - \ **one** JSON object to `verdict_path` with top-level keys `verdict` (ACK |\ - \ NACK), `summary`, `analysis`, `suggestions`, `artifact_references`, `feedback`,\ - \ `timestamp` \u2014 no `per_producer` wrapper, no per-edge schema. Consumer:\ - \ `orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts` (lines\ - \ 251\u2013286) reads `blob.get(\"per_producer\") or {}` and ignores everything\ - \ outside that wrapper. The two schemas are incompatible.\n\n Walking the\ - \ failure end-to-end: a real reviewer_plan agent follows the rubric, writes\ - \ `{\"verdict\": \"NACK\", \"feedback\": \"task_planner role_assignments puts\ - \ a coder task in tests/\", \u2026}`, exits 0. The parser opens the file, finds\ - \ no `per_producer` key, returns `(verdict_path, {})` (lines 270\u2013272 \u2014\ - \ `per_producer = blob.get(\"per_producer\") or {}` followed by an empty-dict\ - \ early-return when `normalised` stays empty). `_apply_reviewer_verdicts` (lines\ - \ 308\u2013349) then computes `verdict_file_present = bool({}) = False`, `fail_closed\ - \ = False and reviewer_exit_code != 0 = False`, so the per-producer loop falls\ - \ into the `if entry is None: \u2026 _record_reviewer_ack(\u2026, reason=\"\ - reviewer_plan ACK (synthetic): verdict file absent AND reviewer exit_code=0\ - \ \u2014 in-process synchronous-spawn-as-signal default per #2717 slice-2\"\ - )` branch for **every** producer. The operator sees `is_complete=True` at the\ - \ plan-HITL gate, approves a plan the reviewer actually rejected, and the reviewer's\ - \ NACK feedback is buried in a JSON file nobody parses.\n\n The \"optimistic-ACK\ - \ when verdict file is missing\" fallback (intended for harness-faked runs)\ - \ silently catches the \"verdict file *present but wrong schema*\" case because\ - \ `read_plan_reviewer_verdicts` collapses both into the same empty-dict return.\ - \ This is the canonical silent-fallback shape: the safety floor (BRC advances)\ - \ is preserved, the operator-facing signal (reviewer's verdict) is masked. The\ - \ diagnostic surface in the placeholder body (`- per_producer: \u2014\ - \ reviewer did not write a parseable verdict JSON`) is only rendered on the\ - \ placeholder code-path (`if not plan_artifact_path.exists():`), so a harness\ - \ that *does* land `-plan.md` swallows it entirely \u2014 and even when\ - \ rendered, \"did not write a parseable verdict JSON\" is wrong: the JSON parses\ - \ fine, it just doesn't carry the key the orchestrator expects.\n\n Compounding\ - \ evidence that the schema mismatch is real, not a coder typo: `grep -rn 'per_producer\\\ - b' orchestrator/ shared/` shows the key exists ONLY in `_plan_phase.py`. No\ - \ rubric, no k3s code path, no existing test fixture produces a `per_producer`\ - \ JSON. The v2 commit body cites a \"Mixed verdict: with a per_producer verdict\ - \ JSON {architect:ACK, task_planner:NACK, risk_analyst:ACK}\" smoke test \u2014\ - \ i.e. the coder hand-crafted the per_producer shape for the smoke and confirmed\ - \ the parser walks it correctly, but never confirmed that a rubric-following\ - \ reviewer would emit that shape. The same lens reviewer who flagged H2 in v1\ - \ finds H3 in v2/v3 because the v1 NACK only said \"parse the reviewer's verdict\"\ - ; it did not specify the schema, and the documenter's rubric (already landed\ - \ in commit 7122ca2d1) defines an incompatible one.\n\n Pick one of the three\ - \ resolutions; all three are acceptable from a holistic-coherence standpoint,\ - \ but the doc and code must agree before slice-2 lands:\n - **(a)** Dispatch\ - \ the reviewer N times (once per producer edge) inside `_plan_phase.run_plan_phase`\ - \ \u2014 one `spawn_plan_reviewer(producer=X)` call per producer, each writing\ - \ its own single-verdict JSON at `.egg-state/agent-outputs/-reviewer_plan--output.json`.\ - \ Aggregate by reading the N files. This matches the rubric and the k3s substrate's\ - \ per-edge routing.\n - **(b)** Keep the single reviewer dispatch and update\ - \ the rubric (documenter coordination) to specify a `per_producer` wrapper schema:\ - \ `{\"per_producer\": {\"architect\": {\"verdict\": \u2026, \"reason\": \u2026\ - , \u2026}, \"task_planner\": {\u2026}, \"risk_analyst\": {\u2026}}}`. The \"\ - Verdict JSON shape\" block in `reviewer_plan.md` and the note at line 102 (\"\ - each edge's verdict is namespaced by the producer role in the artifact handoff\"\ - ) both need to be updated to reference the wrapper. The reviewer's rubric currently\ - \ has no way to produce per-edge verdicts inside a single JSON file \u2014 it\ - \ has to be told.\n - **(c)** Treat the single top-level `verdict` field as\ - \ a whole-plan verdict and broadcast it to all three tracker edges. The orchestrator\ - \ parses the rubric-documented schema; an `\"ACK\"` ACKs every producer edge,\ - \ a `\"NACK\"` NACKs every producer edge with the single `feedback` blob attached\ - \ to all three. Lowest-effort but loses per-edge granularity \u2014 the rubric's\ - \ \"ACK / NACK each producer independently\" promise becomes \"all or nothing\"\ - .\n\n I do NOT have a preference between (a) / (b) / (c) \u2014 the coder\ - \ + documenter should pick the one that lines up with the k3s substrate's behaviour\ - \ (whichever path matches `orchestrator/routes/pipelines.py`'s plan-phase reviewer\ - \ driver is the right one for the doc's \"Your eight review criteria, your evidence\ - \ discipline, and your verdict JSON shape are unchanged\" promise). The blocking\ - \ issue is that today, doc and code disagree, and the resulting silent-fallback\ - \ transforms operator-meaningful NACKs into ACKs.\n\n### Non-blocking\n\n- **Pass\ - \ 2 \u2014 same documenter mismatches surfaced in v1 still pending** (SKILL.md\ - \ \"Plan HITL gate\" lists `approve` instead of `approve_continue`; `request_changes`\ - \ / `change_approach` described as loop-back actions while the code returns\ - \ the artifact path). Not in coder scope; carries to the documenter NACK chain.\n\ - - **Optimistic-ACK fallback misnamed in the placeholder.** When `read_plan_reviewer_verdicts`\ - \ returns `(verdict_path, {})` with a non-None `verdict_path` (file present\ - \ but no parseable per_producer), the placeholder still emits `- per_producer:\ - \ \u2014 reviewer did not write a parseable verdict JSON`. After (a)\ - \ / (b) / (c) above land, this diagnostic line will be accurate; today it is\ - \ misleading because the JSON *was* parseable, it just used the documented top-level\ - \ schema.\n- **`spawn_plan_reviewer` still writes the active-role sentinel (line\ - \ 537) even though the reviewer's spawn never overlaps with another spawn**\ - \ \u2014 that's fine, but the docstring's \"the reviewer dispatches solo (no\ - \ concurrent role-holder)\" claim hinges on call-site ordering that's not enforced\ - \ anywhere. A one-line comment naming the call-site invariant (\"`run_plan_phase_inner`\ - \ calls this after the producer fan-out has joined\") would harden it.\n\nIf\ - \ you take resolution (a) \u2014 separate reviewer invocations \u2014 the v2/v3\ - \ spawn flow already plumbs per-role producer paths into the single reviewer\ - \ dispatch; pulling that into a per-edge loop is a small delta. If you take\ - \ (b) or (c), the documenter has to land a corresponding rubric update; coordinate\ - \ with them on the same NACK cycle so the doc and code reach v4 together.\n" - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - nack_version: 3 - reason: "\nHolistic re-review of coder slice-2 v3 (commit d7b2a6152 \u2014 same\ - \ SHA as v2, re-propose only). v1 blockers H1 (architect-first ordering) and H2\ - \ (reviewer verdict no longer exit-code-only) are both addressed correctly \u2014\ - \ `_plan_phase.run_plan_phase` spawns `architect` synchronously first, then fans\ - \ out `task_planner` + `risk_analyst` through a `ThreadPoolExecutor(max_workers=2)`\ - \ with `EGG_ARCHITECT_OUTPUT_PATH` plumbed into both spawn_env and prompt_text\ - \ (matches the role-dependency declarations in `shared/egg_contracts/agent_roles.py:398/422`);\ - \ `read_plan_reviewer_verdicts` + `_apply_reviewer_verdicts` now drive per-edge\ - \ ACK / NACK on the tracker from a parsed verdict JSON; `_current_phase` flips\ - \ to \"plan\" so heartbeats carry the right phase across the transition; tracker-guard\ - \ rejections log via `log_tracker_warning` instead of bare `except`; placeholder\ - \ body now renders reviewer_plan diagnostics. Good. One new blocker surfaced by\ - \ pass 2 / pass 4 review of the H2 fix:\n\n### Blocking\n\n1. **Pass 2 (doc \u2194\ - \ code symmetry) + Pass 4 (silent fallback) \u2014 reviewer_plan verdict JSON\ - \ schema mismatch between the rubric and the parser; a rubric-following reviewer's\ - \ NACK is silently transformed into an ACK.** Producer: `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md`\ - \ \"Verdict JSON shape\" section (lines 57\u201380) tells the reviewer to write\ - \ **one** JSON object to `verdict_path` with top-level keys `verdict` (ACK | NACK),\ - \ `summary`, `analysis`, `suggestions`, `artifact_references`, `feedback`, `timestamp`\ - \ \u2014 no `per_producer` wrapper, no per-edge schema. Consumer: `orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts`\ - \ (lines 251\u2013286) reads `blob.get(\"per_producer\") or {}` and ignores everything\ - \ outside that wrapper. The two schemas are incompatible.\n\n Walking the failure\ - \ end-to-end: a real reviewer_plan agent follows the rubric, writes `{\"verdict\"\ - : \"NACK\", \"feedback\": \"task_planner role_assignments puts a coder task in\ - \ tests/\", \u2026}`, exits 0. The parser opens the file, finds no `per_producer`\ - \ key, returns `(verdict_path, {})` (lines 270\u2013272 \u2014 `per_producer =\ - \ blob.get(\"per_producer\") or {}` followed by an empty-dict early-return when\ - \ `normalised` stays empty). `_apply_reviewer_verdicts` (lines 308\u2013349) then\ - \ computes `verdict_file_present = bool({}) = False`, `fail_closed = False and\ - \ reviewer_exit_code != 0 = False`, so the per-producer loop falls into the `if\ - \ entry is None: \u2026 _record_reviewer_ack(\u2026, reason=\"reviewer_plan ACK\ - \ (synthetic): verdict file absent AND reviewer exit_code=0 \u2014 in-process\ - \ synchronous-spawn-as-signal default per #2717 slice-2\")` branch for **every**\ - \ producer. The operator sees `is_complete=True` at the plan-HITL gate, approves\ - \ a plan the reviewer actually rejected, and the reviewer's NACK feedback is buried\ - \ in a JSON file nobody parses.\n\n The \"optimistic-ACK when verdict file is\ - \ missing\" fallback (intended for harness-faked runs) silently catches the \"\ - verdict file *present but wrong schema*\" case because `read_plan_reviewer_verdicts`\ - \ collapses both into the same empty-dict return. This is the canonical silent-fallback\ - \ shape: the safety floor (BRC advances) is preserved, the operator-facing signal\ - \ (reviewer's verdict) is masked. The diagnostic surface in the placeholder body\ - \ (`- per_producer: \u2014 reviewer did not write a parseable verdict\ - \ JSON`) is only rendered on the placeholder code-path (`if not plan_artifact_path.exists():`),\ - \ so a harness that *does* land `-plan.md` swallows it entirely \u2014\ - \ and even when rendered, \"did not write a parseable verdict JSON\" is wrong:\ - \ the JSON parses fine, it just doesn't carry the key the orchestrator expects.\n\ - \n Compounding evidence that the schema mismatch is real, not a coder typo:\ - \ `grep -rn 'per_producer\\b' orchestrator/ shared/` shows the key exists ONLY\ - \ in `_plan_phase.py`. No rubric, no k3s code path, no existing test fixture produces\ - \ a `per_producer` JSON. The v2 commit body cites a \"Mixed verdict: with a per_producer\ - \ verdict JSON {architect:ACK, task_planner:NACK, risk_analyst:ACK}\" smoke test\ - \ \u2014 i.e. the coder hand-crafted the per_producer shape for the smoke and\ - \ confirmed the parser walks it correctly, but never confirmed that a rubric-following\ - \ reviewer would emit that shape. The same lens reviewer who flagged H2 in v1\ - \ finds H3 in v2/v3 because the v1 NACK only said \"parse the reviewer's verdict\"\ - ; it did not specify the schema, and the documenter's rubric (already landed in\ - \ commit 7122ca2d1) defines an incompatible one.\n\n Pick one of the three resolutions;\ - \ all three are acceptable from a holistic-coherence standpoint, but the doc and\ - \ code must agree before slice-2 lands:\n - **(a)** Dispatch the reviewer N\ - \ times (once per producer edge) inside `_plan_phase.run_plan_phase` \u2014 one\ - \ `spawn_plan_reviewer(producer=X)` call per producer, each writing its own single-verdict\ - \ JSON at `.egg-state/agent-outputs/-reviewer_plan--output.json`.\ - \ Aggregate by reading the N files. This matches the rubric and the k3s substrate's\ - \ per-edge routing.\n - **(b)** Keep the single reviewer dispatch and update\ - \ the rubric (documenter coordination) to specify a `per_producer` wrapper schema:\ - \ `{\"per_producer\": {\"architect\": {\"verdict\": \u2026, \"reason\": \u2026\ - , \u2026}, \"task_planner\": {\u2026}, \"risk_analyst\": {\u2026}}}`. The \"Verdict\ - \ JSON shape\" block in `reviewer_plan.md` and the note at line 102 (\"each edge's\ - \ verdict is namespaced by the producer role in the artifact handoff\") both need\ - \ to be updated to reference the wrapper. The reviewer's rubric currently has\ - \ no way to produce per-edge verdicts inside a single JSON file \u2014 it has\ - \ to be told.\n - **(c)** Treat the single top-level `verdict` field as a whole-plan\ - \ verdict and broadcast it to all three tracker edges. The orchestrator parses\ - \ the rubric-documented schema; an `\"ACK\"` ACKs every producer edge, a `\"NACK\"\ - ` NACKs every producer edge with the single `feedback` blob attached to all three.\ - \ Lowest-effort but loses per-edge granularity \u2014 the rubric's \"ACK / NACK\ - \ each producer independently\" promise becomes \"all or nothing\".\n\n I do\ - \ NOT have a preference between (a) / (b) / (c) \u2014 the coder + documenter\ - \ should pick the one that lines up with the k3s substrate's behaviour (whichever\ - \ path matches `orchestrator/routes/pipelines.py`'s plan-phase reviewer driver\ - \ is the right one for the doc's \"Your eight review criteria, your evidence discipline,\ - \ and your verdict JSON shape are unchanged\" promise). The blocking issue is\ - \ that today, doc and code disagree, and the resulting silent-fallback transforms\ - \ operator-meaningful NACKs into ACKs.\n\n### Non-blocking\n\n- **Pass 2 \u2014\ - \ same documenter mismatches surfaced in v1 still pending** (SKILL.md \"Plan HITL\ - \ gate\" lists `approve` instead of `approve_continue`; `request_changes` / `change_approach`\ - \ described as loop-back actions while the code returns the artifact path). Not\ - \ in coder scope; carries to the documenter NACK chain.\n- **Optimistic-ACK fallback\ - \ misnamed in the placeholder.** When `read_plan_reviewer_verdicts` returns `(verdict_path,\ - \ {})` with a non-None `verdict_path` (file present but no parseable per_producer),\ - \ the placeholder still emits `- per_producer: \u2014 reviewer did not\ - \ write a parseable verdict JSON`. After (a) / (b) / (c) above land, this diagnostic\ - \ line will be accurate; today it is misleading because the JSON *was* parseable,\ - \ it just used the documented top-level schema.\n- **`spawn_plan_reviewer` still\ - \ writes the active-role sentinel (line 537) even though the reviewer's spawn\ - \ never overlaps with another spawn** \u2014 that's fine, but the docstring's\ - \ \"the reviewer dispatches solo (no concurrent role-holder)\" claim hinges on\ - \ call-site ordering that's not enforced anywhere. A one-line comment naming the\ - \ call-site invariant (\"`run_plan_phase_inner` calls this after the producer\ - \ fan-out has joined\") would harden it.\n\nIf you take resolution (a) \u2014\ - \ separate reviewer invocations \u2014 the v2/v3 spawn flow already plumbs per-role\ - \ producer paths into the single reviewer dispatch; pulling that into a per-edge\ - \ loop is a small delta. If you take (b) or (c), the documenter has to land a\ - \ corresponding rubric update; coordinate with them on the same NACK cycle so\ - \ the doc and code reach v4 together.\n" - revision_count: 3 - slice_id: slice-2 -```` - -### [2026-05-19T20:30:26Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: b1342a50-b07d-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:37Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 460b1a94-91c7-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:36.963716+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:37Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 4f9a0af0-5a2a-41 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:40Z] reviewer_contract (CONSENSUS_CONFIRMED): Confirmed by reviewer_contract - -````yaml -id: 37ca114e-19da-41 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-2 -```` - -### [2026-05-19T20:30:40Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 20edd34c-af96-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:44Z] reviewer_concurrency → tester (CONSENSUS_ACK): ACK from reviewer_concurrency for tester - - -Concurrency-lens review of tester v1 (commit b2be479f1). The proposal adds two test files only — no production code changes — so the concurrency surface is the test harness itself. I walked the fixture-shared state, the per-test isolation, and the executor / mock interaction to confirm the tests don't introduce concurrency hazards or mask the v3 coder's concurrent code path. - -### Verified - -- **Shared-state isolation between tests.** The `isolated_pipeline_state` fixture (`test_inprocess_plan_brc.py:230-248`) clears the module-level `_TRACKERS` / `_PEER_CONSENSUS_TRACKERS` / `_tracker_registry` registry between tests so back-to-back runs don't inherit a previously-confirmed tracker. `fake_home` (`:216-227`) isolates `$HOME` per test so sentinel writes can't bleed across tests. Each test uses a unique pipeline id (`pipeline-plan-brc-spawns`, `pipeline-plan-brc-hitl`, etc.) so two tests in the same worker cannot collide on the registry key even before the explicit `.clear()`. Under `pytest-xdist` the worker-process boundary takes care of the rest. - -- **Exercising the real concurrent path.** `_make_fake_bundle` returns a `MagicMock`-backed spawner whose `.spawn(...)` is recorded into `.call_args_list`. `_run_plan_phase_inner` (coder v3, `orchestrator/substrate/_plan_phase.py:138-150`) submits task_planner + risk_analyst to a real `ThreadPoolExecutor(max_workers=2)`, so both threads concurrently invoke `bundle.spawner.spawn(...)`. CPython's GIL makes `list.append` (the recording mechanism inside `_mock_call`) atomic, so the spawn-call ledger does not tear under the concurrent invocation; the `_EXPECTED_PRODUCERS - plan_spawned` assertion at `test_inprocess_plan_brc.py:428-433` therefore reliably catches a missing-role regression even when the executor fans out. - -- **Background-thread teardown.** Every test's `finally` block calls `gen.close()` then `time.sleep(0.2)` / `0.3` to let the heartbeat / brc-review / bus-tick daemons unwind. The generator's `_shutdown_background_threads` already joins with a 2.0 s timeout, so the sleep is a courtesy flush — no leaked daemon thread can poison the next test's tracker because the registry is `.clear()`'d before the next test starts. The `short_intervals` fixture shrinks the tick intervals to 0.05 s so the tests don't pad to multi-second runtimes waiting for the timeouts. - -- **No retry storms or off-protocol bus emissions.** The fake bundle binds `bundle.bus = InProcessMessageBus()` (`:280-282`) rather than a `MagicMock`, so the heartbeat publisher's `bus.add_message(...)` lands on a real bus and does not silently swallow type errors — the v3 heartbeat-phase fix (`phase=self._current_phase`) flows through correctly under this fixture. - -- **Adversarial probes target the right concurrency-adjacent invariants.** `test_plan_stage_does_not_run_when_operator_rejects_refine` (`:647-705`) pins the safety invariant that an unauthorised concurrent producer dispatch cannot fire on a non-`approve_continue` answer. `test_plan_stage_carries_phase_env_var_to_producers` (`:825-887`) confirms `EGG_PHASE=plan` is on every plan-producer spawn env — important because v3 dropped the concurrent-path sentinel write and now relies on env propagation as the load-bearing role-routing channel. - -### Drift note (non-blocking) - -The commit body and module docstring describe the implementation as "3 producers concurrent" (`:118-125`). Coder v3 actually runs **architect synchronously first** and then fans out task_planner + risk_analyst (2-way concurrent) — the producer count is still 3 but only 2 are concurrent. The test's spawn-set assertion (`_EXPECTED_PRODUCERS - plan_spawned`) doesn't pin ordering and still passes against v3's architect-first sequencing, but the docstring text would mislead a future maintainer reading the comments. Drop a sentence noting "architect runs synchronously first, then task_planner + risk_analyst fan out via ThreadPoolExecutor(max_workers=2)" so the test's narrative matches the v3 implementation. - -### Non-blocking - -- `bundle.worktrees.create = MagicMock(return_value=tmp_path / "wt")` returns the **same** worktree path for every role (`:269`). Under v3's concurrent path, three producers receive the same worktree; the spawner mock doesn't care, so this is benign for the test but it does mean the test won't catch a regression where the production code accidentally reused a single worktree path across roles. Cheap fix: have the fake `worktrees.create` produce `tmp_path / role.value` so the per-role isolation is observable in the recorded spawn envs. - - -````yaml -id: 25a72e7b-0832-41 -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_inprocess_plan_brc.py - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py - reason: "\nConcurrency-lens review of tester v1 (commit b2be479f1). The proposal\ - \ adds two test files only \u2014 no production code changes \u2014 so the concurrency\ - \ surface is the test harness itself. I walked the fixture-shared state, the\ - \ per-test isolation, and the executor / mock interaction to confirm the tests\ - \ don't introduce concurrency hazards or mask the v3 coder's concurrent code\ - \ path.\n\n### Verified\n\n- **Shared-state isolation between tests.** The `isolated_pipeline_state`\ - \ fixture (`test_inprocess_plan_brc.py:230-248`) clears the module-level `_TRACKERS`\ - \ / `_PEER_CONSENSUS_TRACKERS` / `_tracker_registry` registry between tests\ - \ so back-to-back runs don't inherit a previously-confirmed tracker. `fake_home`\ - \ (`:216-227`) isolates `$HOME` per test so sentinel writes can't bleed across\ - \ tests. Each test uses a unique pipeline id (`pipeline-plan-brc-spawns`, `pipeline-plan-brc-hitl`,\ - \ etc.) so two tests in the same worker cannot collide on the registry key even\ - \ before the explicit `.clear()`. Under `pytest-xdist` the worker-process boundary\ - \ takes care of the rest.\n\n- **Exercising the real concurrent path.** `_make_fake_bundle`\ - \ returns a `MagicMock`-backed spawner whose `.spawn(...)` is recorded into\ - \ `.call_args_list`. `_run_plan_phase_inner` (coder v3, `orchestrator/substrate/_plan_phase.py:138-150`)\ - \ submits task_planner + risk_analyst to a real `ThreadPoolExecutor(max_workers=2)`,\ - \ so both threads concurrently invoke `bundle.spawner.spawn(...)`. CPython's\ - \ GIL makes `list.append` (the recording mechanism inside `_mock_call`) atomic,\ - \ so the spawn-call ledger does not tear under the concurrent invocation; the\ - \ `_EXPECTED_PRODUCERS - plan_spawned` assertion at `test_inprocess_plan_brc.py:428-433`\ - \ therefore reliably catches a missing-role regression even when the executor\ - \ fans out.\n\n- **Background-thread teardown.** Every test's `finally` block\ - \ calls `gen.close()` then `time.sleep(0.2)` / `0.3` to let the heartbeat /\ - \ brc-review / bus-tick daemons unwind. The generator's `_shutdown_background_threads`\ - \ already joins with a 2.0 s timeout, so the sleep is a courtesy flush \u2014\ - \ no leaked daemon thread can poison the next test's tracker because the registry\ - \ is `.clear()`'d before the next test starts. The `short_intervals` fixture\ - \ shrinks the tick intervals to 0.05 s so the tests don't pad to multi-second\ - \ runtimes waiting for the timeouts.\n\n- **No retry storms or off-protocol\ - \ bus emissions.** The fake bundle binds `bundle.bus = InProcessMessageBus()`\ - \ (`:280-282`) rather than a `MagicMock`, so the heartbeat publisher's `bus.add_message(...)`\ - \ lands on a real bus and does not silently swallow type errors \u2014 the v3\ - \ heartbeat-phase fix (`phase=self._current_phase`) flows through correctly\ - \ under this fixture.\n\n- **Adversarial probes target the right concurrency-adjacent\ - \ invariants.** `test_plan_stage_does_not_run_when_operator_rejects_refine`\ - \ (`:647-705`) pins the safety invariant that an unauthorised concurrent producer\ - \ dispatch cannot fire on a non-`approve_continue` answer. `test_plan_stage_carries_phase_env_var_to_producers`\ - \ (`:825-887`) confirms `EGG_PHASE=plan` is on every plan-producer spawn env\ - \ \u2014 important because v3 dropped the concurrent-path sentinel write and\ - \ now relies on env propagation as the load-bearing role-routing channel.\n\n\ - ### Drift note (non-blocking)\n\nThe commit body and module docstring describe\ - \ the implementation as \"3 producers concurrent\" (`:118-125`). Coder v3 actually\ - \ runs **architect synchronously first** and then fans out task_planner + risk_analyst\ - \ (2-way concurrent) \u2014 the producer count is still 3 but only 2 are concurrent.\ - \ The test's spawn-set assertion (`_EXPECTED_PRODUCERS - plan_spawned`) doesn't\ - \ pin ordering and still passes against v3's architect-first sequencing, but\ - \ the docstring text would mislead a future maintainer reading the comments.\ - \ Drop a sentence noting \"architect runs synchronously first, then task_planner\ - \ + risk_analyst fan out via ThreadPoolExecutor(max_workers=2)\" so the test's\ - \ narrative matches the v3 implementation.\n\n### Non-blocking\n\n- `bundle.worktrees.create\ - \ = MagicMock(return_value=tmp_path / \"wt\")` returns the **same** worktree\ - \ path for every role (`:269`). Under v3's concurrent path, three producers\ - \ receive the same worktree; the spawner mock doesn't care, so this is benign\ - \ for the test but it does mean the test won't catch a regression where the\ - \ production code accidentally reused a single worktree path across roles. Cheap\ - \ fix: have the fake `worktrees.create` produce `tmp_path / role.value` so the\ - \ per-role isolation is observable in the recorded spawn envs.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:30:44Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6ebf8772-cb93-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:44Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: d3974fba-b79f-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:44.730210+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:48Z] reviewer_concurrency (CONSENSUS_CONFIRMED): Confirmed by reviewer_concurrency - -````yaml -id: a806ee31-ad63-4a -phase: implement -metadata: - consensus_reached: false - slice_id: slice-2 -```` - -### [2026-05-19T20:30:48Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 9df38d4b-a241-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:49Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 773ec3dd-259a-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:49.767497+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:49Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d1d0a5c9-7806-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:50Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e4834915-f712-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:50.751767+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:53Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 1b9beda5-b692-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:53.905702+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:57Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 9cc09dd6-39ae-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:57.063180+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:57Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: fe4f54ae-521a-47 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:57.727546+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:30:57Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 32058ab5-b810-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:30:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: a25f65df-4dac-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:29:58.487845+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:31:06Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: c9b36dad-01ea-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:31:06.346909+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:31:50Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 6baab241-e0af-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:50.751767+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:31:54Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 8a76447f-adfa-43 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:53.905702+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:31:57Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 28206c32-956a-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:30:57.063180+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:31:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: a36f90a5-984f-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:29:58.487845+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:32:06Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 3873d0e8-aafb-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:31:06.346909+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:32:29Z] reviewer_code → tester (CONSENSUS_ACK): ACK from reviewer_code for tester - - -Reviewed the tester's two new test files for TASK-2-4 (plan-phase BRC E2E integration test) and TASK-2-6 (R2-pass no-op regression guard for the restrictions handler). Verified test structure, assertion semantics, fixture isolation, and alignment with the coder's v2 implementation surface. - -### File-by-file analysis - -**integration_tests/regression/test_inprocess_plan_brc.py** (new, 797 lines) — Seven test cases covering task-2-4's four acceptance bullets plus adversarial probing: - -1. *`test_plan_stage_spawns_three_producers_and_one_reviewer`* (lines 286-349) — Drives the generator past the refine gate via `_drive_past_refine_gate`, then walks `bundle.spawner.spawn.call_args_list` to assert architect / task_planner / risk_analyst / reviewer_plan were all dispatched. Excludes the refiner spawn before comparing against the plan-team frozenset (`_EXPECTED_PRODUCERS` / `_EXPECTED_REVIEWERS` at lines 107-110). Bullet 3 satisfied. - -2. *`test_plan_stage_yields_hitl_decision_with_expected_fields`* (lines 358-448) — Asserts the yielded `HITLDecision` has non-empty `id`, `question`, `options`, `decision_type ∈ {phase_gate, choice}`, and `phase == "plan"`. Tolerates both attribute and dict access shapes and both enum and string forms for `phase`. The coder's `_build_plan_gate_decision` (`in_process.py:660-720`) returns `phase="plan"` and `decision_type="phase_gate"`, so the assertions hold. Bullet 4 satisfied. - -3. *`test_plan_stage_reaches_consensus_confirmed_for_each_producer`* (lines 457-543) — Drives past the refine gate, pulls the `_InProcessOrchestrator` runner out of the live generator's frame (via `_runner_from_gen`), reads `runner._plan_tracker.evaluate()`, and asserts every plan-team role (architect / task_planner / risk_analyst / reviewer_plan) has `confirmed=True` in the `agents` map AND `is_complete=True` on the snapshot. The coder's v2 sets `runner._plan_tracker = tracker` at `_plan_phase.py:118` and the evaluate-shape matches `peer_consensus.py:1590-1601` (`agents[role]["confirmed"]` + top-level `is_complete`). Bullet 2 satisfied. - -4. *`test_plan_stage_does_not_run_when_operator_rejects_refine`* (lines 557-615) — Adversarial probe: sending `"stop"` to the refine gate terminates the generator with `StopIteration(value=str)` (the artifact path) and no plan-team roles are spawned. Guards against a regression that fans into plan on any non-continue answer. The coder's check at `in_process.py:226-227` (`_answer_continues_past_refine` returns False for "stop") satisfies this — `return str(artifact_path)` fires before `_run_plan_phase` is called. - -5. *`test_plan_stage_does_not_spawn_implement_phase_roles`* (lines 624-675) — Adversarial probe: a misrouted `_PHASE_ROLES["implement"]` lookup would spawn coder / tester / documenter / reviewer_* roles. The forbidden set covers all eight implement-team roles. Negative invariant — implement-team roles must NOT appear in `bundle.spawner.spawn.call_args_list` after the plan stage runs. Good defense against phase-dispatch off-by-one. - -6. *`test_plan_stage_does_not_invoke_refiner_a_second_time`* (lines 684-726) — Adversarial probe: counts refiner spawn invocations and asserts exactly one (the refine-stage spawn). Guards against a regression that re-includes REFINER in the plan-phase producer set. Sensible single-refiner-spawn invariant. - -7. *`test_plan_stage_carries_phase_env_var_to_producers`* (lines 735-797) — Adversarial probe: every plan-phase spawn's env must set `EGG_PHASE=plan`. Walks the spawn calls (excluding REFINER), pulls the env arg (positional `args[2]` or `kwargs["env"]`), and asserts `env["EGG_PHASE"] == "plan"`. The coder's v2 sets this at `_plan_phase.py:464` (producers) and `:527` (reviewer). Good env-propagation contract guard. - -**Fixtures** (lines 117-196): - -- *`short_intervals`* — Shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL` / `_BUS_TICK_INTERVAL` to 0.05s so the background-thread loops don't drag the test wall-clock. Module-level constant monkeypatch — correct technique. -- *`fake_home`* — Redirects `$HOME` to a tmp dir so the sentinel write at `_write_active_role_sentinel` (still called from `_spawn_refiner` and `_spawn_plan_reviewer`) doesn't pollute the developer's actual `~/.claude/`. Good test hygiene. -- *`isolated_pipeline_state`* — Clears `peer_consensus._TRACKERS` (or sibling names) between tests so back-to-back tests with the same pipeline id don't inherit confirmed state. Defensive against the module-level singleton at `peer_consensus.py:create_peer_consensus_tracker`. -- *`_make_fake_bundle`* — MagicMock spawner returning `MagicMock(exit_code=0, commit_sha="0"*40, stdout="ok")` for every spawn. Backs `bundle.bus` with a real `InProcessMessageBus` so the heartbeat / bus-tick background loops don't trip on MagicMock-returned garbage. - -The `_runner_from_gen` helper at lines 261-270 reaches into `gen.gi_frame.f_locals['self']` to access the runner. Generator-frame introspection is brittle but justified — it's the only way to read `_plan_tracker.evaluate()` for the consensus assertion without adding a leaky public accessor. Acceptable test technique with a clear docstring. - -The skip guards (lines 278-284, 351-356, 451-456, 551-555, 618-622, 678-683, 729-734) all use `_has_plan_stage()` which checks for `_run_plan_phase` or peer names. The coder's v2 has `_run_plan_phase` so the skip never fires. - -**tests/sandbox/egg_agent_tools/test_restrictions_validator.py** (new, 324 lines) — TASK-2-6's R2-pass no-op regression guard. Verifies the slice-2 work did NOT silently extend the in-sandbox handler with R2-fail-only enforcement logic and accidentally change the response shape for the R2-pass path: - -1. *`test_coder_in_allow_list_response_shape_stable`* (lines 100-139) — Coder writing `orchestrator/foo.py` → `can_write=True`, response shape equals `_SINGLE_PATH_FIELDS = {"ok", "role", "path", "can_write", "reason", "alternative_role"}`. `alternative_role=None` on the allowed path. - -2. *`test_tester_in_allow_list_response_shape_stable`* (lines 142-160) — Tester under `tests/sandbox/egg_agent_tools/test_x.py` → `can_write=True`. - -3. *`test_documenter_in_allow_list_response_shape_stable`* (lines 163-173) — Documenter writing `docs/foo.md` → `can_write=True`. - -4. *`test_coder_cannot_write_tester_path_denial_shape_stable`* (lines 182-206) — Cross-role denial: coder writing `tests/sandbox/...` → `can_write=False`, `reason` references `shared/egg_restrictions/patterns.py`, `alternative_role="tester"`. Pins the denial-shape contract that impasse-routing relies on. - -5. *`test_tester_cannot_write_orchestrator_path_denial_shape_stable`* (lines 209-224) — Cross-role denial: tester writing `orchestrator/foo.py` → `can_write=False`, `alternative_role="coder"`. - -6. *`test_no_new_validator_symbol_introduced_in_r2_pass_slice`* (lines 233-259) — Asserts `validate_write_target` (and similar) is NOT in `restrictions` namespace. The R2-pass no-op invariant from TASK-2-5's contingent description. I verified via `git diff origin/main...slice-2 -- sandbox/egg_agent_tools/handlers/restrictions.py` that the slice-2 diff did NOT modify that file — the test holds. - -7. *`test_missing_path_raises_handler_error`* (lines 268-279) — Defensive surface: calling without `path` raises `HandlerError` with `'path' is required` message. - -8. *`test_unknown_role_raises_handler_error`* (lines 282-297) — Defensive surface: unknown role → `HandlerError`, not a permissive `can_write=True`. - -9. *`test_list_path_returns_per_path_results`* (lines 300-324) — Bulk-check surface: list `path` returns `results` array with per-path entries. - -The R2-pass no-op claim is well-supported: TASK-2-5's contract description ("If R2 = pass, this task is a no-op (close with note)") is verified through both shape stability (existing behaviour preserved) and the negative invariant (no new validator symbol). The fail-loop case (R2 = pass + a sneaky new symbol) is the right place to catch a documentation-vs-code drift. - -### Strict-mode tester attestation - -The tester role under BRC strict-mode requires `attestation.tests_run > 0 AND checks_passed non-empty` OR `tests_execution_blocked` OR `no_test_changes_needed` (#2431). The orchestrator accepted the propose at v1, so the attestation passed validation gateway-side. The commit message describes manual test execution intent but doesn't reproduce the attestation payload — I'm trusting the gateway's validation rather than inspecting the attestation directly (the `delphi_redacted=true` flag in the message metadata strips the attestation from peer-visible reads). - -### Non-blocking - -- **integration_tests/regression/test_inprocess_plan_brc.py:519-526** — `agents.get(role, {}) or {}` defaults missing roles to `{}`, then `.get("confirmed", False)` defaults missing flag to `False`. A regression where a role is missing entirely from `agents` would surface as `not_confirmed=[role]` — clear failure mode. Worth a small clarifying assertion that the role *is* in `agents` separately from the `confirmed` flag check (avoids ambiguity between "role missing" and "role present but not confirmed"), but the current shape is sufficiently diagnostic. - -- **integration_tests/regression/test_inprocess_plan_brc.py:399-446** — The test tolerates both attribute and dict access (`getattr(plan_hitl, "id", None) or plan_hitl.get("id", None)`). The current `HITLDecision` is a dataclass with attributes; if the type ever migrates to a dict envelope the test still works. Reasonable forward-compat, but worth noting that the chained `or` swallows the empty-string case (an `id=""` attribute would fall through to the dict branch and fail). Not blocking — empty-string `id` would itself be a bug. - -- **integration_tests/regression/test_inprocess_plan_brc.py:261-270 (`_runner_from_gen`)** — Reaches into `gen.gi_frame.f_locals["self"]`. CPython-specific; PyPy or other interpreters with different generator-frame layouts could break this. Acceptable for now since the rest of the codebase pins CPython, but a follow-up could expose `_plan_tracker` via a public test hook. - -- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259** — `test_no_new_validator_symbol_introduced_in_r2_pass_slice` only checks `validate_write_target`. If TASK-2-5 had landed with a different symbol name (e.g., `enforce_write_policy` or `check_write_target`), this test would pass silently. Consider expanding the `forbidden` set to a more comprehensive list of likely names, or — better — a wildcard check on any new public symbol added to the module relative to `origin/main`. Not blocking because the actual codebase has no such symbol and the slice-2 diff doesn't touch restrictions.py. - -- **integration_tests/regression/test_inprocess_plan_brc.py** — No test exercises the **NACK path** explicitly. With the v2 coder's verdict-JSON parsing (`_apply_reviewer_verdicts`), a verdict file with `per_producer: {task_planner: {verdict: "NACK", reason: "..."}}` should drive the tracker to NACK that producer. The current tests only exercise the optimistic-ACK path (no verdict file, exit_code=0). A follow-up test that writes a fake verdict JSON before the reviewer's "spawn" completes would close the NACK-path regression gap. Not blocking for slice-2 because the acceptance criteria don't name this, but worth filing for slice-3 / hardening. - -No security, correctness, or robustness issues. Tester ACKed. - - -````yaml -id: d18543b1-3f07-4b -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_inprocess_plan_brc.py - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py - reason: "\nReviewed the tester's two new test files for TASK-2-4 (plan-phase BRC\ - \ E2E integration test) and TASK-2-6 (R2-pass no-op regression guard for the\ - \ restrictions handler). Verified test structure, assertion semantics, fixture\ - \ isolation, and alignment with the coder's v2 implementation surface.\n\n###\ - \ File-by-file analysis\n\n**integration_tests/regression/test_inprocess_plan_brc.py**\ - \ (new, 797 lines) \u2014 Seven test cases covering task-2-4's four acceptance\ - \ bullets plus adversarial probing:\n\n1. *`test_plan_stage_spawns_three_producers_and_one_reviewer`*\ - \ (lines 286-349) \u2014 Drives the generator past the refine gate via `_drive_past_refine_gate`,\ - \ then walks `bundle.spawner.spawn.call_args_list` to assert architect / task_planner\ - \ / risk_analyst / reviewer_plan were all dispatched. Excludes the refiner spawn\ - \ before comparing against the plan-team frozenset (`_EXPECTED_PRODUCERS` /\ - \ `_EXPECTED_REVIEWERS` at lines 107-110). Bullet 3 satisfied.\n\n2. *`test_plan_stage_yields_hitl_decision_with_expected_fields`*\ - \ (lines 358-448) \u2014 Asserts the yielded `HITLDecision` has non-empty `id`,\ - \ `question`, `options`, `decision_type \u2208 {phase_gate, choice}`, and `phase\ - \ == \"plan\"`. Tolerates both attribute and dict access shapes and both enum\ - \ and string forms for `phase`. The coder's `_build_plan_gate_decision` (`in_process.py:660-720`)\ - \ returns `phase=\"plan\"` and `decision_type=\"phase_gate\"`, so the assertions\ - \ hold. Bullet 4 satisfied.\n\n3. *`test_plan_stage_reaches_consensus_confirmed_for_each_producer`*\ - \ (lines 457-543) \u2014 Drives past the refine gate, pulls the `_InProcessOrchestrator`\ - \ runner out of the live generator's frame (via `_runner_from_gen`), reads `runner._plan_tracker.evaluate()`,\ - \ and asserts every plan-team role (architect / task_planner / risk_analyst\ - \ / reviewer_plan) has `confirmed=True` in the `agents` map AND `is_complete=True`\ - \ on the snapshot. The coder's v2 sets `runner._plan_tracker = tracker` at `_plan_phase.py:118`\ - \ and the evaluate-shape matches `peer_consensus.py:1590-1601` (`agents[role][\"\ - confirmed\"]` + top-level `is_complete`). Bullet 2 satisfied.\n\n4. *`test_plan_stage_does_not_run_when_operator_rejects_refine`*\ - \ (lines 557-615) \u2014 Adversarial probe: sending `\"stop\"` to the refine\ - \ gate terminates the generator with `StopIteration(value=str)` (the artifact\ - \ path) and no plan-team roles are spawned. Guards against a regression that\ - \ fans into plan on any non-continue answer. The coder's check at `in_process.py:226-227`\ - \ (`_answer_continues_past_refine` returns False for \"stop\") satisfies this\ - \ \u2014 `return str(artifact_path)` fires before `_run_plan_phase` is called.\n\ - \n5. *`test_plan_stage_does_not_spawn_implement_phase_roles`* (lines 624-675)\ - \ \u2014 Adversarial probe: a misrouted `_PHASE_ROLES[\"implement\"]` lookup\ - \ would spawn coder / tester / documenter / reviewer_* roles. The forbidden\ - \ set covers all eight implement-team roles. Negative invariant \u2014 implement-team\ - \ roles must NOT appear in `bundle.spawner.spawn.call_args_list` after the plan\ - \ stage runs. Good defense against phase-dispatch off-by-one.\n\n6. *`test_plan_stage_does_not_invoke_refiner_a_second_time`*\ - \ (lines 684-726) \u2014 Adversarial probe: counts refiner spawn invocations\ - \ and asserts exactly one (the refine-stage spawn). Guards against a regression\ - \ that re-includes REFINER in the plan-phase producer set. Sensible single-refiner-spawn\ - \ invariant.\n\n7. *`test_plan_stage_carries_phase_env_var_to_producers`* (lines\ - \ 735-797) \u2014 Adversarial probe: every plan-phase spawn's env must set `EGG_PHASE=plan`.\ - \ Walks the spawn calls (excluding REFINER), pulls the env arg (positional `args[2]`\ - \ or `kwargs[\"env\"]`), and asserts `env[\"EGG_PHASE\"] == \"plan\"`. The coder's\ - \ v2 sets this at `_plan_phase.py:464` (producers) and `:527` (reviewer). Good\ - \ env-propagation contract guard.\n\n**Fixtures** (lines 117-196):\n\n- *`short_intervals`*\ - \ \u2014 Shrinks `_HEARTBEAT_INTERVAL` / `_BRC_REVIEW_INTERVAL` / `_BUS_TICK_INTERVAL`\ - \ to 0.05s so the background-thread loops don't drag the test wall-clock. Module-level\ - \ constant monkeypatch \u2014 correct technique.\n- *`fake_home`* \u2014 Redirects\ - \ `$HOME` to a tmp dir so the sentinel write at `_write_active_role_sentinel`\ - \ (still called from `_spawn_refiner` and `_spawn_plan_reviewer`) doesn't pollute\ - \ the developer's actual `~/.claude/`. Good test hygiene.\n- *`isolated_pipeline_state`*\ - \ \u2014 Clears `peer_consensus._TRACKERS` (or sibling names) between tests\ - \ so back-to-back tests with the same pipeline id don't inherit confirmed state.\ - \ Defensive against the module-level singleton at `peer_consensus.py:create_peer_consensus_tracker`.\n\ - - *`_make_fake_bundle`* \u2014 MagicMock spawner returning `MagicMock(exit_code=0,\ - \ commit_sha=\"0\"*40, stdout=\"ok\")` for every spawn. Backs `bundle.bus` with\ - \ a real `InProcessMessageBus` so the heartbeat / bus-tick background loops\ - \ don't trip on MagicMock-returned garbage.\n\nThe `_runner_from_gen` helper\ - \ at lines 261-270 reaches into `gen.gi_frame.f_locals['self']` to access the\ - \ runner. Generator-frame introspection is brittle but justified \u2014 it's\ - \ the only way to read `_plan_tracker.evaluate()` for the consensus assertion\ - \ without adding a leaky public accessor. Acceptable test technique with a clear\ - \ docstring.\n\nThe skip guards (lines 278-284, 351-356, 451-456, 551-555, 618-622,\ - \ 678-683, 729-734) all use `_has_plan_stage()` which checks for `_run_plan_phase`\ - \ or peer names. The coder's v2 has `_run_plan_phase` so the skip never fires.\n\ - \n**tests/sandbox/egg_agent_tools/test_restrictions_validator.py** (new, 324\ - \ lines) \u2014 TASK-2-6's R2-pass no-op regression guard. Verifies the slice-2\ - \ work did NOT silently extend the in-sandbox handler with R2-fail-only enforcement\ - \ logic and accidentally change the response shape for the R2-pass path:\n\n\ - 1. *`test_coder_in_allow_list_response_shape_stable`* (lines 100-139) \u2014\ - \ Coder writing `orchestrator/foo.py` \u2192 `can_write=True`, response shape\ - \ equals `_SINGLE_PATH_FIELDS = {\"ok\", \"role\", \"path\", \"can_write\",\ - \ \"reason\", \"alternative_role\"}`. `alternative_role=None` on the allowed\ - \ path.\n\n2. *`test_tester_in_allow_list_response_shape_stable`* (lines 142-160)\ - \ \u2014 Tester under `tests/sandbox/egg_agent_tools/test_x.py` \u2192 `can_write=True`.\n\ - \n3. *`test_documenter_in_allow_list_response_shape_stable`* (lines 163-173)\ - \ \u2014 Documenter writing `docs/foo.md` \u2192 `can_write=True`.\n\n4. *`test_coder_cannot_write_tester_path_denial_shape_stable`*\ - \ (lines 182-206) \u2014 Cross-role denial: coder writing `tests/sandbox/...`\ - \ \u2192 `can_write=False`, `reason` references `shared/egg_restrictions/patterns.py`,\ - \ `alternative_role=\"tester\"`. Pins the denial-shape contract that impasse-routing\ - \ relies on.\n\n5. *`test_tester_cannot_write_orchestrator_path_denial_shape_stable`*\ - \ (lines 209-224) \u2014 Cross-role denial: tester writing `orchestrator/foo.py`\ - \ \u2192 `can_write=False`, `alternative_role=\"coder\"`.\n\n6. *`test_no_new_validator_symbol_introduced_in_r2_pass_slice`*\ - \ (lines 233-259) \u2014 Asserts `validate_write_target` (and similar) is NOT\ - \ in `restrictions` namespace. The R2-pass no-op invariant from TASK-2-5's contingent\ - \ description. I verified via `git diff origin/main...slice-2 -- sandbox/egg_agent_tools/handlers/restrictions.py`\ - \ that the slice-2 diff did NOT modify that file \u2014 the test holds.\n\n\ - 7. *`test_missing_path_raises_handler_error`* (lines 268-279) \u2014 Defensive\ - \ surface: calling without `path` raises `HandlerError` with `'path' is required`\ - \ message.\n\n8. *`test_unknown_role_raises_handler_error`* (lines 282-297)\ - \ \u2014 Defensive surface: unknown role \u2192 `HandlerError`, not a permissive\ - \ `can_write=True`.\n\n9. *`test_list_path_returns_per_path_results`* (lines\ - \ 300-324) \u2014 Bulk-check surface: list `path` returns `results` array with\ - \ per-path entries.\n\nThe R2-pass no-op claim is well-supported: TASK-2-5's\ - \ contract description (\"If R2 = pass, this task is a no-op (close with note)\"\ - ) is verified through both shape stability (existing behaviour preserved) and\ - \ the negative invariant (no new validator symbol). The fail-loop case (R2 =\ - \ pass + a sneaky new symbol) is the right place to catch a documentation-vs-code\ - \ drift.\n\n### Strict-mode tester attestation\n\nThe tester role under BRC\ - \ strict-mode requires `attestation.tests_run > 0 AND checks_passed non-empty`\ - \ OR `tests_execution_blocked` OR `no_test_changes_needed` (#2431). The orchestrator\ - \ accepted the propose at v1, so the attestation passed validation gateway-side.\ - \ The commit message describes manual test execution intent but doesn't reproduce\ - \ the attestation payload \u2014 I'm trusting the gateway's validation rather\ - \ than inspecting the attestation directly (the `delphi_redacted=true` flag\ - \ in the message metadata strips the attestation from peer-visible reads).\n\ - \n### Non-blocking\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:519-526**\ - \ \u2014 `agents.get(role, {}) or {}` defaults missing roles to `{}`, then `.get(\"\ - confirmed\", False)` defaults missing flag to `False`. A regression where a\ - \ role is missing entirely from `agents` would surface as `not_confirmed=[role]`\ - \ \u2014 clear failure mode. Worth a small clarifying assertion that the role\ - \ *is* in `agents` separately from the `confirmed` flag check (avoids ambiguity\ - \ between \"role missing\" and \"role present but not confirmed\"), but the\ - \ current shape is sufficiently diagnostic.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:399-446**\ - \ \u2014 The test tolerates both attribute and dict access (`getattr(plan_hitl,\ - \ \"id\", None) or plan_hitl.get(\"id\", None)`). The current `HITLDecision`\ - \ is a dataclass with attributes; if the type ever migrates to a dict envelope\ - \ the test still works. Reasonable forward-compat, but worth noting that the\ - \ chained `or` swallows the empty-string case (an `id=\"\"` attribute would\ - \ fall through to the dict branch and fail). Not blocking \u2014 empty-string\ - \ `id` would itself be a bug.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py:261-270\ - \ (`_runner_from_gen`)** \u2014 Reaches into `gen.gi_frame.f_locals[\"self\"\ - ]`. CPython-specific; PyPy or other interpreters with different generator-frame\ - \ layouts could break this. Acceptable for now since the rest of the codebase\ - \ pins CPython, but a follow-up could expose `_plan_tracker` via a public test\ - \ hook.\n\n- **tests/sandbox/egg_agent_tools/test_restrictions_validator.py:233-259**\ - \ \u2014 `test_no_new_validator_symbol_introduced_in_r2_pass_slice` only checks\ - \ `validate_write_target`. If TASK-2-5 had landed with a different symbol name\ - \ (e.g., `enforce_write_policy` or `check_write_target`), this test would pass\ - \ silently. Consider expanding the `forbidden` set to a more comprehensive list\ - \ of likely names, or \u2014 better \u2014 a wildcard check on any new public\ - \ symbol added to the module relative to `origin/main`. Not blocking because\ - \ the actual codebase has no such symbol and the slice-2 diff doesn't touch\ - \ restrictions.py.\n\n- **integration_tests/regression/test_inprocess_plan_brc.py**\ - \ \u2014 No test exercises the **NACK path** explicitly. With the v2 coder's\ - \ verdict-JSON parsing (`_apply_reviewer_verdicts`), a verdict file with `per_producer:\ - \ {task_planner: {verdict: \"NACK\", reason: \"...\"}}` should drive the tracker\ - \ to NACK that producer. The current tests only exercise the optimistic-ACK\ - \ path (no verdict file, exit_code=0). A follow-up test that writes a fake verdict\ - \ JSON before the reviewer's \"spawn\" completes would close the NACK-path regression\ - \ gap. Not blocking for slice-2 because the acceptance criteria don't name this,\ - \ but worth filing for slice-3 / hardening.\n\nNo security, correctness, or\ - \ robustness issues. Tester ACKed.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:32:29Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 4db07212-8946-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:32:34Z] reviewer_code (CONSENSUS_CONFIRMED): Confirmed by reviewer_code - -````yaml -id: e56ddae1-811f-4d -phase: implement -metadata: - consensus_reached: false - slice_id: slice-2 -```` - -### [2026-05-19T20:32:34Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 273852cd-cab7-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:32:34Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a04fe866-1607-40 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:32:34Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 45f10d71-5314-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:32:35Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 07997f3a-3e59-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:32:35.790039+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:32:41Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 95b907f9-1d90-41 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:32:41.073146+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:32:41Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: b275cd75-03ea-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:32:41.110634+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:32:41Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 3a7394fc-e955-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:32:41.695415+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:32:42Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a4d4626c-4b7b-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:32:42.397892+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:32:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3f4f4b2d-f534-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:29:58.487845+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:10Z] reviewer_code_holistic → tester (CONSENSUS_ACK): ACK from reviewer_code_holistic for tester - - -Holistic ACK on tester slice-2 v1 (commit b2be479f1). Ran all four mandatory passes against the new test surface; nothing blocking — the tests cover the four AC bullets for TASK-2-4 (3-producer + 1-reviewer spawn, plan-HITL yield with phase=plan, BRC consensus per edge, no-plan-when-operator-stops) plus four useful adversarial regressions (no implement-phase role leak, refiner spawned exactly once, EGG_PHASE=plan propagation, R2-pass restrictions-handler shape stability). TASK-2-6 lands as the documented no-op regression guard pinning the gateway-shape fields exactly so a future cq-6 option-2 fallback can't quietly mutate the response shape without flipping this suite. Holistic asymmetries I checked: - -- **Pass 1 (end-to-end primary use case):** `_drive_past_refine_gate` walks preflight → refiner → refine HITL gate → plan-phase BRC → plan-HITL yield, the actual user path. The fake spawner observes every spawn so the assertion is on the real call graph, not a mock surrogate. Good. -- **Pass 2 (doc ↔ code symmetry):** `test_plan_stage_carries_phase_env_var_to_producers` pins the rubric-promised `EGG_PHASE=plan` env-propagation contract (architect / task_planner / risk_analyst rubrics all reference plan-phase substrate context). `test_plan_stage_yields_hitl_decision_with_expected_fields` pins `phase="plan"` and `decision_type ∈ {phase_gate, choice}` to match the documenter's HITL surface promise. -- **Pass 3 (synthetic key / sentinel):** No new sentinels introduced. The `fake_home` fixture isolates the `_write_active_role_sentinel` writes per-test so the developer's actual `$HOME/.claude/egg-active-role.json` is not polluted — good defence on a sentinel I flagged on the coder side. The `isolated_pipeline_state` fixture clears the module-level `PeerConsensusTracker` registry so back-to-back tests don't inherit confirmed state — necessary correctness given how `get_peer_consensus_tracker` caches per-pipeline trackers. -- **Pass 4 (silent fallback):** `test_plan_stage_does_not_run_when_operator_rejects_refine` and `test_plan_stage_does_not_spawn_implement_phase_roles` pin the negative invariants. Both are exactly the regressions a future reviewer would miss in a single-file diff. `test_plan_stage_does_not_invoke_refiner_a_second_time` closes the off-by-one role-iteration loophole I would have asked for explicitly. - -### Non-blocking - -- **Architect-first ordering not pinned.** The docstring on `test_plan_stage_spawns_three_producers_and_one_reviewer` (lines 297–299) reads "The producer ordering is not pinned — they run concurrently via `ThreadPoolExecutor`". That's correct for coder v1 but stale after coder v2/v3 (architect spawns synchronously first; `task_planner` + `risk_analyst` fan out in `ThreadPoolExecutor(max_workers=2)`). When the coder reaches v4, add a test that pins the new invariant: build a fake spawner that records each call's wall-clock timestamp (or a deterministic counter), drive the generator past the refine gate, and assert `architect`'s call_args index < min(task_planner_index, risk_analyst_index). Otherwise a future regression that flips back to all-concurrent silently passes this suite (my v1 NACK to the coder hinged on exactly that, and the rubrics in `agents/architect.md:23` + `agents/task_planner.md:23` make architect-first part of the doc-claimed contract). -- **Reviewer verdict-JSON parsing path not exercised.** `_make_fake_bundle` returns a fixed `exit_code=0` AgentResult for every role and never writes the verdict JSON the coder's `_apply_reviewer_verdicts` parses. Per my open v3 NACK on the coder, the documented rubric schema (single-verdict JSON) and the coder's parser schema (`per_producer` wrapper) currently disagree — a tester-side fixture that writes the per_producer JSON shape (or whichever schema the coder + documenter converge on in v4) and asserts the tracker reaches the expected ACK / NACK / mixed verdict states would have caught that asymmetry on first contact. Worth adding to slice-2 tester v2. -- **No test for the v3 `_current_phase` heartbeat-phase invariant.** The coder's v2/v3 fix flips `runner._current_phase = "plan"` so HEARTBEAT messages carry the right phase across the refine → plan transition. A test that drives the generator into the plan stage, then reads the bus messages and asserts at least one HEARTBEAT with `phase="plan"` lands, would pin that contract. Today the only proof is the coder's commit body, not a regression guard. -- **`_has_plan_stage()` accepts five candidate method names** (line 215–225). That makes the test resilient to a coder rename but lets a downstream slice rename the method without anyone noticing. Once the dust settles on v4+, pin the canonical name (`_run_plan_phase`) and drop the wildcard. - -The four mandatory holistic passes returned no blocking findings against the test surface. ACKing v1 so the tester can re-propose v2 once coder v4 lands with the verdict-schema fix. - - -````yaml -id: 7ab0d7a1-ed51-48 -phase: implement -metadata: - payload: - artifact_references: - - integration_tests/regression/test_inprocess_plan_brc.py - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py - reason: "\nHolistic ACK on tester slice-2 v1 (commit b2be479f1). Ran all four\ - \ mandatory passes against the new test surface; nothing blocking \u2014 the\ - \ tests cover the four AC bullets for TASK-2-4 (3-producer + 1-reviewer spawn,\ - \ plan-HITL yield with phase=plan, BRC consensus per edge, no-plan-when-operator-stops)\ - \ plus four useful adversarial regressions (no implement-phase role leak, refiner\ - \ spawned exactly once, EGG_PHASE=plan propagation, R2-pass restrictions-handler\ - \ shape stability). TASK-2-6 lands as the documented no-op regression guard\ - \ pinning the gateway-shape fields exactly so a future cq-6 option-2 fallback\ - \ can't quietly mutate the response shape without flipping this suite. Holistic\ - \ asymmetries I checked:\n\n- **Pass 1 (end-to-end primary use case):** `_drive_past_refine_gate`\ - \ walks preflight \u2192 refiner \u2192 refine HITL gate \u2192 plan-phase BRC\ - \ \u2192 plan-HITL yield, the actual user path. The fake spawner observes every\ - \ spawn so the assertion is on the real call graph, not a mock surrogate. Good.\n\ - - **Pass 2 (doc \u2194 code symmetry):** `test_plan_stage_carries_phase_env_var_to_producers`\ - \ pins the rubric-promised `EGG_PHASE=plan` env-propagation contract (architect\ - \ / task_planner / risk_analyst rubrics all reference plan-phase substrate context).\ - \ `test_plan_stage_yields_hitl_decision_with_expected_fields` pins `phase=\"\ - plan\"` and `decision_type \u2208 {phase_gate, choice}` to match the documenter's\ - \ HITL surface promise.\n- **Pass 3 (synthetic key / sentinel):** No new sentinels\ - \ introduced. The `fake_home` fixture isolates the `_write_active_role_sentinel`\ - \ writes per-test so the developer's actual `$HOME/.claude/egg-active-role.json`\ - \ is not polluted \u2014 good defence on a sentinel I flagged on the coder side.\ - \ The `isolated_pipeline_state` fixture clears the module-level `PeerConsensusTracker`\ - \ registry so back-to-back tests don't inherit confirmed state \u2014 necessary\ - \ correctness given how `get_peer_consensus_tracker` caches per-pipeline trackers.\n\ - - **Pass 4 (silent fallback):** `test_plan_stage_does_not_run_when_operator_rejects_refine`\ - \ and `test_plan_stage_does_not_spawn_implement_phase_roles` pin the negative\ - \ invariants. Both are exactly the regressions a future reviewer would miss\ - \ in a single-file diff. `test_plan_stage_does_not_invoke_refiner_a_second_time`\ - \ closes the off-by-one role-iteration loophole I would have asked for explicitly.\n\ - \n### Non-blocking\n\n- **Architect-first ordering not pinned.** The docstring\ - \ on `test_plan_stage_spawns_three_producers_and_one_reviewer` (lines 297\u2013\ - 299) reads \"The producer ordering is not pinned \u2014 they run concurrently\ - \ via `ThreadPoolExecutor`\". That's correct for coder v1 but stale after coder\ - \ v2/v3 (architect spawns synchronously first; `task_planner` + `risk_analyst`\ - \ fan out in `ThreadPoolExecutor(max_workers=2)`). When the coder reaches v4,\ - \ add a test that pins the new invariant: build a fake spawner that records\ - \ each call's wall-clock timestamp (or a deterministic counter), drive the generator\ - \ past the refine gate, and assert `architect`'s call_args index < min(task_planner_index,\ - \ risk_analyst_index). Otherwise a future regression that flips back to all-concurrent\ - \ silently passes this suite (my v1 NACK to the coder hinged on exactly that,\ - \ and the rubrics in `agents/architect.md:23` + `agents/task_planner.md:23`\ - \ make architect-first part of the doc-claimed contract).\n- **Reviewer verdict-JSON\ - \ parsing path not exercised.** `_make_fake_bundle` returns a fixed `exit_code=0`\ - \ AgentResult for every role and never writes the verdict JSON the coder's `_apply_reviewer_verdicts`\ - \ parses. Per my open v3 NACK on the coder, the documented rubric schema (single-verdict\ - \ JSON) and the coder's parser schema (`per_producer` wrapper) currently disagree\ - \ \u2014 a tester-side fixture that writes the per_producer JSON shape (or whichever\ - \ schema the coder + documenter converge on in v4) and asserts the tracker reaches\ - \ the expected ACK / NACK / mixed verdict states would have caught that asymmetry\ - \ on first contact. Worth adding to slice-2 tester v2.\n- **No test for the\ - \ v3 `_current_phase` heartbeat-phase invariant.** The coder's v2/v3 fix flips\ - \ `runner._current_phase = \"plan\"` so HEARTBEAT messages carry the right phase\ - \ across the refine \u2192 plan transition. A test that drives the generator\ - \ into the plan stage, then reads the bus messages and asserts at least one\ - \ HEARTBEAT with `phase=\"plan\"` lands, would pin that contract. Today the\ - \ only proof is the coder's commit body, not a regression guard.\n- **`_has_plan_stage()`\ - \ accepts five candidate method names** (line 215\u2013225). That makes the\ - \ test resilient to a coder rename but lets a downstream slice rename the method\ - \ without anyone noticing. Once the dust settles on v4+, pin the canonical name\ - \ (`_run_plan_phase`) and drop the wildcard.\n\nThe four mandatory holistic\ - \ passes returned no blocking findings against the test surface. ACKing v1 so\ - \ the tester can re-propose v2 once coder v4 lands with the verdict-schema fix.\n" - ack_version: 1 - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:33:10Z] orchestrator → tester (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: ffe9fc42-7041-4f -phase: implement -metadata: - ready_to_confirm: true - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:33:10Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ab55bb15-bc01-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:14Z] tester (CONSENSUS_CONFIRMED): Confirmed by tester - -````yaml -id: 316d896c-1da8-42 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-2 -```` - -### [2026-05-19T20:33:14Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 5b4dcdbe-3fa9-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:14Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 56e5e4e4-23f2-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:14Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 977ae1e7-9e54-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:15Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: d09ccadb-d24a-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:19Z] reviewer_code_holistic (CONSENSUS_CONFIRMED): Confirmed by reviewer_code_holistic (pending_acks) - -Reviewer reviewer_code_holistic cannot confirm: unresolved NACKs. Wait for these producers to re-propose before confirming: ['coder'] - -````yaml -id: 0570e233-193c-48 -phase: implement -metadata: - pending_acks: true - slice_id: slice-2 -```` - -### [2026-05-19T20:33:19Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 798d1efc-43cd-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:19.617388+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:19Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: cef36e5b-60cb-4f -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:19.661083+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:19Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c40e7f11-9f23-41 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:19Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 32c58e2a-3689-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:20Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: df817afd-fa65-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:20.190449+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:20Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: e08fdba4-ddcd-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:20Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 0d641223-2d6d-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:20.580514+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:20Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: e3ce6884-a9a9-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:23Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,CONSENSUS_PROPOSE,OVERSEER_ALERT - -````yaml -id: 35b96164-9686-4b -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:23.705367+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:23Z] tester (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 1843a281-b946-4e -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:24Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 8f3f3a9d-a1b3-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:24.404584+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:24Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: dd7ad53d-5980-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:33:25Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 541b9c3c-2430-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:25.103637+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:26Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 27bd0f66-027b-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:26.649587+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:27Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5963cac2-1194-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:27.941195+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:28Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: f60feb50-d820-49 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:28.076954+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:51Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED - -````yaml -id: 5ea103bb-a31e-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:33:30.025166+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:33:58Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5321e04b-7a8e-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:29:58.487845+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] coder (CONSENSUS_PROPOSE): Proposal from coder - -Slice-2 coder v4 — addresses reviewer_code_holistic v3 NACK blocker H3 (rubric/code verdict-JSON schema mismatch). Commit ecd8336b7. - -H3 fix: `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default single-verdict shape AND the per_producer wrapper. When the verdict JSON's top-level `verdict` is "ACK" or "NACK" (the rubric-documented shape — plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:57-80), the verdict is broadcast to every plan producer edge. ACK acks all three, NACK nacks all three with the rubric's top-level `feedback` blob propagated as the per-edge reason (synthetic placeholder fires if feedback is empty so the tracker's NACK guard doesn't reject the payload). The per_producer wrapper still takes precedence when present and well-formed — reviewers that want explicit edge granularity opt in. The function takes an optional `plan_producers` kwarg so the orchestrator can broadcast across the right role set; the `_read_plan_reviewer_verdicts` class method delegate propagates the kwarg unchanged so tester-side tests keep their access pattern. - -Why broadcast (option (c)) rather than option (a) per-edge spawn: the rubric's "Verdict JSON shape" section documents a single top-level verdict object as the canonical shape. The "ACK only if every criterion passes; NACK if any criterion fails" rubric rule is a whole-plan verdict semantic, so the broadcast preserves it. Per-edge granularity is available via the per_producer extension wrapper for reviewers that want it. No documenter coordination needed; the rubric stays as-shipped. - -End-to-end smoke (manual, in-process, MagicMock subagents) on v4: -- Rubric-default single-verdict NACK: tracker NACKs critical edges (architect, task_planner), advisory edge (risk_analyst) confirms, 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: mixed ACK/NACK applied per-edge (existing behaviour). -- Harness-fake path (no verdict file, reviewer exit 0): optimistic ACK preserved so tester's 16 existing tests keep working. -- Fail-closed (no verdict file, reviewer exit non-zero): critical edges NACK'd (existing behaviour). - -Lint + format + file-size checks all green. `_plan_phase.py` is 747 lines; `in_process.py` 1095 lines. - -Carries forward every v1/v2/v3 blocker fix (C1 sentinel race in concurrent producers, C2 heartbeat phase, H1 architect-first ordering, H2 reviewer verdict parsing, T1 ruff format). - -````yaml -id: 2cf80827-429a-42 -phase: implement -metadata: - payload: - summary: "Slice-2 coder v4 \u2014 addresses reviewer_code_holistic v3 NACK blocker\ - \ H3 (rubric/code verdict-JSON schema mismatch). Commit ecd8336b7.\n\nH3 fix:\ - \ `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default single-verdict\ - \ shape AND the per_producer wrapper. When the verdict JSON's top-level `verdict`\ - \ is \"ACK\" or \"NACK\" (the rubric-documented shape \u2014 plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md:57-80),\ - \ the verdict is broadcast to every plan producer edge. ACK acks all three,\ - \ NACK nacks all three with the rubric's top-level `feedback` blob propagated\ - \ as the per-edge reason (synthetic placeholder fires if feedback is empty so\ - \ the tracker's NACK guard doesn't reject the payload). The per_producer wrapper\ - \ still takes precedence when present and well-formed \u2014 reviewers that\ - \ want explicit edge granularity opt in. The function takes an optional `plan_producers`\ - \ kwarg so the orchestrator can broadcast across the right role set; the `_read_plan_reviewer_verdicts`\ - \ class method delegate propagates the kwarg unchanged so tester-side tests\ - \ keep their access pattern.\n\nWhy broadcast (option (c)) rather than option\ - \ (a) per-edge spawn: the rubric's \"Verdict JSON shape\" section documents\ - \ a single top-level verdict object as the canonical shape. The \"ACK only if\ - \ every criterion passes; NACK if any criterion fails\" rubric rule is a whole-plan\ - \ verdict semantic, so the broadcast preserves it. Per-edge granularity is available\ - \ via the per_producer extension wrapper for reviewers that want it. No documenter\ - \ coordination needed; the rubric stays as-shipped.\n\nEnd-to-end smoke (manual,\ - \ in-process, MagicMock subagents) on v4:\n- Rubric-default single-verdict NACK:\ - \ tracker NACKs critical edges (architect, task_planner), advisory edge (risk_analyst)\ - \ confirms, reviewer_plan blocks consensus. is_complete=False; blocking_agents=['architect',\ - \ 'task_planner', 'reviewer_plan'].\n- Rubric-default single-verdict ACK: every\ - \ edge confirmed; is_complete=True.\n- per_producer wrapper: mixed ACK/NACK\ - \ applied per-edge (existing behaviour).\n- Harness-fake path (no verdict file,\ - \ reviewer exit 0): optimistic ACK preserved so tester's 16 existing tests keep\ - \ working.\n- Fail-closed (no verdict file, reviewer exit non-zero): critical\ - \ edges NACK'd (existing behaviour).\n\nLint + format + file-size checks all\ - \ green. `_plan_phase.py` is 747 lines; `in_process.py` 1095 lines.\n\nCarries\ - \ forward every v1/v2/v3 blocker fix (C1 sentinel race in concurrent producers,\ - \ C2 heartbeat phase, H1 architect-first ordering, H2 reviewer verdict parsing,\ - \ T1 ruff format)." - attestation: - commit_sha: ecd8336b706e7f6df51fa1f4c37211c330efcf54 - commit_shas: - - ecd8336b706e7f6df51fa1f4c37211c330efcf54 - files_changed: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - checks_passed: - - ruff_format_check - - ruff_check_lint - - file_size_lint - - manual_smoke_rubric_default_nack - - manual_smoke_rubric_default_ack - - manual_smoke_per_producer_wrapper - - manual_smoke_harness_fake_optimistic_ack - - manual_smoke_fail_closed - tests_run: 5 - no_test_changes_needed: true - no_test_changes_reason: Coder role's allow-list excludes tests/. Class methods - stay as thin delegates (the new `plan_producers` kwarg on `_read_plan_reviewer_verdicts` - is keyword-only with a None default, so existing callers' access pattern is - unchanged). Five new manual smoke runs cover the dual-schema parser behaviour. - artifacts: - - orchestrator/substrate/in_process.py - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/__init__.py - risk_considered: "v4 risks: (1) Broadcast vs per-edge ambiguity \u2014 a future\ - \ rubric extension that wants per-edge verdicts under a non-`per_producer` key\ - \ would not be honoured. Mitigated by documenting both schemas in `read_plan_reviewer_verdicts`\ - \ and treating the per_producer wrapper as the explicit per-edge opt-in. (2)\ - \ NACK guard rejection on empty feedback \u2014 ReviewPayload validators reject\ - \ NACKs without a reason. Mitigated by synthesising a placeholder reason when\ - \ feedback is empty so the tracker records the NACK rather than discarding via\ - \ `log_tracker_warning`. (3) Test compatibility \u2014 tester's 16 passing v2/v3\ - \ tests rely on the harness-fake path (no verdict file, reviewer exit 0 \u2192\ - \ optimistic ACK). v4 preserves that path verbatim; only the \"verdict file\ - \ present but rubric-default shape\" case changes from \"fall-through to empty-verdicts\"\ - \ to \"broadcast top-level verdict\"." - commit_sha: ecd8336b706e7f6df51fa1f4c37211c330efcf54 - files_changed: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - tests_run: [] - tasks_satisfied: - - task-2-1 - - task-2-2 - - task-2-5 - version: 4 - commit_sha: ecd8336b706e7f6df51fa1f4c37211c330efcf54 - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] orchestrator → reviewer_code (CONSENSUS_RE_REVIEW): Re-review required: coder submitted new proposal v4 - -Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: 8242e968-4bbc-48 -phase: implement -metadata: - producer_role: coder - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] orchestrator → reviewer_contract (CONSENSUS_RE_REVIEW): Re-review required: coder submitted new proposal v4 - -Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: 460d44eb-4172-40 -phase: implement -metadata: - producer_role: coder - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] orchestrator → tester (CONSENSUS_RE_REVIEW): Re-review required: coder submitted new proposal v4 - -Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: bd765861-11d5-4c -phase: implement -metadata: - producer_role: coder - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] orchestrator → reviewer_security (CONSENSUS_RE_REVIEW): Re-review required: coder submitted new proposal v4 - -Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: 28beffe3-b2c0-48 -phase: implement -metadata: - producer_role: coder - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] orchestrator → reviewer_concurrency (CONSENSUS_RE_REVIEW): Re-review required: coder submitted new proposal v4 - -Producer coder has submitted a new proposal (version 4) after withdrawal. Your previous confirmation was on an earlier version. Please re-review and ACK/NACK the new proposal. - -````yaml -id: 1c595b54-c3c3-4a -phase: implement -metadata: - producer_role: coder - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] documenter (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a9b74b5d-f955-4b -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] reviewer_code_holistic (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: fc080014-d5cc-4c -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: eb5806c1-2dcc-44 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: f8f241ac-290d-47 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 566ac92c-d605-4a -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:34:00Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: c407a6f5-89d6-43 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:34:22Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d8528bdf-532f-44 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:34:10.517501+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:34:22Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 25899c4c-90f1-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:34:10.049489+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:05Z] reviewer_concurrency → coder (CONSENSUS_ACK): ACK from reviewer_concurrency for coder - - -Concurrency-lens re-review of coder v4 (commit ecd8336b7). The v3→v4 delta is purely in the verdict-JSON parser (`orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts`) — single-verdict-broadcast schema added alongside the per-producer wrapper, plus a `plan_producers` kwarg passed through from `_run_plan_phase_inner` and the `_read_plan_reviewer_verdicts` class-method delegate. Zero changes to the ThreadPoolExecutor wiring, the spawner call shape, the sentinel write, the heartbeat phase plumbing, the tracker register/propose/ack/confirmed sequencing, or the worktree allocation path. My v3 ACK still applies — re-confirming. - -### What I checked - -- **`read_plan_reviewer_verdicts` runs on the main thread** (`_plan_phase.py:170`, post-`as_completed_fn` block), so adding `plan_producers` as a kwarg and iterating it inside the function body has no thread-safety surface to disturb. No new shared mutable state, no new locks needed. -- **`{role.value: broadcast for role in plan_producers}`** (`:347`) constructs a fresh dict from the list passed in — no aliasing of the list-of-producers across threads. -- **No new background-thread state** — `_publish_heartbeat` still reads `self._current_phase`; `_run_plan_phase` still sets it to `"plan"`; v4 does not regress C2. -- **No new sentinel writes** in the concurrent path — `spawn_plan_producer` is unchanged from v3, still sentinel-free; v4 does not regress C1. -- **`_read_plan_reviewer_verdicts` class-method delegate** (`in_process.py:914-922`) simply forwards the kwarg; called from the same main-thread path as before. - -No new concurrency findings. ACKing v4 against version 4. - - -````yaml -id: 7794f311-e994-45 -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - reason: "\nConcurrency-lens re-review of coder v4 (commit ecd8336b7). The v3\u2192\ - v4 delta is purely in the verdict-JSON parser (`orchestrator/substrate/_plan_phase.py:read_plan_reviewer_verdicts`)\ - \ \u2014 single-verdict-broadcast schema added alongside the per-producer wrapper,\ - \ plus a `plan_producers` kwarg passed through from `_run_plan_phase_inner`\ - \ and the `_read_plan_reviewer_verdicts` class-method delegate. Zero changes\ - \ to the ThreadPoolExecutor wiring, the spawner call shape, the sentinel write,\ - \ the heartbeat phase plumbing, the tracker register/propose/ack/confirmed sequencing,\ - \ or the worktree allocation path. My v3 ACK still applies \u2014 re-confirming.\n\ - \n### What I checked\n\n- **`read_plan_reviewer_verdicts` runs on the main thread**\ - \ (`_plan_phase.py:170`, post-`as_completed_fn` block), so adding `plan_producers`\ - \ as a kwarg and iterating it inside the function body has no thread-safety\ - \ surface to disturb. No new shared mutable state, no new locks needed.\n- **`{role.value:\ - \ broadcast for role in plan_producers}`** (`:347`) constructs a fresh dict\ - \ from the list passed in \u2014 no aliasing of the list-of-producers across\ - \ threads.\n- **No new background-thread state** \u2014 `_publish_heartbeat`\ - \ still reads `self._current_phase`; `_run_plan_phase` still sets it to `\"\ - plan\"`; v4 does not regress C2.\n- **No new sentinel writes** in the concurrent\ - \ path \u2014 `spawn_plan_producer` is unchanged from v3, still sentinel-free;\ - \ v4 does not regress C1.\n- **`_read_plan_reviewer_verdicts` class-method delegate**\ - \ (`in_process.py:914-922`) simply forwards the kwarg; called from the same\ - \ main-thread path as before.\n\nNo new concurrency findings. ACKing v4 against\ - \ version 4.\n" - ack_version: 4 - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:05Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: b21d22f7-6147-45 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:10Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 478b422d-018e-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:10.950600+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:12Z] reviewer_code → coder (CONSENSUS_ACK): ACK from reviewer_code for coder - - -Re-reviewed v4 (commit ecd8336b7) targeting the reviewer_code_holistic v3 NACK blocker H3 — the documenter-shipped `reviewer_plan.md` rubric documents a single top-level verdict object, but v2/v3's `read_plan_reviewer_verdicts` only parsed a `per_producer` wrapper, so a rubric-following reviewer's NACK would have been silently lost into the optimistic-ACK fallback path. - -### What changed v3 → v4 - -**Two-schema verdict parser (`_plan_phase.py:251-353`).** `read_plan_reviewer_verdicts` now accepts both shapes: - -1. **Rubric-default single-verdict (broadcast).** Top-level `verdict ∈ {ACK, NACK}` → broadcast to every plan producer edge. NACK propagates the top-level `feedback` blob as the per-edge `reason`; ACK propagates `artifact_references` and `pre_merge_condition`. If the broadcast verdict is NACK and `feedback` is empty, a synthetic placeholder fires (`f"reviewer_plan broadcast {top_verdict}: top-level verdict without a per-edge feedback blob — see verdict JSON for the criteria-keyed analysis."`) so `ReviewPayload.validate_nack_has_reason` doesn't reject the payload server-side. - -2. **Per-producer extension (per-edge).** Existing `per_producer: {role: {verdict, reason, ...}}` wrapper takes precedence when present AND well-formed (at least one entry survives validation). Reviewers that want explicit edge granularity (ACK architect + NACK task_planner) opt into the wrapper; the rubric's default shape stays broadcast-compatible. - -**Precedence rule**: per_producer wrapper > top-level broadcast > empty (fail-closed / optimistic-ACK fallback in `_apply_reviewer_verdicts`). - -**`plan_producers` kwarg threading.** New `plan_producers: list[Any] | None = None` kwarg on `read_plan_reviewer_verdicts` (lines 252-254). The orchestrator caller passes the producer list (`_run_plan_phase_inner` line 170) so the broadcast knows which producer roles to target. The class-method delegate at `in_process.py:914-923` propagates the kwarg so tester-side tests that call `runner._read_plan_reviewer_verdicts(plan_producers=[...])` retain their access pattern. - -### File-by-file analysis - -**orchestrator/substrate/_plan_phase.py** (+82/-25) — Single-function change in `read_plan_reviewer_verdicts`; the rest of `_run_plan_phase_inner` / `_apply_reviewer_verdicts` / spawn helpers is unchanged. The broadcast construction at lines 348-353 is a dict-comprehension keyed by `role.value` so the resulting `{role: entry, ...}` matches the per_producer wrapper's shape — `_apply_reviewer_verdicts` consumes either path uniformly without changes. The `if not plan_producers: return verdict_path, {}` guard at lines 332-334 keeps legacy callers (any test or future caller that didn't pass `plan_producers`) safe — they fall through to the fail-closed / optimistic-ACK heuristic rather than crashing. - -**orchestrator/substrate/in_process.py** (+3/-1) — `_read_plan_reviewer_verdicts` delegate updated with the same `plan_producers` kwarg. Surface-preserving for the tester's tests. - -### Edge-case behaviour - -- **per_producer wrapper present but all entries invalid (e.g., `verdict` field missing or unrecognized).** The filter loop produces an empty `normalised` dict; `if normalised:` is False; falls through to single-verdict broadcast (if top-level `verdict` is set) or empty (fail-closed/optimistic heuristic). Reasonable. - -- **Both per_producer wrapper AND top-level verdict present.** per_producer wins when it has at least one valid entry. Reviewers that want to override the broadcast for specific edges set the wrapper explicitly. Sensible precedence. - -- **Top-level verdict is invalid (e.g., "MAYBE").** `top_verdict in {"ACK", "NACK"}` returns False; falls through to `return verdict_path, {}` — empty verdicts, fail-closed/optimistic heuristic applies. Good. - -- **Broadcast NACK with empty feedback.** Synthetic placeholder reason fires (lines 339-345). `_record_reviewer_nack` records the NACK successfully. Closes my v3 non-blocking #1. - -- **`plan_producers=None` (legacy caller path).** Returns empty verdicts; `_apply_reviewer_verdicts` falls back to fail-closed / optimistic-ACK based on reviewer exit_code. Safe default. - -### Compatibility with tester's existing tests - -The tester's `test_inprocess_plan_brc.py` uses a MagicMock spawner that never writes a verdict JSON. The new schema parser still returns `(None, {})` when the file is missing, so `_apply_reviewer_verdicts` continues to take the optimistic-ACK path for harness-faked runs — the tester's 7 existing plan-phase tests remain green. The v4 docstring smoke notes confirm this. - -### Non-blocking - -- **orchestrator/substrate/_plan_phase.py:333-334** — When `plan_producers=None` and the verdict file has a top-level single verdict, the function silently returns `(verdict_path, {})`. The fail-closed/optimistic heuristic downstream may then misinterpret a real NACK as an optimistic ACK (if reviewer exit_code=0). The current orchestrator always passes `plan_producers`, so this is a latent footgun for legacy callers. Worth a `logger.warning` when this branch fires noting "single-verdict shape detected but no plan_producers kwarg — broadcast suppressed". - -- **orchestrator/substrate/_plan_phase.py:286-313** — When `per_producer` is a well-formed dict that has entries but ALL entries are filtered out (every entry has `verdict` ∉ {ACK, NACK} or is not a dict), the code falls through to single-verdict broadcast. This is reasonable, but combined with the comment "per-producer extension wrapper takes precedence if it's a well-formed dict" the fall-through behavior may surprise a reviewer who tried to write a per_producer wrapper but typo'd every verdict. A `logger.warning` on "per_producer wrapper present but no valid entries" would help debugging. - -- **orchestrator/substrate/_plan_phase.py:341-345** — The synthetic broadcast-NACK placeholder text references "the criteria-keyed analysis" but doesn't embed the actual verdict JSON path. An operator reading the NACK reason at the HITL gate gets a pointer to "see verdict JSON" without knowing where it lives. Consider templating `{verdict_path}` into the placeholder so the operator can `cat` the file directly. Not blocking — the verdict_path is already surfaced in `runner._verdict_diagnostics` (`_plan_phase.py:171-175`) and the plan-placeholder body. - -No security, correctness, or robustness issues introduced by v4. The change is surgical and well-scoped — addresses H3 (rubric-default verdict schema) without disturbing the v2/v3 per_producer extension semantics. Coder v4 ACKed. - - -````yaml -id: 60324dbb-1aa6-40 -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - reason: "\nRe-reviewed v4 (commit ecd8336b7) targeting the reviewer_code_holistic\ - \ v3 NACK blocker H3 \u2014 the documenter-shipped `reviewer_plan.md` rubric\ - \ documents a single top-level verdict object, but v2/v3's `read_plan_reviewer_verdicts`\ - \ only parsed a `per_producer` wrapper, so a rubric-following reviewer's NACK\ - \ would have been silently lost into the optimistic-ACK fallback path.\n\n###\ - \ What changed v3 \u2192 v4\n\n**Two-schema verdict parser (`_plan_phase.py:251-353`).**\ - \ `read_plan_reviewer_verdicts` now accepts both shapes:\n\n1. **Rubric-default\ - \ single-verdict (broadcast).** Top-level `verdict \u2208 {ACK, NACK}` \u2192\ - \ broadcast to every plan producer edge. NACK propagates the top-level `feedback`\ - \ blob as the per-edge `reason`; ACK propagates `artifact_references` and `pre_merge_condition`.\ - \ If the broadcast verdict is NACK and `feedback` is empty, a synthetic placeholder\ - \ fires (`f\"reviewer_plan broadcast {top_verdict}: top-level verdict without\ - \ a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed analysis.\"\ - `) so `ReviewPayload.validate_nack_has_reason` doesn't reject the payload server-side.\n\ - \n2. **Per-producer extension (per-edge).** Existing `per_producer: {role: {verdict,\ - \ reason, ...}}` wrapper takes precedence when present AND well-formed (at least\ - \ one entry survives validation). Reviewers that want explicit edge granularity\ - \ (ACK architect + NACK task_planner) opt into the wrapper; the rubric's default\ - \ shape stays broadcast-compatible.\n\n**Precedence rule**: per_producer wrapper\ - \ > top-level broadcast > empty (fail-closed / optimistic-ACK fallback in `_apply_reviewer_verdicts`).\n\ - \n**`plan_producers` kwarg threading.** New `plan_producers: list[Any] | None\ - \ = None` kwarg on `read_plan_reviewer_verdicts` (lines 252-254). The orchestrator\ - \ caller passes the producer list (`_run_plan_phase_inner` line 170) so the\ - \ broadcast knows which producer roles to target. The class-method delegate\ - \ at `in_process.py:914-923` propagates the kwarg so tester-side tests that\ - \ call `runner._read_plan_reviewer_verdicts(plan_producers=[...])` retain their\ - \ access pattern.\n\n### File-by-file analysis\n\n**orchestrator/substrate/_plan_phase.py**\ - \ (+82/-25) \u2014 Single-function change in `read_plan_reviewer_verdicts`;\ - \ the rest of `_run_plan_phase_inner` / `_apply_reviewer_verdicts` / spawn helpers\ - \ is unchanged. The broadcast construction at lines 348-353 is a dict-comprehension\ - \ keyed by `role.value` so the resulting `{role: entry, ...}` matches the per_producer\ - \ wrapper's shape \u2014 `_apply_reviewer_verdicts` consumes either path uniformly\ - \ without changes. The `if not plan_producers: return verdict_path, {}` guard\ - \ at lines 332-334 keeps legacy callers (any test or future caller that didn't\ - \ pass `plan_producers`) safe \u2014 they fall through to the fail-closed /\ - \ optimistic-ACK heuristic rather than crashing.\n\n**orchestrator/substrate/in_process.py**\ - \ (+3/-1) \u2014 `_read_plan_reviewer_verdicts` delegate updated with the same\ - \ `plan_producers` kwarg. Surface-preserving for the tester's tests.\n\n###\ - \ Edge-case behaviour\n\n- **per_producer wrapper present but all entries invalid\ - \ (e.g., `verdict` field missing or unrecognized).** The filter loop produces\ - \ an empty `normalised` dict; `if normalised:` is False; falls through to single-verdict\ - \ broadcast (if top-level `verdict` is set) or empty (fail-closed/optimistic\ - \ heuristic). Reasonable.\n\n- **Both per_producer wrapper AND top-level verdict\ - \ present.** per_producer wins when it has at least one valid entry. Reviewers\ - \ that want to override the broadcast for specific edges set the wrapper explicitly.\ - \ Sensible precedence.\n\n- **Top-level verdict is invalid (e.g., \"MAYBE\"\ - ).** `top_verdict in {\"ACK\", \"NACK\"}` returns False; falls through to `return\ - \ verdict_path, {}` \u2014 empty verdicts, fail-closed/optimistic heuristic\ - \ applies. Good.\n\n- **Broadcast NACK with empty feedback.** Synthetic placeholder\ - \ reason fires (lines 339-345). `_record_reviewer_nack` records the NACK successfully.\ - \ Closes my v3 non-blocking #1.\n\n- **`plan_producers=None` (legacy caller\ - \ path).** Returns empty verdicts; `_apply_reviewer_verdicts` falls back to\ - \ fail-closed / optimistic-ACK based on reviewer exit_code. Safe default.\n\n\ - ### Compatibility with tester's existing tests\n\nThe tester's `test_inprocess_plan_brc.py`\ - \ uses a MagicMock spawner that never writes a verdict JSON. The new schema\ - \ parser still returns `(None, {})` when the file is missing, so `_apply_reviewer_verdicts`\ - \ continues to take the optimistic-ACK path for harness-faked runs \u2014 the\ - \ tester's 7 existing plan-phase tests remain green. The v4 docstring smoke\ - \ notes confirm this.\n\n### Non-blocking\n\n- **orchestrator/substrate/_plan_phase.py:333-334**\ - \ \u2014 When `plan_producers=None` and the verdict file has a top-level single\ - \ verdict, the function silently returns `(verdict_path, {})`. The fail-closed/optimistic\ - \ heuristic downstream may then misinterpret a real NACK as an optimistic ACK\ - \ (if reviewer exit_code=0). The current orchestrator always passes `plan_producers`,\ - \ so this is a latent footgun for legacy callers. Worth a `logger.warning` when\ - \ this branch fires noting \"single-verdict shape detected but no plan_producers\ - \ kwarg \u2014 broadcast suppressed\".\n\n- **orchestrator/substrate/_plan_phase.py:286-313**\ - \ \u2014 When `per_producer` is a well-formed dict that has entries but ALL\ - \ entries are filtered out (every entry has `verdict` \u2209 {ACK, NACK} or\ - \ is not a dict), the code falls through to single-verdict broadcast. This is\ - \ reasonable, but combined with the comment \"per-producer extension wrapper\ - \ takes precedence if it's a well-formed dict\" the fall-through behavior may\ - \ surprise a reviewer who tried to write a per_producer wrapper but typo'd every\ - \ verdict. A `logger.warning` on \"per_producer wrapper present but no valid\ - \ entries\" would help debugging.\n\n- **orchestrator/substrate/_plan_phase.py:341-345**\ - \ \u2014 The synthetic broadcast-NACK placeholder text references \"the criteria-keyed\ - \ analysis\" but doesn't embed the actual verdict JSON path. An operator reading\ - \ the NACK reason at the HITL gate gets a pointer to \"see verdict JSON\" without\ - \ knowing where it lives. Consider templating `{verdict_path}` into the placeholder\ - \ so the operator can `cat` the file directly. Not blocking \u2014 the verdict_path\ - \ is already surfaced in `runner._verdict_diagnostics` (`_plan_phase.py:171-175`)\ - \ and the plan-placeholder body.\n\nNo security, correctness, or robustness\ - \ issues introduced by v4. The change is surgical and well-scoped \u2014 addresses\ - \ H3 (rubric-default verdict schema) without disturbing the v2/v3 per_producer\ - \ extension semantics. Coder v4 ACKed.\n" - ack_version: 4 - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:12Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: a620e741-9d78-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:13Z] reviewer_concurrency (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 852dd96b-1ddb-46 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:13.364302+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:16Z] reviewer_security → coder (CONSENSUS_ACK): ACK from reviewer_security for coder - - -Re-reviewed slice-2 coder v4 at commit ecd8336b7 through the security lens. The v3→v4 delta is bounded to `read_plan_reviewer_verdicts` (now accepts both the rubric-default single-verdict schema AND the per_producer wrapper) plus the matching kwarg propagation on the class delegate. No new security findings; the dual-schema parser is well-bounded. - -### Lens checks against the v3→v4 delta - -1. **Cross-file allowlist mismatch (§1):** Unchanged. The newly-supported schema 1 matches the documenter's rubric at plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md lines 57-80 (top-level `verdict`, `feedback`, `analysis`, `artifact_references`) — closes a *real* cross-file mismatch between the documenter-shipped reviewer rubric and the v2/v3 parser, where a rubric-conformant reviewer NACK would have been silently swallowed into the "verdict file present but no parseable per_producer entries" branch. v4 explicitly preserves the "ACK only if every criterion passes" semantic by broadcasting an ACK / NACK to every producer edge. - -2. **Handler-vs-validator path mismatch (§2):** N/A — no new entrypoint. - -3. **Information-disclosure (§3):** The reviewer's `feedback` blob now broadcasts to every producer edge as the per-edge `reason`. The `feedback` originates from the reviewer's own output in a worktree-bounded write, flows into the BRC tracker payload (in-process state) and is repr-truncated to 200 chars in `format_plan_placeholder`'s "reviewer_plan verdict parsing" subsection — same disclosure surface as v3, just propagated to three edges instead of zero when the rubric-default shape is used. No NEW sink. - -4. **Path-traversal / agent-supplied paths (§8):** `verdict_path` construction is unchanged (`outputs_dir / f"{artifact_id}-reviewer_plan-output.json"`); still orchestrator-derived from trusted `state_root` + `issue_number/pipeline_id`. The new schema-1 parser preserves strict input sanitisation: - - `isinstance(blob, dict)` gate before any `.get` access. - - `top_verdict in {"ACK", "NACK"}` whitelist before any tracker emission. - - `if not plan_producers: return verdict_path, {}` fail-safe: a caller that doesn't supply the producer list cannot drive a broadcast. - - All string fields cast through `str()`, list fields through `list()`, dict comprehension builds typed entries. - - Empty-`feedback` NACK is given a deterministic placeholder string so `ReviewPayload.validate_nack_has_reason` cannot reject the payload and silently lose the NACK — closes a class of "reviewer NACK disappears" bugs the v3 parser had if the rubric was followed literally. - -5. **Uncommitted-artifact / symlink mismatch (§4):** N/A. - -6. **Credential-shim modifications (§5):** N/A. - -7. **Secret leakage (§6):** Unchanged sinks. The reviewer's `pre_merge_condition` string is also broadcast to every producer edge via the shared `broadcast` dict (`{role.value: broadcast for role in plan_producers}`); pre_merge_condition is a documented bare-string field on `ReviewPayload`, not a credential carrier. - -8. **Cross-file OWASP top-10 (§7):** No new sources or sinks. The dict-comprehension shares one `broadcast` dict reference across all producer keys, but `_apply_reviewer_verdicts` only reads from those entries; no downstream mutation that would couple per-edge state. Pure code-quality concern, not security. - -### Non-blocking (carried forward where relevant) -- in_process.py:98 — `_SYNTHETIC_PLAN_COMMIT = "ace1ace"` remains unreferenced; defer to reviewer_code. -- _plan_phase.py:288-294 — `json.loads(verdict_path.read_text(...))` still has no file-size cap; hardening-only observation. -- _plan_phase.py:343-348 — the shared `broadcast` dict reference across all producer keys means any future mutation in `_apply_reviewer_verdicts` would silently couple per-edge state. Today's downstream is read-only so this is latent; a follow-up could `copy.deepcopy(broadcast)` per role if mutation becomes warranted. Code-quality / future-proofing only; defer to reviewer_code. - - -````yaml -id: b489a258-f267-4d -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - reason: "\nRe-reviewed slice-2 coder v4 at commit ecd8336b7 through the security\ - \ lens. The v3\u2192v4 delta is bounded to `read_plan_reviewer_verdicts` (now\ - \ accepts both the rubric-default single-verdict schema AND the per_producer\ - \ wrapper) plus the matching kwarg propagation on the class delegate. No new\ - \ security findings; the dual-schema parser is well-bounded.\n\n### Lens checks\ - \ against the v3\u2192v4 delta\n\n1. **Cross-file allowlist mismatch (\xA71):**\ - \ Unchanged. The newly-supported schema 1 matches the documenter's rubric at\ - \ plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md lines 57-80 (top-level\ - \ `verdict`, `feedback`, `analysis`, `artifact_references`) \u2014 closes a\ - \ *real* cross-file mismatch between the documenter-shipped reviewer rubric\ - \ and the v2/v3 parser, where a rubric-conformant reviewer NACK would have been\ - \ silently swallowed into the \"verdict file present but no parseable per_producer\ - \ entries\" branch. v4 explicitly preserves the \"ACK only if every criterion\ - \ passes\" semantic by broadcasting an ACK / NACK to every producer edge.\n\n\ - 2. **Handler-vs-validator path mismatch (\xA72):** N/A \u2014 no new entrypoint.\n\ - \n3. **Information-disclosure (\xA73):** The reviewer's `feedback` blob now\ - \ broadcasts to every producer edge as the per-edge `reason`. The `feedback`\ - \ originates from the reviewer's own output in a worktree-bounded write, flows\ - \ into the BRC tracker payload (in-process state) and is repr-truncated to 200\ - \ chars in `format_plan_placeholder`'s \"reviewer_plan verdict parsing\" subsection\ - \ \u2014 same disclosure surface as v3, just propagated to three edges instead\ - \ of zero when the rubric-default shape is used. No NEW sink.\n\n4. **Path-traversal\ - \ / agent-supplied paths (\xA78):** `verdict_path` construction is unchanged\ - \ (`outputs_dir / f\"{artifact_id}-reviewer_plan-output.json\"`); still orchestrator-derived\ - \ from trusted `state_root` + `issue_number/pipeline_id`. The new schema-1 parser\ - \ preserves strict input sanitisation:\n - `isinstance(blob, dict)` gate before\ - \ any `.get` access.\n - `top_verdict in {\"ACK\", \"NACK\"}` whitelist before\ - \ any tracker emission.\n - `if not plan_producers: return verdict_path, {}`\ - \ fail-safe: a caller that doesn't supply the producer list cannot drive a broadcast.\n\ - \ - All string fields cast through `str()`, list fields through `list()`,\ - \ dict comprehension builds typed entries.\n - Empty-`feedback` NACK is given\ - \ a deterministic placeholder string so `ReviewPayload.validate_nack_has_reason`\ - \ cannot reject the payload and silently lose the NACK \u2014 closes a class\ - \ of \"reviewer NACK disappears\" bugs the v3 parser had if the rubric was followed\ - \ literally.\n\n5. **Uncommitted-artifact / symlink mismatch (\xA74):** N/A.\n\ - \n6. **Credential-shim modifications (\xA75):** N/A.\n\n7. **Secret leakage\ - \ (\xA76):** Unchanged sinks. The reviewer's `pre_merge_condition` string is\ - \ also broadcast to every producer edge via the shared `broadcast` dict (`{role.value:\ - \ broadcast for role in plan_producers}`); pre_merge_condition is a documented\ - \ bare-string field on `ReviewPayload`, not a credential carrier.\n\n8. **Cross-file\ - \ OWASP top-10 (\xA77):** No new sources or sinks. The dict-comprehension shares\ - \ one `broadcast` dict reference across all producer keys, but `_apply_reviewer_verdicts`\ - \ only reads from those entries; no downstream mutation that would couple per-edge\ - \ state. Pure code-quality concern, not security.\n\n### Non-blocking (carried\ - \ forward where relevant)\n- in_process.py:98 \u2014 `_SYNTHETIC_PLAN_COMMIT\ - \ = \"ace1ace\"` remains unreferenced; defer to reviewer_code.\n- _plan_phase.py:288-294\ - \ \u2014 `json.loads(verdict_path.read_text(...))` still has no file-size cap;\ - \ hardening-only observation.\n- _plan_phase.py:343-348 \u2014 the shared `broadcast`\ - \ dict reference across all producer keys means any future mutation in `_apply_reviewer_verdicts`\ - \ would silently couple per-edge state. Today's downstream is read-only so this\ - \ is latent; a follow-up could `copy.deepcopy(broadcast)` per role if mutation\ - \ becomes warranted. Code-quality / future-proofing only; defer to reviewer_code.\n" - ack_version: 4 - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:18Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 9cf0bf20-3a20-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:17.972008+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:18Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 5722f951-2bca-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:18Z] tester → coder (CONSENSUS_ACK): ACK from tester for coder - - -Re-reviewed coder v4 (commit `ecd8336b7`) after v3 ACK was invalidated by the new proposal. The delta is contained to `_plan_phase.py:read_plan_reviewer_verdicts` (now ~107 lines wider than v3 in the function body + helpers) plus a 4-line ripple in `in_process.py` passing the new `plan_producers` kwarg through. - -**My 16/16 tester-authored tests still pass against v4 unchanged.** `ruff format --check .` passes across 962 files; `ruff check` is clean. mypy is green on gateway/shared/sandbox. The schema-acceptance addition is purely additive — the per_producer wrapper (which my harness-fake path doesn't write) still takes precedence when present, and the rubric-default single-verdict path (which my path doesn't trigger either) is only exercised when the JSON file is actually present on disk. The harness-faked optimistic-ACK fallback I depend on (no verdict file + reviewer exit 0) is preserved at the same call site so my test_plan_stage_reaches_consensus_confirmed_for_each_producer continues to see all 4 plan-team roles in confirmed=True. - -### v4 deltas spot-checked - -1. **Dual-schema verdict parsing in `read_plan_reviewer_verdicts`**. The function now accepts an optional `plan_producers` kwarg (default `None`). When the top-level JSON has `verdict ∈ {"ACK", "NACK"}` it's broadcast to every entry in `plan_producers`; when it has a `per_producer` dict, the per-edge granularity wins. The precedence rule (per_producer takes precedence over a top-level verdict if both present and well-formed) is the right default — explicit edge granularity should override a broadcast. - -2. **`_apply_reviewer_verdicts` propagates `plan_producers`**. The class-level delegate at the call site in `_run_plan_phase_inner` passes the producer role list correctly. Sound. - -3. **`feedback` field propagation**. When a top-level NACK has an empty/missing `feedback` field, a synthetic placeholder fires so the tracker's NACK guard doesn't reject the payload. Good defensive shape — without it a sparse NACK verdict would be silently dropped. - -4. **Backward-compat with v3's per_producer schema** confirmed: a reviewer that wrote `{"per_producer": {"architect": {"verdict": "ACK"}, ...}}` continues to produce per-edge ACKs. The v4 change is a strict superset. - -5. **Rubric alignment**. The rubric the documenter shipped (`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md` "Verdict JSON shape", lines 57-80) documents the single top-level `verdict` shape with the 8-criterion analysis block. v4 now accepts the rubric's documented shape; this was the rubric-vs-code drift reviewer_code_holistic v3 flagged. Resolved. - -### Non-blocking (carry to follow-up) - -- **Schema parsing is loose**. `read_plan_reviewer_verdicts` does isinstance probes on dict / string values; a malformed `feedback` field (e.g., a list of strings instead of a single string) would fall into the synthetic-placeholder branch. A pydantic / dataclass schema check (or a JSON Schema) would surface that as a structured error rather than a silent placeholder substitution. Not blocking because the placeholder body surfaces "verdict-not-parsed" on the HITL gate, but worth a follow-up. - -- **The rubric body cites the 8 criteria but the parser doesn't verify the analysis block matches the documented criteria set**. A reviewer that wrote `{"verdict": "ACK", "analysis": {"foo": true}}` would land as a broadcast-ACK with the analysis blob silently retained. Again: HITL gate sees the placeholder body so the operator catches the discrepancy; not blocking. - -All ACs in the contract task-2-1 / task-2-2 / task-2-5 are satisfied; lint is green; my 16/16 tests pass against v4 with no edits; rubric ↔ parser symmetry is resolved. ACK. - - -````yaml -id: d1586d49-1cb2-4d -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - reason: "\nRe-reviewed coder v4 (commit `ecd8336b7`) after v3 ACK was invalidated\ - \ by the new proposal. The delta is contained to `_plan_phase.py:read_plan_reviewer_verdicts`\ - \ (now ~107 lines wider than v3 in the function body + helpers) plus a 4-line\ - \ ripple in `in_process.py` passing the new `plan_producers` kwarg through.\ - \ \n\n**My 16/16 tester-authored tests still pass against v4 unchanged.** `ruff\ - \ format --check .` passes across 962 files; `ruff check` is clean. mypy is\ - \ green on gateway/shared/sandbox. The schema-acceptance addition is purely\ - \ additive \u2014 the per_producer wrapper (which my harness-fake path doesn't\ - \ write) still takes precedence when present, and the rubric-default single-verdict\ - \ path (which my path doesn't trigger either) is only exercised when the JSON\ - \ file is actually present on disk. The harness-faked optimistic-ACK fallback\ - \ I depend on (no verdict file + reviewer exit 0) is preserved at the same call\ - \ site so my test_plan_stage_reaches_consensus_confirmed_for_each_producer continues\ - \ to see all 4 plan-team roles in confirmed=True.\n\n### v4 deltas spot-checked\n\ - \n1. **Dual-schema verdict parsing in `read_plan_reviewer_verdicts`**. The function\ - \ now accepts an optional `plan_producers` kwarg (default `None`). When the\ - \ top-level JSON has `verdict \u2208 {\"ACK\", \"NACK\"}` it's broadcast to\ - \ every entry in `plan_producers`; when it has a `per_producer` dict, the per-edge\ - \ granularity wins. The precedence rule (per_producer takes precedence over\ - \ a top-level verdict if both present and well-formed) is the right default\ - \ \u2014 explicit edge granularity should override a broadcast.\n\n2. **`_apply_reviewer_verdicts`\ - \ propagates `plan_producers`**. The class-level delegate at the call site in\ - \ `_run_plan_phase_inner` passes the producer role list correctly. Sound.\n\n\ - 3. **`feedback` field propagation**. When a top-level NACK has an empty/missing\ - \ `feedback` field, a synthetic placeholder fires so the tracker's NACK guard\ - \ doesn't reject the payload. Good defensive shape \u2014 without it a sparse\ - \ NACK verdict would be silently dropped.\n\n4. **Backward-compat with v3's\ - \ per_producer schema** confirmed: a reviewer that wrote `{\"per_producer\"\ - : {\"architect\": {\"verdict\": \"ACK\"}, ...}}` continues to produce per-edge\ - \ ACKs. The v4 change is a strict superset.\n\n5. **Rubric alignment**. The\ - \ rubric the documenter shipped (`plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md`\ - \ \"Verdict JSON shape\", lines 57-80) documents the single top-level `verdict`\ - \ shape with the 8-criterion analysis block. v4 now accepts the rubric's documented\ - \ shape; this was the rubric-vs-code drift reviewer_code_holistic v3 flagged.\ - \ Resolved.\n\n### Non-blocking (carry to follow-up)\n\n- **Schema parsing is\ - \ loose**. `read_plan_reviewer_verdicts` does isinstance probes on dict / string\ - \ values; a malformed `feedback` field (e.g., a list of strings instead of a\ - \ single string) would fall into the synthetic-placeholder branch. A pydantic\ - \ / dataclass schema check (or a JSON Schema) would surface that as a structured\ - \ error rather than a silent placeholder substitution. Not blocking because\ - \ the placeholder body surfaces \"verdict-not-parsed\" on the HITL gate, but\ - \ worth a follow-up.\n\n- **The rubric body cites the 8 criteria but the parser\ - \ doesn't verify the analysis block matches the documented criteria set**. A\ - \ reviewer that wrote `{\"verdict\": \"ACK\", \"analysis\": {\"foo\": true}}`\ - \ would land as a broadcast-ACK with the analysis blob silently retained. Again:\ - \ HITL gate sees the placeholder body so the operator catches the discrepancy;\ - \ not blocking.\n\nAll ACs in the contract task-2-1 / task-2-2 / task-2-5 are\ - \ satisfied; lint is green; my 16/16 tests pass against v4 with no edits; rubric\ - \ \u2194 parser symmetry is resolved. ACK.\n" - ack_version: 4 - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:18Z] orchestrator → tester (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: bfa55c15-5bbd-4d -phase: implement -metadata: - ready_to_confirm: true - version: 1 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:20Z] reviewer_code (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 9d6aec21-06bd-4d -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:20.636469+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:22Z] documenter (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d8bd6b97-563a-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:34:10.517501+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:23Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b64f54d3-0cee-4e -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:23.248175+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:23Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: cb00829d-389d-47 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:26Z] tester (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_NACK,CONSENSUS_ACK,OVERSEER_ALERT - -````yaml -id: 822431cb-dbf6-4c -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:25.979157+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:27Z] reviewer_contract → coder (CONSENSUS_ACK): ACK from reviewer_contract for coder - - -Re-review of coder v4 (commit ecd8336b7) after CONSENSUS_RE_REVIEW signal. v4 is a focused, additive change to `read_plan_reviewer_verdicts` that resolves reviewer_code_holistic v3 blocker H3 — the reviewer_plan rubric ships a single-verdict top-level JSON shape, but v2/v3's parser only understood the `per_producer` wrapper, so a rubric-following NACK would silently miss the tracker. v4 broadens the parser to accept BOTH schemas with `per_producer` taking precedence when present. - -Re-checked all three coder-owned task ACs against the v4 commit: nothing in v4 regresses any AC. - -### Per-task verification (v4) - -**TASK-2-1 — `_run_plan_phase` end-to-end** (orchestrator/substrate/in_process.py + orchestrator/substrate/_plan_phase.py): - -1. AC "no longer raises NotImplementedError when the operator advances past refine": ✅ Unchanged in v4. `run()` at in_process.py:246 still calls `self._run_plan_phase(...)`; the walking-skeleton fence still fires only on the plan HITL gate's `approve_continue` (slice-3 / slice-4 pointer intact). - -2. AC "plan stage spawns 3 producers concurrently via the executor": ✅ Unchanged in v4. Architect-first synchronous spawn → `task_planner + risk_analyst` concurrent fan-out via `ThreadPoolExecutor(max_workers=2)` (_plan_phase.py:124-161, unchanged in v4). Role-dependency-driven deviation from literal "3 concurrent" is still grounded in `shared/egg_contracts/agent_roles.py` declaring ARCHITECT as the sole dependency of TASK_PLANNER / RISK_ANALYST. - -3. AC "reviewer_plan is spawned after each CONSENSUS_PROPOSE": ✅ Materially strengthened in v4. Reviewer dispatch and tracker advancement structure unchanged; the verdict-parsing layer now correctly recognises the rubric-default shape. A rubric-following `verdict: "NACK"` no longer falls into the optimistic-ACK fallback that masked NACKs from the operator at the plan HITL gate (v3 silent bug). The NACK now broadcasts to every producer edge with `feedback` propagated as each edge's `reason` (_plan_phase.py:325-351) and an explicit synthetic placeholder when `feedback` is empty to avoid hitting `ReviewPayload.validate_nack_has_reason`. Per-edge ACK / NACK still drives `tracker.handle_ack` / `tracker.handle_nack` per producer. - -4. AC "yields a plan-HITL decision after CONSENSUS_CONFIRMED on every producer edge": ✅ Unchanged in v4. `tracker.handle_confirmed` for each role; `evaluate()` snapshot; `_build_plan_gate_decision` yields HITLDecision with `phase="plan"`. The schema-broadening at the parsing layer cannot regress the CONSENSUS_CONFIRMED path because (a) ACK still acks all three on the broadcast path → CONSENSUS_CONFIRMED reachable; (b) NACK paths surface in the eval snapshot's `blocking_agents` exactly as before — the only difference is they now surface for rubric-default JSON shapes too, which is a correctness improvement. - -5. AC "existing refine path still works": ✅ Unchanged. Refine flow at in_process.py:213-240 untouched in v4. - -**TASK-2-2 — `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py): unchanged in v4. ACs remain met. - -**TASK-2-5 — sandbox restrictions parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py): unchanged in v4 (still no edits). The "R2 = pass → no-op" close remains the operative decision; coder commit message lineage preserves the required close-with-note. - -### v4 surface-area assessment (informational) - -- The `read_plan_reviewer_verdicts(runner, *, plan_producers=None)` signature change is backward-compatible (kwarg with `None` default), and the `_read_plan_reviewer_verdicts` class-method delegate on `_InProcessOrchestrator` propagates the new kwarg with the same default. The tester's `test_inprocess_plan_brc.py` does not call this method directly (it inspects `_plan_tracker.evaluate()` after the stage runs), so the existing 16 passing test cases remain intact. -- Legacy-caller safety: when `plan_producers=None` and the JSON is single-verdict, the function returns `({}, verdict_path)` and the orchestrator's fail-closed / optimistic-ACK heuristic in `_apply_reviewer_verdicts` applies — preserves the historical behaviour for any out-of-tree caller. -- Schema 2 (per_producer wrapper) still takes precedence when present and well-formed (_plan_phase.py:301-317), so an explicit per-edge verdict reviewer is not surprised by silently-broadcast behaviour. - -### Non-blocking observations carried forward from v3 review - -- Slice-1 contract bookkeeping (`task-1-1` … `task-1-9` show `status: "pending"` despite linked commits) — informational; operator reconcile before declaring the rollout complete. -- `synthetic_commit_for(role)` SHA-1-derived prefix at _plan_phase.py:644-656 is unchanged; per-role distinguishability holds. -- Schema-1 NACK reason placeholder ("reviewer_plan broadcast NACK: top-level verdict without a per-edge feedback blob — see verdict JSON for the criteria-keyed analysis") is operator-readable and explicit; if a future regression test wants to pin the exact substring, the runner's `_verdict_diagnostics` dict is the structured surface to assert against. - -Marking v4 ACKed. - - -````yaml -id: 033d3b5a-8d22-49 -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - reason: "\nRe-review of coder v4 (commit ecd8336b7) after CONSENSUS_RE_REVIEW\ - \ signal. v4 is a focused, additive change to `read_plan_reviewer_verdicts`\ - \ that resolves reviewer_code_holistic v3 blocker H3 \u2014 the reviewer_plan\ - \ rubric ships a single-verdict top-level JSON shape, but v2/v3's parser only\ - \ understood the `per_producer` wrapper, so a rubric-following NACK would silently\ - \ miss the tracker. v4 broadens the parser to accept BOTH schemas with `per_producer`\ - \ taking precedence when present.\n\nRe-checked all three coder-owned task ACs\ - \ against the v4 commit: nothing in v4 regresses any AC.\n\n### Per-task verification\ - \ (v4)\n\n**TASK-2-1 \u2014 `_run_plan_phase` end-to-end** (orchestrator/substrate/in_process.py\ - \ + orchestrator/substrate/_plan_phase.py):\n\n1. AC \"no longer raises NotImplementedError\ - \ when the operator advances past refine\": \u2705 Unchanged in v4. `run()`\ - \ at in_process.py:246 still calls `self._run_plan_phase(...)`; the walking-skeleton\ - \ fence still fires only on the plan HITL gate's `approve_continue` (slice-3\ - \ / slice-4 pointer intact).\n\n2. AC \"plan stage spawns 3 producers concurrently\ - \ via the executor\": \u2705 Unchanged in v4. Architect-first synchronous spawn\ - \ \u2192 `task_planner + risk_analyst` concurrent fan-out via `ThreadPoolExecutor(max_workers=2)`\ - \ (_plan_phase.py:124-161, unchanged in v4). Role-dependency-driven deviation\ - \ from literal \"3 concurrent\" is still grounded in `shared/egg_contracts/agent_roles.py`\ - \ declaring ARCHITECT as the sole dependency of TASK_PLANNER / RISK_ANALYST.\n\ - \n3. AC \"reviewer_plan is spawned after each CONSENSUS_PROPOSE\": \u2705 Materially\ - \ strengthened in v4. Reviewer dispatch and tracker advancement structure unchanged;\ - \ the verdict-parsing layer now correctly recognises the rubric-default shape.\ - \ A rubric-following `verdict: \"NACK\"` no longer falls into the optimistic-ACK\ - \ fallback that masked NACKs from the operator at the plan HITL gate (v3 silent\ - \ bug). The NACK now broadcasts to every producer edge with `feedback` propagated\ - \ as each edge's `reason` (_plan_phase.py:325-351) and an explicit synthetic\ - \ placeholder when `feedback` is empty to avoid hitting `ReviewPayload.validate_nack_has_reason`.\ - \ Per-edge ACK / NACK still drives `tracker.handle_ack` / `tracker.handle_nack`\ - \ per producer.\n\n4. AC \"yields a plan-HITL decision after CONSENSUS_CONFIRMED\ - \ on every producer edge\": \u2705 Unchanged in v4. `tracker.handle_confirmed`\ - \ for each role; `evaluate()` snapshot; `_build_plan_gate_decision` yields HITLDecision\ - \ with `phase=\"plan\"`. The schema-broadening at the parsing layer cannot regress\ - \ the CONSENSUS_CONFIRMED path because (a) ACK still acks all three on the broadcast\ - \ path \u2192 CONSENSUS_CONFIRMED reachable; (b) NACK paths surface in the eval\ - \ snapshot's `blocking_agents` exactly as before \u2014 the only difference\ - \ is they now surface for rubric-default JSON shapes too, which is a correctness\ - \ improvement.\n\n5. AC \"existing refine path still works\": \u2705 Unchanged.\ - \ Refine flow at in_process.py:213-240 untouched in v4.\n\n**TASK-2-2 \u2014\ - \ `_load_egg_sdlc_role_rubric` extension** (orchestrator/substrate/__init__.py):\ - \ unchanged in v4. ACs remain met.\n\n**TASK-2-5 \u2014 sandbox restrictions\ - \ parallel validator** (sandbox/egg_agent_tools/handlers/restrictions.py): unchanged\ - \ in v4 (still no edits). The \"R2 = pass \u2192 no-op\" close remains the operative\ - \ decision; coder commit message lineage preserves the required close-with-note.\n\ - \n### v4 surface-area assessment (informational)\n\n- The `read_plan_reviewer_verdicts(runner,\ - \ *, plan_producers=None)` signature change is backward-compatible (kwarg with\ - \ `None` default), and the `_read_plan_reviewer_verdicts` class-method delegate\ - \ on `_InProcessOrchestrator` propagates the new kwarg with the same default.\ - \ The tester's `test_inprocess_plan_brc.py` does not call this method directly\ - \ (it inspects `_plan_tracker.evaluate()` after the stage runs), so the existing\ - \ 16 passing test cases remain intact.\n- Legacy-caller safety: when `plan_producers=None`\ - \ and the JSON is single-verdict, the function returns `({}, verdict_path)`\ - \ and the orchestrator's fail-closed / optimistic-ACK heuristic in `_apply_reviewer_verdicts`\ - \ applies \u2014 preserves the historical behaviour for any out-of-tree caller.\n\ - - Schema 2 (per_producer wrapper) still takes precedence when present and well-formed\ - \ (_plan_phase.py:301-317), so an explicit per-edge verdict reviewer is not\ - \ surprised by silently-broadcast behaviour.\n\n### Non-blocking observations\ - \ carried forward from v3 review\n\n- Slice-1 contract bookkeeping (`task-1-1`\ - \ \u2026 `task-1-9` show `status: \"pending\"` despite linked commits) \u2014\ - \ informational; operator reconcile before declaring the rollout complete.\n\ - - `synthetic_commit_for(role)` SHA-1-derived prefix at _plan_phase.py:644-656\ - \ is unchanged; per-role distinguishability holds.\n- Schema-1 NACK reason placeholder\ - \ (\"reviewer_plan broadcast NACK: top-level verdict without a per-edge feedback\ - \ blob \u2014 see verdict JSON for the criteria-keyed analysis\") is operator-readable\ - \ and explicit; if a future regression test wants to pin the exact substring,\ - \ the runner's `_verdict_diagnostics` dict is the structured surface to assert\ - \ against.\n\nMarking v4 ACKed.\n" - ack_version: 4 - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:28Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d2e24485-f3c6-40 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:28.011717+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:28Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: ea2a3991-3c23-49 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:30Z] reviewer_security (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: ed925f8e-7bf1-48 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:30.366283+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:34Z] coder (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 089173ae-c460-45 -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:34.058965+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:39Z] reviewer_contract (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: af593ea0-034b-4a -phase: implement -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T20:35:39.094304+00:00' - slice_id: slice-2 -```` - -### [2026-05-19T20:35:52Z] reviewer_code_holistic → coder (CONSENSUS_ACK): ACK from reviewer_code_holistic for coder - - -Holistic ACK on coder slice-2 v4 (commit ecd8336b7). v3 blocker H3 is resolved: `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default single-verdict schema (broadcast to every producer edge) AND the per-producer wrapper (per-edge granularity), with the per-producer wrapper taking precedence when present. This is "Option (c)" from my v3 NACK with the per-producer wrapper retained as the documented extension shape — both code-paths reach the same `normalised` dict shape, so `_apply_reviewer_verdicts` and the rest of the BRC flow are unchanged. - -Re-ran all four mandatory passes against v4: - -- **Pass 1 (end-to-end primary use case):** Walked the path — refiner → "approve_continue" → architect synchronously → task_planner + risk_analyst fan-out → reviewer_plan → verdict-JSON parse → tracker per-edge ACK/NACK → plan-HITL yield. A rubric-following reviewer's NACK now propagates to the tracker. The `if not plan_producers: return verdict_path, {}` legacy-caller guard (line for the broadcast path) means tester tests that call `_read_plan_reviewer_verdicts()` without the kwarg still get the empty-dict + optimistic-ACK fallback — backwards compatible with the tester's existing assertions. -- **Pass 2 (doc ↔ code symmetry):** The rubric's documented JSON shape (`{"verdict": "ACK"|"NACK", "analysis": {...}, "feedback": ...}`) is now the primary parsed schema. The rubric's claim that the reviewer "ACK / NACK each producer independently" is weakened to "ACK / NACK all producers as a unit, with optional per-producer extension"; the per_producer wrapper is the explicit opt-in for the original per-edge granularity. The documenter may want to add a short note in `reviewer_plan.md` that the per_producer wrapper is the per-edge form (non-blocking; ACK does not depend on the documenter coordinating this turn). -- **Pass 3 (synthetic key / sentinel):** No new synthetic keys. The empty-feedback synthetic-placeholder NACK reason (lines: `f"reviewer_plan broadcast {top_verdict}: top-level verdict without a per-edge feedback blob — see verdict JSON for the criteria-keyed analysis."`) plugs the gap where `ReviewPayload.validate_nack_has_reason` would otherwise reject a NACK with empty feedback — i.e. a real "doc-claimed schema → tracker rejection → silent fallback" path that v4 prevents. -- **Pass 4 (silent fallback):** Verdict-typo silently falls back to optimistic-ACK if reviewer exit 0 (e.g. `{"verdict": "OK"}` returns empty verdicts → optimistic-ACK fires). Borderline; the placeholder body's verdict_path line surfaces the source file so an operator hitting the issue can `cat` the file and see the typo. Not worth blocking — and would be caught by the tester's planned v2 fixture exercising the new parser anyway. - -### Non-blocking - -- **Rubric needs a one-line note that `per_producer` is the per-edge extension.** Documenter-side. The reviewer rubric currently documents only the single-verdict shape; the per_producer wrapper is supported by code but undocumented for the reviewer-agent reader. A future reviewer might never know per-edge granularity is available. -- **`if not plan_producers: return verdict_path, {}` swallows a legitimate single-verdict NACK** when callers omit the kwarg. Today the only caller that omits it is the tester's existing class-method call site, which feeds the empty-verdicts result into the harness-fake path anyway. Worth a one-line docstring note that omitting `plan_producers` disables single-verdict broadcasting so a future caller doesn't mistakenly omit the kwarg and lose the reviewer's NACK. -- **Verdict-typo (e.g. `{"verdict": "OK"}`) silently optimistic-ACKs on reviewer exit 0.** Surfaced in the placeholder, but a `log_tracker_warning(verb="unknown_verdict", role_label=top_verdict, ...)`-equivalent on the unrecognised-verdict path would make the typo show up in the orchestrator log too. Low-priority polish. - -H1 (architect-first ordering), H2 (parsed verdict not exit-code-only), H3 (rubric-schema parser): all three blockers resolved. Non-blocking items above are polish; ACKing so the BRC barrier opens. - - -````yaml -id: 8d8ac0a2-46ec-4d -phase: implement -metadata: - payload: - artifact_references: - - orchestrator/substrate/_plan_phase.py - - orchestrator/substrate/in_process.py - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - reason: "\nHolistic ACK on coder slice-2 v4 (commit ecd8336b7). v3 blocker H3\ - \ is resolved: `read_plan_reviewer_verdicts` now accepts BOTH the rubric-default\ - \ single-verdict schema (broadcast to every producer edge) AND the per-producer\ - \ wrapper (per-edge granularity), with the per-producer wrapper taking precedence\ - \ when present. This is \"Option (c)\" from my v3 NACK with the per-producer\ - \ wrapper retained as the documented extension shape \u2014 both code-paths\ - \ reach the same `normalised` dict shape, so `_apply_reviewer_verdicts` and\ - \ the rest of the BRC flow are unchanged.\n\nRe-ran all four mandatory passes\ - \ against v4:\n\n- **Pass 1 (end-to-end primary use case):** Walked the path\ - \ \u2014 refiner \u2192 \"approve_continue\" \u2192 architect synchronously\ - \ \u2192 task_planner + risk_analyst fan-out \u2192 reviewer_plan \u2192 verdict-JSON\ - \ parse \u2192 tracker per-edge ACK/NACK \u2192 plan-HITL yield. A rubric-following\ - \ reviewer's NACK now propagates to the tracker. The `if not plan_producers:\ - \ return verdict_path, {}` legacy-caller guard (line for the broadcast path)\ - \ means tester tests that call `_read_plan_reviewer_verdicts()` without the\ - \ kwarg still get the empty-dict + optimistic-ACK fallback \u2014 backwards\ - \ compatible with the tester's existing assertions.\n- **Pass 2 (doc \u2194\ - \ code symmetry):** The rubric's documented JSON shape (`{\"verdict\": \"ACK\"\ - |\"NACK\", \"analysis\": {...}, \"feedback\": ...}`) is now the primary parsed\ - \ schema. The rubric's claim that the reviewer \"ACK / NACK each producer independently\"\ - \ is weakened to \"ACK / NACK all producers as a unit, with optional per-producer\ - \ extension\"; the per_producer wrapper is the explicit opt-in for the original\ - \ per-edge granularity. The documenter may want to add a short note in `reviewer_plan.md`\ - \ that the per_producer wrapper is the per-edge form (non-blocking; ACK does\ - \ not depend on the documenter coordinating this turn).\n- **Pass 3 (synthetic\ - \ key / sentinel):** No new synthetic keys. The empty-feedback synthetic-placeholder\ - \ NACK reason (lines: `f\"reviewer_plan broadcast {top_verdict}: top-level verdict\ - \ without a per-edge feedback blob \u2014 see verdict JSON for the criteria-keyed\ - \ analysis.\"`) plugs the gap where `ReviewPayload.validate_nack_has_reason`\ - \ would otherwise reject a NACK with empty feedback \u2014 i.e. a real \"doc-claimed\ - \ schema \u2192 tracker rejection \u2192 silent fallback\" path that v4 prevents.\n\ - - **Pass 4 (silent fallback):** Verdict-typo silently falls back to optimistic-ACK\ - \ if reviewer exit 0 (e.g. `{\"verdict\": \"OK\"}` returns empty verdicts \u2192\ - \ optimistic-ACK fires). Borderline; the placeholder body's verdict_path line\ - \ surfaces the source file so an operator hitting the issue can `cat` the file\ - \ and see the typo. Not worth blocking \u2014 and would be caught by the tester's\ - \ planned v2 fixture exercising the new parser anyway.\n\n### Non-blocking\n\ - \n- **Rubric needs a one-line note that `per_producer` is the per-edge extension.**\ - \ Documenter-side. The reviewer rubric currently documents only the single-verdict\ - \ shape; the per_producer wrapper is supported by code but undocumented for\ - \ the reviewer-agent reader. A future reviewer might never know per-edge granularity\ - \ is available.\n- **`if not plan_producers: return verdict_path, {}` swallows\ - \ a legitimate single-verdict NACK** when callers omit the kwarg. Today the\ - \ only caller that omits it is the tester's existing class-method call site,\ - \ which feeds the empty-verdicts result into the harness-fake path anyway. Worth\ - \ a one-line docstring note that omitting `plan_producers` disables single-verdict\ - \ broadcasting so a future caller doesn't mistakenly omit the kwarg and lose\ - \ the reviewer's NACK.\n- **Verdict-typo (e.g. `{\"verdict\": \"OK\"}`) silently\ - \ optimistic-ACKs on reviewer exit 0.** Surfaced in the placeholder, but a `log_tracker_warning(verb=\"\ - unknown_verdict\", role_label=top_verdict, ...)`-equivalent on the unrecognised-verdict\ - \ path would make the typo show up in the orchestrator log too. Low-priority\ - \ polish.\n\nH1 (architect-first ordering), H2 (parsed verdict not exit-code-only),\ - \ H3 (rubric-schema parser): all three blockers resolved. Non-blocking items\ - \ above are polish; ACKing so the BRC barrier opens.\n" - ack_version: 4 - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:52Z] orchestrator → coder (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 4) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: c6ddeae7-7aae-4b -phase: implement -metadata: - ready_to_confirm: true - version: 4 - slice_id: slice-2 -```` - -### [2026-05-19T20:35:52Z] coder (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 6a7417b9-dd51-48 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:55Z] reviewer_code_holistic (CONSENSUS_CONFIRMED): Confirmed by reviewer_code_holistic - -````yaml -id: 4cfd7c34-845d-46 -phase: implement -metadata: - consensus_reached: false - slice_id: slice-2 -```` - -### [2026-05-19T20:35:55Z] reviewer_concurrency (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 1b9b7bcd-fa04-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:55Z] reviewer_code (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 3197cf38-84f9-4f -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:55Z] reviewer_security (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 033abcb5-9cc6-46 -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:55Z] reviewer_contract (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 68fe0ba0-4e88-4d -phase: implement -metadata: - state: WORKING - slice_id: slice-2 -```` - -### [2026-05-19T20:35:56Z] coder (CONSENSUS_CONFIRMED): Confirmed by coder - -````yaml -id: f1ccbcf4-eaa6-43 -phase: implement -metadata: - consensus_reached: true - slice_id: slice-2 -```` diff --git a/.egg-state/brc-history/2717-plan.json b/.egg-state/brc-history/2717-plan.json deleted file mode 100644 index 72f15a5a5e..0000000000 --- a/.egg-state/brc-history/2717-plan.json +++ /dev/null @@ -1,1742 +0,0 @@ -[ - { - "id": "36495853-a746-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "Plan reviewer ready; analysis read; waiting for first producer proposal.", - "metadata": { - "state": "WAITING_FOR_EVENT" - }, - "timestamp": "2026-05-19T18:07:08.331368+00:00", - "phase": "plan" - }, - { - "id": "6a6f3fe6-bc9b-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:07:11.546878+00:00", - "phase": "plan" - }, - { - "id": "61cd5a31-369c-4f", - "pipeline_id": "issue-2717", - "from_role": "overseer", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "Cycle 1 complete. Pipeline healthy: plan phase active, 4 agents running (architect, reviewer_plan, risk_analyst, task_planner), all started ~56s ago. Orchestrator /status endpoint unreachable (transient timeout) but BRC state confirms pipeline is running. No corrective actions needed this cycle.", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:07:33.663452+00:00", - "phase": "plan" - }, - { - "id": "c3c66686-3d59-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:08:30.228171+00:00", - "phase": "plan" - }, - { - "id": "8a0d744a-b496-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:09:26.601992+00:00", - "phase": "plan" - }, - { - "id": "2bf0f365-5775-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:10:26.646247+00:00", - "phase": "plan" - }, - { - "id": "8ba95dda-2b0b-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:11:26.723574+00:00", - "phase": "plan" - }, - { - "id": "4674bbc2-8ccd-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:12:26.819730+00:00", - "phase": "plan" - }, - { - "id": "24d6cc65-d265-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:13:26.874836+00:00", - "phase": "plan" - }, - { - "id": "dd516713-6692-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:14:26.930375+00:00", - "phase": "plan" - }, - { - "id": "3434654a-5b2e-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:15:26.982364+00:00", - "phase": "plan" - }, - { - "id": "be9fcb5f-f1b2-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:07:11.523831+00:00" - }, - "timestamp": "2026-05-19T18:16:50.828433+00:00", - "phase": "plan" - }, - { - "id": "16ece202-49b5-46", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from architect", - "body": "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 via Option B flatten-stage-script MVP plus R2 hook role-resolution spike (2-subagent worked example) as the first deliverable; (2) Plan-phase substrate wiring (architect/task_planner/risk_analyst + reviewer_plan rubrics + run_pipeline_in_process plan branch); (3) Implement-phase substrate wiring (coder/tester/documenter + 5 reviewer rubrics + implement branch; cq-3 empirical data collected here, daemon-mode bridge transition lands here); (4) PR-phase substrate + 5-issue \u00d7 2-substrate conformance matrix (#2714, #2474, #2261, #2705, #2718) + scope-fence removal at orchestrator/substrate/in_process.py:820-826; (5) Parallel hardening pass with EggHarnessSpawner + `egg-orch local-run --issue N` CLI (DoD #5 feedback Q3 option A), cost cap EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 (cq-6 option 2), drop v0.x markers on claude-code-side modules only (cq-7 option 4), and ADR refresh (DoD #6). Includes runtime-primitive surfacing per issue #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). The analysis identifies 6 anticipated risks (R2 hook multi-subagent, R7 context budget, BRC concurrency on new substrate, cost runaway, decomposition conflict with #2261 slice-15, scope-fence removal blast radius) with mitigations, and 6 open questions for task_planner and risk_analyst to consume.", - "metadata": { - "payload": { - "summary": "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 via Option B flatten-stage-script MVP plus R2 hook role-resolution spike (2-subagent worked example) as the first deliverable; (2) Plan-phase substrate wiring (architect/task_planner/risk_analyst + reviewer_plan rubrics + run_pipeline_in_process plan branch); (3) Implement-phase substrate wiring (coder/tester/documenter + 5 reviewer rubrics + implement branch; cq-3 empirical data collected here, daemon-mode bridge transition lands here); (4) PR-phase substrate + 5-issue \u00d7 2-substrate conformance matrix (#2714, #2474, #2261, #2705, #2718) + scope-fence removal at orchestrator/substrate/in_process.py:820-826; (5) Parallel hardening pass with EggHarnessSpawner + `egg-orch local-run --issue N` CLI (DoD #5 feedback Q3 option A), cost cap EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 (cq-6 option 2), drop v0.x markers on claude-code-side modules only (cq-7 option 4), and ADR refresh (DoD #6). Includes runtime-primitive surfacing per issue #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). The analysis identifies 6 anticipated risks (R2 hook multi-subagent, R7 context budget, BRC concurrency on new substrate, cost runaway, decomposition conflict with #2261 slice-15, scope-fence removal blast radius) with mitigations, and 6 open questions for task_planner and risk_analyst to consume.", - "attestation": {}, - "artifacts": [ - ".egg-state/agent-outputs/2717-architect-output.json" - ], - "risk_considered": "Six risks documented: R2 hook role-resolution under nested dispatch (mitigation: slice-1 ships the 2-subagent spike + MCP-validator fallback BEFORE phase rollout); R7 subagent context budget at deeper-research roles (mitigation: slice-3 metrics collection drives a follow-up issue if regression > 50%); BRC concurrency on new substrate at multi-role scale (mitigation: slice-2 plan-phase tests cover INV-3/INV-5/INV-6 before slice-3's 8-role surface); cost runaway from 15-subagent NACK loops (mitigation: cq-6 option 2 cap at 200); #2261 slice-15 decomposition collision (mitigation: run_pipeline_in_process reuses ConcurrentPhaseExecutor + PeerConsensusTracker rather than touching orchestrator/routes/pipelines.py); scope-fence removal blast radius (mitigation: fence removed in slice-4 only after slices 2 and 3 have proven each phase independently).", - "commit_sha": "c4f4e0ccdb29d4ddfaac28ef833e64b002da44bd", - "files_changed": [ - ".egg-state/agent-outputs/2717-architect-output.json" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 1, - "commit_sha": "c4f4e0ccdb29d4ddfaac28ef833e64b002da44bd" - }, - "timestamp": "2026-05-19T18:17:25.432921+00:00", - "phase": "plan" - }, - { - "id": "7c027e5f-5038-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:17:25.488650+00:00", - "phase": "plan" - }, - { - "id": "4bd23c45-a132-4a", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:17:55.125844+00:00" - }, - "timestamp": "2026-05-19T18:18:20.944823+00:00", - "phase": "plan" - }, - { - "id": "45a45b8f-d482-40", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:17:55.125844+00:00" - }, - "timestamp": "2026-05-19T18:19:10.210198+00:00", - "phase": "plan" - }, - { - "id": "d59fbbc7-8ffc-47", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:17:55.125844+00:00" - }, - "timestamp": "2026-05-19T18:20:10.277865+00:00", - "phase": "plan" - }, - { - "id": "c11dc755-4d86-4e", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from risk_analyst", - "body": "Risk assessment for #2717 substrate-swap follow-up rollout. Enumerates 18 net-new risks (R17\u2013R34) covering: HITL bridge dual-architecture (R17, cq-1 Option C-hybrid), 15-rubric authorship + structural depth-gap closure (R18, #2622), parent-session strain under 8-way harness re-host (R19/R29, cq-3 deferred), reviewer-roster coverage gap (R20, feedback Q5 declined new roles), 5-issue conformance reproducibility (R21, feedback Q1 fixed set), #2261 slice-15 coordination (R22), cost-cap visibility at 200 invocations (R23, cq-6), hardening-wave 5-slice fan-out (R24), scope-fence partial-rollout fragility (R25, feedback Q6), EggHarnessSpawner as 3rd protocol implementation (R26, feedback Q3 Option A), MCP-validator fallback structural enforcement gap (R27, cq-6 option 2 fallback path), phase-sequential late-discovery rework (R28), Option (b) bridge concurrent-HITL aggregation (R30), 15-subagent trust-context scaling (R31, feedback Q4 declined extras), interface-stability marker timing (R32, cq-7), apply-phase DoD ambiguity (R33), and bridge-driver file-boundary risk (R34, #2474-class). Includes 10 runtime-primitive verifications, 6 trust-boundary shifts, 11 prioritized recommendations (REC1-REC11), 5 open questions for implement-phase HITL, and DoD-criterion-by-criterion risk mapping. Recommendation: PROCEED_WITH_MITIGATIONS \u2014 slice-0 R2 validation is the load-bearing gate for downstream slice gating.", - "metadata": { - "payload": { - "summary": "Risk assessment for #2717 substrate-swap follow-up rollout. Enumerates 18 net-new risks (R17\u2013R34) covering: HITL bridge dual-architecture (R17, cq-1 Option C-hybrid), 15-rubric authorship + structural depth-gap closure (R18, #2622), parent-session strain under 8-way harness re-host (R19/R29, cq-3 deferred), reviewer-roster coverage gap (R20, feedback Q5 declined new roles), 5-issue conformance reproducibility (R21, feedback Q1 fixed set), #2261 slice-15 coordination (R22), cost-cap visibility at 200 invocations (R23, cq-6), hardening-wave 5-slice fan-out (R24), scope-fence partial-rollout fragility (R25, feedback Q6), EggHarnessSpawner as 3rd protocol implementation (R26, feedback Q3 Option A), MCP-validator fallback structural enforcement gap (R27, cq-6 option 2 fallback path), phase-sequential late-discovery rework (R28), Option (b) bridge concurrent-HITL aggregation (R30), 15-subagent trust-context scaling (R31, feedback Q4 declined extras), interface-stability marker timing (R32, cq-7), apply-phase DoD ambiguity (R33), and bridge-driver file-boundary risk (R34, #2474-class). Includes 10 runtime-primitive verifications, 6 trust-boundary shifts, 11 prioritized recommendations (REC1-REC11), 5 open questions for implement-phase HITL, and DoD-criterion-by-criterion risk mapping. Recommendation: PROCEED_WITH_MITIGATIONS \u2014 slice-0 R2 validation is the load-bearing gate for downstream slice gating.", - "attestation": {}, - "artifacts": [ - ".egg-state/agent-outputs/2717-risk_analyst-output.json" - ], - "risk_considered": "18 distinct risks across security, performance, design, and compatibility dimensions; 8 flagged for human review (R17 bridge dual-architecture, R18 rubric depth closure, R19 parent-session strain, R21 conformance reproducibility, R27 MCP-validator fallback structural gap, R29 implement-phase context budget, R31 15-subagent trust scaling, R33 apply-phase DoD scope). Each risk includes mitigation strategy, effort, residual-risk level, and (where flagged) suggested reviewer and review reason.", - "commit_sha": "7a5cec59d3ec5170922b1688703d24b072d7f50a", - "files_changed": [ - ".egg-state/agent-outputs/2717-risk_analyst-output.json" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 1, - "commit_sha": "7a5cec59d3ec5170922b1688703d24b072d7f50a" - }, - "timestamp": "2026-05-19T18:21:02.812699+00:00", - "phase": "plan" - }, - { - "id": "1f356f95-c13e-4d", - "pipeline_id": "issue-2717", - "from_role": "overseer", - "to_role": "all", - "message_type": "OVERSEER_ALERT", - "subject": "agent-heartbeat-stall [low]", - "body": "Orchestrator REST API experiencing recurring ~5-7 min intermittent outages on issue-2717\n\nDetail:\nThe orchestrator HTTP API at orchestrator.egg-system.svc.cluster.local:9849 has timed out 4 times during this overseer session (at ~18:06, ~18:15, ~18:19, ~18:20 UTC), each lasting 12-20 seconds. This causes spurious overseer heartbeat_timeout and progress_stall alerts (4 stale alerts now visible in monitor output), and prevents the overseer from registering its own health signals during outage windows. The BRC protocol channel is unaffected \u2014 pipeline agents are communicating normally and pipeline progression continues. The architect has proposed at 18:17:25 and reviewer_plan is actively reviewing. No pipeline agents have been impacted by the API outages.\n\nRecommended action:\nInvestigate orchestrator pod health and any pod restarts or GC pauses around 18:06, 18:15, 18:19 UTC. Consider increasing the heartbeat threshold for the overseer role from 120s to 180s to absorb these transient gaps without generating false alerts.", - "metadata": {}, - "timestamp": "2026-05-19T18:21:31.662300+00:00", - "phase": "plan" - }, - { - "id": "63c3a966-38bd-42", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:17:55.125844+00:00" - }, - "timestamp": "2026-05-19T18:21:31.789447+00:00", - "phase": "plan" - }, - { - "id": "3cb8c405-d404-48", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:17.729796+00:00" - }, - "timestamp": "2026-05-19T18:21:31.790299+00:00", - "phase": "plan" - }, - { - "id": "2546d6c5-0be8-4c", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:21:32.464467+00:00", - "phase": "plan" - }, - { - "id": "cafe3b14-4276-40", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:22:02.319472+00:00", - "phase": "plan" - }, - { - "id": "d22dbf8a-a9e2-4f", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from task_planner", - "body": "Plan for #2717 substrate-swap follow-up rollout. Decomposes the work into a strictly linear 5-slice DAG (each slice has one DAG parent) per cq-2 = Option 3 (Bridge-first then phase-sequential rollout): slice-1 closes the heredoc-HITL bridge gap via flattened stage scripts and ships the cq-5 R2 hook-validation early spike alongside the two refine-team reviewer rubrics; slice-2 wires the plan-phase substrate (architect + task_planner + risk_analyst + reviewer_plan) as the first multi-role BRC stress test; slice-3 wires the implement-phase substrate (3 producers + 5 reviewers) and switches the HITL bridge to a long-lived Python daemon over a UNIX socket (cq-1 Option C, implement half); slice-4 wires the pr-phase, removes the walking-skeleton scope fence (feedback Q6), and ships the 5-issue conformance matrix (#2714, #2474, #2261, #2705, #2718 \u2014 feedback Q1) with pytest.mark.slow gating + 3-hour per-phase ceiling (feedback Q2); slice-5 hardens with the EGG_PIPELINE_MAX_AGENT_INVOCATIONS cost cap default 200 (cq-6), EggHarnessSpawner + egg-orch local-run headless CLI (DoD #5 / feedback Q3), fork-based delegation (cq-10 deferred half from #2623), contingent R15 model-(b) migration based on slice-1's R2 verdict (cq-4), drops the v0.x unstable marker on the four protocols (cq-7), and refreshes the ADR. 42 contract tasks total. Primitives audit per #2594 cites every named symbol with file:line evidence; NEW primitives are tagged (NEW \u2014 TASK-X-Y). Trust-boundary scope: conformance tests live under integration_tests/regression/ (substrate-portable, the substrate fixture at conftest.py:623 skips claude-code dimension in in-sandbox-agent context), not under integration_tests/local_pipeline/ (kubectl-gated). Role assignments: coder for orchestrator/substrate/* Python and orchestrator/cli.py, tester for tests/, documenter for plugins/egg-sdlc/skills/egg-sdlc/agents/*.md role rubrics + SKILL.md + ADR.", - "metadata": { - "payload": { - "summary": "Plan for #2717 substrate-swap follow-up rollout. Decomposes the work into a strictly linear 5-slice DAG (each slice has one DAG parent) per cq-2 = Option 3 (Bridge-first then phase-sequential rollout): slice-1 closes the heredoc-HITL bridge gap via flattened stage scripts and ships the cq-5 R2 hook-validation early spike alongside the two refine-team reviewer rubrics; slice-2 wires the plan-phase substrate (architect + task_planner + risk_analyst + reviewer_plan) as the first multi-role BRC stress test; slice-3 wires the implement-phase substrate (3 producers + 5 reviewers) and switches the HITL bridge to a long-lived Python daemon over a UNIX socket (cq-1 Option C, implement half); slice-4 wires the pr-phase, removes the walking-skeleton scope fence (feedback Q6), and ships the 5-issue conformance matrix (#2714, #2474, #2261, #2705, #2718 \u2014 feedback Q1) with pytest.mark.slow gating + 3-hour per-phase ceiling (feedback Q2); slice-5 hardens with the EGG_PIPELINE_MAX_AGENT_INVOCATIONS cost cap default 200 (cq-6), EggHarnessSpawner + egg-orch local-run headless CLI (DoD #5 / feedback Q3), fork-based delegation (cq-10 deferred half from #2623), contingent R15 model-(b) migration based on slice-1's R2 verdict (cq-4), drops the v0.x unstable marker on the four protocols (cq-7), and refreshes the ADR. 42 contract tasks total. Primitives audit per #2594 cites every named symbol with file:line evidence; NEW primitives are tagged (NEW \u2014 TASK-X-Y). Trust-boundary scope: conformance tests live under integration_tests/regression/ (substrate-portable, the substrate fixture at conftest.py:623 skips claude-code dimension in in-sandbox-agent context), not under integration_tests/local_pipeline/ (kubectl-gated). Role assignments: coder for orchestrator/substrate/* Python and orchestrator/cli.py, tester for tests/, documenter for plugins/egg-sdlc/skills/egg-sdlc/agents/*.md role rubrics + SKILL.md + ADR.", - "attestation": {}, - "artifacts": [ - ".egg-state/drafts/2717-plan.md" - ], - "risk_considered": "Slice-3 is the largest (~10 tasks covering 8 role rubrics + multi-cycle BRC + daemon bridge); plan reviewer may flag it as advisory only per the size guidance (>1000 LOC potential). The R2 contingency in TASK-2-5/TASK-2-7 and TASK-5-7 creates a fork in the plan based on slice-1 outcome \u2014 chose to encode the contingency in task descriptions rather than splitting into two separate plans because the fork is small (model-(a) stays vs. model-(b) migration + MCP-validator-side enforcement). Linear chain has no fan-out and no serialized_chain_order, so the forest constraint is satisfied trivially. Soft size guidance only; no DAG-shape risk.", - "commit_sha": "00ef3ff05", - "files_changed": [ - ".egg-state/drafts/2717-plan.md" - ], - "tests_run": [], - "tasks_satisfied": [ - "TASK-1-1", - "TASK-1-2", - "TASK-1-3", - "TASK-1-4", - "TASK-1-5", - "TASK-1-6", - "TASK-1-7", - "TASK-1-8", - "TASK-2-1", - "TASK-2-2", - "TASK-2-3", - "TASK-2-4", - "TASK-2-5", - "TASK-2-6", - "TASK-2-7", - "TASK-3-1", - "TASK-3-2", - "TASK-3-3", - "TASK-3-4", - "TASK-3-5", - "TASK-3-6", - "TASK-3-7", - "TASK-3-8", - "TASK-3-9", - "TASK-3-10", - "TASK-4-1", - "TASK-4-2", - "TASK-4-3", - "TASK-4-4", - "TASK-4-5", - "TASK-4-6", - "TASK-4-7", - "TASK-5-1", - "TASK-5-2", - "TASK-5-3", - "TASK-5-4", - "TASK-5-5", - "TASK-5-6", - "TASK-5-7", - "TASK-5-8", - "TASK-5-9", - "TASK-5-10" - ] - }, - "version": 1, - "commit_sha": "00ef3ff05" - }, - "timestamp": "2026-05-19T18:22:02.946162+00:00", - "phase": "plan" - }, - { - "id": "f1c6f6d2-44cc-4a", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "risk_analyst", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1 - }, - "timestamp": "2026-05-19T18:22:02.946201+00:00", - "phase": "plan" - }, - { - "id": "5962933b-24ea-43", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:22:02.985166+00:00", - "phase": "plan" - }, - { - "id": "1aacc57b-fc67-43", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by risk_analyst", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T18:22:33.010709+00:00", - "phase": "plan" - }, - { - "id": "d9b0ea07-a953-4c", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:22:33.022789+00:00", - "phase": "plan" - }, - { - "id": "74910704-1586-41", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:23:02.601365+00:00", - "phase": "plan" - }, - { - "id": "a2dfc76c-7278-4b", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:23:02.644475+00:00", - "phase": "plan" - }, - { - "id": "e83ace58-d0f4-41", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:23:34.746790+00:00", - "phase": "plan" - }, - { - "id": "f72e1946-13b3-49", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:23:53.471901+00:00", - "phase": "plan" - }, - { - "id": "95b557a3-2ef4-47", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:24:02.688697+00:00", - "phase": "plan" - }, - { - "id": "8a56f99d-e0d3-44", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:24:34.794360+00:00", - "phase": "plan" - }, - { - "id": "294334f7-398d-44", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:24:53.562708+00:00", - "phase": "plan" - }, - { - "id": "105318fe-f664-4c", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:25:02.742080+00:00", - "phase": "plan" - }, - { - "id": "b3692c59-24be-49", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:25:34.845114+00:00", - "phase": "plan" - }, - { - "id": "b98a716d-cf60-40", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:25:53.606166+00:00", - "phase": "plan" - }, - { - "id": "8eb8cc80-6a09-4a", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:26:02.792772+00:00", - "phase": "plan" - }, - { - "id": "8ab99be4-ba1a-49", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:26:34.892512+00:00", - "phase": "plan" - }, - { - "id": "37bc1ec7-6594-49", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:26:53.696732+00:00", - "phase": "plan" - }, - { - "id": "e0d82584-ebbe-43", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:27:02.909643+00:00", - "phase": "plan" - }, - { - "id": "37cdc7fc-5dad-46", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:27:34.954533+00:00", - "phase": "plan" - }, - { - "id": "81f7bdbd-f639-40", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:27:53.743412+00:00", - "phase": "plan" - }, - { - "id": "5bf3bfb8-5f4e-43", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:28:02.965768+00:00", - "phase": "plan" - }, - { - "id": "dcded3f4-15ae-42", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:28:35.012843+00:00", - "phase": "plan" - }, - { - "id": "a7da5452-f816-46", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:28:53.829532+00:00", - "phase": "plan" - }, - { - "id": "00d50ad1-2772-41", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:29:03.062956+00:00", - "phase": "plan" - }, - { - "id": "d683a567-34df-43", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:29:35.272716+00:00", - "phase": "plan" - }, - { - "id": "265f3444-e165-48", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:29:53.889972+00:00", - "phase": "plan" - }, - { - "id": "5dcd3770-6620-49", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:30:03.225644+00:00", - "phase": "plan" - }, - { - "id": "bb66bc0a-e351-46", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:30.428015+00:00" - }, - "timestamp": "2026-05-19T18:30:35.328182+00:00", - "phase": "plan" - }, - { - "id": "745e0e48-101a-44", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:22:38.425238+00:00" - }, - "timestamp": "2026-05-19T18:30:54.017586+00:00", - "phase": "plan" - }, - { - "id": "99a94500-45fb-40", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:21:44.108650+00:00" - }, - "timestamp": "2026-05-19T18:31:03.316175+00:00", - "phase": "plan" - }, - { - "id": "4d2526d6-39a3-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "architect", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_plan for architect", - "body": "\nReviewed `.egg-state/agent-outputs/2717-architect-output.json` (389 lines) against the refine analysis at `.egg-state/drafts/2717-analysis.md` and the seven settled HITL decisions (cq-1 \u2026 cq-7) plus six feedback answers.\n\n### What's solid\n- **Slice decomposition matches cq-2 Option 3 exactly**: bridge \u2192 plan \u2192 implement \u2192 pr+conformance \u2192 hardening, with the operator's chosen \"phase-sequential then parallel hardening\" shape preserved across all five slices.\n- **Every primitive cited has file:line evidence**, and the citations I verified (run_pipeline_in_process at `in_process.py:84` \u2713, ClaudeCodeSpawner at `spawner.py:64` \u2713, _load_egg_sdlc_role_rubric at `__init__.py:232` \u2713, build_agent_patterns at `patterns.py:768` \u2713, _PHASE_ROLES at `agent_roles.py:1172` \u2713, _PHASE_REVIEWERS at `:1183` \u2713, ConcurrentPhaseExecutor at `concurrent_executor.py:114` \u2713, PeerConsensusTracker at `peer_consensus.py:69` \u2713, validate_invariants at `action_guards.py:631` \u2713, substrate fixture at `conftest.py:622\u2013642` \u2713) all line up with the actual code.\n- **NEW primitives are flagged**: `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`, `egg-orch local-run`, the conformance matrix, the bridge driver are correctly marked as new, and I verified the negative-grep claims (`grep -rn EGG_PIPELINE_MAX_AGENT_INVOCATIONS orchestrator/ shared/` returns zero \u2713; `cmd_local_run` is absent from `orchestrator/cli.py` per `grep -n \"add_parser.*local-run\"` \u2713).\n- **All six feedback Q answers absorbed verbatim** (curated 5 issues #2714/#2474/#2261/#2705/#2718, pytest.mark.slow + 3-hr ceiling, full-local-run Option A, no extra mitigations, no new reviewer roles, fence removal).\n- **Risks well-anticipated**: R2-hook-multi-subagent, R7-context-budget, BRC-concurrency-on-new-substrate, cost-runaway, decomposition-conflict-with-2261, scope-fence-removal-blast-radius all map to concrete mitigations referencing the slice they live in.\n- **`open_questions_for_task_planner_and_risk_analyst` is a genuinely useful handoff** \u2014 the granularity question, the R2 spike scope question, the rollback-flag question are all things the downstream producers should answer rather than the architect prematurely fixing.\n\n### Non-blocking\n- **R2 spike framing implicitly requires partial Agent-tool dispatch in slice-1 that contradicts cq-3.** `slice_1_bridge_plus_r2_spike.deliverables[2]` says \"spawns one role (parent) and has it dispatch a second role (child) via Claude Code's Agent tool (subagent_type='general-purpose')\". I verified that PreToolUseHookPolicy / hook_entry is NOT invoked by the harness path \u2014 `grep -rn \"PreToolUseHookPolicy\\|hook_entry\" shared/egg_harness/` returns zero hits \u2014 so Agent-tool dispatch is the only mechanism that actually triggers the hook. cq-3 selected \"stay on harness re-host\"; the R2 spike therefore needs a minimal Agent-tool dispatch path that the production spawner doesn't use. Either acknowledge this as a slice-1 deliverable (\"ship a minimal Agent-tool dispatch path for the spike only, distinct from the production ClaudeCodeSpawner\") or pivot to a hook-logic unit test (call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE` env vars \u2014 that's not literally \"nested dispatch\" but it does answer the load-bearing question \"does the hook resolve EGG_AGENT_ROLE correctly\"). As-is the analysis is internally consistent, but the task_planner has already diverged from this framing (their TASK-1-5 says \"via the harness\", which doesn't fire the hook at all \u2014 I'm NACKing them on this separately).\n- **Citation accuracy nits** \u2014 these are off by a few lines:\n - `AgentSpawner Protocol` at `spawner.py:73` \u2192 actual `:74`\n - `MessageBus Protocol` at `message_bus.py:27` \u2192 actual `:28`\n - `PolicyEnforcer Protocol` at `policy.py:25` \u2192 actual `:26`\n - `WorktreeManager Protocol` at `worktree.py:24` \u2192 actual `:25`\n - `SubstrateBundle` at `__init__.py:77` \u2192 actual `:78`\n - `_maybe_fence at :820-826` \u2192 the def lives at `:807`; `:821` is the `raise` inside it; `:212` is the call site.\n None of these change the semantics \u2014 but if a downstream slice grep-pins on the cited line it will miss by one.\n- **slice_3 cq-3 empirical data path naming is good** \u2014 `.egg-state/conformance-metrics/-.json` is the right shape, but the task_planner picked a sibling path `.egg-state//substrate-metrics.json` in TASK-3-9. Worth aligning the two so slice-5's cost-cap consumer reads from one canonical location.\n- **`policy_single_source_of_truth` framing mislabels gateway as \"trusted-CI-runner\"**. `gateway/phase_filter.py:1061` is a deployed-pod git-push validator running in the gateway container, not a pytest-context check. The trust-boundary categorisation in `docs/architecture/integration-test-trust-boundary.md` doesn't have a \"gateway-mediated agent operations\" tier today; calling it \"trusted-CI-runner\" muddles the categories. Not load-bearing for the plan, but the ADR refresh in TASK-5-9 should fix this.\n- **`#2261 slice-15` rollout-dependency framing is right** (\"decoupled at the file level\") \u2014 but slice-4's TASK-4-1 says `_run_pr` \"Reuses the post-#2685 context-PR + PR-phase plumbing in `orchestrator/routes/pipelines.py`\". If that plumbing moves into `orchestrator/routes/pipelines/_run_loop/_run_pr.py` mid-rollout, slice-4's import path will need to follow the decomposition. Worth flagging as a coordination point with #2261 owners.\n\nArchitectural design is sound, alignment with the analysis is complete, and the slice DAG is a strictly-linear chain (no forest violation possible). Approving so the task_planner re-proposal cycle can proceed.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/agent-outputs/2717-architect-output.json", - ".egg-state/drafts/2717-analysis.md" - ], - "reason": "\nReviewed `.egg-state/agent-outputs/2717-architect-output.json` (389 lines) against the refine analysis at `.egg-state/drafts/2717-analysis.md` and the seven settled HITL decisions (cq-1 \u2026 cq-7) plus six feedback answers.\n\n### What's solid\n- **Slice decomposition matches cq-2 Option 3 exactly**: bridge \u2192 plan \u2192 implement \u2192 pr+conformance \u2192 hardening, with the operator's chosen \"phase-sequential then parallel hardening\" shape preserved across all five slices.\n- **Every primitive cited has file:line evidence**, and the citations I verified (run_pipeline_in_process at `in_process.py:84` \u2713, ClaudeCodeSpawner at `spawner.py:64` \u2713, _load_egg_sdlc_role_rubric at `__init__.py:232` \u2713, build_agent_patterns at `patterns.py:768` \u2713, _PHASE_ROLES at `agent_roles.py:1172` \u2713, _PHASE_REVIEWERS at `:1183` \u2713, ConcurrentPhaseExecutor at `concurrent_executor.py:114` \u2713, PeerConsensusTracker at `peer_consensus.py:69` \u2713, validate_invariants at `action_guards.py:631` \u2713, substrate fixture at `conftest.py:622\u2013642` \u2713) all line up with the actual code.\n- **NEW primitives are flagged**: `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`, `egg-orch local-run`, the conformance matrix, the bridge driver are correctly marked as new, and I verified the negative-grep claims (`grep -rn EGG_PIPELINE_MAX_AGENT_INVOCATIONS orchestrator/ shared/` returns zero \u2713; `cmd_local_run` is absent from `orchestrator/cli.py` per `grep -n \"add_parser.*local-run\"` \u2713).\n- **All six feedback Q answers absorbed verbatim** (curated 5 issues #2714/#2474/#2261/#2705/#2718, pytest.mark.slow + 3-hr ceiling, full-local-run Option A, no extra mitigations, no new reviewer roles, fence removal).\n- **Risks well-anticipated**: R2-hook-multi-subagent, R7-context-budget, BRC-concurrency-on-new-substrate, cost-runaway, decomposition-conflict-with-2261, scope-fence-removal-blast-radius all map to concrete mitigations referencing the slice they live in.\n- **`open_questions_for_task_planner_and_risk_analyst` is a genuinely useful handoff** \u2014 the granularity question, the R2 spike scope question, the rollback-flag question are all things the downstream producers should answer rather than the architect prematurely fixing.\n\n### Non-blocking\n- **R2 spike framing implicitly requires partial Agent-tool dispatch in slice-1 that contradicts cq-3.** `slice_1_bridge_plus_r2_spike.deliverables[2]` says \"spawns one role (parent) and has it dispatch a second role (child) via Claude Code's Agent tool (subagent_type='general-purpose')\". I verified that PreToolUseHookPolicy / hook_entry is NOT invoked by the harness path \u2014 `grep -rn \"PreToolUseHookPolicy\\|hook_entry\" shared/egg_harness/` returns zero hits \u2014 so Agent-tool dispatch is the only mechanism that actually triggers the hook. cq-3 selected \"stay on harness re-host\"; the R2 spike therefore needs a minimal Agent-tool dispatch path that the production spawner doesn't use. Either acknowledge this as a slice-1 deliverable (\"ship a minimal Agent-tool dispatch path for the spike only, distinct from the production ClaudeCodeSpawner\") or pivot to a hook-logic unit test (call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE` env vars \u2014 that's not literally \"nested dispatch\" but it does answer the load-bearing question \"does the hook resolve EGG_AGENT_ROLE correctly\"). As-is the analysis is internally consistent, but the task_planner has already diverged from this framing (their TASK-1-5 says \"via the harness\", which doesn't fire the hook at all \u2014 I'm NACKing them on this separately).\n- **Citation accuracy nits** \u2014 these are off by a few lines:\n - `AgentSpawner Protocol` at `spawner.py:73` \u2192 actual `:74`\n - `MessageBus Protocol` at `message_bus.py:27` \u2192 actual `:28`\n - `PolicyEnforcer Protocol` at `policy.py:25` \u2192 actual `:26`\n - `WorktreeManager Protocol` at `worktree.py:24` \u2192 actual `:25`\n - `SubstrateBundle` at `__init__.py:77` \u2192 actual `:78`\n - `_maybe_fence at :820-826` \u2192 the def lives at `:807`; `:821` is the `raise` inside it; `:212` is the call site.\n None of these change the semantics \u2014 but if a downstream slice grep-pins on the cited line it will miss by one.\n- **slice_3 cq-3 empirical data path naming is good** \u2014 `.egg-state/conformance-metrics/-.json` is the right shape, but the task_planner picked a sibling path `.egg-state//substrate-metrics.json` in TASK-3-9. Worth aligning the two so slice-5's cost-cap consumer reads from one canonical location.\n- **`policy_single_source_of_truth` framing mislabels gateway as \"trusted-CI-runner\"**. `gateway/phase_filter.py:1061` is a deployed-pod git-push validator running in the gateway container, not a pytest-context check. The trust-boundary categorisation in `docs/architecture/integration-test-trust-boundary.md` doesn't have a \"gateway-mediated agent operations\" tier today; calling it \"trusted-CI-runner\" muddles the categories. Not load-bearing for the plan, but the ADR refresh in TASK-5-9 should fix this.\n- **`#2261 slice-15` rollout-dependency framing is right** (\"decoupled at the file level\") \u2014 but slice-4's TASK-4-1 says `_run_pr` \"Reuses the post-#2685 context-PR + PR-phase plumbing in `orchestrator/routes/pipelines.py`\". If that plumbing moves into `orchestrator/routes/pipelines/_run_loop/_run_pr.py` mid-rollout, slice-4's import path will need to follow the decomposition. Worth flagging as a coordination point with #2261 owners.\n\nArchitectural design is sound, alignment with the analysis is complete, and the slice DAG is a strictly-linear chain (no forest violation possible). Approving so the task_planner re-proposal cycle can proceed.\n", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-19T18:31:22.447343+00:00", - "phase": "plan" - }, - { - "id": "5494de46-183c-46", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "architect", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1 - }, - "timestamp": "2026-05-19T18:31:22.447409+00:00", - "phase": "plan" - }, - { - "id": "573f79a9-099e-40", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:31:22.512008+00:00", - "phase": "plan" - }, - { - "id": "e8f370af-a993-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "task_planner", - "message_type": "CONSENSUS_NACK", - "subject": "NACK from reviewer_plan for task_planner", - "body": "\nReviewed `.egg-state/drafts/2717-plan.md` (1394 lines, 5 slices, 42 tasks) and cross-checked against the architect's design, the refine analysis, and the actual codebase. The slice shape is right (matches cq-2 Option 3), the role\u2194files alignment is clean (I ran the per-file/per-role check across all 42 tasks; every task's `files:` is writable by its `role:` per `shared/egg_restrictions/patterns.py` \u2014 no orchestrator-side `403 restricted_path_modified` predicted), and the primitives table is unusually thorough. But three blocking issues will produce false signals or unimplementable tasks if shipped as-is.\n\n### Blocking\n\n1. **TASK-1-5 R2 spike mechanism is structurally broken \u2014 the test will not exercise the PreToolUse hook at all.** The task says \"Test dispatches a parent subagent (role=architect) and a child subagent (role=tester) via the harness; asserts the PreToolUse hook denies a write to `orchestrator/foo.py` when invoked from the child\". I verified that `shared/egg_harness/client.py:60-150` registers its own `ToolRegistry` with Python-native Bash/Read/Write/Edit/Glob/Grep handlers and routes permission checks through `registry.set_permission_callback(callback)` \u2014 `grep -rn \"PreToolUseHookPolicy\\|hook_entry\" shared/egg_harness/` returns **zero hits**. The PreToolUse hook is invoked exclusively by Claude Code's native tool registry (see `orchestrator/substrate/claude_code/hook_entry.py:17` `\"command\": \"python3 -m orchestrator.substrate.claude_code.hook_entry\"` \u2014 that's Claude Code's PreToolUse settings.json wiring). Therefore: a harness-dispatched child never triggers `hook_entry.decide()`, and the assertion \"the PreToolUse hook denies a write\" cannot fail (the hook isn't in the loop) and cannot pass (the hook isn't in the loop) \u2014 the test will either green on a no-op or assert against a manually-invoked `hook_entry.decide(...)` that has nothing to do with \"nested dispatch\". This is the load-bearing test for the rollout: cq-4 (R15 migration timing) and cq-6 fallback (MCP-validator enforcement, TASK-2-5 and TASK-5-7) both branch on its verdict. **Fix**: pick one of (a) call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE` env vars and rename the task to make clear it's a hook-logic unit test, not a nested-dispatch test (cheap, doesn't answer the architect's question about Agent-tool nested dispatch, but is honest about scope); (b) ship a minimal Claude Code Agent-tool dispatch path *just for the spike* (precursor of ClaudeCodeAgentToolSpawner, distinct from the production `ClaudeCodeSpawner` harness re-host) and run the parent/child dispatch through it \u2014 that does exercise the hook but adds a slice-1 deliverable the plan currently doesn't enumerate. The architect's text picked option (b) implicitly; you picked option (a)'s file location but kept option (b)'s framing. Pick one and update the task description, acceptance criteria, and dependent contingency tasks (TASK-2-5, TASK-5-7) to match.\n\n2. **TASK-4-4 references \"recorded transcripts\" that no task records.** The acceptance text says \"via harness fakes that replay a recorded transcript of that issue's refine + plan + implement + pr phases\". I searched the codebase: there is no existing transcript-replay infrastructure (the existing `integration_tests/regression/test_substrate_smoke.py:56` uses `MagicMock` with hardcoded `stdout=\"ok\"`, not a recorded-transcript replay). No task in slices 1\u20134 records transcripts for the five curated issues; no task in slice 5 either. As specified, TASK-4-4 cannot be implemented \u2014 its `files:` says `test_conformance_matrix.py` but the test body needs 4 phases \u00d7 5 issues = 20 transcripts that don't exist and aren't produced anywhere in the plan. **Fix**: either (a) add a sub-task that records transcripts during slice-3's first real implement-phase run on each curated issue (the natural place \u2014 slice-3 already produces `.egg-state//substrate-metrics.json` per TASK-3-9, so transcript capture co-locates well); (b) change the mechanism to per-subagent `MagicMock` matching `test_substrate_smoke.py`'s pattern (cheaper, but you lose phase-level realism); or (c) acknowledge the matrix runs subagents live in CI and adjust the 3-hour-per-phase ceiling story accordingly. Whichever path, the plan needs an explicit task that produces the inputs TASK-4-4 consumes.\n\n3. **TASK-4-2 fence-removal task cites the wrong line.** The task description says \"Remove the walking-skeleton scope fence at `orchestrator/substrate/in_process.py:212`\". I checked: `:212` is the *call site* (`self._maybe_fence(answer)` inside `_run`). The `_maybe_fence` static-method *definition* is at `:807-826`. Both need to be removed to actually delete the fence \u2014 and the docstring at `:92-125` that promises `NotImplementedError` for plan/implement/pr (which the acceptance already says to update). The current acceptance text says \"delete `_maybe_fence`\" which is right, but the cited line points the coder at the call site only; a coder following the cite literally would leave the def around as dead code. **Fix**: cite both `:212` (call site, delete the call) and `:807-826` (def, delete the method) explicitly in the description so the coder doesn't have to grep.\n\n### Non-blocking\n\n- **TASK-5-5 fork primitive contradicts cq-3.** Description: \"fork spawns a child Claude Code subagent (via the Agent tool with `subagent_type='general-purpose'`)\". cq-3 resolved to \"decide empirically at implement-phase\" \u2014 i.e. *stay on harness re-host* for the rollout, decide later. Shipping a fork primitive that uses Agent-tool dispatch is a structural commitment to Agent-tool capability, which contradicts the operator's deferral. Either (a) acknowledge in TASK-5-5's description that this is a partial Agent-tool migration the operator should re-confirm before slice 5 ships, or (b) implement fork via `subprocess.Popen` of a fresh `egg_harness.run_agent` so it stays on the harness re-host model. Risk_analyst's R29 mitigation also expects fork to land structurally \u2014 flag the cq-3 tension in the slice-5 ADR refresh (TASK-5-9).\n\n- **TASK-2-5 enforcement target may be the wrong file.** Description says enforcement lands in `orchestrator/mcp_tools.py`. The MCP file-restriction tool today is `mcp__sdlc__check_file_restriction`, whose handler implementation lives at `sandbox/egg_agent_tools/handlers/restrictions.py:6-70` (`check_file_restriction(req)` \u2014 verified by `grep -rn \"check_file_restriction\" --include=\"*.py\" orchestrator/ sandbox/`). `orchestrator/mcp_tools.py` exists but its tool surface is different (it's the orchestrator-side MCP server, registering `mcp__progress__*`, `mcp__sdlc__*`, etc. tools \u2014 not the in-sandbox agent's tool boundary). The validator-side fallback (cq-6 option 2) is about agent-side tool calls denying cross-role writes; that surface is `sandbox/egg_agent_tools/handlers/restrictions.py`, not `orchestrator/mcp_tools.py`. **Fix**: confirm which surface you want and update `files:` accordingly; if both surfaces need wiring, split into two tasks.\n\n- **TASK-3-9 metrics path conflicts with the architect's path.** Architect picked `.egg-state/conformance-metrics/-.json`; you picked `.egg-state//substrate-metrics.json`. TASK-5-1's cost-cap and TASK-4-5's phase-ceiling hook both read from your path; the architect's path goes unread. Align \u2014 pick one canonical path and use it in all four references (TASK-3-9, TASK-4-5, TASK-5-1, ADR in TASK-5-9).\n\n- **TASK-3-2 daemon lifecycle is underspecified.** Acceptance says \"Daemon starts on first `python3 bin/run_pipeline.py --daemon` invocation; binds to `.egg-state//sock`\". But each `python3 bin/run_pipeline.py --daemon` call is a child process of the skill's `Bash(python3 ...)` step. If the daemon doesn't detach (via `subprocess.Popen(start_new_session=True)` or equivalent), the daemon dies when the calling Bash exits \u2014 defeating the \"long-lived\" property. The plan should state explicitly how the daemon detaches and survives across multiple skill-Bash invocations. Cite `os.setsid` / `start_new_session=True` or whatever mechanism you choose.\n\n- **TASK-1-7 / TASK-1-6 ordering**: TASK-1-6 modifies `_load_egg_sdlc_role_rubric` to load `reviewer_refine` and `reviewer_agent_design`, but TASK-1-4 (which produces those .md files) is listed before it. If a coder picks up TASK-1-6 before TASK-1-4 is merged, the loader returns text from a missing file. Worth making the dependency explicit in TASK-1-6's description (\"depends on TASK-1-4 having landed the rubric files first\").\n\n- **Slice-2 task numbering**: TASK-2-6 (\"Update SKILL.md\") appears AFTER TASK-2-7 (\"Add tests for MCP-validator policy\") in the file. Cosmetic \u2014 renumber so the numeric order matches the file order.\n\n- **`LocalWorktreeManager` primitive citation**: the table says \"line ~30\"; the actual class def is at `claude_code/worktree.py:59`. Update.\n\n- **Conformance issue states**: feedback Q1 confirmed the 5-issue set is fixed regardless of current state. #2714 has since been *closed* (merged as #2719). The plan doesn't note this, but it's also not a blocker \u2014 the conformance matrix is about replaying the issue's SDLC shape, not its open/closed state. Worth a one-line acknowledgement in TASK-4-4 so future readers don't get confused.\n\n- **Slice-sizing advisories (per #2137 \u2014 advisory only, never NACK per HITL decision-6 opt-2)**:\n - Slice 3: 10 tasks, ~17 file paths affected (8 .md rubrics + 3 .py code + 4 test files + 1 SKILL.md + 1 in_process.py edit). Heuristic LOC \u2248 1,500. **Consider splitting** \u2014 the daemon bridge (TASK-3-2/TASK-3-3/TASK-3-8) could plausibly be its own sub-slice that lands before the implement phase wiring, especially given R17's \"two bridge architectures\" concern.\n - Slice 4: 7 tasks, ~22 file paths affected (TASK-4-3 alone applies the substrate fixture to 14 existing regression tests). Heuristic LOC \u2248 2,200. **Strongly consider splitting** \u2014 fence removal + PR-phase wiring is one concern; substrate-fixture sprawl across 14 existing tests is another; conformance matrix is a third. The phase-ceiling hook (TASK-4-5) could co-locate with the matrix.\n - Slice 5: 10 tasks, ~25 file paths affected (TASK-5-7 alone touches 15 rubric .md files if R2 fails). Heuristic LOC \u2248 2,500. **Strongly consider splitting** \u2014 the contingent R15 migration (TASK-5-7) is conditional and large; if R2 fails, slice-5 doubles in size. Worth pre-splitting R15 migration into its own sub-slice (or a sibling slice that's a no-op on R2 pass).\n None of these are NACK reasons \u2014 the operator/refiner retains size-override authority \u2014 but the implementer should know the soft-target story.\n\n- **R17 dual-bridge concern from risk_analyst**: their mitigation says \"Option (a)'s daemon design influences Option (b)'s state-serialization contract\". Your plan ships Option (b) in slice-1 (TASK-1-1) and Option (a) in slice-3 (TASK-3-2). If slice-1 commits to a state-serialization contract that slice-3 then has to renegotiate, the bridges diverge. Not a NACK reason \u2014 the operator's cq-1 Option C explicitly chose this hybrid sequencing \u2014 but the slice-1 acceptance for TASK-1-1 should require the `.egg-state/contracts/.json#pending_hitl` envelope shape to be a shared contract between (b) and (a), not a slice-1-only invention.\n\nRe-propose with the three blocking issues addressed and the metrics-path/cq-3-fork alignments. The slice DAG itself is fine \u2014 strictly linear chain, no forest violation; #2137 trivially satisfied.\n", - "metadata": { - "payload": { - "reason": "\nReviewed `.egg-state/drafts/2717-plan.md` (1394 lines, 5 slices, 42 tasks) and cross-checked against the architect's design, the refine analysis, and the actual codebase. The slice shape is right (matches cq-2 Option 3), the role\u2194files alignment is clean (I ran the per-file/per-role check across all 42 tasks; every task's `files:` is writable by its `role:` per `shared/egg_restrictions/patterns.py` \u2014 no orchestrator-side `403 restricted_path_modified` predicted), and the primitives table is unusually thorough. But three blocking issues will produce false signals or unimplementable tasks if shipped as-is.\n\n### Blocking\n\n1. **TASK-1-5 R2 spike mechanism is structurally broken \u2014 the test will not exercise the PreToolUse hook at all.** The task says \"Test dispatches a parent subagent (role=architect) and a child subagent (role=tester) via the harness; asserts the PreToolUse hook denies a write to `orchestrator/foo.py` when invoked from the child\". I verified that `shared/egg_harness/client.py:60-150` registers its own `ToolRegistry` with Python-native Bash/Read/Write/Edit/Glob/Grep handlers and routes permission checks through `registry.set_permission_callback(callback)` \u2014 `grep -rn \"PreToolUseHookPolicy\\|hook_entry\" shared/egg_harness/` returns **zero hits**. The PreToolUse hook is invoked exclusively by Claude Code's native tool registry (see `orchestrator/substrate/claude_code/hook_entry.py:17` `\"command\": \"python3 -m orchestrator.substrate.claude_code.hook_entry\"` \u2014 that's Claude Code's PreToolUse settings.json wiring). Therefore: a harness-dispatched child never triggers `hook_entry.decide()`, and the assertion \"the PreToolUse hook denies a write\" cannot fail (the hook isn't in the loop) and cannot pass (the hook isn't in the loop) \u2014 the test will either green on a no-op or assert against a manually-invoked `hook_entry.decide(...)` that has nothing to do with \"nested dispatch\". This is the load-bearing test for the rollout: cq-4 (R15 migration timing) and cq-6 fallback (MCP-validator enforcement, TASK-2-5 and TASK-5-7) both branch on its verdict. **Fix**: pick one of (a) call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE` env vars and rename the task to make clear it's a hook-logic unit test, not a nested-dispatch test (cheap, doesn't answer the architect's question about Agent-tool nested dispatch, but is honest about scope); (b) ship a minimal Claude Code Agent-tool dispatch path *just for the spike* (precursor of ClaudeCodeAgentToolSpawner, distinct from the production `ClaudeCodeSpawner` harness re-host) and run the parent/child dispatch through it \u2014 that does exercise the hook but adds a slice-1 deliverable the plan currently doesn't enumerate. The architect's text picked option (b) implicitly; you picked option (a)'s file location but kept option (b)'s framing. Pick one and update the task description, acceptance criteria, and dependent contingency tasks (TASK-2-5, TASK-5-7) to match.\n\n2. **TASK-4-4 references \"recorded transcripts\" that no task records.** The acceptance text says \"via harness fakes that replay a recorded transcript of that issue's refine + plan + implement + pr phases\". I searched the codebase: there is no existing transcript-replay infrastructure (the existing `integration_tests/regression/test_substrate_smoke.py:56` uses `MagicMock` with hardcoded `stdout=\"ok\"`, not a recorded-transcript replay). No task in slices 1\u20134 records transcripts for the five curated issues; no task in slice 5 either. As specified, TASK-4-4 cannot be implemented \u2014 its `files:` says `test_conformance_matrix.py` but the test body needs 4 phases \u00d7 5 issues = 20 transcripts that don't exist and aren't produced anywhere in the plan. **Fix**: either (a) add a sub-task that records transcripts during slice-3's first real implement-phase run on each curated issue (the natural place \u2014 slice-3 already produces `.egg-state//substrate-metrics.json` per TASK-3-9, so transcript capture co-locates well); (b) change the mechanism to per-subagent `MagicMock` matching `test_substrate_smoke.py`'s pattern (cheaper, but you lose phase-level realism); or (c) acknowledge the matrix runs subagents live in CI and adjust the 3-hour-per-phase ceiling story accordingly. Whichever path, the plan needs an explicit task that produces the inputs TASK-4-4 consumes.\n\n3. **TASK-4-2 fence-removal task cites the wrong line.** The task description says \"Remove the walking-skeleton scope fence at `orchestrator/substrate/in_process.py:212`\". I checked: `:212` is the *call site* (`self._maybe_fence(answer)` inside `_run`). The `_maybe_fence` static-method *definition* is at `:807-826`. Both need to be removed to actually delete the fence \u2014 and the docstring at `:92-125` that promises `NotImplementedError` for plan/implement/pr (which the acceptance already says to update). The current acceptance text says \"delete `_maybe_fence`\" which is right, but the cited line points the coder at the call site only; a coder following the cite literally would leave the def around as dead code. **Fix**: cite both `:212` (call site, delete the call) and `:807-826` (def, delete the method) explicitly in the description so the coder doesn't have to grep.\n\n### Non-blocking\n\n- **TASK-5-5 fork primitive contradicts cq-3.** Description: \"fork spawns a child Claude Code subagent (via the Agent tool with `subagent_type='general-purpose'`)\". cq-3 resolved to \"decide empirically at implement-phase\" \u2014 i.e. *stay on harness re-host* for the rollout, decide later. Shipping a fork primitive that uses Agent-tool dispatch is a structural commitment to Agent-tool capability, which contradicts the operator's deferral. Either (a) acknowledge in TASK-5-5's description that this is a partial Agent-tool migration the operator should re-confirm before slice 5 ships, or (b) implement fork via `subprocess.Popen` of a fresh `egg_harness.run_agent` so it stays on the harness re-host model. Risk_analyst's R29 mitigation also expects fork to land structurally \u2014 flag the cq-3 tension in the slice-5 ADR refresh (TASK-5-9).\n\n- **TASK-2-5 enforcement target may be the wrong file.** Description says enforcement lands in `orchestrator/mcp_tools.py`. The MCP file-restriction tool today is `mcp__sdlc__check_file_restriction`, whose handler implementation lives at `sandbox/egg_agent_tools/handlers/restrictions.py:6-70` (`check_file_restriction(req)` \u2014 verified by `grep -rn \"check_file_restriction\" --include=\"*.py\" orchestrator/ sandbox/`). `orchestrator/mcp_tools.py` exists but its tool surface is different (it's the orchestrator-side MCP server, registering `mcp__progress__*`, `mcp__sdlc__*`, etc. tools \u2014 not the in-sandbox agent's tool boundary). The validator-side fallback (cq-6 option 2) is about agent-side tool calls denying cross-role writes; that surface is `sandbox/egg_agent_tools/handlers/restrictions.py`, not `orchestrator/mcp_tools.py`. **Fix**: confirm which surface you want and update `files:` accordingly; if both surfaces need wiring, split into two tasks.\n\n- **TASK-3-9 metrics path conflicts with the architect's path.** Architect picked `.egg-state/conformance-metrics/-.json`; you picked `.egg-state//substrate-metrics.json`. TASK-5-1's cost-cap and TASK-4-5's phase-ceiling hook both read from your path; the architect's path goes unread. Align \u2014 pick one canonical path and use it in all four references (TASK-3-9, TASK-4-5, TASK-5-1, ADR in TASK-5-9).\n\n- **TASK-3-2 daemon lifecycle is underspecified.** Acceptance says \"Daemon starts on first `python3 bin/run_pipeline.py --daemon` invocation; binds to `.egg-state//sock`\". But each `python3 bin/run_pipeline.py --daemon` call is a child process of the skill's `Bash(python3 ...)` step. If the daemon doesn't detach (via `subprocess.Popen(start_new_session=True)` or equivalent), the daemon dies when the calling Bash exits \u2014 defeating the \"long-lived\" property. The plan should state explicitly how the daemon detaches and survives across multiple skill-Bash invocations. Cite `os.setsid` / `start_new_session=True` or whatever mechanism you choose.\n\n- **TASK-1-7 / TASK-1-6 ordering**: TASK-1-6 modifies `_load_egg_sdlc_role_rubric` to load `reviewer_refine` and `reviewer_agent_design`, but TASK-1-4 (which produces those .md files) is listed before it. If a coder picks up TASK-1-6 before TASK-1-4 is merged, the loader returns text from a missing file. Worth making the dependency explicit in TASK-1-6's description (\"depends on TASK-1-4 having landed the rubric files first\").\n\n- **Slice-2 task numbering**: TASK-2-6 (\"Update SKILL.md\") appears AFTER TASK-2-7 (\"Add tests for MCP-validator policy\") in the file. Cosmetic \u2014 renumber so the numeric order matches the file order.\n\n- **`LocalWorktreeManager` primitive citation**: the table says \"line ~30\"; the actual class def is at `claude_code/worktree.py:59`. Update.\n\n- **Conformance issue states**: feedback Q1 confirmed the 5-issue set is fixed regardless of current state. #2714 has since been *closed* (merged as #2719). The plan doesn't note this, but it's also not a blocker \u2014 the conformance matrix is about replaying the issue's SDLC shape, not its open/closed state. Worth a one-line acknowledgement in TASK-4-4 so future readers don't get confused.\n\n- **Slice-sizing advisories (per #2137 \u2014 advisory only, never NACK per HITL decision-6 opt-2)**:\n - Slice 3: 10 tasks, ~17 file paths affected (8 .md rubrics + 3 .py code + 4 test files + 1 SKILL.md + 1 in_process.py edit). Heuristic LOC \u2248 1,500. **Consider splitting** \u2014 the daemon bridge (TASK-3-2/TASK-3-3/TASK-3-8) could plausibly be its own sub-slice that lands before the implement phase wiring, especially given R17's \"two bridge architectures\" concern.\n - Slice 4: 7 tasks, ~22 file paths affected (TASK-4-3 alone applies the substrate fixture to 14 existing regression tests). Heuristic LOC \u2248 2,200. **Strongly consider splitting** \u2014 fence removal + PR-phase wiring is one concern; substrate-fixture sprawl across 14 existing tests is another; conformance matrix is a third. The phase-ceiling hook (TASK-4-5) could co-locate with the matrix.\n - Slice 5: 10 tasks, ~25 file paths affected (TASK-5-7 alone touches 15 rubric .md files if R2 fails). Heuristic LOC \u2248 2,500. **Strongly consider splitting** \u2014 the contingent R15 migration (TASK-5-7) is conditional and large; if R2 fails, slice-5 doubles in size. Worth pre-splitting R15 migration into its own sub-slice (or a sibling slice that's a no-op on R2 pass).\n None of these are NACK reasons \u2014 the operator/refiner retains size-override authority \u2014 but the implementer should know the soft-target story.\n\n- **R17 dual-bridge concern from risk_analyst**: their mitigation says \"Option (a)'s daemon design influences Option (b)'s state-serialization contract\". Your plan ships Option (b) in slice-1 (TASK-1-1) and Option (a) in slice-3 (TASK-3-2). If slice-1 commits to a state-serialization contract that slice-3 then has to renegotiate, the bridges diverge. Not a NACK reason \u2014 the operator's cq-1 Option C explicitly chose this hybrid sequencing \u2014 but the slice-1 acceptance for TASK-1-1 should require the `.egg-state/contracts/.json#pending_hitl` envelope shape to be a shared contract between (b) and (a), not a slice-1-only invention.\n\nRe-propose with the three blocking issues addressed and the metrics-path/cq-3-fork alignments. The slice DAG itself is fine \u2014 strictly linear chain, no forest violation; #2137 trivially satisfied.\n", - "artifact_references": [ - ".egg-state/drafts/2717-plan.md", - ".egg-state/drafts/2717-analysis.md", - ".egg-state/agent-outputs/2717-architect-output.json" - ], - "nack_version": 1 - }, - "reason": "\nReviewed `.egg-state/drafts/2717-plan.md` (1394 lines, 5 slices, 42 tasks) and cross-checked against the architect's design, the refine analysis, and the actual codebase. The slice shape is right (matches cq-2 Option 3), the role\u2194files alignment is clean (I ran the per-file/per-role check across all 42 tasks; every task's `files:` is writable by its `role:` per `shared/egg_restrictions/patterns.py` \u2014 no orchestrator-side `403 restricted_path_modified` predicted), and the primitives table is unusually thorough. But three blocking issues will produce false signals or unimplementable tasks if shipped as-is.\n\n### Blocking\n\n1. **TASK-1-5 R2 spike mechanism is structurally broken \u2014 the test will not exercise the PreToolUse hook at all.** The task says \"Test dispatches a parent subagent (role=architect) and a child subagent (role=tester) via the harness; asserts the PreToolUse hook denies a write to `orchestrator/foo.py` when invoked from the child\". I verified that `shared/egg_harness/client.py:60-150` registers its own `ToolRegistry` with Python-native Bash/Read/Write/Edit/Glob/Grep handlers and routes permission checks through `registry.set_permission_callback(callback)` \u2014 `grep -rn \"PreToolUseHookPolicy\\|hook_entry\" shared/egg_harness/` returns **zero hits**. The PreToolUse hook is invoked exclusively by Claude Code's native tool registry (see `orchestrator/substrate/claude_code/hook_entry.py:17` `\"command\": \"python3 -m orchestrator.substrate.claude_code.hook_entry\"` \u2014 that's Claude Code's PreToolUse settings.json wiring). Therefore: a harness-dispatched child never triggers `hook_entry.decide()`, and the assertion \"the PreToolUse hook denies a write\" cannot fail (the hook isn't in the loop) and cannot pass (the hook isn't in the loop) \u2014 the test will either green on a no-op or assert against a manually-invoked `hook_entry.decide(...)` that has nothing to do with \"nested dispatch\". This is the load-bearing test for the rollout: cq-4 (R15 migration timing) and cq-6 fallback (MCP-validator enforcement, TASK-2-5 and TASK-5-7) both branch on its verdict. **Fix**: pick one of (a) call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE` env vars and rename the task to make clear it's a hook-logic unit test, not a nested-dispatch test (cheap, doesn't answer the architect's question about Agent-tool nested dispatch, but is honest about scope); (b) ship a minimal Claude Code Agent-tool dispatch path *just for the spike* (precursor of ClaudeCodeAgentToolSpawner, distinct from the production `ClaudeCodeSpawner` harness re-host) and run the parent/child dispatch through it \u2014 that does exercise the hook but adds a slice-1 deliverable the plan currently doesn't enumerate. The architect's text picked option (b) implicitly; you picked option (a)'s file location but kept option (b)'s framing. Pick one and update the task description, acceptance criteria, and dependent contingency tasks (TASK-2-5, TASK-5-7) to match.\n\n2. **TASK-4-4 references \"recorded transcripts\" that no task records.** The acceptance text says \"via harness fakes that replay a recorded transcript of that issue's refine + plan + implement + pr phases\". I searched the codebase: there is no existing transcript-replay infrastructure (the existing `integration_tests/regression/test_substrate_smoke.py:56` uses `MagicMock` with hardcoded `stdout=\"ok\"`, not a recorded-transcript replay). No task in slices 1\u20134 records transcripts for the five curated issues; no task in slice 5 either. As specified, TASK-4-4 cannot be implemented \u2014 its `files:` says `test_conformance_matrix.py` but the test body needs 4 phases \u00d7 5 issues = 20 transcripts that don't exist and aren't produced anywhere in the plan. **Fix**: either (a) add a sub-task that records transcripts during slice-3's first real implement-phase run on each curated issue (the natural place \u2014 slice-3 already produces `.egg-state//substrate-metrics.json` per TASK-3-9, so transcript capture co-locates well); (b) change the mechanism to per-subagent `MagicMock` matching `test_substrate_smoke.py`'s pattern (cheaper, but you lose phase-level realism); or (c) acknowledge the matrix runs subagents live in CI and adjust the 3-hour-per-phase ceiling story accordingly. Whichever path, the plan needs an explicit task that produces the inputs TASK-4-4 consumes.\n\n3. **TASK-4-2 fence-removal task cites the wrong line.** The task description says \"Remove the walking-skeleton scope fence at `orchestrator/substrate/in_process.py:212`\". I checked: `:212` is the *call site* (`self._maybe_fence(answer)` inside `_run`). The `_maybe_fence` static-method *definition* is at `:807-826`. Both need to be removed to actually delete the fence \u2014 and the docstring at `:92-125` that promises `NotImplementedError` for plan/implement/pr (which the acceptance already says to update). The current acceptance text says \"delete `_maybe_fence`\" which is right, but the cited line points the coder at the call site only; a coder following the cite literally would leave the def around as dead code. **Fix**: cite both `:212` (call site, delete the call) and `:807-826` (def, delete the method) explicitly in the description so the coder doesn't have to grep.\n\n### Non-blocking\n\n- **TASK-5-5 fork primitive contradicts cq-3.** Description: \"fork spawns a child Claude Code subagent (via the Agent tool with `subagent_type='general-purpose'`)\". cq-3 resolved to \"decide empirically at implement-phase\" \u2014 i.e. *stay on harness re-host* for the rollout, decide later. Shipping a fork primitive that uses Agent-tool dispatch is a structural commitment to Agent-tool capability, which contradicts the operator's deferral. Either (a) acknowledge in TASK-5-5's description that this is a partial Agent-tool migration the operator should re-confirm before slice 5 ships, or (b) implement fork via `subprocess.Popen` of a fresh `egg_harness.run_agent` so it stays on the harness re-host model. Risk_analyst's R29 mitigation also expects fork to land structurally \u2014 flag the cq-3 tension in the slice-5 ADR refresh (TASK-5-9).\n\n- **TASK-2-5 enforcement target may be the wrong file.** Description says enforcement lands in `orchestrator/mcp_tools.py`. The MCP file-restriction tool today is `mcp__sdlc__check_file_restriction`, whose handler implementation lives at `sandbox/egg_agent_tools/handlers/restrictions.py:6-70` (`check_file_restriction(req)` \u2014 verified by `grep -rn \"check_file_restriction\" --include=\"*.py\" orchestrator/ sandbox/`). `orchestrator/mcp_tools.py` exists but its tool surface is different (it's the orchestrator-side MCP server, registering `mcp__progress__*`, `mcp__sdlc__*`, etc. tools \u2014 not the in-sandbox agent's tool boundary). The validator-side fallback (cq-6 option 2) is about agent-side tool calls denying cross-role writes; that surface is `sandbox/egg_agent_tools/handlers/restrictions.py`, not `orchestrator/mcp_tools.py`. **Fix**: confirm which surface you want and update `files:` accordingly; if both surfaces need wiring, split into two tasks.\n\n- **TASK-3-9 metrics path conflicts with the architect's path.** Architect picked `.egg-state/conformance-metrics/-.json`; you picked `.egg-state//substrate-metrics.json`. TASK-5-1's cost-cap and TASK-4-5's phase-ceiling hook both read from your path; the architect's path goes unread. Align \u2014 pick one canonical path and use it in all four references (TASK-3-9, TASK-4-5, TASK-5-1, ADR in TASK-5-9).\n\n- **TASK-3-2 daemon lifecycle is underspecified.** Acceptance says \"Daemon starts on first `python3 bin/run_pipeline.py --daemon` invocation; binds to `.egg-state//sock`\". But each `python3 bin/run_pipeline.py --daemon` call is a child process of the skill's `Bash(python3 ...)` step. If the daemon doesn't detach (via `subprocess.Popen(start_new_session=True)` or equivalent), the daemon dies when the calling Bash exits \u2014 defeating the \"long-lived\" property. The plan should state explicitly how the daemon detaches and survives across multiple skill-Bash invocations. Cite `os.setsid` / `start_new_session=True` or whatever mechanism you choose.\n\n- **TASK-1-7 / TASK-1-6 ordering**: TASK-1-6 modifies `_load_egg_sdlc_role_rubric` to load `reviewer_refine` and `reviewer_agent_design`, but TASK-1-4 (which produces those .md files) is listed before it. If a coder picks up TASK-1-6 before TASK-1-4 is merged, the loader returns text from a missing file. Worth making the dependency explicit in TASK-1-6's description (\"depends on TASK-1-4 having landed the rubric files first\").\n\n- **Slice-2 task numbering**: TASK-2-6 (\"Update SKILL.md\") appears AFTER TASK-2-7 (\"Add tests for MCP-validator policy\") in the file. Cosmetic \u2014 renumber so the numeric order matches the file order.\n\n- **`LocalWorktreeManager` primitive citation**: the table says \"line ~30\"; the actual class def is at `claude_code/worktree.py:59`. Update.\n\n- **Conformance issue states**: feedback Q1 confirmed the 5-issue set is fixed regardless of current state. #2714 has since been *closed* (merged as #2719). The plan doesn't note this, but it's also not a blocker \u2014 the conformance matrix is about replaying the issue's SDLC shape, not its open/closed state. Worth a one-line acknowledgement in TASK-4-4 so future readers don't get confused.\n\n- **Slice-sizing advisories (per #2137 \u2014 advisory only, never NACK per HITL decision-6 opt-2)**:\n - Slice 3: 10 tasks, ~17 file paths affected (8 .md rubrics + 3 .py code + 4 test files + 1 SKILL.md + 1 in_process.py edit). Heuristic LOC \u2248 1,500. **Consider splitting** \u2014 the daemon bridge (TASK-3-2/TASK-3-3/TASK-3-8) could plausibly be its own sub-slice that lands before the implement phase wiring, especially given R17's \"two bridge architectures\" concern.\n - Slice 4: 7 tasks, ~22 file paths affected (TASK-4-3 alone applies the substrate fixture to 14 existing regression tests). Heuristic LOC \u2248 2,200. **Strongly consider splitting** \u2014 fence removal + PR-phase wiring is one concern; substrate-fixture sprawl across 14 existing tests is another; conformance matrix is a third. The phase-ceiling hook (TASK-4-5) could co-locate with the matrix.\n - Slice 5: 10 tasks, ~25 file paths affected (TASK-5-7 alone touches 15 rubric .md files if R2 fails). Heuristic LOC \u2248 2,500. **Strongly consider splitting** \u2014 the contingent R15 migration (TASK-5-7) is conditional and large; if R2 fails, slice-5 doubles in size. Worth pre-splitting R15 migration into its own sub-slice (or a sibling slice that's a no-op on R2 pass).\n None of these are NACK reasons \u2014 the operator/refiner retains size-override authority \u2014 but the implementer should know the soft-target story.\n\n- **R17 dual-bridge concern from risk_analyst**: their mitigation says \"Option (a)'s daemon design influences Option (b)'s state-serialization contract\". Your plan ships Option (b) in slice-1 (TASK-1-1) and Option (a) in slice-3 (TASK-3-2). If slice-1 commits to a state-serialization contract that slice-3 then has to renegotiate, the bridges diverge. Not a NACK reason \u2014 the operator's cq-1 Option C explicitly chose this hybrid sequencing \u2014 but the slice-1 acceptance for TASK-1-1 should require the `.egg-state/contracts/.json#pending_hitl` envelope shape to be a shared contract between (b) and (a), not a slice-1-only invention.\n\nRe-propose with the three blocking issues addressed and the metrics-path/cq-3-fork alignments. The slice DAG itself is fine \u2014 strictly linear chain, no forest violation; #2137 trivially satisfied.\n", - "revision_count": 1 - }, - "timestamp": "2026-05-19T18:31:22.541520+00:00", - "phase": "plan" - }, - { - "id": "78bb475a-98b2-49", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:31:22.687115+00:00", - "phase": "plan" - }, - { - "id": "9efdfbc6-6b72-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "risk_analyst", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_plan for risk_analyst", - "body": "\nReviewed `.egg-state/agent-outputs/2717-risk_analyst-output.json` \u2014 18 risks (5 HIGH, ~10 MEDIUM, 3 LOW), structured assessment across performance / security / compatibility / DoD-alignment, with concrete mitigations per risk.\n\n### What's solid\n- **HIGH-severity risks are correctly identified and architecturally substantive**:\n - **R17 (dual-bridge architecture)** \u2014 I cross-checked: cq-1 Option C does ship two structurally different bridges (stage-script vs daemon), and the task_planner has TASK-1-1 (Option b) in slice-1 and TASK-3-2 (Option a) in slice-3 \u2014 the divergence risk you flag is real, and the recommendation to make the `.egg-state/contracts/.json#pending_hitl` envelope a shared contract between the two bridges is the right structural mitigation. I'm carrying this through to the task_planner NACK as a non-blocking item.\n - **R18 (15 rubric authoring + #2622 depth gap)** \u2014 verified `orchestrator/substrate/__init__.py:280-284` still raises the \"Walking-skeleton spike (#2623) only ships the refiner rubric; other roles are deferred to the follow-up issue per cq-11\" `ValueError`. Your point that `build_system_prompt(sources)` depth-threading needs per-role structural verification is exactly right \u2014 the spike validated this for *one* role only. The mitigation to add a structural test (assert every role enumerated in `_PHASE_ROLES` has a rubric AND that `build_system_prompt` includes its content) is the right shape; the task_planner's TASK-2-3/3-4/3-5 don't currently include such a structural test, so this risk stays open even after their NACK is addressed.\n - **R19 (8-subagent harness re-host concurrency strain)** \u2014 I checked `shared/egg_harness/client.py:60-150`: each `run_agent` invocation builds its own `ToolRegistry`, `EventBus`, `AnthropicProvider`, and `AgentLoop` in the parent's Python process. Stacking 8 of these is exactly the load you describe. The mitigation to require explicit measurement during plan-phase (4 loops) before approving implement-phase (8 loops) is the right gate; reviewer_plan should hold the implement-phase slice on the plan-phase numbers.\n - **R27 (MCP-validator structural enforcement gap)** \u2014 I checked `sandbox/egg_agent_tools/handlers/restrictions.py:6-15`: \"The agent should call `check_file_restriction` *before* burning a turn\" \u2014 this is voluntary, not enforced. Your point that MCP-validator fallback loses structural enforcement is exactly right. The \"PreToolUse hook STILL fires for Bash and global-deny rules + post-tool-call validator\" defense-in-depth recommendation is concrete and actionable. The task_planner's TASK-2-5 targets `orchestrator/mcp_tools.py` which I'm separately flagging as wrong-surface \u2014 your risk gives the right re-direction.\n - **R29 (subagent context budget under harness re-host)** \u2014 verified each harness loop's `AgentLoop.run(...)` accumulates conversation history in-process; 8 concurrent loops \u00d7 ~50K tokens is a real budget concern. The mitigation to move fork-based delegation up if context pressure manifests during plan-phase is the right contingency \u2014 the task_planner currently defers fork to slice-5 (TASK-5-5) which is the wrong end of the timeline if R29 manifests early.\n\n- **Trust-boundary risks are correctly framed**: R31 (15-subagent credential exposure) and the security_assessment's \"explicit_non_goals_inherited_from_2623\" section correctly inherit R1's qualitative acceptance without re-litigating, while flagging that the 15x multiplier wasn't quantified in cq-1.\n\n- **DoD criterion alignment table** maps each of the 6 DoD bullets to specific risks/mitigations \u2014 DoD #5 (`EggHarnessSpawner` + local-run) lines up with R26; DoD #6 (ADR refresh) lines up with R32 (interface stability marker timing). No DoD bullet is silently dropped.\n\n- **`open_questions_for_implement_phase` list (5 items)** is useful forward-handoff \u2014 context-budget gate, fork-trigger, MCP-validator structural strength, cost-cap soft-warning threshold, runaway-NACK detection \u2014 these all surface decisions implement-phase will need to make and that should land in the task_planner's slice-3 acceptance criteria.\n\n### Non-blocking\n- **R20 (no substrate-specific reviewer) MEDIUM rating** \u2014 feedback Q5 declined the expansion. Worth noting in the risk text that this is operator-accepted rather than \"MEDIUM unresolved\"; the residual risk is unchanged but the framing matters for the merge story.\n- **R21 (#2714 closed since refine)** \u2014 confirmed via `git log --oneline | grep 2714` that #2719 (the close fix) is already merged. Worth either acknowledging the close or escalating to a feedback question if this changes the conformance-matrix shape (e.g., do we replay the issue's pre-fix state or post-fix state?).\n- **R33 (apply phase unreferenced in DoD)** LOW \u2014 agreed; the issue body's DoD doesn't list epic-only apply-phase coverage, so this isn't a NACK reason. The task_planner's TASK-5-7 lists `applier.md` in the conditional R15 migration but no slice ships an unconditional applier rubric. If the operator expects epic-pipeline parity, that should be a feedback question; if not, R33 stays at LOW.\n- **R23 cost cap \"200 leaves room for $1000+ runaway\"** \u2014 the arithmetic is sound, but cq-6 already resolved this; suggest reframing as \"operator accepted; consider soft-warning at 100 invocations\" rather than \"MEDIUM unresolved\". The task_planner's TASK-5-1 implements the cap mechanically; a soft warning would be a non-blocking addition.\n\nOverall: thorough, accurate, and the HIGH-severity risks are the right ones to surface to the operator. Approving so the planner can incorporate the structural mitigations (R17 envelope contract, R18 depth-gap structural test, R19 plan-phase measurement gate, R27 defense-in-depth, R29 fork-trigger contingency) into the re-propose.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/agent-outputs/2717-risk_analyst-output.json", - ".egg-state/drafts/2717-analysis.md" - ], - "reason": "\nReviewed `.egg-state/agent-outputs/2717-risk_analyst-output.json` \u2014 18 risks (5 HIGH, ~10 MEDIUM, 3 LOW), structured assessment across performance / security / compatibility / DoD-alignment, with concrete mitigations per risk.\n\n### What's solid\n- **HIGH-severity risks are correctly identified and architecturally substantive**:\n - **R17 (dual-bridge architecture)** \u2014 I cross-checked: cq-1 Option C does ship two structurally different bridges (stage-script vs daemon), and the task_planner has TASK-1-1 (Option b) in slice-1 and TASK-3-2 (Option a) in slice-3 \u2014 the divergence risk you flag is real, and the recommendation to make the `.egg-state/contracts/.json#pending_hitl` envelope a shared contract between the two bridges is the right structural mitigation. I'm carrying this through to the task_planner NACK as a non-blocking item.\n - **R18 (15 rubric authoring + #2622 depth gap)** \u2014 verified `orchestrator/substrate/__init__.py:280-284` still raises the \"Walking-skeleton spike (#2623) only ships the refiner rubric; other roles are deferred to the follow-up issue per cq-11\" `ValueError`. Your point that `build_system_prompt(sources)` depth-threading needs per-role structural verification is exactly right \u2014 the spike validated this for *one* role only. The mitigation to add a structural test (assert every role enumerated in `_PHASE_ROLES` has a rubric AND that `build_system_prompt` includes its content) is the right shape; the task_planner's TASK-2-3/3-4/3-5 don't currently include such a structural test, so this risk stays open even after their NACK is addressed.\n - **R19 (8-subagent harness re-host concurrency strain)** \u2014 I checked `shared/egg_harness/client.py:60-150`: each `run_agent` invocation builds its own `ToolRegistry`, `EventBus`, `AnthropicProvider`, and `AgentLoop` in the parent's Python process. Stacking 8 of these is exactly the load you describe. The mitigation to require explicit measurement during plan-phase (4 loops) before approving implement-phase (8 loops) is the right gate; reviewer_plan should hold the implement-phase slice on the plan-phase numbers.\n - **R27 (MCP-validator structural enforcement gap)** \u2014 I checked `sandbox/egg_agent_tools/handlers/restrictions.py:6-15`: \"The agent should call `check_file_restriction` *before* burning a turn\" \u2014 this is voluntary, not enforced. Your point that MCP-validator fallback loses structural enforcement is exactly right. The \"PreToolUse hook STILL fires for Bash and global-deny rules + post-tool-call validator\" defense-in-depth recommendation is concrete and actionable. The task_planner's TASK-2-5 targets `orchestrator/mcp_tools.py` which I'm separately flagging as wrong-surface \u2014 your risk gives the right re-direction.\n - **R29 (subagent context budget under harness re-host)** \u2014 verified each harness loop's `AgentLoop.run(...)` accumulates conversation history in-process; 8 concurrent loops \u00d7 ~50K tokens is a real budget concern. The mitigation to move fork-based delegation up if context pressure manifests during plan-phase is the right contingency \u2014 the task_planner currently defers fork to slice-5 (TASK-5-5) which is the wrong end of the timeline if R29 manifests early.\n\n- **Trust-boundary risks are correctly framed**: R31 (15-subagent credential exposure) and the security_assessment's \"explicit_non_goals_inherited_from_2623\" section correctly inherit R1's qualitative acceptance without re-litigating, while flagging that the 15x multiplier wasn't quantified in cq-1.\n\n- **DoD criterion alignment table** maps each of the 6 DoD bullets to specific risks/mitigations \u2014 DoD #5 (`EggHarnessSpawner` + local-run) lines up with R26; DoD #6 (ADR refresh) lines up with R32 (interface stability marker timing). No DoD bullet is silently dropped.\n\n- **`open_questions_for_implement_phase` list (5 items)** is useful forward-handoff \u2014 context-budget gate, fork-trigger, MCP-validator structural strength, cost-cap soft-warning threshold, runaway-NACK detection \u2014 these all surface decisions implement-phase will need to make and that should land in the task_planner's slice-3 acceptance criteria.\n\n### Non-blocking\n- **R20 (no substrate-specific reviewer) MEDIUM rating** \u2014 feedback Q5 declined the expansion. Worth noting in the risk text that this is operator-accepted rather than \"MEDIUM unresolved\"; the residual risk is unchanged but the framing matters for the merge story.\n- **R21 (#2714 closed since refine)** \u2014 confirmed via `git log --oneline | grep 2714` that #2719 (the close fix) is already merged. Worth either acknowledging the close or escalating to a feedback question if this changes the conformance-matrix shape (e.g., do we replay the issue's pre-fix state or post-fix state?).\n- **R33 (apply phase unreferenced in DoD)** LOW \u2014 agreed; the issue body's DoD doesn't list epic-only apply-phase coverage, so this isn't a NACK reason. The task_planner's TASK-5-7 lists `applier.md` in the conditional R15 migration but no slice ships an unconditional applier rubric. If the operator expects epic-pipeline parity, that should be a feedback question; if not, R33 stays at LOW.\n- **R23 cost cap \"200 leaves room for $1000+ runaway\"** \u2014 the arithmetic is sound, but cq-6 already resolved this; suggest reframing as \"operator accepted; consider soft-warning at 100 invocations\" rather than \"MEDIUM unresolved\". The task_planner's TASK-5-1 implements the cap mechanically; a soft warning would be a non-blocking addition.\n\nOverall: thorough, accurate, and the HIGH-severity risks are the right ones to surface to the operator. Approving so the planner can incorporate the structural mitigations (R17 envelope contract, R18 depth-gap structural test, R19 plan-phase measurement gate, R27 defense-in-depth, R29 fork-trigger contingency) into the re-propose.\n", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-19T18:31:22.706865+00:00", - "phase": "plan" - }, - { - "id": "6c7b3b25-fc10-49", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by architect", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T18:31:27.062878+00:00", - "phase": "plan" - }, - { - "id": "10890017-5be8-45", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:31:27.279401+00:00", - "phase": "plan" - }, - { - "id": "73c583aa-58d4-47", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:31:34.996506+00:00", - "phase": "plan" - }, - { - "id": "a014d96d-3338-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:35.568258+00:00" - }, - "timestamp": "2026-05-19T18:31:35.621660+00:00", - "phase": "plan" - }, - { - "id": "fde614d0-0603-4a", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:31:38.068043+00:00", - "phase": "plan" - }, - { - "id": "5d0f412c-4d1d-46", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:32:35.124111+00:00", - "phase": "plan" - }, - { - "id": "5a800bae-3c23-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:35.568258+00:00" - }, - "timestamp": "2026-05-19T18:32:35.681945+00:00", - "phase": "plan" - }, - { - "id": "98a9c72e-a036-40", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:32:38.136564+00:00", - "phase": "plan" - }, - { - "id": "cad4eba4-b740-44", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:33:35.230075+00:00", - "phase": "plan" - }, - { - "id": "a9cc5b5e-55e4-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:35.568258+00:00" - }, - "timestamp": "2026-05-19T18:33:35.793955+00:00", - "phase": "plan" - }, - { - "id": "9c9830ee-bd22-41", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:33:38.232153+00:00", - "phase": "plan" - }, - { - "id": "df0de9e3-9e6a-44", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:34:35.350670+00:00", - "phase": "plan" - }, - { - "id": "06332960-f802-4b", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:35.568258+00:00" - }, - "timestamp": "2026-05-19T18:34:35.879050+00:00", - "phase": "plan" - }, - { - "id": "47b88498-afc5-4a", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:34:38.312184+00:00", - "phase": "plan" - }, - { - "id": "80c11375-1379-4c", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:35:35.412148+00:00", - "phase": "plan" - }, - { - "id": "5ff0e961-03a0-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:35.568258+00:00" - }, - "timestamp": "2026-05-19T18:35:35.931628+00:00", - "phase": "plan" - }, - { - "id": "4e7b4528-78ad-43", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:35:38.366666+00:00", - "phase": "plan" - }, - { - "id": "731f7c34-26fd-47", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:36:56.374159+00:00", - "phase": "plan" - }, - { - "id": "a7576492-226d-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:35.568258+00:00" - }, - "timestamp": "2026-05-19T18:36:56.404213+00:00", - "phase": "plan" - }, - { - "id": "0c058cdd-110f-44", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:36:56.406854+00:00", - "phase": "plan" - }, - { - "id": "53a97e86-4374-4a", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:37:50.498881+00:00", - "phase": "plan" - }, - { - "id": "0d990a0a-2f3b-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:35.568258+00:00" - }, - "timestamp": "2026-05-19T18:37:51.013296+00:00", - "phase": "plan" - }, - { - "id": "5e2e4891-e130-4b", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:37:53.450901+00:00", - "phase": "plan" - }, - { - "id": "ce33505c-b8e2-40", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from task_planner", - "body": "Plan v2 addresses reviewer_plan v1 NACK. Blocking issues fixed: (1) TASK-1-5 R2 spike reworked \u2014 the harness re-host model bypasses PreToolUse hook entirely (shared/egg_harness/client.py uses its own ToolRegistry.set_permission_callback per the reviewer's audit), so added TASK-1-9 (a test-only nested-Agent-tool dispatch fake under integration_tests/regression/_agent_tool_fake.py, coder-owned per MCP check) and reframed TASK-1-5 to use it; the test docstring documents the empirical-vs-test-fake limitation and production stays on ClaudeCodeSpawner harness re-host per cq-3. (2) TASK-4-4 conformance matrix mechanism changed from \"recorded transcripts that no task produces\" to MagicMock-style stubs mirroring test_substrate_smoke.py:56. (3) TASK-4-2 fence-removal task now cites BOTH :212 (call site) and :807-826 (method def) so the coder doesn't leave dead code. Non-blocking fixes also applied: TASK-2-5 enforcement target moved from orchestrator/mcp_tools.py to sandbox/egg_agent_tools/handlers/restrictions.py (the correct in-sandbox surface per the reviewer); TASK-2-6/TASK-2-7 renumbered to match file order; TASK-1-6 dependency on TASK-1-4 made explicit; TASK-3-2 daemon detach via start_new_session=True; TASK-1-1 pending_hitl envelope marked as the shared state-serialization contract for risk_analyst R17 mitigation; TASK-5-5 fork primitive switched from Agent-tool dispatch to subprocess+egg_harness.run_agent so it aligns with cq-3's deferral; LocalWorktreeManager line corrected to :59; _maybe_fence dual-location citation added to the primitives table. The slice DAG remains a strictly linear 5-slice chain (forest constraint trivially satisfied); 43 tasks total across the five slices (added TASK-1-9). All 87 file references pass the per-role allow/block check via shared/egg_restrictions/patterns.py.", - "metadata": { - "payload": { - "summary": "Plan v2 addresses reviewer_plan v1 NACK. Blocking issues fixed: (1) TASK-1-5 R2 spike reworked \u2014 the harness re-host model bypasses PreToolUse hook entirely (shared/egg_harness/client.py uses its own ToolRegistry.set_permission_callback per the reviewer's audit), so added TASK-1-9 (a test-only nested-Agent-tool dispatch fake under integration_tests/regression/_agent_tool_fake.py, coder-owned per MCP check) and reframed TASK-1-5 to use it; the test docstring documents the empirical-vs-test-fake limitation and production stays on ClaudeCodeSpawner harness re-host per cq-3. (2) TASK-4-4 conformance matrix mechanism changed from \"recorded transcripts that no task produces\" to MagicMock-style stubs mirroring test_substrate_smoke.py:56. (3) TASK-4-2 fence-removal task now cites BOTH :212 (call site) and :807-826 (method def) so the coder doesn't leave dead code. Non-blocking fixes also applied: TASK-2-5 enforcement target moved from orchestrator/mcp_tools.py to sandbox/egg_agent_tools/handlers/restrictions.py (the correct in-sandbox surface per the reviewer); TASK-2-6/TASK-2-7 renumbered to match file order; TASK-1-6 dependency on TASK-1-4 made explicit; TASK-3-2 daemon detach via start_new_session=True; TASK-1-1 pending_hitl envelope marked as the shared state-serialization contract for risk_analyst R17 mitigation; TASK-5-5 fork primitive switched from Agent-tool dispatch to subprocess+egg_harness.run_agent so it aligns with cq-3's deferral; LocalWorktreeManager line corrected to :59; _maybe_fence dual-location citation added to the primitives table. The slice DAG remains a strictly linear 5-slice chain (forest constraint trivially satisfied); 43 tasks total across the five slices (added TASK-1-9). All 87 file references pass the per-role allow/block check via shared/egg_restrictions/patterns.py.", - "attestation": {}, - "artifacts": [ - ".egg-state/drafts/2717-plan.md" - ], - "risk_considered": "The R2-spike reframing acknowledges that nested-dispatch hook resolution is structurally not testable in-sandbox without a real Claude Code session; the new TASK-1-9 fake validates hook *logic* given accurate EGG_AGENT_ROLE propagation, and the empirical question becomes the production answer when cq-3 flips to Agent-tool dispatch in a future issue. This means R2 = pass is the most likely verdict (hook logic is straightforward, EGG_AGENT_ROLE-driven) and the cq-4 contingent migration path will likely close as a no-op. Documented this in TASK-5-7's acceptance criteria. The conformance matrix's MagicMock-style stubs trade fidelity for in-sandbox runnability; the trade-off is documented in the test docstring and acknowledged in the slice-4 prose.", - "commit_sha": "cdcaebe6153cb3286ad1aa3fea2ba5f43eeed481", - "files_changed": [ - ".egg-state/drafts/2717-plan.md" - ], - "tests_run": [], - "tasks_satisfied": [ - "TASK-1-1", - "TASK-1-2", - "TASK-1-3", - "TASK-1-4", - "TASK-1-5", - "TASK-1-6", - "TASK-1-7", - "TASK-1-8", - "TASK-1-9", - "TASK-2-1", - "TASK-2-2", - "TASK-2-3", - "TASK-2-4", - "TASK-2-5", - "TASK-2-6", - "TASK-2-7", - "TASK-3-1", - "TASK-3-2", - "TASK-3-3", - "TASK-3-4", - "TASK-3-5", - "TASK-3-6", - "TASK-3-7", - "TASK-3-8", - "TASK-3-9", - "TASK-3-10", - "TASK-4-1", - "TASK-4-2", - "TASK-4-3", - "TASK-4-4", - "TASK-4-5", - "TASK-4-6", - "TASK-4-7", - "TASK-5-1", - "TASK-5-2", - "TASK-5-3", - "TASK-5-4", - "TASK-5-5", - "TASK-5-6", - "TASK-5-7", - "TASK-5-8", - "TASK-5-9", - "TASK-5-10" - ] - }, - "version": 2, - "commit_sha": "cdcaebe6153cb3286ad1aa3fea2ba5f43eeed481" - }, - "timestamp": "2026-05-19T18:38:36.761718+00:00", - "phase": "plan" - }, - { - "id": "40851050-796c-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:38:36.835948+00:00", - "phase": "plan" - }, - { - "id": "b28d8cfa-cbbb-4b", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:38:49.397500+00:00" - }, - "timestamp": "2026-05-19T18:39:04.310795+00:00", - "phase": "plan" - }, - { - "id": "acc949d2-52a4-49", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:39:04.316498+00:00", - "phase": "plan" - }, - { - "id": "b69c1c04-0093-42", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:39:04.319961+00:00", - "phase": "plan" - }, - { - "id": "d485fa69-ec8e-49", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:38.031399+00:00" - }, - "timestamp": "2026-05-19T18:40:04.436760+00:00", - "phase": "plan" - }, - { - "id": "c9402567-719c-47", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:31:34.949499+00:00" - }, - "timestamp": "2026-05-19T18:40:04.442002+00:00", - "phase": "plan" - }, - { - "id": "096dfb32-2132-48", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:38:49.397500+00:00" - }, - "timestamp": "2026-05-19T18:40:04.485635+00:00", - "phase": "plan" - }, - { - "id": "ac360879-d9c9-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "task_planner", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_plan for task_planner", - "body": "\nReviewed `.egg-state/drafts/2717-plan.md` v2 (now 43 tasks across 5 slices). All three v1 blockers are fixed correctly; non-blocking items also addressed.\n\n### Blocker fixes verified\n\n1. **TASK-1-5 R2 spike \u2014 fixed.** The task description now opens with the structural acknowledgement I asked for (\"the harness re-host model bypasses the PreToolUse hook entirely because `shared/egg_harness/client.py:60-150` uses its own `ToolRegistry.set_permission_callback(...)` and `grep -rn \"PreToolUseHookPolicy|hook_entry\" shared/egg_harness/` returns zero hits\"). The new TASK-1-9 ships `integration_tests/regression/_agent_tool_fake.py` as test-only infrastructure with an explicit `if not __name__.startswith(\"integration_tests\")` import guard preventing silent production import. The role\u2194files check confirms `_agent_tool_fake.py` is coder-writable (underscored helper-style name escapes the `**/test_*.py` blocked-glob \u2014 verified via the same per-file restriction lookup I ran on v1; `integration_tests/regression/` is NOT matched by `**/tests/` because the directory is `integration_tests`, not `tests`). The acceptance criteria honestly documents the test-fake limitation in the docstring (\"the test confirms hook *logic* is correct given accurate `EGG_AGENT_ROLE` propagation; whether Claude Code itself propagates `EGG_AGENT_ROLE` correctly under real nested Agent-tool dispatch is verifiable only by running real Claude Code \u2026 when cq-3 flips to Agent-tool dispatch in a future issue, the production path becomes the empirical answer to that remaining half of R2\"). That framing \u2014 \"hook-logic test now, real-dispatch verification deferred to cq-3 follow-up\" \u2014 is the right scoping; the cq-4 / cq-6 contingencies branch on a meaningful signal (hook logic IS correct vs hook logic IS broken) rather than a phantom one.\n\n2. **TASK-4-4 conformance matrix \u2014 fixed.** Mechanism switched to \"per-subagent MagicMock stubs in the same style as `integration_tests/regression/test_substrate_smoke.py:56` (hardcoded `stdout=\"ok\"`, no recorded transcripts)\". This is option (b) from my v1 NACK \u2014 the cheapest path that keeps the matrix in-sandbox-runnable. The acceptance text documents the trade-off (\"MagicMock-vs-real-transcript trade-off\") so future readers know the matrix tests the SDLC *shape* not verbatim text. #2714's closed state is also acknowledged with the right framing (\"irrelevant per feedback Q1's rationale\"). The chicken-and-egg problem of v1 is gone.\n\n3. **TASK-4-2 fence-removal \u2014 fixed.** Now cites both `:212` (call site) and `:807-826` (method def) explicitly in the description (\"Two locations must be touched \u2026 Removing only the call site leaves dead code; both must be deleted\"). Acceptance criteria echoes both locations. A coder following the cite literally will now delete both.\n\n### Non-blocking items also resolved\n\n- **TASK-5-5 fork primitive \u2014 now aligned with cq-3.** Implementation switched from \"Agent tool with `subagent_type='general-purpose'`\" to \"`subprocess.Popen` running a fresh `egg_harness.run_agent` invocation in a clean Python process (NOT via Claude Code's Agent tool \u2014 that would commit to Agent-tool dispatch ahead of the cq-3 empirical verdict)\". The acceptance text also handles the future migration cleanly (\"If cq-3's empirical verdict in slice-5's ADR refresh recommends migrating to Agent-tool dispatch, the fork primitive's implementation will change accordingly in a follow-up \u2014 not in this slice\"). Good \u2014 cq-3 deferral preserved.\n\n- **TASK-2-5 enforcement target \u2014 corrected.** Surface moved from `orchestrator/mcp_tools.py` to `sandbox/egg_agent_tools/handlers/restrictions.py` (where `check_file_restriction(req)` lives per `grep -rn \"check_file_restriction\" --include=\"*.py\" orchestrator/ sandbox/`). The description explicitly cites the audit (\"the correct surface per the plan reviewer's audit is the in-sandbox tool handler, NOT `orchestrator/mcp_tools.py` (which is the orchestrator-side MCP server, a different surface)\"). The dependent test (TASK-2-6) moved to `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` \u2014 confirmed tester-writable.\n\n- **TASK-3-2 daemon lifecycle \u2014 pinned down.** Description now mandates `subprocess.Popen(start_new_session=True)` (or `os.setsid()` after fork) and the acceptance criteria asserts the property in TASK-3-8's test (\"the long-lived-across-Bash-exits property is asserted in TASK-3-8's test\"). The \"daemon dies when calling Bash exits\" failure mode is no longer latent.\n\n- **TASK-1-1 pending_hitl envelope \u2014 marked as shared contract for R17.** TASK-1-9 also calls out that the daemon (TASK-3-2) inherits the same envelope schema (\"closes risk_analyst R17 dual-bridge concern\"). Slice-1 commits to a state-serialization contract slice-3 picks up rather than reinventing.\n\n- **TASK-1-6 dependency on TASK-1-4 \u2014 made explicit.** Description now says \"Depends on TASK-1-4 having landed the two new rubric .md files first \u2026 The implementer should sequence TASK-1-4 \u2192 TASK-1-6 within the slice\".\n\n- **TASK-2-6 / TASK-2-7 renumbered** so file order matches numeric order.\n\n- **`LocalWorktreeManager` line citation corrected to :59**; **`_maybe_fence` dual-location** (`:212` call + `:807-826` def) added to the primitives table.\n\n### Remaining non-blocking observations\n\nThese are observations the planner doesn't need to address before consensus \u2014 surfacing for the implement-phase team:\n\n- **Slice-sizing advisories from v1 still apply** (#2137 \u2014 advisory only, never NACK per HITL decision-6 opt-2): slice-3 ~1,500 LOC (\"consider splitting\"), slice-4 ~2,200 LOC (\"strongly consider splitting\"), slice-5 ~2,500 LOC (\"strongly consider splitting\"). The operator retains override authority; the implementer should know if the slice expands further during implement-phase.\n\n- **R2 verdict semantics narrowed.** TASK-1-5's acceptance now tests hook *logic* given correct env propagation. That answers \"is `hook_entry.decide()` correct?\" not \"does Claude Code propagate `EGG_AGENT_ROLE` correctly under real Agent-tool dispatch?\". The latter question can only be answered when cq-3 flips to real Agent-tool dispatch in a future issue. The plan's framing (\"the production path becomes the empirical answer to that remaining half of R2\") is honest about this \u2014 but the cq-4 / cq-6 contingencies trigger on the *narrower* R2 signal. If the narrower R2 passes but the deferred real-dispatch R2 later fails, cq-4 / cq-6 may need to re-fire mid-future-issue. Worth flagging in the slice-1 ADR update (TASK-1-8) so the operator knows the contingency surface is split.\n\n- **R19 / R29 mitigation gates from risk_analyst** (plan-phase concurrency measurement before implement-phase ships; context-budget contingency on fork-trigger): the plan doesn't add explicit acceptance criteria gates for these on slice-2 (the plan-phase slice that produces the measurement). Risk_analyst's mitigations require reviewer_plan to hold slice-3 on slice-2's numbers; that's a review-cycle obligation, not a code task \u2014 but the slice-2 acceptance for TASK-2-4 (`test_inprocess_plan_brc.py`) could include \"records the per-loop wall-clock and parent-context delta to `.egg-state//substrate-metrics.json`\" so the slice-3 review has data to gate on. Worth considering at implement-time; not a NACK reason now.\n\n- **TASK-4-3 substrate-fixture sprawl across 14 existing regression tests** is itself a sizeable change set. If any of those 14 tests aren't substrate-portable in practice (transitively depend on `egg_stack` or `gateway_url` through a helper), the task will need to add `@pytest.mark.skip(reason=...)` markers \u2014 the acceptance criteria allows for this, but worth budgeting review attention for those 14 individual touches when slice-4 lands.\n\nPlan is ready for ingestion. Slice DAG is strictly linear (slice-1 \u2192 2 \u2192 3 \u2192 4 \u2192 5) so #2137 forest constraint is trivially satisfied. All 43 tasks have role assignments writable per `shared/egg_restrictions/patterns.py` (verified via per-file lookup across the full task list).\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/drafts/2717-plan.md" - ], - "reason": "\nReviewed `.egg-state/drafts/2717-plan.md` v2 (now 43 tasks across 5 slices). All three v1 blockers are fixed correctly; non-blocking items also addressed.\n\n### Blocker fixes verified\n\n1. **TASK-1-5 R2 spike \u2014 fixed.** The task description now opens with the structural acknowledgement I asked for (\"the harness re-host model bypasses the PreToolUse hook entirely because `shared/egg_harness/client.py:60-150` uses its own `ToolRegistry.set_permission_callback(...)` and `grep -rn \"PreToolUseHookPolicy|hook_entry\" shared/egg_harness/` returns zero hits\"). The new TASK-1-9 ships `integration_tests/regression/_agent_tool_fake.py` as test-only infrastructure with an explicit `if not __name__.startswith(\"integration_tests\")` import guard preventing silent production import. The role\u2194files check confirms `_agent_tool_fake.py` is coder-writable (underscored helper-style name escapes the `**/test_*.py` blocked-glob \u2014 verified via the same per-file restriction lookup I ran on v1; `integration_tests/regression/` is NOT matched by `**/tests/` because the directory is `integration_tests`, not `tests`). The acceptance criteria honestly documents the test-fake limitation in the docstring (\"the test confirms hook *logic* is correct given accurate `EGG_AGENT_ROLE` propagation; whether Claude Code itself propagates `EGG_AGENT_ROLE` correctly under real nested Agent-tool dispatch is verifiable only by running real Claude Code \u2026 when cq-3 flips to Agent-tool dispatch in a future issue, the production path becomes the empirical answer to that remaining half of R2\"). That framing \u2014 \"hook-logic test now, real-dispatch verification deferred to cq-3 follow-up\" \u2014 is the right scoping; the cq-4 / cq-6 contingencies branch on a meaningful signal (hook logic IS correct vs hook logic IS broken) rather than a phantom one.\n\n2. **TASK-4-4 conformance matrix \u2014 fixed.** Mechanism switched to \"per-subagent MagicMock stubs in the same style as `integration_tests/regression/test_substrate_smoke.py:56` (hardcoded `stdout=\"ok\"`, no recorded transcripts)\". This is option (b) from my v1 NACK \u2014 the cheapest path that keeps the matrix in-sandbox-runnable. The acceptance text documents the trade-off (\"MagicMock-vs-real-transcript trade-off\") so future readers know the matrix tests the SDLC *shape* not verbatim text. #2714's closed state is also acknowledged with the right framing (\"irrelevant per feedback Q1's rationale\"). The chicken-and-egg problem of v1 is gone.\n\n3. **TASK-4-2 fence-removal \u2014 fixed.** Now cites both `:212` (call site) and `:807-826` (method def) explicitly in the description (\"Two locations must be touched \u2026 Removing only the call site leaves dead code; both must be deleted\"). Acceptance criteria echoes both locations. A coder following the cite literally will now delete both.\n\n### Non-blocking items also resolved\n\n- **TASK-5-5 fork primitive \u2014 now aligned with cq-3.** Implementation switched from \"Agent tool with `subagent_type='general-purpose'`\" to \"`subprocess.Popen` running a fresh `egg_harness.run_agent` invocation in a clean Python process (NOT via Claude Code's Agent tool \u2014 that would commit to Agent-tool dispatch ahead of the cq-3 empirical verdict)\". The acceptance text also handles the future migration cleanly (\"If cq-3's empirical verdict in slice-5's ADR refresh recommends migrating to Agent-tool dispatch, the fork primitive's implementation will change accordingly in a follow-up \u2014 not in this slice\"). Good \u2014 cq-3 deferral preserved.\n\n- **TASK-2-5 enforcement target \u2014 corrected.** Surface moved from `orchestrator/mcp_tools.py` to `sandbox/egg_agent_tools/handlers/restrictions.py` (where `check_file_restriction(req)` lives per `grep -rn \"check_file_restriction\" --include=\"*.py\" orchestrator/ sandbox/`). The description explicitly cites the audit (\"the correct surface per the plan reviewer's audit is the in-sandbox tool handler, NOT `orchestrator/mcp_tools.py` (which is the orchestrator-side MCP server, a different surface)\"). The dependent test (TASK-2-6) moved to `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` \u2014 confirmed tester-writable.\n\n- **TASK-3-2 daemon lifecycle \u2014 pinned down.** Description now mandates `subprocess.Popen(start_new_session=True)` (or `os.setsid()` after fork) and the acceptance criteria asserts the property in TASK-3-8's test (\"the long-lived-across-Bash-exits property is asserted in TASK-3-8's test\"). The \"daemon dies when calling Bash exits\" failure mode is no longer latent.\n\n- **TASK-1-1 pending_hitl envelope \u2014 marked as shared contract for R17.** TASK-1-9 also calls out that the daemon (TASK-3-2) inherits the same envelope schema (\"closes risk_analyst R17 dual-bridge concern\"). Slice-1 commits to a state-serialization contract slice-3 picks up rather than reinventing.\n\n- **TASK-1-6 dependency on TASK-1-4 \u2014 made explicit.** Description now says \"Depends on TASK-1-4 having landed the two new rubric .md files first \u2026 The implementer should sequence TASK-1-4 \u2192 TASK-1-6 within the slice\".\n\n- **TASK-2-6 / TASK-2-7 renumbered** so file order matches numeric order.\n\n- **`LocalWorktreeManager` line citation corrected to :59**; **`_maybe_fence` dual-location** (`:212` call + `:807-826` def) added to the primitives table.\n\n### Remaining non-blocking observations\n\nThese are observations the planner doesn't need to address before consensus \u2014 surfacing for the implement-phase team:\n\n- **Slice-sizing advisories from v1 still apply** (#2137 \u2014 advisory only, never NACK per HITL decision-6 opt-2): slice-3 ~1,500 LOC (\"consider splitting\"), slice-4 ~2,200 LOC (\"strongly consider splitting\"), slice-5 ~2,500 LOC (\"strongly consider splitting\"). The operator retains override authority; the implementer should know if the slice expands further during implement-phase.\n\n- **R2 verdict semantics narrowed.** TASK-1-5's acceptance now tests hook *logic* given correct env propagation. That answers \"is `hook_entry.decide()` correct?\" not \"does Claude Code propagate `EGG_AGENT_ROLE` correctly under real Agent-tool dispatch?\". The latter question can only be answered when cq-3 flips to real Agent-tool dispatch in a future issue. The plan's framing (\"the production path becomes the empirical answer to that remaining half of R2\") is honest about this \u2014 but the cq-4 / cq-6 contingencies trigger on the *narrower* R2 signal. If the narrower R2 passes but the deferred real-dispatch R2 later fails, cq-4 / cq-6 may need to re-fire mid-future-issue. Worth flagging in the slice-1 ADR update (TASK-1-8) so the operator knows the contingency surface is split.\n\n- **R19 / R29 mitigation gates from risk_analyst** (plan-phase concurrency measurement before implement-phase ships; context-budget contingency on fork-trigger): the plan doesn't add explicit acceptance criteria gates for these on slice-2 (the plan-phase slice that produces the measurement). Risk_analyst's mitigations require reviewer_plan to hold slice-3 on slice-2's numbers; that's a review-cycle obligation, not a code task \u2014 but the slice-2 acceptance for TASK-2-4 (`test_inprocess_plan_brc.py`) could include \"records the per-loop wall-clock and parent-context delta to `.egg-state//substrate-metrics.json`\" so the slice-3 review has data to gate on. Worth considering at implement-time; not a NACK reason now.\n\n- **TASK-4-3 substrate-fixture sprawl across 14 existing regression tests** is itself a sizeable change set. If any of those 14 tests aren't substrate-portable in practice (transitively depend on `egg_stack` or `gateway_url` through a helper), the task will need to add `@pytest.mark.skip(reason=...)` markers \u2014 the acceptance criteria allows for this, but worth budgeting review attention for those 14 individual touches when slice-4 lands.\n\nPlan is ready for ingestion. Slice DAG is strictly linear (slice-1 \u2192 2 \u2192 3 \u2192 4 \u2192 5) so #2137 forest constraint is trivially satisfied. All 43 tasks have role assignments writable per `shared/egg_restrictions/patterns.py` (verified via per-file lookup across the full task list).\n", - "ack_version": 2 - }, - "version": 2 - }, - "timestamp": "2026-05-19T18:40:39.042251+00:00", - "phase": "plan" - }, - { - "id": "11599e8d-e19c-46", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "task_planner", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 2) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 2 - }, - "timestamp": "2026-05-19T18:40:39.042354+00:00", - "phase": "plan" - }, - { - "id": "775b63af-32d2-4f", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:40:39.091495+00:00", - "phase": "plan" - }, - { - "id": "307b9e11-91bb-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_plan", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T18:40:43.117934+00:00", - "phase": "plan" - }, - { - "id": "77a581d3-c0d4-4b", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:40:43.234899+00:00", - "phase": "plan" - }, - { - "id": "923e67cc-2ea8-41", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:40:43.244148+00:00", - "phase": "plan" - }, - { - "id": "f6e8c9ff-1067-42", - "pipeline_id": "issue-2717", - "from_role": "task_planner", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by task_planner", - "body": "", - "metadata": { - "consensus_reached": true - }, - "timestamp": "2026-05-19T18:40:44.247959+00:00", - "phase": "plan" - }, - { - "id": "ad1a76e5-31e9-4d", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:40:48.680618+00:00" - }, - "timestamp": "2026-05-19T18:40:48.704197+00:00", - "phase": "plan" - }, - { - "id": "25d11e06-3e27-48", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:40:48.757337+00:00", - "phase": "plan" - }, - { - "id": "ef3b8f50-203f-4a", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:40:49.024551+00:00" - }, - "timestamp": "2026-05-19T18:40:49.066600+00:00", - "phase": "plan" - }, - { - "id": "7be61421-ba4c-4d", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:40:49.155432+00:00", - "phase": "plan" - }, - { - "id": "74f26ae6-dd15-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:40:54.736152+00:00" - }, - "timestamp": "2026-05-19T18:41:17.785559+00:00", - "phase": "plan" - }, - { - "id": "3b65d5e2-9e74-4a", - "pipeline_id": "issue-2717", - "from_role": "architect", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:40:55.448086+00:00" - }, - "timestamp": "2026-05-19T18:41:17.788380+00:00", - "phase": "plan" - }, - { - "id": "980ddd5c-2753-49", - "pipeline_id": "issue-2717", - "from_role": "risk_analyst", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T18:40:56.091835+00:00" - }, - "timestamp": "2026-05-19T18:41:17.793616+00:00", - "phase": "plan" - }, - { - "id": "71ec8095-3e26-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_plan", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T18:41:17.840178+00:00", - "phase": "plan" - } -] \ No newline at end of file diff --git a/.egg-state/brc-history/2717-plan.md b/.egg-state/brc-history/2717-plan.md deleted file mode 100644 index 9c9b86bda3..0000000000 --- a/.egg-state/brc-history/2717-plan.md +++ /dev/null @@ -1,2187 +0,0 @@ -# BRC Consensus History — plan phase - -Generated: 2026-05-19T18:41:17Z -Pipeline: issue-2717 - -### [2026-05-19T18:07:08Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -Plan reviewer ready; analysis read; waiting for first producer proposal. - -````yaml -id: 36495853-a746-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT -```` - -### [2026-05-19T18:07:11Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 6a6f3fe6-bc9b-4d -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:07:33Z] overseer (HEARTBEAT): heartbeat: WORKING - -Cycle 1 complete. Pipeline healthy: plan phase active, 4 agents running (architect, reviewer_plan, risk_analyst, task_planner), all started ~56s ago. Orchestrator /status endpoint unreachable (transient timeout) but BRC state confirms pipeline is running. No corrective actions needed this cycle. - -````yaml -id: 61cd5a31-369c-4f -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:08:30Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: c3c66686-3d59-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:09:26Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8a0d744a-b496-4e -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:10:26Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 2bf0f365-5775-42 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:11:26Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 8ba95dda-2b0b-48 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:12:26Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 4674bbc2-8ccd-48 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:13:26Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 24d6cc65-d265-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:14:26Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: dd516713-6692-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:15:26Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3434654a-5b2e-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:16:50Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: be9fcb5f-f1b2-42 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:07:11.523831+00:00' -```` - -### [2026-05-19T18:17:25Z] architect (CONSENSUS_PROPOSE): Proposal from architect - -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 via Option B flatten-stage-script MVP plus R2 hook role-resolution spike (2-subagent worked example) as the first deliverable; (2) Plan-phase substrate wiring (architect/task_planner/risk_analyst + reviewer_plan rubrics + run_pipeline_in_process plan branch); (3) Implement-phase substrate wiring (coder/tester/documenter + 5 reviewer rubrics + implement branch; cq-3 empirical data collected here, daemon-mode bridge transition lands here); (4) PR-phase substrate + 5-issue × 2-substrate conformance matrix (#2714, #2474, #2261, #2705, #2718) + scope-fence removal at orchestrator/substrate/in_process.py:820-826; (5) Parallel hardening pass with EggHarnessSpawner + `egg-orch local-run --issue N` CLI (DoD #5 feedback Q3 option A), cost cap EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200 (cq-6 option 2), drop v0.x markers on claude-code-side modules only (cq-7 option 4), and ADR refresh (DoD #6). Includes runtime-primitive surfacing per issue #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). The analysis identifies 6 anticipated risks (R2 hook multi-subagent, R7 context budget, BRC concurrency on new substrate, cost runaway, decomposition conflict with #2261 slice-15, scope-fence removal blast radius) with mitigations, and 6 open questions for task_planner and risk_analyst to consume. - -````yaml -id: 16ece202-49b5-46 -phase: plan -metadata: - payload: - summary: "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 via Option B flatten-stage-script\ - \ MVP plus R2 hook role-resolution spike (2-subagent worked example) as the\ - \ first deliverable; (2) Plan-phase substrate wiring (architect/task_planner/risk_analyst\ - \ + reviewer_plan rubrics + run_pipeline_in_process plan branch); (3) Implement-phase\ - \ substrate wiring (coder/tester/documenter + 5 reviewer rubrics + implement\ - \ branch; cq-3 empirical data collected here, daemon-mode bridge transition\ - \ lands here); (4) PR-phase substrate + 5-issue \xD7 2-substrate conformance\ - \ matrix (#2714, #2474, #2261, #2705, #2718) + scope-fence removal at orchestrator/substrate/in_process.py:820-826;\ - \ (5) Parallel hardening pass with EggHarnessSpawner + `egg-orch local-run --issue\ - \ N` CLI (DoD #5 feedback Q3 option A), cost cap EGG_PIPELINE_MAX_AGENT_INVOCATIONS=200\ - \ (cq-6 option 2), drop v0.x markers on claude-code-side modules only (cq-7\ - \ option 4), and ADR refresh (DoD #6). Includes runtime-primitive surfacing\ - \ per issue #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). The analysis identifies 6 anticipated risks (R2 hook\ - \ multi-subagent, R7 context budget, BRC concurrency on new substrate, cost\ - \ runaway, decomposition conflict with #2261 slice-15, scope-fence removal blast\ - \ radius) with mitigations, and 6 open questions for task_planner and risk_analyst\ - \ to consume." - attestation: {} - artifacts: - - .egg-state/agent-outputs/2717-architect-output.json - risk_considered: 'Six risks documented: R2 hook role-resolution under nested dispatch - (mitigation: slice-1 ships the 2-subagent spike + MCP-validator fallback BEFORE - phase rollout); R7 subagent context budget at deeper-research roles (mitigation: - slice-3 metrics collection drives a follow-up issue if regression > 50%); BRC - concurrency on new substrate at multi-role scale (mitigation: slice-2 plan-phase - tests cover INV-3/INV-5/INV-6 before slice-3''s 8-role surface); cost runaway - from 15-subagent NACK loops (mitigation: cq-6 option 2 cap at 200); #2261 slice-15 - decomposition collision (mitigation: run_pipeline_in_process reuses ConcurrentPhaseExecutor - + PeerConsensusTracker rather than touching orchestrator/routes/pipelines.py); - scope-fence removal blast radius (mitigation: fence removed in slice-4 only - after slices 2 and 3 have proven each phase independently).' - commit_sha: c4f4e0ccdb29d4ddfaac28ef833e64b002da44bd - files_changed: - - .egg-state/agent-outputs/2717-architect-output.json - tests_run: [] - tasks_satisfied: [] - version: 1 - commit_sha: c4f4e0ccdb29d4ddfaac28ef833e64b002da44bd -```` - -### [2026-05-19T18:17:25Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 7c027e5f-5038-49 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:18:20Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 4bd23c45-a132-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:17:55.125844+00:00' -```` - -### [2026-05-19T18:19:10Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 45a45b8f-d482-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:17:55.125844+00:00' -```` - -### [2026-05-19T18:20:10Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d59fbbc7-8ffc-47 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:17:55.125844+00:00' -```` - -### [2026-05-19T18:21:02Z] risk_analyst (CONSENSUS_PROPOSE): Proposal from risk_analyst - -Risk assessment for #2717 substrate-swap follow-up rollout. Enumerates 18 net-new risks (R17–R34) covering: HITL bridge dual-architecture (R17, cq-1 Option C-hybrid), 15-rubric authorship + structural depth-gap closure (R18, #2622), parent-session strain under 8-way harness re-host (R19/R29, cq-3 deferred), reviewer-roster coverage gap (R20, feedback Q5 declined new roles), 5-issue conformance reproducibility (R21, feedback Q1 fixed set), #2261 slice-15 coordination (R22), cost-cap visibility at 200 invocations (R23, cq-6), hardening-wave 5-slice fan-out (R24), scope-fence partial-rollout fragility (R25, feedback Q6), EggHarnessSpawner as 3rd protocol implementation (R26, feedback Q3 Option A), MCP-validator fallback structural enforcement gap (R27, cq-6 option 2 fallback path), phase-sequential late-discovery rework (R28), Option (b) bridge concurrent-HITL aggregation (R30), 15-subagent trust-context scaling (R31, feedback Q4 declined extras), interface-stability marker timing (R32, cq-7), apply-phase DoD ambiguity (R33), and bridge-driver file-boundary risk (R34, #2474-class). Includes 10 runtime-primitive verifications, 6 trust-boundary shifts, 11 prioritized recommendations (REC1-REC11), 5 open questions for implement-phase HITL, and DoD-criterion-by-criterion risk mapping. Recommendation: PROCEED_WITH_MITIGATIONS — slice-0 R2 validation is the load-bearing gate for downstream slice gating. - -````yaml -id: c11dc755-4d86-4e -phase: plan -metadata: - payload: - summary: "Risk assessment for #2717 substrate-swap follow-up rollout. Enumerates\ - \ 18 net-new risks (R17\u2013R34) covering: HITL bridge dual-architecture (R17,\ - \ cq-1 Option C-hybrid), 15-rubric authorship + structural depth-gap closure\ - \ (R18, #2622), parent-session strain under 8-way harness re-host (R19/R29,\ - \ cq-3 deferred), reviewer-roster coverage gap (R20, feedback Q5 declined new\ - \ roles), 5-issue conformance reproducibility (R21, feedback Q1 fixed set),\ - \ #2261 slice-15 coordination (R22), cost-cap visibility at 200 invocations\ - \ (R23, cq-6), hardening-wave 5-slice fan-out (R24), scope-fence partial-rollout\ - \ fragility (R25, feedback Q6), EggHarnessSpawner as 3rd protocol implementation\ - \ (R26, feedback Q3 Option A), MCP-validator fallback structural enforcement\ - \ gap (R27, cq-6 option 2 fallback path), phase-sequential late-discovery rework\ - \ (R28), Option (b) bridge concurrent-HITL aggregation (R30), 15-subagent trust-context\ - \ scaling (R31, feedback Q4 declined extras), interface-stability marker timing\ - \ (R32, cq-7), apply-phase DoD ambiguity (R33), and bridge-driver file-boundary\ - \ risk (R34, #2474-class). Includes 10 runtime-primitive verifications, 6 trust-boundary\ - \ shifts, 11 prioritized recommendations (REC1-REC11), 5 open questions for\ - \ implement-phase HITL, and DoD-criterion-by-criterion risk mapping. Recommendation:\ - \ PROCEED_WITH_MITIGATIONS \u2014 slice-0 R2 validation is the load-bearing\ - \ gate for downstream slice gating." - attestation: {} - artifacts: - - .egg-state/agent-outputs/2717-risk_analyst-output.json - risk_considered: 18 distinct risks across security, performance, design, and compatibility - dimensions; 8 flagged for human review (R17 bridge dual-architecture, R18 rubric - depth closure, R19 parent-session strain, R21 conformance reproducibility, R27 - MCP-validator fallback structural gap, R29 implement-phase context budget, R31 - 15-subagent trust scaling, R33 apply-phase DoD scope). Each risk includes mitigation - strategy, effort, residual-risk level, and (where flagged) suggested reviewer - and review reason. - commit_sha: 7a5cec59d3ec5170922b1688703d24b072d7f50a - files_changed: - - .egg-state/agent-outputs/2717-risk_analyst-output.json - tests_run: [] - tasks_satisfied: [] - version: 1 - commit_sha: 7a5cec59d3ec5170922b1688703d24b072d7f50a -```` - -### [2026-05-19T18:21:31Z] overseer (OVERSEER_ALERT): agent-heartbeat-stall [low] - -Orchestrator REST API experiencing recurring ~5-7 min intermittent outages on issue-2717 - -Detail: -The orchestrator HTTP API at orchestrator.egg-system.svc.cluster.local:9849 has timed out 4 times during this overseer session (at ~18:06, ~18:15, ~18:19, ~18:20 UTC), each lasting 12-20 seconds. This causes spurious overseer heartbeat_timeout and progress_stall alerts (4 stale alerts now visible in monitor output), and prevents the overseer from registering its own health signals during outage windows. The BRC protocol channel is unaffected — pipeline agents are communicating normally and pipeline progression continues. The architect has proposed at 18:17:25 and reviewer_plan is actively reviewing. No pipeline agents have been impacted by the API outages. - -Recommended action: -Investigate orchestrator pod health and any pod restarts or GC pauses around 18:06, 18:15, 18:19 UTC. Consider increasing the heartbeat threshold for the overseer role from 120s to 180s to absorb these transient gaps without generating false alerts. - -````yaml -id: 1f356f95-c13e-4d -phase: plan -```` - -### [2026-05-19T18:21:31Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 63c3a966-38bd-42 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:17:55.125844+00:00' -```` - -### [2026-05-19T18:21:31Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 3cb8c405-d404-48 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:17.729796+00:00' -```` - -### [2026-05-19T18:21:32Z] architect (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 2546d6c5-0be8-4c -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:22:02Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: cafe3b14-4276-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:22:02Z] task_planner (CONSENSUS_PROPOSE): Proposal from task_planner - -Plan for #2717 substrate-swap follow-up rollout. Decomposes the work into a strictly linear 5-slice DAG (each slice has one DAG parent) per cq-2 = Option 3 (Bridge-first then phase-sequential rollout): slice-1 closes the heredoc-HITL bridge gap via flattened stage scripts and ships the cq-5 R2 hook-validation early spike alongside the two refine-team reviewer rubrics; slice-2 wires the plan-phase substrate (architect + task_planner + risk_analyst + reviewer_plan) as the first multi-role BRC stress test; slice-3 wires the implement-phase substrate (3 producers + 5 reviewers) and switches the HITL bridge to a long-lived Python daemon over a UNIX socket (cq-1 Option C, implement half); slice-4 wires the pr-phase, removes the walking-skeleton scope fence (feedback Q6), and ships the 5-issue conformance matrix (#2714, #2474, #2261, #2705, #2718 — feedback Q1) with pytest.mark.slow gating + 3-hour per-phase ceiling (feedback Q2); slice-5 hardens with the EGG_PIPELINE_MAX_AGENT_INVOCATIONS cost cap default 200 (cq-6), EggHarnessSpawner + egg-orch local-run headless CLI (DoD #5 / feedback Q3), fork-based delegation (cq-10 deferred half from #2623), contingent R15 model-(b) migration based on slice-1's R2 verdict (cq-4), drops the v0.x unstable marker on the four protocols (cq-7), and refreshes the ADR. 42 contract tasks total. Primitives audit per #2594 cites every named symbol with file:line evidence; NEW primitives are tagged (NEW — TASK-X-Y). Trust-boundary scope: conformance tests live under integration_tests/regression/ (substrate-portable, the substrate fixture at conftest.py:623 skips claude-code dimension in in-sandbox-agent context), not under integration_tests/local_pipeline/ (kubectl-gated). Role assignments: coder for orchestrator/substrate/* Python and orchestrator/cli.py, tester for tests/, documenter for plugins/egg-sdlc/skills/egg-sdlc/agents/*.md role rubrics + SKILL.md + ADR. - -````yaml -id: d22dbf8a-a9e2-4f -phase: plan -metadata: - payload: - summary: "Plan for #2717 substrate-swap follow-up rollout. Decomposes the work\ - \ into a strictly linear 5-slice DAG (each slice has one DAG parent) per cq-2\ - \ = Option 3 (Bridge-first then phase-sequential rollout): slice-1 closes the\ - \ heredoc-HITL bridge gap via flattened stage scripts and ships the cq-5 R2\ - \ hook-validation early spike alongside the two refine-team reviewer rubrics;\ - \ slice-2 wires the plan-phase substrate (architect + task_planner + risk_analyst\ - \ + reviewer_plan) as the first multi-role BRC stress test; slice-3 wires the\ - \ implement-phase substrate (3 producers + 5 reviewers) and switches the HITL\ - \ bridge to a long-lived Python daemon over a UNIX socket (cq-1 Option C, implement\ - \ half); slice-4 wires the pr-phase, removes the walking-skeleton scope fence\ - \ (feedback Q6), and ships the 5-issue conformance matrix (#2714, #2474, #2261,\ - \ #2705, #2718 \u2014 feedback Q1) with pytest.mark.slow gating + 3-hour per-phase\ - \ ceiling (feedback Q2); slice-5 hardens with the EGG_PIPELINE_MAX_AGENT_INVOCATIONS\ - \ cost cap default 200 (cq-6), EggHarnessSpawner + egg-orch local-run headless\ - \ CLI (DoD #5 / feedback Q3), fork-based delegation (cq-10 deferred half from\ - \ #2623), contingent R15 model-(b) migration based on slice-1's R2 verdict (cq-4),\ - \ drops the v0.x unstable marker on the four protocols (cq-7), and refreshes\ - \ the ADR. 42 contract tasks total. Primitives audit per #2594 cites every named\ - \ symbol with file:line evidence; NEW primitives are tagged (NEW \u2014 TASK-X-Y).\ - \ Trust-boundary scope: conformance tests live under integration_tests/regression/\ - \ (substrate-portable, the substrate fixture at conftest.py:623 skips claude-code\ - \ dimension in in-sandbox-agent context), not under integration_tests/local_pipeline/\ - \ (kubectl-gated). Role assignments: coder for orchestrator/substrate/* Python\ - \ and orchestrator/cli.py, tester for tests/, documenter for plugins/egg-sdlc/skills/egg-sdlc/agents/*.md\ - \ role rubrics + SKILL.md + ADR." - attestation: {} - artifacts: - - .egg-state/drafts/2717-plan.md - risk_considered: "Slice-3 is the largest (~10 tasks covering 8 role rubrics +\ - \ multi-cycle BRC + daemon bridge); plan reviewer may flag it as advisory only\ - \ per the size guidance (>1000 LOC potential). The R2 contingency in TASK-2-5/TASK-2-7\ - \ and TASK-5-7 creates a fork in the plan based on slice-1 outcome \u2014 chose\ - \ to encode the contingency in task descriptions rather than splitting into\ - \ two separate plans because the fork is small (model-(a) stays vs. model-(b)\ - \ migration + MCP-validator-side enforcement). Linear chain has no fan-out and\ - \ no serialized_chain_order, so the forest constraint is satisfied trivially.\ - \ Soft size guidance only; no DAG-shape risk." - commit_sha: 00ef3ff05 - files_changed: - - .egg-state/drafts/2717-plan.md - tests_run: [] - tasks_satisfied: - - TASK-1-1 - - TASK-1-2 - - TASK-1-3 - - TASK-1-4 - - TASK-1-5 - - TASK-1-6 - - TASK-1-7 - - TASK-1-8 - - TASK-2-1 - - TASK-2-2 - - TASK-2-3 - - TASK-2-4 - - TASK-2-5 - - TASK-2-6 - - TASK-2-7 - - TASK-3-1 - - TASK-3-2 - - TASK-3-3 - - TASK-3-4 - - TASK-3-5 - - TASK-3-6 - - TASK-3-7 - - TASK-3-8 - - TASK-3-9 - - TASK-3-10 - - TASK-4-1 - - TASK-4-2 - - TASK-4-3 - - TASK-4-4 - - TASK-4-5 - - TASK-4-6 - - TASK-4-7 - - TASK-5-1 - - TASK-5-2 - - TASK-5-3 - - TASK-5-4 - - TASK-5-5 - - TASK-5-6 - - TASK-5-7 - - TASK-5-8 - - TASK-5-9 - - TASK-5-10 - version: 1 - commit_sha: 00ef3ff05 -```` - -### [2026-05-19T18:22:02Z] orchestrator → risk_analyst (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: f1c6f6d2-44cc-4a -phase: plan -metadata: - ready_to_confirm: true - version: 1 -```` - -### [2026-05-19T18:22:02Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 5962933b-24ea-43 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:22:33Z] risk_analyst (CONSENSUS_CONFIRMED): Confirmed by risk_analyst - -````yaml -id: 1aacc57b-fc67-43 -phase: plan -metadata: - consensus_reached: false -```` - -### [2026-05-19T18:22:33Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d9b0ea07-a953-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:23:02Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 74910704-1586-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:23:02Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: a2dfc76c-7278-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:23:34Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e83ace58-d0f4-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:23:53Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: f72e1946-13b3-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:24:02Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 95b557a3-2ef4-47 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:24:34Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8a56f99d-e0d3-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:24:53Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 294334f7-398d-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:25:02Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 105318fe-f664-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:25:34Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b3692c59-24be-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:25:53Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: b98a716d-cf60-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:26:02Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8eb8cc80-6a09-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:26:34Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 8ab99be4-ba1a-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:26:53Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 37bc1ec7-6594-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:27:02Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e0d82584-ebbe-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:27:34Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 37cdc7fc-5dad-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:27:53Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 81f7bdbd-f639-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:28:02Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5bf3bfb8-5f4e-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:28:35Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: dcded3f4-15ae-42 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:28:53Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: a7da5452-f816-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:29:03Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 00d50ad1-2772-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:29:35Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: d683a567-34df-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:29:53Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 265f3444-e165-48 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:30:03Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 5dcd3770-6620-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:30:35Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: bb66bc0a-e351-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:30.428015+00:00' -```` - -### [2026-05-19T18:30:54Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 745e0e48-101a-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:22:38.425238+00:00' -```` - -### [2026-05-19T18:31:03Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 99a94500-45fb-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:21:44.108650+00:00' -```` - -### [2026-05-19T18:31:22Z] reviewer_plan → architect (CONSENSUS_ACK): ACK from reviewer_plan for architect - - -Reviewed `.egg-state/agent-outputs/2717-architect-output.json` (389 lines) against the refine analysis at `.egg-state/drafts/2717-analysis.md` and the seven settled HITL decisions (cq-1 … cq-7) plus six feedback answers. - -### What's solid -- **Slice decomposition matches cq-2 Option 3 exactly**: bridge → plan → implement → pr+conformance → hardening, with the operator's chosen "phase-sequential then parallel hardening" shape preserved across all five slices. -- **Every primitive cited has file:line evidence**, and the citations I verified (run_pipeline_in_process at `in_process.py:84` ✓, ClaudeCodeSpawner at `spawner.py:64` ✓, _load_egg_sdlc_role_rubric at `__init__.py:232` ✓, build_agent_patterns at `patterns.py:768` ✓, _PHASE_ROLES at `agent_roles.py:1172` ✓, _PHASE_REVIEWERS at `:1183` ✓, ConcurrentPhaseExecutor at `concurrent_executor.py:114` ✓, PeerConsensusTracker at `peer_consensus.py:69` ✓, validate_invariants at `action_guards.py:631` ✓, substrate fixture at `conftest.py:622–642` ✓) all line up with the actual code. -- **NEW primitives are flagged**: `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`, `egg-orch local-run`, the conformance matrix, the bridge driver are correctly marked as new, and I verified the negative-grep claims (`grep -rn EGG_PIPELINE_MAX_AGENT_INVOCATIONS orchestrator/ shared/` returns zero ✓; `cmd_local_run` is absent from `orchestrator/cli.py` per `grep -n "add_parser.*local-run"` ✓). -- **All six feedback Q answers absorbed verbatim** (curated 5 issues #2714/#2474/#2261/#2705/#2718, pytest.mark.slow + 3-hr ceiling, full-local-run Option A, no extra mitigations, no new reviewer roles, fence removal). -- **Risks well-anticipated**: R2-hook-multi-subagent, R7-context-budget, BRC-concurrency-on-new-substrate, cost-runaway, decomposition-conflict-with-2261, scope-fence-removal-blast-radius all map to concrete mitigations referencing the slice they live in. -- **`open_questions_for_task_planner_and_risk_analyst` is a genuinely useful handoff** — the granularity question, the R2 spike scope question, the rollback-flag question are all things the downstream producers should answer rather than the architect prematurely fixing. - -### Non-blocking -- **R2 spike framing implicitly requires partial Agent-tool dispatch in slice-1 that contradicts cq-3.** `slice_1_bridge_plus_r2_spike.deliverables[2]` says "spawns one role (parent) and has it dispatch a second role (child) via Claude Code's Agent tool (subagent_type='general-purpose')". I verified that PreToolUseHookPolicy / hook_entry is NOT invoked by the harness path — `grep -rn "PreToolUseHookPolicy\|hook_entry" shared/egg_harness/` returns zero hits — so Agent-tool dispatch is the only mechanism that actually triggers the hook. cq-3 selected "stay on harness re-host"; the R2 spike therefore needs a minimal Agent-tool dispatch path that the production spawner doesn't use. Either acknowledge this as a slice-1 deliverable ("ship a minimal Agent-tool dispatch path for the spike only, distinct from the production ClaudeCodeSpawner") or pivot to a hook-logic unit test (call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE` env vars — that's not literally "nested dispatch" but it does answer the load-bearing question "does the hook resolve EGG_AGENT_ROLE correctly"). As-is the analysis is internally consistent, but the task_planner has already diverged from this framing (their TASK-1-5 says "via the harness", which doesn't fire the hook at all — I'm NACKing them on this separately). -- **Citation accuracy nits** — these are off by a few lines: - - `AgentSpawner Protocol` at `spawner.py:73` → actual `:74` - - `MessageBus Protocol` at `message_bus.py:27` → actual `:28` - - `PolicyEnforcer Protocol` at `policy.py:25` → actual `:26` - - `WorktreeManager Protocol` at `worktree.py:24` → actual `:25` - - `SubstrateBundle` at `__init__.py:77` → actual `:78` - - `_maybe_fence at :820-826` → the def lives at `:807`; `:821` is the `raise` inside it; `:212` is the call site. - None of these change the semantics — but if a downstream slice grep-pins on the cited line it will miss by one. -- **slice_3 cq-3 empirical data path naming is good** — `.egg-state/conformance-metrics/-.json` is the right shape, but the task_planner picked a sibling path `.egg-state//substrate-metrics.json` in TASK-3-9. Worth aligning the two so slice-5's cost-cap consumer reads from one canonical location. -- **`policy_single_source_of_truth` framing mislabels gateway as "trusted-CI-runner"**. `gateway/phase_filter.py:1061` is a deployed-pod git-push validator running in the gateway container, not a pytest-context check. The trust-boundary categorisation in `docs/architecture/integration-test-trust-boundary.md` doesn't have a "gateway-mediated agent operations" tier today; calling it "trusted-CI-runner" muddles the categories. Not load-bearing for the plan, but the ADR refresh in TASK-5-9 should fix this. -- **`#2261 slice-15` rollout-dependency framing is right** ("decoupled at the file level") — but slice-4's TASK-4-1 says `_run_pr` "Reuses the post-#2685 context-PR + PR-phase plumbing in `orchestrator/routes/pipelines.py`". If that plumbing moves into `orchestrator/routes/pipelines/_run_loop/_run_pr.py` mid-rollout, slice-4's import path will need to follow the decomposition. Worth flagging as a coordination point with #2261 owners. - -Architectural design is sound, alignment with the analysis is complete, and the slice DAG is a strictly-linear chain (no forest violation possible). Approving so the task_planner re-proposal cycle can proceed. - - -````yaml -id: 4d2526d6-39a3-40 -phase: plan -metadata: - payload: - artifact_references: - - .egg-state/agent-outputs/2717-architect-output.json - - .egg-state/drafts/2717-analysis.md - reason: "\nReviewed `.egg-state/agent-outputs/2717-architect-output.json` (389\ - \ lines) against the refine analysis at `.egg-state/drafts/2717-analysis.md`\ - \ and the seven settled HITL decisions (cq-1 \u2026 cq-7) plus six feedback\ - \ answers.\n\n### What's solid\n- **Slice decomposition matches cq-2 Option\ - \ 3 exactly**: bridge \u2192 plan \u2192 implement \u2192 pr+conformance \u2192\ - \ hardening, with the operator's chosen \"phase-sequential then parallel hardening\"\ - \ shape preserved across all five slices.\n- **Every primitive cited has file:line\ - \ evidence**, and the citations I verified (run_pipeline_in_process at `in_process.py:84`\ - \ \u2713, ClaudeCodeSpawner at `spawner.py:64` \u2713, _load_egg_sdlc_role_rubric\ - \ at `__init__.py:232` \u2713, build_agent_patterns at `patterns.py:768` \u2713\ - , _PHASE_ROLES at `agent_roles.py:1172` \u2713, _PHASE_REVIEWERS at `:1183`\ - \ \u2713, ConcurrentPhaseExecutor at `concurrent_executor.py:114` \u2713, PeerConsensusTracker\ - \ at `peer_consensus.py:69` \u2713, validate_invariants at `action_guards.py:631`\ - \ \u2713, substrate fixture at `conftest.py:622\u2013642` \u2713) all line up\ - \ with the actual code.\n- **NEW primitives are flagged**: `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`,\ - \ `egg-orch local-run`, the conformance matrix, the bridge driver are correctly\ - \ marked as new, and I verified the negative-grep claims (`grep -rn EGG_PIPELINE_MAX_AGENT_INVOCATIONS\ - \ orchestrator/ shared/` returns zero \u2713; `cmd_local_run` is absent from\ - \ `orchestrator/cli.py` per `grep -n \"add_parser.*local-run\"` \u2713).\n-\ - \ **All six feedback Q answers absorbed verbatim** (curated 5 issues #2714/#2474/#2261/#2705/#2718,\ - \ pytest.mark.slow + 3-hr ceiling, full-local-run Option A, no extra mitigations,\ - \ no new reviewer roles, fence removal).\n- **Risks well-anticipated**: R2-hook-multi-subagent,\ - \ R7-context-budget, BRC-concurrency-on-new-substrate, cost-runaway, decomposition-conflict-with-2261,\ - \ scope-fence-removal-blast-radius all map to concrete mitigations referencing\ - \ the slice they live in.\n- **`open_questions_for_task_planner_and_risk_analyst`\ - \ is a genuinely useful handoff** \u2014 the granularity question, the R2 spike\ - \ scope question, the rollback-flag question are all things the downstream producers\ - \ should answer rather than the architect prematurely fixing.\n\n### Non-blocking\n\ - - **R2 spike framing implicitly requires partial Agent-tool dispatch in slice-1\ - \ that contradicts cq-3.** `slice_1_bridge_plus_r2_spike.deliverables[2]` says\ - \ \"spawns one role (parent) and has it dispatch a second role (child) via Claude\ - \ Code's Agent tool (subagent_type='general-purpose')\". I verified that PreToolUseHookPolicy\ - \ / hook_entry is NOT invoked by the harness path \u2014 `grep -rn \"PreToolUseHookPolicy\\\ - |hook_entry\" shared/egg_harness/` returns zero hits \u2014 so Agent-tool dispatch\ - \ is the only mechanism that actually triggers the hook. cq-3 selected \"stay\ - \ on harness re-host\"; the R2 spike therefore needs a minimal Agent-tool dispatch\ - \ path that the production spawner doesn't use. Either acknowledge this as a\ - \ slice-1 deliverable (\"ship a minimal Agent-tool dispatch path for the spike\ - \ only, distinct from the production ClaudeCodeSpawner\") or pivot to a hook-logic\ - \ unit test (call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE`\ - \ env vars \u2014 that's not literally \"nested dispatch\" but it does answer\ - \ the load-bearing question \"does the hook resolve EGG_AGENT_ROLE correctly\"\ - ). As-is the analysis is internally consistent, but the task_planner has already\ - \ diverged from this framing (their TASK-1-5 says \"via the harness\", which\ - \ doesn't fire the hook at all \u2014 I'm NACKing them on this separately).\n\ - - **Citation accuracy nits** \u2014 these are off by a few lines:\n - `AgentSpawner\ - \ Protocol` at `spawner.py:73` \u2192 actual `:74`\n - `MessageBus Protocol`\ - \ at `message_bus.py:27` \u2192 actual `:28`\n - `PolicyEnforcer Protocol`\ - \ at `policy.py:25` \u2192 actual `:26`\n - `WorktreeManager Protocol` at `worktree.py:24`\ - \ \u2192 actual `:25`\n - `SubstrateBundle` at `__init__.py:77` \u2192 actual\ - \ `:78`\n - `_maybe_fence at :820-826` \u2192 the def lives at `:807`; `:821`\ - \ is the `raise` inside it; `:212` is the call site.\n None of these change\ - \ the semantics \u2014 but if a downstream slice grep-pins on the cited line\ - \ it will miss by one.\n- **slice_3 cq-3 empirical data path naming is good**\ - \ \u2014 `.egg-state/conformance-metrics/-.json` is the right\ - \ shape, but the task_planner picked a sibling path `.egg-state//substrate-metrics.json`\ - \ in TASK-3-9. Worth aligning the two so slice-5's cost-cap consumer reads from\ - \ one canonical location.\n- **`policy_single_source_of_truth` framing mislabels\ - \ gateway as \"trusted-CI-runner\"**. `gateway/phase_filter.py:1061` is a deployed-pod\ - \ git-push validator running in the gateway container, not a pytest-context\ - \ check. The trust-boundary categorisation in `docs/architecture/integration-test-trust-boundary.md`\ - \ doesn't have a \"gateway-mediated agent operations\" tier today; calling it\ - \ \"trusted-CI-runner\" muddles the categories. Not load-bearing for the plan,\ - \ but the ADR refresh in TASK-5-9 should fix this.\n- **`#2261 slice-15` rollout-dependency\ - \ framing is right** (\"decoupled at the file level\") \u2014 but slice-4's\ - \ TASK-4-1 says `_run_pr` \"Reuses the post-#2685 context-PR + PR-phase plumbing\ - \ in `orchestrator/routes/pipelines.py`\". If that plumbing moves into `orchestrator/routes/pipelines/_run_loop/_run_pr.py`\ - \ mid-rollout, slice-4's import path will need to follow the decomposition.\ - \ Worth flagging as a coordination point with #2261 owners.\n\nArchitectural\ - \ design is sound, alignment with the analysis is complete, and the slice DAG\ - \ is a strictly-linear chain (no forest violation possible). Approving so the\ - \ task_planner re-proposal cycle can proceed.\n" - ack_version: 1 - version: 1 -```` - -### [2026-05-19T18:31:22Z] orchestrator → architect (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: 5494de46-183c-46 -phase: plan -metadata: - ready_to_confirm: true - version: 1 -```` - -### [2026-05-19T18:31:22Z] architect (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 573f79a9-099e-40 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:31:22Z] reviewer_plan → task_planner (CONSENSUS_NACK): NACK from reviewer_plan for task_planner - - -Reviewed `.egg-state/drafts/2717-plan.md` (1394 lines, 5 slices, 42 tasks) and cross-checked against the architect's design, the refine analysis, and the actual codebase. The slice shape is right (matches cq-2 Option 3), the role↔files alignment is clean (I ran the per-file/per-role check across all 42 tasks; every task's `files:` is writable by its `role:` per `shared/egg_restrictions/patterns.py` — no orchestrator-side `403 restricted_path_modified` predicted), and the primitives table is unusually thorough. But three blocking issues will produce false signals or unimplementable tasks if shipped as-is. - -### Blocking - -1. **TASK-1-5 R2 spike mechanism is structurally broken — the test will not exercise the PreToolUse hook at all.** The task says "Test dispatches a parent subagent (role=architect) and a child subagent (role=tester) via the harness; asserts the PreToolUse hook denies a write to `orchestrator/foo.py` when invoked from the child". I verified that `shared/egg_harness/client.py:60-150` registers its own `ToolRegistry` with Python-native Bash/Read/Write/Edit/Glob/Grep handlers and routes permission checks through `registry.set_permission_callback(callback)` — `grep -rn "PreToolUseHookPolicy\|hook_entry" shared/egg_harness/` returns **zero hits**. The PreToolUse hook is invoked exclusively by Claude Code's native tool registry (see `orchestrator/substrate/claude_code/hook_entry.py:17` `"command": "python3 -m orchestrator.substrate.claude_code.hook_entry"` — that's Claude Code's PreToolUse settings.json wiring). Therefore: a harness-dispatched child never triggers `hook_entry.decide()`, and the assertion "the PreToolUse hook denies a write" cannot fail (the hook isn't in the loop) and cannot pass (the hook isn't in the loop) — the test will either green on a no-op or assert against a manually-invoked `hook_entry.decide(...)` that has nothing to do with "nested dispatch". This is the load-bearing test for the rollout: cq-4 (R15 migration timing) and cq-6 fallback (MCP-validator enforcement, TASK-2-5 and TASK-5-7) both branch on its verdict. **Fix**: pick one of (a) call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE` env vars and rename the task to make clear it's a hook-logic unit test, not a nested-dispatch test (cheap, doesn't answer the architect's question about Agent-tool nested dispatch, but is honest about scope); (b) ship a minimal Claude Code Agent-tool dispatch path *just for the spike* (precursor of ClaudeCodeAgentToolSpawner, distinct from the production `ClaudeCodeSpawner` harness re-host) and run the parent/child dispatch through it — that does exercise the hook but adds a slice-1 deliverable the plan currently doesn't enumerate. The architect's text picked option (b) implicitly; you picked option (a)'s file location but kept option (b)'s framing. Pick one and update the task description, acceptance criteria, and dependent contingency tasks (TASK-2-5, TASK-5-7) to match. - -2. **TASK-4-4 references "recorded transcripts" that no task records.** The acceptance text says "via harness fakes that replay a recorded transcript of that issue's refine + plan + implement + pr phases". I searched the codebase: there is no existing transcript-replay infrastructure (the existing `integration_tests/regression/test_substrate_smoke.py:56` uses `MagicMock` with hardcoded `stdout="ok"`, not a recorded-transcript replay). No task in slices 1–4 records transcripts for the five curated issues; no task in slice 5 either. As specified, TASK-4-4 cannot be implemented — its `files:` says `test_conformance_matrix.py` but the test body needs 4 phases × 5 issues = 20 transcripts that don't exist and aren't produced anywhere in the plan. **Fix**: either (a) add a sub-task that records transcripts during slice-3's first real implement-phase run on each curated issue (the natural place — slice-3 already produces `.egg-state//substrate-metrics.json` per TASK-3-9, so transcript capture co-locates well); (b) change the mechanism to per-subagent `MagicMock` matching `test_substrate_smoke.py`'s pattern (cheaper, but you lose phase-level realism); or (c) acknowledge the matrix runs subagents live in CI and adjust the 3-hour-per-phase ceiling story accordingly. Whichever path, the plan needs an explicit task that produces the inputs TASK-4-4 consumes. - -3. **TASK-4-2 fence-removal task cites the wrong line.** The task description says "Remove the walking-skeleton scope fence at `orchestrator/substrate/in_process.py:212`". I checked: `:212` is the *call site* (`self._maybe_fence(answer)` inside `_run`). The `_maybe_fence` static-method *definition* is at `:807-826`. Both need to be removed to actually delete the fence — and the docstring at `:92-125` that promises `NotImplementedError` for plan/implement/pr (which the acceptance already says to update). The current acceptance text says "delete `_maybe_fence`" which is right, but the cited line points the coder at the call site only; a coder following the cite literally would leave the def around as dead code. **Fix**: cite both `:212` (call site, delete the call) and `:807-826` (def, delete the method) explicitly in the description so the coder doesn't have to grep. - -### Non-blocking - -- **TASK-5-5 fork primitive contradicts cq-3.** Description: "fork spawns a child Claude Code subagent (via the Agent tool with `subagent_type='general-purpose'`)". cq-3 resolved to "decide empirically at implement-phase" — i.e. *stay on harness re-host* for the rollout, decide later. Shipping a fork primitive that uses Agent-tool dispatch is a structural commitment to Agent-tool capability, which contradicts the operator's deferral. Either (a) acknowledge in TASK-5-5's description that this is a partial Agent-tool migration the operator should re-confirm before slice 5 ships, or (b) implement fork via `subprocess.Popen` of a fresh `egg_harness.run_agent` so it stays on the harness re-host model. Risk_analyst's R29 mitigation also expects fork to land structurally — flag the cq-3 tension in the slice-5 ADR refresh (TASK-5-9). - -- **TASK-2-5 enforcement target may be the wrong file.** Description says enforcement lands in `orchestrator/mcp_tools.py`. The MCP file-restriction tool today is `mcp__sdlc__check_file_restriction`, whose handler implementation lives at `sandbox/egg_agent_tools/handlers/restrictions.py:6-70` (`check_file_restriction(req)` — verified by `grep -rn "check_file_restriction" --include="*.py" orchestrator/ sandbox/`). `orchestrator/mcp_tools.py` exists but its tool surface is different (it's the orchestrator-side MCP server, registering `mcp__progress__*`, `mcp__sdlc__*`, etc. tools — not the in-sandbox agent's tool boundary). The validator-side fallback (cq-6 option 2) is about agent-side tool calls denying cross-role writes; that surface is `sandbox/egg_agent_tools/handlers/restrictions.py`, not `orchestrator/mcp_tools.py`. **Fix**: confirm which surface you want and update `files:` accordingly; if both surfaces need wiring, split into two tasks. - -- **TASK-3-9 metrics path conflicts with the architect's path.** Architect picked `.egg-state/conformance-metrics/-.json`; you picked `.egg-state//substrate-metrics.json`. TASK-5-1's cost-cap and TASK-4-5's phase-ceiling hook both read from your path; the architect's path goes unread. Align — pick one canonical path and use it in all four references (TASK-3-9, TASK-4-5, TASK-5-1, ADR in TASK-5-9). - -- **TASK-3-2 daemon lifecycle is underspecified.** Acceptance says "Daemon starts on first `python3 bin/run_pipeline.py --daemon` invocation; binds to `.egg-state//sock`". But each `python3 bin/run_pipeline.py --daemon` call is a child process of the skill's `Bash(python3 ...)` step. If the daemon doesn't detach (via `subprocess.Popen(start_new_session=True)` or equivalent), the daemon dies when the calling Bash exits — defeating the "long-lived" property. The plan should state explicitly how the daemon detaches and survives across multiple skill-Bash invocations. Cite `os.setsid` / `start_new_session=True` or whatever mechanism you choose. - -- **TASK-1-7 / TASK-1-6 ordering**: TASK-1-6 modifies `_load_egg_sdlc_role_rubric` to load `reviewer_refine` and `reviewer_agent_design`, but TASK-1-4 (which produces those .md files) is listed before it. If a coder picks up TASK-1-6 before TASK-1-4 is merged, the loader returns text from a missing file. Worth making the dependency explicit in TASK-1-6's description ("depends on TASK-1-4 having landed the rubric files first"). - -- **Slice-2 task numbering**: TASK-2-6 ("Update SKILL.md") appears AFTER TASK-2-7 ("Add tests for MCP-validator policy") in the file. Cosmetic — renumber so the numeric order matches the file order. - -- **`LocalWorktreeManager` primitive citation**: the table says "line ~30"; the actual class def is at `claude_code/worktree.py:59`. Update. - -- **Conformance issue states**: feedback Q1 confirmed the 5-issue set is fixed regardless of current state. #2714 has since been *closed* (merged as #2719). The plan doesn't note this, but it's also not a blocker — the conformance matrix is about replaying the issue's SDLC shape, not its open/closed state. Worth a one-line acknowledgement in TASK-4-4 so future readers don't get confused. - -- **Slice-sizing advisories (per #2137 — advisory only, never NACK per HITL decision-6 opt-2)**: - - Slice 3: 10 tasks, ~17 file paths affected (8 .md rubrics + 3 .py code + 4 test files + 1 SKILL.md + 1 in_process.py edit). Heuristic LOC ≈ 1,500. **Consider splitting** — the daemon bridge (TASK-3-2/TASK-3-3/TASK-3-8) could plausibly be its own sub-slice that lands before the implement phase wiring, especially given R17's "two bridge architectures" concern. - - Slice 4: 7 tasks, ~22 file paths affected (TASK-4-3 alone applies the substrate fixture to 14 existing regression tests). Heuristic LOC ≈ 2,200. **Strongly consider splitting** — fence removal + PR-phase wiring is one concern; substrate-fixture sprawl across 14 existing tests is another; conformance matrix is a third. The phase-ceiling hook (TASK-4-5) could co-locate with the matrix. - - Slice 5: 10 tasks, ~25 file paths affected (TASK-5-7 alone touches 15 rubric .md files if R2 fails). Heuristic LOC ≈ 2,500. **Strongly consider splitting** — the contingent R15 migration (TASK-5-7) is conditional and large; if R2 fails, slice-5 doubles in size. Worth pre-splitting R15 migration into its own sub-slice (or a sibling slice that's a no-op on R2 pass). - None of these are NACK reasons — the operator/refiner retains size-override authority — but the implementer should know the soft-target story. - -- **R17 dual-bridge concern from risk_analyst**: their mitigation says "Option (a)'s daemon design influences Option (b)'s state-serialization contract". Your plan ships Option (b) in slice-1 (TASK-1-1) and Option (a) in slice-3 (TASK-3-2). If slice-1 commits to a state-serialization contract that slice-3 then has to renegotiate, the bridges diverge. Not a NACK reason — the operator's cq-1 Option C explicitly chose this hybrid sequencing — but the slice-1 acceptance for TASK-1-1 should require the `.egg-state/contracts/.json#pending_hitl` envelope shape to be a shared contract between (b) and (a), not a slice-1-only invention. - -Re-propose with the three blocking issues addressed and the metrics-path/cq-3-fork alignments. The slice DAG itself is fine — strictly linear chain, no forest violation; #2137 trivially satisfied. - - -````yaml -id: e8f370af-a993-4b -phase: plan -metadata: - payload: - reason: "\nReviewed `.egg-state/drafts/2717-plan.md` (1394 lines, 5 slices, 42\ - \ tasks) and cross-checked against the architect's design, the refine analysis,\ - \ and the actual codebase. The slice shape is right (matches cq-2 Option 3),\ - \ the role\u2194files alignment is clean (I ran the per-file/per-role check\ - \ across all 42 tasks; every task's `files:` is writable by its `role:` per\ - \ `shared/egg_restrictions/patterns.py` \u2014 no orchestrator-side `403 restricted_path_modified`\ - \ predicted), and the primitives table is unusually thorough. But three blocking\ - \ issues will produce false signals or unimplementable tasks if shipped as-is.\n\ - \n### Blocking\n\n1. **TASK-1-5 R2 spike mechanism is structurally broken \u2014\ - \ the test will not exercise the PreToolUse hook at all.** The task says \"\ - Test dispatches a parent subagent (role=architect) and a child subagent (role=tester)\ - \ via the harness; asserts the PreToolUse hook denies a write to `orchestrator/foo.py`\ - \ when invoked from the child\". I verified that `shared/egg_harness/client.py:60-150`\ - \ registers its own `ToolRegistry` with Python-native Bash/Read/Write/Edit/Glob/Grep\ - \ handlers and routes permission checks through `registry.set_permission_callback(callback)`\ - \ \u2014 `grep -rn \"PreToolUseHookPolicy\\|hook_entry\" shared/egg_harness/`\ - \ returns **zero hits**. The PreToolUse hook is invoked exclusively by Claude\ - \ Code's native tool registry (see `orchestrator/substrate/claude_code/hook_entry.py:17`\ - \ `\"command\": \"python3 -m orchestrator.substrate.claude_code.hook_entry\"\ - ` \u2014 that's Claude Code's PreToolUse settings.json wiring). Therefore: a\ - \ harness-dispatched child never triggers `hook_entry.decide()`, and the assertion\ - \ \"the PreToolUse hook denies a write\" cannot fail (the hook isn't in the\ - \ loop) and cannot pass (the hook isn't in the loop) \u2014 the test will either\ - \ green on a no-op or assert against a manually-invoked `hook_entry.decide(...)`\ - \ that has nothing to do with \"nested dispatch\". This is the load-bearing\ - \ test for the rollout: cq-4 (R15 migration timing) and cq-6 fallback (MCP-validator\ - \ enforcement, TASK-2-5 and TASK-5-7) both branch on its verdict. **Fix**: pick\ - \ one of (a) call `hook_entry.decide()` directly with controlled `EGG_AGENT_ROLE`\ - \ env vars and rename the task to make clear it's a hook-logic unit test, not\ - \ a nested-dispatch test (cheap, doesn't answer the architect's question about\ - \ Agent-tool nested dispatch, but is honest about scope); (b) ship a minimal\ - \ Claude Code Agent-tool dispatch path *just for the spike* (precursor of ClaudeCodeAgentToolSpawner,\ - \ distinct from the production `ClaudeCodeSpawner` harness re-host) and run\ - \ the parent/child dispatch through it \u2014 that does exercise the hook but\ - \ adds a slice-1 deliverable the plan currently doesn't enumerate. The architect's\ - \ text picked option (b) implicitly; you picked option (a)'s file location but\ - \ kept option (b)'s framing. Pick one and update the task description, acceptance\ - \ criteria, and dependent contingency tasks (TASK-2-5, TASK-5-7) to match.\n\ - \n2. **TASK-4-4 references \"recorded transcripts\" that no task records.**\ - \ The acceptance text says \"via harness fakes that replay a recorded transcript\ - \ of that issue's refine + plan + implement + pr phases\". I searched the codebase:\ - \ there is no existing transcript-replay infrastructure (the existing `integration_tests/regression/test_substrate_smoke.py:56`\ - \ uses `MagicMock` with hardcoded `stdout=\"ok\"`, not a recorded-transcript\ - \ replay). No task in slices 1\u20134 records transcripts for the five curated\ - \ issues; no task in slice 5 either. As specified, TASK-4-4 cannot be implemented\ - \ \u2014 its `files:` says `test_conformance_matrix.py` but the test body needs\ - \ 4 phases \xD7 5 issues = 20 transcripts that don't exist and aren't produced\ - \ anywhere in the plan. **Fix**: either (a) add a sub-task that records transcripts\ - \ during slice-3's first real implement-phase run on each curated issue (the\ - \ natural place \u2014 slice-3 already produces `.egg-state//substrate-metrics.json`\ - \ per TASK-3-9, so transcript capture co-locates well); (b) change the mechanism\ - \ to per-subagent `MagicMock` matching `test_substrate_smoke.py`'s pattern (cheaper,\ - \ but you lose phase-level realism); or (c) acknowledge the matrix runs subagents\ - \ live in CI and adjust the 3-hour-per-phase ceiling story accordingly. Whichever\ - \ path, the plan needs an explicit task that produces the inputs TASK-4-4 consumes.\n\ - \n3. **TASK-4-2 fence-removal task cites the wrong line.** The task description\ - \ says \"Remove the walking-skeleton scope fence at `orchestrator/substrate/in_process.py:212`\"\ - . I checked: `:212` is the *call site* (`self._maybe_fence(answer)` inside `_run`).\ - \ The `_maybe_fence` static-method *definition* is at `:807-826`. Both need\ - \ to be removed to actually delete the fence \u2014 and the docstring at `:92-125`\ - \ that promises `NotImplementedError` for plan/implement/pr (which the acceptance\ - \ already says to update). The current acceptance text says \"delete `_maybe_fence`\"\ - \ which is right, but the cited line points the coder at the call site only;\ - \ a coder following the cite literally would leave the def around as dead code.\ - \ **Fix**: cite both `:212` (call site, delete the call) and `:807-826` (def,\ - \ delete the method) explicitly in the description so the coder doesn't have\ - \ to grep.\n\n### Non-blocking\n\n- **TASK-5-5 fork primitive contradicts cq-3.**\ - \ Description: \"fork spawns a child Claude Code subagent (via the Agent tool\ - \ with `subagent_type='general-purpose'`)\". cq-3 resolved to \"decide empirically\ - \ at implement-phase\" \u2014 i.e. *stay on harness re-host* for the rollout,\ - \ decide later. Shipping a fork primitive that uses Agent-tool dispatch is a\ - \ structural commitment to Agent-tool capability, which contradicts the operator's\ - \ deferral. Either (a) acknowledge in TASK-5-5's description that this is a\ - \ partial Agent-tool migration the operator should re-confirm before slice 5\ - \ ships, or (b) implement fork via `subprocess.Popen` of a fresh `egg_harness.run_agent`\ - \ so it stays on the harness re-host model. Risk_analyst's R29 mitigation also\ - \ expects fork to land structurally \u2014 flag the cq-3 tension in the slice-5\ - \ ADR refresh (TASK-5-9).\n\n- **TASK-2-5 enforcement target may be the wrong\ - \ file.** Description says enforcement lands in `orchestrator/mcp_tools.py`.\ - \ The MCP file-restriction tool today is `mcp__sdlc__check_file_restriction`,\ - \ whose handler implementation lives at `sandbox/egg_agent_tools/handlers/restrictions.py:6-70`\ - \ (`check_file_restriction(req)` \u2014 verified by `grep -rn \"check_file_restriction\"\ - \ --include=\"*.py\" orchestrator/ sandbox/`). `orchestrator/mcp_tools.py` exists\ - \ but its tool surface is different (it's the orchestrator-side MCP server,\ - \ registering `mcp__progress__*`, `mcp__sdlc__*`, etc. tools \u2014 not the\ - \ in-sandbox agent's tool boundary). The validator-side fallback (cq-6 option\ - \ 2) is about agent-side tool calls denying cross-role writes; that surface\ - \ is `sandbox/egg_agent_tools/handlers/restrictions.py`, not `orchestrator/mcp_tools.py`.\ - \ **Fix**: confirm which surface you want and update `files:` accordingly; if\ - \ both surfaces need wiring, split into two tasks.\n\n- **TASK-3-9 metrics path\ - \ conflicts with the architect's path.** Architect picked `.egg-state/conformance-metrics/-.json`;\ - \ you picked `.egg-state//substrate-metrics.json`. TASK-5-1's cost-cap\ - \ and TASK-4-5's phase-ceiling hook both read from your path; the architect's\ - \ path goes unread. Align \u2014 pick one canonical path and use it in all four\ - \ references (TASK-3-9, TASK-4-5, TASK-5-1, ADR in TASK-5-9).\n\n- **TASK-3-2\ - \ daemon lifecycle is underspecified.** Acceptance says \"Daemon starts on first\ - \ `python3 bin/run_pipeline.py --daemon` invocation; binds to `.egg-state//sock`\"\ - . But each `python3 bin/run_pipeline.py --daemon` call is a child process of\ - \ the skill's `Bash(python3 ...)` step. If the daemon doesn't detach (via `subprocess.Popen(start_new_session=True)`\ - \ or equivalent), the daemon dies when the calling Bash exits \u2014 defeating\ - \ the \"long-lived\" property. The plan should state explicitly how the daemon\ - \ detaches and survives across multiple skill-Bash invocations. Cite `os.setsid`\ - \ / `start_new_session=True` or whatever mechanism you choose.\n\n- **TASK-1-7\ - \ / TASK-1-6 ordering**: TASK-1-6 modifies `_load_egg_sdlc_role_rubric` to load\ - \ `reviewer_refine` and `reviewer_agent_design`, but TASK-1-4 (which produces\ - \ those .md files) is listed before it. If a coder picks up TASK-1-6 before\ - \ TASK-1-4 is merged, the loader returns text from a missing file. Worth making\ - \ the dependency explicit in TASK-1-6's description (\"depends on TASK-1-4 having\ - \ landed the rubric files first\").\n\n- **Slice-2 task numbering**: TASK-2-6\ - \ (\"Update SKILL.md\") appears AFTER TASK-2-7 (\"Add tests for MCP-validator\ - \ policy\") in the file. Cosmetic \u2014 renumber so the numeric order matches\ - \ the file order.\n\n- **`LocalWorktreeManager` primitive citation**: the table\ - \ says \"line ~30\"; the actual class def is at `claude_code/worktree.py:59`.\ - \ Update.\n\n- **Conformance issue states**: feedback Q1 confirmed the 5-issue\ - \ set is fixed regardless of current state. #2714 has since been *closed* (merged\ - \ as #2719). The plan doesn't note this, but it's also not a blocker \u2014\ - \ the conformance matrix is about replaying the issue's SDLC shape, not its\ - \ open/closed state. Worth a one-line acknowledgement in TASK-4-4 so future\ - \ readers don't get confused.\n\n- **Slice-sizing advisories (per #2137 \u2014\ - \ advisory only, never NACK per HITL decision-6 opt-2)**:\n - Slice 3: 10 tasks,\ - \ ~17 file paths affected (8 .md rubrics + 3 .py code + 4 test files + 1 SKILL.md\ - \ + 1 in_process.py edit). Heuristic LOC \u2248 1,500. **Consider splitting**\ - \ \u2014 the daemon bridge (TASK-3-2/TASK-3-3/TASK-3-8) could plausibly be its\ - \ own sub-slice that lands before the implement phase wiring, especially given\ - \ R17's \"two bridge architectures\" concern.\n - Slice 4: 7 tasks, ~22 file\ - \ paths affected (TASK-4-3 alone applies the substrate fixture to 14 existing\ - \ regression tests). Heuristic LOC \u2248 2,200. **Strongly consider splitting**\ - \ \u2014 fence removal + PR-phase wiring is one concern; substrate-fixture sprawl\ - \ across 14 existing tests is another; conformance matrix is a third. The phase-ceiling\ - \ hook (TASK-4-5) could co-locate with the matrix.\n - Slice 5: 10 tasks, ~25\ - \ file paths affected (TASK-5-7 alone touches 15 rubric .md files if R2 fails).\ - \ Heuristic LOC \u2248 2,500. **Strongly consider splitting** \u2014 the contingent\ - \ R15 migration (TASK-5-7) is conditional and large; if R2 fails, slice-5 doubles\ - \ in size. Worth pre-splitting R15 migration into its own sub-slice (or a sibling\ - \ slice that's a no-op on R2 pass).\n None of these are NACK reasons \u2014\ - \ the operator/refiner retains size-override authority \u2014 but the implementer\ - \ should know the soft-target story.\n\n- **R17 dual-bridge concern from risk_analyst**:\ - \ their mitigation says \"Option (a)'s daemon design influences Option (b)'s\ - \ state-serialization contract\". Your plan ships Option (b) in slice-1 (TASK-1-1)\ - \ and Option (a) in slice-3 (TASK-3-2). If slice-1 commits to a state-serialization\ - \ contract that slice-3 then has to renegotiate, the bridges diverge. Not a\ - \ NACK reason \u2014 the operator's cq-1 Option C explicitly chose this hybrid\ - \ sequencing \u2014 but the slice-1 acceptance for TASK-1-1 should require the\ - \ `.egg-state/contracts/.json#pending_hitl` envelope shape to be a shared\ - \ contract between (b) and (a), not a slice-1-only invention.\n\nRe-propose\ - \ with the three blocking issues addressed and the metrics-path/cq-3-fork alignments.\ - \ The slice DAG itself is fine \u2014 strictly linear chain, no forest violation;\ - \ #2137 trivially satisfied.\n" - artifact_references: - - .egg-state/drafts/2717-plan.md - - .egg-state/drafts/2717-analysis.md - - .egg-state/agent-outputs/2717-architect-output.json - nack_version: 1 - reason: "\nReviewed `.egg-state/drafts/2717-plan.md` (1394 lines, 5 slices, 42 tasks)\ - \ and cross-checked against the architect's design, the refine analysis, and the\ - \ actual codebase. The slice shape is right (matches cq-2 Option 3), the role\u2194\ - files alignment is clean (I ran the per-file/per-role check across all 42 tasks;\ - \ every task's `files:` is writable by its `role:` per `shared/egg_restrictions/patterns.py`\ - \ \u2014 no orchestrator-side `403 restricted_path_modified` predicted), and the\ - \ primitives table is unusually thorough. But three blocking issues will produce\ - \ false signals or unimplementable tasks if shipped as-is.\n\n### Blocking\n\n\ - 1. **TASK-1-5 R2 spike mechanism is structurally broken \u2014 the test will not\ - \ exercise the PreToolUse hook at all.** The task says \"Test dispatches a parent\ - \ subagent (role=architect) and a child subagent (role=tester) via the harness;\ - \ asserts the PreToolUse hook denies a write to `orchestrator/foo.py` when invoked\ - \ from the child\". I verified that `shared/egg_harness/client.py:60-150` registers\ - \ its own `ToolRegistry` with Python-native Bash/Read/Write/Edit/Glob/Grep handlers\ - \ and routes permission checks through `registry.set_permission_callback(callback)`\ - \ \u2014 `grep -rn \"PreToolUseHookPolicy\\|hook_entry\" shared/egg_harness/`\ - \ returns **zero hits**. The PreToolUse hook is invoked exclusively by Claude\ - \ Code's native tool registry (see `orchestrator/substrate/claude_code/hook_entry.py:17`\ - \ `\"command\": \"python3 -m orchestrator.substrate.claude_code.hook_entry\"`\ - \ \u2014 that's Claude Code's PreToolUse settings.json wiring). Therefore: a harness-dispatched\ - \ child never triggers `hook_entry.decide()`, and the assertion \"the PreToolUse\ - \ hook denies a write\" cannot fail (the hook isn't in the loop) and cannot pass\ - \ (the hook isn't in the loop) \u2014 the test will either green on a no-op or\ - \ assert against a manually-invoked `hook_entry.decide(...)` that has nothing\ - \ to do with \"nested dispatch\". This is the load-bearing test for the rollout:\ - \ cq-4 (R15 migration timing) and cq-6 fallback (MCP-validator enforcement, TASK-2-5\ - \ and TASK-5-7) both branch on its verdict. **Fix**: pick one of (a) call `hook_entry.decide()`\ - \ directly with controlled `EGG_AGENT_ROLE` env vars and rename the task to make\ - \ clear it's a hook-logic unit test, not a nested-dispatch test (cheap, doesn't\ - \ answer the architect's question about Agent-tool nested dispatch, but is honest\ - \ about scope); (b) ship a minimal Claude Code Agent-tool dispatch path *just\ - \ for the spike* (precursor of ClaudeCodeAgentToolSpawner, distinct from the production\ - \ `ClaudeCodeSpawner` harness re-host) and run the parent/child dispatch through\ - \ it \u2014 that does exercise the hook but adds a slice-1 deliverable the plan\ - \ currently doesn't enumerate. The architect's text picked option (b) implicitly;\ - \ you picked option (a)'s file location but kept option (b)'s framing. Pick one\ - \ and update the task description, acceptance criteria, and dependent contingency\ - \ tasks (TASK-2-5, TASK-5-7) to match.\n\n2. **TASK-4-4 references \"recorded\ - \ transcripts\" that no task records.** The acceptance text says \"via harness\ - \ fakes that replay a recorded transcript of that issue's refine + plan + implement\ - \ + pr phases\". I searched the codebase: there is no existing transcript-replay\ - \ infrastructure (the existing `integration_tests/regression/test_substrate_smoke.py:56`\ - \ uses `MagicMock` with hardcoded `stdout=\"ok\"`, not a recorded-transcript replay).\ - \ No task in slices 1\u20134 records transcripts for the five curated issues;\ - \ no task in slice 5 either. As specified, TASK-4-4 cannot be implemented \u2014\ - \ its `files:` says `test_conformance_matrix.py` but the test body needs 4 phases\ - \ \xD7 5 issues = 20 transcripts that don't exist and aren't produced anywhere\ - \ in the plan. **Fix**: either (a) add a sub-task that records transcripts during\ - \ slice-3's first real implement-phase run on each curated issue (the natural\ - \ place \u2014 slice-3 already produces `.egg-state//substrate-metrics.json`\ - \ per TASK-3-9, so transcript capture co-locates well); (b) change the mechanism\ - \ to per-subagent `MagicMock` matching `test_substrate_smoke.py`'s pattern (cheaper,\ - \ but you lose phase-level realism); or (c) acknowledge the matrix runs subagents\ - \ live in CI and adjust the 3-hour-per-phase ceiling story accordingly. Whichever\ - \ path, the plan needs an explicit task that produces the inputs TASK-4-4 consumes.\n\ - \n3. **TASK-4-2 fence-removal task cites the wrong line.** The task description\ - \ says \"Remove the walking-skeleton scope fence at `orchestrator/substrate/in_process.py:212`\"\ - . I checked: `:212` is the *call site* (`self._maybe_fence(answer)` inside `_run`).\ - \ The `_maybe_fence` static-method *definition* is at `:807-826`. Both need to\ - \ be removed to actually delete the fence \u2014 and the docstring at `:92-125`\ - \ that promises `NotImplementedError` for plan/implement/pr (which the acceptance\ - \ already says to update). The current acceptance text says \"delete `_maybe_fence`\"\ - \ which is right, but the cited line points the coder at the call site only; a\ - \ coder following the cite literally would leave the def around as dead code.\ - \ **Fix**: cite both `:212` (call site, delete the call) and `:807-826` (def,\ - \ delete the method) explicitly in the description so the coder doesn't have to\ - \ grep.\n\n### Non-blocking\n\n- **TASK-5-5 fork primitive contradicts cq-3.**\ - \ Description: \"fork spawns a child Claude Code subagent (via the Agent tool\ - \ with `subagent_type='general-purpose'`)\". cq-3 resolved to \"decide empirically\ - \ at implement-phase\" \u2014 i.e. *stay on harness re-host* for the rollout,\ - \ decide later. Shipping a fork primitive that uses Agent-tool dispatch is a structural\ - \ commitment to Agent-tool capability, which contradicts the operator's deferral.\ - \ Either (a) acknowledge in TASK-5-5's description that this is a partial Agent-tool\ - \ migration the operator should re-confirm before slice 5 ships, or (b) implement\ - \ fork via `subprocess.Popen` of a fresh `egg_harness.run_agent` so it stays on\ - \ the harness re-host model. Risk_analyst's R29 mitigation also expects fork to\ - \ land structurally \u2014 flag the cq-3 tension in the slice-5 ADR refresh (TASK-5-9).\n\ - \n- **TASK-2-5 enforcement target may be the wrong file.** Description says enforcement\ - \ lands in `orchestrator/mcp_tools.py`. The MCP file-restriction tool today is\ - \ `mcp__sdlc__check_file_restriction`, whose handler implementation lives at `sandbox/egg_agent_tools/handlers/restrictions.py:6-70`\ - \ (`check_file_restriction(req)` \u2014 verified by `grep -rn \"check_file_restriction\"\ - \ --include=\"*.py\" orchestrator/ sandbox/`). `orchestrator/mcp_tools.py` exists\ - \ but its tool surface is different (it's the orchestrator-side MCP server, registering\ - \ `mcp__progress__*`, `mcp__sdlc__*`, etc. tools \u2014 not the in-sandbox agent's\ - \ tool boundary). The validator-side fallback (cq-6 option 2) is about agent-side\ - \ tool calls denying cross-role writes; that surface is `sandbox/egg_agent_tools/handlers/restrictions.py`,\ - \ not `orchestrator/mcp_tools.py`. **Fix**: confirm which surface you want and\ - \ update `files:` accordingly; if both surfaces need wiring, split into two tasks.\n\ - \n- **TASK-3-9 metrics path conflicts with the architect's path.** Architect picked\ - \ `.egg-state/conformance-metrics/-.json`; you picked `.egg-state//substrate-metrics.json`.\ - \ TASK-5-1's cost-cap and TASK-4-5's phase-ceiling hook both read from your path;\ - \ the architect's path goes unread. Align \u2014 pick one canonical path and use\ - \ it in all four references (TASK-3-9, TASK-4-5, TASK-5-1, ADR in TASK-5-9).\n\ - \n- **TASK-3-2 daemon lifecycle is underspecified.** Acceptance says \"Daemon\ - \ starts on first `python3 bin/run_pipeline.py --daemon` invocation; binds to\ - \ `.egg-state//sock`\". But each `python3 bin/run_pipeline.py --daemon`\ - \ call is a child process of the skill's `Bash(python3 ...)` step. If the daemon\ - \ doesn't detach (via `subprocess.Popen(start_new_session=True)` or equivalent),\ - \ the daemon dies when the calling Bash exits \u2014 defeating the \"long-lived\"\ - \ property. The plan should state explicitly how the daemon detaches and survives\ - \ across multiple skill-Bash invocations. Cite `os.setsid` / `start_new_session=True`\ - \ or whatever mechanism you choose.\n\n- **TASK-1-7 / TASK-1-6 ordering**: TASK-1-6\ - \ modifies `_load_egg_sdlc_role_rubric` to load `reviewer_refine` and `reviewer_agent_design`,\ - \ but TASK-1-4 (which produces those .md files) is listed before it. If a coder\ - \ picks up TASK-1-6 before TASK-1-4 is merged, the loader returns text from a\ - \ missing file. Worth making the dependency explicit in TASK-1-6's description\ - \ (\"depends on TASK-1-4 having landed the rubric files first\").\n\n- **Slice-2\ - \ task numbering**: TASK-2-6 (\"Update SKILL.md\") appears AFTER TASK-2-7 (\"\ - Add tests for MCP-validator policy\") in the file. Cosmetic \u2014 renumber so\ - \ the numeric order matches the file order.\n\n- **`LocalWorktreeManager` primitive\ - \ citation**: the table says \"line ~30\"; the actual class def is at `claude_code/worktree.py:59`.\ - \ Update.\n\n- **Conformance issue states**: feedback Q1 confirmed the 5-issue\ - \ set is fixed regardless of current state. #2714 has since been *closed* (merged\ - \ as #2719). The plan doesn't note this, but it's also not a blocker \u2014 the\ - \ conformance matrix is about replaying the issue's SDLC shape, not its open/closed\ - \ state. Worth a one-line acknowledgement in TASK-4-4 so future readers don't\ - \ get confused.\n\n- **Slice-sizing advisories (per #2137 \u2014 advisory only,\ - \ never NACK per HITL decision-6 opt-2)**:\n - Slice 3: 10 tasks, ~17 file paths\ - \ affected (8 .md rubrics + 3 .py code + 4 test files + 1 SKILL.md + 1 in_process.py\ - \ edit). Heuristic LOC \u2248 1,500. **Consider splitting** \u2014 the daemon\ - \ bridge (TASK-3-2/TASK-3-3/TASK-3-8) could plausibly be its own sub-slice that\ - \ lands before the implement phase wiring, especially given R17's \"two bridge\ - \ architectures\" concern.\n - Slice 4: 7 tasks, ~22 file paths affected (TASK-4-3\ - \ alone applies the substrate fixture to 14 existing regression tests). Heuristic\ - \ LOC \u2248 2,200. **Strongly consider splitting** \u2014 fence removal + PR-phase\ - \ wiring is one concern; substrate-fixture sprawl across 14 existing tests is\ - \ another; conformance matrix is a third. The phase-ceiling hook (TASK-4-5) could\ - \ co-locate with the matrix.\n - Slice 5: 10 tasks, ~25 file paths affected (TASK-5-7\ - \ alone touches 15 rubric .md files if R2 fails). Heuristic LOC \u2248 2,500.\ - \ **Strongly consider splitting** \u2014 the contingent R15 migration (TASK-5-7)\ - \ is conditional and large; if R2 fails, slice-5 doubles in size. Worth pre-splitting\ - \ R15 migration into its own sub-slice (or a sibling slice that's a no-op on R2\ - \ pass).\n None of these are NACK reasons \u2014 the operator/refiner retains\ - \ size-override authority \u2014 but the implementer should know the soft-target\ - \ story.\n\n- **R17 dual-bridge concern from risk_analyst**: their mitigation\ - \ says \"Option (a)'s daemon design influences Option (b)'s state-serialization\ - \ contract\". Your plan ships Option (b) in slice-1 (TASK-1-1) and Option (a)\ - \ in slice-3 (TASK-3-2). If slice-1 commits to a state-serialization contract\ - \ that slice-3 then has to renegotiate, the bridges diverge. Not a NACK reason\ - \ \u2014 the operator's cq-1 Option C explicitly chose this hybrid sequencing\ - \ \u2014 but the slice-1 acceptance for TASK-1-1 should require the `.egg-state/contracts/.json#pending_hitl`\ - \ envelope shape to be a shared contract between (b) and (a), not a slice-1-only\ - \ invention.\n\nRe-propose with the three blocking issues addressed and the metrics-path/cq-3-fork\ - \ alignments. The slice DAG itself is fine \u2014 strictly linear chain, no forest\ - \ violation; #2137 trivially satisfied.\n" - revision_count: 1 -```` - -### [2026-05-19T18:31:22Z] task_planner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 78bb475a-98b2-49 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:31:22Z] reviewer_plan → risk_analyst (CONSENSUS_ACK): ACK from reviewer_plan for risk_analyst - - -Reviewed `.egg-state/agent-outputs/2717-risk_analyst-output.json` — 18 risks (5 HIGH, ~10 MEDIUM, 3 LOW), structured assessment across performance / security / compatibility / DoD-alignment, with concrete mitigations per risk. - -### What's solid -- **HIGH-severity risks are correctly identified and architecturally substantive**: - - **R17 (dual-bridge architecture)** — I cross-checked: cq-1 Option C does ship two structurally different bridges (stage-script vs daemon), and the task_planner has TASK-1-1 (Option b) in slice-1 and TASK-3-2 (Option a) in slice-3 — the divergence risk you flag is real, and the recommendation to make the `.egg-state/contracts/.json#pending_hitl` envelope a shared contract between the two bridges is the right structural mitigation. I'm carrying this through to the task_planner NACK as a non-blocking item. - - **R18 (15 rubric authoring + #2622 depth gap)** — verified `orchestrator/substrate/__init__.py:280-284` still raises the "Walking-skeleton spike (#2623) only ships the refiner rubric; other roles are deferred to the follow-up issue per cq-11" `ValueError`. Your point that `build_system_prompt(sources)` depth-threading needs per-role structural verification is exactly right — the spike validated this for *one* role only. The mitigation to add a structural test (assert every role enumerated in `_PHASE_ROLES` has a rubric AND that `build_system_prompt` includes its content) is the right shape; the task_planner's TASK-2-3/3-4/3-5 don't currently include such a structural test, so this risk stays open even after their NACK is addressed. - - **R19 (8-subagent harness re-host concurrency strain)** — I checked `shared/egg_harness/client.py:60-150`: each `run_agent` invocation builds its own `ToolRegistry`, `EventBus`, `AnthropicProvider`, and `AgentLoop` in the parent's Python process. Stacking 8 of these is exactly the load you describe. The mitigation to require explicit measurement during plan-phase (4 loops) before approving implement-phase (8 loops) is the right gate; reviewer_plan should hold the implement-phase slice on the plan-phase numbers. - - **R27 (MCP-validator structural enforcement gap)** — I checked `sandbox/egg_agent_tools/handlers/restrictions.py:6-15`: "The agent should call `check_file_restriction` *before* burning a turn" — this is voluntary, not enforced. Your point that MCP-validator fallback loses structural enforcement is exactly right. The "PreToolUse hook STILL fires for Bash and global-deny rules + post-tool-call validator" defense-in-depth recommendation is concrete and actionable. The task_planner's TASK-2-5 targets `orchestrator/mcp_tools.py` which I'm separately flagging as wrong-surface — your risk gives the right re-direction. - - **R29 (subagent context budget under harness re-host)** — verified each harness loop's `AgentLoop.run(...)` accumulates conversation history in-process; 8 concurrent loops × ~50K tokens is a real budget concern. The mitigation to move fork-based delegation up if context pressure manifests during plan-phase is the right contingency — the task_planner currently defers fork to slice-5 (TASK-5-5) which is the wrong end of the timeline if R29 manifests early. - -- **Trust-boundary risks are correctly framed**: R31 (15-subagent credential exposure) and the security_assessment's "explicit_non_goals_inherited_from_2623" section correctly inherit R1's qualitative acceptance without re-litigating, while flagging that the 15x multiplier wasn't quantified in cq-1. - -- **DoD criterion alignment table** maps each of the 6 DoD bullets to specific risks/mitigations — DoD #5 (`EggHarnessSpawner` + local-run) lines up with R26; DoD #6 (ADR refresh) lines up with R32 (interface stability marker timing). No DoD bullet is silently dropped. - -- **`open_questions_for_implement_phase` list (5 items)** is useful forward-handoff — context-budget gate, fork-trigger, MCP-validator structural strength, cost-cap soft-warning threshold, runaway-NACK detection — these all surface decisions implement-phase will need to make and that should land in the task_planner's slice-3 acceptance criteria. - -### Non-blocking -- **R20 (no substrate-specific reviewer) MEDIUM rating** — feedback Q5 declined the expansion. Worth noting in the risk text that this is operator-accepted rather than "MEDIUM unresolved"; the residual risk is unchanged but the framing matters for the merge story. -- **R21 (#2714 closed since refine)** — confirmed via `git log --oneline | grep 2714` that #2719 (the close fix) is already merged. Worth either acknowledging the close or escalating to a feedback question if this changes the conformance-matrix shape (e.g., do we replay the issue's pre-fix state or post-fix state?). -- **R33 (apply phase unreferenced in DoD)** LOW — agreed; the issue body's DoD doesn't list epic-only apply-phase coverage, so this isn't a NACK reason. The task_planner's TASK-5-7 lists `applier.md` in the conditional R15 migration but no slice ships an unconditional applier rubric. If the operator expects epic-pipeline parity, that should be a feedback question; if not, R33 stays at LOW. -- **R23 cost cap "200 leaves room for $1000+ runaway"** — the arithmetic is sound, but cq-6 already resolved this; suggest reframing as "operator accepted; consider soft-warning at 100 invocations" rather than "MEDIUM unresolved". The task_planner's TASK-5-1 implements the cap mechanically; a soft warning would be a non-blocking addition. - -Overall: thorough, accurate, and the HIGH-severity risks are the right ones to surface to the operator. Approving so the planner can incorporate the structural mitigations (R17 envelope contract, R18 depth-gap structural test, R19 plan-phase measurement gate, R27 defense-in-depth, R29 fork-trigger contingency) into the re-propose. - - -````yaml -id: 9efdfbc6-6b72-49 -phase: plan -metadata: - payload: - artifact_references: - - .egg-state/agent-outputs/2717-risk_analyst-output.json - - .egg-state/drafts/2717-analysis.md - reason: "\nReviewed `.egg-state/agent-outputs/2717-risk_analyst-output.json` \u2014\ - \ 18 risks (5 HIGH, ~10 MEDIUM, 3 LOW), structured assessment across performance\ - \ / security / compatibility / DoD-alignment, with concrete mitigations per\ - \ risk.\n\n### What's solid\n- **HIGH-severity risks are correctly identified\ - \ and architecturally substantive**:\n - **R17 (dual-bridge architecture)**\ - \ \u2014 I cross-checked: cq-1 Option C does ship two structurally different\ - \ bridges (stage-script vs daemon), and the task_planner has TASK-1-1 (Option\ - \ b) in slice-1 and TASK-3-2 (Option a) in slice-3 \u2014 the divergence risk\ - \ you flag is real, and the recommendation to make the `.egg-state/contracts/.json#pending_hitl`\ - \ envelope a shared contract between the two bridges is the right structural\ - \ mitigation. I'm carrying this through to the task_planner NACK as a non-blocking\ - \ item.\n - **R18 (15 rubric authoring + #2622 depth gap)** \u2014 verified\ - \ `orchestrator/substrate/__init__.py:280-284` still raises the \"Walking-skeleton\ - \ spike (#2623) only ships the refiner rubric; other roles are deferred to the\ - \ follow-up issue per cq-11\" `ValueError`. Your point that `build_system_prompt(sources)`\ - \ depth-threading needs per-role structural verification is exactly right \u2014\ - \ the spike validated this for *one* role only. The mitigation to add a structural\ - \ test (assert every role enumerated in `_PHASE_ROLES` has a rubric AND that\ - \ `build_system_prompt` includes its content) is the right shape; the task_planner's\ - \ TASK-2-3/3-4/3-5 don't currently include such a structural test, so this risk\ - \ stays open even after their NACK is addressed.\n - **R19 (8-subagent harness\ - \ re-host concurrency strain)** \u2014 I checked `shared/egg_harness/client.py:60-150`:\ - \ each `run_agent` invocation builds its own `ToolRegistry`, `EventBus`, `AnthropicProvider`,\ - \ and `AgentLoop` in the parent's Python process. Stacking 8 of these is exactly\ - \ the load you describe. The mitigation to require explicit measurement during\ - \ plan-phase (4 loops) before approving implement-phase (8 loops) is the right\ - \ gate; reviewer_plan should hold the implement-phase slice on the plan-phase\ - \ numbers.\n - **R27 (MCP-validator structural enforcement gap)** \u2014 I\ - \ checked `sandbox/egg_agent_tools/handlers/restrictions.py:6-15`: \"The agent\ - \ should call `check_file_restriction` *before* burning a turn\" \u2014 this\ - \ is voluntary, not enforced. Your point that MCP-validator fallback loses structural\ - \ enforcement is exactly right. The \"PreToolUse hook STILL fires for Bash and\ - \ global-deny rules + post-tool-call validator\" defense-in-depth recommendation\ - \ is concrete and actionable. The task_planner's TASK-2-5 targets `orchestrator/mcp_tools.py`\ - \ which I'm separately flagging as wrong-surface \u2014 your risk gives the\ - \ right re-direction.\n - **R29 (subagent context budget under harness re-host)**\ - \ \u2014 verified each harness loop's `AgentLoop.run(...)` accumulates conversation\ - \ history in-process; 8 concurrent loops \xD7 ~50K tokens is a real budget concern.\ - \ The mitigation to move fork-based delegation up if context pressure manifests\ - \ during plan-phase is the right contingency \u2014 the task_planner currently\ - \ defers fork to slice-5 (TASK-5-5) which is the wrong end of the timeline if\ - \ R29 manifests early.\n\n- **Trust-boundary risks are correctly framed**: R31\ - \ (15-subagent credential exposure) and the security_assessment's \"explicit_non_goals_inherited_from_2623\"\ - \ section correctly inherit R1's qualitative acceptance without re-litigating,\ - \ while flagging that the 15x multiplier wasn't quantified in cq-1.\n\n- **DoD\ - \ criterion alignment table** maps each of the 6 DoD bullets to specific risks/mitigations\ - \ \u2014 DoD #5 (`EggHarnessSpawner` + local-run) lines up with R26; DoD #6\ - \ (ADR refresh) lines up with R32 (interface stability marker timing). No DoD\ - \ bullet is silently dropped.\n\n- **`open_questions_for_implement_phase` list\ - \ (5 items)** is useful forward-handoff \u2014 context-budget gate, fork-trigger,\ - \ MCP-validator structural strength, cost-cap soft-warning threshold, runaway-NACK\ - \ detection \u2014 these all surface decisions implement-phase will need to\ - \ make and that should land in the task_planner's slice-3 acceptance criteria.\n\ - \n### Non-blocking\n- **R20 (no substrate-specific reviewer) MEDIUM rating**\ - \ \u2014 feedback Q5 declined the expansion. Worth noting in the risk text that\ - \ this is operator-accepted rather than \"MEDIUM unresolved\"; the residual\ - \ risk is unchanged but the framing matters for the merge story.\n- **R21 (#2714\ - \ closed since refine)** \u2014 confirmed via `git log --oneline | grep 2714`\ - \ that #2719 (the close fix) is already merged. Worth either acknowledging the\ - \ close or escalating to a feedback question if this changes the conformance-matrix\ - \ shape (e.g., do we replay the issue's pre-fix state or post-fix state?).\n\ - - **R33 (apply phase unreferenced in DoD)** LOW \u2014 agreed; the issue body's\ - \ DoD doesn't list epic-only apply-phase coverage, so this isn't a NACK reason.\ - \ The task_planner's TASK-5-7 lists `applier.md` in the conditional R15 migration\ - \ but no slice ships an unconditional applier rubric. If the operator expects\ - \ epic-pipeline parity, that should be a feedback question; if not, R33 stays\ - \ at LOW.\n- **R23 cost cap \"200 leaves room for $1000+ runaway\"** \u2014\ - \ the arithmetic is sound, but cq-6 already resolved this; suggest reframing\ - \ as \"operator accepted; consider soft-warning at 100 invocations\" rather\ - \ than \"MEDIUM unresolved\". The task_planner's TASK-5-1 implements the cap\ - \ mechanically; a soft warning would be a non-blocking addition.\n\nOverall:\ - \ thorough, accurate, and the HIGH-severity risks are the right ones to surface\ - \ to the operator. Approving so the planner can incorporate the structural mitigations\ - \ (R17 envelope contract, R18 depth-gap structural test, R19 plan-phase measurement\ - \ gate, R27 defense-in-depth, R29 fork-trigger contingency) into the re-propose.\n" - ack_version: 1 - version: 1 -```` - -### [2026-05-19T18:31:27Z] architect (CONSENSUS_CONFIRMED): Confirmed by architect - -````yaml -id: 6c7b3b25-fc10-49 -phase: plan -metadata: - consensus_reached: false -```` - -### [2026-05-19T18:31:27Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 10890017-5be8-45 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:31:34Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 73c583aa-58d4-47 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:31:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a014d96d-3338-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:35.568258+00:00' -```` - -### [2026-05-19T18:31:38Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: fde614d0-0603-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:32:35Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 5d0f412c-4d1d-46 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:32:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5a800bae-3c23-42 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:35.568258+00:00' -```` - -### [2026-05-19T18:32:38Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 98a9c72e-a036-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:33:35Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: cad4eba4-b740-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:33:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a9cc5b5e-55e4-40 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:35.568258+00:00' -```` - -### [2026-05-19T18:33:38Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 9c9830ee-bd22-41 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:34:35Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: df0de9e3-9e6a-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:34:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 06332960-f802-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:35.568258+00:00' -```` - -### [2026-05-19T18:34:38Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 47b88498-afc5-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:35:35Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 80c11375-1379-4c -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:35:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 5ff0e961-03a0-4d -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:35.568258+00:00' -```` - -### [2026-05-19T18:35:38Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 4e7b4528-78ad-43 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:36:56Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 731f7c34-26fd-47 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:36:56Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: a7576492-226d-4e -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:35.568258+00:00' -```` - -### [2026-05-19T18:36:56Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 0c058cdd-110f-44 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:37:50Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 53a97e86-4374-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:37:51Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 0d990a0a-2f3b-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:35.568258+00:00' -```` - -### [2026-05-19T18:37:53Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 5e2e4891-e130-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:38:36Z] task_planner (CONSENSUS_PROPOSE): Proposal from task_planner - -Plan v2 addresses reviewer_plan v1 NACK. Blocking issues fixed: (1) TASK-1-5 R2 spike reworked — the harness re-host model bypasses PreToolUse hook entirely (shared/egg_harness/client.py uses its own ToolRegistry.set_permission_callback per the reviewer's audit), so added TASK-1-9 (a test-only nested-Agent-tool dispatch fake under integration_tests/regression/_agent_tool_fake.py, coder-owned per MCP check) and reframed TASK-1-5 to use it; the test docstring documents the empirical-vs-test-fake limitation and production stays on ClaudeCodeSpawner harness re-host per cq-3. (2) TASK-4-4 conformance matrix mechanism changed from "recorded transcripts that no task produces" to MagicMock-style stubs mirroring test_substrate_smoke.py:56. (3) TASK-4-2 fence-removal task now cites BOTH :212 (call site) and :807-826 (method def) so the coder doesn't leave dead code. Non-blocking fixes also applied: TASK-2-5 enforcement target moved from orchestrator/mcp_tools.py to sandbox/egg_agent_tools/handlers/restrictions.py (the correct in-sandbox surface per the reviewer); TASK-2-6/TASK-2-7 renumbered to match file order; TASK-1-6 dependency on TASK-1-4 made explicit; TASK-3-2 daemon detach via start_new_session=True; TASK-1-1 pending_hitl envelope marked as the shared state-serialization contract for risk_analyst R17 mitigation; TASK-5-5 fork primitive switched from Agent-tool dispatch to subprocess+egg_harness.run_agent so it aligns with cq-3's deferral; LocalWorktreeManager line corrected to :59; _maybe_fence dual-location citation added to the primitives table. The slice DAG remains a strictly linear 5-slice chain (forest constraint trivially satisfied); 43 tasks total across the five slices (added TASK-1-9). All 87 file references pass the per-role allow/block check via shared/egg_restrictions/patterns.py. - -````yaml -id: ce33505c-b8e2-40 -phase: plan -metadata: - payload: - summary: "Plan v2 addresses reviewer_plan v1 NACK. Blocking issues fixed: (1)\ - \ TASK-1-5 R2 spike reworked \u2014 the harness re-host model bypasses PreToolUse\ - \ hook entirely (shared/egg_harness/client.py uses its own ToolRegistry.set_permission_callback\ - \ per the reviewer's audit), so added TASK-1-9 (a test-only nested-Agent-tool\ - \ dispatch fake under integration_tests/regression/_agent_tool_fake.py, coder-owned\ - \ per MCP check) and reframed TASK-1-5 to use it; the test docstring documents\ - \ the empirical-vs-test-fake limitation and production stays on ClaudeCodeSpawner\ - \ harness re-host per cq-3. (2) TASK-4-4 conformance matrix mechanism changed\ - \ from \"recorded transcripts that no task produces\" to MagicMock-style stubs\ - \ mirroring test_substrate_smoke.py:56. (3) TASK-4-2 fence-removal task now\ - \ cites BOTH :212 (call site) and :807-826 (method def) so the coder doesn't\ - \ leave dead code. Non-blocking fixes also applied: TASK-2-5 enforcement target\ - \ moved from orchestrator/mcp_tools.py to sandbox/egg_agent_tools/handlers/restrictions.py\ - \ (the correct in-sandbox surface per the reviewer); TASK-2-6/TASK-2-7 renumbered\ - \ to match file order; TASK-1-6 dependency on TASK-1-4 made explicit; TASK-3-2\ - \ daemon detach via start_new_session=True; TASK-1-1 pending_hitl envelope marked\ - \ as the shared state-serialization contract for risk_analyst R17 mitigation;\ - \ TASK-5-5 fork primitive switched from Agent-tool dispatch to subprocess+egg_harness.run_agent\ - \ so it aligns with cq-3's deferral; LocalWorktreeManager line corrected to\ - \ :59; _maybe_fence dual-location citation added to the primitives table. The\ - \ slice DAG remains a strictly linear 5-slice chain (forest constraint trivially\ - \ satisfied); 43 tasks total across the five slices (added TASK-1-9). All 87\ - \ file references pass the per-role allow/block check via shared/egg_restrictions/patterns.py." - attestation: {} - artifacts: - - .egg-state/drafts/2717-plan.md - risk_considered: The R2-spike reframing acknowledges that nested-dispatch hook - resolution is structurally not testable in-sandbox without a real Claude Code - session; the new TASK-1-9 fake validates hook *logic* given accurate EGG_AGENT_ROLE - propagation, and the empirical question becomes the production answer when cq-3 - flips to Agent-tool dispatch in a future issue. This means R2 = pass is the - most likely verdict (hook logic is straightforward, EGG_AGENT_ROLE-driven) and - the cq-4 contingent migration path will likely close as a no-op. Documented - this in TASK-5-7's acceptance criteria. The conformance matrix's MagicMock-style - stubs trade fidelity for in-sandbox runnability; the trade-off is documented - in the test docstring and acknowledged in the slice-4 prose. - commit_sha: cdcaebe6153cb3286ad1aa3fea2ba5f43eeed481 - files_changed: - - .egg-state/drafts/2717-plan.md - tests_run: [] - tasks_satisfied: - - TASK-1-1 - - TASK-1-2 - - TASK-1-3 - - TASK-1-4 - - TASK-1-5 - - TASK-1-6 - - TASK-1-7 - - TASK-1-8 - - TASK-1-9 - - TASK-2-1 - - TASK-2-2 - - TASK-2-3 - - TASK-2-4 - - TASK-2-5 - - TASK-2-6 - - TASK-2-7 - - TASK-3-1 - - TASK-3-2 - - TASK-3-3 - - TASK-3-4 - - TASK-3-5 - - TASK-3-6 - - TASK-3-7 - - TASK-3-8 - - TASK-3-9 - - TASK-3-10 - - TASK-4-1 - - TASK-4-2 - - TASK-4-3 - - TASK-4-4 - - TASK-4-5 - - TASK-4-6 - - TASK-4-7 - - TASK-5-1 - - TASK-5-2 - - TASK-5-3 - - TASK-5-4 - - TASK-5-5 - - TASK-5-6 - - TASK-5-7 - - TASK-5-8 - - TASK-5-9 - - TASK-5-10 - version: 2 - commit_sha: cdcaebe6153cb3286ad1aa3fea2ba5f43eeed481 -```` - -### [2026-05-19T18:38:36Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 40851050-796c-4d -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:39:04Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: b28d8cfa-cbbb-4b -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:38:49.397500+00:00' -```` - -### [2026-05-19T18:39:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: acc949d2-52a4-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:39:04Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: b69c1c04-0093-42 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:40:04Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: d485fa69-ec8e-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:38.031399+00:00' -```` - -### [2026-05-19T18:40:04Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: c9402567-719c-47 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:31:34.949499+00:00' -```` - -### [2026-05-19T18:40:04Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 096dfb32-2132-48 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:38:49.397500+00:00' -```` - -### [2026-05-19T18:40:39Z] reviewer_plan → task_planner (CONSENSUS_ACK): ACK from reviewer_plan for task_planner - - -Reviewed `.egg-state/drafts/2717-plan.md` v2 (now 43 tasks across 5 slices). All three v1 blockers are fixed correctly; non-blocking items also addressed. - -### Blocker fixes verified - -1. **TASK-1-5 R2 spike — fixed.** The task description now opens with the structural acknowledgement I asked for ("the harness re-host model bypasses the PreToolUse hook entirely because `shared/egg_harness/client.py:60-150` uses its own `ToolRegistry.set_permission_callback(...)` and `grep -rn "PreToolUseHookPolicy|hook_entry" shared/egg_harness/` returns zero hits"). The new TASK-1-9 ships `integration_tests/regression/_agent_tool_fake.py` as test-only infrastructure with an explicit `if not __name__.startswith("integration_tests")` import guard preventing silent production import. The role↔files check confirms `_agent_tool_fake.py` is coder-writable (underscored helper-style name escapes the `**/test_*.py` blocked-glob — verified via the same per-file restriction lookup I ran on v1; `integration_tests/regression/` is NOT matched by `**/tests/` because the directory is `integration_tests`, not `tests`). The acceptance criteria honestly documents the test-fake limitation in the docstring ("the test confirms hook *logic* is correct given accurate `EGG_AGENT_ROLE` propagation; whether Claude Code itself propagates `EGG_AGENT_ROLE` correctly under real nested Agent-tool dispatch is verifiable only by running real Claude Code … when cq-3 flips to Agent-tool dispatch in a future issue, the production path becomes the empirical answer to that remaining half of R2"). That framing — "hook-logic test now, real-dispatch verification deferred to cq-3 follow-up" — is the right scoping; the cq-4 / cq-6 contingencies branch on a meaningful signal (hook logic IS correct vs hook logic IS broken) rather than a phantom one. - -2. **TASK-4-4 conformance matrix — fixed.** Mechanism switched to "per-subagent MagicMock stubs in the same style as `integration_tests/regression/test_substrate_smoke.py:56` (hardcoded `stdout="ok"`, no recorded transcripts)". This is option (b) from my v1 NACK — the cheapest path that keeps the matrix in-sandbox-runnable. The acceptance text documents the trade-off ("MagicMock-vs-real-transcript trade-off") so future readers know the matrix tests the SDLC *shape* not verbatim text. #2714's closed state is also acknowledged with the right framing ("irrelevant per feedback Q1's rationale"). The chicken-and-egg problem of v1 is gone. - -3. **TASK-4-2 fence-removal — fixed.** Now cites both `:212` (call site) and `:807-826` (method def) explicitly in the description ("Two locations must be touched … Removing only the call site leaves dead code; both must be deleted"). Acceptance criteria echoes both locations. A coder following the cite literally will now delete both. - -### Non-blocking items also resolved - -- **TASK-5-5 fork primitive — now aligned with cq-3.** Implementation switched from "Agent tool with `subagent_type='general-purpose'`" to "`subprocess.Popen` running a fresh `egg_harness.run_agent` invocation in a clean Python process (NOT via Claude Code's Agent tool — that would commit to Agent-tool dispatch ahead of the cq-3 empirical verdict)". The acceptance text also handles the future migration cleanly ("If cq-3's empirical verdict in slice-5's ADR refresh recommends migrating to Agent-tool dispatch, the fork primitive's implementation will change accordingly in a follow-up — not in this slice"). Good — cq-3 deferral preserved. - -- **TASK-2-5 enforcement target — corrected.** Surface moved from `orchestrator/mcp_tools.py` to `sandbox/egg_agent_tools/handlers/restrictions.py` (where `check_file_restriction(req)` lives per `grep -rn "check_file_restriction" --include="*.py" orchestrator/ sandbox/`). The description explicitly cites the audit ("the correct surface per the plan reviewer's audit is the in-sandbox tool handler, NOT `orchestrator/mcp_tools.py` (which is the orchestrator-side MCP server, a different surface)"). The dependent test (TASK-2-6) moved to `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` — confirmed tester-writable. - -- **TASK-3-2 daemon lifecycle — pinned down.** Description now mandates `subprocess.Popen(start_new_session=True)` (or `os.setsid()` after fork) and the acceptance criteria asserts the property in TASK-3-8's test ("the long-lived-across-Bash-exits property is asserted in TASK-3-8's test"). The "daemon dies when calling Bash exits" failure mode is no longer latent. - -- **TASK-1-1 pending_hitl envelope — marked as shared contract for R17.** TASK-1-9 also calls out that the daemon (TASK-3-2) inherits the same envelope schema ("closes risk_analyst R17 dual-bridge concern"). Slice-1 commits to a state-serialization contract slice-3 picks up rather than reinventing. - -- **TASK-1-6 dependency on TASK-1-4 — made explicit.** Description now says "Depends on TASK-1-4 having landed the two new rubric .md files first … The implementer should sequence TASK-1-4 → TASK-1-6 within the slice". - -- **TASK-2-6 / TASK-2-7 renumbered** so file order matches numeric order. - -- **`LocalWorktreeManager` line citation corrected to :59**; **`_maybe_fence` dual-location** (`:212` call + `:807-826` def) added to the primitives table. - -### Remaining non-blocking observations - -These are observations the planner doesn't need to address before consensus — surfacing for the implement-phase team: - -- **Slice-sizing advisories from v1 still apply** (#2137 — advisory only, never NACK per HITL decision-6 opt-2): slice-3 ~1,500 LOC ("consider splitting"), slice-4 ~2,200 LOC ("strongly consider splitting"), slice-5 ~2,500 LOC ("strongly consider splitting"). The operator retains override authority; the implementer should know if the slice expands further during implement-phase. - -- **R2 verdict semantics narrowed.** TASK-1-5's acceptance now tests hook *logic* given correct env propagation. That answers "is `hook_entry.decide()` correct?" not "does Claude Code propagate `EGG_AGENT_ROLE` correctly under real Agent-tool dispatch?". The latter question can only be answered when cq-3 flips to real Agent-tool dispatch in a future issue. The plan's framing ("the production path becomes the empirical answer to that remaining half of R2") is honest about this — but the cq-4 / cq-6 contingencies trigger on the *narrower* R2 signal. If the narrower R2 passes but the deferred real-dispatch R2 later fails, cq-4 / cq-6 may need to re-fire mid-future-issue. Worth flagging in the slice-1 ADR update (TASK-1-8) so the operator knows the contingency surface is split. - -- **R19 / R29 mitigation gates from risk_analyst** (plan-phase concurrency measurement before implement-phase ships; context-budget contingency on fork-trigger): the plan doesn't add explicit acceptance criteria gates for these on slice-2 (the plan-phase slice that produces the measurement). Risk_analyst's mitigations require reviewer_plan to hold slice-3 on slice-2's numbers; that's a review-cycle obligation, not a code task — but the slice-2 acceptance for TASK-2-4 (`test_inprocess_plan_brc.py`) could include "records the per-loop wall-clock and parent-context delta to `.egg-state//substrate-metrics.json`" so the slice-3 review has data to gate on. Worth considering at implement-time; not a NACK reason now. - -- **TASK-4-3 substrate-fixture sprawl across 14 existing regression tests** is itself a sizeable change set. If any of those 14 tests aren't substrate-portable in practice (transitively depend on `egg_stack` or `gateway_url` through a helper), the task will need to add `@pytest.mark.skip(reason=...)` markers — the acceptance criteria allows for this, but worth budgeting review attention for those 14 individual touches when slice-4 lands. - -Plan is ready for ingestion. Slice DAG is strictly linear (slice-1 → 2 → 3 → 4 → 5) so #2137 forest constraint is trivially satisfied. All 43 tasks have role assignments writable per `shared/egg_restrictions/patterns.py` (verified via per-file lookup across the full task list). - - -````yaml -id: ac360879-d9c9-40 -phase: plan -metadata: - payload: - artifact_references: - - .egg-state/drafts/2717-plan.md - reason: "\nReviewed `.egg-state/drafts/2717-plan.md` v2 (now 43 tasks across 5\ - \ slices). All three v1 blockers are fixed correctly; non-blocking items also\ - \ addressed.\n\n### Blocker fixes verified\n\n1. **TASK-1-5 R2 spike \u2014\ - \ fixed.** The task description now opens with the structural acknowledgement\ - \ I asked for (\"the harness re-host model bypasses the PreToolUse hook entirely\ - \ because `shared/egg_harness/client.py:60-150` uses its own `ToolRegistry.set_permission_callback(...)`\ - \ and `grep -rn \"PreToolUseHookPolicy|hook_entry\" shared/egg_harness/` returns\ - \ zero hits\"). The new TASK-1-9 ships `integration_tests/regression/_agent_tool_fake.py`\ - \ as test-only infrastructure with an explicit `if not __name__.startswith(\"\ - integration_tests\")` import guard preventing silent production import. The\ - \ role\u2194files check confirms `_agent_tool_fake.py` is coder-writable (underscored\ - \ helper-style name escapes the `**/test_*.py` blocked-glob \u2014 verified\ - \ via the same per-file restriction lookup I ran on v1; `integration_tests/regression/`\ - \ is NOT matched by `**/tests/` because the directory is `integration_tests`,\ - \ not `tests`). The acceptance criteria honestly documents the test-fake limitation\ - \ in the docstring (\"the test confirms hook *logic* is correct given accurate\ - \ `EGG_AGENT_ROLE` propagation; whether Claude Code itself propagates `EGG_AGENT_ROLE`\ - \ correctly under real nested Agent-tool dispatch is verifiable only by running\ - \ real Claude Code \u2026 when cq-3 flips to Agent-tool dispatch in a future\ - \ issue, the production path becomes the empirical answer to that remaining\ - \ half of R2\"). That framing \u2014 \"hook-logic test now, real-dispatch verification\ - \ deferred to cq-3 follow-up\" \u2014 is the right scoping; the cq-4 / cq-6\ - \ contingencies branch on a meaningful signal (hook logic IS correct vs hook\ - \ logic IS broken) rather than a phantom one.\n\n2. **TASK-4-4 conformance matrix\ - \ \u2014 fixed.** Mechanism switched to \"per-subagent MagicMock stubs in the\ - \ same style as `integration_tests/regression/test_substrate_smoke.py:56` (hardcoded\ - \ `stdout=\"ok\"`, no recorded transcripts)\". This is option (b) from my v1\ - \ NACK \u2014 the cheapest path that keeps the matrix in-sandbox-runnable. The\ - \ acceptance text documents the trade-off (\"MagicMock-vs-real-transcript trade-off\"\ - ) so future readers know the matrix tests the SDLC *shape* not verbatim text.\ - \ #2714's closed state is also acknowledged with the right framing (\"irrelevant\ - \ per feedback Q1's rationale\"). The chicken-and-egg problem of v1 is gone.\n\ - \n3. **TASK-4-2 fence-removal \u2014 fixed.** Now cites both `:212` (call site)\ - \ and `:807-826` (method def) explicitly in the description (\"Two locations\ - \ must be touched \u2026 Removing only the call site leaves dead code; both\ - \ must be deleted\"). Acceptance criteria echoes both locations. A coder following\ - \ the cite literally will now delete both.\n\n### Non-blocking items also resolved\n\ - \n- **TASK-5-5 fork primitive \u2014 now aligned with cq-3.** Implementation\ - \ switched from \"Agent tool with `subagent_type='general-purpose'`\" to \"\ - `subprocess.Popen` running a fresh `egg_harness.run_agent` invocation in a clean\ - \ Python process (NOT via Claude Code's Agent tool \u2014 that would commit\ - \ to Agent-tool dispatch ahead of the cq-3 empirical verdict)\". The acceptance\ - \ text also handles the future migration cleanly (\"If cq-3's empirical verdict\ - \ in slice-5's ADR refresh recommends migrating to Agent-tool dispatch, the\ - \ fork primitive's implementation will change accordingly in a follow-up \u2014\ - \ not in this slice\"). Good \u2014 cq-3 deferral preserved.\n\n- **TASK-2-5\ - \ enforcement target \u2014 corrected.** Surface moved from `orchestrator/mcp_tools.py`\ - \ to `sandbox/egg_agent_tools/handlers/restrictions.py` (where `check_file_restriction(req)`\ - \ lives per `grep -rn \"check_file_restriction\" --include=\"*.py\" orchestrator/\ - \ sandbox/`). The description explicitly cites the audit (\"the correct surface\ - \ per the plan reviewer's audit is the in-sandbox tool handler, NOT `orchestrator/mcp_tools.py`\ - \ (which is the orchestrator-side MCP server, a different surface)\"). The dependent\ - \ test (TASK-2-6) moved to `tests/sandbox/egg_agent_tools/test_restrictions_validator.py`\ - \ \u2014 confirmed tester-writable.\n\n- **TASK-3-2 daemon lifecycle \u2014\ - \ pinned down.** Description now mandates `subprocess.Popen(start_new_session=True)`\ - \ (or `os.setsid()` after fork) and the acceptance criteria asserts the property\ - \ in TASK-3-8's test (\"the long-lived-across-Bash-exits property is asserted\ - \ in TASK-3-8's test\"). The \"daemon dies when calling Bash exits\" failure\ - \ mode is no longer latent.\n\n- **TASK-1-1 pending_hitl envelope \u2014 marked\ - \ as shared contract for R17.** TASK-1-9 also calls out that the daemon (TASK-3-2)\ - \ inherits the same envelope schema (\"closes risk_analyst R17 dual-bridge concern\"\ - ). Slice-1 commits to a state-serialization contract slice-3 picks up rather\ - \ than reinventing.\n\n- **TASK-1-6 dependency on TASK-1-4 \u2014 made explicit.**\ - \ Description now says \"Depends on TASK-1-4 having landed the two new rubric\ - \ .md files first \u2026 The implementer should sequence TASK-1-4 \u2192 TASK-1-6\ - \ within the slice\".\n\n- **TASK-2-6 / TASK-2-7 renumbered** so file order\ - \ matches numeric order.\n\n- **`LocalWorktreeManager` line citation corrected\ - \ to :59**; **`_maybe_fence` dual-location** (`:212` call + `:807-826` def)\ - \ added to the primitives table.\n\n### Remaining non-blocking observations\n\ - \nThese are observations the planner doesn't need to address before consensus\ - \ \u2014 surfacing for the implement-phase team:\n\n- **Slice-sizing advisories\ - \ from v1 still apply** (#2137 \u2014 advisory only, never NACK per HITL decision-6\ - \ opt-2): slice-3 ~1,500 LOC (\"consider splitting\"), slice-4 ~2,200 LOC (\"\ - strongly consider splitting\"), slice-5 ~2,500 LOC (\"strongly consider splitting\"\ - ). The operator retains override authority; the implementer should know if the\ - \ slice expands further during implement-phase.\n\n- **R2 verdict semantics\ - \ narrowed.** TASK-1-5's acceptance now tests hook *logic* given correct env\ - \ propagation. That answers \"is `hook_entry.decide()` correct?\" not \"does\ - \ Claude Code propagate `EGG_AGENT_ROLE` correctly under real Agent-tool dispatch?\"\ - . The latter question can only be answered when cq-3 flips to real Agent-tool\ - \ dispatch in a future issue. The plan's framing (\"the production path becomes\ - \ the empirical answer to that remaining half of R2\") is honest about this\ - \ \u2014 but the cq-4 / cq-6 contingencies trigger on the *narrower* R2 signal.\ - \ If the narrower R2 passes but the deferred real-dispatch R2 later fails, cq-4\ - \ / cq-6 may need to re-fire mid-future-issue. Worth flagging in the slice-1\ - \ ADR update (TASK-1-8) so the operator knows the contingency surface is split.\n\ - \n- **R19 / R29 mitigation gates from risk_analyst** (plan-phase concurrency\ - \ measurement before implement-phase ships; context-budget contingency on fork-trigger):\ - \ the plan doesn't add explicit acceptance criteria gates for these on slice-2\ - \ (the plan-phase slice that produces the measurement). Risk_analyst's mitigations\ - \ require reviewer_plan to hold slice-3 on slice-2's numbers; that's a review-cycle\ - \ obligation, not a code task \u2014 but the slice-2 acceptance for TASK-2-4\ - \ (`test_inprocess_plan_brc.py`) could include \"records the per-loop wall-clock\ - \ and parent-context delta to `.egg-state//substrate-metrics.json`\"\ - \ so the slice-3 review has data to gate on. Worth considering at implement-time;\ - \ not a NACK reason now.\n\n- **TASK-4-3 substrate-fixture sprawl across 14\ - \ existing regression tests** is itself a sizeable change set. If any of those\ - \ 14 tests aren't substrate-portable in practice (transitively depend on `egg_stack`\ - \ or `gateway_url` through a helper), the task will need to add `@pytest.mark.skip(reason=...)`\ - \ markers \u2014 the acceptance criteria allows for this, but worth budgeting\ - \ review attention for those 14 individual touches when slice-4 lands.\n\nPlan\ - \ is ready for ingestion. Slice DAG is strictly linear (slice-1 \u2192 2 \u2192\ - \ 3 \u2192 4 \u2192 5) so #2137 forest constraint is trivially satisfied. All\ - \ 43 tasks have role assignments writable per `shared/egg_restrictions/patterns.py`\ - \ (verified via per-file lookup across the full task list).\n" - ack_version: 2 - version: 2 -```` - -### [2026-05-19T18:40:39Z] orchestrator → task_planner (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 2) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: 11599e8d-e19c-46 -phase: plan -metadata: - ready_to_confirm: true - version: 2 -```` - -### [2026-05-19T18:40:39Z] task_planner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 775b63af-32d2-4f -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:40:43Z] reviewer_plan (CONSENSUS_CONFIRMED): Confirmed by reviewer_plan - -````yaml -id: 307b9e11-91bb-4e -phase: plan -metadata: - consensus_reached: false -```` - -### [2026-05-19T18:40:43Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 77a581d3-c0d4-4b -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:40:43Z] architect (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 923e67cc-2ea8-41 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:40:44Z] task_planner (CONSENSUS_CONFIRMED): Confirmed by task_planner - -````yaml -id: f6e8c9ff-1067-42 -phase: plan -metadata: - consensus_reached: true -```` - -### [2026-05-19T18:40:48Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: ad1a76e5-31e9-4d -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:40:48.680618+00:00' -```` - -### [2026-05-19T18:40:48Z] architect (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 25d11e06-3e27-48 -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:40:49Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: ef3b8f50-203f-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:40:49.024551+00:00' -```` - -### [2026-05-19T18:40:49Z] risk_analyst (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 7be61421-ba4c-4d -phase: plan -metadata: - state: WORKING -```` - -### [2026-05-19T18:41:17Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 74f26ae6-dd15-4f -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:40:54.736152+00:00' -```` - -### [2026-05-19T18:41:17Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 3b65d5e2-9e74-4a -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:40:55.448086+00:00' -```` - -### [2026-05-19T18:41:17Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT - -````yaml -id: 980ddd5c-2753-49 -phase: plan -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T18:40:56.091835+00:00' -```` - -### [2026-05-19T18:41:17Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 71ec8095-3e26-46 -phase: plan -metadata: - state: WORKING -```` diff --git a/.egg-state/brc-history/2717-refine.json b/.egg-state/brc-history/2717-refine.json deleted file mode 100644 index b161d67587..0000000000 --- a/.egg-state/brc-history/2717-refine.json +++ /dev/null @@ -1,606 +0,0 @@ -[ - { - "id": "715599e5-aa0f-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:10:22.994676+00:00", - "phase": "refine" - }, - { - "id": "308156fa-6e0f-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:10:23.508778+00:00", - "phase": "refine" - }, - { - "id": "facfde93-d1aa-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:11:45.300124+00:00", - "phase": "refine" - }, - { - "id": "0f583b37-8881-45", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:11:45.329591+00:00", - "phase": "refine" - }, - { - "id": "51058cc3-8a78-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:12:38.061139+00:00", - "phase": "refine" - }, - { - "id": "7413a360-feea-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:12:38.581728+00:00", - "phase": "refine" - }, - { - "id": "7781de15-0998-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:13:38.120257+00:00", - "phase": "refine" - }, - { - "id": "553322f7-5a74-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:13:38.649127+00:00", - "phase": "refine" - }, - { - "id": "b825951a-8b55-43", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:14:38.165052+00:00", - "phase": "refine" - }, - { - "id": "26aa9d8b-69b6-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:14:38.693754+00:00", - "phase": "refine" - }, - { - "id": "4383e465-5f62-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:15:38.221657+00:00", - "phase": "refine" - }, - { - "id": "afa292d9-03be-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:15:38.753619+00:00", - "phase": "refine" - }, - { - "id": "e118bc1f-1dda-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:16:38.270450+00:00", - "phase": "refine" - }, - { - "id": "928861fe-89a2-48", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:16:38.826552+00:00", - "phase": "refine" - }, - { - "id": "52de0575-8838-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:17:38.325899+00:00", - "phase": "refine" - }, - { - "id": "b6e4358e-2f16-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:17:38.873437+00:00", - "phase": "refine" - }, - { - "id": "d58868bb-d7d3-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:18:38.381333+00:00", - "phase": "refine" - }, - { - "id": "a6040b24-0734-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:18:38.921161+00:00", - "phase": "refine" - }, - { - "id": "3da916df-5fb8-4f", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:13.545929+00:00" - }, - "timestamp": "2026-05-19T17:19:38.410913+00:00", - "phase": "refine" - }, - { - "id": "53ae00fe-e9d5-46", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:10:23.479786+00:00" - }, - "timestamp": "2026-05-19T17:19:38.970832+00:00", - "phase": "refine" - }, - { - "id": "7c39d8c4-929f-4d", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:20:27.122947+00:00" - }, - "timestamp": "2026-05-19T17:20:27.900269+00:00", - "phase": "refine" - }, - { - "id": "9a3bae4a-9c1b-40", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "CONSENSUS_PROPOSE", - "subject": "Proposal from refiner", - "body": "Refine analysis for #2717 substrate-swap rollout. Documents the spike's current end-state (refine-only, refiner-only, harness re-host in-process; no skill driver bridges generator-yields to AskUserQuestion; only refiner rubric ships); enumerates the role rosters for plan/implement/pr the rollout has to wire; surfaces 7 net-new HITL decisions (cq-1 bridge-gap design Option-(a)-daemon vs Option-(b)-stage-scripts vs hybrid; cq-2 slice-DAG shape; cq-3 ClaudeCodeSpawner harness-rehost vs Agent-tool dispatcher migration; cq-4 R15 model-(a)/(b) timing; cq-5 R2 PreToolUse hook empirical-validation timing; cq-6 EGG_PIPELINE_MAX_AGENT_INVOCATIONS default; cq-7 k3s adapter parity scope) and 6 open-ended feedback questions (curated 5-issue conformance set specifics, latency/cost budget, EggHarnessSpawner scope, multi-role R1 mitigations, reviewer-roster expansion, post-rollout scope-fence). Recommends Option-C slice shape (bridge \u2192 plan \u2192 implement \u2192 pr+conformance \u2192 parallel hardening) with bridge Option-(c) hybrid.", - "metadata": { - "payload": { - "summary": "Refine analysis for #2717 substrate-swap rollout. Documents the spike's current end-state (refine-only, refiner-only, harness re-host in-process; no skill driver bridges generator-yields to AskUserQuestion; only refiner rubric ships); enumerates the role rosters for plan/implement/pr the rollout has to wire; surfaces 7 net-new HITL decisions (cq-1 bridge-gap design Option-(a)-daemon vs Option-(b)-stage-scripts vs hybrid; cq-2 slice-DAG shape; cq-3 ClaudeCodeSpawner harness-rehost vs Agent-tool dispatcher migration; cq-4 R15 model-(a)/(b) timing; cq-5 R2 PreToolUse hook empirical-validation timing; cq-6 EGG_PIPELINE_MAX_AGENT_INVOCATIONS default; cq-7 k3s adapter parity scope) and 6 open-ended feedback questions (curated 5-issue conformance set specifics, latency/cost budget, EggHarnessSpawner scope, multi-role R1 mitigations, reviewer-roster expansion, post-rollout scope-fence). Recommends Option-C slice shape (bridge \u2192 plan \u2192 implement \u2192 pr+conformance \u2192 parallel hardening) with bridge Option-(c) hybrid.", - "attestation": {}, - "artifacts": [ - ".egg-state/drafts/2717-analysis.md" - ], - "risk_considered": "Risk: surfacing too many open questions overwhelms operator. Mitigated by separating cq-1..cq-7 multi-choice decisions (each with concrete options + tradeoffs) from open-ended feedback Q1-Q6; the inherited cq-1..cq-12 from #2623 are explicitly noted as 'Resolved in pre-refine' to avoid re-litigation. Risk: recommending Option-C provisionally before cq-1/cq-2/cq-3 resolve \u2014 analysis documents the dependency explicitly so operator knows the recommendation may shift.", - "commit_sha": "e529722349e5b193b21d27ef43c0d35182f4799c", - "files_changed": [ - ".egg-state/drafts/2717-analysis.md" - ], - "tests_run": [], - "tasks_satisfied": [] - }, - "version": 1, - "commit_sha": "e529722349e5b193b21d27ef43c0d35182f4799c" - }, - "timestamp": "2026-05-19T17:20:28.308052+00:00", - "phase": "refine" - }, - { - "id": "b9b6c68a-f656-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T17:20:28.356884+00:00", - "phase": "refine" - }, - { - "id": "9aeab0b8-67c4-47", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T17:20:28.385145+00:00", - "phase": "refine" - }, - { - "id": "e0cea532-1184-42", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:20:39.468415+00:00" - }, - "timestamp": "2026-05-19T17:20:56.102888+00:00", - "phase": "refine" - }, - { - "id": "06a12521-7404-4d", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:20:39.468415+00:00" - }, - "timestamp": "2026-05-19T17:21:54.532371+00:00", - "phase": "refine" - }, - { - "id": "187f809e-c9a8-42", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "refiner", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_agent_design for refiner", - "body": "\nReviewed `.egg-state/drafts/2717-analysis.md` end-to-end through the agent-design lens against `docs/guides/agent-mode-design.md` and the agent-mode-design review criteria. The draft is well-aligned with agent-mode design; no blocking concerns.\n\n### What I checked and why it lands clean\n\n1. **Policy enforcement is sandbox-anchored, not prompt-level.** \"Constraints \u2192 Architectural\" (lines 70-79) reiterates that PreToolUse hooks (cq-6) are the load-bearing seam and that `shared/egg_restrictions/patterns.py` is the single source of truth for both the hook and the MCP-validator fallback. R2 (hook role-resolution under nested dispatch) is correctly flagged as an empirical question with cq-6 option 2 (MCP-validator-side enforcement) named as the structural fallback if hooks can't resolve roles \u2014 neither path is \"tell the agent not to do X in the prompt.\" This is exactly the pattern the agent-design guidance asks for.\n\n2. **R15 model (a) \u2192 model (b) migration is treated seriously, not hand-waved.** cq-4 in \"Open Questions\" (line 203) registers when to migrate from ad-hoc spawner-assembled prompts to `.claude/agents/.md` definitions with structural tool restrictions \u2014 that's the correct framing (structural enforcement preferred; prompt discipline is a stop-gap, not a destination).\n\n3. **Subagent dispatch direction is toward the Agent tool, not raw API calls.** The \"Critical scope note\" (line 28) names that today's `ClaudeCodeSpawner` re-hosts `egg_harness.run_agent` in-process and reserves `ClaudeCodeAgentToolSpawner` for the real `Agent` tool with `subagent_type=\"general-purpose\"` dispatch. cq-3 (line 202) registers when to migrate. No proposal to bypass the Agent SDK / Agent tool with raw HTTP appears anywhere.\n\n4. **HITL surface is `AskUserQuestion`-native.** cq-7 (line 67) is reaffirmed; the bridge gap analysis (\"Current Behavior \u2192 What does NOT work today\", line 36) frames the missing piece as \"no skill driver can ferry `HITLDecision` to `AskUserQuestion`,\" not \"let's parse agent output and act on it.\" Bridge Options (a/b/c) on lines 149-153 are about persisting operator state across stages / generator-resumability \u2014 they are NOT agent-output post-processing pipelines (the contract JSON holds *operator answers*, not agent output the orchestrator parses to take actions).\n\n5. **No pre-fetched diffs / file contents baked into prompts.** The analysis discusses prompts at the structural level (rubric depth, `build_system_prompt(sources)` reaching every new role rubric \u2014 line 73) without proposing to bake repo state into them. Role rubric files are role-orientation context, not pre-fetched task context \u2014 that's fine per the philosophy section.\n\n6. **No structured output for humans, no hardcoded model IDs, no rigid procedure micromanagement.** The \"Recommended Approach\" stays at slice-ordering granularity (line 157-167); it does not prescribe role-prompt step-by-step procedures.\n\n7. **Cost-cap (REC5) at 15-subagents-per-pipeline scale is appropriately escalated.** \"Operational constraints\" (line 82) correctly recognizes the scaling delta from the spike's 1 subagent to the rollout's 15 changes the urgency calculus on `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` \u2014 that's a real safety property of the substrate, not a prompt-level concern.\n\n### Non-blocking\n\n- **Line 28 / cq-3** \u2014 The harness-re-host vs Agent-tool-dispatch distinction is the most consequential agent-design choice in the rollout. The current draft surfaces it as one of seven cq questions; consider letting the plan phase weight it explicitly when assembling slice order, because Option C's plan-phase slice has different shape under cq-3 = \"stay\" (no new spawner) vs cq-3 = \"migrate now\" (new spawner is itself a slice). Not blocking \u2014 the recommendation already calls out provisionality on line 171.\n- **Line 156-167** \u2014 Bridge Option (c) hybrid is the right call agent-design-wise (operator-state persistence chosen per-phase based on cost gradient, not a single shape forced everywhere). Worth a one-line note in the recommendation that Option (b)'s `.egg-state/contracts/.json`-threaded answer flow does NOT involve the orchestrator parsing agent output \u2014 it's pure operator-input persistence \u2014 so a future reviewer doesn't mistake it for a post-processing pipeline anti-pattern.\n- **Line 215 / Q5** \u2014 The \"any new reviewer roles\" question (`reviewer_substrate`, `reviewer_concurrency_substrate`) is fine to ask, but adding a substrate-specific reviewer role would itself be an agent-design choice: it conflates \"what the code does\" (substrate-aware) with \"what role reviews it\" (single producer concern). The cleaner shape is teaching existing reviewers about the substrate axis via the rubric, not minting a new role. Operator's call; calling out the agent-design preference here.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/drafts/2717-analysis.md" - ], - "reason": "\nReviewed `.egg-state/drafts/2717-analysis.md` end-to-end through the agent-design lens against `docs/guides/agent-mode-design.md` and the agent-mode-design review criteria. The draft is well-aligned with agent-mode design; no blocking concerns.\n\n### What I checked and why it lands clean\n\n1. **Policy enforcement is sandbox-anchored, not prompt-level.** \"Constraints \u2192 Architectural\" (lines 70-79) reiterates that PreToolUse hooks (cq-6) are the load-bearing seam and that `shared/egg_restrictions/patterns.py` is the single source of truth for both the hook and the MCP-validator fallback. R2 (hook role-resolution under nested dispatch) is correctly flagged as an empirical question with cq-6 option 2 (MCP-validator-side enforcement) named as the structural fallback if hooks can't resolve roles \u2014 neither path is \"tell the agent not to do X in the prompt.\" This is exactly the pattern the agent-design guidance asks for.\n\n2. **R15 model (a) \u2192 model (b) migration is treated seriously, not hand-waved.** cq-4 in \"Open Questions\" (line 203) registers when to migrate from ad-hoc spawner-assembled prompts to `.claude/agents/.md` definitions with structural tool restrictions \u2014 that's the correct framing (structural enforcement preferred; prompt discipline is a stop-gap, not a destination).\n\n3. **Subagent dispatch direction is toward the Agent tool, not raw API calls.** The \"Critical scope note\" (line 28) names that today's `ClaudeCodeSpawner` re-hosts `egg_harness.run_agent` in-process and reserves `ClaudeCodeAgentToolSpawner` for the real `Agent` tool with `subagent_type=\"general-purpose\"` dispatch. cq-3 (line 202) registers when to migrate. No proposal to bypass the Agent SDK / Agent tool with raw HTTP appears anywhere.\n\n4. **HITL surface is `AskUserQuestion`-native.** cq-7 (line 67) is reaffirmed; the bridge gap analysis (\"Current Behavior \u2192 What does NOT work today\", line 36) frames the missing piece as \"no skill driver can ferry `HITLDecision` to `AskUserQuestion`,\" not \"let's parse agent output and act on it.\" Bridge Options (a/b/c) on lines 149-153 are about persisting operator state across stages / generator-resumability \u2014 they are NOT agent-output post-processing pipelines (the contract JSON holds *operator answers*, not agent output the orchestrator parses to take actions).\n\n5. **No pre-fetched diffs / file contents baked into prompts.** The analysis discusses prompts at the structural level (rubric depth, `build_system_prompt(sources)` reaching every new role rubric \u2014 line 73) without proposing to bake repo state into them. Role rubric files are role-orientation context, not pre-fetched task context \u2014 that's fine per the philosophy section.\n\n6. **No structured output for humans, no hardcoded model IDs, no rigid procedure micromanagement.** The \"Recommended Approach\" stays at slice-ordering granularity (line 157-167); it does not prescribe role-prompt step-by-step procedures.\n\n7. **Cost-cap (REC5) at 15-subagents-per-pipeline scale is appropriately escalated.** \"Operational constraints\" (line 82) correctly recognizes the scaling delta from the spike's 1 subagent to the rollout's 15 changes the urgency calculus on `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` \u2014 that's a real safety property of the substrate, not a prompt-level concern.\n\n### Non-blocking\n\n- **Line 28 / cq-3** \u2014 The harness-re-host vs Agent-tool-dispatch distinction is the most consequential agent-design choice in the rollout. The current draft surfaces it as one of seven cq questions; consider letting the plan phase weight it explicitly when assembling slice order, because Option C's plan-phase slice has different shape under cq-3 = \"stay\" (no new spawner) vs cq-3 = \"migrate now\" (new spawner is itself a slice). Not blocking \u2014 the recommendation already calls out provisionality on line 171.\n- **Line 156-167** \u2014 Bridge Option (c) hybrid is the right call agent-design-wise (operator-state persistence chosen per-phase based on cost gradient, not a single shape forced everywhere). Worth a one-line note in the recommendation that Option (b)'s `.egg-state/contracts/.json`-threaded answer flow does NOT involve the orchestrator parsing agent output \u2014 it's pure operator-input persistence \u2014 so a future reviewer doesn't mistake it for a post-processing pipeline anti-pattern.\n- **Line 215 / Q5** \u2014 The \"any new reviewer roles\" question (`reviewer_substrate`, `reviewer_concurrency_substrate`) is fine to ask, but adding a substrate-specific reviewer role would itself be an agent-design choice: it conflates \"what the code does\" (substrate-aware) with \"what role reviews it\" (single producer concern). The cleaner shape is teaching existing reviewers about the substrate axis via the rubric, not minting a new role. Operator's call; calling out the agent-design preference here.\n", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-19T17:22:21.346541+00:00", - "phase": "refine" - }, - { - "id": "59dd831a-a23d-48", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T17:22:21.434407+00:00", - "phase": "refine" - }, - { - "id": "5fa72f5f-5989-49", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_agent_design", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T17:22:24.944550+00:00", - "phase": "refine" - }, - { - "id": "9a9e9659-b8f3-4c", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:22:29.979402+00:00" - }, - "timestamp": "2026-05-19T17:22:30.009822+00:00", - "phase": "refine" - }, - { - "id": "4fbbc878-fad1-4a", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:22:30.030734+00:00" - }, - "timestamp": "2026-05-19T17:22:30.051234+00:00", - "phase": "refine" - }, - { - "id": "ac056bf7-d515-4e", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:22:29.979402+00:00" - }, - "timestamp": "2026-05-19T17:23:30.064954+00:00", - "phase": "refine" - }, - { - "id": "fe20b943-03bd-40", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WAITING_FOR_EVENT", - "body": "wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT", - "metadata": { - "state": "WAITING_FOR_EVENT", - "since": "2026-05-19T17:22:30.030734+00:00" - }, - "timestamp": "2026-05-19T17:23:30.137452+00:00", - "phase": "refine" - }, - { - "id": "c20728e4-b824-4c", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "refiner", - "message_type": "CONSENSUS_ACK", - "subject": "ACK from reviewer_refine for refiner", - "body": "\nReviewed `.egg-state/drafts/2717-analysis.md` (221 lines, commit `e529722349`) and cross-verified `.egg-state/contracts/issue-2717.json` against the analysis's open-question inventory. Verified all 7 HITL decisions (cq-1..cq-7) and 6 feedback questions (Q1..Q6) are registered with substantive option labels. Verified file-path / line-range citations against the actual codebase (e.g. `shared/egg_contracts/agent_roles.py:1172-1209` for `_PHASE_ROLES` / `_PHASE_REVIEWERS`, `orchestrator/substrate/__init__.py:77-94` for `select_substrate`, `orchestrator/substrate/claude_code/spawner.py:9-26` for the spike-scope harness re-host comment). No blocking issues found.\n\n### Section-by-section evaluation\n\n1. **Problem Statement (\u00a7 Problem Statement, :5-20)** \u2014 Anchors the work in cq-11 (\"spike then plan\") and correctly characterizes #2715 as the spike + this issue as the re-plan. The list of spike-shipped artifacts (Protocol shims, claude-code implementations, K3sSpawnerAdapter, plugin scaffold, ADR, smoke test) matches what is actually in-tree. Desired outcome ties cleanly to the 6 DoD criteria from the issue body. **Strong.**\n\n2. **Current Behavior (\u00a7 :22-62)** \u2014 Excellent split between \"works today\" and \"doesn't work today\" with specific file:line anchors. The critical scope note at :28 (harness re-host vs Agent-tool dispatch) is exactly the right level of detail to surface as a decision and is correctly registered as cq-3. The phase-roles table at :49-55 matches `_PHASE_ROLES` / `_PHASE_REVIEWERS`. The trust-boundary scaling delta paragraph at :61 quantifies the R1 multiplier (15 subagents). The \"3 + 3 + 1 + 8 = 15\" math is correct if \"refine reviewers\" is read as \"refine producers + reviewers\" \u2014 the verbal label lumps the refiner in (15 = 1 refiner + 2 refine reviewers + 3 plan producers + 1 plan reviewer + 3 impl producers + 5 impl reviewers). The labeling is slightly imprecise but the count itself is right. **Strong.**\n\n3. **Constraints (\u00a7 :63-90)** \u2014 Architectural / operational / dependency split is clean. Inherited cq-1..cq-12 from #2623 are correctly carried as settled-not-re-litigated. The #2261 slice-15 dependency call-out at :88 is a genuine coordination risk \u2014 the rollout's `run_pipeline_in_process` plan/implement/pr wiring will touch the same `_run_loop/` cluster that #2261's terminal slice is decomposing. Worth surfacing this prominently in the plan-phase. **Strong.**\n\n4. **Options Considered (\u00a7 :92-153)** \u2014 Four slice-shape options (A linear / B parallel / C sequential-then-parallel-hardening / D defer hardening) are meaningfully distinct and reasonably exhaustive. Bridge-gap sub-options (a daemon / b stage scripts / c hybrid) are orthogonal as advertised and correctly registered as cq-1. Pros / cons are stated concretely (PR count, blast radius, BRC stress-test timing, slice-DAG width). **Strong.**\n\n5. **Recommended Approach (\u00a7 :155-171)** \u2014 Option C + Option (c) hybrid is internally consistent with the analysis's findings (bridge is long-pole; plan-phase BRC is the right first stress test; pr+conformance share a slice; hardening parallel saves time). Five-bullet justification is specific. The \"provisional \u2014 depends on cq-1 / cq-2 / cq-3\" caveat at :171 is honest and correct \u2014 the operator's resolution on those three changes the slice plan materially. **Strong.**\n\n6. **Open Questions (\u00a7 :173-217)** \u2014 7 decision questions + 6 feedback questions, all cross-referenced to the contract. Decisions are specific (default cost-cap value, R2 validation timing, k3s adapter scope, R15 model timing); feedback covers genuinely open items (specific conformance issues, latency budget, EggHarnessSpawner scope, multi-role-scale trust mitigations, reviewer-roster expansion, scope-fence behavior post-rollout). **Strong.**\n\n7. **HITL Registration (cross-check against `egg-contract show`)** \u2014 All 7 cq-* decisions exist in the contract with option labels matching the analysis's \"Decision questions\" section. All 6 feedback questions in `feedback-1`. No prose-only open questions were left unregistered. **Verified.**\n\n### Non-blocking\n\n- **`.md:61` \u2014 subagent count breakdown labeling.** \"3 + 3 + 1 + 8 = 15 (refine reviewers + plan producers/reviewer + implement producers/reviewers)\" lumps the refiner in with \"refine reviewers\" (which on the phase table at :51 is 2 not 3). The total 15 is right; the label should be \"refine roles\" or split as \"1 refiner + 2 refine reviewers + 3 plan producers + 1 plan reviewer + 8 implement\". Trivial fix; not blocking.\n\n- **`.md:200-206` \u2014 `cq-N` namespace collision with #2623.** This issue's net-new decisions are numbered cq-1..cq-7. Issue #2623's settled decisions were also numbered cq-1..cq-12. The analysis tries to disambiguate by separating \"Resolved in pre-refine\" from \"Decision questions\", but an operator reading just the contract IDs sees \"cq-1\" and can't tell which namespace it belongs to. Optional improvement: renumber this issue's decisions as cq-21..cq-27 (or prefix with `2717-`) to avoid the collision. Non-blocking \u2014 the contract IDs are stable once the operator answers them.\n\n- **Feedback Q1 suggested conformance issues** \u2014 #2714 is named as the suggested \"bug\" but #2714 has been fixed in #2719 (merged) per recent git log. That's actually fine for a conformance test (the substrate has to reproduce the fix), but worth confirming with the operator that \"bug already merged on `main`\" is the desired shape for the conformance bug-issue rather than \"bug still open at the time of running the matrix\". Trivially answerable in the operator's reply to Q1.\n\n- **R2 validation gating (cq-5).** The analysis correctly surfaces this as a decision but the recommendation section does not explicitly say which option it prefers. Defensible (the operator picks); could optionally add \"the rollout's bridge-first sequencing naturally puts R2 inside the plan-phase slice (cq-5 option 2), so if a hard preference is needed that's the default\" to give the operator a steer.\n\n- **`docs/architecture/claude-code-substrate.md:272-287` \"Rollout deltas\" reference** \u2014 referenced at :20 as having 11 deferred items. Quick sanity-check would be useful in the next iteration (mid-cycle re-propose isn't needed for this) but the reference is consistent with the ADR's intended structure.\n", - "metadata": { - "payload": { - "artifact_references": [ - ".egg-state/drafts/2717-analysis.md", - ".egg-state/contracts/issue-2717.json" - ], - "reason": "\nReviewed `.egg-state/drafts/2717-analysis.md` (221 lines, commit `e529722349`) and cross-verified `.egg-state/contracts/issue-2717.json` against the analysis's open-question inventory. Verified all 7 HITL decisions (cq-1..cq-7) and 6 feedback questions (Q1..Q6) are registered with substantive option labels. Verified file-path / line-range citations against the actual codebase (e.g. `shared/egg_contracts/agent_roles.py:1172-1209` for `_PHASE_ROLES` / `_PHASE_REVIEWERS`, `orchestrator/substrate/__init__.py:77-94` for `select_substrate`, `orchestrator/substrate/claude_code/spawner.py:9-26` for the spike-scope harness re-host comment). No blocking issues found.\n\n### Section-by-section evaluation\n\n1. **Problem Statement (\u00a7 Problem Statement, :5-20)** \u2014 Anchors the work in cq-11 (\"spike then plan\") and correctly characterizes #2715 as the spike + this issue as the re-plan. The list of spike-shipped artifacts (Protocol shims, claude-code implementations, K3sSpawnerAdapter, plugin scaffold, ADR, smoke test) matches what is actually in-tree. Desired outcome ties cleanly to the 6 DoD criteria from the issue body. **Strong.**\n\n2. **Current Behavior (\u00a7 :22-62)** \u2014 Excellent split between \"works today\" and \"doesn't work today\" with specific file:line anchors. The critical scope note at :28 (harness re-host vs Agent-tool dispatch) is exactly the right level of detail to surface as a decision and is correctly registered as cq-3. The phase-roles table at :49-55 matches `_PHASE_ROLES` / `_PHASE_REVIEWERS`. The trust-boundary scaling delta paragraph at :61 quantifies the R1 multiplier (15 subagents). The \"3 + 3 + 1 + 8 = 15\" math is correct if \"refine reviewers\" is read as \"refine producers + reviewers\" \u2014 the verbal label lumps the refiner in (15 = 1 refiner + 2 refine reviewers + 3 plan producers + 1 plan reviewer + 3 impl producers + 5 impl reviewers). The labeling is slightly imprecise but the count itself is right. **Strong.**\n\n3. **Constraints (\u00a7 :63-90)** \u2014 Architectural / operational / dependency split is clean. Inherited cq-1..cq-12 from #2623 are correctly carried as settled-not-re-litigated. The #2261 slice-15 dependency call-out at :88 is a genuine coordination risk \u2014 the rollout's `run_pipeline_in_process` plan/implement/pr wiring will touch the same `_run_loop/` cluster that #2261's terminal slice is decomposing. Worth surfacing this prominently in the plan-phase. **Strong.**\n\n4. **Options Considered (\u00a7 :92-153)** \u2014 Four slice-shape options (A linear / B parallel / C sequential-then-parallel-hardening / D defer hardening) are meaningfully distinct and reasonably exhaustive. Bridge-gap sub-options (a daemon / b stage scripts / c hybrid) are orthogonal as advertised and correctly registered as cq-1. Pros / cons are stated concretely (PR count, blast radius, BRC stress-test timing, slice-DAG width). **Strong.**\n\n5. **Recommended Approach (\u00a7 :155-171)** \u2014 Option C + Option (c) hybrid is internally consistent with the analysis's findings (bridge is long-pole; plan-phase BRC is the right first stress test; pr+conformance share a slice; hardening parallel saves time). Five-bullet justification is specific. The \"provisional \u2014 depends on cq-1 / cq-2 / cq-3\" caveat at :171 is honest and correct \u2014 the operator's resolution on those three changes the slice plan materially. **Strong.**\n\n6. **Open Questions (\u00a7 :173-217)** \u2014 7 decision questions + 6 feedback questions, all cross-referenced to the contract. Decisions are specific (default cost-cap value, R2 validation timing, k3s adapter scope, R15 model timing); feedback covers genuinely open items (specific conformance issues, latency budget, EggHarnessSpawner scope, multi-role-scale trust mitigations, reviewer-roster expansion, scope-fence behavior post-rollout). **Strong.**\n\n7. **HITL Registration (cross-check against `egg-contract show`)** \u2014 All 7 cq-* decisions exist in the contract with option labels matching the analysis's \"Decision questions\" section. All 6 feedback questions in `feedback-1`. No prose-only open questions were left unregistered. **Verified.**\n\n### Non-blocking\n\n- **`.md:61` \u2014 subagent count breakdown labeling.** \"3 + 3 + 1 + 8 = 15 (refine reviewers + plan producers/reviewer + implement producers/reviewers)\" lumps the refiner in with \"refine reviewers\" (which on the phase table at :51 is 2 not 3). The total 15 is right; the label should be \"refine roles\" or split as \"1 refiner + 2 refine reviewers + 3 plan producers + 1 plan reviewer + 8 implement\". Trivial fix; not blocking.\n\n- **`.md:200-206` \u2014 `cq-N` namespace collision with #2623.** This issue's net-new decisions are numbered cq-1..cq-7. Issue #2623's settled decisions were also numbered cq-1..cq-12. The analysis tries to disambiguate by separating \"Resolved in pre-refine\" from \"Decision questions\", but an operator reading just the contract IDs sees \"cq-1\" and can't tell which namespace it belongs to. Optional improvement: renumber this issue's decisions as cq-21..cq-27 (or prefix with `2717-`) to avoid the collision. Non-blocking \u2014 the contract IDs are stable once the operator answers them.\n\n- **Feedback Q1 suggested conformance issues** \u2014 #2714 is named as the suggested \"bug\" but #2714 has been fixed in #2719 (merged) per recent git log. That's actually fine for a conformance test (the substrate has to reproduce the fix), but worth confirming with the operator that \"bug already merged on `main`\" is the desired shape for the conformance bug-issue rather than \"bug still open at the time of running the matrix\". Trivially answerable in the operator's reply to Q1.\n\n- **R2 validation gating (cq-5).** The analysis correctly surfaces this as a decision but the recommendation section does not explicitly say which option it prefers. Defensible (the operator picks); could optionally add \"the rollout's bridge-first sequencing naturally puts R2 inside the plan-phase slice (cq-5 option 2), so if a hard preference is needed that's the default\" to give the operator a steer.\n\n- **`docs/architecture/claude-code-substrate.md:272-287` \"Rollout deltas\" reference** \u2014 referenced at :20 as having 11 deferred items. Quick sanity-check would be useful in the next iteration (mid-cycle re-propose isn't needed for this) but the reference is consistent with the ADR's intended structure.\n", - "ack_version": 1 - }, - "version": 1 - }, - "timestamp": "2026-05-19T17:23:47.648467+00:00", - "phase": "refine" - }, - { - "id": "eb495343-a57d-41", - "pipeline_id": "issue-2717", - "from_role": "orchestrator", - "to_role": "refiner", - "message_type": "STATUS", - "subject": "Ready to confirm \u2014 all confirm preconditions satisfied", - "body": "Your proposal (version 1) is ready to confirm \u2014 all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm.", - "metadata": { - "ready_to_confirm": true, - "version": 1 - }, - "timestamp": "2026-05-19T17:23:47.648595+00:00", - "phase": "refine" - }, - { - "id": "15ddbed2-0cd3-40", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T17:23:47.685004+00:00", - "phase": "refine" - }, - { - "id": "59dd531c-1c3b-4a", - "pipeline_id": "issue-2717", - "from_role": "refiner", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by refiner", - "body": "", - "metadata": { - "consensus_reached": false - }, - "timestamp": "2026-05-19T17:23:51.060476+00:00", - "phase": "refine" - }, - { - "id": "9bb713f5-febd-4e", - "pipeline_id": "issue-2717", - "from_role": "reviewer_agent_design", - "to_role": "all", - "message_type": "HEARTBEAT", - "subject": "heartbeat: WORKING", - "body": "wait_loop exited", - "metadata": { - "state": "WORKING" - }, - "timestamp": "2026-05-19T17:23:51.140287+00:00", - "phase": "refine" - }, - { - "id": "2ed302c1-0ae5-44", - "pipeline_id": "issue-2717", - "from_role": "reviewer_refine", - "to_role": "all", - "message_type": "CONSENSUS_CONFIRMED", - "subject": "Confirmed by reviewer_refine", - "body": "", - "metadata": { - "consensus_reached": true - }, - "timestamp": "2026-05-19T17:23:51.342020+00:00", - "phase": "refine" - } -] \ No newline at end of file diff --git a/.egg-state/brc-history/2717-refine.md b/.egg-state/brc-history/2717-refine.md deleted file mode 100644 index e13b289c11..0000000000 --- a/.egg-state/brc-history/2717-refine.md +++ /dev/null @@ -1,694 +0,0 @@ -# BRC Consensus History — refine phase - -Generated: 2026-05-19T17:23:51Z -Pipeline: issue-2717 - -### [2026-05-19T17:10:22Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 715599e5-aa0f-40 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:10:23Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 308156fa-6e0f-43 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:11:45Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: facfde93-d1aa-47 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:11:45Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 0f583b37-8881-45 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:12:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 51058cc3-8a78-4d -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:12:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7413a360-feea-48 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:13:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7781de15-0998-49 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:13:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 553322f7-5a74-4a -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:14:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b825951a-8b55-43 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:14:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 26aa9d8b-69b6-44 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:15:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 4383e465-5f62-49 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:15:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: afa292d9-03be-4d -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:16:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: e118bc1f-1dda-48 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:16:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 928861fe-89a2-48 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:17:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 52de0575-8838-4c -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:17:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: b6e4358e-2f16-4a -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:18:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: d58868bb-d7d3-42 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:18:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: a6040b24-0734-49 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:19:38Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 3da916df-5fb8-4f -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:13.545929+00:00' -```` - -### [2026-05-19T17:19:38Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 53ae00fe-e9d5-46 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:10:23.479786+00:00' -```` - -### [2026-05-19T17:20:27Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE - -````yaml -id: 7c39d8c4-929f-4d -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:20:27.122947+00:00' -```` - -### [2026-05-19T17:20:28Z] refiner (CONSENSUS_PROPOSE): Proposal from refiner - -Refine analysis for #2717 substrate-swap rollout. Documents the spike's current end-state (refine-only, refiner-only, harness re-host in-process; no skill driver bridges generator-yields to AskUserQuestion; only refiner rubric ships); enumerates the role rosters for plan/implement/pr the rollout has to wire; surfaces 7 net-new HITL decisions (cq-1 bridge-gap design Option-(a)-daemon vs Option-(b)-stage-scripts vs hybrid; cq-2 slice-DAG shape; cq-3 ClaudeCodeSpawner harness-rehost vs Agent-tool dispatcher migration; cq-4 R15 model-(a)/(b) timing; cq-5 R2 PreToolUse hook empirical-validation timing; cq-6 EGG_PIPELINE_MAX_AGENT_INVOCATIONS default; cq-7 k3s adapter parity scope) and 6 open-ended feedback questions (curated 5-issue conformance set specifics, latency/cost budget, EggHarnessSpawner scope, multi-role R1 mitigations, reviewer-roster expansion, post-rollout scope-fence). Recommends Option-C slice shape (bridge → plan → implement → pr+conformance → parallel hardening) with bridge Option-(c) hybrid. - -````yaml -id: 9a3bae4a-9c1b-40 -phase: refine -metadata: - payload: - summary: "Refine analysis for #2717 substrate-swap rollout. Documents the spike's\ - \ current end-state (refine-only, refiner-only, harness re-host in-process;\ - \ no skill driver bridges generator-yields to AskUserQuestion; only refiner\ - \ rubric ships); enumerates the role rosters for plan/implement/pr the rollout\ - \ has to wire; surfaces 7 net-new HITL decisions (cq-1 bridge-gap design Option-(a)-daemon\ - \ vs Option-(b)-stage-scripts vs hybrid; cq-2 slice-DAG shape; cq-3 ClaudeCodeSpawner\ - \ harness-rehost vs Agent-tool dispatcher migration; cq-4 R15 model-(a)/(b)\ - \ timing; cq-5 R2 PreToolUse hook empirical-validation timing; cq-6 EGG_PIPELINE_MAX_AGENT_INVOCATIONS\ - \ default; cq-7 k3s adapter parity scope) and 6 open-ended feedback questions\ - \ (curated 5-issue conformance set specifics, latency/cost budget, EggHarnessSpawner\ - \ scope, multi-role R1 mitigations, reviewer-roster expansion, post-rollout\ - \ scope-fence). Recommends Option-C slice shape (bridge \u2192 plan \u2192 implement\ - \ \u2192 pr+conformance \u2192 parallel hardening) with bridge Option-(c) hybrid." - attestation: {} - artifacts: - - .egg-state/drafts/2717-analysis.md - risk_considered: "Risk: surfacing too many open questions overwhelms operator.\ - \ Mitigated by separating cq-1..cq-7 multi-choice decisions (each with concrete\ - \ options + tradeoffs) from open-ended feedback Q1-Q6; the inherited cq-1..cq-12\ - \ from #2623 are explicitly noted as 'Resolved in pre-refine' to avoid re-litigation.\ - \ Risk: recommending Option-C provisionally before cq-1/cq-2/cq-3 resolve \u2014\ - \ analysis documents the dependency explicitly so operator knows the recommendation\ - \ may shift." - commit_sha: e529722349e5b193b21d27ef43c0d35182f4799c - files_changed: - - .egg-state/drafts/2717-analysis.md - tests_run: [] - tasks_satisfied: [] - version: 1 - commit_sha: e529722349e5b193b21d27ef43c0d35182f4799c -```` - -### [2026-05-19T17:20:28Z] reviewer_agent_design (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: b9b6c68a-f656-44 -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-19T17:20:28Z] reviewer_refine (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 9aeab0b8-67c4-47 -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-19T17:20:56Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: e0cea532-1184-42 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:20:39.468415+00:00' -```` - -### [2026-05-19T17:21:54Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 06a12521-7404-4d -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:20:39.468415+00:00' -```` - -### [2026-05-19T17:22:21Z] reviewer_agent_design → refiner (CONSENSUS_ACK): ACK from reviewer_agent_design for refiner - - -Reviewed `.egg-state/drafts/2717-analysis.md` end-to-end through the agent-design lens against `docs/guides/agent-mode-design.md` and the agent-mode-design review criteria. The draft is well-aligned with agent-mode design; no blocking concerns. - -### What I checked and why it lands clean - -1. **Policy enforcement is sandbox-anchored, not prompt-level.** "Constraints → Architectural" (lines 70-79) reiterates that PreToolUse hooks (cq-6) are the load-bearing seam and that `shared/egg_restrictions/patterns.py` is the single source of truth for both the hook and the MCP-validator fallback. R2 (hook role-resolution under nested dispatch) is correctly flagged as an empirical question with cq-6 option 2 (MCP-validator-side enforcement) named as the structural fallback if hooks can't resolve roles — neither path is "tell the agent not to do X in the prompt." This is exactly the pattern the agent-design guidance asks for. - -2. **R15 model (a) → model (b) migration is treated seriously, not hand-waved.** cq-4 in "Open Questions" (line 203) registers when to migrate from ad-hoc spawner-assembled prompts to `.claude/agents/.md` definitions with structural tool restrictions — that's the correct framing (structural enforcement preferred; prompt discipline is a stop-gap, not a destination). - -3. **Subagent dispatch direction is toward the Agent tool, not raw API calls.** The "Critical scope note" (line 28) names that today's `ClaudeCodeSpawner` re-hosts `egg_harness.run_agent` in-process and reserves `ClaudeCodeAgentToolSpawner` for the real `Agent` tool with `subagent_type="general-purpose"` dispatch. cq-3 (line 202) registers when to migrate. No proposal to bypass the Agent SDK / Agent tool with raw HTTP appears anywhere. - -4. **HITL surface is `AskUserQuestion`-native.** cq-7 (line 67) is reaffirmed; the bridge gap analysis ("Current Behavior → What does NOT work today", line 36) frames the missing piece as "no skill driver can ferry `HITLDecision` to `AskUserQuestion`," not "let's parse agent output and act on it." Bridge Options (a/b/c) on lines 149-153 are about persisting operator state across stages / generator-resumability — they are NOT agent-output post-processing pipelines (the contract JSON holds *operator answers*, not agent output the orchestrator parses to take actions). - -5. **No pre-fetched diffs / file contents baked into prompts.** The analysis discusses prompts at the structural level (rubric depth, `build_system_prompt(sources)` reaching every new role rubric — line 73) without proposing to bake repo state into them. Role rubric files are role-orientation context, not pre-fetched task context — that's fine per the philosophy section. - -6. **No structured output for humans, no hardcoded model IDs, no rigid procedure micromanagement.** The "Recommended Approach" stays at slice-ordering granularity (line 157-167); it does not prescribe role-prompt step-by-step procedures. - -7. **Cost-cap (REC5) at 15-subagents-per-pipeline scale is appropriately escalated.** "Operational constraints" (line 82) correctly recognizes the scaling delta from the spike's 1 subagent to the rollout's 15 changes the urgency calculus on `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` — that's a real safety property of the substrate, not a prompt-level concern. - -### Non-blocking - -- **Line 28 / cq-3** — The harness-re-host vs Agent-tool-dispatch distinction is the most consequential agent-design choice in the rollout. The current draft surfaces it as one of seven cq questions; consider letting the plan phase weight it explicitly when assembling slice order, because Option C's plan-phase slice has different shape under cq-3 = "stay" (no new spawner) vs cq-3 = "migrate now" (new spawner is itself a slice). Not blocking — the recommendation already calls out provisionality on line 171. -- **Line 156-167** — Bridge Option (c) hybrid is the right call agent-design-wise (operator-state persistence chosen per-phase based on cost gradient, not a single shape forced everywhere). Worth a one-line note in the recommendation that Option (b)'s `.egg-state/contracts/.json`-threaded answer flow does NOT involve the orchestrator parsing agent output — it's pure operator-input persistence — so a future reviewer doesn't mistake it for a post-processing pipeline anti-pattern. -- **Line 215 / Q5** — The "any new reviewer roles" question (`reviewer_substrate`, `reviewer_concurrency_substrate`) is fine to ask, but adding a substrate-specific reviewer role would itself be an agent-design choice: it conflates "what the code does" (substrate-aware) with "what role reviews it" (single producer concern). The cleaner shape is teaching existing reviewers about the substrate axis via the rubric, not minting a new role. Operator's call; calling out the agent-design preference here. - - -````yaml -id: 187f809e-c9a8-42 -phase: refine -metadata: - payload: - artifact_references: - - .egg-state/drafts/2717-analysis.md - reason: "\nReviewed `.egg-state/drafts/2717-analysis.md` end-to-end through the\ - \ agent-design lens against `docs/guides/agent-mode-design.md` and the agent-mode-design\ - \ review criteria. The draft is well-aligned with agent-mode design; no blocking\ - \ concerns.\n\n### What I checked and why it lands clean\n\n1. **Policy enforcement\ - \ is sandbox-anchored, not prompt-level.** \"Constraints \u2192 Architectural\"\ - \ (lines 70-79) reiterates that PreToolUse hooks (cq-6) are the load-bearing\ - \ seam and that `shared/egg_restrictions/patterns.py` is the single source of\ - \ truth for both the hook and the MCP-validator fallback. R2 (hook role-resolution\ - \ under nested dispatch) is correctly flagged as an empirical question with\ - \ cq-6 option 2 (MCP-validator-side enforcement) named as the structural fallback\ - \ if hooks can't resolve roles \u2014 neither path is \"tell the agent not to\ - \ do X in the prompt.\" This is exactly the pattern the agent-design guidance\ - \ asks for.\n\n2. **R15 model (a) \u2192 model (b) migration is treated seriously,\ - \ not hand-waved.** cq-4 in \"Open Questions\" (line 203) registers when to\ - \ migrate from ad-hoc spawner-assembled prompts to `.claude/agents/.md`\ - \ definitions with structural tool restrictions \u2014 that's the correct framing\ - \ (structural enforcement preferred; prompt discipline is a stop-gap, not a\ - \ destination).\n\n3. **Subagent dispatch direction is toward the Agent tool,\ - \ not raw API calls.** The \"Critical scope note\" (line 28) names that today's\ - \ `ClaudeCodeSpawner` re-hosts `egg_harness.run_agent` in-process and reserves\ - \ `ClaudeCodeAgentToolSpawner` for the real `Agent` tool with `subagent_type=\"\ - general-purpose\"` dispatch. cq-3 (line 202) registers when to migrate. No proposal\ - \ to bypass the Agent SDK / Agent tool with raw HTTP appears anywhere.\n\n4.\ - \ **HITL surface is `AskUserQuestion`-native.** cq-7 (line 67) is reaffirmed;\ - \ the bridge gap analysis (\"Current Behavior \u2192 What does NOT work today\"\ - , line 36) frames the missing piece as \"no skill driver can ferry `HITLDecision`\ - \ to `AskUserQuestion`,\" not \"let's parse agent output and act on it.\" Bridge\ - \ Options (a/b/c) on lines 149-153 are about persisting operator state across\ - \ stages / generator-resumability \u2014 they are NOT agent-output post-processing\ - \ pipelines (the contract JSON holds *operator answers*, not agent output the\ - \ orchestrator parses to take actions).\n\n5. **No pre-fetched diffs / file\ - \ contents baked into prompts.** The analysis discusses prompts at the structural\ - \ level (rubric depth, `build_system_prompt(sources)` reaching every new role\ - \ rubric \u2014 line 73) without proposing to bake repo state into them. Role\ - \ rubric files are role-orientation context, not pre-fetched task context \u2014\ - \ that's fine per the philosophy section.\n\n6. **No structured output for humans,\ - \ no hardcoded model IDs, no rigid procedure micromanagement.** The \"Recommended\ - \ Approach\" stays at slice-ordering granularity (line 157-167); it does not\ - \ prescribe role-prompt step-by-step procedures.\n\n7. **Cost-cap (REC5) at\ - \ 15-subagents-per-pipeline scale is appropriately escalated.** \"Operational\ - \ constraints\" (line 82) correctly recognizes the scaling delta from the spike's\ - \ 1 subagent to the rollout's 15 changes the urgency calculus on `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`\ - \ \u2014 that's a real safety property of the substrate, not a prompt-level\ - \ concern.\n\n### Non-blocking\n\n- **Line 28 / cq-3** \u2014 The harness-re-host\ - \ vs Agent-tool-dispatch distinction is the most consequential agent-design\ - \ choice in the rollout. The current draft surfaces it as one of seven cq questions;\ - \ consider letting the plan phase weight it explicitly when assembling slice\ - \ order, because Option C's plan-phase slice has different shape under cq-3\ - \ = \"stay\" (no new spawner) vs cq-3 = \"migrate now\" (new spawner is itself\ - \ a slice). Not blocking \u2014 the recommendation already calls out provisionality\ - \ on line 171.\n- **Line 156-167** \u2014 Bridge Option (c) hybrid is the right\ - \ call agent-design-wise (operator-state persistence chosen per-phase based\ - \ on cost gradient, not a single shape forced everywhere). Worth a one-line\ - \ note in the recommendation that Option (b)'s `.egg-state/contracts/.json`-threaded\ - \ answer flow does NOT involve the orchestrator parsing agent output \u2014\ - \ it's pure operator-input persistence \u2014 so a future reviewer doesn't mistake\ - \ it for a post-processing pipeline anti-pattern.\n- **Line 215 / Q5** \u2014\ - \ The \"any new reviewer roles\" question (`reviewer_substrate`, `reviewer_concurrency_substrate`)\ - \ is fine to ask, but adding a substrate-specific reviewer role would itself\ - \ be an agent-design choice: it conflates \"what the code does\" (substrate-aware)\ - \ with \"what role reviews it\" (single producer concern). The cleaner shape\ - \ is teaching existing reviewers about the substrate axis via the rubric, not\ - \ minting a new role. Operator's call; calling out the agent-design preference\ - \ here.\n" - ack_version: 1 - version: 1 -```` - -### [2026-05-19T17:22:21Z] refiner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 59dd831a-a23d-48 -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-19T17:22:24Z] reviewer_agent_design (CONSENSUS_CONFIRMED): Confirmed by reviewer_agent_design - -````yaml -id: 5fa72f5f-5989-49 -phase: refine -metadata: - consensus_reached: false -```` - -### [2026-05-19T17:22:30Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: 9a9e9659-b8f3-4c -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:22:29.979402+00:00' -```` - -### [2026-05-19T17:22:30Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: 4fbbc878-fad1-4a -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:22:30.030734+00:00' -```` - -### [2026-05-19T17:23:30Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT - -````yaml -id: ac056bf7-d515-4e -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:22:29.979402+00:00' -```` - -### [2026-05-19T17:23:30Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT - -wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT - -````yaml -id: fe20b943-03bd-40 -phase: refine -metadata: - state: WAITING_FOR_EVENT - since: '2026-05-19T17:22:30.030734+00:00' -```` - -### [2026-05-19T17:23:47Z] reviewer_refine → refiner (CONSENSUS_ACK): ACK from reviewer_refine for refiner - - -Reviewed `.egg-state/drafts/2717-analysis.md` (221 lines, commit `e529722349`) and cross-verified `.egg-state/contracts/issue-2717.json` against the analysis's open-question inventory. Verified all 7 HITL decisions (cq-1..cq-7) and 6 feedback questions (Q1..Q6) are registered with substantive option labels. Verified file-path / line-range citations against the actual codebase (e.g. `shared/egg_contracts/agent_roles.py:1172-1209` for `_PHASE_ROLES` / `_PHASE_REVIEWERS`, `orchestrator/substrate/__init__.py:77-94` for `select_substrate`, `orchestrator/substrate/claude_code/spawner.py:9-26` for the spike-scope harness re-host comment). No blocking issues found. - -### Section-by-section evaluation - -1. **Problem Statement (§ Problem Statement, :5-20)** — Anchors the work in cq-11 ("spike then plan") and correctly characterizes #2715 as the spike + this issue as the re-plan. The list of spike-shipped artifacts (Protocol shims, claude-code implementations, K3sSpawnerAdapter, plugin scaffold, ADR, smoke test) matches what is actually in-tree. Desired outcome ties cleanly to the 6 DoD criteria from the issue body. **Strong.** - -2. **Current Behavior (§ :22-62)** — Excellent split between "works today" and "doesn't work today" with specific file:line anchors. The critical scope note at :28 (harness re-host vs Agent-tool dispatch) is exactly the right level of detail to surface as a decision and is correctly registered as cq-3. The phase-roles table at :49-55 matches `_PHASE_ROLES` / `_PHASE_REVIEWERS`. The trust-boundary scaling delta paragraph at :61 quantifies the R1 multiplier (15 subagents). The "3 + 3 + 1 + 8 = 15" math is correct if "refine reviewers" is read as "refine producers + reviewers" — the verbal label lumps the refiner in (15 = 1 refiner + 2 refine reviewers + 3 plan producers + 1 plan reviewer + 3 impl producers + 5 impl reviewers). The labeling is slightly imprecise but the count itself is right. **Strong.** - -3. **Constraints (§ :63-90)** — Architectural / operational / dependency split is clean. Inherited cq-1..cq-12 from #2623 are correctly carried as settled-not-re-litigated. The #2261 slice-15 dependency call-out at :88 is a genuine coordination risk — the rollout's `run_pipeline_in_process` plan/implement/pr wiring will touch the same `_run_loop/` cluster that #2261's terminal slice is decomposing. Worth surfacing this prominently in the plan-phase. **Strong.** - -4. **Options Considered (§ :92-153)** — Four slice-shape options (A linear / B parallel / C sequential-then-parallel-hardening / D defer hardening) are meaningfully distinct and reasonably exhaustive. Bridge-gap sub-options (a daemon / b stage scripts / c hybrid) are orthogonal as advertised and correctly registered as cq-1. Pros / cons are stated concretely (PR count, blast radius, BRC stress-test timing, slice-DAG width). **Strong.** - -5. **Recommended Approach (§ :155-171)** — Option C + Option (c) hybrid is internally consistent with the analysis's findings (bridge is long-pole; plan-phase BRC is the right first stress test; pr+conformance share a slice; hardening parallel saves time). Five-bullet justification is specific. The "provisional — depends on cq-1 / cq-2 / cq-3" caveat at :171 is honest and correct — the operator's resolution on those three changes the slice plan materially. **Strong.** - -6. **Open Questions (§ :173-217)** — 7 decision questions + 6 feedback questions, all cross-referenced to the contract. Decisions are specific (default cost-cap value, R2 validation timing, k3s adapter scope, R15 model timing); feedback covers genuinely open items (specific conformance issues, latency budget, EggHarnessSpawner scope, multi-role-scale trust mitigations, reviewer-roster expansion, scope-fence behavior post-rollout). **Strong.** - -7. **HITL Registration (cross-check against `egg-contract show`)** — All 7 cq-* decisions exist in the contract with option labels matching the analysis's "Decision questions" section. All 6 feedback questions in `feedback-1`. No prose-only open questions were left unregistered. **Verified.** - -### Non-blocking - -- **`.md:61` — subagent count breakdown labeling.** "3 + 3 + 1 + 8 = 15 (refine reviewers + plan producers/reviewer + implement producers/reviewers)" lumps the refiner in with "refine reviewers" (which on the phase table at :51 is 2 not 3). The total 15 is right; the label should be "refine roles" or split as "1 refiner + 2 refine reviewers + 3 plan producers + 1 plan reviewer + 8 implement". Trivial fix; not blocking. - -- **`.md:200-206` — `cq-N` namespace collision with #2623.** This issue's net-new decisions are numbered cq-1..cq-7. Issue #2623's settled decisions were also numbered cq-1..cq-12. The analysis tries to disambiguate by separating "Resolved in pre-refine" from "Decision questions", but an operator reading just the contract IDs sees "cq-1" and can't tell which namespace it belongs to. Optional improvement: renumber this issue's decisions as cq-21..cq-27 (or prefix with `2717-`) to avoid the collision. Non-blocking — the contract IDs are stable once the operator answers them. - -- **Feedback Q1 suggested conformance issues** — #2714 is named as the suggested "bug" but #2714 has been fixed in #2719 (merged) per recent git log. That's actually fine for a conformance test (the substrate has to reproduce the fix), but worth confirming with the operator that "bug already merged on `main`" is the desired shape for the conformance bug-issue rather than "bug still open at the time of running the matrix". Trivially answerable in the operator's reply to Q1. - -- **R2 validation gating (cq-5).** The analysis correctly surfaces this as a decision but the recommendation section does not explicitly say which option it prefers. Defensible (the operator picks); could optionally add "the rollout's bridge-first sequencing naturally puts R2 inside the plan-phase slice (cq-5 option 2), so if a hard preference is needed that's the default" to give the operator a steer. - -- **`docs/architecture/claude-code-substrate.md:272-287` "Rollout deltas" reference** — referenced at :20 as having 11 deferred items. Quick sanity-check would be useful in the next iteration (mid-cycle re-propose isn't needed for this) but the reference is consistent with the ADR's intended structure. - - -````yaml -id: c20728e4-b824-4c -phase: refine -metadata: - payload: - artifact_references: - - .egg-state/drafts/2717-analysis.md - - .egg-state/contracts/issue-2717.json - reason: "\nReviewed `.egg-state/drafts/2717-analysis.md` (221 lines, commit `e529722349`)\ - \ and cross-verified `.egg-state/contracts/issue-2717.json` against the analysis's\ - \ open-question inventory. Verified all 7 HITL decisions (cq-1..cq-7) and 6\ - \ feedback questions (Q1..Q6) are registered with substantive option labels.\ - \ Verified file-path / line-range citations against the actual codebase (e.g.\ - \ `shared/egg_contracts/agent_roles.py:1172-1209` for `_PHASE_ROLES` / `_PHASE_REVIEWERS`,\ - \ `orchestrator/substrate/__init__.py:77-94` for `select_substrate`, `orchestrator/substrate/claude_code/spawner.py:9-26`\ - \ for the spike-scope harness re-host comment). No blocking issues found.\n\n\ - ### Section-by-section evaluation\n\n1. **Problem Statement (\xA7 Problem Statement,\ - \ :5-20)** \u2014 Anchors the work in cq-11 (\"spike then plan\") and correctly\ - \ characterizes #2715 as the spike + this issue as the re-plan. The list of\ - \ spike-shipped artifacts (Protocol shims, claude-code implementations, K3sSpawnerAdapter,\ - \ plugin scaffold, ADR, smoke test) matches what is actually in-tree. Desired\ - \ outcome ties cleanly to the 6 DoD criteria from the issue body. **Strong.**\n\ - \n2. **Current Behavior (\xA7 :22-62)** \u2014 Excellent split between \"works\ - \ today\" and \"doesn't work today\" with specific file:line anchors. The critical\ - \ scope note at :28 (harness re-host vs Agent-tool dispatch) is exactly the\ - \ right level of detail to surface as a decision and is correctly registered\ - \ as cq-3. The phase-roles table at :49-55 matches `_PHASE_ROLES` / `_PHASE_REVIEWERS`.\ - \ The trust-boundary scaling delta paragraph at :61 quantifies the R1 multiplier\ - \ (15 subagents). The \"3 + 3 + 1 + 8 = 15\" math is correct if \"refine reviewers\"\ - \ is read as \"refine producers + reviewers\" \u2014 the verbal label lumps\ - \ the refiner in (15 = 1 refiner + 2 refine reviewers + 3 plan producers + 1\ - \ plan reviewer + 3 impl producers + 5 impl reviewers). The labeling is slightly\ - \ imprecise but the count itself is right. **Strong.**\n\n3. **Constraints (\xA7\ - \ :63-90)** \u2014 Architectural / operational / dependency split is clean.\ - \ Inherited cq-1..cq-12 from #2623 are correctly carried as settled-not-re-litigated.\ - \ The #2261 slice-15 dependency call-out at :88 is a genuine coordination risk\ - \ \u2014 the rollout's `run_pipeline_in_process` plan/implement/pr wiring will\ - \ touch the same `_run_loop/` cluster that #2261's terminal slice is decomposing.\ - \ Worth surfacing this prominently in the plan-phase. **Strong.**\n\n4. **Options\ - \ Considered (\xA7 :92-153)** \u2014 Four slice-shape options (A linear / B\ - \ parallel / C sequential-then-parallel-hardening / D defer hardening) are meaningfully\ - \ distinct and reasonably exhaustive. Bridge-gap sub-options (a daemon / b stage\ - \ scripts / c hybrid) are orthogonal as advertised and correctly registered\ - \ as cq-1. Pros / cons are stated concretely (PR count, blast radius, BRC stress-test\ - \ timing, slice-DAG width). **Strong.**\n\n5. **Recommended Approach (\xA7 :155-171)**\ - \ \u2014 Option C + Option (c) hybrid is internally consistent with the analysis's\ - \ findings (bridge is long-pole; plan-phase BRC is the right first stress test;\ - \ pr+conformance share a slice; hardening parallel saves time). Five-bullet\ - \ justification is specific. The \"provisional \u2014 depends on cq-1 / cq-2\ - \ / cq-3\" caveat at :171 is honest and correct \u2014 the operator's resolution\ - \ on those three changes the slice plan materially. **Strong.**\n\n6. **Open\ - \ Questions (\xA7 :173-217)** \u2014 7 decision questions + 6 feedback questions,\ - \ all cross-referenced to the contract. Decisions are specific (default cost-cap\ - \ value, R2 validation timing, k3s adapter scope, R15 model timing); feedback\ - \ covers genuinely open items (specific conformance issues, latency budget,\ - \ EggHarnessSpawner scope, multi-role-scale trust mitigations, reviewer-roster\ - \ expansion, scope-fence behavior post-rollout). **Strong.**\n\n7. **HITL Registration\ - \ (cross-check against `egg-contract show`)** \u2014 All 7 cq-* decisions exist\ - \ in the contract with option labels matching the analysis's \"Decision questions\"\ - \ section. All 6 feedback questions in `feedback-1`. No prose-only open questions\ - \ were left unregistered. **Verified.**\n\n### Non-blocking\n\n- **`.md:61`\ - \ \u2014 subagent count breakdown labeling.** \"3 + 3 + 1 + 8 = 15 (refine reviewers\ - \ + plan producers/reviewer + implement producers/reviewers)\" lumps the refiner\ - \ in with \"refine reviewers\" (which on the phase table at :51 is 2 not 3).\ - \ The total 15 is right; the label should be \"refine roles\" or split as \"\ - 1 refiner + 2 refine reviewers + 3 plan producers + 1 plan reviewer + 8 implement\"\ - . Trivial fix; not blocking.\n\n- **`.md:200-206` \u2014 `cq-N` namespace collision\ - \ with #2623.** This issue's net-new decisions are numbered cq-1..cq-7. Issue\ - \ #2623's settled decisions were also numbered cq-1..cq-12. The analysis tries\ - \ to disambiguate by separating \"Resolved in pre-refine\" from \"Decision questions\"\ - , but an operator reading just the contract IDs sees \"cq-1\" and can't tell\ - \ which namespace it belongs to. Optional improvement: renumber this issue's\ - \ decisions as cq-21..cq-27 (or prefix with `2717-`) to avoid the collision.\ - \ Non-blocking \u2014 the contract IDs are stable once the operator answers\ - \ them.\n\n- **Feedback Q1 suggested conformance issues** \u2014 #2714 is named\ - \ as the suggested \"bug\" but #2714 has been fixed in #2719 (merged) per recent\ - \ git log. That's actually fine for a conformance test (the substrate has to\ - \ reproduce the fix), but worth confirming with the operator that \"bug already\ - \ merged on `main`\" is the desired shape for the conformance bug-issue rather\ - \ than \"bug still open at the time of running the matrix\". Trivially answerable\ - \ in the operator's reply to Q1.\n\n- **R2 validation gating (cq-5).** The analysis\ - \ correctly surfaces this as a decision but the recommendation section does\ - \ not explicitly say which option it prefers. Defensible (the operator picks);\ - \ could optionally add \"the rollout's bridge-first sequencing naturally puts\ - \ R2 inside the plan-phase slice (cq-5 option 2), so if a hard preference is\ - \ needed that's the default\" to give the operator a steer.\n\n- **`docs/architecture/claude-code-substrate.md:272-287`\ - \ \"Rollout deltas\" reference** \u2014 referenced at :20 as having 11 deferred\ - \ items. Quick sanity-check would be useful in the next iteration (mid-cycle\ - \ re-propose isn't needed for this) but the reference is consistent with the\ - \ ADR's intended structure.\n" - ack_version: 1 - version: 1 -```` - -### [2026-05-19T17:23:47Z] orchestrator → refiner (STATUS): Ready to confirm — all confirm preconditions satisfied - -Your proposal (version 1) is ready to confirm — all blocking reviews are clear and global confirm preconditions are met. Run `egg-orch consensus confirmed` to confirm. - -````yaml -id: eb495343-a57d-41 -phase: refine -metadata: - ready_to_confirm: true - version: 1 -```` - -### [2026-05-19T17:23:47Z] refiner (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 15ddbed2-0cd3-40 -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-19T17:23:51Z] refiner (CONSENSUS_CONFIRMED): Confirmed by refiner - -````yaml -id: 59dd531c-1c3b-4a -phase: refine -metadata: - consensus_reached: false -```` - -### [2026-05-19T17:23:51Z] reviewer_agent_design (HEARTBEAT): heartbeat: WORKING - -wait_loop exited - -````yaml -id: 9bb713f5-febd-4e -phase: refine -metadata: - state: WORKING -```` - -### [2026-05-19T17:23:51Z] reviewer_refine (CONSENSUS_CONFIRMED): Confirmed by reviewer_refine - -````yaml -id: 2ed302c1-0ae5-44 -phase: refine -metadata: - consensus_reached: true -```` diff --git a/.egg-state/contracts/issue-2623.json b/.egg-state/contracts/issue-2623.json deleted file mode 100644 index 528cc4a534..0000000000 --- a/.egg-state/contracts/issue-2623.json +++ /dev/null @@ -1,1435 +0,0 @@ -{ - "schemaVersion": "1.1", - "issue": { - "number": 2623, - "title": "Issue #2623", - "url": "https://github.com/jwbron/egg/issues/2623" - }, - "pipeline_id": "issue-2623", - "current_phase": "refine", - "acceptance_criteria": [], - "slices": [ - { - "id": "slice-1", - "name": "Walking-skeleton Claude Code substrate (refiner-only spike)", - "status": "pending", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "escalation_reason": null, - "tasks": [ - { - "id": "task-1-1", - "description": "Create `orchestrator/substrate/` with four\n`typing.Protocol` interfaces (`AgentSpawner`, `MessageBus`,\n`PolicyEnforcer`, `WorktreeManager`) plus a\n`select_substrate(env)` factory reading `EGG_SUBSTRATE`.\n`AgentSpawner.spawn` signature pinned by cq-4 (synchronous,\nblocking). `AgentResult` dataclass MUST include\n`commit_sha: str | None` to satisfy INV-6\n(`orchestrator/action_guards.py:631`, body at `:757`).\nAlso ship `orchestrator/substrate/k3s_adapter.py` \u2014 a\nworking `K3sSpawnerAdapter` wrapping\n`orchestrator/kubernetes_spawner.py:1564\ncreate_concurrent_spawn_fn` and capturing `commit_sha` via\n`git rev-parse HEAD` so the k3s leg of `select_substrate`\nreturns a working spawner from day one (cq-1 parallel-\nsubstrates). Module docstrings link to the ADR\n(TASK-1-9) and carry the `# v0.x unstable` marker (R10).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Module files exist\n(`__init__.py`, `spawner.py`, `message_bus.py`,\n`policy.py`, `worktree.py`, `k3s_adapter.py`);\n`AgentSpawner.spawn` matches cq-4; `AgentResult` includes\n`commit_sha: str | None`;\n`select_substrate({})` defaults to `\"k3s\"` and returns a\nworking `K3sSpawnerAdapter` wrapping\n`create_concurrent_spawn_fn`\n(`orchestrator/kubernetes_spawner.py:1564`);\n`select_substrate({\"EGG_SUBSTRATE\": \"claude-code\"})`\nreturns a bundle wired to the claude-code\nimplementations (TASK-1-2..1-5);\n`orchestrator/concurrent_executor.py`,\n`orchestrator/kubernetes_*.py`, and `gateway/` are NOT\nmodified by this task (the k3s adapter imports from\n`kubernetes_spawner.py` but does not patch the call\nsite).", - "files_affected": [ - "orchestrator/substrate/__init__.py", - "orchestrator/substrate/spawner.py", - "orchestrator/substrate/message_bus.py", - "orchestrator/substrate/policy.py", - "orchestrator/substrate/worktree.py", - "orchestrator/substrate/k3s_adapter.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-2", - "description": "Implement `ClaudeCodeSpawner` satisfying `AgentSpawner`\n(TASK-1-1). Blocks the caller, dispatches to Claude Code's\nsubagent surface via `shared/egg_harness`, returns an\n`AgentResult` once the subagent completes. Internal\nconcurrency is owned by the spawner per cq-4. Assembles\nthe system prompt via `build_system_prompt`\n(`shared/egg_harness/prompt.py:24`) \u2014 structural fix from\n#2622. Captures `commit_sha` via\n`git -C rev-parse HEAD` immediately after the\nsubagent returns and records it on `AgentResult.commit_sha`\n(INV-6). Also patches\n`orchestrator/concurrent_executor.py:504 _spawn_agent` to\ndispatch through\n`select_substrate(os.environ).spawner` \u2014 both substrate\nlegs are working (k3s via TASK-1-1's `K3sSpawnerAdapter`,\nclaude-code via this task's `ClaudeCodeSpawner`).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`ClaudeCodeSpawner` conforms to `AgentSpawner` (verified\nby isinstance check in a unit test);\n`spawn(role, prompt, env, worktree)` returns an\n`AgentResult` with `stdout`, `exit_code`,\n`duration_seconds`, `worktree`, and `commit_sha`\n(40-char hex captured via `git rev-parse HEAD`);\n`build_system_prompt` is invoked with the role's\n`PromptSource` list (verified by unit test);\n`orchestrator/concurrent_executor.py:504` dispatches via\n`select_substrate(...)`; a smoke test asserts the\n`_spawn_agent` dispatch routes through the bundle under\nboth `EGG_SUBSTRATE=k3s` (via `K3sSpawnerAdapter`) and\n`EGG_SUBSTRATE=claude-code` (via `ClaudeCodeSpawner`).", - "files_affected": [ - "orchestrator/substrate/claude_code/__init__.py", - "orchestrator/substrate/claude_code/spawner.py", - "orchestrator/concurrent_executor.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-3", - "description": "Implement `InProcessMessageBus` satisfying `MessageBus`\n(TASK-1-1) using Python dict / threading.Lock / queue.\nPreserves BRC invariants INV-3 (stale-version rejection)\nand INV-5 (open-NACK barrier) from\n`orchestrator/action_guards.py:631 validate_invariants`.\nMay subclass or delegate to\n`orchestrator/message_store.py:200 MessageStore`. The\nbehavioral oracle is the existing BRC test suite at\n`orchestrator/tests/test_brc_*.py` (7+ files including\n`test_brc_open_nacks_barrier.py` and\n`test_brc_content_validation.py`); TASK-1-8 ports the\nequivalent scenarios onto the new bus surface.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`InProcessMessageBus` conforms to `MessageBus`; INV-3 and\nINV-5 preserved (verified by TASK-1-8 unit tests at\n`shared/tests/test_in_process_message_bus.py` that mirror\nthe scenarios in\n`orchestrator/tests/test_brc_open_nacks_barrier.py` and\n`orchestrator/tests/test_brc_content_validation.py`); the\nbus operates without any Redis or k3s dependency.", - "files_affected": [ - "orchestrator/substrate/claude_code/message_bus.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-4", - "description": "Implement `PreToolUseHookPolicy` (`PolicyEnforcer`) plus a\nPython hook entry script\n(`orchestrator/substrate/claude_code/hook_entry.py`)\nreferenced from a `.claude/settings.json` template. The\nhook reads tool name + tool input from stdin (PreToolUse\ncontract), imports `build_agent_patterns` from\n`shared/egg_restrictions/patterns.py:768`, emits deny+\nmessage JSON when the write target is outside the role's\nallow-list. Allow/deny semantics match the gateway's\nexisting `check_agent_restrictions`\n(`gateway/phase_filter.py:1061`).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`PreToolUseHookPolicy` conforms to `PolicyEnforcer`;\n`hook_entry.py` is runnable as a script; a template\n`settings.template.json` shows the hook wired up; an\nout-of-role write yields a stdout JSON deny with a\ngateway-equivalent message (verified by unit test in\nTASK-1-8); the hook imports\n`shared/egg_restrictions/patterns.py:768\nbuild_agent_patterns` directly \u2014 no parallel logic.", - "files_affected": [ - "orchestrator/substrate/claude_code/policy.py", - "orchestrator/substrate/claude_code/hook_entry.py", - "orchestrator/substrate/claude_code/settings.template.json" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-5", - "description": "Implement `LocalWorktreeManager` satisfying\n`WorktreeManager` (TASK-1-1) using filesystem ops under\n`.egg-state///`. Default base mirrors\n`gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` shape\n(~/.egg-worktrees/) but `EGG_WORKTREE_BASE` overrides per\ncq-5. Creates per-agent worktrees with `git worktree add`;\ntracks them in a dict; tears them down at phase end.\nPath-escape safety mirrors the `is_relative_to` defense at\n`gateway/worktree_manager.py:1711` (call site within\n`_remove_worktree`; matching `base.resolve()` at\n`gateway/worktree_manager.py:1700`).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`LocalWorktreeManager` conforms to `WorktreeManager`;\n`create(pipeline_id, role)` returns a path under the\nconfigured base; `tear_down(pipeline_id)` removes all\nworktrees and cleans up git refs; default base matches\n`gateway/worktree_manager.py:49` when env var unset;\nescape attempts are rejected via\n`child.resolve().is_relative_to(base.resolve())` (verified\nby unit test in TASK-1-8 using\n`gateway/worktree_manager.py:1711` as the reference\npattern).", - "files_affected": [ - "orchestrator/substrate/claude_code/worktree.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-6", - "description": "Build the in-process orchestrator entry point at\n`orchestrator/substrate/in_process.py`. This is the slice's\nmost expensive task \u2014 risk_analyst R4 flagged the\norchestrator-reentrancy refactor as `requires_human_review`\n(effort HIGH). Today the orchestrator is a Flask/waitress\nHTTP daemon (`orchestrator/cli.py:83 cmd_serve`) with\n`ConcurrentPhaseExecutor`\n(`orchestrator/concurrent_executor.py:114`) running its own\n`ThreadPoolExecutor` and `PeerConsensusTracker`\n(`orchestrator/peer_consensus.py:69`) holding its own\nlocks. Wrapping into a generator that yields\n`HITLDecision` (cq-7 heredoc-style synchronous HITL) and\nresumes via `.send(...)` is the actual refactor.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`run_pipeline_in_process` is a generator yielding\n`HITLDecision` and returning the refine artifact path on\ncompletion. With `EGG_SUBSTRATE=claude-code` and a curated\nissue body the refiner completes; artifact lands under\n`.egg-state/drafts/-analysis.md`.\n**Heartbeat-during-HITL**: background heartbeat poll, BRC\nre-review, and message-bus tick threads remain alive\nwhile the generator is paused at a yield boundary\n(verified by a unit test asserting heartbeat-thread\n`is_alive()` at the yield).\n**Background-thread lifetime**: the generator returns\ncleanly on both normal completion AND on `GeneratorExit`\n(verified by a unit test that drops the generator\nmid-cycle and asserts no leaked threads via\n`threading.enumerate()` delta).\n**Contract-state synchronization**: the in-process\norchestrator writes to the same\n`.egg-state/contracts/.json` filesystem path the HTTP\ndaemon uses (verified by reading the contract file after\nthe first yield and asserting the pending-decision entry\nexists).\nExisting primitives stay in the path: `build_system_prompt`\n(`shared/egg_harness/prompt.py:24`),\n`ConcurrentPhaseExecutor`\n(`orchestrator/concurrent_executor.py:114`),\n`HITLDecision` (`orchestrator/models.py:300`),\n`PeerConsensusTracker`\n(`orchestrator/peer_consensus.py:69`).\n`EGG_SUBSTRATE=k3s` raises `NotImplementedError` with a\nmessage naming the follow-up issue (k3s users keep using\n`orchestrator/cli.py:83 cmd_serve`; the in-process entry\nis claude-code-only for this spike \u2014 cq-11 scope-fence).", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-7", - "description": "Ship `plugins/egg-sdlc/.claude-plugin/plugin.json` per\ncq-8. The pip dependency string is whatever the operator\nselects in **cq-12** (the new HITL decision asking for\ncanonical pip name + registry \u2014 options: PyPI\n`egg-shared`, `git+https://github.com/jwbron/egg.git#subdirectory=shared`,\nor \"Defer to follow-up\" TODO placeholder). Also ship any\nPython pre-flight helper the SKILL.md will call (e.g. a\nstub that calls `import egg_orchestrator` and emits a\nclear install instruction when missing). User-facing\ninstruction markdown (SKILL.md + role files) is\ndocumenter-owned and split into TASK-1-11.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`plugins/egg-sdlc/.claude-plugin/plugin.json` exists,\nlists the pip dependency string selected by cq-12\nverbatim (or a TODO comment pointing at the docs if cq-12\nresolves to \"Defer to follow-up\"), includes marketplace\ndescription and keywords; if a Python pre-flight helper is\nshipped it fails loudly when `egg_orchestrator` is missing\n(no developer-mode CC feature flag required per feedback\nQ3); the plugin metadata names the skill the documenter\nwrites in TASK-1-11; the install-error message in the\npre-flight helper matches the install instruction\ndocumenter writes in TASK-1-11's SKILL.md (no drift\nbetween the two role-split halves).", - "files_affected": [ - "plugins/egg-sdlc/.claude-plugin/plugin.json" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-8", - "description": "Extend `integration_tests/regression/conftest.py` with a\n`substrate` parametrize-able fixture\n(`\"k3s\"`, `\"claude-code\"`). Add a NEW\nsubstrate-distinguishing test at\n`integration_tests/regression/test_substrate_smoke.py`\nthat drives `select_substrate(...).spawner.spawn(...)` and\n`.bus.add_message/get_messages` directly \u2014 under\n`\"k3s\"` the spawner is `K3sSpawnerAdapter` (mocked\nunderlying job dispatch); under `\"claude-code\"` the\nspawner is `ClaudeCodeSpawner` + `InProcessMessageBus`.\nBoth parameters run pure-Python in-process; no kubectl\ngate is needed. Also add unit tests under `shared/tests/`\n(or `orchestrator/tests/` where the BRC behavioral oracle\nlives) covering protocols, implementations, and TASK-1-6's\nthree R4 acceptance bullets (heartbeat-during-HITL,\nbackground-thread lifetime, contract-state sync).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`substrate` fixture exists in\n`integration_tests/regression/conftest.py`; claude-code\ndimension `pytest.skip`s when running inside an\nin-sandbox-agent trust context;\n`test_substrate_smoke.py` is parametrized over\n`substrate` and passes on both dimensions\n(pure-Python, no kubectl); the test exercises\n`select_substrate(...).spawner.spawn(...)` (asserts\npopulated `AgentResult.commit_sha`) and\n`.bus.add_message/get_messages` (asserts INV-3\nstale-version rejection); unit tests cover\n(a) `ClaudeCodeSpawner` conforms to `AgentSpawner` and\ncaptures `commit_sha`; (b) `K3sSpawnerAdapter` conforms\nand wraps `create_concurrent_spawn_fn`\n(`orchestrator/kubernetes_spawner.py:1564`);\n(c) `InProcessMessageBus` preserves INV-3 and INV-5 using\nscenarios from\n`orchestrator/tests/test_brc_open_nacks_barrier.py` and\n`orchestrator/tests/test_brc_content_validation.py` as\noracles; (d) `PreToolUseHookPolicy` denies out-of-role\nwrites matching `gateway/phase_filter.py:1061\ncheck_agent_restrictions`; (e) `LocalWorktreeManager`\npath-escape rejection via\n`gateway/worktree_manager.py:1711`; (f)\n`run_pipeline_in_process` raises `NotImplementedError`\nfor `EGG_SUBSTRATE=k3s`, preserves heartbeat-thread\nliveness across HITL yields, and drops background\nthreads cleanly on `GeneratorExit`; all tests respect\nthe tester role's file restrictions.", - "files_affected": [ - "integration_tests/regression/conftest.py", - "integration_tests/regression/test_substrate_smoke.py", - "shared/tests/test_substrate_interfaces.py", - "shared/tests/test_claude_code_spawner.py", - "shared/tests/test_k3s_spawner_adapter.py", - "shared/tests/test_in_process_message_bus.py", - "shared/tests/test_pretooluse_hook_policy.py", - "shared/tests/test_local_worktree_manager.py", - "shared/tests/test_run_pipeline_in_process.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-9", - "description": "Write the ADR-style design doc\n`docs/architecture/claude-code-substrate.md`. Documents\nthe four interfaces, the implementations (working\nK3sSpawnerAdapter shim + claude-code implementations),\nthe `EGG_SUBSTRATE` selector, the cq-7 heredoc-HITL\nsurface, the cq-6 PreToolUse hook seam, the cq-5 local\nworktree model, and the cq-3 conformance-by-CI-matrix\nshape. Names every primitive (existing + new) in the\nPrimitives table. Adds risk-mitigation subsections for\nR1/R2/R7/R10 and recommendations REC5/R15. Links to/from\n`docs/architecture/README.md`.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`docs/architecture/claude-code-substrate.md` exists and\nis linked from `docs/architecture/README.md`; doc covers\nall twelve cq decisions (cq-1..cq-12) and all six\nfeedback items; doc names every primitive in the\nPrimitives table (module names explicit); \"Trust-context\nshift (R1)\" subsection names credential exposure change\nand operator's acceptance; \"PreToolUse hook fallback\n(R2)\" subsection documents primary + fallback enforcement\npaths; \"Subagent context budget regression (R7)\"\nsubsection documents `max_turns: 1000` regression and\ncheckpoint mitigation; \"Interface stability marker (R10)\"\nsubsection states `# v0.x unstable` and revision policy;\n\"Cost cap (REC5)\" subsection recommends\n`EGG_PIPELINE_MAX_AGENT_INVOCATIONS`; \"Subagent type\nmodel (R15)\" subsection documents\n`subagent_type=\"general-purpose\"` choice; \"Open work\"\nappendix lists what the spike does NOT do\n(plan/implement/pr phases, BRC concurrency, full 5-issue\nconformance, k3s interface adapter beyond the shim,\nEggHarnessSpawner, prune verb, fork-based sub-task\ndelegation, `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`\nimplementation, custom `subagent_type` migration).", - "files_affected": [ - "docs/architecture/claude-code-substrate.md", - "docs/architecture/README.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-10", - "description": "Extend `docs/architecture/claude-code-substrate.md` (same\nfile as TASK-1-9) with a \"Follow-up issue draft\" section\ncontaining the issue body text the reviewer copies when\nfiling the rollout follow-up. Documenter is role-blocked\nfrom `.github/` so the section is reviewer-pasted, not\nauto-filed.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "\"Follow-up issue draft\" section exists near the bottom\nof `docs/architecture/claude-code-substrate.md`; lists,\none bullet per item: extend to plan/implement/pr phases\n(cq-2 unfinished), full conformance matrix (Q1), perf\nbudget (Q2), full k3s interface adapter beyond the shim\n(cq-1 k3s side), optional EggHarnessSpawner (Q4), ship\n`egg-state prune` verb (Q6), fork-based sub-task\ndelegation (cq-10 deferred half), implement\n`EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost cap (REC5),\nmigrate to custom `subagent_type` per-role agent files\n(R15); section header explicitly states\n\"reviewer-pasted, not auto-filed\" so the operator\nunderstands the handoff.", - "files_affected": [ - "docs/architecture/claude-code-substrate.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-11", - "description": "Author the user-facing skill instruction markdown\n(`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`) plus the\nper-role prompt-prepend file\n(`plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md`)\nmirroring the layout of\n`plugins/refine-plan/skills/refine-plan/agents/refiner.md`.\nSKILL.md documents the heredoc-HITL loop as the\nuser-facing contract (how the parent session drives the\n`run_pipeline_in_process` generator and renders each\n`HITLDecision` via `AskUserQuestion`). The refiner role\nfile is what `build_system_prompt(sources)` prepends.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` exists and\ndocuments the heredoc-HITL loop;\n`plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md`\nexists and follows the existing\n`plugins/refine-plan/skills/refine-plan/agents/refiner.md`\nfront-matter+body shape; SKILL.md states explicitly that\nthe spike's exercised scope is refiner-only (plan/\nimplement/pr roles documented as out of scope, matching\nTASK-1-7's plugin metadata).", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - } - ], - "dependencies": [], - "serialized_chain_order": [], - "parent_branch_at_creation": null, - "commit": null, - "review_feedback": [] - } - ], - "decisions": [ - { - "id": "cq-1", - "question": "Substrate coexistence strategy: how should the Claude Code substrate relate to the existing k3s substrate?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Parallel substrates, env-var-selected: AgentSpawner/MessageBus/PolicyEnforcer interfaces in egg_orchestrator with two implementations; EGG_SUBSTRATE=claude-code|k3s; both run the same conformance suite (Option A)", - "description": null - }, - { - "id": "opt-2", - "label": "Full cut-over to Claude Code substrate: delete k3s/Redis/Docker/gateway code; orchestrator only runs in-process to a Claude Code session (Option B)", - "description": null - }, - { - "id": "opt-3", - "label": "Skill-only: do not refactor the orchestrator; ship a beefier refine-plan/sdlc skill that imports egg_orchestrator Python for non-protocol code (Option C \u2014 contradicts the issue's substrate-swap framing, listed for completeness)", - "description": null - }, - { - "id": "opt-4", - "label": "In-process binding without interfaces: monkey-patch / DI the new spawner+bus at boot path; no named abstraction layer (Option D)", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Parallel substrates, env-var-selected: AgentSpawner/MessageBus/PolicyEnforcer interfaces in egg_orchestrator with two implementations; EGG_SUBSTRATE=claude-code|k3s; both run the same conformance suite (Option A)\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:34:20.757853Z", - "debounce_until": null - }, - { - "id": "cq-2", - "question": "Initial pipeline-phase scope for the substrate swap: which phases must work on the Claude Code substrate before this parent issue closes?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Refine + plan only: prove the substrate on the existing refine-plan skill's footprint; defer implement+pr to a follow-up", - "description": null - }, - { - "id": "opt-2", - "label": "All phases (refine + plan + implement + pr): full SDLC inside Claude Code; matches the issue body's North Star verbatim", - "description": null - }, - { - "id": "opt-3", - "label": "Explore-first: ship the abstraction interfaces + a single-role 'spawn one agent against an issue' smoke path; defer multi-role + BRC to follow-up issues once the spawner shape is settled", - "description": null - }, - { - "id": "opt-4", - "label": "Refine + plan + implement (no pr): prove BRC mechanics on producer roles; let pr-phase keep using k3s for now since PR creation is an external-API operation, not a coordination test", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"All phases (refine + plan + implement + pr): full SDLC inside Claude Code; matches the issue body's North Star verbatim\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:34:50.687612Z", - "debounce_until": null - }, - { - "id": "cq-3", - "question": "Conformance-suite scoping: where does the substrate-portable behavioral test set live?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Extend integration_tests/regression/ with a substrate parameter (CI matrix): each existing test runs under both k3s and claude-code substrates; tests that are inherently k3s-specific (e.g. live-pod-guard) skip on the claude-code dimension", - "description": null - }, - { - "id": "opt-2", - "label": "New shared conformance package (e.g. integration_tests/conformance/): factor substrate-portable invariants out of integration_tests/regression/; the regression suite keeps the k3s-specific stuff and the new package is what both substrates run", - "description": null - }, - { - "id": "opt-3", - "label": "Per-substrate test suites: integration_tests/regression/ stays k3s; integration_tests/claude_code/ is the new substrate's suite; cross-substrate conformance becomes a documented set of invariants verified by inspection, not by a single CI matrix", - "description": null - }, - { - "id": "opt-4", - "label": "Skill-internal cycle harness: the Claude Code substrate gets its own end-to-end smoke test that re-runs the refine-plan skill's existing local repro against \u22655 representative issues; no cross-substrate matrix", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Extend integration_tests/regression/ with a substrate parameter (CI matrix): each existing test runs under both k3s and claude-code substrates; tests that are inherently k3s-specific (e.g. live-pod-guard) skip on the claude-code dimension\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:46:45.575594Z", - "debounce_until": null - }, - { - "id": "cq-4", - "question": "Agent-spawner interface shape: what method signature should AgentSpawner expose so both KubernetesSpawner and ClaudeCodeSpawner can satisfy it?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Synchronous spawn(role, prompt, env, worktree) -> AgentResult: caller blocks until agent completes; spawner handles internal concurrency. Maps cleanly to the Agent tool's call-and-wait model; k8s implementation polls internally", - "description": null - }, - { - "id": "opt-2", - "label": "Async dispatch + poll: spawn(...) -> AgentHandle; poll(handle) -> AgentStatus; matches today's KubernetesSpawner+monitor split, but requires the Claude Code spawner to fake handles (no underlying job ID) and complicates the in-process model", - "description": null - }, - { - "id": "opt-3", - "label": "Stream-shaped: spawn(...) yields events (start, tool-call, output, completion); the orchestrator drives the agent via the event stream; mirrors how egg_harness exposes the agent loop. Most powerful, biggest refactor", - "description": null - }, - { - "id": "opt-4", - "label": "Agent-tool-direct (Claude Code only): the abstraction is 'request a subagent run' and the k8s implementation translates by spawning a process running egg_harness \u2014 the interface is shaped around Claude Code's call shape, k8s adapts", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Synchronous spawn(role, prompt, env, worktree) -> AgentResult: caller blocks until agent completes; spawner handles internal concurrency. Maps cleanly to the Agent tool's call-and-wait model; k8s implementation polls internally\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:48:08.845497Z", - "debounce_until": null - }, - { - "id": "cq-5", - "question": "Worktree-management ownership in the Claude Code substrate: who creates and tears down the per-agent worktree?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Claude Code's EnterWorktree / ExitWorktree tools: the substrate uses the harness-provided worktree primitive; egg's WORKTREE_BASE_DIR layout is replaced by ~/.claude/worktrees/. Native, but loses egg's per-repo-per-pipeline shared-checkout optimization", - "description": null - }, - { - "id": "opt-2", - "label": "Port egg's WORKTREE_BASE_DIR model: agents run inside .egg-state// subdirectories on the user's filesystem; the spawner manages worktree creation/teardown explicitly. Preserves egg's checkout-sharing; doesn't use Claude Code's native worktree mechanism", - "description": null - }, - { - "id": "opt-3", - "label": "Hybrid: parent session creates the pipeline-level shared checkout under .egg-state//; each subagent call passes through EnterWorktree to get an isolated branch; teardown is filesystem-level by the orchestrator at phase end", - "description": null - }, - { - "id": "opt-4", - "label": "Single-worktree mode: no per-agent worktree; agents run sequentially against the parent session's repo and the BRC bus serializes their writes. Loses concurrency but maximally simple", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Port egg's WORKTREE_BASE_DIR model: agents run inside .egg-state// subdirectories on the user's filesystem; the spawner manages worktree creation/teardown explicitly. Preserves egg's checkout-sharing; doesn't use Claude Code's native worktree mechanism\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:48:08.861461Z", - "debounce_until": null - }, - { - "id": "cq-6", - "question": "Policy enforcement seam: where does the substrate enforce the gateway-equivalent rules (file-write restrictions per role, git-push allow/deny, gh-operation phase gates)?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "PreToolUse hooks in .claude/settings.json: skill-installed hook intercepts Write/Edit/Bash/etc. and calls into shared/egg_restrictions/patterns.py; structural enforcement at the tool boundary", - "description": null - }, - { - "id": "opt-2", - "label": "MCP-tool-side validators: every state-mutating MCP verb re-validates the caller's role+path against patterns.py; tools the harness uses (Write/Bash) are guarded by the harness's existing permission_callback. No hooks; existing layers strengthened", - "description": null - }, - { - "id": "opt-3", - "label": "In-process Python imports only: the orchestrator drives subagent prompts that *announce* role restrictions and the subagent self-polices; depends on prompt discipline (not structural enforcement). Listed for completeness \u2014 rejected upfront unless cost of structural enforcement is too high", - "description": null - }, - { - "id": "opt-4", - "label": "All three layered: PreToolUse hook for hard cuts (writes to blocked paths), MCP validators for state-mutation gates, prompt-time restrictions for graceful messaging. Most defense-in-depth, most build cost", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"PreToolUse hooks in .claude/settings.json: skill-installed hook intercepts Write/Edit/Bash/etc. and calls into shared/egg_restrictions/patterns.py; structural enforcement at the tool boundary\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:48:08.875069Z", - "debounce_until": null - }, - { - "id": "cq-7", - "question": "HITL surface in Claude Code mode: how does the orchestrator pause for a human decision when running in-process to a Claude Code session?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Parent-session AskUserQuestion: orchestrator yields a HITL decision back to the parent Claude session, which surfaces it via AskUserQuestion; user reply resumes the pipeline. Best UX for solo developer; requires the orchestrator's run loop to be reentrant from the parent's perspective", - "description": null - }, - { - "id": "opt-2", - "label": "MCP provide_input verb: matches today's pattern; the parent calls a provide_input MCP tool from the user's prompt. Awkward inside a single Claude session (user typed something, agent calls back through MCP into the same process)", - "description": null - }, - { - "id": "opt-3", - "label": "Filesystem journal + parent poll: orchestrator writes the pending decision to .egg-state/hitl/.json; parent polls and surfaces it; user replies via a slash-command or skill verb that writes back. Works without orchestrator-as-callee but adds round-trip friction", - "description": null - }, - { - "id": "opt-4", - "label": "Heredoc-style synchronous: the orchestrator surface is a generator that yields HITLDecision objects; the skill's outer loop renders them with AskUserQuestion and feeds answers back. Hybrid of 1 and 3", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Heredoc-style synchronous: the orchestrator surface is a generator that yields HITLDecision objects; the skill's outer loop renders them with AskUserQuestion and feeds answers back. Hybrid of 1 and 3\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:52:06.893277Z", - "debounce_until": null - }, - { - "id": "cq-8", - "question": "Install / packaging footprint: how does the user obtain the egg Python packages required by the in-process orchestrator?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Skill ships Python wheels in the plugin: marketplace install drops wheels into a venv the skill bootstraps; isolates the install from system Python; biggest plugin size", - "description": null - }, - { - "id": "opt-2", - "label": "Plugin metadata declares pip dependencies: user runs 'pip install egg' (or equivalent) once; plugin.json documents the requirement; smallest plugin but pre-flight check needed before the skill works", - "description": null - }, - { - "id": "opt-3", - "label": "Vendor minimal subset into the plugin: the skill copies only the egg_orchestrator + egg_contracts + egg_harness + shared/prompts/ subtree it needs; no wheels, no pip; biggest source-of-truth-drift risk between the vendored copy and the upstream", - "description": null - }, - { - "id": "opt-4", - "label": "Two-skill layout: a thin entry-point skill + a heavier 'egg-runtime' plugin that ships the wheels; the entry-point depends on the runtime; user installs both. More moving parts but cleaner separation", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Plugin metadata declares pip dependencies: user runs 'pip install egg' (or equivalent) once; plugin.json documents the requirement; smallest plugin but pre-flight check needed before the skill works\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:52:06.910707Z", - "debounce_until": null - }, - { - "id": "cq-9", - "question": "k3s deprecation timing: what is the disposition of the k3s substrate after the Claude Code substrate ships green?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Deprecate k3s on substrate-swap merge: remove kubernetes_spawner.py / kubernetes_monitor.py / RedisMessageStore / gateway sidecar in the same PR family; Claude Code substrate becomes the only substrate", - "description": null - }, - { - "id": "opt-2", - "label": "Leave k3s indefinitely as a co-equal substrate: both substrates are supported; CI matrix tests both; k3s is the answer for headless / server deployments, Claude Code substrate is the answer for solo developers. (default, lowest-risk)", - "description": null - }, - { - "id": "opt-3", - "label": "Mark k3s 'CI-only': k3s stops being a deployment target for end users (no docs guidance, no images shipped); only remains as a CI environment that runs the conformance suite as a second data point. Removal scheduled for a future cleanup issue", - "description": null - }, - { - "id": "opt-4", - "label": "Defer the decision: ship substrate-swap, let user feedback drive a follow-up issue that decides k3s's fate after \u22653 months of real-world use of the Claude Code substrate", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Leave k3s indefinitely as a co-equal substrate: both substrates are supported; CI matrix tests both; k3s is the answer for headless / server deployments, Claude Code substrate is the answer for solo developers. (default, lowest-risk)\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:52:06.923611Z", - "debounce_until": null - }, - { - "id": "cq-10", - "question": "Subagent context-window strategy: how does the substrate handle deep research that would exceed a subagent's context budget (egg targets max_turns: 1000)?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Port egg_container checkpointing: subagents write intermediate findings to .egg-state/checkpoints/; a re-spawned subagent resumes from the checkpoint. Faithful to today's recovery model; requires the Claude Code spawner to re-invoke the agent with checkpoint context", - "description": null - }, - { - "id": "opt-2", - "label": "Accept smaller-than-1000 turn budget: subagents are bounded by Claude Code's native context limit; role rubrics and the system-prompt depth do most of the work; deep-research breadth is reduced but consistent. Lowest implementation cost", - "description": null - }, - { - "id": "opt-3", - "label": "Per-agent forked subagent for deep research: a refiner whose context fills up forks a child subagent to do a sub-task ('read all files matching X and summarize'), the child's summary returns to the parent. Mirrors how a human delegates", - "description": null - }, - { - "id": "opt-4", - "label": "Hybrid: checkpoint for cross-turn recovery, fork for sub-task delegation; the system prompt teaches when to use which", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Hybrid: checkpoint for cross-turn recovery, fork for sub-task delegation; the system prompt teaches when to use which\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:52:38.111737Z", - "debounce_until": null - }, - { - "id": "cq-11", - "question": "Slice-DAG decomposition shape: how should this substrate-swap work be sliced for shippable PRs? (slice count = PR count; siblings in a wave run in parallel)", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Single slice \u2014 entire substrate swap as one PR family (interfaces + ClaudeCodeSpawner + InProcessMessageBus + PreToolUseHookPolicy + conformance matrix + skill entry point + ADR); 1 PR. Largest blast radius, simplest integration", - "description": null - }, - { - "id": "opt-2", - "label": "Two-wave parallel: [substrate-interfaces + ADR] -> [ClaudeCodeSpawner || InProcessMessageBus || PolicyEnforcer || WorktreeManager (4 parallel implementations)] -> [conformance matrix + skill entry point]; 7 PRs across 3 waves", - "description": null - }, - { - "id": "opt-3", - "label": "Three-wave parallel: [interfaces only] -> [each implementation in parallel] -> [k3s side of the interface adapter || conformance matrix || skill entry point || ADR (all parallel)]; ~9 PRs, very parallel", - "description": null - }, - { - "id": "opt-4", - "label": "Linear chain: interfaces -> message bus -> spawner -> policy -> worktree -> conformance -> skill -> ADR; 8 sequential PRs. Lowest review burden per PR; slowest end-to-end", - "description": null - }, - { - "id": "opt-5", - "label": "Spike then plan: a single 'walking skeleton' slice that gets one role (refiner) running through one substrate (claude-code) end-to-end, then re-plan the rollout based on what the spike learns; 1 PR for the spike + follow-up issue", - "description": null - }, - { - "id": "opt-6", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Spike then plan: a single 'walking skeleton' slice that gets one role (refiner) running through one substrate (claude-code) end-to-end, then re-plan the rollout based on what the spike learns; 1 PR for the spike + follow-up issue\"}", - "resolved_by": "human", - "resolved_at": "2026-05-18T23:52:38.274810Z", - "debounce_until": null - }, - { - "id": "cq-12", - "question": "Canonical pip package name + registry for the egg Python deps the egg-sdlc skill imports. Per refine cq-8 the operator chose 'Plugin metadata declares pip dependencies', but the actual PyPI/git+https reference is not yet set. The plan and the skill's plugin.json need a definitive string to ship to users.", - "type": "hitl", - "phase": "plan", - "options": [ - { - "id": "opt-1", - "label": "egg-shared on PyPI (publish a new wheel from shared/pyproject.toml under that name)", - "description": null - }, - { - "id": "opt-2", - "label": "git+https://github.com/jwbron/egg.git#subdirectory=shared (no PyPI publish required; users pip-install directly from the repo)", - "description": null - }, - { - "id": "opt-3", - "label": "Defer to follow-up: plugin.json ships with a TODO placeholder + clear install error pointing at the docs; the spike does not commit to a name yet", - "description": null - }, - { - "id": "opt-4", - "label": "Other (specify in reply)", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - } - ], - "workflow_owner": null, - "audit_log": [ - { - "timestamp": "2026-05-18T22:32:06.905884Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.0", - "old_value": null, - "new_value": { - "id": "cq-1", - "question": "Substrate coexistence strategy: how should the Claude Code substrate relate to the existing k3s substrate?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Parallel substrates, env-var-selected: AgentSpawner/MessageBus/PolicyEnforcer interfaces in egg_orchestrator with two implementations; EGG_SUBSTRATE=claude-code|k3s; both run the same conformance suite (Option A)", - "description": null - }, - { - "id": "opt-2", - "label": "Full cut-over to Claude Code substrate: delete k3s/Redis/Docker/gateway code; orchestrator only runs in-process to a Claude Code session (Option B)", - "description": null - }, - { - "id": "opt-3", - "label": "Skill-only: do not refactor the orchestrator; ship a beefier refine-plan/sdlc skill that imports egg_orchestrator Python for non-protocol code (Option C \u2014 contradicts the issue's substrate-swap framing, listed for completeness)", - "description": null - }, - { - "id": "opt-4", - "label": "In-process binding without interfaces: monkey-patch / DI the new spawner+bus at boot path; no named abstraction layer (Option D)", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Substrate coexistence strategy: how should the Cla...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:07.466877Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.1", - "old_value": null, - "new_value": { - "id": "cq-2", - "question": "Initial pipeline-phase scope for the substrate swap: which phases must work on the Claude Code substrate before this parent issue closes?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Refine + plan only: prove the substrate on the existing refine-plan skill's footprint; defer implement+pr to a follow-up", - "description": null - }, - { - "id": "opt-2", - "label": "All phases (refine + plan + implement + pr): full SDLC inside Claude Code; matches the issue body's North Star verbatim", - "description": null - }, - { - "id": "opt-3", - "label": "Explore-first: ship the abstraction interfaces + a single-role 'spawn one agent against an issue' smoke path; defer multi-role + BRC to follow-up issues once the spawner shape is settled", - "description": null - }, - { - "id": "opt-4", - "label": "Refine + plan + implement (no pr): prove BRC mechanics on producer roles; let pr-phase keep using k3s for now since PR creation is an external-API operation, not a coordination test", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Initial pipeline-phase scope for the substrate swa...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:07.982037Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.2", - "old_value": null, - "new_value": { - "id": "cq-3", - "question": "Conformance-suite scoping: where does the substrate-portable behavioral test set live?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Extend integration_tests/regression/ with a substrate parameter (CI matrix): each existing test runs under both k3s and claude-code substrates; tests that are inherently k3s-specific (e.g. live-pod-guard) skip on the claude-code dimension", - "description": null - }, - { - "id": "opt-2", - "label": "New shared conformance package (e.g. integration_tests/conformance/): factor substrate-portable invariants out of integration_tests/regression/; the regression suite keeps the k3s-specific stuff and the new package is what both substrates run", - "description": null - }, - { - "id": "opt-3", - "label": "Per-substrate test suites: integration_tests/regression/ stays k3s; integration_tests/claude_code/ is the new substrate's suite; cross-substrate conformance becomes a documented set of invariants verified by inspection, not by a single CI matrix", - "description": null - }, - { - "id": "opt-4", - "label": "Skill-internal cycle harness: the Claude Code substrate gets its own end-to-end smoke test that re-runs the refine-plan skill's existing local repro against \u22655 representative issues; no cross-substrate matrix", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Conformance-suite scoping: where does the substrat...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:33.588175Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.3", - "old_value": null, - "new_value": { - "id": "cq-4", - "question": "Agent-spawner interface shape: what method signature should AgentSpawner expose so both KubernetesSpawner and ClaudeCodeSpawner can satisfy it?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Synchronous spawn(role, prompt, env, worktree) -> AgentResult: caller blocks until agent completes; spawner handles internal concurrency. Maps cleanly to the Agent tool's call-and-wait model; k8s implementation polls internally", - "description": null - }, - { - "id": "opt-2", - "label": "Async dispatch + poll: spawn(...) -> AgentHandle; poll(handle) -> AgentStatus; matches today's KubernetesSpawner+monitor split, but requires the Claude Code spawner to fake handles (no underlying job ID) and complicates the in-process model", - "description": null - }, - { - "id": "opt-3", - "label": "Stream-shaped: spawn(...) yields events (start, tool-call, output, completion); the orchestrator drives the agent via the event stream; mirrors how egg_harness exposes the agent loop. Most powerful, biggest refactor", - "description": null - }, - { - "id": "opt-4", - "label": "Agent-tool-direct (Claude Code only): the abstraction is 'request a subagent run' and the k8s implementation translates by spawning a process running egg_harness \u2014 the interface is shaped around Claude Code's call shape, k8s adapts", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Agent-spawner interface shape: what method signatu...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:34.075182Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.4", - "old_value": null, - "new_value": { - "id": "cq-5", - "question": "Worktree-management ownership in the Claude Code substrate: who creates and tears down the per-agent worktree?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Claude Code's EnterWorktree / ExitWorktree tools: the substrate uses the harness-provided worktree primitive; egg's WORKTREE_BASE_DIR layout is replaced by ~/.claude/worktrees/. Native, but loses egg's per-repo-per-pipeline shared-checkout optimization", - "description": null - }, - { - "id": "opt-2", - "label": "Port egg's WORKTREE_BASE_DIR model: agents run inside .egg-state// subdirectories on the user's filesystem; the spawner manages worktree creation/teardown explicitly. Preserves egg's checkout-sharing; doesn't use Claude Code's native worktree mechanism", - "description": null - }, - { - "id": "opt-3", - "label": "Hybrid: parent session creates the pipeline-level shared checkout under .egg-state//; each subagent call passes through EnterWorktree to get an isolated branch; teardown is filesystem-level by the orchestrator at phase end", - "description": null - }, - { - "id": "opt-4", - "label": "Single-worktree mode: no per-agent worktree; agents run sequentially against the parent session's repo and the BRC bus serializes their writes. Loses concurrency but maximally simple", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Worktree-management ownership in the Claude Code s...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:34.585734Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.5", - "old_value": null, - "new_value": { - "id": "cq-6", - "question": "Policy enforcement seam: where does the substrate enforce the gateway-equivalent rules (file-write restrictions per role, git-push allow/deny, gh-operation phase gates)?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "PreToolUse hooks in .claude/settings.json: skill-installed hook intercepts Write/Edit/Bash/etc. and calls into shared/egg_restrictions/patterns.py; structural enforcement at the tool boundary", - "description": null - }, - { - "id": "opt-2", - "label": "MCP-tool-side validators: every state-mutating MCP verb re-validates the caller's role+path against patterns.py; tools the harness uses (Write/Bash) are guarded by the harness's existing permission_callback. No hooks; existing layers strengthened", - "description": null - }, - { - "id": "opt-3", - "label": "In-process Python imports only: the orchestrator drives subagent prompts that *announce* role restrictions and the subagent self-polices; depends on prompt discipline (not structural enforcement). Listed for completeness \u2014 rejected upfront unless cost of structural enforcement is too high", - "description": null - }, - { - "id": "opt-4", - "label": "All three layered: PreToolUse hook for hard cuts (writes to blocked paths), MCP validators for state-mutation gates, prompt-time restrictions for graceful messaging. Most defense-in-depth, most build cost", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Policy enforcement seam: where does the substrate ...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:58.114592Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.6", - "old_value": null, - "new_value": { - "id": "cq-7", - "question": "HITL surface in Claude Code mode: how does the orchestrator pause for a human decision when running in-process to a Claude Code session?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Parent-session AskUserQuestion: orchestrator yields a HITL decision back to the parent Claude session, which surfaces it via AskUserQuestion; user reply resumes the pipeline. Best UX for solo developer; requires the orchestrator's run loop to be reentrant from the parent's perspective", - "description": null - }, - { - "id": "opt-2", - "label": "MCP provide_input verb: matches today's pattern; the parent calls a provide_input MCP tool from the user's prompt. Awkward inside a single Claude session (user typed something, agent calls back through MCP into the same process)", - "description": null - }, - { - "id": "opt-3", - "label": "Filesystem journal + parent poll: orchestrator writes the pending decision to .egg-state/hitl/.json; parent polls and surfaces it; user replies via a slash-command or skill verb that writes back. Works without orchestrator-as-callee but adds round-trip friction", - "description": null - }, - { - "id": "opt-4", - "label": "Heredoc-style synchronous: the orchestrator surface is a generator that yields HITLDecision objects; the skill's outer loop renders them with AskUserQuestion and feeds answers back. Hybrid of 1 and 3", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: HITL surface in Claude Code mode: how does the orc...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:58.622955Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.7", - "old_value": null, - "new_value": { - "id": "cq-8", - "question": "Install / packaging footprint: how does the user obtain the egg Python packages required by the in-process orchestrator?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Skill ships Python wheels in the plugin: marketplace install drops wheels into a venv the skill bootstraps; isolates the install from system Python; biggest plugin size", - "description": null - }, - { - "id": "opt-2", - "label": "Plugin metadata declares pip dependencies: user runs 'pip install egg' (or equivalent) once; plugin.json documents the requirement; smallest plugin but pre-flight check needed before the skill works", - "description": null - }, - { - "id": "opt-3", - "label": "Vendor minimal subset into the plugin: the skill copies only the egg_orchestrator + egg_contracts + egg_harness + shared/prompts/ subtree it needs; no wheels, no pip; biggest source-of-truth-drift risk between the vendored copy and the upstream", - "description": null - }, - { - "id": "opt-4", - "label": "Two-skill layout: a thin entry-point skill + a heavier 'egg-runtime' plugin that ships the wheels; the entry-point depends on the runtime; user installs both. More moving parts but cleaner separation", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Install / packaging footprint: how does the user o...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:32:59.120232Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.8", - "old_value": null, - "new_value": { - "id": "cq-9", - "question": "k3s deprecation timing: what is the disposition of the k3s substrate after the Claude Code substrate ships green?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Deprecate k3s on substrate-swap merge: remove kubernetes_spawner.py / kubernetes_monitor.py / RedisMessageStore / gateway sidecar in the same PR family; Claude Code substrate becomes the only substrate", - "description": null - }, - { - "id": "opt-2", - "label": "Leave k3s indefinitely as a co-equal substrate: both substrates are supported; CI matrix tests both; k3s is the answer for headless / server deployments, Claude Code substrate is the answer for solo developers. (default, lowest-risk)", - "description": null - }, - { - "id": "opt-3", - "label": "Mark k3s 'CI-only': k3s stops being a deployment target for end users (no docs guidance, no images shipped); only remains as a CI environment that runs the conformance suite as a second data point. Removal scheduled for a future cleanup issue", - "description": null - }, - { - "id": "opt-4", - "label": "Defer the decision: ship substrate-swap, let user feedback drive a follow-up issue that decides k3s's fate after \u22653 months of real-world use of the Claude Code substrate", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: k3s deprecation timing: what is the disposition of...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:33:28.888801Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.9", - "old_value": null, - "new_value": { - "id": "cq-10", - "question": "Subagent context-window strategy: how does the substrate handle deep research that would exceed a subagent's context budget (egg targets max_turns: 1000)?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Port egg_container checkpointing: subagents write intermediate findings to .egg-state/checkpoints/; a re-spawned subagent resumes from the checkpoint. Faithful to today's recovery model; requires the Claude Code spawner to re-invoke the agent with checkpoint context", - "description": null - }, - { - "id": "opt-2", - "label": "Accept smaller-than-1000 turn budget: subagents are bounded by Claude Code's native context limit; role rubrics and the system-prompt depth do most of the work; deep-research breadth is reduced but consistent. Lowest implementation cost", - "description": null - }, - { - "id": "opt-3", - "label": "Per-agent forked subagent for deep research: a refiner whose context fills up forks a child subagent to do a sub-task ('read all files matching X and summarize'), the child's summary returns to the parent. Mirrors how a human delegates", - "description": null - }, - { - "id": "opt-4", - "label": "Hybrid: checkpoint for cross-turn recovery, fork for sub-task delegation; the system prompt teaches when to use which", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Subagent context-window strategy: how does the sub...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:33:29.403312Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.10", - "old_value": null, - "new_value": { - "id": "cq-11", - "question": "Slice-DAG decomposition shape: how should this substrate-swap work be sliced for shippable PRs? (slice count = PR count; siblings in a wave run in parallel)", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Single slice \u2014 entire substrate swap as one PR family (interfaces + ClaudeCodeSpawner + InProcessMessageBus + PreToolUseHookPolicy + conformance matrix + skill entry point + ADR); 1 PR. Largest blast radius, simplest integration", - "description": null - }, - { - "id": "opt-2", - "label": "Two-wave parallel: [substrate-interfaces + ADR] -> [ClaudeCodeSpawner || InProcessMessageBus || PolicyEnforcer || WorktreeManager (4 parallel implementations)] -> [conformance matrix + skill entry point]; 7 PRs across 3 waves", - "description": null - }, - { - "id": "opt-3", - "label": "Three-wave parallel: [interfaces only] -> [each implementation in parallel] -> [k3s side of the interface adapter || conformance matrix || skill entry point || ADR (all parallel)]; ~9 PRs, very parallel", - "description": null - }, - { - "id": "opt-4", - "label": "Linear chain: interfaces -> message bus -> spawner -> policy -> worktree -> conformance -> skill -> ADR; 8 sequential PRs. Lowest review burden per PR; slowest end-to-end", - "description": null - }, - { - "id": "opt-5", - "label": "Spike then plan: a single 'walking skeleton' slice that gets one role (refiner) running through one substrate (claude-code) end-to-end, then re-plan the rollout based on what the spike learns; 1 PR for the spike + follow-up issue", - "description": null - }, - { - "id": "opt-6", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Slice-DAG decomposition shape: how should this sub...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-18T22:33:29.935887Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "feedback", - "old_value": null, - "new_value": { - "id": "feedback-1", - "phase": "refine", - "questions": [ - { - "id": "Q1", - "question": "Definition of 'conformance passes on \u22655 representative issues': should the \u22655 measurement issues be a fixed curated set (and if so, which? \u2014 please name them or describe selection criteria), or sampled per-CI-run from open issues, or some other rule?", - "answer": null - }, - { - "id": "Q2", - "question": "Are there latency or throughput budgets the Claude Code substrate should meet relative to the k3s substrate? (E.g. 'refine phase \u2264 2x k3s latency'.) If so, what are they? If none, is performance acceptable as 'whatever Claude Code gives us so long as it terminates'?", - "answer": null - }, - { - "id": "Q3", - "question": "Are there constraints on third-party dependencies the substrate is allowed to introduce \u2014 e.g. any specific Python packages forbidden, any size cap on the marketplace plugin install footprint, any restriction on requiring a developer-mode Claude Code feature flag?", - "answer": null - }, - { - "id": "Q4", - "question": "Should the in-process orchestrator support being driven by NON-Claude-Code callers too (e.g. a CLI 'egg-orch local-run --issue 1234' that uses the in-process orchestrator with a non-Claude-Code agent harness)? Or is the Claude Code substrate intentionally Claude-Code-only?", - "answer": null - }, - { - "id": "Q5", - "question": "Is there appetite to use this issue to also fix structural-cause #5 from #2622 (no tool-use budget signaling) and #6 (no minimum-breadth targets in role files) \u2014 given they require role-file edits that touch shared/prompts/ regardless of substrate \u2014 or should those stay scoped to #2622?", - "answer": null - }, - { - "id": "Q6", - "question": "Any concerns about telemetry / privacy regression when the orchestrator runs in the user's session vs. a k3s pod (e.g. checkpoints written to local filesystem may contain sensitive prompts/contexts that previously stayed in cluster-storage)?", - "answer": null - } - ], - "submitted": false, - "submitted_by": null, - "submitted_at": null, - "comment_id": null, - "debounce_until": null - }, - "reason": "Created feedback request with 6 question(s)", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T00:14:45.222763Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.11", - "old_value": null, - "new_value": { - "id": "cq-12", - "question": "Canonical pip package name + registry for the egg Python deps the egg-sdlc skill imports. Per refine cq-8 the operator chose 'Plugin metadata declares pip dependencies', but the actual PyPI/git+https reference is not yet set. The plan and the skill's plugin.json need a definitive string to ship to users.", - "type": "hitl", - "phase": "plan", - "options": [ - { - "id": "opt-1", - "label": "egg-shared on PyPI (publish a new wheel from shared/pyproject.toml under that name)", - "description": null - }, - { - "id": "opt-2", - "label": "git+https://github.com/jwbron/egg.git#subdirectory=shared (no PyPI publish required; users pip-install directly from the repo)", - "description": null - }, - { - "id": "opt-3", - "label": "Defer to follow-up: plugin.json ships with a TODO placeholder + clear install error pointing at the docs; the spike does not commit to a name yet", - "description": null - }, - { - "id": "opt-4", - "label": "Other (specify in reply)", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Canonical pip package name + registry for the egg ...", - "checkpoint_id": null - } - ], - "refine_review_cycles": 0, - "refine_review_feedback": "", - "plan_review_cycles": 0, - "plan_review_feedback": "", - "pr": { - "title": "Spike: walking-skeleton Claude Code substrate for egg SDLC (#2623)", - "description": "## Context\n\nIssue #2623 reframes the prior \"approximate k3s pipeline quality\"\ngoal as a substrate swap: instead of building a parallel\nMarkdown-skill implementation and chasing parity, swap the\nsubstrate underneath the real `egg_orchestrator` stack so quality\nbecomes structurally inevitable. The refine phase settled eleven\ndecisions; the most consequential is **cq-11 = \"Spike then\nplan\"** \u2014 land a walking-skeleton slice for one role end-to-end\non the Claude Code substrate, then re-plan the rollout in a\nfollow-up issue once real numbers exist for subagent context\nbehavior, hook ergonomics, and `Agent` tool throughput.\n\n## Changes\n\n1. **Substrate interfaces** (`orchestrator/substrate/`):\n `AgentSpawner`, `MessageBus`, `PolicyEnforcer`, and\n `WorktreeManager` as `typing.Protocol`s, plus a\n `select_substrate(env)` factory that reads `EGG_SUBSTRATE`\n (cq-1). The k3s adapter is a deferred TODO that raises\n `NotImplementedError` (cq-11 explicit boundary).\n2. **Claude Code implementations**: `ClaudeCodeSpawner`\n (synchronous spawn per cq-4), `InProcessMessageBus`\n (preserves BRC INV-3/INV-5), `PreToolUseHookPolicy` (cq-6\n PreToolUse hook routing into existing\n `shared/egg_restrictions/patterns.py:768\n build_agent_patterns`), `LocalWorktreeManager` (cq-5\n WORKTREE_BASE_DIR shape).\n3. **In-process orchestrator boot**:\n `run_pipeline_in_process(...)` is a generator yielding\n `HITLDecision` objects (cq-7 heredoc-style synchronous HITL);\n the skill renders each via `AskUserQuestion`.\n4. **Skill entry point** (`plugins/egg-sdlc/`): plugin metadata\n declares the pip dependency (cq-8); the skill drives the\n refiner role end-to-end against a curated spike issue.\n5. **Conformance matrix proof**:\n `integration_tests/regression/conftest.py` gains a `substrate`\n parameter; one test in `test_brc_single_cycle.py` is\n parametrized over both substrates. Full matrix factor-out is\n deferred to the follow-up.\n6. **ADR**: `docs/architecture/claude-code-substrate.md` names\n the interfaces, the implementations, the deferred work, and\n the follow-up issue draft.\n\n## Impact\n\nUsers with Claude Code installed can `pip install egg-shared` and\nrun the curated spike issue through the refiner role on the new\nsubstrate with no k3s/Redis/Docker/gateway daemon. k3s users are\nunaffected \u2014 `KubernetesSpawner`, `RedisMessageStore`, and the\ngateway sidecar are untouched; the spike only re-routes the\nspawn seam at `orchestrator/concurrent_executor.py:504` through\nthe new protocol. The follow-up issue (drafted in the ADR)\ncaptures plan/implement/pr phases, full conformance matrix, perf\nbudget, k3s adapter, optional `EggHarnessSpawner`, prune verb,\nand fork-based sub-task delegation.", - "test_plan": "- Automated: protocol-conformance unit tests, BRC INV-3/INV-5\n preservation against `InProcessMessageBus`, PreToolUse hook\n semantics, worktree path-escape safety, `EGG_SUBSTRATE=k3s`\n raises the cq-11 TODO marker, and `test_brc_single_cycle.py`\n passes under both `substrate` parameters.\n- Manual: `pip install egg-shared`; install the `egg-sdlc`\n plugin; run `/egg-sdlc ` in a fresh\n Claude Code session; verify the refiner artifact lands under\n `.egg-state/drafts/`; deliberately attempt an out-of-role\n write to confirm the PreToolUse hook denies it; verify HITL\n surfaces through `AskUserQuestion`.", - "manual_steps": "Pre-merge:\n- Reviewer files the follow-up issue using the\n \"Follow-up issue draft\" section in\n `docs/architecture/claude-code-substrate.md`. Documenter is\n role-blocked from `.github/` so it cannot auto-file.\n- Confirm\n `orchestrator/substrate/claude_code/settings.template.json` is\n treated as a template (users opt in by copying it into their\n own `.claude/settings.json`), not silently activated.\n\nPost-merge:\n- Operator installs the `egg-sdlc` plugin and runs the curated\n spike issue end-to-end; reports back qualitative observations\n (subagent context behavior, hook ergonomics, `Agent` tool\n throughput) so the follow-up issue can set a perf/latency\n budget (feedback Q2).", - "context_title": "Strategic plan for #2623 \u2014 substrate-swap refine/plan analysis + BRC history", - "context_description": "Issue #2623's spike-then-plan slice ships under a stacked PR.\nThis context PR carries the refine analysis, the plan, and the\nBRC consensus history that approved each. Reviewers approaching\nthe slice PR can read this PR first for the strategic narrative\n\u2014 why \"Spike then plan\" was chosen over the parallel rollouts,\nhow the eleven cq-N decisions and six feedback answers shape the\nscope, and what the follow-up issue inherits.", - "context_branch": null, - "context_pr_number": null, - "deferred_actions": [] - }, - "feedback": { - "id": "feedback-1", - "phase": "refine", - "questions": [ - { - "id": "Q1", - "question": "Definition of 'conformance passes on \u22655 representative issues': should the \u22655 measurement issues be a fixed curated set (and if so, which? \u2014 please name them or describe selection criteria), or sampled per-CI-run from open issues, or some other rule?", - "answer": "Fixed curated set covering SDLC hot paths: one bug fix, one feature add, one refactor, one infra/script change, one doc change. Specific issue numbers picked during the spike or follow-up. Rationale: random sampling would let CI vary without insight; fixed set gives a stable benchmark you can A/B against substrate changes." - }, - { - "id": "Q2", - "question": "Are there latency or throughput budgets the Claude Code substrate should meet relative to the k3s substrate? (E.g. 'refine phase \u2264 2x k3s latency'.) If so, what are they? If none, is performance acceptable as 'whatever Claude Code gives us so long as it terminates'?", - "answer": "No explicit budget; re-evaluate after the spike with real numbers. The Claude Code substrate is inherently slower per-agent-call (subagent dispatch overhead) and ratios depend on empirical data we don't have yet. Locking 'X*k3s latency' before the spike risks setting an arbitrary line. Set a budget once data exists." - }, - { - "id": "Q3", - "question": "Are there constraints on third-party dependencies the substrate is allowed to introduce \u2014 e.g. any specific Python packages forbidden, any size cap on the marketplace plugin install footprint, any restriction on requiring a developer-mode Claude Code feature flag?", - "answer": "No hard constraints. Soft preferences only: (a) avoid huge transitive dep trees \u2014 prefer existing egg deps where reasonable; (b) no dependency on developer-mode Claude Code feature flags \u2014 substrate must work on stable CC; (c) no strict plugin-size cap, but flag if marketplace install exceeds ~100 MB." - }, - { - "id": "Q4", - "question": "Should the in-process orchestrator support being driven by NON-Claude-Code callers too (e.g. a CLI 'egg-orch local-run --issue 1234' that uses the in-process orchestrator with a non-Claude-Code agent harness)? Or is the Claude Code substrate intentionally Claude-Code-only?", - "answer": "Yes, support non-Claude-Code callers as a secondary goal. If the AgentSpawner interface is right (cq-4 \u2192 synchronous), a third EggHarnessSpawner (subprocess running egg_harness) gives headless / CLI mode (egg-orch local-run --issue 1234) nearly for free, battle-tests the abstraction with three implementations, and unlocks CI usage of the in-process orchestrator without needing Claude Code. Primary use case stays the skill." - }, - { - "id": "Q5", - "question": "Is there appetite to use this issue to also fix structural-cause #5 from #2622 (no tool-use budget signaling) and #6 (no minimum-breadth targets in role files) \u2014 given they require role-file edits that touch shared/prompts/ regardless of substrate \u2014 or should those stay scoped to #2622?", - "answer": "Scope to #2622, not this issue. Both #5 (tool-use budget signaling) and #6 (minimum-breadth targets) are pure prompt edits to shared/prompts/*.md that improve depth on both substrates regardless of the swap \u2014 orthogonal to substrate work. This issue is already large (abstraction layer + two implementations + conformance matrix + ADR, even with spike-first sequencing). Letting #2622 stay the home keeps both issues cohesive." - }, - { - "id": "Q6", - "question": "Any concerns about telemetry / privacy regression when the orchestrator runs in the user's session vs. a k3s pod (e.g. checkpoints written to local filesystem may contain sensitive prompts/contexts that previously stayed in cluster-storage)?", - "answer": "Flag, don't block. Document checkpoint location in install docs (~/.egg-state//). Add a local cleanup verb (egg-state prune or similar) for the Claude Code substrate. No telemetry sent (local-only). Not a strict regression vs k3s (which also wrote checkpoints to local PV / emptyDir); the difference is just persistence horizon and the user is the one who can prune." - } - ], - "submitted": true, - "submitted_by": "human", - "submitted_at": "2026-05-18T23:54:33.877799Z", - "comment_id": null, - "debounce_until": null - }, - "phase_configs": null, - "agent_executions": [] -} diff --git a/.egg-state/contracts/issue-2717.json b/.egg-state/contracts/issue-2717.json deleted file mode 100644 index f50eaafc7e..0000000000 --- a/.egg-state/contracts/issue-2717.json +++ /dev/null @@ -1,1754 +0,0 @@ -{ - "schemaVersion": "1.1", - "issue": { - "number": 2717, - "title": "Issue #2717", - "url": "https://github.com/jwbron/egg/issues/2717" - }, - "pipeline_id": "issue-2717", - "current_phase": "refine", - "acceptance_criteria": [], - "slices": [ - { - "id": "slice-1", - "name": "Bridge gap (flattened) + R2 hook validation + refine reviewers", - "status": "pending", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "escalation_reason": null, - "tasks": [ - { - "id": "task-1-1", - "description": "Add `bin/run_pipeline.py` stage driver under\n`plugins/egg-sdlc/skills/egg-sdlc/bin/`. The driver loads\npipeline state from `.egg-state/contracts/.json`,\ncalls `run_pipeline_in_process(...)` and advances the\ngenerator to its next yield via `generator.send(answer)`\n(where `answer` is the operator's last `AskUserQuestion`\nresponse read from the contract's `pending_hitl.answer`\nfield), serialises the yielded `HITLDecision` back to\n`pending_hitl.decision`, and exits. The skill body in\n`SKILL.md` is updated to call this driver once per stage.\n**The `pending_hitl` envelope shape defined here is the\nshared state-serialization contract between Option B\n(this flattened driver) and Option A (slice-3's daemon\nvariant in TASK-3-2)** \u2014 risk_analyst R17 mitigation.\nDefine the schema with a comment in the file marking\nfields that must remain stable for the daemon variant\nto consume the same envelopes (decision, answer,\nversion, pipeline_id, timestamp).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`bin/run_pipeline.py` exists; it imports\n`run_pipeline_in_process` from\n`orchestrator.substrate.in_process` (line 84); on\ninvocation it round-trips a single `HITLDecision` through\n`.egg-state/contracts/.json` and exits with status 0\nwhen the generator yields, status 1 on internal error.\nThe `SKILL.md` \"Usage\" section is updated to call this\ndriver in a loop. The `pending_hitl` envelope schema is\ndocumented as a stable contract (top-of-file comment\nlisting the contract fields).", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-2", - "description": "Update `SKILL.md` to call the new `bin/run_pipeline.py`\ndriver in a loop: invoke the driver, read\n`pending_hitl.decision`, render via `AskUserQuestion`,\nwrite the operator's answer to `pending_hitl.answer`,\nloop. Replace the \"Walking-skeleton bridge gap\" callout\n(line 97-102) with a brief \"How the flattened bridge\nworks\" subsection that names `pending_hitl` as the\nsingle-yield carrier.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`SKILL.md` no longer carries the \"Walking-skeleton bridge\ngap\" callout for refine-phase; the new usage section\ndocuments the flattened stage-script loop; the\n`allowed-tools` frontmatter still includes\n`AskUserQuestion`, `Read`, `Bash(python3 *:*)` so the\nloop can actually run.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-3", - "description": "Add `test_bridge_flattened_round_trip.py` under\n`integration_tests/regression/`. Test invokes\n`bin/run_pipeline.py` twice against a deterministic\npipeline id: first invocation produces a preflight\n`HITLDecision`; test writes the answer to\n`pending_hitl.answer`; second invocation produces a\nrefine-gate `HITLDecision`. Validates that generator\nstate survives via the contract-state round-trip even\nthough the Python process exits between calls.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "The test runs in <30s, does not require a real Anthropic\nAPI call (uses harness fakes for the subagent spawn), and\nasserts: (a) `pending_hitl.decision.question` matches the\npreflight question on first run; (b) after writing\n`pending_hitl.answer`, the second run consumes it and\nyields the refine-gate decision.", - "files_affected": [ - "integration_tests/regression/test_bridge_flattened_round_trip.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-4", - "description": "Add `reviewer_refine.md` and `reviewer_agent_design.md`\nrole rubric files under\n`plugins/egg-sdlc/skills/egg-sdlc/agents/`. Mirror the\nshape of the existing `refiner.md` (frontmatter + markdown\nbody). Pull the rubric content from the corresponding k3s\nprompt sources under `shared/prompts/` so the substrate\nversion stays in sync with what the k3s reviewer sees.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Both files exist with valid frontmatter (`name`,\n`description`); both bodies start with a \"You are the\n**reviewer_refine** running on the Claude Code substrate\"\n(or analogous) preamble; both reference the same review\ncriteria the k3s reviewer uses.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-5", - "description": "Add `test_pretooluse_hook_nested.py` under\n`integration_tests/regression/`. **The R2 question\n(\"does the hook resolve role under nested dispatch?\")\ncan only be answered when subagents run via Claude\nCode's Agent-tool dispatch \u2014 the harness re-host model\n(`ClaudeCodeSpawner` per cq-3) bypasses the PreToolUse\nhook entirely because `shared/egg_harness/client.py:60-150`\nuses its own `ToolRegistry.set_permission_callback(...)`\nand `grep -rn \"PreToolUseHookPolicy|hook_entry\"\nshared/egg_harness/` returns zero hits.** This task\ntherefore uses the test-only Agent-tool dispatch fake\nfrom TASK-1-9: the test spawns a parent fake-subagent\nwith `EGG_AGENT_ROLE=architect` and a nested child fake-\nsubagent with `EGG_AGENT_ROLE=tester`, and asserts the\nPreToolUse hook (invoked via the fake's\n`pre_tool_use_callback`) denies a write to\n`orchestrator/foo.py` from the child even though the\nparent's role would allow it. Records the verdict to\n`.egg-state//r2-verdict.json` so slice 5's\ncontingent R15 migration task can read it. **This is the\ncq-5 early-spike gating test for the rollout.** Production\ndispatch remains on `ClaudeCodeSpawner` (harness re-host)\nper cq-3 \u2014 the Agent-tool dispatch fake in TASK-1-9 is\ntest infrastructure only, not a production spawner.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Test runs in <60s; uses the TASK-1-9 fake for nested\ndispatch (no real Anthropic API call); asserts\n`hook_entry.decide(...)` returns\n`{\"action\": \"deny\", \"message\": \"...\"}` for the child's\nwrite attempt when the parent's role would allow it;\nwrites `{\"r2_verdict\": \"pass\"}` or\n`{\"r2_verdict\": \"fail\", \"reason\": \"...\"}` to\n`.egg-state//r2-verdict.json`. The test\ndocstring documents the empirical-vs-test-fake limitation\n(the test confirms hook *logic* is correct given accurate\n`EGG_AGENT_ROLE` propagation; whether Claude Code itself\npropagates `EGG_AGENT_ROLE` correctly under real nested\nAgent-tool dispatch is verifiable only by running real\nClaude Code, which the in-sandbox test cannot do \u2014 but\nwhen cq-3 flips to Agent-tool dispatch in a future issue,\nthe production path becomes the empirical answer to that\nremaining half of R2).", - "files_affected": [ - "integration_tests/regression/test_pretooluse_hook_nested.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-9", - "description": "Add a minimal test-only nested-Agent-tool dispatch fake\nunder `integration_tests/regression/_agent_tool_fake.py`.\nSimulates Claude Code's `Agent` tool by spawning a\nsubprocess with controlled `EGG_AGENT_ROLE` env var per\ndispatch; each fake-subagent has a\n`pre_tool_use_callback` that invokes\n`orchestrator.substrate.claude_code.hook_entry.decide(...)`\nwith the tool input. **This is test infrastructure\nONLY** \u2014 it does NOT become a production spawner and is\nNOT registered in `select_substrate`. It exists so\nTASK-1-5 can drive a deterministic nested-dispatch\nscenario without a real Claude Code session. The shared\n`pending_hitl` envelope schema invented in TASK-1-1 also\nflows through this fake so slice 3's daemon variant\n(TASK-3-2) inherits the same state-serialization contract\n(closes risk_analyst R17 dual-bridge concern).\n\n**Role assignment**: the underscored helper-style name\n(mirroring the existing\n`integration_tests/regression/_helpers.py`) places this\nfile outside the `test_*.py` / `conftest.py` glob, so it\nis **coder-owned**, not tester-owned (verified via\n`mcp__sdlc__check_file_restriction`). This is the same\nowner the existing `_helpers.py` has today; the test\nfile that consumes it (TASK-1-5's\n`test_pretooluse_hook_nested.py`) remains tester-owned.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`_agent_tool_fake.py` exists; exposes a\n`dispatch(parent_role, child_role, write_target)`\nhelper; the helper invokes `hook_entry.decide(...)` via\nthe simulated child's `pre_tool_use_callback` and\nreturns the hook verdict. Documented in the module\ndocstring as test-only; protected with a top-of-file\n`if not __name__.startswith(\"integration_tests\")` import\nguard so it can't be silently imported by production\ncode.", - "files_affected": [ - "integration_tests/regression/_agent_tool_fake.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-6", - "description": "Update `_load_egg_sdlc_role_rubric` in\n`orchestrator/substrate/__init__.py:232` to remove the\n\"spike only ships refiner rubric\" `ValueError` (lines\n280-284) when the role is `reviewer_refine` or\n`reviewer_agent_design`. The loader continues to raise\n`ValueError` for plan/implement roles until slice 2/3\nadds their rubrics (this preserves the structured-error\ncontract for missing rubrics \u2014 the test in TASK-1-7\ncovers this). **Depends on TASK-1-4 having landed the\ntwo new rubric .md files first** \u2014 if a coder picks up\nTASK-1-6 before TASK-1-4 is merged, the loader returns\ntext from a missing file. The implementer should\nsequence TASK-1-4 \u2192 TASK-1-6 within the slice.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`_load_egg_sdlc_role_rubric(REVIEWER_REFINE)` returns the\nmarkdown body of `reviewer_refine.md`;\n`_load_egg_sdlc_role_rubric(REVIEWER_AGENT_DESIGN)`\nreturns the body of `reviewer_agent_design.md`;\n`_load_egg_sdlc_role_rubric(ARCHITECT)` still raises\n`ValueError` with the \"follow-up issue per cq-11\" hint\nupdated to \"follow-up slice 2\".", - "files_affected": [ - "orchestrator/substrate/__init__.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-7", - "description": "Add unit tests for the updated rubric loader: assert that\nthe two refine reviewer roles load successfully, and that\nplan/implement roles still raise `ValueError` with the\nupdated diagnostic message. Test file lives under\n`shared/tests/` next to the existing\n`test_substrate_interfaces.py` and `test_claude_code_spawner.py`\nper the repo's substrate-tests convention.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "New test file `shared/tests/test_rubric_loader.py`\ncontains four test cases: load refiner (existing,\nregression), load reviewer_refine, load\nreviewer_agent_design, load architect raises ValueError.", - "files_affected": [ - "shared/tests/test_rubric_loader.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-1-8", - "description": "Update the ADR at\n`docs/architecture/claude-code-substrate.md` to reflect:\n(a) the flattened bridge mechanism (replacing the\n\"Walking-skeleton bridge gap\" callout); (b) the R2\nverdict (point to `r2-verdict.json` and TASK-1-5);\n(c) the refine-team expansion (refiner + 2 reviewers now\non the substrate); (d) the \"Rollout deltas\" section\n(line 272-287) marked off the first bullet (bridge gap\nclosed for refine/plan).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "ADR no longer references the bridge gap as deferred for\nrefine-phase; \"Rollout deltas\" first bullet shows\ncompleted-with-strikethrough or moved to a \"Completed in\nthis rollout\" subsection; the \"Open empirical question\n(R2)\" section either documents the pass result or\ndocuments the fail result + slice 5 contingency.", - "files_affected": [ - "docs/architecture/claude-code-substrate.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - } - ], - "dependencies": [], - "serialized_chain_order": [], - "parent_branch_at_creation": null, - "commit": null, - "review_feedback": [] - }, - { - "id": "slice-2", - "name": "Plan-phase substrate (3 producers + 1 reviewer)", - "status": "pending", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "escalation_reason": null, - "tasks": [ - { - "id": "task-2-1", - "description": "Add `_run_plan` stage to\n`_InProcessOrchestrator.run()` in\n`orchestrator/substrate/in_process.py`. The stage is\ndispatched after the refine HITL gate when the operator\nchooses \"continue to plan\". It spawns the three\nproducers via the existing `concurrent_executor.py`\nseam (line 569) so multi-producer BRC runs through the\n`InProcessMessageBus`. After producers reach\n`CONSENSUS_PROPOSE`, `reviewer_plan` is spawned for the\nACK/NACK cycle. The stage yields a plan-HITL decision\nwhen consensus is reached.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`run_pipeline_in_process(...)` no longer raises\n`NotImplementedError` when the operator advances past\nrefine; the plan stage spawns 3 producers concurrently\nvia the executor; reviewer_plan is spawned after each\n`CONSENSUS_PROPOSE`; the stage yields a plan-HITL\ndecision after CONSENSUS_CONFIRMED on every producer\nedge; the existing refine path still works.", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-2-2", - "description": "Extend `_load_egg_sdlc_role_rubric`\n(`orchestrator/substrate/__init__.py:232`) to load the\nnew plan-team rubrics (architect, task_planner,\nrisk_analyst, reviewer_plan); the loader continues to\nraise `ValueError` for implement-team roles.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Loader returns rubric bodies for all four plan-team\nroles; implement-team roles still raise `ValueError`\nwith the \"follow-up slice 3\" hint.", - "files_affected": [ - "orchestrator/substrate/__init__.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-2-3", - "description": "Add four plan-team rubric files under\n`plugins/egg-sdlc/skills/egg-sdlc/agents/`:\n`architect.md`, `task_planner.md`, `risk_analyst.md`,\n`reviewer_plan.md`. Mirror `refiner.md`'s shape; body\ncontent mirrors what the k3s prompts in\n`shared/prompts/` produce for each role.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "All four files exist with valid frontmatter; bodies\nreference the substrate context (\"You are the\n**architect** running on the Claude Code substrate of\negg's SDLC pipeline.\") and the role's specific outputs\n(architect \u2192 architecture analysis; task_planner \u2192\nslice DAG plan; risk_analyst \u2192 risk assessment;\nreviewer_plan \u2192 ACK/NACK on the three producer\nartifacts).", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-2-4", - "description": "Add `test_inprocess_plan_brc.py` under\n`integration_tests/regression/`. Test boots\n`run_pipeline_in_process` against a deterministic\npipeline id with harness-faked subagents; advances past\nthe refine HITL gate; asserts the plan stage spawns\n3 producers + 1 reviewer; asserts CONSENSUS_CONFIRMED\nfires on every producer edge; asserts the stage yields\na plan-HITL decision with the expected fields.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Test runs in <120s; uses harness fakes; reaches\nCONSENSUS_CONFIRMED for all three plan-phase BRC edges\n(architect \u2192 reviewer_plan, task_planner \u2192\nreviewer_plan, risk_analyst \u2192 reviewer_plan); asserts\nplan-HITL decision is yielded.", - "files_affected": [ - "integration_tests/regression/test_inprocess_plan_brc.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-2-5", - "description": "Contingent on slice 1's R2 verdict (see\n`.egg-state//r2-verdict.json` from\nTASK-1-5). If R2 = fail, wire **agent-side**\n(sandbox-tool-handler) policy enforcement parallel to\nthe PreToolUse hook: extend\n`sandbox/egg_agent_tools/handlers/restrictions.py`\n(which already exposes\n`check_file_restriction(req)` at line 70) so that\nevery write-bearing tool call passes through it before\nthe agent burns a tool turn, returning the gateway's\n`403 restricted_path_modified` shape on violation. This\nis cq-6 option 2 from #2623 (\"MCP-validator-side\nenforcement\"); the correct surface per the plan\nreviewer's audit is the in-sandbox tool handler, NOT\n`orchestrator/mcp_tools.py` (which is the\norchestrator-side MCP server, a different surface). If\nR2 = pass, this task is a no-op (close with note). Tests\nfor this code path land in TASK-2-6 (tester-owned).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "If R2 fail:\n`sandbox/egg_agent_tools/handlers/restrictions.py` adds\na `validate_write_target(req)` helper (or extends an\nexisting one) that imports\n`build_agent_patterns(role)` from\n`shared/egg_restrictions/patterns.py:768` and returns\nthe gateway's `403 restricted_path_modified` shape\nmirroring `gateway/phase_filter.py:1061`. If R2 pass:\ntask closed with note \"no-op: hooks resolve role\ncorrectly; structural enforcement remains hook-side\".", - "files_affected": [ - "sandbox/egg_agent_tools/handlers/restrictions.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-2-6", - "description": "Add tests for the agent-side policy enforcement landed\nin TASK-2-5. Only meaningful if slice 1's R2 verdict\nwas fail; otherwise the test is a no-op regression guard\nasserting the validator helper does NOT change the\nresponse shape for in-allow-list writes. File lives\nunder `tests/sandbox/egg_agent_tools/`.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "New test file\n`tests/sandbox/egg_agent_tools/test_restrictions_validator.py`\ncovers: (R2 fail) validator denies a cross-role write\ntool call and the response matches the gateway's\n`403 restricted_path_modified` shape; (R2 pass) asserts\nthe validator helper is a no-op for in-allow-list\nwrites (the contingency is documented in the test\ndocstring).", - "files_affected": [ - "tests/sandbox/egg_agent_tools/test_restrictions_validator.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-2-7", - "description": "Update `SKILL.md` to drop the \"Plan phase deferred\"\ncallout in \"What's NOT in this skill\" (line 163); add a\nnew \"Plan phase\" subsection naming the four roles and\nthe plan HITL gate.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`SKILL.md` documents the plan phase as supported; the\n\"What's NOT in this skill\" section no longer lists\nplan-phase deferral; the plan-HITL gate is named.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - } - ], - "dependencies": [ - "slice-1" - ], - "serialized_chain_order": [], - "parent_branch_at_creation": null, - "commit": null, - "review_feedback": [] - }, - { - "id": "slice-3", - "name": "Implement-phase substrate (3 producers + 5 reviewers) + daemon HITL bridge", - "status": "pending", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "escalation_reason": null, - "tasks": [ - { - "id": "task-3-1", - "description": "Add `_run_implement` stage to\n`_InProcessOrchestrator.run()`. Dispatched after the\nplan HITL gate when the operator chooses \"continue to\nimplement\". Spawns 3 producers + 5 reviewers; manages\nmulti-cycle BRC with NACK aggregation, conditional ACK\n(pre-merge obligations), RE_REVIEW, CONFIRMED.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "The implement stage spawns 3 producers + 5 reviewers\n(per `_PHASE_ROLES[\"implement\"]` at\n`shared/egg_contracts/agent_roles.py:1173` and\n`_PHASE_REVIEWERS[\"implement\"]` at\n`:1184`); BRC mechanics survive the substrate\ntransition (INV-1..6 covered); the stage yields\nimplement-HITL decisions for each producer's PR.", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-2", - "description": "Add daemon-style HITL bridge under\n`orchestrator/substrate/claude_code/hitl_daemon.py`:\na long-lived Python process that owns the\n`run_pipeline_in_process` generator across multiple\n`AskUserQuestion` round-trips. The skill talks to it\nvia JSON-RPC envelopes over a UNIX domain socket at\n`.egg-state//sock`. Per cq-1 Option C,\nthis is the implement-phase variant \u2014 refine/plan keep\nthe flattened path from slice 1. **Daemon must detach\nfrom the calling Bash session** via\n`subprocess.Popen(start_new_session=True)` (or\nequivalent `os.setsid()` after fork); otherwise the\ndaemon dies when the skill's `Bash(python3 ...)` step\nexits, defeating the \"long-lived\" property. The daemon\nconsumes the `pending_hitl` envelope schema defined in\nTASK-1-1 so the two bridges share a state-serialization\ncontract (risk_analyst R17 mitigation).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Daemon starts on first `python3 bin/run_pipeline.py\n--daemon` invocation; the launcher uses\n`start_new_session=True` (or `os.setsid()`) so the\ndaemon survives the calling Bash's exit; binds to\n`.egg-state//sock`; serves JSON-RPC\nenvelopes: `{\"method\": \"next_decision\"}` returns the\nnext `HITLDecision`; `{\"method\": \"send_answer\", \"answer\":\n...}` resumes the generator; `{\"method\": \"shutdown\"}`\njoins background threads cleanly via `GeneratorExit`.\nSocket lifecycle is per-pipeline; the daemon\nself-terminates on pipeline completion. The\nlong-lived-across-Bash-exits property is asserted in\nTASK-3-8's test.", - "files_affected": [ - "orchestrator/substrate/claude_code/hitl_daemon.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-3", - "description": "Update `bin/run_pipeline.py` (from TASK-1-1) to add a\n`--daemon` flag that connects to / launches the\n`hitl_daemon`. The flattened single-yield path stays\nthe default for refine/plan; `--daemon` activates for\nimplement-phase.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`bin/run_pipeline.py --daemon` launches the daemon if\nnot running, connects to its socket, ferries one\ndecision-answer round-trip per invocation; exit code\n0 on success; the SKILL.md body picks the right mode\nbased on the current phase.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-4", - "description": "Add three producer rubrics: `coder.md`, `tester.md`,\n`documenter.md` under\n`plugins/egg-sdlc/skills/egg-sdlc/agents/`. Mirror\n`refiner.md`'s shape; body content mirrors what the\nk3s prompts produce for each role.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "All three files exist with valid frontmatter; bodies\nname the substrate; bodies reference the file-write\nrestrictions per `shared/egg_restrictions/patterns.py:768`\nso the operator can sanity-check the hook's behavior.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/coder.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/tester.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/documenter.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-5", - "description": "Add five reviewer rubrics:\n`reviewer_code.md`, `reviewer_code_holistic.md`,\n`reviewer_contract.md`, `reviewer_security.md`,\n`reviewer_concurrency.md` under\n`plugins/egg-sdlc/skills/egg-sdlc/agents/`.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "All five files exist with valid frontmatter and bodies\nreferencing the substrate; review criteria mirror the\nk3s reviewer prompts.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code_holistic.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_contract.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_security.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_concurrency.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-6", - "description": "Extend `_load_egg_sdlc_role_rubric` to load all eight\nnew implement-team rubrics; the loader no longer raises\n`ValueError` for any production role (overseer,\ninspector, autofixer, conflict_resolver remain\nintentionally unhandled).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Loader returns rubric bodies for coder, tester,\ndocumenter, and all five reviewer roles; the existing\nrefine + plan rubrics still load.", - "files_affected": [ - "orchestrator/substrate/__init__.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-7", - "description": "Add `test_inprocess_implement_brc.py` under\n`integration_tests/regression/`. Drives a full\nimplement-phase BRC cycle through harness fakes;\nasserts ACK / NACK / RE_REVIEW / CONFIRMED transitions\nacross 3 producers + 5 reviewers; covers NACK\naggregation (two reviewers NACK same producer);\ncovers conditional ACK with pre-merge obligation.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Test runs in <240s; uses harness fakes; asserts:\n(a) all 3 producer \u00d7 5 reviewer edges reach\nCONSENSUS_CONFIRMED; (b) NACK aggregation triggers\norchestrator HTTP 409 on stale-version re-propose; (c)\nconditional ACK surfaces in the produced PR body's\npre-merge obligations section.", - "files_affected": [ - "integration_tests/regression/test_inprocess_implement_brc.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-8", - "description": "Add `test_daemon_bridge.py` under\n`integration_tests/regression/`. Validates the daemon\nround-trips JSON-RPC envelopes; validates that\ngenerator state survives between AskUserQuestion calls\n(simulated by separate JSON-RPC clients); validates\nclean shutdown via `GeneratorExit`.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Test runs in <60s; spawns the daemon via `subprocess`;\nasserts \u22653 decision-answer round-trips through the\nsocket; asserts daemon process exits cleanly on\n`{\"method\": \"shutdown\"}`; asserts no leaked\nbackground threads (the test process's thread count\nreturns to baseline).", - "files_affected": [ - "integration_tests/regression/test_daemon_bridge.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-9", - "description": "Add empirical metrics collection: instrument\n`_InProcessOrchestrator` to record per-role subagent\ninvocations, wall-clock latency, peak context size to\n`.egg-state//substrate-metrics.json`.\nSlice 5's cq-3 Agent-tool dispatcher decision reads\nthis; slice 5's cost cap (cq-6) also reads\ninvocations_count from this file.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "After a full implement-phase BRC cycle, the metrics\nfile contains: one entry per spawned subagent with\n{role, wall_clock_seconds, context_tokens_peak,\nretry_count}; total invocations field at the top\nlevel; format documented in `_InProcessOrchestrator`\nclass docstring.", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-3-10", - "description": "Update `SKILL.md` to document implement-phase support;\nadd a \"When to use daemon mode\" subsection (refine /\nplan stay flattened; implement uses daemon).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`SKILL.md` covers all three phases supported so far\n(refine, plan, implement); the \"What's NOT in this\nskill\" section's implement-phase line is removed; the\ndaemon mode is documented with a snippet showing the\nUNIX socket round-trip.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - } - ], - "dependencies": [ - "slice-2" - ], - "serialized_chain_order": [], - "parent_branch_at_creation": null, - "commit": null, - "review_feedback": [] - }, - { - "id": "slice-4", - "name": "PR-phase substrate + 5-issue conformance matrix + scope-fence removal", - "status": "pending", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "escalation_reason": null, - "tasks": [ - { - "id": "task-4-1", - "description": "Add `_run_pr` stage to\n`_InProcessOrchestrator.run()`. The pr-phase has no\nproducer/reviewer roles; it's pure orchestration:\ncreate the stacked PRs per slice, render the context\nPR with refine/plan artifacts and BRC consensus\nhistory per #2548, render pre-merge obligations from\nthe contract's `pr.deferred_actions`.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`_run_pr` runs after every slice's implement-phase\nCONSENSUS_CONFIRMED; creates one PR per slice; creates\nthe context PR at the root of the stack; injects\npre-merge obligation rendering; the stage yields a\npr-HITL gate (\"approve and merge / request changes /\nabort\").", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-4-2", - "description": "Remove the walking-skeleton scope fence from\n`orchestrator/substrate/in_process.py`. Two locations\nmust be touched: (a) the call site at `:212`\n(`self._maybe_fence(answer)` inside `_run`) and (b) the\nmethod definition at `:807-826` (`def _maybe_fence(...)`\nand its `NotImplementedError` body). Removing only the\ncall site leaves dead code; both must be deleted.\nAlso update the docstring at lines 92-125 to drop the\n\"raises NotImplementedError for plan / implement / pr\"\nlanguage. With pr-phase wired, the operator can chain\nall four phases freely.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`_maybe_fence` call site at `:212` AND method\ndefinition at `:807-826` are both deleted; the\ngenerator no longer raises `NotImplementedError` for\nany phase advancement; the docstring reflects the new\nfour-phase support; existing tests for the fence are\ndeleted (they're obsolete) and replaced with the\nintegration test in TASK-4-4.", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-4-3", - "description": "Apply the substrate parametrize fixture from\n`integration_tests/regression/conftest.py:623` to every\nregression test that is substrate-portable. Each test\nmust declare via the `substrate` fixture; the fixture\ncontinues to skip the claude-code dimension under\n`_in_sandbox_agent_context()` (line 636-641).\nSubstrate-portable: any test that does not transitively\ndepend on `egg_stack` or `gateway_url` fixtures.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Every test in `integration_tests/regression/` either\n(a) consumes the `substrate` fixture, (b) is marked\n`@pytest.mark.skip(reason=\"k3s-only\" | \"claude-code-only\")`,\nor (c) is exempt with an inline comment explaining why\nit's not portable. The CI matrix runs both dimensions.", - "files_affected": [ - "integration_tests/regression/test_brc_concurrency.py", - "integration_tests/regression/test_brc_edge_cases.py", - "integration_tests/regression/test_brc_gap_audit.py", - "integration_tests/regression/test_brc_phase_timeout.py", - "integration_tests/regression/test_brc_single_cycle.py", - "integration_tests/regression/test_hitl_round_trip.py", - "integration_tests/regression/test_long_name_round_trip.py", - "integration_tests/regression/test_message_bus_routing.py", - "integration_tests/regression/test_recovery_invariants.py", - "integration_tests/regression/test_salvage_ttl_sweep.py", - "integration_tests/regression/test_slice_restart_branch_invariants.py", - "integration_tests/regression/test_slice_spawn_env_threading.py", - "integration_tests/regression/test_start_pipeline_recovery.py", - "integration_tests/regression/test_unpushed_commit_salvage.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-4-4", - "description": "Add `test_conformance_matrix.py` under\n`integration_tests/regression/`. Parametrize over the\n5 curated issues: [2714, 2474, 2261, 2705, 2718] X 2\nsubstrates = 10 parametrize axes. **Conformance fixture\nmechanism**: each axis uses per-subagent `MagicMock`\nstubs in the same style as\n`integration_tests/regression/test_substrate_smoke.py:56`\n(hardcoded `stdout=\"ok\"`, no recorded transcripts) \u2014\nthis is option (b) per the plan reviewer's audit, the\ncheapest path that keeps the matrix in-sandbox-runnable\nand avoids the chicken-and-egg of \"which task records\nthe transcripts the matrix consumes\". The axis asserts:\n(1) `run_pipeline_in_process` reaches a pr-HITL gate\nwithout raising for the issue's recorded SDLC shape\n(bug fix / feature / refactor / infra / doc); (2) the\ntwo substrate dimensions agree on the produced\nartifact paths (`.egg-state/drafts/-analysis.md`,\n`.egg-state/drafts/-plan.md`). The matrix is about\nreplaying the SDLC *shape*, not the verbatim text;\nper feedback Q1 the issues are fixed regardless of\ncurrent GitHub state (note: #2714 closed since the\nrefine analysis was written, but this is irrelevant to\nthe matrix per Q1's rationale). Mark all 10 axes as\n`pytest.mark.slow`.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Test exists with 10 parametrize axes (5 issues x 2\nsubstrates); uses MagicMock-style stubs (no recorded\ntranscripts); runs only under `pytest --slow`; each\naxis completes within the 3-hour per-phase ceiling\nenforced by TASK-4-5's hook; both substrate dimensions\nagree on the produced artifact paths for every curated\nissue. The test docstring documents the\nMagicMock-vs-real-transcript trade-off and notes that\n#2714's closed state is irrelevant per feedback Q1.", - "files_affected": [ - "integration_tests/regression/test_conformance_matrix.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-4-5", - "description": "Add a `pytest_runtest_setup` hook to\n`integration_tests/regression/conftest.py` that fails\nany single phase exceeding the 3-hour ceiling\n(feedback Q2). The hook reads the per-phase\nwall-clock from `.egg-state//substrate-metrics.json`\n(populated by TASK-3-9) and fails fast.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Hook lands in `conftest.py`; new test\n`test_phase_ceiling.py` verifies a synthetic 4-hour\nphase fails the gate; documented in the conftest\nmodule docstring.", - "files_affected": [ - "integration_tests/regression/conftest.py", - "integration_tests/regression/test_phase_ceiling.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-4-6", - "description": "Update `SKILL.md` to remove the \"Walking-skeleton\nscope\" callout entirely (lines 8-12 in the current\nfile); the skill now supports all four phases. Remove\nthe \"What's NOT in this skill\" section's pr-phase\nline; remove the bridge-gap callout entirely.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`SKILL.md` reflects full four-phase support; no\nwalking-skeleton language remains; the front matter's\n`description` is updated to remove the \"walking-skeleton\nspike\" language.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/SKILL.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-4-7", - "description": "Update the ADR's \"Rollout deltas\" section\n(`docs/architecture/claude-code-substrate.md:272-287`)\nto mark off bullets: plan/implement/pr phase rollout\n(line 276), conformance matrix (line 278), perf\nbudget (line 279). Move the satisfied bullets to a\nnew \"Completed in #2717 rollout\" section.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "ADR no longer lists the three phase-rollout bullets,\nconformance, and perf-budget bullets as open; a new\n\"Completed\" section summarises what slice 4 shipped.", - "files_affected": [ - "docs/architecture/claude-code-substrate.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - } - ], - "dependencies": [ - "slice-3" - ], - "serialized_chain_order": [], - "parent_branch_at_creation": null, - "commit": null, - "review_feedback": [] - }, - { - "id": "slice-5", - "name": "Hardening (cost cap + EggHarnessSpawner + R15 contingent + fork primitive + ADR + v0.x marker drop)", - "status": "pending", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "escalation_reason": null, - "tasks": [ - { - "id": "task-5-1", - "description": "Implement `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost\ncap with default 200 (cq-6 = Option 2). Tracked on\n`_InProcessOrchestrator._invocation_count`; raises\n`RuntimeError(\"cost cap exceeded; current=,\ncap=\")` past the cap. The cap reads\n`.egg-state//substrate-metrics.json` to\nstay in sync with TASK-3-9's metrics.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`_InProcessOrchestrator._invocation_count` increments\non every spawn; cap default = 200; env override\nworks; the cap raises BEFORE the over-cap spawn\nexecutes (so the operator's bill stops at the cap).", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-2", - "description": "Add `EggHarnessSpawner` under\n`orchestrator/substrate/egg_harness/spawner.py`. The\nspawner satisfies the `AgentSpawner` Protocol (third\nimplementation, validating cq-7's stability claim\nper feedback Q3). It drives `egg_harness.run_agent`\nvia `subprocess.Popen` per role; captures\n`commit_sha` via `git rev-parse HEAD` immediately\nafter the subprocess returns (per\n`orchestrator/substrate/claude_code/spawner.py:201`\nfor the existing pattern).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`EggHarnessSpawner` exists and implements\n`AgentSpawner`; `select_substrate(env={\"EGG_SUBSTRATE\":\n\"egg-harness\"})` returns a bundle with this spawner;\na `test_egg_harness_spawner.py` test asserts the\nthird spawner round-trips a fake agent and captures\n`commit_sha`.", - "files_affected": [ - "orchestrator/substrate/egg_harness/__init__.py", - "orchestrator/substrate/egg_harness/spawner.py", - "orchestrator/substrate/__init__.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-3", - "description": "Add `cmd_local_run` subcommand to\n`orchestrator/cli.py`. Mirrors the shape of existing\nsubcommands (`cmd_pipelines_status` at\n`orchestrator/cli.py:541` is the closest reference).\nUsage: `egg-orch local-run --issue --repo\n`. Boots\n`run_pipeline_in_process(...)` against an\n`EggHarnessSpawner`-backed substrate; prints\n`HITLDecision` envelopes to stdout in a JSON\nenvelope; reads operator answers from stdin (the\nenvelope shape mirrors `.egg-state/contracts/.json#pending_hitl`).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`egg-orch local-run --help` documents the\nsubcommand; running `egg-orch local-run --issue\n2718 --repo jwbron/egg` advances the pipeline\nheadlessly; pipeline completes through all four\nphases against the curated doc-only issue.", - "files_affected": [ - "orchestrator/cli.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-4", - "description": "Add `test_egg_harness_spawner.py` under\n`integration_tests/regression/`. Asserts the spawner\nsatisfies `AgentSpawner`; round-trips a fake agent\n(no real Anthropic call); captures `commit_sha`;\nworks against `egg-orch local-run` end-to-end against\na curated issue (uses harness fakes for the\nsubagents themselves, but exercises the real CLI\nshell).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Test runs in <60s; asserts the three Protocol methods\n(`spawn`, `cleanup`, etc.); asserts the CLI exits 0\non the curated doc-only issue (#2718).", - "files_affected": [ - "integration_tests/regression/test_egg_harness_spawner.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-5", - "description": "Add fork-based delegation primitive on\n`_InProcessOrchestrator`:\n`fork_subagent(parent_role: AgentRole, sub_task: str)\n-> SubAgentResult`. **Implementation stays on the\nharness re-host model per cq-3 (decide empirically\npost-implement)**: the fork spawns a child via\n`subprocess.Popen` running a fresh\n`egg_harness.run_agent` invocation in a clean Python\nprocess (NOT via Claude Code's Agent tool with\n`subagent_type=\"general-purpose\"` \u2014 that would commit\nto Agent-tool dispatch ahead of the cq-3 empirical\nverdict). The child does the sub-task with a focused\nprompt; the parent receives the summary back. Mirrors\nhow a human delegates (\"read all files matching X and\nsummarize\") per cq-10's deferred half from #2623. If\ncq-3's empirical verdict in slice-5's ADR refresh\n(TASK-5-9) recommends migrating to Agent-tool\ndispatch, the fork primitive's implementation will\nchange accordingly in a follow-up \u2014 not in this slice.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "`fork_subagent` exists on\n`_InProcessOrchestrator`; spawns the child via\n`subprocess.Popen(...)` of `egg_harness.run_agent`\n(NOT via Agent tool); the child inherits the parent's\nrole via `EGG_AGENT_ROLE` env so the harness's own\npermission callback continues to enforce file-write\nrestrictions; the return is a `SubAgentResult`\ndataclass with `summary` and `tool_calls` fields.\nUnit-tested via harness fakes (TASK-5-8). The\ndocstring documents the cq-3 alignment.", - "files_affected": [ - "orchestrator/substrate/in_process.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-6", - "description": "Drop the `INTERFACE STABILITY: v0.x unstable.` marker\nand the `# v0.x \u2014 unstable until \u22653 roles exercise`\ncomments from the four substrate protocol modules:\n`orchestrator/substrate/spawner.py`, `message_bus.py`,\n`policy.py`, `worktree.py`. cq-7 selected \"drop the\nmarker after \u22653 roles exercise the claude-code side\";\nby slice 4 the substrate has exercised \u226515 roles\n(refiner + 2 refine reviewers + 3 plan producers +\n1 plan reviewer + 3 implement producers + 5 implement\nreviewers). Regression-guard tests for the absence of\nthese markers land in TASK-5-10 (tester-owned).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Grep for `INTERFACE STABILITY: v0.x unstable.` and\n`v0.x \u2014 unstable` returns zero hits across\n`orchestrator/substrate/`. The module docstrings now\ndocument the protocol as stable.", - "files_affected": [ - "orchestrator/substrate/spawner.py", - "orchestrator/substrate/message_bus.py", - "orchestrator/substrate/policy.py", - "orchestrator/substrate/worktree.py" - ], - "role": "coder", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-10", - "description": "Add a regression guard test that asserts the v0.x\nunstable markers are absent from all four substrate\nprotocol modules. File lives under `shared/tests/`\nnext to existing `test_substrate_interfaces.py` per\nthe repo's substrate-tests convention.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "New test file\n`shared/tests/test_substrate_protocol_stability.py`\nopens each of the four protocol module files and\nasserts neither `INTERFACE STABILITY: v0.x unstable.`\nnor `v0.x \u2014 unstable` substring is present in the\nmodule text. A separate test asserts the module\ndocstring contains positive stability language\n(e.g., \"stable v1 interface\").", - "files_affected": [ - "shared/tests/test_substrate_protocol_stability.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-7", - "description": "Contingent on slice 1's R2 verdict (read\n`.egg-state//r2-verdict.json` from\nTASK-1-5). If R2 = fail, migrate every role rubric\nunder `plugins/egg-sdlc/skills/egg-sdlc/agents/.md`\nto a real `.claude/agents/.md` definition with\nfrontmatter tool restrictions per\n`build_agent_patterns()`. If R2 = pass, this\ntask is a no-op (close with note in the slice's BRC\nhistory).", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "If R2 fail: every rubric file gains frontmatter with\n`allowed-tools` reflecting the role's allow-list per\n`shared/egg_restrictions/patterns.py:768`; structural\ntool-restriction enforcement now layers on top of the\nPreToolUse hook; a new test under\n`tests/plugins/egg_sdlc/test_role_frontmatter.py`\nasserts every rubric's `allowed-tools` matches its\n`build_agent_patterns(role).allow` set. If R2 pass:\ntask closed with note.", - "files_affected": [ - "plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/coder.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/tester.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/documenter.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code_holistic.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_contract.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_security.md", - "plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_concurrency.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-8", - "description": "Add `test_cost_cap.py` and `test_fork_subagent.py`\nunder `integration_tests/regression/`. Cost-cap test\nasserts the default (200) and env override behavior;\nfork test asserts child summary returns to parent\nwithout leaking the child's full tool-call history.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "Both tests pass; cost cap raises `RuntimeError` past\n200 and at the env override; fork primitive returns\n`SubAgentResult` with `summary` set and `tool_calls`\ntruncated to the parent.", - "files_affected": [ - "integration_tests/regression/test_cost_cap.py", - "integration_tests/regression/test_fork_subagent.py" - ], - "role": "tester", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - }, - { - "id": "task-5-9", - "description": "ADR refresh at\n`docs/architecture/claude-code-substrate.md`: rewrite\nthe \"Rollout deltas\" section to a \"Completed in #2717\"\nsection; document the cost cap default (200) and its\nenv override; document `EggHarnessSpawner` + `egg-orch\nlocal-run` shape; document the fork primitive; document\nthe R2 verdict (pass or fail) and what shipped as a\nconsequence (R15 migration or no-op); update R10\n(stability marker) to reflect the drop.", - "status": "pending", - "commit": null, - "checkpoint_id": null, - "notes": "", - "acceptance_criteria": "ADR no longer carries the \"Rollout deltas\" follow-up\nframing; every cq decision from this issue is\nreflected in the relevant section; the document\nreads as a stable substrate ADR rather than a\nwalking-skeleton plus rollout draft.", - "files_affected": [ - "docs/architecture/claude-code-substrate.md" - ], - "role": "documenter", - "review_cycles": 0, - "max_cycles": 3, - "escalated": false, - "delegation_attempts": 0, - "gaps": [], - "jira_key": null, - "jira_action": null, - "jira_action_status": null - } - ], - "dependencies": [ - "slice-4" - ], - "serialized_chain_order": [], - "parent_branch_at_creation": null, - "commit": null, - "review_feedback": [] - } - ], - "decisions": [ - { - "id": "cq-1", - "question": "How should the HITL bridge gap (reviewer v1 blocker #6) be closed \u2014 i.e. how does the skill drive a long-lived `run_pipeline_in_process(...)` generator across multiple `AskUserQuestion` round-trips?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Option A \u2014 Long-lived Python REPL/daemon the skill talks to via JSON-RPC envelopes; generator state survives between AskUserQuestion calls (closest match to engineered surface, requires daemon lifecycle management)", - "description": null - }, - { - "id": "opt-2", - "label": "Option B \u2014 Flatten generator into a hand-shaped sequence of single-yield `python3 .py` invocations; decisions/answers thread through .egg-state/contracts/.json (no daemon, but loses generator's heartbeat-during-HITL property; demands restructuring of in-process generator)", - "description": null - }, - { - "id": "opt-3", - "label": "Option C \u2014 Hybrid: ship Option B first as MVP for refine/plan; design Option A for implement (multi-producer concurrency makes a daemon cheaper than N separate stage scripts)", - "description": null - }, - { - "id": "opt-4", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Option C \u2014 Hybrid: ship Option B first as MVP for refine/plan; design Option A for implement (multi-producer concurrency makes a daemon cheaper than N separate stage scripts)\"}", - "resolved_by": "human", - "resolved_at": "2026-05-19T17:46:12.119676Z", - "debounce_until": null - }, - { - "id": "cq-2", - "question": "How should this rollout work be decomposed into slices (slice count = PR count; siblings in a wave run in parallel)? Items in scope: bridge gap closure, plan/implement/pr phase rollout (+role rubrics), 5-issue conformance matrix, EggHarnessSpawner, fork-based delegation, cost cap, R15 subagent-type migration, R2 hook validation, k3s adapter promotion.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Linear chain \u2014 [bridge gap] -> [plan-phase substrate + rubrics + reviewer wiring] -> [implement-phase substrate + rubrics + reviewer wiring] -> [pr-phase substrate] -> [5-issue conformance matrix] -> [EggHarnessSpawner] -> [hardening pass: R2 validation + R15 migration + cost cap + k3s adapter] (~7 sequential PRs, low review burden, slowest end-to-end)", - "description": null - }, - { - "id": "opt-2", - "label": "Bridge-first then parallel rollout \u2014 [bridge gap] -> [plan substrate || implement substrate || pr substrate || conformance fixtures] -> [EggHarnessSpawner || cost cap || R15 migration || R2 validation || k3s adapter] (3 waves; bridge is the hard dep then parallelism unlocks; ~10 PRs)", - "description": null - }, - { - "id": "opt-3", - "label": "Bridge-first then phase-sequential rollout \u2014 [bridge gap] -> [plan substrate (3 producers + 1 reviewer)] -> [implement substrate (3 producers + 5 reviewers)] -> [pr-phase substrate + conformance matrix] -> [hardening pass: EggHarnessSpawner + cost cap + R15 + R2 + k3s adapter, all parallel] (~5 PRs; phase ordering surfaces BRC issues earlier with smaller blast radius)", - "description": null - }, - { - "id": "opt-4", - "label": "Defer hardening to a tertiary issue \u2014 in this issue: bridge gap + plan + implement + pr + conformance + EggHarnessSpawner. Spin out R15 migration, cost cap, R2 hook validation, k3s adapter promotion, and fork-based delegation into a separate hardening follow-up. Slices: [bridge] -> [plan] -> [implement] -> [pr + conformance + EggHarnessSpawner] (~4 PRs in this issue)", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Bridge-first then phase-sequential rollout \u2014 [bridge gap] -> [plan substrate (3 producers + 1 reviewer)] -> [implement substrate (3 producers + 5 reviewers)] -> [pr-phase substrate + conformance matrix] -> [hardening pass: EggHarnessSpawner + cost cap + R15 + R2 + k3s adapter, all parallel] (~5 PRs; phase ordering surfaces BRC issues earlier with smaller blast radius)\"}", - "resolved_by": "human", - "resolved_at": "2026-05-19T17:46:12.132257Z", - "debounce_until": null - }, - { - "id": "cq-3", - "question": "Should the rollout migrate ClaudeCodeSpawner from \"re-host the existing egg_harness loop in the parent Claude Code session\" (today's spike implementation per `orchestrator/substrate/claude_code/spawner.py:14-26`) to a `ClaudeCodeAgentToolSpawner` that emits an actual `Agent`-tool envelope to the parent session's outer loop? This is the difference between \"harness re-host\" and a real substrate swap to Claude Code primitives.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Stay on harness re-host \u2014 keep ClaudeCodeSpawner running egg_harness in-process for all roles in this rollout; defer Agent-tool dispatcher to a future issue. Pros: zero re-architecture; matches the spike's tested seam. Cons: parent session still drives the harness for every subagent; not a true substrate-swap; concurrency uses ThreadPoolExecutor instead of native Agent fan-out", - "description": null - }, - { - "id": "opt-2", - "label": "Migrate to Agent-tool dispatcher in this rollout \u2014 build ClaudeCodeAgentToolSpawner that yields an `Agent` tool envelope; the parent session's outer loop executes the Agent tool and feeds results back via the message bus. Pros: real substrate swap; uses Claude Code's native subagent isolation. Cons: requires bridge-gap design to also carry agent-output flow, not just HITL", - "description": null - }, - { - "id": "opt-3", - "label": "Hybrid \u2014 reviewers stay on harness re-host (they're more research-heavy, harness depth helps), producers migrate to Agent-tool dispatch (typically larger context budget, more parallelism)", - "description": null - }, - { - "id": "opt-4", - "label": "Decide empirically at implement-phase \u2014 ship plan + implement rollout on harness re-host; measure concurrency / cost / latency / context usage; decide R15 migration timing once we have data", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Decide empirically at implement-phase \u2014 ship plan + implement rollout on harness re-host; measure concurrency / cost / latency / context usage; decide R15 migration timing once we have data\"}", - "resolved_by": "human", - "resolved_at": "2026-05-19T17:47:02.422102Z", - "debounce_until": null - }, - { - "id": "cq-4", - "question": "When (if at all) should the rollout migrate from R15 model (a) \u2014 `subagent_type=\"general-purpose\"` + ad-hoc prompt \u2014 to model (b) \u2014 per-role `.claude/agents/.md` definition files with structural tool restrictions in frontmatter? Model (a) puts all enforcement on PreToolUse hooks (R2). Model (b) gives structural tool-restrictions per role.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "In this rollout, alongside each phase's roles \u2014 every new rubric file (architect.md, coder.md, etc.) ships as a real `.claude/agents/.md` definition; the existing refiner.md migrates with the plan-phase wave. Highest correctness on day one; biggest review surface", - "description": null - }, - { - "id": "opt-2", - "label": "Defer entirely to a later hardening issue \u2014 ship every role in this rollout as ad-hoc prompts under `plugins/egg-sdlc/skills/egg-sdlc/agents/.md` (matching spike). Promote to model (b) only after R2 PreToolUse hook empirical validation closes (which it might not, in which case the model (b) migration becomes mandatory)", - "description": null - }, - { - "id": "opt-3", - "label": "Migrate as part of this issue's hardening pass \u2014 ship all rollout phases on model (a) first, then a single dedicated slice in this same issue converts all roles to model (b) at once. Pros: single conversion event, easier to review consistency; Cons: every role's rubric has to be re-validated as part of the conversion", - "description": null - }, - { - "id": "opt-4", - "label": "Make the choice contingent on R2's empirical result \u2014 if PreToolUse hooks reliably resolve calling role for nested subagents, stay on model (a) indefinitely; if not, migrate to model (b) as a fallback. Treat the R2 spike (item from issue body's 'Out of scope' list) as the gating decision", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Make the choice contingent on R2's empirical result \u2014 if PreToolUse hooks reliably resolve calling role for nested subagents, stay on model (a) indefinitely; if not, migrate to model (b) as a fallback. Treat the R2 spike (item from issue body's 'Out of scope' list) as the gating decision\"}", - "resolved_by": "human", - "resolved_at": "2026-05-19T17:47:02.493075Z", - "debounce_until": null - }, - { - "id": "cq-5", - "question": "When should the R2 empirical question be answered \u2014 does Claude Code's PreToolUse hook reliably resolve \"which subagent / role is calling Write()\" under nested / multi-subagent dispatch (today's spike validated this only for the single-role refiner case)? The fallback if hooks can't is cq-6 option 2: MCP-validator-side enforcement.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Early-spike before phase rollout begins \u2014 ship a 2-subagent worked example as the first slice (uses two roles, one parent + one child Agent dispatch; hook records EGG_AGENT_ROLE for each). If hooks fail this test, switch to MCP-validator-side enforcement before any further role rollout. Pros: catches the architectural problem before doing N role rollouts; Cons: pushes the timeline of substantive rollout by one slice", - "description": null - }, - { - "id": "opt-2", - "label": "Validate inline during plan-phase rollout \u2014 plan phase has 3 producers running concurrently; their dispatch naturally exercises multi-subagent hooks. Fold the R2 validation into the plan-phase acceptance criteria. Pros: no extra slice; Cons: risks rolling back plan-phase work if hooks fail late", - "description": null - }, - { - "id": "opt-3", - "label": "Validate inline during implement-phase rollout \u2014 implement has the most concurrent roles (3 producers + 5 reviewers); deferring R2 validation here gives maximum dispatch diversity but maximum rollback risk if hooks fail. Pros: most thorough; Cons: latest possible feedback", - "description": null - }, - { - "id": "opt-4", - "label": "Validate at hardening pass after all phases ship \u2014 treat R2 as a release-blocker check. If hooks fail this point, ship the rollout but require operators to opt into MCP-validator-side enforcement until a fix lands. Lowest impact on rollout schedule; highest risk of having to re-platform later", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Early-spike before phase rollout begins \u2014 ship a 2-subagent worked example as the first slice (uses two roles, one parent + one child Agent dispatch; hook records EGG_AGENT_ROLE for each). If hooks fail this test, switch to MCP-validator-side enforcement before any further role rollout. Pros: catches the architectural problem before doing N role rollouts; Cons: pushes the timeline of substantive rollout by one slice\"}", - "resolved_by": "human", - "resolved_at": "2026-05-19T17:50:26.145491Z", - "debounce_until": null - }, - { - "id": "cq-6", - "question": "What default value should `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` (REC5 cost cap) ship with? The ADR suggests \"e.g. 50\" as a conservative default; the cap protects against runaway-cost scenarios (R9) for users on direct-billing Anthropic accounts.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Conservative: 50 (ADR's suggested value) \u2014 enough for a typical issue with refine + plan + implement + pr on small-to-medium slices; trips early on large slice-DAGs and forces operator to opt up", - "description": null - }, - { - "id": "opt-2", - "label": "Moderate: 200 \u2014 enough for a 3-slice slice-DAG with concurrent producers/reviewers + a couple of NACK cycles before tripping; balances cost safety vs. legitimate large pipelines", - "description": null - }, - { - "id": "opt-3", - "label": "Generous: 500 \u2014 only catches genuine infinite loops or runaway NACK ping-pong; most pipelines never trip", - "description": null - }, - { - "id": "opt-4", - "label": "Opt-in only: no default cap (unset means unlimited); operator must set the env var explicitly to enable. Documented as 'recommended for direct-billing users'", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Moderate: 200 \u2014 enough for a 3-slice slice-DAG with concurrent producers/reviewers + a couple of NACK cycles before tripping; balances cost safety vs. legitimate large pipelines\"}", - "resolved_by": "human", - "resolved_at": "2026-05-19T17:50:52.314176Z", - "debounce_until": null - }, - { - "id": "cq-7", - "question": "Should the full k3s interface adapter (MessageBus, PolicyEnforcer, WorktreeManager protocols backed by Redis / gateway / gateway-worktree-manager) ship in this rollout, or stay deferred? The DoD doesn't require k3s-side adapter parity \u2014 the cq-1 \"parallel substrates\" promise is mostly about claude-code working alongside k3s; the protocol contract on the k3s side today is only `K3sSpawnerAdapter` (a working shim) plus direct use of `RedisMessageStore` / `gateway` for the rest.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Defer entirely (recommended) \u2014 k3s side keeps using RedisMessageStore / gateway directly; only AgentSpawner is unified. The protocols stay v0.x unstable on the k3s leg until a future issue promotes them. Pros: smallest blast radius for this rollout; Cons: leaves cq-1's full 'parallel substrates' promise incomplete", - "description": null - }, - { - "id": "opt-2", - "label": "Ship inside this issue as a hardening slice \u2014 add K3sMessageBus / K3sPolicyEnforcer / K3sWorktreeManager adapters as one slice in the hardening pass. Pros: closes cq-1 fully; Cons: adds review burden; risks regressions in production k3s users while the substrate-portable code paths are still bedding in", - "description": null - }, - { - "id": "opt-3", - "label": "Ship only the K3sMessageBus adapter (promote RedisMessageStore onto the protocol) \u2014 the MessageBus protocol is the smallest seam and the one most likely to surface BRC differences. Policy/worktree adapters can wait. Pros: pragmatic; Cons: partial-promotion is awkward", - "description": null - }, - { - "id": "opt-4", - "label": "Defer entirely AND drop the v0.x unstable marker after \u22653 roles exercise the claude-code side \u2014 i.e. accept the interfaces are stable on the claude-code leg even if the k3s leg hasn't promoted yet. Documents the actual stability boundary precisely", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": true, - "resolution": "{\"action\": \"select\", \"selected\": \"Defer entirely AND drop the v0.x unstable marker after \u22653 roles exercise the claude-code side \u2014 i.e. accept the interfaces are stable on the claude-code leg even if the k3s leg hasn't promoted yet. Documents the actual stability boundary precisely\"}", - "resolved_by": "human", - "resolved_at": "2026-05-19T17:53:37.552281Z", - "debounce_until": null - } - ], - "workflow_owner": null, - "audit_log": [ - { - "timestamp": "2026-05-19T17:14:09.298852Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.0", - "old_value": null, - "new_value": { - "id": "cq-1", - "question": "How should the HITL bridge gap (reviewer v1 blocker #6) be closed \u2014 i.e. how does the skill drive a long-lived `run_pipeline_in_process(...)` generator across multiple `AskUserQuestion` round-trips?", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Option A \u2014 Long-lived Python REPL/daemon the skill talks to via JSON-RPC envelopes; generator state survives between AskUserQuestion calls (closest match to engineered surface, requires daemon lifecycle management)", - "description": null - }, - { - "id": "opt-2", - "label": "Option B \u2014 Flatten generator into a hand-shaped sequence of single-yield `python3 .py` invocations; decisions/answers thread through .egg-state/contracts/.json (no daemon, but loses generator's heartbeat-during-HITL property; demands restructuring of in-process generator)", - "description": null - }, - { - "id": "opt-3", - "label": "Option C \u2014 Hybrid: ship Option B first as MVP for refine/plan; design Option A for implement (multi-producer concurrency makes a daemon cheaper than N separate stage scripts)", - "description": null - }, - { - "id": "opt-4", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: How should the HITL bridge gap (reviewer v1 blocke...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T17:14:22.339312Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.1", - "old_value": null, - "new_value": { - "id": "cq-2", - "question": "How should this rollout work be decomposed into slices (slice count = PR count; siblings in a wave run in parallel)? Items in scope: bridge gap closure, plan/implement/pr phase rollout (+role rubrics), 5-issue conformance matrix, EggHarnessSpawner, fork-based delegation, cost cap, R15 subagent-type migration, R2 hook validation, k3s adapter promotion.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Linear chain \u2014 [bridge gap] -> [plan-phase substrate + rubrics + reviewer wiring] -> [implement-phase substrate + rubrics + reviewer wiring] -> [pr-phase substrate] -> [5-issue conformance matrix] -> [EggHarnessSpawner] -> [hardening pass: R2 validation + R15 migration + cost cap + k3s adapter] (~7 sequential PRs, low review burden, slowest end-to-end)", - "description": null - }, - { - "id": "opt-2", - "label": "Bridge-first then parallel rollout \u2014 [bridge gap] -> [plan substrate || implement substrate || pr substrate || conformance fixtures] -> [EggHarnessSpawner || cost cap || R15 migration || R2 validation || k3s adapter] (3 waves; bridge is the hard dep then parallelism unlocks; ~10 PRs)", - "description": null - }, - { - "id": "opt-3", - "label": "Bridge-first then phase-sequential rollout \u2014 [bridge gap] -> [plan substrate (3 producers + 1 reviewer)] -> [implement substrate (3 producers + 5 reviewers)] -> [pr-phase substrate + conformance matrix] -> [hardening pass: EggHarnessSpawner + cost cap + R15 + R2 + k3s adapter, all parallel] (~5 PRs; phase ordering surfaces BRC issues earlier with smaller blast radius)", - "description": null - }, - { - "id": "opt-4", - "label": "Defer hardening to a tertiary issue \u2014 in this issue: bridge gap + plan + implement + pr + conformance + EggHarnessSpawner. Spin out R15 migration, cost cap, R2 hook validation, k3s adapter promotion, and fork-based delegation into a separate hardening follow-up. Slices: [bridge] -> [plan] -> [implement] -> [pr + conformance + EggHarnessSpawner] (~4 PRs in this issue)", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: How should this rollout work be decomposed into sl...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T17:14:36.564465Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.2", - "old_value": null, - "new_value": { - "id": "cq-3", - "question": "Should the rollout migrate ClaudeCodeSpawner from \"re-host the existing egg_harness loop in the parent Claude Code session\" (today's spike implementation per `orchestrator/substrate/claude_code/spawner.py:14-26`) to a `ClaudeCodeAgentToolSpawner` that emits an actual `Agent`-tool envelope to the parent session's outer loop? This is the difference between \"harness re-host\" and a real substrate swap to Claude Code primitives.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Stay on harness re-host \u2014 keep ClaudeCodeSpawner running egg_harness in-process for all roles in this rollout; defer Agent-tool dispatcher to a future issue. Pros: zero re-architecture; matches the spike's tested seam. Cons: parent session still drives the harness for every subagent; not a true substrate-swap; concurrency uses ThreadPoolExecutor instead of native Agent fan-out", - "description": null - }, - { - "id": "opt-2", - "label": "Migrate to Agent-tool dispatcher in this rollout \u2014 build ClaudeCodeAgentToolSpawner that yields an `Agent` tool envelope; the parent session's outer loop executes the Agent tool and feeds results back via the message bus. Pros: real substrate swap; uses Claude Code's native subagent isolation. Cons: requires bridge-gap design to also carry agent-output flow, not just HITL", - "description": null - }, - { - "id": "opt-3", - "label": "Hybrid \u2014 reviewers stay on harness re-host (they're more research-heavy, harness depth helps), producers migrate to Agent-tool dispatch (typically larger context budget, more parallelism)", - "description": null - }, - { - "id": "opt-4", - "label": "Decide empirically at implement-phase \u2014 ship plan + implement rollout on harness re-host; measure concurrency / cost / latency / context usage; decide R15 migration timing once we have data", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Should the rollout migrate ClaudeCodeSpawner from ...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T17:14:50.507124Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.3", - "old_value": null, - "new_value": { - "id": "cq-4", - "question": "When (if at all) should the rollout migrate from R15 model (a) \u2014 `subagent_type=\"general-purpose\"` + ad-hoc prompt \u2014 to model (b) \u2014 per-role `.claude/agents/.md` definition files with structural tool restrictions in frontmatter? Model (a) puts all enforcement on PreToolUse hooks (R2). Model (b) gives structural tool-restrictions per role.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "In this rollout, alongside each phase's roles \u2014 every new rubric file (architect.md, coder.md, etc.) ships as a real `.claude/agents/.md` definition; the existing refiner.md migrates with the plan-phase wave. Highest correctness on day one; biggest review surface", - "description": null - }, - { - "id": "opt-2", - "label": "Defer entirely to a later hardening issue \u2014 ship every role in this rollout as ad-hoc prompts under `plugins/egg-sdlc/skills/egg-sdlc/agents/.md` (matching spike). Promote to model (b) only after R2 PreToolUse hook empirical validation closes (which it might not, in which case the model (b) migration becomes mandatory)", - "description": null - }, - { - "id": "opt-3", - "label": "Migrate as part of this issue's hardening pass \u2014 ship all rollout phases on model (a) first, then a single dedicated slice in this same issue converts all roles to model (b) at once. Pros: single conversion event, easier to review consistency; Cons: every role's rubric has to be re-validated as part of the conversion", - "description": null - }, - { - "id": "opt-4", - "label": "Make the choice contingent on R2's empirical result \u2014 if PreToolUse hooks reliably resolve calling role for nested subagents, stay on model (a) indefinitely; if not, migrate to model (b) as a fallback. Treat the R2 spike (item from issue body's 'Out of scope' list) as the gating decision", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: When (if at all) should the rollout migrate from R...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T17:15:04.939921Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.4", - "old_value": null, - "new_value": { - "id": "cq-5", - "question": "When should the R2 empirical question be answered \u2014 does Claude Code's PreToolUse hook reliably resolve \"which subagent / role is calling Write()\" under nested / multi-subagent dispatch (today's spike validated this only for the single-role refiner case)? The fallback if hooks can't is cq-6 option 2: MCP-validator-side enforcement.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Early-spike before phase rollout begins \u2014 ship a 2-subagent worked example as the first slice (uses two roles, one parent + one child Agent dispatch; hook records EGG_AGENT_ROLE for each). If hooks fail this test, switch to MCP-validator-side enforcement before any further role rollout. Pros: catches the architectural problem before doing N role rollouts; Cons: pushes the timeline of substantive rollout by one slice", - "description": null - }, - { - "id": "opt-2", - "label": "Validate inline during plan-phase rollout \u2014 plan phase has 3 producers running concurrently; their dispatch naturally exercises multi-subagent hooks. Fold the R2 validation into the plan-phase acceptance criteria. Pros: no extra slice; Cons: risks rolling back plan-phase work if hooks fail late", - "description": null - }, - { - "id": "opt-3", - "label": "Validate inline during implement-phase rollout \u2014 implement has the most concurrent roles (3 producers + 5 reviewers); deferring R2 validation here gives maximum dispatch diversity but maximum rollback risk if hooks fail. Pros: most thorough; Cons: latest possible feedback", - "description": null - }, - { - "id": "opt-4", - "label": "Validate at hardening pass after all phases ship \u2014 treat R2 as a release-blocker check. If hooks fail this point, ship the rollout but require operators to opt into MCP-validator-side enforcement until a fix lands. Lowest impact on rollout schedule; highest risk of having to re-platform later", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: When should the R2 empirical question be answered ...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T17:15:15.142183Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.5", - "old_value": null, - "new_value": { - "id": "cq-6", - "question": "What default value should `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` (REC5 cost cap) ship with? The ADR suggests \"e.g. 50\" as a conservative default; the cap protects against runaway-cost scenarios (R9) for users on direct-billing Anthropic accounts.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Conservative: 50 (ADR's suggested value) \u2014 enough for a typical issue with refine + plan + implement + pr on small-to-medium slices; trips early on large slice-DAGs and forces operator to opt up", - "description": null - }, - { - "id": "opt-2", - "label": "Moderate: 200 \u2014 enough for a 3-slice slice-DAG with concurrent producers/reviewers + a couple of NACK cycles before tripping; balances cost safety vs. legitimate large pipelines", - "description": null - }, - { - "id": "opt-3", - "label": "Generous: 500 \u2014 only catches genuine infinite loops or runaway NACK ping-pong; most pipelines never trip", - "description": null - }, - { - "id": "opt-4", - "label": "Opt-in only: no default cap (unset means unlimited); operator must set the env var explicitly to enable. Documented as 'recommended for direct-billing users'", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: What default value should `EGG_PIPELINE_MAX_AGENT_...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T17:15:29.342794Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "decisions.6", - "old_value": null, - "new_value": { - "id": "cq-7", - "question": "Should the full k3s interface adapter (MessageBus, PolicyEnforcer, WorktreeManager protocols backed by Redis / gateway / gateway-worktree-manager) ship in this rollout, or stay deferred? The DoD doesn't require k3s-side adapter parity \u2014 the cq-1 \"parallel substrates\" promise is mostly about claude-code working alongside k3s; the protocol contract on the k3s side today is only `K3sSpawnerAdapter` (a working shim) plus direct use of `RedisMessageStore` / `gateway` for the rest.", - "type": "hitl", - "phase": "refine", - "options": [ - { - "id": "opt-1", - "label": "Defer entirely (recommended) \u2014 k3s side keeps using RedisMessageStore / gateway directly; only AgentSpawner is unified. The protocols stay v0.x unstable on the k3s leg until a future issue promotes them. Pros: smallest blast radius for this rollout; Cons: leaves cq-1's full 'parallel substrates' promise incomplete", - "description": null - }, - { - "id": "opt-2", - "label": "Ship inside this issue as a hardening slice \u2014 add K3sMessageBus / K3sPolicyEnforcer / K3sWorktreeManager adapters as one slice in the hardening pass. Pros: closes cq-1 fully; Cons: adds review burden; risks regressions in production k3s users while the substrate-portable code paths are still bedding in", - "description": null - }, - { - "id": "opt-3", - "label": "Ship only the K3sMessageBus adapter (promote RedisMessageStore onto the protocol) \u2014 the MessageBus protocol is the smallest seam and the one most likely to surface BRC differences. Policy/worktree adapters can wait. Pros: pragmatic; Cons: partial-promotion is awkward", - "description": null - }, - { - "id": "opt-4", - "label": "Defer entirely AND drop the v0.x unstable marker after \u22653 roles exercise the claude-code side \u2014 i.e. accept the interfaces are stable on the claude-code leg even if the k3s leg hasn't promoted yet. Documents the actual stability boundary precisely", - "description": null - }, - { - "id": "opt-5", - "label": "Other (explain in reply)", - "description": null - } - ], - "resolved": false, - "resolution": null, - "resolved_by": null, - "resolved_at": null, - "debounce_until": null - }, - "reason": "Created HITL decision: Should the full k3s interface adapter (MessageBus,...", - "checkpoint_id": null - }, - { - "timestamp": "2026-05-19T17:15:56.741142Z", - "actor": "egg", - "role": "implementer", - "action": "update", - "field_path": "feedback", - "old_value": null, - "new_value": { - "id": "feedback-1", - "phase": "refine", - "questions": [ - { - "id": "Q1", - "question": "Curated 5-issue conformance set (feedback Q1 in #2623 settled the rule, not the specific issues): name the 5 GitHub issue numbers covering the SDLC hot paths: one bug fix, one feature add, one refactor, one infra/script change, one doc change. The conformance matrix will run all 5 under both `EGG_SUBSTRATE=k3s` and `EGG_SUBSTRATE=claude-code` in CI. Suggested candidates the analysis can pre-select: #2714 (bug), #2474 (feature/regression suite), #2261 (refactor), #2705 (infra), #2718 (doc) \u2014 are these acceptable, or would you prefer different ones?", - "answer": null - }, - { - "id": "Q2", - "question": "Latency / cost budget for the conformance matrix (feedback Q2 deferred to this issue): now that we'll run refine + plan + implement + pr against 5 issues under both substrates, do you want a budget per phase (e.g., 'refine \u2264 2x k3s wall-clock', 'cost per issue \u2264 $5 of Anthropic spend'), or a global per-issue budget, or no budget at all (just `pytest.mark.slow` if any test exceeds N minutes)?", - "answer": null - }, - { - "id": "Q3", - "question": "EggHarnessSpawner scope (feedback Q4 named it a secondary goal; DoD #5 promotes it to required for parent-close): how much of the EggHarnessSpawner ships in this issue? A) a working `egg-orch local-run --issue 1234` end-to-end against a curated issue; B) just the spawner class + protocol satisfaction + unit tests (CLI in a follow-up); C) full local-run including HITL flow via stdin/stdout JSON prompts; D) defer entirely \u2014 we promised in DoD but it can be a tertiary issue.", - "answer": null - }, - { - "id": "Q4", - "question": "Operator-acceptance for trust-context shift (R1) at multi-role scale: the spike's R1 acceptance is scoped to 'one refiner subagent running against a trusted repo'. Plan / implement phases dispatch 3+8 = 11 subagents per pipeline, each with access to the parent session's API credential context. Do you want any additional mitigations beyond what the spike documents (e.g. an opt-in `EGG_REQUIRE_CREDENTIAL_PROXY=true` flag that forces operators to acknowledge multi-subagent credential exposure, or a per-role allowlist of which subagents may run on the claude-code substrate)?", - "answer": null - }, - { - "id": "Q5", - "question": "Reviewer-roster expansion: are any new reviewer roles needed for the substrate-rollout work itself? For example, `reviewer_substrate` or `reviewer_concurrency_substrate` to give bridge / message bus / hook work a dedicated reviewer beyond the existing `reviewer_code`, `reviewer_concurrency`, `reviewer_security`, `reviewer_agent_design`? Or are the existing reviewers adequate?", - "answer": null - }, - { - "id": "Q6", - "question": "Walking-skeleton scope-fence behavior post-rollout: today `run_pipeline_in_process` raises NotImplementedError if the operator chooses 'continue to plan'. After plan / implement / pr land, should the fence be removed entirely (operator can chain phases freely), kept as an opt-in `--single-phase` flag for short runs, or moved to a per-phase fence each operator must explicitly clear?", - "answer": null - } - ], - "submitted": false, - "submitted_by": null, - "submitted_at": null, - "comment_id": null, - "debounce_until": null - }, - "reason": "Created feedback request with 6 question(s)", - "checkpoint_id": null - } - ], - "refine_review_cycles": 0, - "refine_review_feedback": "", - "plan_review_cycles": 0, - "plan_review_feedback": "", - "pr": { - "title": "Roll out Claude Code substrate to remaining roles + plan/implement/pr", - "description": "Issue #2715 shipped the walking-skeleton spike for the Claude\nCode substrate (one role \u00d7 one phase end-to-end). This issue\nrolls the substrate out from there per **cq-11 = \"Spike then\nplan\"**.\n\n**What this PR stack does** (5 slices, stacked PRs):\n\n1. **Bridge gap + R2 spike + refine reviewers (slice 1)** \u2014\n Closes the heredoc-HITL bridge gap via flattened\n `python3 .py` invocations (cq-1 Option C, refine/plan\n half). Ships a 2-subagent worked example that validates\n PreToolUse hooks resolve role under nested dispatch (cq-5).\n Adds the two refine-team reviewer rubrics (`reviewer_refine`,\n `reviewer_agent_design`).\n2. **Plan-phase substrate (slice 2)** \u2014 First multi-role BRC\n stress test. Wires architect / task_planner / risk_analyst\n producers and `reviewer_plan` through the in-process\n orchestrator.\n3. **Implement-phase substrate + daemon bridge (slice 3)** \u2014\n Second BRC stress test at largest scale (3 producers + 5\n reviewers). Switches the HITL bridge to Option A (long-lived\n daemon over UNIX socket) because the flattened path is\n impractical at this many yields.\n4. **PR-phase + conformance matrix (slice 4)** \u2014 Wires the PR\n phase, removes the walking-skeleton fence (feedback Q6),\n ships the 5-issue conformance matrix (#2714, #2474, #2261,\n #2705, #2718) under both substrates with `pytest.mark.slow`\n gating + 3-hour per-phase ceiling.\n5. **Hardening (slice 5)** \u2014 `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`\n cost cap default 200 (cq-6); `EggHarnessSpawner` + `egg-orch\n local-run` CLI (DoD #5 / feedback Q3); fork-based delegation\n (cq-10 deferred half); contingent R15 model (b) migration\n based on slice 1's R2 verdict (cq-4); drop the v0.x unstable\n marker on the four substrate protocols (cq-7); ADR refresh.\n\n**Impact**: every agent role in egg's SDLC pipeline gains a\nsecond substrate; the operator can drive a full pipeline\nnatively in Claude Code via `AskUserQuestion` without an MCP\n`provide_input` round-trip; the conformance matrix is green on\nboth substrate dimensions; `egg-orch local-run` enables headless\nruns; the four substrate protocols are stable.", - "test_plan": "- Automated: every slice ships substrate-portable tests under\n `integration_tests/regression/` (per slice: bridge round-trip,\n R2 nested-hook denial, plan-phase BRC, implement-phase BRC,\n daemon round-trip, conformance matrix x5 issues x2 substrates,\n cost cap, EggHarnessSpawner, fork primitive).\n- Manual (slice 4 / slice 5): operator runs\n `python3 bin/run_pipeline.py issue-2717` end-to-end and\n `egg-orch local-run --issue 2718` headlessly; both produce the\n expected PR stack and metrics.", - "manual_steps": "Pre-merge (each slice): reviewer copies\n`orchestrator/substrate/claude_code/settings.template.json` into\ntheir own `.claude/settings.json` before exercising the\nconformance matrix; reviewer spot-checks BRC consensus history\nagainst the plan's primitives table.\n\nPost-merge (slice 5): if slice 3's empirical metrics warrant the\nAgent-tool dispatcher migration (cq-3), reviewer files a\nfollow-up issue; if R15 model (b) migration shipped (R2 fail\npath), reviewer files a follow-up to deprecate model (a) after\noperator migration.", - "context_title": null, - "context_description": null, - "context_branch": null, - "context_pr_number": null, - "deferred_actions": [] - }, - "feedback": { - "id": "feedback-1", - "phase": "refine", - "questions": [ - { - "id": "Q1", - "question": "Curated 5-issue conformance set (feedback Q1 in #2623 settled the rule, not the specific issues): name the 5 GitHub issue numbers covering the SDLC hot paths: one bug fix, one feature add, one refactor, one infra/script change, one doc change. The conformance matrix will run all 5 under both `EGG_SUBSTRATE=k3s` and `EGG_SUBSTRATE=claude-code` in CI. Suggested candidates the analysis can pre-select: #2714 (bug), #2474 (feature/regression suite), #2261 (refactor), #2705 (infra), #2718 (doc) \u2014 are these acceptable, or would you prefer different ones?", - "answer": "Accept the refiner's suggested 5: #2714 (bug \u2014 sync regression on dirty worktree), #2474 (feature / regression suite \u2014 k3s integration coverage), #2261 (refactor \u2014 decomposing 15 oversize source files), #2705 (infra \u2014 Cilium datapath / k3s setup), #2718 (doc \u2014 claude-code substrate docs). Each represents a distinct SDLC shape; current state (open/closed/merged) is not relevant for using them as conformance fixtures. Note: #2714 was closed since this issue was filed (presumably part of the operator's recent redeploy)." - }, - { - "id": "Q2", - "question": "Latency / cost budget for the conformance matrix (feedback Q2 deferred to this issue): now that we'll run refine + plan + implement + pr against 5 issues under both substrates, do you want a budget per phase (e.g., 'refine \u2264 2x k3s wall-clock', 'cost per issue \u2264 $5 of Anthropic spend'), or a global per-issue budget, or no budget at all (just `pytest.mark.slow` if any test exceeds N minutes)?", - "answer": "No hard budget; pytest.mark.slow as soft inclusion gate; 3-hour-per-phase ceiling as safety fallback that fails the CI job if any single phase exceeds it. Per-phase cost/latency budgets remain deferred until we have empirical rollout data from the 5x2 matrix; reassess in a follow-up issue once the matrix has run end-to-end at least once. The 3-hour ceiling is calibrated against this issue's predecessor (#2623) which had an 80-minute implement phase on k3s; claude-code substrate is expected to be slower per-call (subagent dispatch overhead), so 3 hours leaves headroom for legitimate multi-cycle BRC without inviting runaway loops." - }, - { - "id": "Q3", - "question": "EggHarnessSpawner scope (feedback Q4 named it a secondary goal; DoD #5 promotes it to required for parent-close): how much of the EggHarnessSpawner ships in this issue? A) a working `egg-orch local-run --issue 1234` end-to-end against a curated issue; B) just the spawner class + protocol satisfaction + unit tests (CLI in a follow-up); C) full local-run including HITL flow via stdin/stdout JSON prompts; D) defer entirely \u2014 we promised in DoD but it can be a tertiary issue.", - "answer": "Option A \u2014 Full local-run end-to-end. DoD #5 in the issue body says 'EggHarnessSpawner ships and can drive a pipeline headlessly (egg-orch local-run --issue 1234)' which Option B (spawner-only, defer CLI) does not satisfy. Option A also gives the third spawner implementation needed to battle-test the AgentSpawner protocol contract with >2 implementations (per cq-7's stability rationale)." - }, - { - "id": "Q4", - "question": "Operator-acceptance for trust-context shift (R1) at multi-role scale: the spike's R1 acceptance is scoped to 'one refiner subagent running against a trusted repo'. Plan / implement phases dispatch 3+8 = 11 subagents per pipeline, each with access to the parent session's API credential context. Do you want any additional mitigations beyond what the spike documents (e.g. an opt-in `EGG_REQUIRE_CREDENTIAL_PROXY=true` flag that forces operators to acknowledge multi-subagent credential exposure, or a per-role allowlist of which subagents may run on the claude-code substrate)?", - "answer": "No additional mitigations for v1. R1's acceptance was scoped to 'trusted-repo SDLC on operator's own machine' \u2014 that scales to multi-subagent because the trust model is unchanged (same operator's session credentials, same operator's repo, same operator's machine). The 11-15 subagents per pipeline expand the credential-exposure surface but not the credential-trust model. Revisit only if multi-tenant operator use cases or CI-runner-managed substrate use cases emerge. An opt-in EGG_REQUIRE_CREDENTIAL_PROXY flag was considered but adds operator friction with no concrete threat-model justification for v1; can be added later if real use cases warrant it." - }, - { - "id": "Q5", - "question": "Reviewer-roster expansion: are any new reviewer roles needed for the substrate-rollout work itself? For example, `reviewer_substrate` or `reviewer_concurrency_substrate` to give bridge / message bus / hook work a dedicated reviewer beyond the existing `reviewer_code`, `reviewer_concurrency`, `reviewer_security`, `reviewer_agent_design`? Or are the existing reviewers adequate?", - "answer": "No new reviewer roles. The existing roster (reviewer_code, reviewer_concurrency, reviewer_security, reviewer_agent_design) already covers the substrate dimensions: substrate concurrency falls under reviewer_concurrency; substrate code under reviewer_code; substrate hooks / policy / credential boundaries under reviewer_security; substrate architecture / design choices under reviewer_agent_design. Adding reviewer_substrate or reviewer_concurrency_substrate bloats dispatch fan-out (already 8 reviewers in implement phase) without clear coverage value. If a specific gap surfaces during the rollout \u2014 e.g. a reviewer dimension that isn't being caught by any existing reviewer \u2014 file it as a follow-up issue with a concrete coverage gap." - }, - { - "id": "Q6", - "question": "Walking-skeleton scope-fence behavior post-rollout: today `run_pipeline_in_process` raises NotImplementedError if the operator chooses 'continue to plan'. After plan / implement / pr land, should the fence be removed entirely (operator can chain phases freely), kept as an opt-in `--single-phase` flag for short runs, or moved to a per-phase fence each operator must explicitly clear?", - "answer": "Remove the scope-fence entirely once plan/implement/pr land. Today's NotImplementedError fence was a spike-scope guard; once all phases work, chaining phases freely is the expected behavior. The 'single-phase' use case is already structurally covered by the start_phase + hitl_gates: false config flags (used today by the short flow). A per-phase fence each operator must explicitly clear adds friction with no benefit. A --single-phase flag is redundant with start_phase. Fence removal should land as part of the pr-phase + conformance slice." - } - ], - "submitted": true, - "submitted_by": "human", - "submitted_at": "2026-05-19T18:05:18.325428Z", - "comment_id": null, - "debounce_until": null - }, - "phase_configs": null, - "agent_executions": [] -} diff --git a/.egg-state/drafts/2623-analysis.md b/.egg-state/drafts/2623-analysis.md deleted file mode 100644 index 8e7d39b141..0000000000 --- a/.egg-state/drafts/2623-analysis.md +++ /dev/null @@ -1,326 +0,0 @@ -# Analysis: Run the full egg SDLC stack natively in Claude Code (substrate swap) - -> Issue: #2623 | Phase: refine - -## Problem Statement - -The egg SDLC stack today runs on a **k3s + Redis Streams + Docker + sidecar gateway** substrate (`orchestrator/kubernetes_spawner.py`, `orchestrator/redis_message_store.py`, `gateway/gateway.py`, `sandbox/Dockerfile`). Running egg requires standing up a k3s cluster, building/loading sandbox images, running Redis, and proxying Anthropic credentials through a long-lived gateway. That is a heavy install footprint for a tool whose value proposition — agent-driven SDLC pipelines — is increasingly something a single developer wants to run from a vanilla Claude Code session. - -PR #2608 shipped a **`refine-plan` Markdown skill** (`plugins/refine-plan/skills/refine-plan/SKILL.md`, 451 lines) that *approximates* egg's refine+plan phases inside Claude Code using subagents and a filesystem verdict journal. Child issues #2612 (concurrent BRC) and #2622 (depth gap) catalogue the structural shortcomings of that approximation: it is iterated rather than concurrent, has no real message bus or version tracking, no mid-cycle revision, no `build_system_prompt` depth, and reviewer rubrics that don't enforce evidence breadth. - -The North Star this parent issue articulates is a different framing entirely. Rather than chase parity with a parallel Markdown implementation, **swap the substrate underneath the real stack**: keep `egg_orchestrator`, `egg_harness`, `egg_contracts`, `egg_agent`, `shared/prompts/` unchanged, and re-platform them onto Claude Code's native primitives: - -| Today's substrate | Claude-Code-native replacement | -|---|---| -| k3s schedules agent pods (`KubernetesSpawner`) | `Agent` tool spawns subagents | -| Docker container per agent (`sandbox/Dockerfile`) | `Agent` tool's `isolation: "worktree"` + Bash sandbox mode | -| Redis Streams message bus (`RedisMessageStore`) | In-process Python (`MessageStore` interface already exists) | -| Gateway sidecar policy + creds (`gateway/`) | PreToolUse hooks in `.claude/settings.json` | -| Persistent volume for worktrees | User's local filesystem | -| Sandbox container image | User's local Claude Code install + `agents/*.md` | -| `kubectl get pods` health checks | In-process `egg_health` thread | -| Overseer pod | In-process overseer thread | - -If the swap is real (vs. an approximation), depth, BRC mechanics, contract schema, role prompts, and HITL semantics come for free from the reused upstream code. **Quality becomes structurally inevitable**: the only question is "does the substrate-swap layer faithfully expose the orchestrator's coordination surface to a Claude Code session?" - -The desired outcome: - -1. A user with Claude Code installed and an Anthropic API key can run **the full egg SDLC** (refine → plan → implement → PR) against an issue from inside their session, with **no k3s, Docker, Redis, or gateway daemon** running. -2. The same `egg_orchestrator` Python that runs in k3s today runs in-process to the parent Claude session, dispatching agents via the `Agent` tool instead of `KubernetesSpawner`. -3. The conformance suite (integration tests already shipped under `integration_tests/regression/` from #2474) passes against **both** substrates — proving behavioral equivalence. -4. The substrate-swap architecture is documented in an ADR-style file in the repo. - -## Current Behavior - -### Orchestrator boot is a Flask HTTP daemon - -`orchestrator/cli.py::cmd_serve()` (lines 83–150) imports `api.py` and calls `waitress.serve()` on port 9849. There is **no in-process / embedded / local mode** — a grep for `embedded`, `local`, `in_process` finds nothing. `egg-orch` (the CLI sandbox agents call) is a thin HTTP client; it POSTs to the orchestrator API. All BRC coordination, message-bus interaction, and agent dispatch flow through this HTTP boundary. - -The Flask app is composed of 12 route blueprints under `orchestrator/routes/`; most handler logic is wrapped in `jsonify()` and not currently callable as a library API. - -### Message bus has a clean abstraction; the production binding is Redis - -`orchestrator/message_store.py` defines a `MessageStore` interface (in-memory dict per pipeline) and `Message`/`MessageType` Pydantic types. `orchestrator/redis_message_store.py` wraps Redis Streams. Routes already access messages through the store interface (`get_messages()`, `add_message()`), so the swap surface is small: instantiate `MessageStore` instead of `RedisMessageStore`, or add an in-process bus variant that's drop-in compatible. - -Only three files import the `redis` package directly: `redis_message_store.py`, `routes/anchors.py`, and tests. Most of the orchestrator is bus-implementation-agnostic. - -### Agent dispatch is hardcoded to k8s - -`orchestrator/kubernetes_spawner.py::KubernetesSpawner.spawn_agent_job()` constructs a k8s Job manifest with labels (`LABEL_PIPELINE_ID`, `LABEL_AGENT_ROLE`), service URLs (`GATEWAY_K8S_URL`, `ORCHESTRATOR_K8S_URL`), worktree volume mounts, and posts it to the k8s API. `orchestrator/kubernetes_monitor.py` polls k8s for pod status changes (~2 KLOC). `orchestrator/container_spawner.py` is a 36-line back-compat shim that aliases `ContainerSpawner = KubernetesSpawner`. - -There is no `AgentSpawner` interface today; the orchestrator just imports `KubernetesSpawner` directly in `concurrent_executor.py::_spawn_agent()`. This is the **largest abstraction gap** in the codebase for the substrate swap. - -### Gateway is an HTTP sidecar with portable policy logic - -`gateway/gateway.py` exposes Flask REST endpoints (`/api/v1/git/push`, `/api/v1/github/...`, `/api/v1/worktrees/create`, `/api/v1/anthropic/...`). The policy logic (allow/deny by phase + role + path) lives in `gateway/agent_restrictions.py` and is a pure Python module. The credential layer (`gateway/anthropic_credentials.py`, `gateway/github_client.py`) holds tokens server-side and injects them into upstream calls so the sandbox never sees them. - -For Claude Code substrate, `agent_restrictions.py` is a directly-importable module; the HTTP wrapper isn't load-bearing. The credentials story is different: in Claude Code the user's session already holds the API key, so there's no need to "inject" — but PreToolUse hooks (or the existing MCP tools) would still need to enforce the same allow/deny rules. - -### Contracts and checkpoints are already filesystem-native - -Contracts live at `/.egg-state/contracts/.json` as Pydantic-serializable `Contract` v1.1 objects (`shared/egg_contracts/models.py`). BRC history (`.egg-state/brc-history/-.json`) and agent outputs (`.egg-state/agent-outputs/`) are JSON on disk. Worktrees default to `WORKTREE_BASE_DIR=/home/egg/.egg-worktrees//` (env-var overridable, no k8s coupling). - -This entire data plane is portable. The substrate swap doesn't touch storage format. - -### Existing refine-plan skill is a useful template, not the destination - -`plugins/refine-plan/skills/refine-plan/SKILL.md` shows the shape of a Claude-Code-orchestrated SDLC: parent session reads role markdown, prepends per-role rubric, spawns subagents via `Agent` tool with `subagent_type: "general-purpose"`, journals verdicts to `.refine-plan-state//`. It does **not** import `egg_orchestrator` — it's a parallel implementation. #2622 documents the resulting depth gap (narrow research, narrative-not-evidence trade-offs, no live mid-cycle revision). - -The substrate-swap target moves the other direction: keep the orchestrator unchanged, push the Claude-Code primitives *underneath* it. - -### Harness already supports Claude Code - -`shared/egg_harness/` is the real agent runtime (replaces Claude Agent SDK as egg's primary harness). It already supports multiple providers, multi-KB role system prompts via `build_system_prompt(sources)`, JSONL session persistence, permission-callback gating. `EGG_HARNESS=claude-code` selects a Claude Code harness binding. So a "in-process orchestrator" doesn't need to invent agent runtime — it just needs to invent **agent spawning** (and let the harness handle the rest, OR replace harness-driven dispatch with `Agent`-tool-driven dispatch). - -## Constraints - -### Hard constraints from the issue body -- **`SendMessage` is platform-gated** (anthropics/claude-code#36196). Mid-cycle peer-to-peer agent messaging — what would let a producer revise mid-cycle after a reviewer NACK — is not currently available. The orchestrator-as-message-bus model (what egg does today via Redis Streams) sidesteps this for **BRC mechanics correctness**; it remains a *quality booster*, not a *correctness requirement*. -- **Subagent context windows vs. `max_turns: 1000`** (`docs/guides/concurrent-execution.md:97`). A subagent in Claude Code has a smaller context than an egg sandbox. Deep refines that span 30+ files may exhaust the window. Egg's `egg_container` checkpoint primitives are the existing answer; whether they port cleanly is open. -- **HITL must surface through the parent session**. The k3s pipeline lets HITL flow through MCP `provide_input` while the orchestrator stays alive in a long-running pod. In Claude Code, the orchestrator is in-process to the parent session; pausing means returning control to the parent so it can `AskUserQuestion` (or surface the decision via comment), then resuming. -- **Concurrency ceiling on `Agent` tool spawns**. Egg's `ThreadPoolExecutor` in `concurrent_executor.py` spawns N agents per phase; Claude Code has a practical ceiling on parallel subagents. Should map cleanly for ≤6-role phases but may bite the larger slice-DAG phases. -- **Install footprint is larger** than a pure-Markdown skill. The skill now depends on (or ships) the egg Python packages. Per the issue, this is acceptable — but must be documented and the failure mode (missing deps) handled. - -### Architectural / design constraints -- **`build_system_prompt(sources)` depth** (`shared/egg_harness/prompt.py`) must reach the subagents — otherwise the depth gap (#2622) reopens. The substrate swap must keep the real prompt assembly in the path, not regress to thin per-role markdown. -- **BRC concurrency invariants** (INV-1..5, `orchestrator/action_guards.py::validate_invariants()`): version tracking on producer re-proposals, stale-ACK un-confirmation, open-NACK barrier. These live in `egg_orchestrator` and survive unchanged if the orchestrator-as-bus model is preserved. -- **Gateway-equivalent enforcement**. The integration-test trust-boundary doc (`docs/architecture/integration-test-trust-boundary.md`) distinguishes in-sandbox-agent / trusted-CI-runner / human-operator execution contexts. The substrate swap shifts most agent execution from "in-sandbox" to "in-parent-Claude-Code-session" — that's a new trust context. PreToolUse hooks must be the structural enforcement layer; prompt-only restrictions are insufficient. -- **File-write boundaries (`shared/egg_restrictions/patterns.py`)** must continue to enforce role-based path restrictions. The gateway enforces these on `git push` today; the Claude Code substrate needs an equivalent — either via PreToolUse hooks intercepting writes, or via the MCP `check_file_restriction` tool agents already query. - -### Conformance / proof obligation -- The issue's definition of done requires **passing the conformance suite on ≥5 representative issues across both substrates**. The integration tests at `integration_tests/regression/` (BRC happy-path, live-pod guard, unpushed-commit salvage, HITL round-trip, slice-DAG restart, phase-aware timeouts, babysit-PR single-final-push) are k3s-shaped. Some — like "live-pod guard on restart" — are k3s-specific concerns that don't translate to a single-process substrate. The conformance set needs **substrate-portable invariants**, not k3s-specific assertions, factored out of the existing suite. - -### Dependencies on other systems / features -- Marketplace packaging (`.claude-plugin/plugin.json` exists for refine-plan, 22 lines). Distribution path for a heavier skill (with Python deps) is open. -- `docs/guides/harness-configuration.md` already documents three harness modes (`claude-sdk`, `claude-code`, `egg`). The substrate swap should pick one and document the choice, or thread the existing selector through. - -## Options Considered - -### Option A: Abstraction-first, parallel substrates, feature-flagged - -**Approach**: Land `AgentSpawner`, `MessageBus`, `PolicyEnforcer`, `WorktreeManager` interfaces in `egg_orchestrator`. Keep `KubernetesSpawner` + `RedisMessageStore` + `gateway/` as one implementation. Add `ClaudeCodeSpawner` + `InProcessMessageBus` + `PreToolUseHookPolicy` as a second implementation. Select via env var (e.g. `EGG_SUBSTRATE=claude-code` vs `EGG_SUBSTRATE=k3s`). Both substrates run the same integration suite as a CI matrix; conformance is structurally verified rather than asserted. The skill is a thin entry point that boots the in-process orchestrator with the claude-code substrate selected. - -**Pros**: -- The conformance proof is **executable** — both substrates run the same test, divergence is immediately visible. -- Migration risk is contained: k3s users keep working; Claude Code users get a new path. -- The abstraction layer is the document — code shapes naturally explain the substrate boundary, reducing ADR-vs-reality drift. -- Refactor is interface-driven, naturally chunkable into per-component slices (spawner, bus, gateway, worktree). -- The four "structural causes" #2622 catalogs (`general-purpose` subagents, missing depth targets, no mid-cycle revision, no tool-use budget signaling) get fixed for both substrates simultaneously when the real `build_system_prompt` flows through. - -**Cons**: -- Doubles the maintained surface area until k3s is deprecated. -- The interfaces have to be designed to fit Claude Code constraints (subagent ceiling, no `SendMessage`) without unduly constraining the k3s implementation. -- Conformance test factoring takes real work — the existing tests assume k3s/Redis primitives. - -### Option B: All-at-once full substrate replacement (delete k3s) - -**Approach**: Replace `KubernetesSpawner` with `ClaudeCodeSpawner` directly. Delete `kubernetes_monitor.py`, the gateway sidecar, Redis Streams. Re-implement integration tests for the new substrate only. - -**Pros**: -- One substrate to maintain. -- Code is simpler post-cut: no abstraction layer, no env-var dispatch. -- The Claude Code substrate becomes the default and only substrate; "egg" effectively becomes a Claude Code skill. - -**Cons**: -- **High blast radius**. k3s users (CI, anyone running egg today) are broken until the swap is complete and conformance is passing. -- The conformance proof becomes "Claude Code substrate ships green" — which is necessary but not sufficient. The hard question (does the substrate swap faithfully reproduce the orchestrator's coordination behavior?) is harder to answer without a side-by-side comparison. -- Loses the structural ability to ever run the orchestrator outside Claude Code (e.g. for CI integration tests, batch automation against a registry of issues, server-mode deployments). - -### Option C: Skill-only — don't touch the orchestrator - -**Approach**: Treat the issue body's framing as aspirational; ship a beefier `refine-plan` (or `sdlc`) skill that imports `egg_orchestrator` Python *but only for non-protocol code* (contract schema, prompt assembly, file restrictions). Continue using verdict-journal-style BRC. Close #2622 by routing through `build_system_prompt`. Defer concurrent BRC to #2612. - -**Pros**: -- Smallest code change; no orchestrator refactor required. -- Skill stays portable (Python deps but no Redis/k3s). -- Depth gap closes structurally because `build_system_prompt` is in the path. - -**Cons**: -- **Misses the issue's North Star**: this is parity-chasing, not substrate-swap. BRC mechanics (version tracking, open-NACK barrier, mid-cycle revision) remain "BRC-inspired" approximations, exactly the gap #2612 already documents. -- The conformance suite cannot be reused — there's no real orchestrator to test. -- Effectively closes #2622 only, leaving #2612 and the parent's substrate-swap intent unresolved. - -### Option D: In-process orchestrator binding, single substrate, no abstraction layer - -**Approach**: Build a `ClaudeCodeSpawner` and `InProcessMessageBus` that the existing orchestrator code can use *when imported into a Claude Code session*. Don't extract clean interfaces — instead, monkey-patch / dependency-inject the new components at the boot path. The orchestrator file structure stays, but module-level imports become per-mode. - -**Pros**: -- Less refactoring than Option A. -- The orchestrator code base doesn't grow new abstraction layers. -- Faster to a working demo. - -**Cons**: -- The substrate boundary isn't visible in the code — it's implicit in the import order and DI wiring. ADR-vs-reality drift is high. -- Conformance proof depends on inspection rather than execution. -- Testing both substrates from the same suite is painful (mocking-heavy). -- Long-term maintenance hazard: the "two implementations" exist as untyped facts about boot-time configuration, not as named interfaces. - -## Recommended Approach - -**Option A — abstraction-first parallel substrates with conformance-by-CI-matrix.** - -Justification: - -1. **The conformance proof is executable, not narrative.** The issue's definition of done requires "passes the same behavioral conformance tests as the k3s substrate, on a measurement set of ≥5 representative issues." Option A makes this a CI matrix dimension rather than a manual test plan. Option B forfeits the comparison; Option D's coupling makes the comparison expensive to maintain. - -2. **The Claude Code constraints (`SendMessage` gating, subagent ceiling, context windows) are best surfaced as interface contracts.** `AgentSpawner.spawn(...)` and `MessageBus.send(...)` named on an interface let the constraints live as method signatures, return types, and documented limitations. Option D's monkey-patching hides them; Option C never confronts them. - -3. **Depth gap closure is structural in Option A.** Once `build_system_prompt(sources)` is in the Claude Code path, the four #2622 causes (general-purpose subagents, missing depth targets, no mid-cycle revision via bus, no tool-use budget) close as the natural consequence of running the real harness inside a Claude Code session — not as a parallel rubric rewrite of the skill. - -4. **It respects the issue body's exclusion of #2474, #2612, #2622 from this issue's must-do list** — the children are absorbed structurally (the conformance suite from #2474 stays load-bearing; the depth gap and concurrent BRC close because the real orchestrator is in the path), but this parent doesn't need to fully solve them, only verify the substrate carries them. - -5. **Migration safety is preserved.** k3s deployments (CI, server-mode automation) keep working until the operator decides to deprecate; the Claude Code substrate is additive. - -The work tracked by this parent is therefore **the abstraction layer + the two implementations + the conformance matrix + the ADR**. The plan phase will decide the slice-DAG shape for the rollout; this analysis stays focused on framing. - -## Open Questions - -Every open question below is registered as a contract decision or feedback item. Decision IDs map to the order they were created. - -### Pre-Refine Context (already settled by the operator's framing update) - -The issue body itself replaced the original "S1/S2/S3 substrate options" framing with the substrate-swap framing, and the operator's comment on the issue explicitly cleared three sub-decisions: - -- **Substrate intent**: pursue substrate-swap, not parity-chase (rules out Option C below in spirit). -- **#2474 disposition**: integration tests stay as the cross-substrate conformance suite. -- **#2612 disposition**: largely absorbed; `SendMessage`/Agent Teams remains a quality booster, not a correctness requirement (orchestrator-as-bus is the answer for BRC mechanics). -- **#2622 disposition**: largely absorbed; real `build_system_prompt` closes the depth gap structurally. - -These are noted here so the plan phase doesn't re-litigate them. Everything else is open. - -### Registered decisions and feedback - -The following questions were registered via `egg-contract` and will be populated with their markdown checkboxes after this section. - - - -**Substrate coexistence strategy: how should the Claude Code substrate relate to the existing k3s substrate?** - -- [ ] Parallel substrates, env-var-selected: AgentSpawner/MessageBus/PolicyEnforcer interfaces in egg_orchestrator with two implementations; EGG_SUBSTRATE=claude-code|k3s; both run the same conformance suite (Option A) -- [ ] Full cut-over to Claude Code substrate: delete k3s/Redis/Docker/gateway code; orchestrator only runs in-process to a Claude Code session (Option B) -- [ ] Skill-only: do not refactor the orchestrator; ship a beefier refine-plan/sdlc skill that imports egg_orchestrator Python for non-protocol code (Option C — contradicts the issue's substrate-swap framing, listed for completeness) -- [ ] In-process binding without interfaces: monkey-patch / DI the new spawner+bus at boot path; no named abstraction layer (Option D) -- [ ] Other (explain in reply) - - - -**Initial pipeline-phase scope for the substrate swap: which phases must work on the Claude Code substrate before this parent issue closes?** - -- [ ] Refine + plan only: prove the substrate on the existing refine-plan skill's footprint; defer implement+pr to a follow-up -- [ ] All phases (refine + plan + implement + pr): full SDLC inside Claude Code; matches the issue body's North Star verbatim -- [ ] Explore-first: ship the abstraction interfaces + a single-role 'spawn one agent against an issue' smoke path; defer multi-role + BRC to follow-up issues once the spawner shape is settled -- [ ] Refine + plan + implement (no pr): prove BRC mechanics on producer roles; let pr-phase keep using k3s for now since PR creation is an external-API operation, not a coordination test -- [ ] Other (explain in reply) - - - -**Conformance-suite scoping: where does the substrate-portable behavioral test set live?** - -- [ ] Extend integration_tests/regression/ with a substrate parameter (CI matrix): each existing test runs under both k3s and claude-code substrates; tests that are inherently k3s-specific (e.g. live-pod-guard) skip on the claude-code dimension -- [ ] New shared conformance package (e.g. integration_tests/conformance/): factor substrate-portable invariants out of integration_tests/regression/; the regression suite keeps the k3s-specific stuff and the new package is what both substrates run -- [ ] Per-substrate test suites: integration_tests/regression/ stays k3s; integration_tests/claude_code/ is the new substrate's suite; cross-substrate conformance becomes a documented set of invariants verified by inspection, not by a single CI matrix -- [ ] Skill-internal cycle harness: the Claude Code substrate gets its own end-to-end smoke test that re-runs the refine-plan skill's existing local repro against ≥5 representative issues; no cross-substrate matrix -- [ ] Other (explain in reply) - - - -**Agent-spawner interface shape: what method signature should AgentSpawner expose so both KubernetesSpawner and ClaudeCodeSpawner can satisfy it?** - -- [ ] Synchronous spawn(role, prompt, env, worktree) -> AgentResult: caller blocks until agent completes; spawner handles internal concurrency. Maps cleanly to the Agent tool's call-and-wait model; k8s implementation polls internally -- [ ] Async dispatch + poll: spawn(...) -> AgentHandle; poll(handle) -> AgentStatus; matches today's KubernetesSpawner+monitor split, but requires the Claude Code spawner to fake handles (no underlying job ID) and complicates the in-process model -- [ ] Stream-shaped: spawn(...) yields events (start, tool-call, output, completion); the orchestrator drives the agent via the event stream; mirrors how egg_harness exposes the agent loop. Most powerful, biggest refactor -- [ ] Agent-tool-direct (Claude Code only): the abstraction is 'request a subagent run' and the k8s implementation translates by spawning a process running egg_harness — the interface is shaped around Claude Code's call shape, k8s adapts -- [ ] Other (explain in reply) - - - -**Worktree-management ownership in the Claude Code substrate: who creates and tears down the per-agent worktree?** - -- [ ] Claude Code's EnterWorktree / ExitWorktree tools: the substrate uses the harness-provided worktree primitive; egg's WORKTREE_BASE_DIR layout is replaced by ~/.claude/worktrees/. Native, but loses egg's per-repo-per-pipeline shared-checkout optimization -- [ ] Port egg's WORKTREE_BASE_DIR model: agents run inside .egg-state// subdirectories on the user's filesystem; the spawner manages worktree creation/teardown explicitly. Preserves egg's checkout-sharing; doesn't use Claude Code's native worktree mechanism -- [ ] Hybrid: parent session creates the pipeline-level shared checkout under .egg-state//; each subagent call passes through EnterWorktree to get an isolated branch; teardown is filesystem-level by the orchestrator at phase end -- [ ] Single-worktree mode: no per-agent worktree; agents run sequentially against the parent session's repo and the BRC bus serializes their writes. Loses concurrency but maximally simple -- [ ] Other (explain in reply) - - - -**Policy enforcement seam: where does the substrate enforce the gateway-equivalent rules (file-write restrictions per role, git-push allow/deny, gh-operation phase gates)?** - -- [ ] PreToolUse hooks in .claude/settings.json: skill-installed hook intercepts Write/Edit/Bash/etc. and calls into shared/egg_restrictions/patterns.py; structural enforcement at the tool boundary -- [ ] MCP-tool-side validators: every state-mutating MCP verb re-validates the caller's role+path against patterns.py; tools the harness uses (Write/Bash) are guarded by the harness's existing permission_callback. No hooks; existing layers strengthened -- [ ] In-process Python imports only: the orchestrator drives subagent prompts that *announce* role restrictions and the subagent self-polices; depends on prompt discipline (not structural enforcement). Listed for completeness — rejected upfront unless cost of structural enforcement is too high -- [ ] All three layered: PreToolUse hook for hard cuts (writes to blocked paths), MCP validators for state-mutation gates, prompt-time restrictions for graceful messaging. Most defense-in-depth, most build cost -- [ ] Other (explain in reply) - - - -**HITL surface in Claude Code mode: how does the orchestrator pause for a human decision when running in-process to a Claude Code session?** - -- [ ] Parent-session AskUserQuestion: orchestrator yields a HITL decision back to the parent Claude session, which surfaces it via AskUserQuestion; user reply resumes the pipeline. Best UX for solo developer; requires the orchestrator's run loop to be reentrant from the parent's perspective -- [ ] MCP provide_input verb: matches today's pattern; the parent calls a provide_input MCP tool from the user's prompt. Awkward inside a single Claude session (user typed something, agent calls back through MCP into the same process) -- [ ] Filesystem journal + parent poll: orchestrator writes the pending decision to .egg-state/hitl/.json; parent polls and surfaces it; user replies via a slash-command or skill verb that writes back. Works without orchestrator-as-callee but adds round-trip friction -- [ ] Heredoc-style synchronous: the orchestrator surface is a generator that yields HITLDecision objects; the skill's outer loop renders them with AskUserQuestion and feeds answers back. Hybrid of 1 and 3 -- [ ] Other (explain in reply) - - - -**Install / packaging footprint: how does the user obtain the egg Python packages required by the in-process orchestrator?** - -- [ ] Skill ships Python wheels in the plugin: marketplace install drops wheels into a venv the skill bootstraps; isolates the install from system Python; biggest plugin size -- [ ] Plugin metadata declares pip dependencies: user runs 'pip install egg' (or equivalent) once; plugin.json documents the requirement; smallest plugin but pre-flight check needed before the skill works -- [ ] Vendor minimal subset into the plugin: the skill copies only the egg_orchestrator + egg_contracts + egg_harness + shared/prompts/ subtree it needs; no wheels, no pip; biggest source-of-truth-drift risk between the vendored copy and the upstream -- [ ] Two-skill layout: a thin entry-point skill + a heavier 'egg-runtime' plugin that ships the wheels; the entry-point depends on the runtime; user installs both. More moving parts but cleaner separation -- [ ] Other (explain in reply) - - - -**k3s deprecation timing: what is the disposition of the k3s substrate after the Claude Code substrate ships green?** - -- [ ] Deprecate k3s on substrate-swap merge: remove kubernetes_spawner.py / kubernetes_monitor.py / RedisMessageStore / gateway sidecar in the same PR family; Claude Code substrate becomes the only substrate -- [ ] Leave k3s indefinitely as a co-equal substrate: both substrates are supported; CI matrix tests both; k3s is the answer for headless / server deployments, Claude Code substrate is the answer for solo developers. (default, lowest-risk) -- [ ] Mark k3s 'CI-only': k3s stops being a deployment target for end users (no docs guidance, no images shipped); only remains as a CI environment that runs the conformance suite as a second data point. Removal scheduled for a future cleanup issue -- [ ] Defer the decision: ship substrate-swap, let user feedback drive a follow-up issue that decides k3s's fate after ≥3 months of real-world use of the Claude Code substrate -- [ ] Other (explain in reply) - - - -**Subagent context-window strategy: how does the substrate handle deep research that would exceed a subagent's context budget (egg targets max_turns: 1000)?** - -- [ ] Port egg_container checkpointing: subagents write intermediate findings to .egg-state/checkpoints/; a re-spawned subagent resumes from the checkpoint. Faithful to today's recovery model; requires the Claude Code spawner to re-invoke the agent with checkpoint context -- [ ] Accept smaller-than-1000 turn budget: subagents are bounded by Claude Code's native context limit; role rubrics and the system-prompt depth do most of the work; deep-research breadth is reduced but consistent. Lowest implementation cost -- [ ] Per-agent forked subagent for deep research: a refiner whose context fills up forks a child subagent to do a sub-task ('read all files matching X and summarize'), the child's summary returns to the parent. Mirrors how a human delegates -- [ ] Hybrid: checkpoint for cross-turn recovery, fork for sub-task delegation; the system prompt teaches when to use which -- [ ] Other (explain in reply) - - - -**Slice-DAG decomposition shape: how should this substrate-swap work be sliced for shippable PRs? (slice count = PR count; siblings in a wave run in parallel)** - -- [ ] Single slice — entire substrate swap as one PR family (interfaces + ClaudeCodeSpawner + InProcessMessageBus + PreToolUseHookPolicy + conformance matrix + skill entry point + ADR); 1 PR. Largest blast radius, simplest integration -- [ ] Two-wave parallel: [substrate-interfaces + ADR] -> [ClaudeCodeSpawner || InProcessMessageBus || PolicyEnforcer || WorktreeManager (4 parallel implementations)] -> [conformance matrix + skill entry point]; 7 PRs across 3 waves -- [ ] Three-wave parallel: [interfaces only] -> [each implementation in parallel] -> [k3s side of the interface adapter || conformance matrix || skill entry point || ADR (all parallel)]; ~9 PRs, very parallel -- [ ] Linear chain: interfaces -> message bus -> spawner -> policy -> worktree -> conformance -> skill -> ADR; 8 sequential PRs. Lowest review burden per PR; slowest end-to-end -- [ ] Spike then plan: a single 'walking skeleton' slice that gets one role (refiner) running through one substrate (claude-code) end-to-end, then re-plan the rollout based on what the spike learns; 1 PR for the spike + follow-up issue -- [ ] Other (explain in reply) - - - -## Open-ended feedback - -Please **edit the feedback comment on the issue** to answer the following. The same questions are mirrored in the contract's feedback record so they survive cross-phase: - -**Q1**: Definition of 'conformance passes on ≥5 representative issues' — should the ≥5 measurement issues be a fixed curated set (and if so, which? — please name them or describe selection criteria), or sampled per-CI-run from open issues, or some other rule? - -**Q2**: Are there latency or throughput budgets the Claude Code substrate should meet relative to the k3s substrate? (E.g. 'refine phase ≤ 2x k3s latency'.) If so, what are they? If none, is performance acceptable as 'whatever Claude Code gives us so long as it terminates'? - -**Q3**: Are there constraints on third-party dependencies the substrate is allowed to introduce — e.g. any specific Python packages forbidden, any size cap on the marketplace plugin install footprint, any restriction on requiring a developer-mode Claude Code feature flag? - -**Q4**: Should the in-process orchestrator support being driven by NON-Claude-Code callers too (e.g. a CLI 'egg-orch local-run --issue 1234' that uses the in-process orchestrator with a non-Claude-Code agent harness)? Or is the Claude Code substrate intentionally Claude-Code-only? - -**Q5**: Is there appetite to use this issue to also fix structural-cause #5 from #2622 (no tool-use budget signaling) and #6 (no minimum-breadth targets in role files) — given they require role-file edits that touch shared/prompts/ regardless of substrate — or should those stay scoped to #2622? - -**Q6**: Any concerns about telemetry / privacy regression when the orchestrator runs in the user's session vs. a k3s pod (e.g. checkpoints written to local filesystem may contain sensitive prompts/contexts that previously stayed in cluster-storage)? - ---- - -*Authored-by: egg* diff --git a/.egg-state/drafts/2623-plan.md b/.egg-state/drafts/2623-plan.md deleted file mode 100644 index 1a9a0b0a58..0000000000 --- a/.egg-state/drafts/2623-plan.md +++ /dev/null @@ -1,1348 +0,0 @@ -# Plan: Run the full egg SDLC stack natively in Claude Code (spike-first walking skeleton) - -> Issue: #2623 | Phase: plan | Refine settles → **spike-then-plan** rollout (cq-11) - -## Approach - -The refine phase settled eleven decisions and six feedback items. The -single most consequential decision is **cq-11 = "Spike then plan"**: -the operator chose a *walking-skeleton* slice that gets one role -(refiner) running through the Claude Code substrate end-to-end, then -**re-plans the rollout in a follow-up issue** once the spike has -returned real numbers (subagent context behavior, hook-permission -ergonomics, `Agent` tool throughput). - -Concretely this means **one slice**, not the 7–9 PR rollouts in -cq-11's parallel/linear alternatives. The slice must: - -1. Land the abstraction interfaces (`AgentSpawner`, `MessageBus`, - `PolicyEnforcer`, `WorktreeManager`) — but only minimal shapes that - the Claude Code implementation actually exercises. Per the - operator's **cq-4 = synchronous spawn**, `AgentSpawner.spawn(...) → - AgentResult` blocks until completion; per **cq-5 = port - WORKTREE_BASE_DIR model**, the worktree manager keeps egg's - per-pipeline checkout shape; per **cq-6 = PreToolUse hooks**, the - policy enforcer is hook-side. -2. Ship a single Claude Code implementation of each interface - (`ClaudeCodeSpawner`, `InProcessMessageBus`, `PreToolUseHookPolicy`, - `LocalWorktreeManager`). The k3s side stays untouched and the - existing `KubernetesSpawner` / `RedisMessageStore` / `gateway/` - stack continues to back `EGG_SUBSTRATE=k3s` *implicitly* (the - adapter that retrofits them onto the interface is **explicitly out - of scope** for the spike — a TODO marker in the interface module - captures the obligation for the follow-up). -3. Boot the in-process orchestrator from a skill entry point with - **just the refiner role** exercised end-to-end against a single - curated issue (per feedback Q1 the conformance set is a fixed - curated 5 — for the spike we use one of them, not the full five; - the follow-up issue extends to all five). -4. Wire one integration test in `integration_tests/regression/` - through a substrate-parameter fixture to **prove the matrix shape - works**. The rest of the regression suite stays unparametrized - until the follow-up issue does the full conformance factor-out - (cq-3 lands the *direction* — the spike lands the *minimum proof*). -5. Document the substrate-swap design in an ADR-style file under - `docs/architecture/`, naming the four interfaces and how - `EGG_SUBSTRATE` selects between implementations. -6. Open a follow-up issue that captures the rollout deltas: BRC - concurrency, the remaining 5+ roles, the k3s interface adapter, - full conformance-matrix factor-out, deprecation timing for any - bits the spike obsoletes, plus the perf/latency budget (feedback - Q2 explicitly defers this measurement until after the spike). - -This is intentionally a **single big slice** — that's what cq-11 -chose. The slice is over the soft 1k LOC advisory ceiling -(~1.8–2.5k LOC including the ADR), and the plan reviewer will flag it -as advisory only. Splitting into multiple slices would re-litigate -cq-11; the operator decided the spike-then-plan trade-off, so a -single-slice plan is correct here. - -### How the eleven decisions shape the work - -| Decision | Selection | Plan consequence | -|----|----|----| -| cq-1 substrate strategy | Option A — parallel substrates, env-var-selected | Interfaces in `orchestrator/`; `EGG_SUBSTRATE=claude-code`/`k3s` switch at boot | -| cq-2 phase scope (parent close) | All phases (refine + plan + implement + pr) | **Spike scope is refine-only**; cq-11 explicitly overrides cq-2 for *this* slice. The follow-up issue extends to plan/implement/pr | -| cq-3 conformance scoping | Extend regression with substrate parameter | One regression test parametrized via fixture in the spike; full matrix in follow-up | -| cq-4 spawner shape | Synchronous `spawn(role, prompt, env, worktree) → AgentResult` | `AgentSpawner` protocol method signature pinned | -| cq-5 worktree ownership | Port `WORKTREE_BASE_DIR` model | `LocalWorktreeManager` mirrors `gateway/worktree_manager.py:49` shape under `.egg-state///` | -| cq-6 policy seam | PreToolUse hooks | `PreToolUseHookPolicy` ships a Python hook script + `.claude/settings.json` template; uses `shared/egg_restrictions/patterns.py:768 build_agent_patterns` | -| cq-7 HITL surface | Heredoc-style synchronous generator | `HITLDecision`-yielding generator on the orchestrator boot path; skill renders via `AskUserQuestion` | -| cq-8 packaging | Plugin metadata declares pip deps | `.claude-plugin/plugin.json` lists `egg-shared` pip name + pre-flight check in skill | -| cq-9 k3s disposition | Leave indefinitely | k3s code untouched in this spike; follow-up issue may revisit | -| cq-10 context-window strategy | Hybrid — checkpoint + fork | Spike picks the lighter half: ports `egg_container` checkpoint hooks. Forking is documented but deferred to follow-up (it's a quality booster, not a correctness requirement) | -| cq-11 slice shape | Spike then plan | **One slice; follow-up issue created in the same slice's tasks** | -| cq-12 pip-name selection | NEW HITL (this re-propose cycle) | TASK-1-7 carries the operator's selection verbatim; if "Defer", plugin.json ships TODO placeholder | - -### Feedback items applied - -- **Q1 (conformance set)**: spike runs against **one** fixed curated - issue; follow-up issue extends to 5. Selection rule documented in - the ADR. -- **Q2 (latency budget)**: explicitly deferred — no perf gate in the - spike's acceptance criteria; follow-up captures budget once real - numbers exist. -- **Q3 (deps)**: spike pins to existing egg pip deps; no new - dependencies introduced. Marketplace footprint stays well under the - 100MB soft cap (declarations only). -- **Q4 (non-Claude-Code callers)**: secondary goal — interfaces are - designed to admit an `EggHarnessSpawner` later (subprocess-driven - `egg_harness` for headless CLI), but the spike does not build it. - Documented as a TODO in the interface module. -- **Q5 (#2622 structural causes #5/#6)**: out of scope; stays with - #2622. -- **Q6 (telemetry/privacy)**: ADR notes checkpoint location - (`.egg-state//checkpoints/`) and reserves a future - `egg-state prune` CLI verb; no telemetry is sent. Spike does not - ship the prune verb. - -## Primitives - -Every primitive cited in this plan is verified to exist at the -`file:line` shown, or marked `(NEW — task TASK-X-Y)` for primitives a -task in this plan creates. Primitives marked NEW have a corresponding -task whose acceptance criteria produce the named primitive in the -named module. - -### Existing primitives (cited verbatim from grep) - -| Primitive | Location | Notes | -|----|----|----| -| `class KubernetesSpawner` | `orchestrator/kubernetes_spawner.py:249` | Spike does NOT touch — left as-is for `EGG_SUBSTRATE=k3s` | -| `def spawn_agent_job` | `orchestrator/kubernetes_spawner.py:411` | Same — k3s path stays | -| `class KubernetesMonitor` | `orchestrator/kubernetes_monitor.py:85` | Untouched in spike | -| `class MessageStore` | `orchestrator/message_store.py:200` | In-memory dict already exists; `InProcessMessageBus` extends this shape | -| `class RedisMessageStore` | `orchestrator/redis_message_store.py:107` | Untouched in spike | -| `class ConcurrentPhaseExecutor` | `orchestrator/concurrent_executor.py:114` | Spike does NOT replace; orchestrator boot path injects the new spawner at the `_spawn_agent` seam | -| `def _spawn_agent` | `orchestrator/concurrent_executor.py:504` | The seam where `AgentSpawner.spawn(...)` is invoked. Task TASK-1-2 patches this to dispatch on the new protocol | -| `def cmd_serve` | `orchestrator/cli.py:83` | k3s daemon entry — left untouched | -| `def cmd_pipelines_create` | `orchestrator/cli.py:504` | Used as the model for a new in-process boot helper (TASK-1-6) | -| `def create_concurrent_spawn_fn` | `orchestrator/kubernetes_spawner.py:1564` | Existing factory the k3s adapter wraps to satisfy `AgentSpawner` | -| `def validate_invariants` (INV-6 at line 757) | `orchestrator/action_guards.py:631` | Requires `ack_commit_sha` consistency — drives `AgentResult.commit_sha` in TASK-1-1 | -| `class HITLDecision` | `orchestrator/models.py:300` | Reused unchanged; orchestrator yields these | -| `def build_system_prompt` | `shared/egg_harness/prompt.py:24` | MUST stay in the path — the structural depth fix #2622 relies on | -| `def set_permission_callback` | `shared/egg_harness/tools/registry.py:114` | Reused for in-Claude-Code permission gating; complements the PreToolUse hook | -| `class AgentFilePattern` | `shared/egg_restrictions/patterns.py:44` | Hook calls `build_agent_patterns(...)` (next row) to resolve role → allowed paths | -| `def build_agent_patterns` | `shared/egg_restrictions/patterns.py:768` | The PreToolUse hook imports this exact symbol — no parallel logic | -| `WORKTREE_BASE_DIR` | `gateway/worktree_manager.py:49` | Shape carried over; `LocalWorktreeManager` defaults to the same path layout but env-var-overridable to `.egg-state//` | -| `def check_agent_restrictions` | `gateway/phase_filter.py:1061` | Reference implementation for the hook's allow/deny semantics | -| Path-escape `is_relative_to` defense | `gateway/worktree_manager.py:1711` | Reference implementation for `LocalWorktreeManager.tear_down` safety (TASK-1-5) | -| `class EggStack` | `integration_tests/conftest.py:71` | Existing dataclass holding `gateway_url`/`orchestrator_url` strings — referenced only to ground the Trust-Boundary section; the spike's tests do NOT depend on it | -| `def egg_stack` (fixture) | `integration_tests/conftest.py:340` | Kubectl-gated; spike's substrate-parameter test does NOT depend on this — claude-code parameter runs pure-Python in-process | -| `def orchestrator_url` (fixture) | `integration_tests/conftest.py:357` | Derived from `egg_stack`; same — spike's test does not depend on it | -| Existing regression fixtures (`single_reviewer_graph`, `event_capture`, `filter_events`) | `integration_tests/regression/conftest.py:564`, `:519`, `:552` | The substrate-distinguishing test in TASK-1-8 may compose with these or use them as templates | -| `def make_tracker` | `integration_tests/regression/_helpers.py:156` | Existing helper for building `PeerConsensusTracker` instances in regression tests — reusable as the substrate-distinguishing test exercises the bus path | -| `class PeerConsensusTracker` | `orchestrator/peer_consensus.py:69` | The BRC mechanics object the InProcessMessageBus serves under the bus-routing tests | -| BRC test directory | `orchestrator/tests/test_brc_*.py` | 7+ existing unit tests; TASK-1-3's invariant-preservation tests live in `shared/tests/test_in_process_message_bus.py` (TASK-1-8) and **reference** these as the behavioral oracle | -| `plugins/refine-plan/skills/refine-plan/SKILL.md` | (entire file) | Reference shape for the new skill entry point (TASK-1-7 + TASK-1-11) | -| `plugins/refine-plan/skills/refine-plan/agents/refiner.md` | (entire file) | Reference shape for `agents/refiner.md` in TASK-1-11 | -| `integration_tests/regression/conftest.py` | (entire file) | Where the new substrate-parameter fixture lands | - -### New primitives (created in this plan) - -| Primitive | Created by | Module | -|----|----|----| -| `class AgentSpawner` (Protocol) | TASK-1-1 | `orchestrator/substrate/spawner.py` (NEW module) | -| `class AgentResult` (dataclass with `commit_sha: str \| None`) | TASK-1-1 | `orchestrator/substrate/spawner.py` — `commit_sha` is REQUIRED for INV-6 (`orchestrator/action_guards.py:631`, invariant body at `:757`) | -| `class MessageBus` (Protocol) | TASK-1-1 | `orchestrator/substrate/message_bus.py` (NEW module) | -| `class PolicyEnforcer` (Protocol) | TASK-1-1 | `orchestrator/substrate/policy.py` (NEW module) | -| `class WorktreeManager` (Protocol) | TASK-1-1 | `orchestrator/substrate/worktree.py` (NEW module) | -| `def select_substrate(env: Mapping[str, str]) -> SubstrateBundle` | TASK-1-1 | `orchestrator/substrate/__init__.py` (NEW) — reads `EGG_SUBSTRATE`, returns bundle | -| `class K3sSpawnerAdapter` | TASK-1-1 | `orchestrator/substrate/k3s_adapter.py` (NEW) — thin wrapper around `orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn` so the k3s leg of `select_substrate({})` returns a **working** `AgentSpawner` rather than `NotImplementedError`. Captures `commit_sha` via `git rev-parse HEAD` after the underlying job completes | -| `class ClaudeCodeSpawner` | TASK-1-2 | `orchestrator/substrate/claude_code/spawner.py` (NEW) — captures `commit_sha` via `git rev-parse HEAD` in the spawned worktree before returning the `AgentResult` | -| `class InProcessMessageBus` | TASK-1-3 | `orchestrator/substrate/claude_code/message_bus.py` (NEW) | -| `class PreToolUseHookPolicy` | TASK-1-4 | `orchestrator/substrate/claude_code/policy.py` (NEW) | -| Hook entry script `egg-pretooluse-hook` | TASK-1-4 | `orchestrator/substrate/claude_code/hook_entry.py` (NEW) — referenced from `.claude/settings.json` template | -| `class LocalWorktreeManager` | TASK-1-5 | `orchestrator/substrate/claude_code/worktree.py` (NEW) | -| `def run_pipeline_in_process(...)` generator | TASK-1-6 | `orchestrator/substrate/in_process.py` (NEW) — yields `HITLDecision` per cq-7 | -| `egg-sdlc` plugin.json | TASK-1-7 | `plugins/egg-sdlc/.claude-plugin/plugin.json` (NEW) — declares pip dep | -| Skill SKILL.md + agents/refiner.md for `egg-sdlc` plugin | TASK-1-11 | `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` (NEW), `plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md` (NEW) | -| `substrate` pytest parameter + fixture | TASK-1-8 (tester) | `integration_tests/regression/conftest.py` (existing — extended) | -| `test_brc_single_cycle_claude_code` | TASK-1-8 (tester) | `integration_tests/regression/test_brc_single_cycle.py` (existing — extended) | -| ADR doc | TASK-1-9 (documenter) | `docs/architecture/claude-code-substrate.md` (NEW) | -| Follow-up issue tracking entry | TASK-1-10 (documenter) | `docs/architecture/claude-code-substrate.md` (same ADR, "Open work" appendix) | - -### Trust-boundary scope - -All new tests in this spike target the **integration-test trust -boundary** documented at -`docs/architecture/integration-test-trust-boundary.md`. The actual -fixture layout in the current tree (verified by grep, not by the -stale trust-boundary doc) is: - -- `class EggStack` (`integration_tests/conftest.py:71`) — dataclass - carrying the `gateway_url`/`orchestrator_url` strings; lives in the - root conftest. -- `def egg_stack` fixture (`integration_tests/conftest.py:340`) — - the kubectl-gated entry: it stands up the local k3s stack and - yields an `EggStack`. `pytest.skip`s when the stack cannot start. -- `def orchestrator_url` fixture (`integration_tests/conftest.py:357`) - — derives the URL from `egg_stack`; inherits the kubectl gate - transitively. - -The deprecated `integration_tests/local_pipeline/` tree was deleted -in commit `f7803637d1`; the trust-boundary doc that still references -it is itself stale and outside the scope of this spike. **TASK-1-9 -(ADR)** explicitly notes that the substrate-portable invariants now -live directly in `integration_tests/regression/`'s own -`conftest.py`-resident fixtures (`single_reviewer_graph`, -`event_capture`, `filter_events`, `make_tracker`) rather than the -deleted `local_pipeline_stack` tier. - -Implications for the spike: - -- The substrate-parameter fixture and the substrate-distinguishing - test in TASK-1-8 live in `integration_tests/regression/conftest.py` - and a NEW test file at - `integration_tests/regression/test_substrate_smoke.py`. They do - NOT depend on `egg_stack`/`orchestrator_url` — both parameters run - pure-Python in-process against the new `select_substrate(...)` - bundle. No kubectl gate is needed (or claimed) for either - parameter. -- The `substrate` parameter `pytest.skip`s the claude-code dimension - when running inside an in-sandbox-agent trust context (e.g., when - the test process is gateway-blocked from spawning subagents). This - detection uses the same env-var heuristic the existing - `integration_tests/regression/conftest.py` uses for sandbox-aware - test gating. -- No agent-runtime `GATEWAY_URL` env coupling — the spike's - PreToolUse hook runs in the user's Claude Code session, not a - gateway-backed agent pod. - -This satisfies the §10 Trust-Boundary Audit: the only fixture -surface touched is `integration_tests/regression/conftest.py`, which -is regression-suite-local and does not depend on the deleted -`local_pipeline_stack` tier. - -## Slice rationale (single slice) - -cq-11 selected "Spike then plan" — one walking-skeleton slice + a -follow-up issue. The forest constraint is satisfied trivially with -one root slice. No `serialized_chain_order` is needed because no -downstream slices exist in this plan; the follow-up issue starts a -new pipeline. - -LOC estimate ≈ 1,800–2,500 (over the 1k advisory ceiling). This is -expected for a walking-skeleton spike: every interface needs at least -one implementation, and the implementation count is fixed at four -(spawner, bus, policy, worktree) regardless of how the work is split. -Splitting into multiple slices would force the spike-then-plan -fence to fall on an arbitrary intra-spike boundary and re-introduce -serial latency the operator's cq-11 choice was trying to avoid. The -plan reviewer flags this as advisory; per slice-DAG guidance size is -never a NACK trigger. - -## Tasks - -The tasks below are organized by role. Tester and documenter tasks -follow producer tasks they verify or document. - -### TASK-1-1 (coder) — Substrate interfaces + k3s adapter shim - -Create `orchestrator/substrate/` with four `typing.Protocol` -interfaces (`AgentSpawner`, `MessageBus`, `PolicyEnforcer`, -`WorktreeManager`) plus a `select_substrate(env)` factory that reads -`EGG_SUBSTRATE` and returns a bundle. Method signatures must match -cq-4 (`AgentSpawner.spawn(role, prompt, env, worktree) → -AgentResult`) and cq-5 (worktree manager creates/teardowns under a -configurable base; default mirrors `gateway/worktree_manager.py:49`). -`AgentResult` is a dataclass with `stdout`, `exit_code`, -`duration_seconds`, `worktree`, **and `commit_sha: str | None`** — -required so reviewers can attach commit-bound ACKs per the existing -INV-6 invariant in `orchestrator/action_guards.py:631` (invariant -body at `:757`). - -Also ship a thin k3s adapter so `EGG_SUBSTRATE=k3s` (or the empty -default) returns a **working** `AgentSpawner` from day one — the -operator chose cq-1 = "Parallel substrates, env-var-selected" -(parallel, both supported), so the k3s leg cannot raise -`NotImplementedError`. The adapter wraps -`orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn` -(the existing factory) and captures the resulting `commit_sha` via -`git rev-parse HEAD` once the underlying job completes. The only -`NotImplementedError` raised in this slice belongs to TASK-1-6's -`run_pipeline_in_process` k3s leg — the *in-process orchestrator -boot path*, which is the cq-11 walking-skeleton's deliberate -scope-fence — not the spawner protocol itself. - -**Acceptance**: -1. `orchestrator/substrate/spawner.py`, `message_bus.py`, - `policy.py`, `worktree.py`, `__init__.py`, `k3s_adapter.py` exist - with the named classes/functions. -2. `AgentSpawner.spawn(role: AgentRole, prompt: str, env: Mapping[str, - str], worktree: Path) → AgentResult` exposes the cq-4 signature - (synchronous, blocking). `AgentResult` includes - `commit_sha: str | None`. -3. `select_substrate({})` (no `EGG_SUBSTRATE`) defaults to the - `"k3s"` bundle and returns a **working** `K3sSpawnerAdapter` - wrapping `create_concurrent_spawn_fn` - (`orchestrator/kubernetes_spawner.py:1564`). Existing k3s - deployments stay green. -4. `select_substrate({"EGG_SUBSTRATE": "claude-code"})` returns a - bundle wired to the new Claude Code implementations from tasks - TASK-1-2 through TASK-1-5. -5. A module-level docstring in each protocol file links to the ADR - path `docs/architecture/claude-code-substrate.md`. -6. **No production code in `orchestrator/concurrent_executor.py`, - `orchestrator/kubernetes_*.py`, or `gateway/` is modified by this - task.** The k3s adapter is purely additive — it imports from - `kubernetes_spawner.py` but does not patch the call site (that - happens in TASK-1-2). - -**Role**: coder -**Files**: -- `orchestrator/substrate/__init__.py` -- `orchestrator/substrate/spawner.py` -- `orchestrator/substrate/message_bus.py` -- `orchestrator/substrate/policy.py` -- `orchestrator/substrate/worktree.py` -- `orchestrator/substrate/k3s_adapter.py` - -### TASK-1-2 (coder) — ClaudeCodeSpawner + concurrent_executor seam patch - -Implement `ClaudeCodeSpawner` satisfying `AgentSpawner` (TASK-1-1). -The spawner blocks the caller, invokes the underlying Claude Code -`Agent` tool surface (via `shared/egg_harness` when run in-process), -and returns an `AgentResult` once the subagent completes. Internal -concurrency is owned by the spawner (cq-4 explicit), so callers can -issue multiple `spawn()` invocations from a `ThreadPoolExecutor`. The -spawner is responsible for assembling the system prompt via -`build_system_prompt` (`shared/egg_harness/prompt.py:24`) — this is -the structural depth fix from #2622. The spawner runs -`git -C rev-parse HEAD` immediately after the subagent -returns and records the SHA on `AgentResult.commit_sha` — INV-6 -(`orchestrator/action_guards.py:631`, body at `:757`) requires this -so reviewers can attach commit-bound ACKs. - -Also patch `orchestrator/concurrent_executor.py:504 _spawn_agent` so -it dispatches through the `select_substrate(...)`-returned bundle's -`AgentSpawner` instead of importing `KubernetesSpawner` directly. -**Both** substrate legs are working under this patch: the k3s leg -returns the `K3sSpawnerAdapter` shim from TASK-1-1 (wrapping -`create_concurrent_spawn_fn` at -`orchestrator/kubernetes_spawner.py:1564`); the claude-code leg -returns this task's `ClaudeCodeSpawner`. The patch is a re-wiring, -not a behavior change — k3s deployments stay green because the shim -calls into the existing factory the existing call site is built on. - -**Acceptance**: -1. `class ClaudeCodeSpawner` exists in - `orchestrator/substrate/claude_code/spawner.py` and conforms to - the `AgentSpawner` protocol (verified by an `assert - isinstance(...)` in a unit test). -2. `spawn(role, prompt, env, worktree)` returns an `AgentResult` with - `stdout`, `exit_code`, `duration_seconds`, `worktree`, **and - `commit_sha`** (captured via `git -C rev-parse HEAD`) - populated. A unit test asserts `commit_sha` is a 40-char hex - string after a spawn that produced a commit. -3. The spawner calls `build_system_prompt(sources)` from - `shared/egg_harness/prompt.py:24` to assemble the per-role prompt; - this is verified by a unit test that asserts the helper is invoked - with the role's `PromptSource` list. -4. `orchestrator/concurrent_executor.py:504 _spawn_agent` is patched - to dispatch through `select_substrate(os.environ).spawner`; an - existing-k3s-path smoke test (the parametrized substrate test in - TASK-1-8) passes under `EGG_SUBSTRATE=k3s` by routing through the - `K3sSpawnerAdapter` shim. The patch is a re-wire only — no - spawner-internal behavior change. - -**Role**: coder -**Files**: -- `orchestrator/substrate/claude_code/__init__.py` -- `orchestrator/substrate/claude_code/spawner.py` -- `orchestrator/concurrent_executor.py` - -### TASK-1-3 (coder) — InProcessMessageBus - -Implement `InProcessMessageBus` satisfying `MessageBus` (TASK-1-1) -using Python `dict`/`threading.Lock`/`queue` primitives. The bus -serves the same `add_message` / `get_messages` / `wait_for` shape as -`orchestrator/message_store.py:200 MessageStore` (which already does -in-memory, so this implementation may subclass or delegate to it). -The bus must support **the existing BRC concurrency invariants** — -INV-1..5 from `orchestrator/action_guards.py::validate_invariants` — -unchanged; this is the structural fix #2612 documents (orchestrator -preserves BRC mechanics regardless of bus implementation). - -**Acceptance**: -1. `class InProcessMessageBus` in - `orchestrator/substrate/claude_code/message_bus.py` conforms to - `MessageBus`. -2. Stale-version rejection (INV-3) and open-NACK barrier (INV-5) - semantics are preserved — verified by new unit tests at - `shared/tests/test_in_process_message_bus.py` (TASK-1-8) that - issue conflicting proposals/acks against the bus. The existing - BRC test suite at `orchestrator/tests/test_brc_*.py` (7+ files - including `test_brc_open_nacks_barrier.py` and - `test_brc_content_validation.py`) is the **behavioral oracle** - the new bus must match — TASK-1-8's new tests assert the same - semantics on the `InProcessMessageBus` surface. Tester role - allows writes to `**/tests/`, so the new behavioral tests may - alternatively live alongside the existing suite at - `orchestrator/tests/test_brc_in_process_message_bus.py` if the - tester prefers — both locations are tester-allowed and the - acceptance is satisfied either way. - -**Role**: coder -**Files**: -- `orchestrator/substrate/claude_code/message_bus.py` - -### TASK-1-4 (coder) — PreToolUseHookPolicy - -Implement `PreToolUseHookPolicy` satisfying `PolicyEnforcer` -(TASK-1-1) plus a Python hook entry script -(`orchestrator/substrate/claude_code/hook_entry.py`) that is -referenced from a `.claude/settings.json` template. The hook reads -the tool name + tool input from stdin (the PreToolUse contract), -imports `build_agent_patterns` from -`shared/egg_restrictions/patterns.py:768`, and emits -`deny`+`message` JSON to stdout when the write target lands outside -the caller's role's allow-list. The hook's allow/deny semantics match -the gateway's existing `check_agent_restrictions` -(`gateway/phase_filter.py:1061`) — same paths, same role table. - -**Acceptance**: -1. `class PreToolUseHookPolicy` exists in - `orchestrator/substrate/claude_code/policy.py` and conforms to - `PolicyEnforcer`. -2. `orchestrator/substrate/claude_code/hook_entry.py` is a runnable - script (sets `EGG_AGENT_ROLE` from the env, reads stdin JSON, - writes stdout JSON per the Claude Code PreToolUse hook spec). -3. A `.claude/settings.json` template at - `orchestrator/substrate/claude_code/settings.template.json` shows - the hook wired up. -4. The hook denies a write to a path outside the role's allow-list - with an explanatory `message` that matches the gateway's existing - denial format — verified by a unit test that synthesizes a hook - stdin payload and asserts the stdout. -5. The hook uses **the existing** - `shared/egg_restrictions/patterns.py:768 build_agent_patterns` - symbol — no parallel restriction logic. - -**Role**: coder -**Files**: -- `orchestrator/substrate/claude_code/policy.py` -- `orchestrator/substrate/claude_code/hook_entry.py` -- `orchestrator/substrate/claude_code/settings.template.json` - -### TASK-1-5 (coder) — LocalWorktreeManager - -Implement `LocalWorktreeManager` satisfying `WorktreeManager` -(TASK-1-1) using filesystem operations under -`.egg-state///`. Defaults to -`gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` shape but allows -override via env var so the spike can run inside an alternate path. -Creates per-agent worktrees with `git worktree add`, tracks them in a -dict, tears them down at phase end. - -**Acceptance**: -1. `class LocalWorktreeManager` in - `orchestrator/substrate/claude_code/worktree.py` conforms to - `WorktreeManager`. -2. `create(pipeline_id, role)` returns a `Path` under the configured - base; `tear_down(pipeline_id)` removes all worktrees for the - pipeline and cleans up git refs. -3. Default base path matches `gateway/worktree_manager.py:49` - (`~/.egg-worktrees/`) when env var unset; respects - `EGG_WORKTREE_BASE` override per cq-5. -4. Safety: `tear_down` resolves the target through - `child.resolve().is_relative_to(base.resolve())` so a malicious - pipeline ID can't escape the base — same defense as - `gateway/worktree_manager.py:1711` (call site within - `_remove_worktree`; the matching `base.resolve()` is at - `gateway/worktree_manager.py:1700`). - -**Role**: coder -**Files**: -- `orchestrator/substrate/claude_code/worktree.py` - -### TASK-1-6 (coder) — In-process orchestrator entry point + HITL generator - -Build the in-process orchestrator entry point at -`orchestrator/substrate/in_process.py`. This task is **explicit -refactor work** — the orchestrator today is a Flask/waitress HTTP -daemon (`orchestrator/cli.py:83 cmd_serve`) with `PeerConsensusTracker` -locks and `ConcurrentPhaseExecutor` (`orchestrator/concurrent_executor.py:114`) -running its own `ThreadPoolExecutor`. Wrapping this into a generator -that yields `HITLDecision` and resumes via `.send(...)` is the -non-trivial work risk_analyst R4 flagged; it is the **single most -expensive task** in this slice. - -Implements cq-7's heredoc-style synchronous HITL: the function is a -Python generator that yields `HITLDecision` objects (from -`orchestrator/models.py:300`) when the pipeline pauses for a human -decision. The caller (the skill's parent loop) renders each via -`AskUserQuestion`, sends the answer back via `generator.send(...)`, -and the orchestrator resumes. - -The function instantiates `select_substrate(os.environ)` (TASK-1-1), -runs the **refine phase only** with the **refiner role only** (per -the spike scope), and returns the refine-phase artifact path. - -**Acceptance**: -1. `run_pipeline_in_process` is a generator function yielding - `HITLDecision` and returning the refine artifact path on - completion. -2. With `EGG_SUBSTRATE=claude-code` and a curated issue body, the - generator runs the refiner through to completion. The refiner's - output lands under `.egg-state/drafts/-analysis.md` (the - existing drafts location). -3. **Heartbeat-during-HITL**: while the generator is paused at a - yield boundary, the in-process orchestrator's background threads - (heartbeat poll, BRC re-review, message-bus tick) continue to run - so a long-paused HITL does not cause stuck-phase-transition - alerts. Verified by a unit test that pauses at a yielded - `HITLDecision` and asserts heartbeat-thread `is_alive()`. -4. **Background-thread lifetime**: the generator returns cleanly - (background threads joined) on the normal completion path **and** - on `GeneratorExit` (if the caller drops the generator without - exhausting it). Verified by a unit test that drops the generator - mid-cycle and asserts no leaked threads via a `threading.enumerate()` - delta. -5. **Contract-state synchronization**: the in-process orchestrator - uses the same `.egg-state/contracts/.json` filesystem write - path the HTTP daemon uses — no separate state store. Verified by - reading the contract file after the generator yields its first - `HITLDecision` and asserting the pending-decision entry exists. -6. Existing primitives stay in the path: `build_system_prompt` - (`shared/egg_harness/prompt.py:24`), `ConcurrentPhaseExecutor` - (`orchestrator/concurrent_executor.py:114`), `class HITLDecision` - (`orchestrator/models.py:300`), `class PeerConsensusTracker` - (`orchestrator/peer_consensus.py:69`). -7. A `EGG_SUBSTRATE=k3s` call raises `NotImplementedError` with a - message naming the follow-up issue — this is the explicit cq-11 - TODO scope-fence (k3s users still use the HTTP daemon entry - `orchestrator/cli.py:83 cmd_serve`; the *in-process* entry is - claude-code-only in this spike). - -**Role**: coder -**Files**: -- `orchestrator/substrate/in_process.py` - -### TASK-1-7 (coder) — Skill plugin metadata + pre-flight Python glue - -Ship `plugins/egg-sdlc/.claude-plugin/plugin.json` (cq-8 metadata: -declares the pip dependency per **cq-12**, the new HITL decision -asking the operator to pick the canonical pip name / registry — -options: PyPI `egg-shared`, `git+https://github.com/jwbron/egg.git#subdirectory=shared`, -or "Defer to follow-up" TODO placeholder). Whatever the operator -selects is the literal string the plugin.json carries. - -Also ship any Python pre-flight helper the skill needs (for example, -a stub the SKILL.md imports that calls `import egg_orchestrator` and -emits a clear install instruction when the import fails). The -skill's instruction markdown (`SKILL.md` and `agents/refiner.md`) is -**documenter**-owned and is split into TASK-1-11. - -**Acceptance**: -1. `plugins/egg-sdlc/.claude-plugin/plugin.json` exists; lists the - pip dependency string selected by cq-12 verbatim; includes a - marketplace-style `description` and keywords. If cq-12 resolves - to "Defer to follow-up", the dependency field is a TODO comment - pointing at the docs (not an invented package name). -2. Any Python pre-flight helper imports `egg_orchestrator` (or - equivalent) and emits a clear instruction when the import fails - (per feedback Q3: no developer-mode Claude Code feature flags - required). If no Python glue is required (the SKILL.md performs - the import inline via a bash one-liner), this task is metadata- - only and the bullet is N/A. -3. The plugin metadata names the skill the documenter writes in - TASK-1-11 so both halves stitch cleanly at install time. -4. The plugin.json's install-error message in the pre-flight helper - matches the install instruction the documenter writes in - TASK-1-11's SKILL.md — no drift between the two role-split halves. - -**Role**: coder -**Files**: -- `plugins/egg-sdlc/.claude-plugin/plugin.json` -- (optional Python pre-flight helper under - `orchestrator/substrate/claude_code/` if the SKILL.md needs one) - -### TASK-1-8 (tester) — Conformance matrix proof + interface unit tests - -Extend `integration_tests/regression/conftest.py` with a -`substrate` parametrize-able fixture (`"k3s"`, `"claude-code"`); add -a **new** substrate-distinguishing test at -`integration_tests/regression/test_substrate_smoke.py` that drives -the `select_substrate(...).spawner.spawn(...)` and `.bus.add_message(...) -/ .bus.get_messages(...)` surfaces directly. The two substrate -parameters must produce divergent execution paths — under `"k3s"` -the spawner is `K3sSpawnerAdapter` (TASK-1-1) wrapping -`create_concurrent_spawn_fn` -(`orchestrator/kubernetes_spawner.py:1564`), with the underlying -job dispatch mocked so the test does not require kubectl; under -`"claude-code"` the spawner is `ClaudeCodeSpawner` and the bus is -`InProcessMessageBus`. This test is the actual substrate-matrix -proof: it asserts both implementations produce an `AgentResult` with -a populated `commit_sha`, and the bus preserves an INV-3 stale- -version rejection round-trip. - -Also add unit tests under `shared/tests/` (or -`orchestrator/tests/` where the BRC behavioral oracle lives) for the -new protocols and implementations introduced by TASK-1-1 through -TASK-1-6. - -**Acceptance**: -1. `substrate` parametrize fixture exists in - `integration_tests/regression/conftest.py`; the fixture - `pytest.skip`s the claude-code dimension when running inside an - in-sandbox-agent trust context (per the Trust-Boundary section - above). Both dimensions run **pure-Python in-process** — no - kubectl gate is needed for either parameter (the k3s dimension - mocks the underlying job dispatch since the spike's test surface - is the `K3sSpawnerAdapter` shim, not the live k3s API). -2. `integration_tests/regression/test_substrate_smoke.py` is a NEW - substrate-distinguishing test parametrized over `substrate` and - passes on both dimensions. It exercises - `select_substrate(...).spawner.spawn(...)` (asserts populated - `AgentResult.commit_sha`) and `bus.add_message/get_messages` - (asserts INV-3 stale-version rejection round-trip). The two - parameters drive divergent code paths — the k3s parameter routes - through `K3sSpawnerAdapter`, the claude-code parameter routes - through `ClaudeCodeSpawner` + `InProcessMessageBus`. -3. Unit tests cover: (a) `ClaudeCodeSpawner` conforms to - `AgentSpawner` protocol and captures `commit_sha` via - `git rev-parse HEAD`; (b) `K3sSpawnerAdapter` conforms to - `AgentSpawner` and wraps `create_concurrent_spawn_fn`; - (c) `InProcessMessageBus` preserves INV-3 stale-version rejection - and INV-5 open-NACK barrier (using the same scenarios as - `orchestrator/tests/test_brc_open_nacks_barrier.py` and - `orchestrator/tests/test_brc_content_validation.py` as - behavioral templates); (d) `PreToolUseHookPolicy` denies - out-of-role writes matching gateway semantics from - `gateway/phase_filter.py:1061 check_agent_restrictions`; - (e) `LocalWorktreeManager.tear_down` rejects path-escape - attempts via the `is_relative_to` defense - (`gateway/worktree_manager.py:1711`); - (f) `run_pipeline_in_process` raises `NotImplementedError` on - `EGG_SUBSTRATE=k3s` (the cq-11 TODO scope-fence) AND preserves - heartbeat-thread liveness across an HITL yield AND drops - background threads cleanly on `GeneratorExit` — the three R4 - acceptance criteria from TASK-1-6. -4. Tests are written using the existing pytest patterns and respect - the tester role's file restrictions (`tests/`/`**/conftest.py`/etc.). - -**Role**: tester -**Files**: -- `integration_tests/regression/conftest.py` -- `integration_tests/regression/test_substrate_smoke.py` (NEW) -- `shared/tests/test_substrate_interfaces.py` (NEW) -- `shared/tests/test_claude_code_spawner.py` (NEW) -- `shared/tests/test_k3s_spawner_adapter.py` (NEW) -- `shared/tests/test_in_process_message_bus.py` (NEW) -- `shared/tests/test_pretooluse_hook_policy.py` (NEW) -- `shared/tests/test_local_worktree_manager.py` (NEW) -- `shared/tests/test_run_pipeline_in_process.py` (NEW) - -### TASK-1-9 (documenter) — ADR-style substrate-swap design doc - -Write `docs/architecture/claude-code-substrate.md`. Documents the -four interfaces (`AgentSpawner`, `MessageBus`, `PolicyEnforcer`, -`WorktreeManager`), the two implementations (k3s adapter is a -deferred TODO; claude-code implementation is the spike), the -`EGG_SUBSTRATE` selector, the heredoc-HITL surface (cq-7), the -PreToolUse hook seam (cq-6), the WORKTREE_BASE_DIR-style local -worktree model (cq-5), and the conformance-by-CI-matrix proof shape -(cq-3). Link from `docs/architecture/README.md` and -`docs/index.md`. - -**Acceptance**: -1. `docs/architecture/claude-code-substrate.md` exists and is linked - from `docs/architecture/README.md`. -2. Doc covers all twelve cq decisions (cq-1 through cq-12, including - the cq-12 pip-name HITL from this re-propose cycle) and all six - feedback items; each cited decision is named (`cq-N`). -3. Doc names every primitive in the Primitives table above - (existing + new) with at least the module path; full - `file:line` not required (the plan carries that detail) but the - module names are explicit. -4. **Trust-context shift (R1)** subsection names the credential - exposure change: today the Anthropic API key is gateway-isolated; - in the claude-code substrate it lives in the user's parent - session. The subsection states the operator's explicit - acceptance of this shift (per risk_analyst recommendation_rationale - and REC2) and documents the mitigation (skill imports do not log - credentials; the PreToolUse hook does not exfiltrate env to - stdout). -5. **PreToolUse hook fallback (R2)** subsection: documents the - primary cq-6 enforcement seam (PreToolUse hook) AND the fallback - path to cq-6 opt-2 (MCP-validator-side enforcement) if spike - evidence shows the PreToolUse hook cannot reliably resolve the - caller's role for nested subagent dispatch. The follow-up issue - inherits the empirical question. -6. **Subagent context budget regression (R7)** subsection: documents - that Claude Code subagents have a smaller context than egg's - `max_turns: 1000` (`docs/guides/concurrent-execution.md:97`); the - spike accepts this regression per cq-10's hybrid choice - (checkpoint enabled, fork deferred); the smaller-budget fallback - (cq-10 opt-2) is the documented escape if checkpoints prove - inadequate. -7. **Interface stability marker (R10)** subsection: the four - `Protocol`s carry a `# v0.x — unstable until ≥3 roles exercise` - comment in their module docstrings; the ADR states the - interfaces are explicitly subject to revision after the follow-up - issue extends to plan/implement/pr roles. -8. **Cost cap recommendation (REC5)** subsection: notes that - without a gateway, a buggy NACK loop bills the user's Anthropic - account directly; recommends `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` - env var with a conservative default (e.g. 50) but defers the - actual implementation to the follow-up issue. -9. **Subagent type model (R15)** subsection: documents the spike's - choice between `subagent_type="general-purpose"` (prompt-discipline + - PreToolUse hook enforcement, matches existing `refine-plan` - skill) and custom `subagent_type` via - `.claude/agents/.md` (structural tool restrictions). The - spike picks `"general-purpose"` to match the existing layout; - the ADR documents the trade-off and reserves a future custom - subagent_type migration to the follow-up. -10. "Open work" appendix explicitly lists what the spike does NOT do - (plan/implement/pr phases, BRC concurrency, full 5-issue - conformance, k3s interface adapter beyond the shim, - EggHarnessSpawner, prune verb, fork-based sub-task delegation, - `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` implementation, custom - `subagent_type` migration) so the follow-up issue body has a - written source. - -**Role**: documenter -**Files**: -- `docs/architecture/claude-code-substrate.md` -- `docs/architecture/README.md` (link entry — single-line - modification to the existing index) - -### TASK-1-10 (documenter) — Follow-up issue draft - -Compose the follow-up issue body capturing the rollout work the spike -defers. Write it to `docs/architecture/claude-code-substrate.md` -under an explicit "Follow-up issue draft" section (since -`documenter` is blocked from `.github/` and from creating issues -directly — a human files the issue using this section's contents). - -**Acceptance**: -1. `docs/architecture/claude-code-substrate.md` includes a - "Follow-up issue draft" section near the bottom containing the - issue body text the reviewer pastes when filing the follow-up. -2. The draft lists, with one bullet per item: extend claude-code - substrate to plan/implement/pr phases (cq-2 unfinished); land full - conformance matrix across all 5 curated issues (Q1); set perf/ - latency budget (Q2); implement k3s interface adapter (cq-1's k3s - side); optional EggHarnessSpawner (Q4); ship `egg-state prune` - verb (Q6); fork-based sub-task delegation (cq-10's deferred half). -3. Section explicitly states "this is reviewer-pasted, not auto-filed - from the pipeline" so the operator understands the handoff. - -**Role**: documenter -**Files**: -- `docs/architecture/claude-code-substrate.md` (extended — same file - as TASK-1-9; written as the same task split because documenter is - one role) - -### TASK-1-11 (documenter) — Skill instructions + role prompt - -Author the user-facing skill instruction markdown -(`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`) and the per-role prompt- -prepend file (`plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md`). -The SKILL.md documents the heredoc-HITL loop as the user-facing -contract — how the parent session imports `run_pipeline_in_process`, -drives the generator, and renders each `HITLDecision` via -`AskUserQuestion`. The `agents/refiner.md` file mirrors the existing -`refine-plan` plugin's per-role layout -(`plugins/refine-plan/skills/refine-plan/agents/refiner.md`) so the -in-process orchestrator's `build_system_prompt(sources)` can prepend -it just like the k3s substrate prepends `shared/prompts/`-resident -role files. - -**Acceptance**: -1. `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` exists and documents - the heredoc-HITL loop. -2. `plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md` exists and - follows the existing - `plugins/refine-plan/skills/refine-plan/agents/refiner.md` - shape (front-matter + body) so the in-process orchestrator can - read it without per-skill custom logic. -3. SKILL.md states **explicitly** that the spike's exercised scope is - refiner-only; plan/implement/pr roles are documented as out of - scope (matches TASK-1-7's plugin metadata). - -**Role**: documenter -**Files**: -- `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` -- `plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md` - -## Test strategy - -### Automated coverage (TASK-1-8) - -- **Protocol conformance** (`shared/tests/test_substrate_interfaces.py`): - asserts each new implementation satisfies its protocol via - `isinstance(impl, Protocol)`-style checks. -- **Behavioral parity** (parametrized regression test): the existing - `test_brc_single_cycle.py` runs under both `substrate` parameters; - the claude-code parameter exercises `InProcessMessageBus` + - `ClaudeCodeSpawner`. Pass on both = behavioral parity proof. -- **BRC invariants** (`test_in_process_message_bus.py`): preserves - INV-3 stale-version rejection and INV-5 open-NACK barrier across - the new bus. -- **Hook semantics** (`test_pretooluse_hook_policy.py`): a synthetic - PreToolUse stdin payload yields stdout that denies an out-of-role - write with a gateway-equivalent message. -- **Worktree safety** (`test_local_worktree_manager.py`): - `tear_down` rejects path-escape attempts; create/destroy is - symmetric. -- **Boot path** (`test_run_pipeline_in_process.py`): with - `EGG_SUBSTRATE=claude-code`, the generator advances; with - `EGG_SUBSTRATE=k3s`, it raises `NotImplementedError` (the cq-11 - TODO marker is visible to tests, not a hidden surprise). - -### Manual verification (reviewer) - -1. `pip install egg-shared` (or whichever pip name is settled by - TASK-1-7), then add the `egg-sdlc` plugin path to Claude Code. -2. From a fresh Claude Code session, run `/egg-sdlc ` - — the curated spike target. -3. Observe: skill bootstraps the in-process orchestrator; the - refiner subagent runs in a worktree under - `.egg-state///`; the PreToolUse hook denies an - out-of-role write (test by editing a path the role isn't allowed - to touch); HITL surfaces through `AskUserQuestion`; refine - artifact lands under `.egg-state/drafts/-analysis.md`. -4. Verify the ADR (`docs/architecture/claude-code-substrate.md`) is - linked from `docs/architecture/README.md` and accurately - describes the substrate seam. - -## Manual steps - -**Pre-merge**: -1. **Follow-up issue filing** (TASK-1-10): the reviewer copies the - "Follow-up issue draft" section out of the ADR and files a new - GitHub issue. The follow-up issue captures: plan/implement/pr - phase rollout, full conformance matrix, perf budget, k3s adapter, - EggHarnessSpawner, prune verb, fork-based sub-task delegation. - Documenter cannot file the issue directly (role-blocked from - `.github/`). -2. **`.claude/settings.json` review**: the - `orchestrator/substrate/claude_code/settings.template.json` - shipped by TASK-1-4 is a *template* — users opt in by copying it - into their per-skill or per-project `.claude/settings.json`. The - reviewer confirms the template is documented in the ADR and not - silently activated. -3. **No `.github-staging/` changes** are needed by this spike (no CI - workflow / CODEOWNERS / dependabot edits). - -**Post-merge**: -1. Operator installs the `egg-sdlc` plugin from the marketplace (once - it's published — packaging is per cq-8 metadata-only, no wheel - shipping). Test that `pip install egg-shared` is the only manual - step required. -2. Operator runs the curated spike issue through the new substrate - and reports back on subagent context behavior, hook ergonomics, - and Agent-tool throughput — the qualitative inputs feeding - feedback Q2's deferred latency budget. - -```yaml -# yaml-tasks -pr: - title: |- - Spike: walking-skeleton Claude Code substrate for egg SDLC (#2623) - description: | - ## Context - - Issue #2623 reframes the prior "approximate k3s pipeline quality" - goal as a substrate swap: instead of building a parallel - Markdown-skill implementation and chasing parity, swap the - substrate underneath the real `egg_orchestrator` stack so quality - becomes structurally inevitable. The refine phase settled eleven - decisions; the most consequential is **cq-11 = "Spike then - plan"** — land a walking-skeleton slice for one role end-to-end - on the Claude Code substrate, then re-plan the rollout in a - follow-up issue once real numbers exist for subagent context - behavior, hook ergonomics, and `Agent` tool throughput. - - ## Changes - - 1. **Substrate interfaces** (`orchestrator/substrate/`): - `AgentSpawner`, `MessageBus`, `PolicyEnforcer`, and - `WorktreeManager` as `typing.Protocol`s, plus a - `select_substrate(env)` factory that reads `EGG_SUBSTRATE` - (cq-1). The k3s adapter is a deferred TODO that raises - `NotImplementedError` (cq-11 explicit boundary). - 2. **Claude Code implementations**: `ClaudeCodeSpawner` - (synchronous spawn per cq-4), `InProcessMessageBus` - (preserves BRC INV-3/INV-5), `PreToolUseHookPolicy` (cq-6 - PreToolUse hook routing into existing - `shared/egg_restrictions/patterns.py:768 - build_agent_patterns`), `LocalWorktreeManager` (cq-5 - WORKTREE_BASE_DIR shape). - 3. **In-process orchestrator boot**: - `run_pipeline_in_process(...)` is a generator yielding - `HITLDecision` objects (cq-7 heredoc-style synchronous HITL); - the skill renders each via `AskUserQuestion`. - 4. **Skill entry point** (`plugins/egg-sdlc/`): plugin metadata - declares the pip dependency (cq-8); the skill drives the - refiner role end-to-end against a curated spike issue. - 5. **Conformance matrix proof**: - `integration_tests/regression/conftest.py` gains a `substrate` - parameter; one test in `test_brc_single_cycle.py` is - parametrized over both substrates. Full matrix factor-out is - deferred to the follow-up. - 6. **ADR**: `docs/architecture/claude-code-substrate.md` names - the interfaces, the implementations, the deferred work, and - the follow-up issue draft. - - ## Impact - - Users with Claude Code installed can `pip install egg-shared` and - run the curated spike issue through the refiner role on the new - substrate with no k3s/Redis/Docker/gateway daemon. k3s users are - unaffected — `KubernetesSpawner`, `RedisMessageStore`, and the - gateway sidecar are untouched; the spike only re-routes the - spawn seam at `orchestrator/concurrent_executor.py:504` through - the new protocol. The follow-up issue (drafted in the ADR) - captures plan/implement/pr phases, full conformance matrix, perf - budget, k3s adapter, optional `EggHarnessSpawner`, prune verb, - and fork-based sub-task delegation. - test_plan: | - - Automated: protocol-conformance unit tests, BRC INV-3/INV-5 - preservation against `InProcessMessageBus`, PreToolUse hook - semantics, worktree path-escape safety, `EGG_SUBSTRATE=k3s` - raises the cq-11 TODO marker, and `test_brc_single_cycle.py` - passes under both `substrate` parameters. - - Manual: `pip install egg-shared`; install the `egg-sdlc` - plugin; run `/egg-sdlc ` in a fresh - Claude Code session; verify the refiner artifact lands under - `.egg-state/drafts/`; deliberately attempt an out-of-role - write to confirm the PreToolUse hook denies it; verify HITL - surfaces through `AskUserQuestion`. - manual_steps: | - Pre-merge: - - Reviewer files the follow-up issue using the - "Follow-up issue draft" section in - `docs/architecture/claude-code-substrate.md`. Documenter is - role-blocked from `.github/` so it cannot auto-file. - - Confirm - `orchestrator/substrate/claude_code/settings.template.json` is - treated as a template (users opt in by copying it into their - own `.claude/settings.json`), not silently activated. - - Post-merge: - - Operator installs the `egg-sdlc` plugin and runs the curated - spike issue end-to-end; reports back qualitative observations - (subagent context behavior, hook ergonomics, `Agent` tool - throughput) so the follow-up issue can set a perf/latency - budget (feedback Q2). - context_title: |- - Strategic plan for #2623 — substrate-swap refine/plan analysis + BRC history - context_description: | - Issue #2623's spike-then-plan slice ships under a stacked PR. - This context PR carries the refine analysis, the plan, and the - BRC consensus history that approved each. Reviewers approaching - the slice PR can read this PR first for the strategic narrative - — why "Spike then plan" was chosen over the parallel rollouts, - how the eleven cq-N decisions and six feedback answers shape the - scope, and what the follow-up issue inherits. -slices: - - id: 1 - name: |- - Walking-skeleton Claude Code substrate (refiner-only spike) - goal: |- - Land the four substrate interfaces (AgentSpawner, MessageBus, - PolicyEnforcer, WorktreeManager), the Claude Code - implementations of each, the in-process orchestrator boot - generator, the egg-sdlc skill entry point, one passing - regression test parametrized over substrate, the ADR, and a - reviewer-pasted follow-up issue draft. k3s side stays - untouched; the k3s adapter is a deferred TODO marker per - cq-11. Spike scope is refiner-only; plan/implement/pr roles - and the broader rollout are explicitly out of scope. - tasks: - - id: TASK-1-1 - description: |- - Create `orchestrator/substrate/` with four - `typing.Protocol` interfaces (`AgentSpawner`, `MessageBus`, - `PolicyEnforcer`, `WorktreeManager`) plus a - `select_substrate(env)` factory reading `EGG_SUBSTRATE`. - `AgentSpawner.spawn` signature pinned by cq-4 (synchronous, - blocking). `AgentResult` dataclass MUST include - `commit_sha: str | None` to satisfy INV-6 - (`orchestrator/action_guards.py:631`, body at `:757`). - Also ship `orchestrator/substrate/k3s_adapter.py` — a - working `K3sSpawnerAdapter` wrapping - `orchestrator/kubernetes_spawner.py:1564 - create_concurrent_spawn_fn` and capturing `commit_sha` via - `git rev-parse HEAD` so the k3s leg of `select_substrate` - returns a working spawner from day one (cq-1 parallel- - substrates). Module docstrings link to the ADR - (TASK-1-9) and carry the `# v0.x unstable` marker (R10). - acceptance: |- - Module files exist - (`__init__.py`, `spawner.py`, `message_bus.py`, - `policy.py`, `worktree.py`, `k3s_adapter.py`); - `AgentSpawner.spawn` matches cq-4; `AgentResult` includes - `commit_sha: str | None`; - `select_substrate({})` defaults to `"k3s"` and returns a - working `K3sSpawnerAdapter` wrapping - `create_concurrent_spawn_fn` - (`orchestrator/kubernetes_spawner.py:1564`); - `select_substrate({"EGG_SUBSTRATE": "claude-code"})` - returns a bundle wired to the claude-code - implementations (TASK-1-2..1-5); - `orchestrator/concurrent_executor.py`, - `orchestrator/kubernetes_*.py`, and `gateway/` are NOT - modified by this task (the k3s adapter imports from - `kubernetes_spawner.py` but does not patch the call - site). - role: coder - files: - - orchestrator/substrate/__init__.py - - orchestrator/substrate/spawner.py - - orchestrator/substrate/message_bus.py - - orchestrator/substrate/policy.py - - orchestrator/substrate/worktree.py - - orchestrator/substrate/k3s_adapter.py - - id: TASK-1-2 - description: |- - Implement `ClaudeCodeSpawner` satisfying `AgentSpawner` - (TASK-1-1). Blocks the caller, dispatches to Claude Code's - subagent surface via `shared/egg_harness`, returns an - `AgentResult` once the subagent completes. Internal - concurrency is owned by the spawner per cq-4. Assembles - the system prompt via `build_system_prompt` - (`shared/egg_harness/prompt.py:24`) — structural fix from - #2622. Captures `commit_sha` via - `git -C rev-parse HEAD` immediately after the - subagent returns and records it on `AgentResult.commit_sha` - (INV-6). Also patches - `orchestrator/concurrent_executor.py:504 _spawn_agent` to - dispatch through - `select_substrate(os.environ).spawner` — both substrate - legs are working (k3s via TASK-1-1's `K3sSpawnerAdapter`, - claude-code via this task's `ClaudeCodeSpawner`). - acceptance: |- - `ClaudeCodeSpawner` conforms to `AgentSpawner` (verified - by isinstance check in a unit test); - `spawn(role, prompt, env, worktree)` returns an - `AgentResult` with `stdout`, `exit_code`, - `duration_seconds`, `worktree`, and `commit_sha` - (40-char hex captured via `git rev-parse HEAD`); - `build_system_prompt` is invoked with the role's - `PromptSource` list (verified by unit test); - `orchestrator/concurrent_executor.py:504` dispatches via - `select_substrate(...)`; a smoke test asserts the - `_spawn_agent` dispatch routes through the bundle under - both `EGG_SUBSTRATE=k3s` (via `K3sSpawnerAdapter`) and - `EGG_SUBSTRATE=claude-code` (via `ClaudeCodeSpawner`). - role: coder - files: - - orchestrator/substrate/claude_code/__init__.py - - orchestrator/substrate/claude_code/spawner.py - - orchestrator/concurrent_executor.py - - id: TASK-1-3 - description: |- - Implement `InProcessMessageBus` satisfying `MessageBus` - (TASK-1-1) using Python dict / threading.Lock / queue. - Preserves BRC invariants INV-3 (stale-version rejection) - and INV-5 (open-NACK barrier) from - `orchestrator/action_guards.py:631 validate_invariants`. - May subclass or delegate to - `orchestrator/message_store.py:200 MessageStore`. The - behavioral oracle is the existing BRC test suite at - `orchestrator/tests/test_brc_*.py` (7+ files including - `test_brc_open_nacks_barrier.py` and - `test_brc_content_validation.py`); TASK-1-8 ports the - equivalent scenarios onto the new bus surface. - acceptance: |- - `InProcessMessageBus` conforms to `MessageBus`; INV-3 and - INV-5 preserved (verified by TASK-1-8 unit tests at - `shared/tests/test_in_process_message_bus.py` that mirror - the scenarios in - `orchestrator/tests/test_brc_open_nacks_barrier.py` and - `orchestrator/tests/test_brc_content_validation.py`); the - bus operates without any Redis or k3s dependency. - role: coder - files: - - orchestrator/substrate/claude_code/message_bus.py - - id: TASK-1-4 - description: |- - Implement `PreToolUseHookPolicy` (`PolicyEnforcer`) plus a - Python hook entry script - (`orchestrator/substrate/claude_code/hook_entry.py`) - referenced from a `.claude/settings.json` template. The - hook reads tool name + tool input from stdin (PreToolUse - contract), imports `build_agent_patterns` from - `shared/egg_restrictions/patterns.py:768`, emits deny+ - message JSON when the write target is outside the role's - allow-list. Allow/deny semantics match the gateway's - existing `check_agent_restrictions` - (`gateway/phase_filter.py:1061`). - acceptance: |- - `PreToolUseHookPolicy` conforms to `PolicyEnforcer`; - `hook_entry.py` is runnable as a script; a template - `settings.template.json` shows the hook wired up; an - out-of-role write yields a stdout JSON deny with a - gateway-equivalent message (verified by unit test in - TASK-1-8); the hook imports - `shared/egg_restrictions/patterns.py:768 - build_agent_patterns` directly — no parallel logic. - role: coder - files: - - orchestrator/substrate/claude_code/policy.py - - orchestrator/substrate/claude_code/hook_entry.py - - orchestrator/substrate/claude_code/settings.template.json - - id: TASK-1-5 - description: |- - Implement `LocalWorktreeManager` satisfying - `WorktreeManager` (TASK-1-1) using filesystem ops under - `.egg-state///`. Default base mirrors - `gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` shape - (~/.egg-worktrees/) but `EGG_WORKTREE_BASE` overrides per - cq-5. Creates per-agent worktrees with `git worktree add`; - tracks them in a dict; tears them down at phase end. - Path-escape safety mirrors the `is_relative_to` defense at - `gateway/worktree_manager.py:1711` (call site within - `_remove_worktree`; matching `base.resolve()` at - `gateway/worktree_manager.py:1700`). - acceptance: |- - `LocalWorktreeManager` conforms to `WorktreeManager`; - `create(pipeline_id, role)` returns a path under the - configured base; `tear_down(pipeline_id)` removes all - worktrees and cleans up git refs; default base matches - `gateway/worktree_manager.py:49` when env var unset; - escape attempts are rejected via - `child.resolve().is_relative_to(base.resolve())` (verified - by unit test in TASK-1-8 using - `gateway/worktree_manager.py:1711` as the reference - pattern). - role: coder - files: - - orchestrator/substrate/claude_code/worktree.py - - id: TASK-1-6 - description: |- - Build the in-process orchestrator entry point at - `orchestrator/substrate/in_process.py`. This is the slice's - most expensive task — risk_analyst R4 flagged the - orchestrator-reentrancy refactor as `requires_human_review` - (effort HIGH). Today the orchestrator is a Flask/waitress - HTTP daemon (`orchestrator/cli.py:83 cmd_serve`) with - `ConcurrentPhaseExecutor` - (`orchestrator/concurrent_executor.py:114`) running its own - `ThreadPoolExecutor` and `PeerConsensusTracker` - (`orchestrator/peer_consensus.py:69`) holding its own - locks. Wrapping into a generator that yields - `HITLDecision` (cq-7 heredoc-style synchronous HITL) and - resumes via `.send(...)` is the actual refactor. - acceptance: |- - `run_pipeline_in_process` is a generator yielding - `HITLDecision` and returning the refine artifact path on - completion. With `EGG_SUBSTRATE=claude-code` and a curated - issue body the refiner completes; artifact lands under - `.egg-state/drafts/-analysis.md`. - **Heartbeat-during-HITL**: background heartbeat poll, BRC - re-review, and message-bus tick threads remain alive - while the generator is paused at a yield boundary - (verified by a unit test asserting heartbeat-thread - `is_alive()` at the yield). - **Background-thread lifetime**: the generator returns - cleanly on both normal completion AND on `GeneratorExit` - (verified by a unit test that drops the generator - mid-cycle and asserts no leaked threads via - `threading.enumerate()` delta). - **Contract-state synchronization**: the in-process - orchestrator writes to the same - `.egg-state/contracts/.json` filesystem path the HTTP - daemon uses (verified by reading the contract file after - the first yield and asserting the pending-decision entry - exists). - Existing primitives stay in the path: `build_system_prompt` - (`shared/egg_harness/prompt.py:24`), - `ConcurrentPhaseExecutor` - (`orchestrator/concurrent_executor.py:114`), - `HITLDecision` (`orchestrator/models.py:300`), - `PeerConsensusTracker` - (`orchestrator/peer_consensus.py:69`). - `EGG_SUBSTRATE=k3s` raises `NotImplementedError` with a - message naming the follow-up issue (k3s users keep using - `orchestrator/cli.py:83 cmd_serve`; the in-process entry - is claude-code-only for this spike — cq-11 scope-fence). - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-1-7 - description: |- - Ship `plugins/egg-sdlc/.claude-plugin/plugin.json` per - cq-8. The pip dependency string is whatever the operator - selects in **cq-12** (the new HITL decision asking for - canonical pip name + registry — options: PyPI - `egg-shared`, `git+https://github.com/jwbron/egg.git#subdirectory=shared`, - or "Defer to follow-up" TODO placeholder). Also ship any - Python pre-flight helper the SKILL.md will call (e.g. a - stub that calls `import egg_orchestrator` and emits a - clear install instruction when missing). User-facing - instruction markdown (SKILL.md + role files) is - documenter-owned and split into TASK-1-11. - acceptance: |- - `plugins/egg-sdlc/.claude-plugin/plugin.json` exists, - lists the pip dependency string selected by cq-12 - verbatim (or a TODO comment pointing at the docs if cq-12 - resolves to "Defer to follow-up"), includes marketplace - description and keywords; if a Python pre-flight helper is - shipped it fails loudly when `egg_orchestrator` is missing - (no developer-mode CC feature flag required per feedback - Q3); the plugin metadata names the skill the documenter - writes in TASK-1-11; the install-error message in the - pre-flight helper matches the install instruction - documenter writes in TASK-1-11's SKILL.md (no drift - between the two role-split halves). - role: coder - files: - - plugins/egg-sdlc/.claude-plugin/plugin.json - - id: TASK-1-8 - description: |- - Extend `integration_tests/regression/conftest.py` with a - `substrate` parametrize-able fixture - (`"k3s"`, `"claude-code"`). Add a NEW - substrate-distinguishing test at - `integration_tests/regression/test_substrate_smoke.py` - that drives `select_substrate(...).spawner.spawn(...)` and - `.bus.add_message/get_messages` directly — under - `"k3s"` the spawner is `K3sSpawnerAdapter` (mocked - underlying job dispatch); under `"claude-code"` the - spawner is `ClaudeCodeSpawner` + `InProcessMessageBus`. - Both parameters run pure-Python in-process; no kubectl - gate is needed. Also add unit tests under `shared/tests/` - (or `orchestrator/tests/` where the BRC behavioral oracle - lives) covering protocols, implementations, and TASK-1-6's - three R4 acceptance bullets (heartbeat-during-HITL, - background-thread lifetime, contract-state sync). - acceptance: |- - `substrate` fixture exists in - `integration_tests/regression/conftest.py`; claude-code - dimension `pytest.skip`s when running inside an - in-sandbox-agent trust context; - `test_substrate_smoke.py` is parametrized over - `substrate` and passes on both dimensions - (pure-Python, no kubectl); the test exercises - `select_substrate(...).spawner.spawn(...)` (asserts - populated `AgentResult.commit_sha`) and - `.bus.add_message/get_messages` (asserts INV-3 - stale-version rejection); unit tests cover - (a) `ClaudeCodeSpawner` conforms to `AgentSpawner` and - captures `commit_sha`; (b) `K3sSpawnerAdapter` conforms - and wraps `create_concurrent_spawn_fn` - (`orchestrator/kubernetes_spawner.py:1564`); - (c) `InProcessMessageBus` preserves INV-3 and INV-5 using - scenarios from - `orchestrator/tests/test_brc_open_nacks_barrier.py` and - `orchestrator/tests/test_brc_content_validation.py` as - oracles; (d) `PreToolUseHookPolicy` denies out-of-role - writes matching `gateway/phase_filter.py:1061 - check_agent_restrictions`; (e) `LocalWorktreeManager` - path-escape rejection via - `gateway/worktree_manager.py:1711`; (f) - `run_pipeline_in_process` raises `NotImplementedError` - for `EGG_SUBSTRATE=k3s`, preserves heartbeat-thread - liveness across HITL yields, and drops background - threads cleanly on `GeneratorExit`; all tests respect - the tester role's file restrictions. - role: tester - files: - - integration_tests/regression/conftest.py - - integration_tests/regression/test_substrate_smoke.py - - shared/tests/test_substrate_interfaces.py - - shared/tests/test_claude_code_spawner.py - - shared/tests/test_k3s_spawner_adapter.py - - shared/tests/test_in_process_message_bus.py - - shared/tests/test_pretooluse_hook_policy.py - - shared/tests/test_local_worktree_manager.py - - shared/tests/test_run_pipeline_in_process.py - - id: TASK-1-9 - description: |- - Write the ADR-style design doc - `docs/architecture/claude-code-substrate.md`. Documents - the four interfaces, the implementations (working - K3sSpawnerAdapter shim + claude-code implementations), - the `EGG_SUBSTRATE` selector, the cq-7 heredoc-HITL - surface, the cq-6 PreToolUse hook seam, the cq-5 local - worktree model, and the cq-3 conformance-by-CI-matrix - shape. Names every primitive (existing + new) in the - Primitives table. Adds risk-mitigation subsections for - R1/R2/R7/R10 and recommendations REC5/R15. Links to/from - `docs/architecture/README.md`. - acceptance: |- - `docs/architecture/claude-code-substrate.md` exists and - is linked from `docs/architecture/README.md`; doc covers - all twelve cq decisions (cq-1..cq-12) and all six - feedback items; doc names every primitive in the - Primitives table (module names explicit); "Trust-context - shift (R1)" subsection names credential exposure change - and operator's acceptance; "PreToolUse hook fallback - (R2)" subsection documents primary + fallback enforcement - paths; "Subagent context budget regression (R7)" - subsection documents `max_turns: 1000` regression and - checkpoint mitigation; "Interface stability marker (R10)" - subsection states `# v0.x unstable` and revision policy; - "Cost cap (REC5)" subsection recommends - `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`; "Subagent type - model (R15)" subsection documents - `subagent_type="general-purpose"` choice; "Open work" - appendix lists what the spike does NOT do - (plan/implement/pr phases, BRC concurrency, full 5-issue - conformance, k3s interface adapter beyond the shim, - EggHarnessSpawner, prune verb, fork-based sub-task - delegation, `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` - implementation, custom `subagent_type` migration). - role: documenter - files: - - docs/architecture/claude-code-substrate.md - - docs/architecture/README.md - - id: TASK-1-10 - description: |- - Extend `docs/architecture/claude-code-substrate.md` (same - file as TASK-1-9) with a "Follow-up issue draft" section - containing the issue body text the reviewer copies when - filing the rollout follow-up. Documenter is role-blocked - from `.github/` so the section is reviewer-pasted, not - auto-filed. - acceptance: |- - "Follow-up issue draft" section exists near the bottom - of `docs/architecture/claude-code-substrate.md`; lists, - one bullet per item: extend to plan/implement/pr phases - (cq-2 unfinished), full conformance matrix (Q1), perf - budget (Q2), full k3s interface adapter beyond the shim - (cq-1 k3s side), optional EggHarnessSpawner (Q4), ship - `egg-state prune` verb (Q6), fork-based sub-task - delegation (cq-10 deferred half), implement - `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost cap (REC5), - migrate to custom `subagent_type` per-role agent files - (R15); section header explicitly states - "reviewer-pasted, not auto-filed" so the operator - understands the handoff. - role: documenter - files: - - docs/architecture/claude-code-substrate.md - - id: TASK-1-11 - description: |- - Author the user-facing skill instruction markdown - (`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`) plus the - per-role prompt-prepend file - (`plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md`) - mirroring the layout of - `plugins/refine-plan/skills/refine-plan/agents/refiner.md`. - SKILL.md documents the heredoc-HITL loop as the - user-facing contract (how the parent session drives the - `run_pipeline_in_process` generator and renders each - `HITLDecision` via `AskUserQuestion`). The refiner role - file is what `build_system_prompt(sources)` prepends. - acceptance: |- - `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` exists and - documents the heredoc-HITL loop; - `plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md` - exists and follows the existing - `plugins/refine-plan/skills/refine-plan/agents/refiner.md` - front-matter+body shape; SKILL.md states explicitly that - the spike's exercised scope is refiner-only (plan/ - implement/pr roles documented as out of scope, matching - TASK-1-7's plugin metadata). - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md -``` diff --git a/.egg-state/drafts/2717-analysis.md b/.egg-state/drafts/2717-analysis.md deleted file mode 100644 index 115f5d2dca..0000000000 --- a/.egg-state/drafts/2717-analysis.md +++ /dev/null @@ -1,221 +0,0 @@ -# Analysis: Substrate-swap follow-up — wire remaining roles + plan/implement/pr phases on Claude Code substrate - -> Issue: #2717 | Phase: refine - -## Problem Statement - -PR [#2715](https://github.com/jwbron/egg/pull/2715) shipped the **walking-skeleton spike** for the Claude Code substrate ([parent #2623](https://github.com/jwbron/egg/issues/2623)). The spike landed: - -- Four substrate `typing.Protocol`s (`AgentSpawner`, `MessageBus`, `PolicyEnforcer`, `WorktreeManager`) under `orchestrator/substrate/`. -- A working Claude Code implementation for each (`ClaudeCodeSpawner`, `InProcessMessageBus`, `PreToolUseHookPolicy`, `LocalWorktreeManager`) under `orchestrator/substrate/claude_code/`. -- A `K3sSpawnerAdapter` shim so `EGG_SUBSTRATE=k3s` stays green for the legacy substrate. -- `run_pipeline_in_process(...)` — a generator-shaped in-process orchestrator entry (`orchestrator/substrate/in_process.py`). -- The `egg-sdlc` plugin scaffold (`plugins/egg-sdlc/`) with the install / pre-flight surface. -- One per-role rubric (`plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md`). -- ADR at `docs/architecture/claude-code-substrate.md`. -- A substrate-parametrized smoke test (`integration_tests/regression/test_substrate_smoke.py`). - -Per **cq-11 = "Spike then plan"**, the spike intentionally proved exactly *one role × one phase* end-to-end. This issue is the re-plan that rolls the substrate out from there. **Desired outcome**: every agent role on the SDLC pipeline runs on the Claude Code substrate; all four phases (refine + plan + implement + pr) work; the conformance matrix is green on both substrate dimensions; the operator can drive a full pipeline including all HITL decisions through `AskUserQuestion` without an MCP `provide_input` round-trip; `EggHarnessSpawner` ships as a third spawner implementation; the ADR is refreshed. - -The issue body's "Definition of done" enumerates six concrete completion criteria; the "Rollout deltas" section of the ADR's Follow-up draft (`docs/architecture/claude-code-substrate.md:272-287`) lists eleven deferred items. The bulk of this analysis is about *how* those items decompose, *what order* to ship them in, and *which* of the still-open design questions need the operator's input. - -## Current Behavior - -### What works today (post-#2715) - -- `select_substrate(env)` factory (`orchestrator/substrate/__init__.py:77-94`) reads `EGG_SUBSTRATE` and returns either the k3s shim or the claude-code bundle; the seam at `orchestrator/concurrent_executor.py:_spawn_agent` dispatches via the bundle's spawner only when `EGG_SUBSTRATE=claude-code` is set (reviewer v1 blocker #1). -- `ClaudeCodeSpawner` (`orchestrator/substrate/claude_code/spawner.py:64`) blocks the caller, drives the egg harness loop in-process, assembles the role rubric via `build_system_prompt(sources)` from `shared/egg_harness/prompt.py:24`, and captures `commit_sha` from `git -C rev-parse HEAD` so reviewers can attach commit-bound ACKs per INV-6 (`orchestrator/action_guards.py:631`, body at `:757`). -- **Critical scope note (`orchestrator/substrate/claude_code/spawner.py:9-26`)**: today's `ClaudeCodeSpawner` re-hosts `egg_harness.run_agent` *in the parent Claude Code session's Python process* — it does NOT dispatch via Claude Code's native `Agent` tool with `subagent_type="general-purpose"`. The ADR documents this as a deliberate spike scope decision and reserves a future `ClaudeCodeAgentToolSpawner` for the real Agent-tool dispatch path. This is one of the open questions below (cq-3 in Open Questions). -- `InProcessMessageBus` (`orchestrator/substrate/claude_code/message_bus.py:30`) subclasses `orchestrator/message_store.py:200 MessageStore` — the same in-memory bus that already powers `EGG_MESSAGE_STORE_BACKEND=memory` local-dev runs, so BRC mechanics survive substrate transitions per INV-3 / INV-5 / INV-6. -- `PreToolUseHookPolicy` + `hook_entry.py` calls into `shared/egg_restrictions/patterns.py:768 build_agent_patterns` — single source of truth shared with the gateway. Validated only for the single-role refiner case (`EGG_AGENT_ROLE` set in the spawn env, hook reads it). -- `LocalWorktreeManager` defaults to `~/.egg-worktrees///`, respects `EGG_WORKTREE_BASE`, mirrors the `is_relative_to` + `resolve()` defense at `gateway/worktree_manager.py:1700-1711`. -- `run_pipeline_in_process(...)` (`orchestrator/substrate/in_process.py:155`) is a Python generator with three daemon threads (heartbeat, BRC re-review, bus tick) and `GeneratorExit` discipline; **runs the refiner role only** and raises `NotImplementedError` for any other phase. The k3s leg of this entry point also raises `NotImplementedError` with a marker substring (`_K3S_FENCE_MESSAGE`). - -### What does NOT work today - -- **No skill driver invokes `run_pipeline_in_process(...)`.** SKILL.md (`plugins/egg-sdlc/skills/egg-sdlc/SKILL.md:97-102`) explicitly flags this: *"A Claude Code skill cannot drive a long-lived Python generator across multiple `AskUserQuestion` round-trips today: `AskUserQuestion` is a tool the LLM calls, not a function callable from inside a `python3` subprocess, and every `python3` invocation from a Bash skill step is a fresh process whose generator state, background threads, and `gi_frame` die at exit."* The bridge is reviewer v1 blocker #6 / reviewer v2 blocker B1 — deferred. The install / pre-flight machinery works; the orchestrator-boot step has no driver shipped today. -- **Only the refiner has a rubric file**. `plugins/egg-sdlc/skills/egg-sdlc/agents/` contains only `refiner.md`; the `_load_egg_sdlc_role_rubric` loader (`orchestrator/substrate/__init__.py:278-284`) raises `ValueError` for any other role. -- **Phase rollout is refine-only**. `run_pipeline_in_process` raises `NotImplementedError` for plan / implement / pr. -- **Conformance matrix is minimal**. `integration_tests/regression/test_substrate_smoke.py` covers `select_substrate` shape, spawner round-trip type, in-process bus round-trip, and a single INV-3 test via `PeerConsensusTracker`. No substrate-portable `conftest.py` fixture has landed beyond a `pytest.skip` heuristic for in-sandbox-agent contexts (the ADR cq-3 documents the target shape; the parametrize set is empty). -- **Curated 5-issue conformance set is undefined**. ADR `:49` and `:278-279` settle the *selection rule* — one bug, one feature, one refactor, one infra, one doc — but no specific 5 issues are named anywhere in the repo. -- **No k3s adapter parity beyond the spawner**. `K3sSpawnerAdapter` is the only k3s-side implementation of any protocol. `RedisMessageStore`, the gateway-side `check_agent_restrictions`, and `gateway/worktree_manager.py` are NOT yet promoted to satisfy `MessageBus` / `PolicyEnforcer` / `WorktreeManager`. -- **`EggHarnessSpawner` does not exist.** DoD #5 promotes feedback-Q4's "secondary goal" to a parent-close requirement; the spike did not build it. -- **Cost cap, fork-based delegation, R15 model (b), R2 multi-subagent hook validation, interface stabilization** — all deferred per the ADR's "Open work" section (`docs/architecture/claude-code-substrate.md:246-258`). - -### Phase roles the rollout must wire up - -From `shared/egg_contracts/agent_roles.py:1172-1209` (`_PHASE_ROLES` + `_PHASE_REVIEWERS`): - -| Phase | Producers | Reviewers | -|---|---|---| -| refine | `REFINER` (shipped) | `REVIEWER_REFINE`, `REVIEWER_AGENT_DESIGN` | -| plan | `ARCHITECT`, `TASK_PLANNER`, `RISK_ANALYST` | `REVIEWER_PLAN` | -| apply (epic-only) | `APPLIER` | `REVIEWER_CONTRACT` | -| implement | `CODER`, `TESTER`, `DOCUMENTER` | `REVIEWER_CODE`, `REVIEWER_CODE_HOLISTIC`, `REVIEWER_CONTRACT`, `REVIEWER_SECURITY`, `REVIEWER_CONCURRENCY` | -| pr | (no agent producers — automation in `orchestrator/routes/pipelines.py`) | (no agent reviewers) | - -Refine *reviewers* (`REVIEWER_REFINE`, `REVIEWER_AGENT_DESIGN`) are themselves not yet exercised on the claude-code substrate — the spike's reviewer leg was implicit because cq-11 narrowed end-to-end exercise to the single producer role. Plan adds 3 producers + 1 reviewer; implement adds 3 producers + 5 reviewers; pr is largely automation. The roll-out adds **15 new role rubrics** (architect, task_planner, risk_analyst, coder, tester, documenter, applier, reviewer_refine, reviewer_agent_design, reviewer_plan, reviewer_code, reviewer_code_holistic, reviewer_contract, reviewer_security, reviewer_concurrency) plus glue for the orchestrator-as-bus model under nested Agent-tool dispatch. - -### Trust-boundary surface area (scaling delta) - -`docs/architecture/integration-test-trust-boundary.md` distinguishes in-sandbox-agent / trusted-CI-runner / human-operator contexts. The spike accepted R1's trust-context shift for **one** refiner subagent against a trusted repo. The rollout dispatches **3 + 3 + 1 + 8 = 15 subagents per non-epic pipeline** (refine reviewers + plan producers/reviewer + implement producers/reviewers; epic pipelines add APPLIER), each inheriting the parent session's credential context. The R1 acceptance text in the ADR (`:184-188`) accepts the credential shift *qualitatively*; the per-subagent multiplier wasn't quantified. - -## Constraints - -### Inherited from #2623 refine phase (settled, not re-litigated) - -- **cq-1** through **cq-12** stand (issue body's "Settled context"). cq-2 = all four phases must work on claude-code. cq-3 = substrate-parameter on `integration_tests/regression/`. cq-4 = synchronous spawn shape. cq-5 = `WORKTREE_BASE_DIR` model ports. cq-6 = PreToolUse hooks as primary policy seam (fallback: MCP-validator-side). cq-7 = heredoc generator + `AskUserQuestion`. cq-8 = pip dependencies via plugin metadata. cq-9 = leave k3s indefinitely as co-equal. cq-10 = hybrid checkpoint + fork. cq-11 = spike-then-plan (this issue is the "then plan"). cq-12 = pip name deferred. -- **Feedback Q1–Q6** stand. Q1 = fixed curated 5 (rule settled, specifics open). Q2 = budget deferred until empirical numbers exist (this issue is "empirical numbers exist"). Q3 = no new pip deps; ~100 MB plugin cap. Q4 = `EggHarnessSpawner` is secondary but in DoD now (DoD #5). Q5 = #2622 structural causes #5/#6 stay scoped to #2622. Q6 = flag-don't-block on local-filesystem checkpoint privacy. - -### Architectural constraints - -- **BRC concurrency invariants** (INV-1..6, `orchestrator/action_guards.py:631 validate_invariants`) must survive substrate transitions. `InProcessMessageBus` already inherits from `MessageStore` so the bus mechanics match. The plan/implement/pr rollout must NOT introduce parallel BRC implementations. -- **`build_system_prompt(sources)` depth** must reach every new role rubric — otherwise the depth gap (#2622) reopens for plan/implement/pr roles. This is the structural fix from #2622 and is non-negotiable for the rollout. -- **PreToolUse hook role-resolution under nested dispatch is unproven (R2)**. The spike's single-role validation does NOT prove that hooks correctly identify the calling role for the plan-phase's 3 concurrent producers or the implement-phase's 8 concurrent producers + reviewers. Without that, structural file-write enforcement collapses to "every subagent is `EGG_AGENT_ROLE=`" and the rollout has to fall back to cq-6 option 2 (MCP-validator-side enforcement). -- **`shared/egg_restrictions/patterns.py` is the single source of truth** for role file-write boundaries. The hook *and* any MCP-validator fallback *and* the gateway must all import the same `build_agent_patterns(role)` symbol — no parallel restriction logic. -- **Trust-context shift (R1)** is accepted for trusted-repo SDLC. The rollout inherits the acceptance; the scaling delta (15 subagents per pipeline vs 1) is documented but does not re-open the decision. -- **Subagent context budget regression (R7)**. cq-10's hybrid (checkpoint + fork) is partially implemented: the spike ports checkpoints; forking is deferred. The plan-phase architect role is the next likely context-budget pressure point (deeper research than refiner), so the fork half may move from "quality booster" to "blocker" before implement phase lands. -- **Interfaces are v0.x unstable** per R10 (`# v0.x — unstable until ≥3 roles exercise` comment in module docstrings). The rollout exercises ≥3 roles; the stability marker should be dropped *during* the rollout, not after. - -### Operational constraints - -- **Cost cap (REC5)** is recommended in the ADR but not implemented. With 15 subagents per pipeline on the parent's direct-billing account, a NACK ping-pong infinite loop can rack up real money fast — implementing the cap (REC5) becomes more urgent at multi-role scale than the ADR's "mechanically simple, intentionally deferred" framing suggests. -- **Conformance matrix** must run under both `EGG_SUBSTRATE` values in CI. The claude-code dimension already `pytest.skip`s in in-sandbox-agent contexts (`integration_tests/regression/conftest.py`); the matrix factor-out needs to land on the broader regression suite, not just the smoke test. -- **`make test` (changeset-aware)** is the inner-loop default. The substrate fixture has to play nicely with the changeset-narrowing logic — any test that imports `orchestrator.substrate.*` will be in-scope for nearly every PR in this rollout. - -### Dependencies on other systems / features - -- **#2261 slice-15** is decomposing `orchestrator/routes/pipelines.py` (~16,400 lines) into a sub-package. The pre-allocated `_run_loop/` cluster has `_run_pipeline`, `PHASE_HANDLERS`, and per-phase handlers (`_run_refine.py`, `_run_plan.py`, `_run_implement.py`, `_run_pr.py`) listed as TBD. The rollout's plan / implement / pr work in `run_pipeline_in_process` will touch the same code paths; coordination with #2261's terminal slice is necessary or the rollout will conflict. -- **Marketplace publishing** (cq-12 / DoD-adjacent) is gated on packaging UX work. Not a blocker for the rollout but the install-instruction story remains "from-source" until cq-12 lands. -- **`#2714`** (sync regression on dirty worktree) is explicitly out of scope (issue body), but `LocalWorktreeManager` may surface a related class of bug. Track separately. - -## Options Considered - -The two largest open shape decisions are **how to close the bridge gap** and **how to slice the rollout work**. Each is registered as an HITL decision; the options below are the choices the operator picks between. - -### Option A: Linear chain — bridge → plan → implement → pr → conformance → EggHarnessSpawner → hardening - -**Approach**: 7 sequential PRs, each smaller in blast radius. Land the bridge first (closes the most foundational defect), then phase rollout one at a time, then conformance matrix and EggHarnessSpawner, then a hardening pass (R15 + R2 + cost cap + k3s adapter). - -**Pros**: -- Smallest per-PR review burden (each phase is a single-concern PR). -- Operator can pause after any slice if a phase's BRC mechanics surface unexpected issues. -- Plan-phase rollout is a self-contained "first multi-role test" — 3 producers + 1 reviewer is enough to validate BRC concurrency on the new substrate without the implement-phase's 8-subagent dispatch. - -**Cons**: -- Slowest end-to-end (each slice serializes on the prior). -- Bridge-then-plan-then-implement means real BRC stress doesn't surface until partway through. - -### Option B: Bridge-first then parallel-phase rollout - -**Approach**: Land the bridge as a hard dependency. Then run plan / implement / pr / conformance fixtures *in parallel* as a wave-2 fan-out. Then a wave-3 hardening pass with EggHarnessSpawner / cost cap / R15 / R2 / k3s adapter all in parallel. ~10 PRs across 3 waves. - -**Pros**: -- Fastest end-to-end after the bridge lands; the slice scheduler runs wave-2 siblings concurrently. -- Hardening pass parallelism maximizes throughput. - -**Cons**: -- Plan + implement landing in parallel means BRC mechanics are stressed simultaneously; if a regression surfaces, both PRs need rework. -- 10 PRs is a lot of review surface; the conformance matrix fixture lands without phase rollouts to back-test against. -- pr phase has no producer/reviewer roles, so its slice is more of a wiring / fixture concern; parallelizing it doesn't buy as much as it looks. - -### Option C: Bridge-first then phase-sequential rollout, hardening pass parallel at end - -**Approach**: Bridge → plan → implement → (pr + conformance matrix together) → (hardening: EggHarnessSpawner + cost cap + R15 + R2 + k3s adapter all parallel). ~5 PRs. - -**Pros**: -- Hits the sweet spot: smaller blast radius than Option B's parallel waves but faster than Option A's full serialization. -- pr phase + conformance matrix go together because pr's substrate work is minimal (mostly automation) and the conformance matrix is what proves pr works. -- Hardening parallelism at the end gives a clear "the rollout is feature-complete, polish remaining" beat. - -**Cons**: -- pr + conformance combined PR is larger. -- Hardening parallelism requires the slice-DAG to fan out 5 sibling slices, which is more than any other slice in the codebase has hit; may stress the slice scheduler. - -### Option D: Defer hardening to a tertiary issue - -**Approach**: This issue ships bridge + plan + implement + pr + conformance + EggHarnessSpawner. R15, cost cap, R2 validation, k3s adapter promotion, and fork-based delegation are spun out into a separate hardening follow-up. ~4 PRs in this issue. - -**Pros**: -- Minimal scope for this issue; clean parent-close criteria. -- Hardening items have natural follow-up framing; doesn't pollute this rollout's review. - -**Cons**: -- R2 validation deferred means the substrate ships with a known unvalidated assumption; R15 deferred means model (a) ad-hoc prompts ship for all roles without structural enforcement. -- Cost cap not landing means the rollout is "you better watch your Anthropic bill" — operator-visible regression vs. the gateway-side cap. - -### Bridge-gap sub-options (orthogonal to slice shape) - -**Option (a)** — Long-lived Python REPL/daemon the skill talks to via JSON-RPC envelopes; generator state survives between `AskUserQuestion` calls. Matches the engineered surface of `run_pipeline_in_process(...)` exactly (heartbeat threads stay alive across pauses). Requires daemon lifecycle management (start, stop, crash recovery, port allocation). - -**Option (b)** — Flatten the generator into a hand-shaped sequence of single-yield `python3 .py` invocations; decisions and answers thread through `.egg-state/contracts/.json`. No daemon needed. Loses the "heartbeat-during-HITL" property the generator carefully engineered; demands restructuring the in-process generator into checkpoint-resumable stages. Easier to ship, larger refactor of the engineered surface. - -**Option (c)** — Hybrid: ship (b) first as the MVP for refine and plan (single-pass-per-phase + per-phase HITL gate); design (a) for implement (multi-producer concurrency makes a daemon cheaper than N separate stage scripts because BRC state needs to survive across re-review cycles). - -## Recommended Approach - -**Bridge → phase-sequential → hardening-in-parallel (Option C)**, with the **bridge gap closed via Option (c) hybrid** (flatten for refine/plan, daemon for implement). - -Why: - -1. **Bridge is the long-pole**. Until a skill step can actually invoke `run_pipeline_in_process(...)` and ferry `HITLDecision` objects to `AskUserQuestion`, nothing else in this rollout is operator-visible end-to-end. It belongs at slice-0 regardless of which other shape wins. -2. **Plan-phase BRC is the right first stress test of the multi-role substrate**. 3 producers + 1 reviewer surfaces BRC concurrency issues with a small enough blast radius that a single-slice rework is feasible. Implement-phase's 8-subagent dispatch is the second stress test. -3. **pr + conformance share a natural slice boundary**. pr phase is mostly automation in `orchestrator/routes/pipelines.py:_run_pr.py` (TBD per #2261 slice-15); the conformance matrix is what proves pr actually works on the claude-code substrate. They go together. -4. **Hardening in parallel saves time without stressing review**. R15 migration, cost cap, R2 validation, k3s adapter, fork delegation — each is independently scoped and reviewable. Once phase rollout is feature-complete they fan out cleanly. -5. **Bridge Option (c) hybrid** matches the actual cost gradient: refine/plan are single-pass-per-phase (Option (b)'s stage script suffices); implement is multi-producer + multi-reviewer + multi-NACK-cycle (Option (a)'s daemon recoups its lifecycle cost). Picking one shape across the rollout overpays in either direction. - -The justification for *not* recommending Option D (defer hardening): the cost cap is a real safety regression at 15-subagents-per-pipeline scale and the R2 validation is mandatory for trusting hook-based enforcement. Spinning these out leaves the rollout "shipped but not safe" which is worse than the longer review surface of keeping them in. - -### Inputs the recommendation depends on - -This recommendation is **provisional** — it depends on the operator's resolution of cq-1 (bridge gap design), cq-2 (slice decomposition), and cq-3 (harness re-host vs Agent-tool dispatcher). If cq-3 selects "migrate to Agent-tool dispatcher in this rollout" (Option 2) the slice plan needs an additional slice for the dispatcher itself; if cq-3 selects "stay on harness re-host" the rollout's spawner layer is unchanged and the slice plan is as written. - -## Open Questions - -The operator has already settled cq-1 through cq-12 and feedback Q1–Q6 from the parent #2623 refine phase (see issue body's "Settled context" section). The questions below are **net-new** to this rollout and need explicit answers; they do not re-litigate the inherited decisions. - -### Resolved in pre-refine - -Inherited from #2623 (not re-registered): - -- Substrate strategy: parallel substrates, env-var-selected (cq-1) -- Phase scope for parent-close: all four phases (cq-2) -- Spawner shape: synchronous `spawn(role, prompt, env, worktree) → AgentResult` (cq-4) -- Worktree ownership: port `WORKTREE_BASE_DIR` model (cq-5) -- Policy seam: PreToolUse hooks primary, MCP-validator fallback (cq-6) -- HITL surface: heredoc-style synchronous generator (cq-7) -- Packaging: plugin metadata declares pip deps (cq-8); pip name deferred (cq-12) -- k3s disposition: leave indefinitely as co-equal (cq-9) -- Context-window strategy: hybrid checkpoint + fork (cq-10) -- Slicing meta-strategy: spike then plan; this issue is "then plan" (cq-11) -- Conformance set rule: fixed curated 5 covering SDLC hot paths (feedback Q1; specifics are net-new — see Q1 below) -- Trust-context shift acceptance (R1) for trusted-repo SDLC -- Marketplace footprint under ~100 MB; no new third-party deps (feedback Q3) -- Telemetry/privacy: flag-don't-block on local-filesystem checkpoints (feedback Q6) - -### Decision questions - -The full markdown for each decision is captured in the contract; the canonical text is what the operator sees when resolving the HITL gate. Brief reference: - -- **cq-1** — How should the HITL bridge gap (reviewer v1 blocker #6) be closed? (4 options: long-lived JSON-RPC daemon / flattened stage scripts / hybrid / Other) -- **cq-2** — How should the rollout work be decomposed into slices? (4 options: linear chain / parallel after bridge / phase-sequential then parallel hardening / defer hardening to tertiary issue / Other) -- **cq-3** — Migrate to `ClaudeCodeAgentToolSpawner` (real Agent-tool dispatch) or stay on the spike's harness re-host? (4 options: stay / migrate now / hybrid per-role / decide empirically post-implement / Other) -- **cq-4** — When (if at all) should the rollout migrate from R15 model (a) ad-hoc prompts to model (b) `.claude/agents/.md` definitions? (4 options: alongside each phase rollout / defer to later issue / dedicated hardening slice / contingent on R2 result / Other) -- **cq-5** — When should the R2 PreToolUse hook empirical validation happen? (4 options: early-spike first slice / inline during plan / inline during implement / hardening-pass release-blocker / Other) -- **cq-6** — Default value for `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` (REC5)? (4 options: 50 / 200 / 500 / opt-in only / Other) -- **cq-7** — Should the full k3s interface adapter ship in this rollout? (4 options: defer entirely / hardening-slice / MessageBus-only / defer + drop unstable marker on claude-code leg / Other) - -### Feedback questions - -Open-ended questions in `feedback-1`: - -- **Q1** — Name the 5 GitHub issues for the conformance set (bug / feature / refactor / infra / doc). Pre-suggested candidates: #2714 (bug), #2474 (feature/regression suite), #2261 (refactor), #2705 (infra), #2718 (doc) — confirm or replace. -- **Q2** — Latency / cost budget for the conformance matrix now that empirical numbers will exist post-rollout. -- **Q3** — `EggHarnessSpawner` scope: full local-run / spawner-only / HITL-bridged local-run / defer. -- **Q4** — Multi-role-scale operator acceptance for trust-context shift (R1): additional mitigations needed at 15-subagent scale? -- **Q5** — Reviewer roster expansion: any new reviewer roles (`reviewer_substrate`, `reviewer_concurrency_substrate`)? -- **Q6** — Walking-skeleton scope-fence behavior post-rollout: remove entirely / opt-in `--single-phase` / per-phase explicit clear. - ---- - -*Authored-by: egg* diff --git a/.egg-state/drafts/2717-plan.md b/.egg-state/drafts/2717-plan.md deleted file mode 100644 index 2a8b47a501..0000000000 --- a/.egg-state/drafts/2717-plan.md +++ /dev/null @@ -1,1557 +0,0 @@ -# Plan: Substrate-swap follow-up — wire remaining roles + plan/implement/pr phases on Claude Code substrate - -> Issue: #2717 | Phase: plan | Predecessor PR: #2715 (walking-skeleton spike) -> -> The refine phase settled seven HITL decisions (cq-1 … cq-7 in this -> issue's numbering — distinct from #2623's cq-1 … cq-12) and six -> open-ended feedback items. The plan below is the executable form of -> those settled choices; nothing here re-litigates them. - -## Approach - -The refine analysis (`.egg-state/drafts/2717-analysis.md`) decomposed -the rollout work into a **5-slice phase-sequential stack** per -**cq-2 = Option 3 ("Bridge-first then phase-sequential rollout")**. -The chain is: - -``` -slice-1 (bridge + R2 spike + refine reviewers) - └─ slice-2 (plan-phase substrate) - └─ slice-3 (implement-phase substrate + daemon HITL bridge) - └─ slice-4 (pr-phase substrate + conformance matrix + scope-fence removal) - └─ slice-5 (hardening: EggHarnessSpawner + cost cap + R15 + fork + ADR) -``` - -The chain is **strictly linear** — every slice has exactly one parent. -The forest constraint (#2137) is satisfied trivially; no slice has -fan-out that would require `serialized_chain_order`. - -### How each settled decision shapes the slice DAG - -- **cq-1 (HITL bridge gap) = hybrid (Option C)** — flatten the - generator for refine/plan via single-yield `python3 .py` - invocations; ship a daemon variant for implement-phase where - multi-producer concurrency makes the daemon's lifecycle cost pay - off. Slice 1 lands the flattened path; slice 3 lands the daemon - variant alongside the implement-phase wiring. -- **cq-2 (slice decomposition) = phase-sequential** — five stacked - PRs as above; pr + conformance go together; hardening is a - single tail slice with parallel-but-cohesive tasks. -- **cq-3 (Agent-tool dispatcher migration) = decide empirically - post-implement** — slices 1–4 keep - `orchestrator/substrate/claude_code/spawner.py:64` - (`ClaudeCodeSpawner`, the harness re-host). Slice 5's hardening - pass collects measured concurrency / cost / latency / context - data and decides whether to migrate; if the data says - "migrate now", the migration ships in slice 5, otherwise it - spins out to a future issue. -- **cq-4 (R15 model (b) timing) = contingent on R2** — slice 1 - produces the R2 verdict via a test-only Agent-tool dispatch fake - (TASK-1-9 + TASK-1-5). If R2 passes, every later slice stays on - R15 model (a) — ad-hoc role prompts under - `plugins/egg-sdlc/skills/egg-sdlc/agents/.md`. If R2 fails, - slice 5 migrates every role rubric to a real - `.claude/agents/.md` definition with frontmatter tool - restrictions, AND slice 2 adds agent-side - (`sandbox/egg_agent_tools/handlers/restrictions.py`) policy - enforcement (cq-6 option 2 from #2623) as a parallel policy - seam. Note: under the production harness re-host model (cq-3), - the PreToolUse hook is NOT in the agent's tool-call loop — - `shared/egg_harness/client.py` uses its own `ToolRegistry` - permission callback. R2 therefore validates hook *logic* (given - accurate `EGG_AGENT_ROLE` propagation) and is a future-facing - gate that becomes load-bearing when cq-3 flips to Agent-tool - dispatch in a future issue. -- **cq-5 (R2 validation timing) = early spike** — slice 1 ships - a 2-subagent worked example that proves PreToolUse hooks resolve - the calling role under nested dispatch. This is the gating test - for the rest of the rollout. -- **cq-6 (`EGG_PIPELINE_MAX_AGENT_INVOCATIONS` default) = 200** — - slice 5 implements the cap. The default is encoded once in - `orchestrator/substrate/in_process.py` (and the conformance - matrix tests confirm the default value). -- **cq-7 (k3s adapter scope) = defer entirely AND drop v0.x - unstable marker after ≥3 roles exercise** — no slice ships k3s - adapter code. Slice 5 drops the - `# v0.x — unstable until ≥3 roles exercise` markers from the four - substrate protocol modules (`orchestrator/substrate/spawner.py`, - `message_bus.py`, `policy.py`, `worktree.py`); by slice 4 the - substrate has exercised 1 producer + 2 reviewers (refine) + 3 - producers + 1 reviewer (plan) + 3 producers + 5 reviewers - (implement) = 15 roles, so the threshold is comfortably cleared. - -### Feedback Q answers shape - -- **Q1 (curated 5 issues)** = #2714, #2474, #2261, #2705, #2718 — - slice 4 parametrizes the conformance matrix across all five. -- **Q2 (latency/cost budget)** = pytest.mark.slow gating + 3-hour - per-phase ceiling — slice 4 wires both. -- **Q3 (EggHarnessSpawner scope)** = Option A full local-run — slice - 5 ships `egg-orch local-run --issue ` end-to-end. -- **Q4 (multi-subagent trust)** = no additional mitigations for v1 — - slice 1's ADR update notes the 15-subagent scale but does NOT add - an `EGG_REQUIRE_CREDENTIAL_PROXY` flag. -- **Q5 (reviewer roster)** = no new reviewer roles — the existing - reviewer_code / reviewer_concurrency / reviewer_security / - reviewer_agent_design roster covers the substrate work. -- **Q6 (scope-fence behavior)** = remove the fence entirely once - pr-phase lands — slice 4 deletes the `NotImplementedError` guard - in `orchestrator/substrate/in_process.py:212` (`_maybe_fence`). - -## Primitives - -Audit per #2594. Every primitive named in the task descriptions -below is listed here with its `file:line` citation OR marked -`(NEW — TASK-X-Y)` when the task itself creates it. Each row also -records the execution-context scope (R2 trust boundary, in-sandbox -fixture availability) the consuming task assumes. - -### Existing primitives the rollout depends on - -| Primitive | Location | Scope notes | -|-----------|----------|-------------| -| `select_substrate(env, ...)` | `orchestrator/substrate/__init__.py:96` | Substrate factory; both legs | -| `SubstrateBundle` dataclass | `orchestrator/substrate/__init__.py:78` | Carries spawner / bus / policy / worktrees | -| `_load_egg_sdlc_role_rubric(role)` | `orchestrator/substrate/__init__.py:232` | Currently raises `ValueError` for any role other than `refiner` (line 280-284); slice 1+ extends the rubric set | -| `ClaudeCodeSpawner` | `orchestrator/substrate/claude_code/spawner.py:64` | Harness re-host per cq-3 verdict | -| `InProcessMessageBus` | `orchestrator/substrate/claude_code/message_bus.py:30` | Subclasses `MessageStore`; BRC mechanics shared with k3s memory backend | -| `PreToolUseHookPolicy` | `orchestrator/substrate/claude_code/policy.py` (class definition; see module head) | Wraps `hook_entry.py`; reads `EGG_AGENT_ROLE` | -| `hook_entry.py` (PreToolUse hook script) | `orchestrator/substrate/claude_code/hook_entry.py` | Reads `EGG_AGENT_ROLE` (line 632, 697), imports `build_agent_patterns` (line 648), denies on path mismatch | -| `LocalWorktreeManager` | `orchestrator/substrate/claude_code/worktree.py:59` | Per-pipeline worktrees under `EGG_WORKTREE_BASE` (default `~/.egg-worktrees/`) | -| `run_pipeline_in_process(...)` generator | `orchestrator/substrate/in_process.py:84` | Three-stage today: preflight → spawn refiner → refine HITL gate; `NotImplementedError` for plan/implement/pr; `_maybe_fence` call site at `:212` and method definition at `:807-826` (BOTH need to be removed in TASK-4-2) | -| `_K3S_FENCE_MESSAGE` constant | `orchestrator/substrate/in_process.py:76` | Raised on `EGG_SUBSTRATE=k3s` (line 131) | -| `_InProcessOrchestrator` class | `orchestrator/substrate/in_process.py:152` | Background-thread lifecycle for in-process generator | -| `build_system_prompt(sources)` | `shared/egg_harness/prompt.py:24` | Single source of truth for role rubric depth (#2622 fix) | -| `build_agent_patterns(role, repo=None)` | `shared/egg_restrictions/patterns.py:768` | Single source of truth for file-write restrictions | -| `_PHASE_ROLES` map | `shared/egg_contracts/agent_roles.py:1172` | All four phases already populated; the rollout does NOT mutate this | -| `_PHASE_REVIEWERS` map | `shared/egg_contracts/agent_roles.py:1183` | Reviewer roster already populated; the rollout does NOT add new reviewer roles (feedback Q5) | -| `AgentRole` enum members | `shared/egg_contracts/agent_roles.py:46-…` | All 15 roles named here exist; the rollout does NOT add new enum values | -| `_spawn_agent_via_substrate(...)` | `orchestrator/concurrent_executor.py:569` | Substrate-dispatching seam already wired for `EGG_SUBSTRATE=claude-code` | -| `substrate` pytest fixture | `integration_tests/regression/conftest.py:623` | Parametrizes test over `["k3s", "claude-code"]`; `pytest.skip`s claude-code in in-sandbox-agent context (line 636-641) | -| `test_substrate_smoke.py` | `integration_tests/regression/test_substrate_smoke.py` | Existing single-role smoke; slice 4 extends to per-phase matrix | -| `cmd_serve` daemon entry | `orchestrator/cli.py:83` | k3s leg boot; the in-process leg is `run_pipeline_in_process` and remains parallel | -| `cmd_health` / `cmd_pipelines_*` / `cmd_gateway_status` / `cmd_anchor_*` | `orchestrator/cli.py:436, :467, :600, :632` | Reference shape for `cmd_local_run` (NEW in slice 5) | -| `refiner.md` role rubric template | `plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md` | Shape every new role rubric mirrors (frontmatter + markdown body) | -| `SKILL.md` | `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` | Top-level skill manifest; slice 4 updates the "What's NOT in this skill" section as phases land | -| `settings.template.json` | `orchestrator/substrate/claude_code/settings.template.json` | PreToolUse hook wiring template; slice 1's R2 spike validates this stays correct under nested dispatch | -| `preflight.py` | `plugins/egg-sdlc/skills/egg-sdlc/bin/preflight.py` | Import-check pattern the new `bin/run_pipeline.py` stage scripts mirror | -| `claude-code-substrate.md` ADR | `docs/architecture/claude-code-substrate.md` | "Open work" section (line 246-258); "Follow-up draft" rollout deltas (line 272-287) | - -### NEW primitives this plan creates - -| Primitive | Created in | Notes | -|-----------|------------|-------| -| `bin/run_pipeline.py` flattened stage driver | `(NEW — TASK-1-2)` | Skill-side Python entry: reads `.egg-state/contracts/.json`, advances `run_pipeline_in_process` to the next yield, serialises the resulting `HITLDecision` back to the contract, exits. The skill body calls it once per stage and renders each decision via `AskUserQuestion`. | -| `HITLDecision` JSON-serialised envelope | `(NEW — TASK-1-2)` | The single-yield carrier; lives at `.egg-state/contracts/.json#pending_hitl`. Mirrors the structure of existing `decisions[]` entries so `mcp__sdlc__check_hitl_answers` already understands it. | -| `_run_plan` stage in `run_pipeline_in_process` | `(NEW — TASK-2-1)` | Adds plan-phase dispatch to the in-process generator. Replaces the `NotImplementedError` branch in `_maybe_fence`. | -| `architect.md`, `task_planner.md`, `risk_analyst.md`, `reviewer_plan.md` rubrics | `(NEW — TASK-2-3)` | New role rubric files mirroring `refiner.md`'s shape. Documenter-owned (markdown). | -| `_run_implement` stage in `run_pipeline_in_process` | `(NEW — TASK-3-1)` | Adds implement-phase dispatch; multi-producer BRC. | -| Daemon-style HITL bridge (Option A) for implement-phase | `(NEW — TASK-3-2)` | Long-lived Python REPL the skill talks to via JSON-RPC envelopes over a UNIX domain socket under `.egg-state//sock`. Required because multi-producer BRC has too many yields for the flattened path to be practical. | -| `coder.md`, `tester.md`, `documenter.md` producer rubrics | `(NEW — TASK-3-4)` | Producer rubrics. | -| `reviewer_code.md`, `reviewer_code_holistic.md`, `reviewer_contract.md`, `reviewer_security.md`, `reviewer_concurrency.md` reviewer rubrics | `(NEW — TASK-3-5)` | Reviewer rubrics. | -| `reviewer_refine.md`, `reviewer_agent_design.md` refine reviewer rubrics | `(NEW — TASK-1-4)` | Slice 1 also closes the refine reviewer gap so the substrate has a complete refine team. | -| Test-only nested-Agent-tool dispatch fake (`_agent_tool_fake.py`) | `(NEW — TASK-1-9)` | Test infrastructure for the R2 spike. Simulates Claude Code's `Agent` tool by spawning subprocesses with controlled `EGG_AGENT_ROLE`; invokes `hook_entry.decide(...)` via each fake's `pre_tool_use_callback`. Does NOT become a production spawner; not registered in `select_substrate`. Carries the shared `pending_hitl` envelope schema for risk_analyst R17 mitigation. | -| `_run_pr` stage in `run_pipeline_in_process` | `(NEW — TASK-4-1)` | Adds PR-phase automation; no producer/reviewer roles, mostly orchestration. | -| Scope-fence removal | `(NEW — TASK-4-2)` | Deletes `_maybe_fence` (`orchestrator/substrate/in_process.py:212`) and its `NotImplementedError` branch; the generator now chains phases freely. | -| 5-issue conformance parametrize set | `(NEW — TASK-4-4)` | `@pytest.mark.parametrize("conformance_issue", [2714, 2474, 2261, 2705, 2718])` applied to substrate-portable regression tests. | -| `pytest.mark.slow` decorator for conformance matrix | `(NEW — TASK-4-5)` | Plus a 3-hour per-phase ceiling implemented as a `pytest_runtest_setup` hook. | -| `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost cap | `(NEW — TASK-5-1)` | Default 200 (cq-6). Tracks invocations on `_InProcessOrchestrator._invocation_count`; raises `RuntimeError("cost cap exceeded")` past the cap. | -| `EggHarnessSpawner` | `(NEW — TASK-5-2)` | Third spawner implementation under `orchestrator/substrate/egg_harness/spawner.py`. Drives the existing `egg_harness.run_agent` from a fresh `subprocess.Popen` per role. | -| `cmd_local_run` CLI subcommand | `(NEW — TASK-5-3)` | `egg-orch local-run --issue 1234 --repo owner/name`. Boots `run_pipeline_in_process` against the `EggHarnessSpawner`-backed substrate and prints HITL decisions to stdout in a JSON envelope the operator drives via stdin. | -| Fork primitive — `fork_subagent(parent_role, sub_task)` | `(NEW — TASK-5-5)` | The deferred half of cq-10 from #2623. Producer agents whose context fills can fork a child Claude Code subagent to do a sub-task ("read all files matching X and summarize") and receive the summary back. | -| v0.x marker drop in protocol modules | `(NEW — TASK-5-6)` | Removes `INTERFACE STABILITY: v0.x unstable.` from `orchestrator/substrate/spawner.py`, `message_bus.py`, `policy.py`, `worktree.py`. By slice 5 the substrate has exercised ≥15 roles (cq-7). | - -### Trust-boundary scope citations (#2594 §10) - -- **Pytest fixture `gateway_url`** — single hit at - `integration_tests/local_pipeline/conftest.py:261`; kubectl-gated. - The conformance matrix tests in slice 4 do NOT consume this - fixture — they exercise `run_pipeline_in_process` in-process and - reach gateway/orchestrator via the substrate bundle, not via a - test-side fixture. Conformance tests therefore live under - `integration_tests/regression/` (substrate-portable), not under - `integration_tests/local_pipeline/` (kubectl-gated). -- **`EGG_AGENT_ROLE` env var** — set by orchestrator on every spawned - subagent; read by `hook_entry.py:697` (PreToolUse hook) and by - `conftest.py:620` (`_in_sandbox_agent_context()`). The R2 spike - in TASK-1-5 validates the hook reads `EGG_AGENT_ROLE` correctly - under nested dispatch. -- **`InProcessMessageBus`** — pure in-process; no Redis dependency. - Sandbox-agent-safe. -- **`ClaudeCodeSpawner`** — in-parent-Claude-Code-session execution - per the spike's R1 acceptance. Conformance tests that exercise - it are skipped via `conftest.py:636-641` when - `_in_sandbox_agent_context()` is true. This is the - trust-boundary fence the substrate-portable conformance matrix - relies on. - -## Slice-by-slice approach - -### Slice 1 — Bridge gap (flattened stage scripts) + R2 hook validation + refine reviewers - -**Goal**: Close the bridge gap (reviewer v1 blocker #6 / reviewer v2 -blocker B1 from #2715) for refine-phase HITL; validate the R2 -empirical question (PreToolUse hooks resolve role under nested -dispatch); add the two refine-phase reviewer rubrics so the substrate -has a complete refine team. - -**Deliverables**: - -1. A skill-side `bin/run_pipeline.py` stage driver that loads the - pipeline state from `.egg-state/contracts/.json`, advances - `run_pipeline_in_process` to its next yield, writes the yielded - `HITLDecision` back to the contract, and exits cleanly. The - `SKILL.md` body calls this driver once per stage; between calls, - the skill renders the pending decision via `AskUserQuestion` and - writes the operator's answer back to the contract so the next - `python3 bin/run_pipeline.py` invocation can pick it up via - `generator.send(answer)`. -2. A 2-subagent worked-example test under - `integration_tests/regression/test_pretooluse_hook_nested.py` that - uses a **test-only Agent-tool dispatch fake** - (`integration_tests/regression/_agent_tool_fake.py`, TASK-1-9) to - simulate nested dispatch with controlled `EGG_AGENT_ROLE` env vars; - asserts `hook_entry.decide(...)` denies a write that violates the - *child's* role pattern even when the parent's role would allow it. - The fake is test infrastructure only — production dispatch stays - on the harness re-host model per cq-3 (in the harness path the - PreToolUse hook is NOT in the loop because - `shared/egg_harness/client.py` uses its own - `ToolRegistry.set_permission_callback(...)` — verified by - `grep -rn "PreToolUseHookPolicy|hook_entry" - shared/egg_harness/` returning zero hits). **Result drives cq-4 - contingency**: pass → stay on model (a); fail → trigger R15 - model (b) migration in slice 5 + agent-side - (`sandbox/egg_agent_tools/handlers/restrictions.py`) enforcement - in slice 2 (TASK-2-5). -3. The two refine-team reviewer rubrics under - `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md` and - `reviewer_agent_design.md`. Together with the existing - `refiner.md` this completes the refine team on the substrate. -4. `_load_egg_sdlc_role_rubric` (`orchestrator/substrate/__init__.py:232`) - loses its "spike only ships refiner rubric" `ValueError` for the - refine reviewers (lines 280-284); the same loader continues to - raise `ValueError` for plan/implement roles until slice 2/3 adds - their rubrics. -5. ADR refresh: document the flattened bridge mechanism, the R2 - verdict, and the refine-team expansion. - -**Why this slice exists first**: cq-5 explicitly chose "early-spike -before phase rollout begins" — if hooks fail under nested dispatch, -every subsequent slice has to add MCP-validator-side enforcement -*in parallel*. Catching that early avoids redoing slices 2–4. - -### Slice 2 — Plan-phase substrate (3 producers + 1 reviewer) - -**Goal**: First multi-role BRC stress test on the substrate. Wire -architect / task_planner / risk_analyst through the in-process -orchestrator's plan stage; wire reviewer_plan as a reviewer; the four -roles converge to a plan-phase HITL gate (the operator approves / -NACKs the plan). - -**Deliverables**: - -1. `_run_plan` stage added to `_InProcessOrchestrator.run()` in - `orchestrator/substrate/in_process.py`, dispatched after the - refine HITL gate when the operator chooses "continue to plan". -2. Multi-producer concurrency through the existing - `concurrent_executor.py` seam; 3 producer + 1 reviewer BRC cycle - runs under `InProcessMessageBus`. -3. Four new role rubrics (`architect.md`, `task_planner.md`, - `risk_analyst.md`, `reviewer_plan.md`) under - `plugins/egg-sdlc/skills/egg-sdlc/agents/`. -4. Plan HITL gate added to the flattened stage driver; the operator - sees plan-phase decisions through `AskUserQuestion` just like - refine decisions. -5. New BRC-end-to-end test under - `integration_tests/regression/test_inprocess_plan_brc.py` proving - `_run_plan` reaches CONFIRMED state through the - `InProcessMessageBus` (no real Anthropic API call — uses the - existing harness fakes). -6. If slice 1's R2 spike failed, this slice also wires MCP-validator-side - policy enforcement parallel to the hook (TASK-2-5 conditional). - -### Slice 3 — Implement-phase substrate (3 producers + 5 reviewers) + daemon HITL bridge - -**Goal**: Second BRC stress test at the largest scale (8 subagents). -Wire coder / tester / documenter through the in-process orchestrator's -implement stage; wire the five implement reviewers; switch the HITL -bridge to Option A (daemon) because the flattened path is impractical -at this many yields. - -**Deliverables**: - -1. `_run_implement` stage added to `_InProcessOrchestrator.run()`. -2. Daemon-style HITL bridge: a long-lived Python REPL under - `.egg-state//sock` (UNIX domain socket); skill body - talks to it via JSON-RPC envelopes. Generator state survives - between `AskUserQuestion` calls so the multi-producer BRC cycle - can pause/resume without rebuilding per-stage state. The flattened - driver from slice 1 stays available for refine/plan (single-pass - phases). -3. Eight new role rubrics: producers (`coder.md`, `tester.md`, - `documenter.md`) + reviewers (`reviewer_code.md`, - `reviewer_code_holistic.md`, `reviewer_contract.md`, - `reviewer_security.md`, `reviewer_concurrency.md`). -4. BRC end-to-end test for implement phase under - `integration_tests/regression/test_inprocess_implement_brc.py` - covering ACK / NACK / RE_REVIEW / CONFIRMED transitions across - 3 producers + 5 reviewers. -5. Empirical data collection for cq-3: count subagent invocations, - wall-clock latency, peak context per role. Stored under - `.egg-state//substrate-metrics.json` for slice 5 to - inform the Agent-tool dispatcher decision. - -### Slice 4 — PR-phase substrate + conformance matrix + scope-fence removal - -**Goal**: Wire the PR phase (orchestration-only, no producer/reviewer -roles); remove the walking-skeleton fence; ship the 5-issue -conformance matrix. - -**Deliverables**: - -1. `_run_pr` stage added to `_InProcessOrchestrator.run()`. PR phase - is mostly automation in `orchestrator/routes/pipelines.py` — - wire the in-process equivalent (slice-DAG stacked PR creation - via `gh` from the parent's credentials, context-PR creation, - manual-step rendering). -2. Removal of `_maybe_fence` (`orchestrator/substrate/in_process.py:212`) - and its `NotImplementedError("claude-code substrate runs refine - only")` branch; the operator can now chain phases freely - (feedback Q6). -3. Substrate-parameter applied to substrate-portable regression - tests; 5-issue conformance matrix parametrized: - `[#2714, #2474, #2261, #2705, #2718]` across both substrates. -4. `pytest.mark.slow` gating for conformance tests; 3-hour - per-phase ceiling enforced via `pytest_runtest_setup` hook - (calls `pytest.fail` if any phase exceeds 3 hours wall-clock). -5. `SKILL.md` updates: "What's NOT in this skill" section purged of - refine-only / plan-only items now that all four phases work; - "Walking-skeleton bridge gap" callout removed. - -### Slice 5 — Hardening (cost cap + EggHarnessSpawner + R15 contingent + fork primitive + ADR + v0.x marker drop) - -**Goal**: Production-ready substrate. Cost-safety + headless CLI + -deferred-half cq-10 + R15 decision based on R2 result + protocol -stability. - -**Deliverables** (parallel work within the slice; all coder-owned -unless noted): - -1. `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost cap, default 200 - (cq-6 = Option 2). Tracked on `_InProcessOrchestrator`. Raises - `RuntimeError` at the cap; the operator sees a clear diagnostic. -2. `EggHarnessSpawner` (Option A from feedback Q3 — full local-run - end-to-end). Lives under - `orchestrator/substrate/egg_harness/spawner.py`; spawns - `egg_harness.run_agent` via `subprocess.Popen` per role. -3. `egg-orch local-run --issue --repo ` CLI - subcommand (`cmd_local_run` in `orchestrator/cli.py`). Boots - `run_pipeline_in_process` against `EggHarnessSpawner`-backed - substrate; prints `HITLDecision` envelopes to stdout, reads - operator answers from stdin (JSON envelope shape mirroring the - contract's pending_hitl entries). -4. R15 model (b) migration **contingent on slice 1's R2 verdict**: - - R2 pass → no migration; remove the contingent task with note. - - R2 fail → convert every rubric file under - `plugins/egg-sdlc/skills/egg-sdlc/agents/.md` to a real - `.claude/agents/.md` definition with frontmatter tool - restrictions per role's allow-list in - `shared/egg_restrictions/patterns.py:768`. -5. Fork-based delegation primitive (cq-10 deferred half from #2623). - `fork_subagent(parent_role, sub_task) -> SubAgentResult` on - `_InProcessOrchestrator`; producer agents whose context fills can - fork a child to "read all files matching X and summarize" and - receive the summary back. -6. Drop the `INTERFACE STABILITY: v0.x unstable.` marker from the - four protocol modules (`orchestrator/substrate/spawner.py`, - `message_bus.py`, `policy.py`, `worktree.py`). Documenter-only - change in the protocol module docstrings (these are Python files - with module docstrings; the marker is comment-only — coder-owned). -7. ADR refresh (`docs/architecture/claude-code-substrate.md`): - reflect every rollout-time change (R2 verdict, cost cap default, - EggHarnessSpawner shape, fork primitive, marker drop, follow-up - draft "Open work" appendix archive). Documenter-owned. - -## Test strategy - -**Automated tests** (substrate-portable; live under -`integration_tests/regression/` to avoid kubectl gates): - -- Slice 1: `test_pretooluse_hook_nested.py` — 2-subagent R2 spike. - Uses the test-only `_agent_tool_fake.py` (TASK-1-9) to simulate - nested dispatch with controlled `EGG_AGENT_ROLE` env vars; - asserts `hook_entry.decide(...)` denies a write that violates the - *child* role's pattern even when the parent's role would allow it. - The fake's existence acknowledges that the production - `ClaudeCodeSpawner` (harness re-host per cq-3) does NOT route - through the hook; R2 validates hook *logic* and becomes the - empirical answer when cq-3 flips to Agent-tool dispatch. -- Slice 1: `test_bridge_flattened_round_trip.py` — multi-yield - generator survives across `python3 bin/run_pipeline.py` - invocations; contract-state sync round-trips the operator's - answer. -- Slice 2: `test_inprocess_plan_brc.py` — plan-phase BRC reaches - CONFIRMED through `InProcessMessageBus`; uses harness fakes for - the 3 producer + 1 reviewer subagents (no real Anthropic call). -- Slice 3: `test_inprocess_implement_brc.py` — implement-phase BRC - across 3 producers + 5 reviewers; covers ACK / NACK / RE_REVIEW / - CONFIRMED transitions, NACK aggregation, conditional ACK with - pre-merge obligation. -- Slice 3: `test_daemon_bridge.py` — daemon REPL round-trips - JSON-RPC envelopes; generator state survives between - `AskUserQuestion` calls. -- Slice 4: existing `test_substrate_smoke.py` extended to cover all - four phases under `EGG_SUBSTRATE=claude-code`. -- Slice 4: conformance matrix across 5 issues; each marked - `pytest.mark.slow`; 3-hour per-phase ceiling enforced via - `pytest_runtest_setup` hook. -- Slice 5: `test_cost_cap.py` — cap is enforced at default 200 and - honored via env override. -- Slice 5: `test_egg_harness_spawner.py` — third spawner satisfies - `AgentSpawner` Protocol; `cmd_local_run` boots end-to-end against - a curated issue. -- Slice 5: `test_fork_subagent.py` — fork primitive returns child - summary to parent without leaking child context. - -**Manual verification** (slice 4 acceptance): - -- Operator runs `python3 bin/run_pipeline.py issue-2717` against - #2717 itself; full refine → plan → implement → pr cycle - completes; each phase's HITL decisions render via - `AskUserQuestion`; the produced PR stack matches the slice DAG - output. -- Operator runs `egg-orch local-run --issue 2718` (the doc-only - curated issue) headlessly; pipeline completes; output PR stack - matches. - -## Manual steps - -**Pre-merge** (each slice's PR): - -- Reviewer confirms the slice's BRC consensus history references the - correct primitives from this plan (#2594 spot-check). -- Reviewer copies the PreToolUse hook config from - `orchestrator/substrate/claude_code/settings.template.json` into - their own `.claude/settings.json` before exercising the - conformance matrix locally (the template is NOT silently - activated by plugin install). - -**Post-merge** (slice 5): - -- Reviewer files a follow-up issue if slice 3's empirical metrics - warrant the Agent-tool dispatcher migration (cq-3). The metrics - live at `.egg-state//substrate-metrics.json` for - every conformance-matrix run; the slice-5 ADR refresh names the - threshold that triggers the follow-up. -- If R15 model (b) migration shipped in slice 5 (R2 fail path), the - reviewer files a follow-up to deprecate model (a) entirely once - every operator has migrated. - -```yaml -# yaml-tasks -pr: - title: "Roll out Claude Code substrate to remaining roles + plan/implement/pr" - description: | - Issue #2715 shipped the walking-skeleton spike for the Claude - Code substrate (one role × one phase end-to-end). This issue - rolls the substrate out from there per **cq-11 = "Spike then - plan"**. - - **What this PR stack does** (5 slices, stacked PRs): - - 1. **Bridge gap + R2 spike + refine reviewers (slice 1)** — - Closes the heredoc-HITL bridge gap via flattened - `python3 .py` invocations (cq-1 Option C, refine/plan - half). Ships a 2-subagent worked example that validates - PreToolUse hooks resolve role under nested dispatch (cq-5). - Adds the two refine-team reviewer rubrics (`reviewer_refine`, - `reviewer_agent_design`). - 2. **Plan-phase substrate (slice 2)** — First multi-role BRC - stress test. Wires architect / task_planner / risk_analyst - producers and `reviewer_plan` through the in-process - orchestrator. - 3. **Implement-phase substrate + daemon bridge (slice 3)** — - Second BRC stress test at largest scale (3 producers + 5 - reviewers). Switches the HITL bridge to Option A (long-lived - daemon over UNIX socket) because the flattened path is - impractical at this many yields. - 4. **PR-phase + conformance matrix (slice 4)** — Wires the PR - phase, removes the walking-skeleton fence (feedback Q6), - ships the 5-issue conformance matrix (#2714, #2474, #2261, - #2705, #2718) under both substrates with `pytest.mark.slow` - gating + 3-hour per-phase ceiling. - 5. **Hardening (slice 5)** — `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` - cost cap default 200 (cq-6); `EggHarnessSpawner` + `egg-orch - local-run` CLI (DoD #5 / feedback Q3); fork-based delegation - (cq-10 deferred half); contingent R15 model (b) migration - based on slice 1's R2 verdict (cq-4); drop the v0.x unstable - marker on the four substrate protocols (cq-7); ADR refresh. - - **Impact**: every agent role in egg's SDLC pipeline gains a - second substrate; the operator can drive a full pipeline - natively in Claude Code via `AskUserQuestion` without an MCP - `provide_input` round-trip; the conformance matrix is green on - both substrate dimensions; `egg-orch local-run` enables headless - runs; the four substrate protocols are stable. - test_plan: | - - Automated: every slice ships substrate-portable tests under - `integration_tests/regression/` (per slice: bridge round-trip, - R2 nested-hook denial, plan-phase BRC, implement-phase BRC, - daemon round-trip, conformance matrix x5 issues x2 substrates, - cost cap, EggHarnessSpawner, fork primitive). - - Manual (slice 4 / slice 5): operator runs - `python3 bin/run_pipeline.py issue-2717` end-to-end and - `egg-orch local-run --issue 2718` headlessly; both produce the - expected PR stack and metrics. - manual_steps: | - Pre-merge (each slice): reviewer copies - `orchestrator/substrate/claude_code/settings.template.json` into - their own `.claude/settings.json` before exercising the - conformance matrix; reviewer spot-checks BRC consensus history - against the plan's primitives table. - - Post-merge (slice 5): if slice 3's empirical metrics warrant the - Agent-tool dispatcher migration (cq-3), reviewer files a - follow-up issue; if R15 model (b) migration shipped (R2 fail - path), reviewer files a follow-up to deprecate model (a) after - operator migration. -slices: - - id: 1 - name: |- - Bridge gap (flattened) + R2 hook validation + refine reviewers - goal: |- - Close the heredoc-HITL bridge gap for refine-phase (Option B - flattened stage scripts per cq-1 Option C); ship a 2-subagent - worked example that validates PreToolUse hooks resolve role - under nested dispatch (cq-5 early spike); add the two - refine-team reviewer rubrics so the substrate has a complete - refine team. - tasks: - - id: TASK-1-1 - description: |- - Add `bin/run_pipeline.py` stage driver under - `plugins/egg-sdlc/skills/egg-sdlc/bin/`. The driver loads - pipeline state from `.egg-state/contracts/.json`, - calls `run_pipeline_in_process(...)` and advances the - generator to its next yield via `generator.send(answer)` - (where `answer` is the operator's last `AskUserQuestion` - response read from the contract's `pending_hitl.answer` - field), serialises the yielded `HITLDecision` back to - `pending_hitl.decision`, and exits. The skill body in - `SKILL.md` is updated to call this driver once per stage. - **The `pending_hitl` envelope shape defined here is the - shared state-serialization contract between Option B - (this flattened driver) and Option A (slice-3's daemon - variant in TASK-3-2)** — risk_analyst R17 mitigation. - Define the schema with a comment in the file marking - fields that must remain stable for the daemon variant - to consume the same envelopes (decision, answer, - version, pipeline_id, timestamp). - acceptance: |- - `bin/run_pipeline.py` exists; it imports - `run_pipeline_in_process` from - `orchestrator.substrate.in_process` (line 84); on - invocation it round-trips a single `HITLDecision` through - `.egg-state/contracts/.json` and exits with status 0 - when the generator yields, status 1 on internal error. - The `SKILL.md` "Usage" section is updated to call this - driver in a loop. The `pending_hitl` envelope schema is - documented as a stable contract (top-of-file comment - listing the contract fields). - role: coder - files: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - id: TASK-1-2 - description: |- - Update `SKILL.md` to call the new `bin/run_pipeline.py` - driver in a loop: invoke the driver, read - `pending_hitl.decision`, render via `AskUserQuestion`, - write the operator's answer to `pending_hitl.answer`, - loop. Replace the "Walking-skeleton bridge gap" callout - (line 97-102) with a brief "How the flattened bridge - works" subsection that names `pending_hitl` as the - single-yield carrier. - acceptance: |- - `SKILL.md` no longer carries the "Walking-skeleton bridge - gap" callout for refine-phase; the new usage section - documents the flattened stage-script loop; the - `allowed-tools` frontmatter still includes - `AskUserQuestion`, `Read`, `Bash(python3 *:*)` so the - loop can actually run. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - id: TASK-1-3 - description: |- - Add `test_bridge_flattened_round_trip.py` under - `integration_tests/regression/`. Test invokes - `bin/run_pipeline.py` twice against a deterministic - pipeline id: first invocation produces a preflight - `HITLDecision`; test writes the answer to - `pending_hitl.answer`; second invocation produces a - refine-gate `HITLDecision`. Validates that generator - state survives via the contract-state round-trip even - though the Python process exits between calls. - acceptance: |- - The test runs in <30s, does not require a real Anthropic - API call (uses harness fakes for the subagent spawn), and - asserts: (a) `pending_hitl.decision.question` matches the - preflight question on first run; (b) after writing - `pending_hitl.answer`, the second run consumes it and - yields the refine-gate decision. - role: tester - files: - - integration_tests/regression/test_bridge_flattened_round_trip.py - - id: TASK-1-4 - description: |- - Add `reviewer_refine.md` and `reviewer_agent_design.md` - role rubric files under - `plugins/egg-sdlc/skills/egg-sdlc/agents/`. Mirror the - shape of the existing `refiner.md` (frontmatter + markdown - body). Pull the rubric content from the corresponding k3s - prompt sources under `shared/prompts/` so the substrate - version stays in sync with what the k3s reviewer sees. - acceptance: |- - Both files exist with valid frontmatter (`name`, - `description`); both bodies start with a "You are the - **reviewer_refine** running on the Claude Code substrate" - (or analogous) preamble; both reference the same review - criteria the k3s reviewer uses. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md - - id: TASK-1-5 - description: |- - Add `test_pretooluse_hook_nested.py` under - `integration_tests/regression/`. **The R2 question - ("does the hook resolve role under nested dispatch?") - can only be answered when subagents run via Claude - Code's Agent-tool dispatch — the harness re-host model - (`ClaudeCodeSpawner` per cq-3) bypasses the PreToolUse - hook entirely because `shared/egg_harness/client.py:60-150` - uses its own `ToolRegistry.set_permission_callback(...)` - and `grep -rn "PreToolUseHookPolicy|hook_entry" - shared/egg_harness/` returns zero hits.** This task - therefore uses the test-only Agent-tool dispatch fake - from TASK-1-9: the test spawns a parent fake-subagent - with `EGG_AGENT_ROLE=architect` and a nested child fake- - subagent with `EGG_AGENT_ROLE=tester`, and asserts the - PreToolUse hook (invoked via the fake's - `pre_tool_use_callback`) denies a write to - `orchestrator/foo.py` from the child even though the - parent's role would allow it. Records the verdict to - `.egg-state//r2-verdict.json` so slice 5's - contingent R15 migration task can read it. **This is the - cq-5 early-spike gating test for the rollout.** Production - dispatch remains on `ClaudeCodeSpawner` (harness re-host) - per cq-3 — the Agent-tool dispatch fake in TASK-1-9 is - test infrastructure only, not a production spawner. - acceptance: |- - Test runs in <60s; uses the TASK-1-9 fake for nested - dispatch (no real Anthropic API call); asserts - `hook_entry.decide(...)` returns - `{"action": "deny", "message": "..."}` for the child's - write attempt when the parent's role would allow it; - writes `{"r2_verdict": "pass"}` or - `{"r2_verdict": "fail", "reason": "..."}` to - `.egg-state//r2-verdict.json`. The test - docstring documents the empirical-vs-test-fake limitation - (the test confirms hook *logic* is correct given accurate - `EGG_AGENT_ROLE` propagation; whether Claude Code itself - propagates `EGG_AGENT_ROLE` correctly under real nested - Agent-tool dispatch is verifiable only by running real - Claude Code, which the in-sandbox test cannot do — but - when cq-3 flips to Agent-tool dispatch in a future issue, - the production path becomes the empirical answer to that - remaining half of R2). - role: tester - files: - - integration_tests/regression/test_pretooluse_hook_nested.py - - id: TASK-1-9 - description: |- - Add a minimal test-only nested-Agent-tool dispatch fake - under `integration_tests/regression/_agent_tool_fake.py`. - Simulates Claude Code's `Agent` tool by spawning a - subprocess with controlled `EGG_AGENT_ROLE` env var per - dispatch; each fake-subagent has a - `pre_tool_use_callback` that invokes - `orchestrator.substrate.claude_code.hook_entry.decide(...)` - with the tool input. **This is test infrastructure - ONLY** — it does NOT become a production spawner and is - NOT registered in `select_substrate`. It exists so - TASK-1-5 can drive a deterministic nested-dispatch - scenario without a real Claude Code session. The shared - `pending_hitl` envelope schema invented in TASK-1-1 also - flows through this fake so slice 3's daemon variant - (TASK-3-2) inherits the same state-serialization contract - (closes risk_analyst R17 dual-bridge concern). - - **Role assignment**: the underscored helper-style name - (mirroring the existing - `integration_tests/regression/_helpers.py`) places this - file outside the `test_*.py` / `conftest.py` glob, so it - is **coder-owned**, not tester-owned (verified via - `mcp__sdlc__check_file_restriction`). This is the same - owner the existing `_helpers.py` has today; the test - file that consumes it (TASK-1-5's - `test_pretooluse_hook_nested.py`) remains tester-owned. - acceptance: |- - `_agent_tool_fake.py` exists; exposes a - `dispatch(parent_role, child_role, write_target)` - helper; the helper invokes `hook_entry.decide(...)` via - the simulated child's `pre_tool_use_callback` and - returns the hook verdict. Documented in the module - docstring as test-only; protected with a top-of-file - `if not __name__.startswith("integration_tests")` import - guard so it can't be silently imported by production - code. - role: coder - files: - - integration_tests/regression/_agent_tool_fake.py - - id: TASK-1-6 - description: |- - Update `_load_egg_sdlc_role_rubric` in - `orchestrator/substrate/__init__.py:232` to remove the - "spike only ships refiner rubric" `ValueError` (lines - 280-284) when the role is `reviewer_refine` or - `reviewer_agent_design`. The loader continues to raise - `ValueError` for plan/implement roles until slice 2/3 - adds their rubrics (this preserves the structured-error - contract for missing rubrics — the test in TASK-1-7 - covers this). **Depends on TASK-1-4 having landed the - two new rubric .md files first** — if a coder picks up - TASK-1-6 before TASK-1-4 is merged, the loader returns - text from a missing file. The implementer should - sequence TASK-1-4 → TASK-1-6 within the slice. - acceptance: |- - `_load_egg_sdlc_role_rubric(REVIEWER_REFINE)` returns the - markdown body of `reviewer_refine.md`; - `_load_egg_sdlc_role_rubric(REVIEWER_AGENT_DESIGN)` - returns the body of `reviewer_agent_design.md`; - `_load_egg_sdlc_role_rubric(ARCHITECT)` still raises - `ValueError` with the "follow-up issue per cq-11" hint - updated to "follow-up slice 2". - role: coder - files: - - orchestrator/substrate/__init__.py - - id: TASK-1-7 - description: |- - Add unit tests for the updated rubric loader: assert that - the two refine reviewer roles load successfully, and that - plan/implement roles still raise `ValueError` with the - updated diagnostic message. Test file lives under - `shared/tests/` next to the existing - `test_substrate_interfaces.py` and `test_claude_code_spawner.py` - per the repo's substrate-tests convention. - acceptance: |- - New test file `shared/tests/test_rubric_loader.py` - contains four test cases: load refiner (existing, - regression), load reviewer_refine, load - reviewer_agent_design, load architect raises ValueError. - role: tester - files: - - shared/tests/test_rubric_loader.py - - id: TASK-1-8 - description: |- - Update the ADR at - `docs/architecture/claude-code-substrate.md` to reflect: - (a) the flattened bridge mechanism (replacing the - "Walking-skeleton bridge gap" callout); (b) the R2 - verdict (point to `r2-verdict.json` and TASK-1-5); - (c) the refine-team expansion (refiner + 2 reviewers now - on the substrate); (d) the "Rollout deltas" section - (line 272-287) marked off the first bullet (bridge gap - closed for refine/plan). - acceptance: |- - ADR no longer references the bridge gap as deferred for - refine-phase; "Rollout deltas" first bullet shows - completed-with-strikethrough or moved to a "Completed in - this rollout" subsection; the "Open empirical question - (R2)" section either documents the pass result or - documents the fail result + slice 5 contingency. - role: documenter - files: - - docs/architecture/claude-code-substrate.md - - id: 2 - name: |- - Plan-phase substrate (3 producers + 1 reviewer) - goal: |- - First multi-role BRC stress test. Wire architect / - task_planner / risk_analyst producers and reviewer_plan - through the in-process orchestrator's plan stage. Single-pass - HITL via the flattened bridge from slice 1. - dependencies: - - slice-1 - tasks: - - id: TASK-2-1 - description: |- - Add `_run_plan` stage to - `_InProcessOrchestrator.run()` in - `orchestrator/substrate/in_process.py`. The stage is - dispatched after the refine HITL gate when the operator - chooses "continue to plan". It spawns the three - producers via the existing `concurrent_executor.py` - seam (line 569) so multi-producer BRC runs through the - `InProcessMessageBus`. After producers reach - `CONSENSUS_PROPOSE`, `reviewer_plan` is spawned for the - ACK/NACK cycle. The stage yields a plan-HITL decision - when consensus is reached. - acceptance: |- - `run_pipeline_in_process(...)` no longer raises - `NotImplementedError` when the operator advances past - refine; the plan stage spawns 3 producers concurrently - via the executor; reviewer_plan is spawned after each - `CONSENSUS_PROPOSE`; the stage yields a plan-HITL - decision after CONSENSUS_CONFIRMED on every producer - edge; the existing refine path still works. - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-2-2 - description: |- - Extend `_load_egg_sdlc_role_rubric` - (`orchestrator/substrate/__init__.py:232`) to load the - new plan-team rubrics (architect, task_planner, - risk_analyst, reviewer_plan); the loader continues to - raise `ValueError` for implement-team roles. - acceptance: |- - Loader returns rubric bodies for all four plan-team - roles; implement-team roles still raise `ValueError` - with the "follow-up slice 3" hint. - role: coder - files: - - orchestrator/substrate/__init__.py - - id: TASK-2-3 - description: |- - Add four plan-team rubric files under - `plugins/egg-sdlc/skills/egg-sdlc/agents/`: - `architect.md`, `task_planner.md`, `risk_analyst.md`, - `reviewer_plan.md`. Mirror `refiner.md`'s shape; body - content mirrors what the k3s prompts in - `shared/prompts/` produce for each role. - acceptance: |- - All four files exist with valid frontmatter; bodies - reference the substrate context ("You are the - **architect** running on the Claude Code substrate of - egg's SDLC pipeline.") and the role's specific outputs - (architect → architecture analysis; task_planner → - slice DAG plan; risk_analyst → risk assessment; - reviewer_plan → ACK/NACK on the three producer - artifacts). - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - - id: TASK-2-4 - description: |- - Add `test_inprocess_plan_brc.py` under - `integration_tests/regression/`. Test boots - `run_pipeline_in_process` against a deterministic - pipeline id with harness-faked subagents; advances past - the refine HITL gate; asserts the plan stage spawns - 3 producers + 1 reviewer; asserts CONSENSUS_CONFIRMED - fires on every producer edge; asserts the stage yields - a plan-HITL decision with the expected fields. - acceptance: |- - Test runs in <120s; uses harness fakes; reaches - CONSENSUS_CONFIRMED for all three plan-phase BRC edges - (architect → reviewer_plan, task_planner → - reviewer_plan, risk_analyst → reviewer_plan); asserts - plan-HITL decision is yielded. - role: tester - files: - - integration_tests/regression/test_inprocess_plan_brc.py - - id: TASK-2-5 - description: |- - Contingent on slice 1's R2 verdict (see - `.egg-state//r2-verdict.json` from - TASK-1-5). If R2 = fail, wire **agent-side** - (sandbox-tool-handler) policy enforcement parallel to - the PreToolUse hook: extend - `sandbox/egg_agent_tools/handlers/restrictions.py` - (which already exposes - `check_file_restriction(req)` at line 70) so that - every write-bearing tool call passes through it before - the agent burns a tool turn, returning the gateway's - `403 restricted_path_modified` shape on violation. This - is cq-6 option 2 from #2623 ("MCP-validator-side - enforcement"); the correct surface per the plan - reviewer's audit is the in-sandbox tool handler, NOT - `orchestrator/mcp_tools.py` (which is the - orchestrator-side MCP server, a different surface). If - R2 = pass, this task is a no-op (close with note). Tests - for this code path land in TASK-2-6 (tester-owned). - acceptance: |- - If R2 fail: - `sandbox/egg_agent_tools/handlers/restrictions.py` adds - a `validate_write_target(req)` helper (or extends an - existing one) that imports - `build_agent_patterns(role)` from - `shared/egg_restrictions/patterns.py:768` and returns - the gateway's `403 restricted_path_modified` shape - mirroring `gateway/phase_filter.py:1061`. If R2 pass: - task closed with note "no-op: hooks resolve role - correctly; structural enforcement remains hook-side". - role: coder - files: - - sandbox/egg_agent_tools/handlers/restrictions.py - - id: TASK-2-6 - description: |- - Add tests for the agent-side policy enforcement landed - in TASK-2-5. Only meaningful if slice 1's R2 verdict - was fail; otherwise the test is a no-op regression guard - asserting the validator helper does NOT change the - response shape for in-allow-list writes. File lives - under `tests/sandbox/egg_agent_tools/`. - acceptance: |- - New test file - `tests/sandbox/egg_agent_tools/test_restrictions_validator.py` - covers: (R2 fail) validator denies a cross-role write - tool call and the response matches the gateway's - `403 restricted_path_modified` shape; (R2 pass) asserts - the validator helper is a no-op for in-allow-list - writes (the contingency is documented in the test - docstring). - role: tester - files: - - tests/sandbox/egg_agent_tools/test_restrictions_validator.py - - id: TASK-2-7 - description: |- - Update `SKILL.md` to drop the "Plan phase deferred" - callout in "What's NOT in this skill" (line 163); add a - new "Plan phase" subsection naming the four roles and - the plan HITL gate. - acceptance: |- - `SKILL.md` documents the plan phase as supported; the - "What's NOT in this skill" section no longer lists - plan-phase deferral; the plan-HITL gate is named. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - id: 3 - name: |- - Implement-phase substrate (3 producers + 5 reviewers) + daemon HITL bridge - goal: |- - Largest BRC stress test (8 subagents). Switch HITL bridge to - Option A (long-lived Python daemon over UNIX socket) because - flattened single-yield scripts are impractical at this many - yields. Collect empirical metrics for cq-3 (Agent-tool - dispatcher) and cq-4 (R15 model-(b)) decisions. - dependencies: - - slice-2 - tasks: - - id: TASK-3-1 - description: |- - Add `_run_implement` stage to - `_InProcessOrchestrator.run()`. Dispatched after the - plan HITL gate when the operator chooses "continue to - implement". Spawns 3 producers + 5 reviewers; manages - multi-cycle BRC with NACK aggregation, conditional ACK - (pre-merge obligations), RE_REVIEW, CONFIRMED. - acceptance: |- - The implement stage spawns 3 producers + 5 reviewers - (per `_PHASE_ROLES["implement"]` at - `shared/egg_contracts/agent_roles.py:1173` and - `_PHASE_REVIEWERS["implement"]` at - `:1184`); BRC mechanics survive the substrate - transition (INV-1..6 covered); the stage yields - implement-HITL decisions for each producer's PR. - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-3-2 - description: |- - Add daemon-style HITL bridge under - `orchestrator/substrate/claude_code/hitl_daemon.py`: - a long-lived Python process that owns the - `run_pipeline_in_process` generator across multiple - `AskUserQuestion` round-trips. The skill talks to it - via JSON-RPC envelopes over a UNIX domain socket at - `.egg-state//sock`. Per cq-1 Option C, - this is the implement-phase variant — refine/plan keep - the flattened path from slice 1. **Daemon must detach - from the calling Bash session** via - `subprocess.Popen(start_new_session=True)` (or - equivalent `os.setsid()` after fork); otherwise the - daemon dies when the skill's `Bash(python3 ...)` step - exits, defeating the "long-lived" property. The daemon - consumes the `pending_hitl` envelope schema defined in - TASK-1-1 so the two bridges share a state-serialization - contract (risk_analyst R17 mitigation). - acceptance: |- - Daemon starts on first `python3 bin/run_pipeline.py - --daemon` invocation; the launcher uses - `start_new_session=True` (or `os.setsid()`) so the - daemon survives the calling Bash's exit; binds to - `.egg-state//sock`; serves JSON-RPC - envelopes: `{"method": "next_decision"}` returns the - next `HITLDecision`; `{"method": "send_answer", "answer": - ...}` resumes the generator; `{"method": "shutdown"}` - joins background threads cleanly via `GeneratorExit`. - Socket lifecycle is per-pipeline; the daemon - self-terminates on pipeline completion. The - long-lived-across-Bash-exits property is asserted in - TASK-3-8's test. - role: coder - files: - - orchestrator/substrate/claude_code/hitl_daemon.py - - id: TASK-3-3 - description: |- - Update `bin/run_pipeline.py` (from TASK-1-1) to add a - `--daemon` flag that connects to / launches the - `hitl_daemon`. The flattened single-yield path stays - the default for refine/plan; `--daemon` activates for - implement-phase. - acceptance: |- - `bin/run_pipeline.py --daemon` launches the daemon if - not running, connects to its socket, ferries one - decision-answer round-trip per invocation; exit code - 0 on success; the SKILL.md body picks the right mode - based on the current phase. - role: coder - files: - - plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py - - id: TASK-3-4 - description: |- - Add three producer rubrics: `coder.md`, `tester.md`, - `documenter.md` under - `plugins/egg-sdlc/skills/egg-sdlc/agents/`. Mirror - `refiner.md`'s shape; body content mirrors what the - k3s prompts produce for each role. - acceptance: |- - All three files exist with valid frontmatter; bodies - name the substrate; bodies reference the file-write - restrictions per `shared/egg_restrictions/patterns.py:768` - so the operator can sanity-check the hook's behavior. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/agents/coder.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/tester.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/documenter.md - - id: TASK-3-5 - description: |- - Add five reviewer rubrics: - `reviewer_code.md`, `reviewer_code_holistic.md`, - `reviewer_contract.md`, `reviewer_security.md`, - `reviewer_concurrency.md` under - `plugins/egg-sdlc/skills/egg-sdlc/agents/`. - acceptance: |- - All five files exist with valid frontmatter and bodies - referencing the substrate; review criteria mirror the - k3s reviewer prompts. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code_holistic.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_contract.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_security.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_concurrency.md - - id: TASK-3-6 - description: |- - Extend `_load_egg_sdlc_role_rubric` to load all eight - new implement-team rubrics; the loader no longer raises - `ValueError` for any production role (overseer, - inspector, autofixer, conflict_resolver remain - intentionally unhandled). - acceptance: |- - Loader returns rubric bodies for coder, tester, - documenter, and all five reviewer roles; the existing - refine + plan rubrics still load. - role: coder - files: - - orchestrator/substrate/__init__.py - - id: TASK-3-7 - description: |- - Add `test_inprocess_implement_brc.py` under - `integration_tests/regression/`. Drives a full - implement-phase BRC cycle through harness fakes; - asserts ACK / NACK / RE_REVIEW / CONFIRMED transitions - across 3 producers + 5 reviewers; covers NACK - aggregation (two reviewers NACK same producer); - covers conditional ACK with pre-merge obligation. - acceptance: |- - Test runs in <240s; uses harness fakes; asserts: - (a) all 3 producer × 5 reviewer edges reach - CONSENSUS_CONFIRMED; (b) NACK aggregation triggers - orchestrator HTTP 409 on stale-version re-propose; (c) - conditional ACK surfaces in the produced PR body's - pre-merge obligations section. - role: tester - files: - - integration_tests/regression/test_inprocess_implement_brc.py - - id: TASK-3-8 - description: |- - Add `test_daemon_bridge.py` under - `integration_tests/regression/`. Validates the daemon - round-trips JSON-RPC envelopes; validates that - generator state survives between AskUserQuestion calls - (simulated by separate JSON-RPC clients); validates - clean shutdown via `GeneratorExit`. - acceptance: |- - Test runs in <60s; spawns the daemon via `subprocess`; - asserts ≥3 decision-answer round-trips through the - socket; asserts daemon process exits cleanly on - `{"method": "shutdown"}`; asserts no leaked - background threads (the test process's thread count - returns to baseline). - role: tester - files: - - integration_tests/regression/test_daemon_bridge.py - - id: TASK-3-9 - description: |- - Add empirical metrics collection: instrument - `_InProcessOrchestrator` to record per-role subagent - invocations, wall-clock latency, peak context size to - `.egg-state//substrate-metrics.json`. - Slice 5's cq-3 Agent-tool dispatcher decision reads - this; slice 5's cost cap (cq-6) also reads - invocations_count from this file. - acceptance: |- - After a full implement-phase BRC cycle, the metrics - file contains: one entry per spawned subagent with - {role, wall_clock_seconds, context_tokens_peak, - retry_count}; total invocations field at the top - level; format documented in `_InProcessOrchestrator` - class docstring. - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-3-10 - description: |- - Update `SKILL.md` to document implement-phase support; - add a "When to use daemon mode" subsection (refine / - plan stay flattened; implement uses daemon). - acceptance: |- - `SKILL.md` covers all three phases supported so far - (refine, plan, implement); the "What's NOT in this - skill" section's implement-phase line is removed; the - daemon mode is documented with a snippet showing the - UNIX socket round-trip. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - id: 4 - name: |- - PR-phase substrate + 5-issue conformance matrix + scope-fence removal - goal: |- - Wire the pr-phase (automation-only, no producer/reviewer - roles); remove the walking-skeleton fence (feedback Q6); - land the 5-issue conformance matrix per feedback Q1 - (#2714, #2474, #2261, #2705, #2718) under both substrates - with pytest.mark.slow gating + 3-hour per-phase ceiling - (feedback Q2). - dependencies: - - slice-3 - tasks: - - id: TASK-4-1 - description: |- - Add `_run_pr` stage to - `_InProcessOrchestrator.run()`. The pr-phase has no - producer/reviewer roles; it's pure orchestration: - create the stacked PRs per slice, render the context - PR with refine/plan artifacts and BRC consensus - history per #2548, render pre-merge obligations from - the contract's `pr.deferred_actions`. - acceptance: |- - `_run_pr` runs after every slice's implement-phase - CONSENSUS_CONFIRMED; creates one PR per slice; creates - the context PR at the root of the stack; injects - pre-merge obligation rendering; the stage yields a - pr-HITL gate ("approve and merge / request changes / - abort"). - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-4-2 - description: |- - Remove the walking-skeleton scope fence from - `orchestrator/substrate/in_process.py`. Two locations - must be touched: (a) the call site at `:212` - (`self._maybe_fence(answer)` inside `_run`) and (b) the - method definition at `:807-826` (`def _maybe_fence(...)` - and its `NotImplementedError` body). Removing only the - call site leaves dead code; both must be deleted. - Also update the docstring at lines 92-125 to drop the - "raises NotImplementedError for plan / implement / pr" - language. With pr-phase wired, the operator can chain - all four phases freely. - acceptance: |- - `_maybe_fence` call site at `:212` AND method - definition at `:807-826` are both deleted; the - generator no longer raises `NotImplementedError` for - any phase advancement; the docstring reflects the new - four-phase support; existing tests for the fence are - deleted (they're obsolete) and replaced with the - integration test in TASK-4-4. - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-4-3 - description: |- - Apply the substrate parametrize fixture from - `integration_tests/regression/conftest.py:623` to every - regression test that is substrate-portable. Each test - must declare via the `substrate` fixture; the fixture - continues to skip the claude-code dimension under - `_in_sandbox_agent_context()` (line 636-641). - Substrate-portable: any test that does not transitively - depend on `egg_stack` or `gateway_url` fixtures. - acceptance: |- - Every test in `integration_tests/regression/` either - (a) consumes the `substrate` fixture, (b) is marked - `@pytest.mark.skip(reason="k3s-only" | "claude-code-only")`, - or (c) is exempt with an inline comment explaining why - it's not portable. The CI matrix runs both dimensions. - role: tester - files: - - integration_tests/regression/test_brc_concurrency.py - - integration_tests/regression/test_brc_edge_cases.py - - integration_tests/regression/test_brc_gap_audit.py - - integration_tests/regression/test_brc_phase_timeout.py - - integration_tests/regression/test_brc_single_cycle.py - - integration_tests/regression/test_hitl_round_trip.py - - integration_tests/regression/test_long_name_round_trip.py - - integration_tests/regression/test_message_bus_routing.py - - integration_tests/regression/test_recovery_invariants.py - - integration_tests/regression/test_salvage_ttl_sweep.py - - integration_tests/regression/test_slice_restart_branch_invariants.py - - integration_tests/regression/test_slice_spawn_env_threading.py - - integration_tests/regression/test_start_pipeline_recovery.py - - integration_tests/regression/test_unpushed_commit_salvage.py - - id: TASK-4-4 - description: |- - Add `test_conformance_matrix.py` under - `integration_tests/regression/`. Parametrize over the - 5 curated issues: [2714, 2474, 2261, 2705, 2718] X 2 - substrates = 10 parametrize axes. **Conformance fixture - mechanism**: each axis uses per-subagent `MagicMock` - stubs in the same style as - `integration_tests/regression/test_substrate_smoke.py:56` - (hardcoded `stdout="ok"`, no recorded transcripts) — - this is option (b) per the plan reviewer's audit, the - cheapest path that keeps the matrix in-sandbox-runnable - and avoids the chicken-and-egg of "which task records - the transcripts the matrix consumes". The axis asserts: - (1) `run_pipeline_in_process` reaches a pr-HITL gate - without raising for the issue's recorded SDLC shape - (bug fix / feature / refactor / infra / doc); (2) the - two substrate dimensions agree on the produced - artifact paths (`.egg-state/drafts/-analysis.md`, - `.egg-state/drafts/-plan.md`). The matrix is about - replaying the SDLC *shape*, not the verbatim text; - per feedback Q1 the issues are fixed regardless of - current GitHub state (note: #2714 closed since the - refine analysis was written, but this is irrelevant to - the matrix per Q1's rationale). Mark all 10 axes as - `pytest.mark.slow`. - acceptance: |- - Test exists with 10 parametrize axes (5 issues x 2 - substrates); uses MagicMock-style stubs (no recorded - transcripts); runs only under `pytest --slow`; each - axis completes within the 3-hour per-phase ceiling - enforced by TASK-4-5's hook; both substrate dimensions - agree on the produced artifact paths for every curated - issue. The test docstring documents the - MagicMock-vs-real-transcript trade-off and notes that - #2714's closed state is irrelevant per feedback Q1. - role: tester - files: - - integration_tests/regression/test_conformance_matrix.py - - id: TASK-4-5 - description: |- - Add a `pytest_runtest_setup` hook to - `integration_tests/regression/conftest.py` that fails - any single phase exceeding the 3-hour ceiling - (feedback Q2). The hook reads the per-phase - wall-clock from `.egg-state//substrate-metrics.json` - (populated by TASK-3-9) and fails fast. - acceptance: |- - Hook lands in `conftest.py`; new test - `test_phase_ceiling.py` verifies a synthetic 4-hour - phase fails the gate; documented in the conftest - module docstring. - role: tester - files: - - integration_tests/regression/conftest.py - - integration_tests/regression/test_phase_ceiling.py - - id: TASK-4-6 - description: |- - Update `SKILL.md` to remove the "Walking-skeleton - scope" callout entirely (lines 8-12 in the current - file); the skill now supports all four phases. Remove - the "What's NOT in this skill" section's pr-phase - line; remove the bridge-gap callout entirely. - acceptance: |- - `SKILL.md` reflects full four-phase support; no - walking-skeleton language remains; the front matter's - `description` is updated to remove the "walking-skeleton - spike" language. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/SKILL.md - - id: TASK-4-7 - description: |- - Update the ADR's "Rollout deltas" section - (`docs/architecture/claude-code-substrate.md:272-287`) - to mark off bullets: plan/implement/pr phase rollout - (line 276), conformance matrix (line 278), perf - budget (line 279). Move the satisfied bullets to a - new "Completed in #2717 rollout" section. - acceptance: |- - ADR no longer lists the three phase-rollout bullets, - conformance, and perf-budget bullets as open; a new - "Completed" section summarises what slice 4 shipped. - role: documenter - files: - - docs/architecture/claude-code-substrate.md - - id: 5 - name: |- - Hardening (cost cap + EggHarnessSpawner + R15 contingent + fork primitive + ADR + v0.x marker drop) - goal: |- - Production-ready substrate. Add the cost cap default 200 - (cq-6); ship `EggHarnessSpawner` + `egg-orch local-run` - headless CLI (DoD #5, feedback Q3 = Option A); contingent - R15 model-(b) migration if slice 1's R2 verdict was fail - (cq-4); fork-based delegation (cq-10 deferred half); drop - v0.x marker on the four protocols (cq-7); ADR refresh - naming every change from this rollout. - dependencies: - - slice-4 - tasks: - - id: TASK-5-1 - description: |- - Implement `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost - cap with default 200 (cq-6 = Option 2). Tracked on - `_InProcessOrchestrator._invocation_count`; raises - `RuntimeError("cost cap exceeded; current=, - cap=")` past the cap. The cap reads - `.egg-state//substrate-metrics.json` to - stay in sync with TASK-3-9's metrics. - acceptance: |- - `_InProcessOrchestrator._invocation_count` increments - on every spawn; cap default = 200; env override - works; the cap raises BEFORE the over-cap spawn - executes (so the operator's bill stops at the cap). - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-5-2 - description: |- - Add `EggHarnessSpawner` under - `orchestrator/substrate/egg_harness/spawner.py`. The - spawner satisfies the `AgentSpawner` Protocol (third - implementation, validating cq-7's stability claim - per feedback Q3). It drives `egg_harness.run_agent` - via `subprocess.Popen` per role; captures - `commit_sha` via `git rev-parse HEAD` immediately - after the subprocess returns (per - `orchestrator/substrate/claude_code/spawner.py:201` - for the existing pattern). - acceptance: |- - `EggHarnessSpawner` exists and implements - `AgentSpawner`; `select_substrate(env={"EGG_SUBSTRATE": - "egg-harness"})` returns a bundle with this spawner; - a `test_egg_harness_spawner.py` test asserts the - third spawner round-trips a fake agent and captures - `commit_sha`. - role: coder - files: - - orchestrator/substrate/egg_harness/__init__.py - - orchestrator/substrate/egg_harness/spawner.py - - orchestrator/substrate/__init__.py - - id: TASK-5-3 - description: |- - Add `cmd_local_run` subcommand to - `orchestrator/cli.py`. Mirrors the shape of existing - subcommands (`cmd_pipelines_status` at - `orchestrator/cli.py:541` is the closest reference). - Usage: `egg-orch local-run --issue --repo - `. Boots - `run_pipeline_in_process(...)` against an - `EggHarnessSpawner`-backed substrate; prints - `HITLDecision` envelopes to stdout in a JSON - envelope; reads operator answers from stdin (the - envelope shape mirrors `.egg-state/contracts/.json#pending_hitl`). - acceptance: |- - `egg-orch local-run --help` documents the - subcommand; running `egg-orch local-run --issue - 2718 --repo jwbron/egg` advances the pipeline - headlessly; pipeline completes through all four - phases against the curated doc-only issue. - role: coder - files: - - orchestrator/cli.py - - id: TASK-5-4 - description: |- - Add `test_egg_harness_spawner.py` under - `integration_tests/regression/`. Asserts the spawner - satisfies `AgentSpawner`; round-trips a fake agent - (no real Anthropic call); captures `commit_sha`; - works against `egg-orch local-run` end-to-end against - a curated issue (uses harness fakes for the - subagents themselves, but exercises the real CLI - shell). - acceptance: |- - Test runs in <60s; asserts the three Protocol methods - (`spawn`, `cleanup`, etc.); asserts the CLI exits 0 - on the curated doc-only issue (#2718). - role: tester - files: - - integration_tests/regression/test_egg_harness_spawner.py - - id: TASK-5-5 - description: |- - Add fork-based delegation primitive on - `_InProcessOrchestrator`: - `fork_subagent(parent_role: AgentRole, sub_task: str) - -> SubAgentResult`. **Implementation stays on the - harness re-host model per cq-3 (decide empirically - post-implement)**: the fork spawns a child via - `subprocess.Popen` running a fresh - `egg_harness.run_agent` invocation in a clean Python - process (NOT via Claude Code's Agent tool with - `subagent_type="general-purpose"` — that would commit - to Agent-tool dispatch ahead of the cq-3 empirical - verdict). The child does the sub-task with a focused - prompt; the parent receives the summary back. Mirrors - how a human delegates ("read all files matching X and - summarize") per cq-10's deferred half from #2623. If - cq-3's empirical verdict in slice-5's ADR refresh - (TASK-5-9) recommends migrating to Agent-tool - dispatch, the fork primitive's implementation will - change accordingly in a follow-up — not in this slice. - acceptance: |- - `fork_subagent` exists on - `_InProcessOrchestrator`; spawns the child via - `subprocess.Popen(...)` of `egg_harness.run_agent` - (NOT via Agent tool); the child inherits the parent's - role via `EGG_AGENT_ROLE` env so the harness's own - permission callback continues to enforce file-write - restrictions; the return is a `SubAgentResult` - dataclass with `summary` and `tool_calls` fields. - Unit-tested via harness fakes (TASK-5-8). The - docstring documents the cq-3 alignment. - role: coder - files: - - orchestrator/substrate/in_process.py - - id: TASK-5-6 - description: |- - Drop the `INTERFACE STABILITY: v0.x unstable.` marker - and the `# v0.x — unstable until ≥3 roles exercise` - comments from the four substrate protocol modules: - `orchestrator/substrate/spawner.py`, `message_bus.py`, - `policy.py`, `worktree.py`. cq-7 selected "drop the - marker after ≥3 roles exercise the claude-code side"; - by slice 4 the substrate has exercised ≥15 roles - (refiner + 2 refine reviewers + 3 plan producers + - 1 plan reviewer + 3 implement producers + 5 implement - reviewers). Regression-guard tests for the absence of - these markers land in TASK-5-10 (tester-owned). - acceptance: |- - Grep for `INTERFACE STABILITY: v0.x unstable.` and - `v0.x — unstable` returns zero hits across - `orchestrator/substrate/`. The module docstrings now - document the protocol as stable. - role: coder - files: - - orchestrator/substrate/spawner.py - - orchestrator/substrate/message_bus.py - - orchestrator/substrate/policy.py - - orchestrator/substrate/worktree.py - - id: TASK-5-10 - description: |- - Add a regression guard test that asserts the v0.x - unstable markers are absent from all four substrate - protocol modules. File lives under `shared/tests/` - next to existing `test_substrate_interfaces.py` per - the repo's substrate-tests convention. - acceptance: |- - New test file - `shared/tests/test_substrate_protocol_stability.py` - opens each of the four protocol module files and - asserts neither `INTERFACE STABILITY: v0.x unstable.` - nor `v0.x — unstable` substring is present in the - module text. A separate test asserts the module - docstring contains positive stability language - (e.g., "stable v1 interface"). - role: tester - files: - - shared/tests/test_substrate_protocol_stability.py - - id: TASK-5-7 - description: |- - Contingent on slice 1's R2 verdict (read - `.egg-state//r2-verdict.json` from - TASK-1-5). If R2 = fail, migrate every role rubric - under `plugins/egg-sdlc/skills/egg-sdlc/agents/.md` - to a real `.claude/agents/.md` definition with - frontmatter tool restrictions per - `build_agent_patterns()`. If R2 = pass, this - task is a no-op (close with note in the slice's BRC - history). - acceptance: |- - If R2 fail: every rubric file gains frontmatter with - `allowed-tools` reflecting the role's allow-list per - `shared/egg_restrictions/patterns.py:768`; structural - tool-restriction enforcement now layers on top of the - PreToolUse hook; a new test under - `tests/plugins/egg_sdlc/test_role_frontmatter.py` - asserts every rubric's `allowed-tools` matches its - `build_agent_patterns(role).allow` set. If R2 pass: - task closed with note. - role: documenter - files: - - plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/coder.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/tester.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/documenter.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_code_holistic.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_contract.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_security.md - - plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_concurrency.md - - id: TASK-5-8 - description: |- - Add `test_cost_cap.py` and `test_fork_subagent.py` - under `integration_tests/regression/`. Cost-cap test - asserts the default (200) and env override behavior; - fork test asserts child summary returns to parent - without leaking the child's full tool-call history. - acceptance: |- - Both tests pass; cost cap raises `RuntimeError` past - 200 and at the env override; fork primitive returns - `SubAgentResult` with `summary` set and `tool_calls` - truncated to the parent. - role: tester - files: - - integration_tests/regression/test_cost_cap.py - - integration_tests/regression/test_fork_subagent.py - - id: TASK-5-9 - description: |- - ADR refresh at - `docs/architecture/claude-code-substrate.md`: rewrite - the "Rollout deltas" section to a "Completed in #2717" - section; document the cost cap default (200) and its - env override; document `EggHarnessSpawner` + `egg-orch - local-run` shape; document the fork primitive; document - the R2 verdict (pass or fail) and what shipped as a - consequence (R15 migration or no-op); update R10 - (stability marker) to reflect the drop. - acceptance: |- - ADR no longer carries the "Rollout deltas" follow-up - framing; every cq decision from this issue is - reflected in the relevant section; the document - reads as a stable substrate ADR rather than a - walking-skeleton plus rollout draft. - role: documenter - files: - - docs/architecture/claude-code-substrate.md -``` diff --git a/docs/architecture/README.md b/docs/architecture/README.md index ad1eea63e5..d40a17c181 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -191,7 +191,6 @@ The SDLC pipeline orchestrates agent-based development with structurally enforce - [Credential Injection](credential-injection.md) - Zero-credential sandbox with API key proxy - [Network Isolation](network-isolation.md) - Public/private network modes - [SDLC Pipeline](sdlc-pipeline.md) - Structurally enforced agent checkpoints -- [Claude Code Substrate](claude-code-substrate.md) - Substrate-swap walking skeleton (#2623): four `Protocol`s (`AgentSpawner` / `MessageBus` / `PolicyEnforcer` / `WorktreeManager`) under `orchestrator/substrate/`, env-var-selected via `EGG_SUBSTRATE`, with a working `K3sSpawnerAdapter` shim and Claude-Code-native implementations (`ClaudeCodeSpawner`, `InProcessMessageBus`, `PreToolUseHookPolicy`, `LocalWorktreeManager`); ships an in-process orchestrator generator (`run_pipeline_in_process`) yielding `HITLDecision` objects (cq-7 heredoc-HITL). The ADR records the trust-context shift (R1), PreToolUse-hook fallback to MCP-validator-side enforcement (R2), subagent context-budget regression vs `max_turns:1000` (R7), and the `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost-cap recommendation (REC5); the four interfaces are marked `# v0.x — unstable until ≥3 roles exercise`; a "Follow-up issue draft" appendix captures the deferred rollout (plan / implement / pr phases, full 5-issue conformance matrix, perf budget) - [Declarative Setup](declarative-setup.md) - Python-based setup - [Logging](logging.md) - Structured JSON logging - [Integration-Test Trust Boundary](integration-test-trust-boundary.md) - Test execution contexts (in-sandbox-agent / trusted-CI-runner / human-operator), fixture tier table, and hard-NACK rules for plan-phase Primitive-Existence (§9) and Trust-Boundary (§10) audits diff --git a/docs/architecture/claude-code-substrate.md b/docs/architecture/claude-code-substrate.md deleted file mode 100644 index a5f2cf3331..0000000000 --- a/docs/architecture/claude-code-substrate.md +++ /dev/null @@ -1,297 +0,0 @@ -# Claude Code Substrate (#2623 spike → #2717 rollout) - -> Status: **rollout in progress, interfaces still unstable** — the spike for [#2623](https://github.com/jwbron/egg/issues/2623) landed the four substrate `Protocol`s with one role (refiner) exercising them end-to-end on the Claude Code side and a thin `K3sSpawnerAdapter` shim keeping `EGG_SUBSTRATE=k3s` green. The rollout under [#2717](https://github.com/jwbron/egg/issues/2717) extends the substrate slice by slice. **Slice 1 (refine-team expansion + flattened HITL bridge + R2 spike) has landed**; slices 2–5 (plan / implement / pr / hardening) are pending. The interfaces still carry the `# v0.x — unstable until ≥3 roles exercise` marker (R10); the marker drops in slice 5. The full conformance matrix, the real k3s interface adapter, and the cost-cap / prune / fork primitives are tracked in the [Rollout deltas](#rollout-deltas) section below. - -This ADR documents the substrate-swap landed by issue [#2623](https://github.com/jwbron/egg/issues/2623): a parallel Claude-Code-native execution substrate for the egg SDLC stack, sitting behind four named `typing.Protocol`s in `orchestrator/substrate/` and selected at boot by an `EGG_SUBSTRATE` env var. The k3s substrate (`KubernetesSpawner`, `RedisMessageStore`, gateway sidecar) keeps working unchanged; the Claude Code substrate is opt-in. - -The walking-skeleton scope was settled by the refine-phase HITL: **cq-11 = "Spike then plan"**. One slice, one role, end-to-end on the new substrate, then re-plan the rollout in a follow-up issue once the spike has returned real numbers on subagent context behavior, hook ergonomics, and `Agent` tool throughput. - -## Why a substrate swap, not a parallel skill - -PR #2608 shipped `plugins/refine-plan/` — a Markdown skill that *approximates* egg's refine + plan phases using Claude Code subagents and a filesystem verdict journal. Child issues #2612 (concurrent BRC) and #2622 (depth gap) catalogued the structural shortcomings of that approximation: iterated rather than concurrent, no real message bus or version tracking, no mid-cycle revision, no `build_system_prompt` depth, reviewer rubrics that don't enforce evidence breadth. - -This issue takes a different framing. Rather than chase parity with a parallel Markdown implementation, **swap the substrate underneath the real stack**: keep `egg_orchestrator`, `egg_harness`, `egg_contracts`, `egg_agent`, and `shared/prompts/` unchanged, and re-platform them onto Claude Code's native primitives. If the swap is real (versus an approximation), depth, BRC mechanics, contract schema, role prompts, and HITL semantics come for free from the reused upstream code. **Quality becomes structurally inevitable**: the only question is "does the substrate-swap layer faithfully expose the orchestrator's coordination surface to a Claude Code session?" - -| Today's substrate | Claude-Code-native replacement | -|---|---| -| **k3s** schedules agent pods (`KubernetesSpawner`) | Claude Code's `Agent` tool spawns subagents; `ClaudeCodeSpawner` dispatches via `shared/egg_harness` | -| **Docker container** per agent (`sandbox/Dockerfile`) | `Agent` tool's `isolation: "worktree"` + Bash sandbox mode | -| **Redis Streams** message bus (`RedisMessageStore`) | In-process Python (`InProcessMessageBus`) — reuses the existing in-memory `MessageStore` | -| **Gateway sidecar** policy + creds (`gateway/`) | PreToolUse hooks in `.claude/settings.json` calling into the existing `shared/egg_restrictions/patterns.py` | -| **Persistent volume** for worktrees | User's local filesystem (`~/.egg-worktrees///` by default via `LocalWorktreeManager`; `EGG_WORKTREE_BASE` overrides) | -| **Sandbox container image** | User's local Claude Code install + per-role `agents/*.md` files | -| **`kubectl get pods` health checks** | In-process `egg_health` thread | -| **Overseer pod** | In-process overseer thread | - -This is **cq-1 = parallel substrates, env-var-selected (Option A)**, executed via cq-11's spike-first sequencing. - -## The eleven cq decisions, in one table - -The refine-phase HITL settled eleven decisions and six feedback items. Each one shapes a piece of the substrate seam, named below alongside the module it lands in. - -| Decision | Selection | What lands | -|---|---|---| -| **cq-1** substrate strategy | Option A — parallel substrates, env-var-selected | `orchestrator/substrate/` with `select_substrate(env)` factory reading `EGG_SUBSTRATE` | -| **cq-2** parent-close phase scope | All phases (refine + plan + implement + pr) | Spike landed refine-only per cq-11. **Slice 1 of #2717 expands the refine roster** to the full refine-team (refiner + reviewer_refine + reviewer_agent_design). Slices 2 / 3 / 4 land plan / implement / pr | -| **cq-3** conformance scoping | Extend `integration_tests/regression/` with a `substrate` parameter (CI matrix) | One regression test parametrized via the new fixture in this spike; full matrix factor-out deferred | -| **cq-4** spawner shape | Synchronous `spawn(role, prompt, env, worktree) → AgentResult` | `AgentSpawner` protocol pinned at this signature | -| **cq-5** worktree ownership | Port `WORKTREE_BASE_DIR` model | `LocalWorktreeManager` mirrors `gateway/worktree_manager.py:49` shape; per-agent worktrees land at `///`; default `` is `~/.egg-worktrees/`, `EGG_WORKTREE_BASE` overrides (typical override: `./.egg-state/`) | -| **cq-6** policy seam | PreToolUse hooks | `PreToolUseHookPolicy` ships a hook entry script + `settings.template.json`; calls the existing `shared/egg_restrictions/patterns.py:768 build_agent_patterns` | -| **cq-7** HITL surface | Heredoc-style synchronous generator | `run_pipeline_in_process(...)` is a generator yielding `HITLDecision` objects; the skill renders each via `AskUserQuestion` and resumes via `.send(...)`. **Bridge: flattened (Option C, refine/plan) + daemon (Option C, implement).** Slice 1 of #2717 closes the refine-phase bridge gap via `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` — a single-yield stage driver that round-trips each `HITLDecision` through `.egg-state/contracts/.json#pending_hitl`. Slice 3 of #2717 ships the daemon variant for implement-phase concurrency. The `pending_hitl` envelope is a shared state-serialization contract between the two bridges (risk_analyst R17 mitigation). | -| **cq-8** packaging | Plugin metadata declares pip dependencies | **Deferred to follow-up** (depends on cq-12). `plugins/egg-sdlc/.claude-plugin/plugin.json` currently carries an `egg.install_instructions` from-source command (`git clone … && pip install -r requirements.txt && export PYTHONPATH=…`) as the operator-actionable surface — see the [`egg-sdlc plugin`](#the-egg-sdlc-plugin) section at line 122. Resolving cq-12 swaps this back to a pip-dep declaration. | -| **cq-9** k3s disposition | Leave indefinitely | k3s code untouched in this spike; deprecation is a future-issue question | -| **cq-10** context-window strategy | Hybrid — checkpoint + fork | Spike ports the checkpoint half; forking is deferred to follow-up (it's a quality booster, not a correctness requirement) | -| **cq-11** slice shape | Spike then plan | One slice in the spike; #2717 is the follow-up planning issue with a 5-slice DAG | -| **cq-12** canonical pip name | **Deferred to follow-up** | Operator-decidable scope; `plugins/egg-sdlc/.claude-plugin/plugin.json` carries `egg.install_instructions` (from-source command) as the operator-actionable surface until cq-12 settles. See the [`egg-sdlc plugin`](#the-egg-sdlc-plugin) section at line 122. | - -### Feedback applied - -- **Q1** (conformance set): spike runs against **one** fixed curated issue; follow-up extends to 5. Selection rule documented in the follow-up draft. -- **Q2** (latency budget): explicitly deferred — no perf gate in the spike's acceptance criteria; follow-up captures budget once real numbers exist. -- **Q3** (deps): spike pins to existing egg pip deps; no new third-party dependencies; no developer-mode Claude Code feature flag required; marketplace footprint stays well under the soft ~100 MB cap. -- **Q4** (non-Claude-Code callers): secondary goal. Interfaces are designed to admit an `EggHarnessSpawner` later (subprocess-driven `egg_harness` for headless CLI). The spike does not build it. -- **Q5** (#2622 structural causes #5/#6): out of scope for this issue; stays with #2622. -- **Q6** (telemetry/privacy): checkpoints are local-filesystem-only (`.egg-state/checkpoints//`); no telemetry sent. Spike does not ship a prune verb — reserved for the follow-up. - -## The four interfaces - -All four interfaces live under `orchestrator/substrate/` as `typing.Protocol`s and carry the `# v0.x — unstable until ≥3 roles exercise` marker in their module docstrings (R10). The k3s side gets a working `K3sSpawnerAdapter` shim from day one so `select_substrate({})` returns a working spawner — the cq-1 parallel-substrates choice rules out raising `NotImplementedError` for the k3s leg. - -### `AgentSpawner` — `orchestrator/substrate/spawner.py` - -cq-4: synchronous `spawn(role, prompt, env, worktree) → AgentResult`. The caller blocks until the agent completes. Internal concurrency is owned by the spawner, so the orchestrator's existing `ThreadPoolExecutor` in `orchestrator/concurrent_executor.py:114` keeps issuing parallel `spawn()` calls without changes. `AgentResult` is a dataclass with `stdout`, `exit_code`, `duration_seconds`, `worktree`, **and `commit_sha: str | None`** — the SHA is required so reviewers can attach commit-bound ACKs per the existing INV-6 invariant in `orchestrator/action_guards.py:631` (invariant body at `:757`). - -- **k3s implementation**: `K3sSpawnerAdapter` (in `orchestrator/substrate/k3s_adapter.py`) wraps `create_concurrent_spawn_fn` (`orchestrator/kubernetes_spawner.py:1564`). It returns `AgentResult.commit_sha=None` by design: the legacy factory is fire-and-monitor, so a `git rev-parse HEAD` on the orchestrator host at adapter-return time would capture the *pre*-spawn HEAD and BRC reviewers would attach commit-bound ACKs to the wrong SHA. The legitimate INV-6 SHA for k3s is supplied through the existing gateway-side attestation channel that reads it off `SpawnedContainer.container_info` after the pod terminates. Plumbing that channel into the protocol's `commit_sha` field directly is tracked in the [Rollout deltas](#rollout-deltas) ("wire gateway attestation into `AgentResult.commit_sha`"). No behavior change for k3s users — the dispatch seam at `_spawn_agent` is gated on `EGG_SUBSTRATE=claude-code` only and the legacy path remains in place for unset / `"k3s"` (reviewer v1 blocker #1). -- **Claude Code implementation**: `ClaudeCodeSpawner` (in `orchestrator/substrate/claude_code/spawner.py`) blocks the caller, dispatches to Claude Code's `Agent` tool surface via `shared/egg_harness`, and runs `git -C rev-parse HEAD` immediately after the subagent returns to capture `commit_sha`. It assembles the per-role system prompt via `build_system_prompt(sources)` (`shared/egg_harness/prompt.py:24`) — this is the structural depth fix from #2622: by routing through the real prompt assembler, all four depth-gap structural causes close as a side-effect of running the real harness in a Claude Code session. - -The dispatch seam at `orchestrator/concurrent_executor.py:504 _spawn_agent` is patched to invoke `select_substrate(os.environ).spawner.spawn(...)` **only when `EGG_SUBSTRATE=claude-code` is set explicitly**. Unset or `"k3s"` preserves the legacy `self.spawn_fn(...)` path verbatim (reviewer v1 blocker #1 — until `K3sSpawnerAdapter` forwards slice-aware branches and the BRC consensus-wrapped command, setting `EGG_SUBSTRATE=k3s` would silently lose both). The follow-up issue extends the adapter and re-opens the seam to k3s once the gaps close. - -### `MessageBus` — `orchestrator/substrate/message_bus.py` - -The BRC-mechanics object the orchestrator drives. Implementations must preserve INV-3 (stale-version rejection) and INV-5 (open-NACK barrier) from `orchestrator/action_guards.py:631 validate_invariants` — these are the BRC concurrency invariants that survive substrate transitions per the issue body's "if we use the real orchestrator in-process, BRC mechanics come for free." - -- **k3s implementation**: backed by `RedisMessageStore` (`orchestrator/redis_message_store.py:107`). Untouched by this spike. -- **Claude Code implementation**: `InProcessMessageBus` (in `orchestrator/substrate/claude_code/message_bus.py`) uses Python `dict` / `threading.Lock` / `queue` primitives. It may subclass or delegate to the existing in-memory `MessageStore` (`orchestrator/message_store.py:200`); the BRC test suite at `orchestrator/tests/test_brc_*.py` is the behavioral oracle the bus must match. - -### `PolicyEnforcer` — `orchestrator/substrate/policy.py` - -The gateway-equivalent for file-write and tool-use restrictions. cq-6 picked PreToolUse hooks as the primary enforcement seam. - -- **k3s implementation**: the existing gateway's `check_agent_restrictions` (`gateway/phase_filter.py:1061`) is the reference. The spike does not adapt this onto the new protocol — that's a follow-up task (R2 fallback path below). -- **Claude Code implementation**: `PreToolUseHookPolicy` (in `orchestrator/substrate/claude_code/policy.py`) plus a hook entry script (`hook_entry.py`) referenced from a `.claude/settings.json` template (`settings.template.json`). The hook reads tool name + tool input from stdin per the Claude Code PreToolUse contract, imports `build_agent_patterns` from `shared/egg_restrictions/patterns.py:768`, and emits `deny` + `message` JSON to stdout when the write target lands outside the caller's role's allow-list. **Single source of truth**: the hook calls the *exact same* `build_agent_patterns` symbol the gateway uses — no parallel restriction logic. - -### `WorktreeManager` — `orchestrator/substrate/worktree.py` - -cq-5: port the existing `WORKTREE_BASE_DIR` model rather than use Claude Code's native `EnterWorktree` primitive. The substrate creates per-agent worktrees in a `//` subdirectory under a single configurable base, tracks them in a dict, and tears them down at phase end. (The path keys on `role`, not `repo`, because the in-process orchestrator runs against a single repo per pipeline and the worktree's per-role isolation is what matters.) - -The default base is **`~/.egg-worktrees/`** — matching the shape of `gateway/worktree_manager.py:49 WORKTREE_BASE_DIR` (which hardcodes `/home/egg/.egg-worktrees` for the gateway container; the Claude-Code-substrate implementation expands `~` against the calling user's `$HOME` instead). `EGG_WORKTREE_BASE` overrides the base — typical override is `./.egg-state/` so worktrees live alongside the contract / drafts / agent-outputs files in the same `.egg-state//` tree, per cq-5's literal text. Full path on disk by default: `~/.egg-worktrees///` (one worktree per role); under the typical override: `.egg-state///`. The branch name created in each worktree follows `egg//` for legibility in `git branch` output. - -- **k3s implementation**: the existing `gateway/worktree_manager.py` already implements this shape (default base at `gateway/worktree_manager.py:49`, hardcoded to `/home/egg/.egg-worktrees`). The k3s adapter is not implemented in this spike — left as a TODO in the protocol module. -- **Claude Code implementation**: `LocalWorktreeManager` (in `orchestrator/substrate/claude_code/worktree.py`) defaults to the same `~/.egg-worktrees/` base shape but respects an `EGG_WORKTREE_BASE` override. Path-escape safety mirrors the `is_relative_to` + `resolve()` defense at `gateway/worktree_manager.py:1700-1711` (call site within `list_orphan_worktree_dirs`, defined at `:1687`). The bug class is identical to worktree teardown — symlink-traversal attempts must be rejected before any filesystem mutation. - -## `EGG_SUBSTRATE` and `select_substrate(env)` - -The factory function at `orchestrator/substrate/__init__.py` reads `EGG_SUBSTRATE` from the environment and returns a substrate bundle: - -| `EGG_SUBSTRATE` | Returns | Notes | -|---|---|---| -| unset, `""`, or `"k3s"` | `SubstrateBundle(spawner=K3sSpawnerAdapter, ...)` | The default; existing k3s deployments stay green | -| `"claude-code"` | `SubstrateBundle(spawner=ClaudeCodeSpawner, bus=InProcessMessageBus, policy=PreToolUseHookPolicy, worktrees=LocalWorktreeManager)` | Opt-in claude-code path | -| any other value | raises | Misconfigured env should fail loudly, not silently fall back | - -## The in-process orchestrator: `run_pipeline_in_process(...)` - -The spike's most expensive task. Today the orchestrator is a Flask + waitress HTTP daemon (`orchestrator/cli.py:83 cmd_serve`) with `ConcurrentPhaseExecutor` (`orchestrator/concurrent_executor.py:114`) running its own `ThreadPoolExecutor` and `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`) holding its own locks. The in-process boot path is **net-new** to the claude-code substrate — `egg-orch` remains a thin HTTP client for the k3s side. - -`run_pipeline_in_process(...)` (in `orchestrator/substrate/in_process.py`) is a Python generator that yields `HITLDecision` objects (`orchestrator/models.py:300`) when the pipeline pauses for a human decision. Per cq-7 = heredoc-style synchronous, the skill renders each yielded decision via `AskUserQuestion` and resumes via `generator.send(answer)`. - -### The flattened bridge (Option C, refine + plan — landed in slice 1 of #2717) - -A Claude Code skill cannot drive a long-lived Python generator across multiple `AskUserQuestion` round-trips natively — `AskUserQuestion` is a tool the LLM calls, not a function callable from a `python3` subprocess, and every `python3` invocation from a Bash skill step is a fresh process whose `gi_frame` dies at exit. **Per cq-1 = hybrid (Option C)**, the rollout picks the *flattened* approach for refine and plan phases and a *daemon* variant for implement phase: - -- **Flattened (slice 1, refine + plan).** The skill loops over invocations of `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py`. Each invocation loads `.egg-state/contracts/.json`, promotes the operator's most recent `pending_hitl.answer` into `pending_hitl.answer_log`, spawns a fresh `run_pipeline_in_process(...)` generator, **replays the entire `answer_log`** into it to reach the next un-answered yield (the flattened bridge is deterministic-replay-based, not single-step resumption — fresh process every call), serialises the yielded `HITLDecision` into `pending_hitl.decision`, and exits 0 with `pending_hitl.status = "pending"`. The skill renders the decision via `AskUserQuestion`, writes the operator's selection back to `pending_hitl.answer` + `status = "answered"`, and re-invokes the driver. On `StopIteration`, the driver clears `pending_hitl.decision` to `None`, sets `status = "completed"` (or `"aborted"` if the last answer was an abort), and writes the generator's return value (typically the analysis path) to `pending_hitl.result`. The skill loop's exit predicate is `status ∈ {completed, aborted, error}`. -- **Daemon (slice 3, implement).** A long-lived Python REPL the skill talks to via a JSON-RPC envelope, so the generator state survives the multi-producer concurrency of implement-phase BRC (and replay-based fast-forward becomes prohibitively expensive). The daemon variant consumes the **same 9-field `pending_hitl` envelope shape** the flattened driver writes — `version`, `pipeline_id`, `timestamp`, `decision`, `answer`, `status`, `result`, `error`, `answer_log` — risk_analyst R17 mitigation. A pipeline started on the flattened bridge can be resumed on the daemon variant and vice-versa; the daemon variant simply skips the replay step because its generator survives across invocations. - -The full 9-field `pending_hitl` envelope is a stable cross-bridge contract. The flattened driver's top-of-file comment at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:20-46` is the source of truth; the daemon variant in slice 3 (TASK-3-2) consumes the same shape. SKILL.md mirrors the schema in its "How the flattened bridge works" section. - -Key properties (unchanged from the spike): - -1. **Heartbeat-during-HITL** within an invocation: while the generator is paused at a yield boundary inside a single `bin/run_pipeline.py` invocation, the in-process orchestrator's background threads (heartbeat poll, BRC re-review, message-bus tick) continue to run so a long-paused HITL does not cause stuck-phase-transition alerts within that invocation. Between invocations the Python process has exited and orchestrator state lives only in the contract file. R4-driven acceptance criterion. -2. **Background-thread lifetime**: the generator returns cleanly (background threads joined) on the normal completion path **and** on `GeneratorExit` (the flattened driver exiting between yields). No leaked threads across the skill→Python boundary. -3. **Contract-state synchronization**: the in-process orchestrator uses the same `.egg-state/contracts/.json` filesystem write path the HTTP daemon uses — no separate state store. The `pending_hitl` envelope is layered onto the contract under a dedicated key. -4. **Existing primitives stay in the path**: `build_system_prompt` (`shared/egg_harness/prompt.py:24`), `ConcurrentPhaseExecutor` (`orchestrator/concurrent_executor.py:114`), `HITLDecision` (`orchestrator/models.py:300`), `PeerConsensusTracker` (`orchestrator/peer_consensus.py:69`). -5. **`EGG_SUBSTRATE=k3s` raises `NotImplementedError`** with a message naming the k3s HTTP daemon entry. k3s users keep using `orchestrator/cli.py:83 cmd_serve`; the *in-process* entry is claude-code-only. - -## The `egg-sdlc` plugin - -`plugins/egg-sdlc/` is the skill entry point for the claude-code substrate. It is a thin wrapper that drives `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` in a loop and renders each yielded `HITLDecision` via `AskUserQuestion`. - -- `plugins/egg-sdlc/.claude-plugin/plugin.json` carries the `egg.install_instructions` field (reviewer v1 blocker #8: the previous `python_dependency` field was a TODO string the preflight printed verbatim as the install command). Until cq-12 settles on a pip-installable package name, the field holds the actionable from-source command (`git clone … && pip install -r requirements.txt && export PYTHONPATH=…`). `bin/preflight.py` and SKILL.md read from the same field so the install error stays in sync. Resolving cq-12 + publishing a pip name swaps this back to a `python_dependency`-style field; tracked in the rollout. -- `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md` documents the user-facing heredoc-HITL loop, the flattened bridge mechanism, and the slice-by-slice rollout status (refine-team landed in slice 1; plan / implement / pr land in slices 2 / 3 / 4 of #2717). -- `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` is the flattened stage driver added by slice 1 (TASK-1-1). It imports `run_pipeline_in_process(...)` and round-trips a single `HITLDecision` through `.egg-state/contracts/.json#pending_hitl` per invocation. -- `plugins/egg-sdlc/skills/egg-sdlc/agents/` holds the per-role prompt-prepend files the in-process orchestrator's `build_system_prompt(sources)` reads via the `role_rubric_loader` injected in `select_substrate(...)` (reviewer v1 blocker #5). The files mirror the layout of `plugins/refine-plan/skills/refine-plan/agents/` so the prompt assembler does not need per-skill custom logic. Refine-team rubrics that ship with slice 1 of the #2717 rollout: - - `agents/refiner.md` — refiner role (landed with the original spike). - - `agents/reviewer_refine.md` — refine-team reviewer for analysis quality, research depth, options analysis, and open-question specificity (TASK-1-4). - - `agents/reviewer_agent_design.md` — refine-team reviewer for agent-mode design alignment and anti-patterns; spawned only when the target repo is `jwbron/egg` (TASK-1-4). - -The loader at `orchestrator/substrate/__init__.py:232 _load_egg_sdlc_role_rubric` returns the rubric body for any role whose `.md` file is present in `agents/` and raises `ValueError` for roles whose rubric is not yet on the substrate (plan / implement / pr roles until later slices land — TASK-1-6). - -## Conformance proof: substrate-parameter CI matrix - -cq-3 picked "extend `integration_tests/regression/` with a substrate parameter (CI matrix)". The spike lands the minimum proof; slice 1 of #2717 adds the R2 + flattened-bridge proofs; the full factor-out across all 5 curated issues lands in slice 4. - -- A `substrate` parametrize-able fixture in `integration_tests/regression/conftest.py` (values: `"k3s"`, `"claude-code"`). The claude-code dimension `pytest.skip`s when running inside an in-sandbox-agent trust context. -- A substrate-distinguishing test at `integration_tests/regression/test_substrate_smoke.py` parametrized over both substrates. It drives `select_substrate(...).spawner.spawn(...)` (asserts the round-trip returns an `AgentResult` instance) and `.bus.add_message / .bus.get_messages` (asserts INV-3 stale-version rejection round-trip). Both parameters run pure-Python in-process — no kubectl gate. The smoke does not assert populated `AgentResult.commit_sha` because the k3s adapter returns `None` by design (see `AgentSpawner.spawn` docstring); the populated-SHA path is covered by `shared/tests/test_claude_code_spawner.py` for the claude-code leg. -- **Slice 1 of #2717 additions**: `integration_tests/regression/test_bridge_flattened_round_trip.py` (TASK-1-3) asserts the flattened bridge round-trips a `HITLDecision` across two `bin/run_pipeline.py` invocations via the `pending_hitl` envelope. `integration_tests/regression/test_pretooluse_hook_nested.py` (TASK-1-5) asserts the PreToolUse hook denies a child subagent's write when the parent role's allow-list would otherwise permit it, and writes the R2 verdict. `shared/tests/test_rubric_loader.py` (TASK-1-7) asserts `_load_egg_sdlc_role_rubric` returns the new refine-team reviewer rubrics and still raises `ValueError` for plan / implement roles. -- Protocol-conformance unit tests under `shared/tests/` for each implementation (`ClaudeCodeSpawner`, `K3sSpawnerAdapter`, `InProcessMessageBus`, `PreToolUseHookPolicy`, `LocalWorktreeManager`, `run_pipeline_in_process`). The bus tests mirror the scenarios in `orchestrator/tests/test_brc_open_nacks_barrier.py` and `orchestrator/tests/test_brc_content_validation.py` — the behavioral oracle. - -## Primitives table - -Existing primitives the spike reuses or wraps, and new primitives the spike creates. - -### Existing (reused or wrapped) - -| Primitive | Module | -|---|---| -| `class KubernetesSpawner`, `def spawn_agent_job` | `orchestrator/kubernetes_spawner.py` | -| `class KubernetesMonitor` | `orchestrator/kubernetes_monitor.py` | -| `def create_concurrent_spawn_fn` | `orchestrator/kubernetes_spawner.py:1564` — wrapped by `K3sSpawnerAdapter` | -| `class MessageStore` (in-memory) | `orchestrator/message_store.py:200` — basis for `InProcessMessageBus` | -| `class RedisMessageStore` | `orchestrator/redis_message_store.py` | -| `class ConcurrentPhaseExecutor`, `def _spawn_agent` | `orchestrator/concurrent_executor.py` — `_spawn_agent` is the patched dispatch seam | -| `def cmd_serve`, `def cmd_pipelines_create` | `orchestrator/cli.py` — `cmd_pipelines_create` is the model for `run_pipeline_in_process` | -| `def validate_invariants` (INV-3, INV-5, INV-6) | `orchestrator/action_guards.py` | -| `class HITLDecision` | `orchestrator/models.py:300` — yielded by `run_pipeline_in_process` | -| `class PeerConsensusTracker` | `orchestrator/peer_consensus.py:69` | -| `def build_system_prompt`, `PromptSource` | `shared/egg_harness/prompt.py` — the structural depth fix #2622 relies on; `ClaudeCodeSpawner` MUST keep it in the path | -| `def set_permission_callback` | `shared/egg_harness/tools/registry.py` | -| `class AgentFilePattern`, `def build_agent_patterns`, `def check_agent_restrictions` | `shared/egg_restrictions/patterns.py`, `gateway/phase_filter.py:1061` — single source of truth shared between gateway and PreToolUse hook | -| `WORKTREE_BASE_DIR`, `is_relative_to` + `resolve()` defense | `gateway/worktree_manager.py:49`, `:1700-1711` (within `list_orphan_worktree_dirs` at `:1687`) | -| Regression-suite fixtures | `integration_tests/regression/conftest.py` | -| `plugins/refine-plan/skills/refine-plan/SKILL.md`, `agents/refiner.md` | reference shape for the new `egg-sdlc` plugin | - -### New (created by this spike) - -| Primitive | Module | -|---|---| -| `AgentSpawner` (Protocol), `AgentResult` (dataclass with `commit_sha`) | `orchestrator/substrate/spawner.py` | -| `MessageBus` (Protocol) | `orchestrator/substrate/message_bus.py` | -| `PolicyEnforcer` (Protocol) | `orchestrator/substrate/policy.py` | -| `WorktreeManager` (Protocol) | `orchestrator/substrate/worktree.py` | -| `select_substrate(env)` factory + `SubstrateBundle` | `orchestrator/substrate/__init__.py` | -| `K3sSpawnerAdapter` | `orchestrator/substrate/k3s_adapter.py` | -| `ClaudeCodeSpawner` | `orchestrator/substrate/claude_code/spawner.py` | -| `InProcessMessageBus` | `orchestrator/substrate/claude_code/message_bus.py` | -| `PreToolUseHookPolicy`, hook entry script, `settings.template.json` | `orchestrator/substrate/claude_code/policy.py`, `hook_entry.py`, `settings.template.json` | -| `LocalWorktreeManager` | `orchestrator/substrate/claude_code/worktree.py` | -| `run_pipeline_in_process(...)` generator | `orchestrator/substrate/in_process.py` | -| `egg-sdlc` plugin metadata + SKILL.md + per-role rubrics | `plugins/egg-sdlc/.claude-plugin/plugin.json`, `plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`, `plugins/egg-sdlc/skills/egg-sdlc/agents/{refiner,reviewer_refine,reviewer_agent_design}.md` (reviewer rubrics added by slice 1 of #2717, TASK-1-4) | -| `bin/run_pipeline.py` flattened stage driver (slice 1, TASK-1-1) | `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` | -| Test-only nested-Agent-tool dispatch fake (slice 1, TASK-1-9) | `integration_tests/regression/_agent_tool_fake.py` | -| R2 nested-dispatch test (slice 1, TASK-1-5) | `integration_tests/regression/test_pretooluse_hook_nested.py` | -| `substrate` pytest fixture + parametrized smoke test | `integration_tests/regression/conftest.py`, `integration_tests/regression/test_substrate_smoke.py` | -| ADR + rollout-deltas tracker | `docs/architecture/claude-code-substrate.md` (this file) | - -## What's NOT the same as before (risk-mitigation subsections) - -The risk_analyst identified several risks that materially shift egg's behavior under the new substrate. Each is documented here per **REC2** so the ADR is more than "how the substrate works" — it is also the audit-trail for *what changes* and the operator's explicit acceptance of those changes. - -### Trust-context shift (R1) - -**The change.** Today the Anthropic API key is gateway-isolated: the sandbox NEVER sees the real key — the gateway intercepts API requests and injects credentials server-side (`gateway/anthropic_credentials.py`); the sandbox sees only a `sk-ant-oat01-PROXY-INJECTED-...` placeholder. In the Claude Code substrate, the parent Claude session HOLDS the real key, and every subagent the orchestrator spawns inherits the session's credential context. A subagent compromised via prompt injection (untrusted issue body, malicious PR content) can in principle read or exfiltrate the key from environment / disk / network; in the k3s model the same compromise would only have access to the placeholder. - -**Why it's accepted.** The substrate-swap goal is to run egg from a single developer's Claude Code session against the developer's own repos. The threat model is not "agent from a randomly-encountered issue" — it is the user's own SDLC. Keeping the gateway in the loop contradicts the substrate-swap goal; documenting the shift and scoping use to trusted repos is the realistic mitigation (R1 mitigation strategy). - -**Operator acceptance.** Per the cq-1 = "parallel substrates" selection, the operator explicitly accepts running the claude-code substrate against *trusted-repo* SDLC streams. The gateway-isolated k3s substrate remains available indefinitely (cq-9) for any caller that needs the credential-isolation boundary. - -**Mitigations in this spike.** - -- Skill imports never log credentials. The plugin's pre-flight import does not echo env to stdout. -- The PreToolUse hook entry script does not exfiltrate environment to stdout; it only emits `deny` / `allow` + `message`. -- The install docs (SKILL.md) name the trust-context shift explicitly so the user reads it before installing. -- Future work: an opt-in `credentialed-proxy` mode that routes the claude-code substrate through a local gateway-equivalent for users running against untrusted issue streams. Tracked in the follow-up. - -### PreToolUse hook fallback (R2) - -**The primary seam.** cq-6 selected PreToolUse hooks as the policy enforcement boundary. The hook reads tool name + tool input from stdin (PreToolUse contract), imports `build_agent_patterns` from `shared/egg_restrictions/patterns.py:768`, and emits `deny` + `message` JSON to stdout when the write target is outside the caller's role's allow-list. - -**The empirical question.** Whether Claude Code's PreToolUse hooks can reliably resolve "which subagent / role is calling Write()" from the hook's process context under nested Agent-tool dispatch. The spike validated the single-role path; **slice 1 of #2717 ships the worked 2-subagent example** as the cq-5 early-spike gating test: - -- `integration_tests/regression/test_pretooluse_hook_nested.py` (TASK-1-5) spawns a parent fake-subagent with `EGG_AGENT_ROLE=architect` and a nested child fake-subagent with `EGG_AGENT_ROLE=tester`. It asserts the hook denies a write to `orchestrator/foo.py` from the child even though the parent's role would allow it. -- `integration_tests/regression/_agent_tool_fake.py` (TASK-1-9) is the test-only Agent-tool dispatch fake; it simulates Claude Code's `Agent` tool by spawning a subprocess with controlled `EGG_AGENT_ROLE` per dispatch and invokes `hook_entry.decide(...)` via each fake's `pre_tool_use_callback`. **Test infrastructure only** — not registered in `select_substrate`, not a production spawner. -- The test writes the verdict to `.egg-state//r2-verdict.json` as either `{"r2_verdict": "pass"}` or `{"r2_verdict": "fail", "reason": "..."}`. Slice 5's contingent R15 migration task reads this file. - -**What R2 today validates (and what it does not).** Production dispatch under cq-3 remains on `ClaudeCodeSpawner` (the harness re-host model) — `shared/egg_harness/client.py:60-150` uses its own `ToolRegistry.set_permission_callback(...)` and does NOT invoke the PreToolUse hook. R2 therefore validates hook *logic* given accurate `EGG_AGENT_ROLE` propagation; it does **not** validate that Claude Code itself propagates `EGG_AGENT_ROLE` correctly under real nested Agent-tool dispatch (which is verifiable only by running real Claude Code, which the in-sandbox test cannot do). The R2 result becomes load-bearing only if cq-3 flips to Agent-tool dispatch in a future issue. The test docstring documents this limitation. - -**Documented fallback path.** If the R2 verdict is `fail`, slice 5 wires the fallback — **cq-6 option 2 — MCP-validator-side enforcement** combined with R15 model (b) migration: agent-side enforcement at `sandbox/egg_agent_tools/handlers/restrictions.py` re-validates the caller's role + path against `patterns.py`, and every role rubric moves to a real `.claude/agents/.md` definition with frontmatter tool restrictions. This fallback is known to work because egg already ships `check_file_restriction` as an MCP tool today. - -### Subagent context budget regression (R7) - -**The change.** Egg targets `max_turns: 1000` in concurrent execution today (`docs/guides/concurrent-execution.md:97`). Claude Code subagents inherit a smaller context budget from the model's hard limit. Deep refines of large issues may exhaust the subagent context before they finish. - -**What the spike accepts.** cq-10 picked hybrid checkpoint + fork. **The spike implements the lighter half: checkpoints.** Forking a subagent for sub-task delegation is documented but deferred to the follow-up — it is a quality booster, not a correctness requirement. The follow-up issue captures fork-based sub-task delegation explicitly. - -**The fallback if checkpoints prove inadequate.** cq-10 option 2 (accept smaller-than-1000 turn budget; role rubrics and system-prompt depth do most of the work; deep-research breadth is reduced but consistent) is the documented escape path. The spike does not commit to "hybrid works for all role workloads"; it commits to "checkpoints are in the path, the smaller-budget fallback is named, and the follow-up will measure". - -### Interface stability marker (R10) - -The four `Protocol`s in `orchestrator/substrate/` carry a `# v0.x — unstable until ≥3 roles exercise` comment in their module docstrings. The ADR states the interfaces are **explicitly subject to revision** after the follow-up issue extends the substrate to plan / implement / pr roles. Downstream code should treat the interfaces as a moving target until at least three roles run through them end-to-end. - -The risk this manages: a single-role spike does not exercise the interface diversity the second wave needs. The plan's design reviewer reasoned through the interfaces against the full role roster, but design review is not a substitute for end-to-end exercise. Marking the interfaces unstable lets the follow-up iterate without breaking-change ceremony. - -### Cost cap recommendation (REC5) - -**The change.** Today the gateway can rate-limit and per-pipeline cap Anthropic API usage server-side. In the Claude Code substrate, every agent invocation bills directly to the user's Anthropic account / OAuth token. A buggy infinite-loop in a single phase (e.g., a NACK ping-pong) could rack up significant cost before the user notices. - -**Recommended mitigation.** An `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` env var with a conservative default (e.g., 50) — a hard cap on total agent dispatches per pipeline run, configurable upward for legitimate large slice-DAGs. - -**Spike scope.** This spike **does not implement** the cost cap. The recommendation is captured here as ADR-level guidance for the follow-up. The mechanically simple implementation (counter + check + raise) is intentionally deferred to keep the spike scope tight — the follow-up issue lists `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` as one of the deferred items. - -### Subagent type model (R15) - -Claude Code supports two subagent-dispatch models: - -- **Model (a)**: `Agent` tool with `subagent_type="general-purpose"` plus an ad-hoc prompt assembled by the spawner. Tool restrictions rely on PreToolUse hooks + prompt discipline. -- **Model (b)**: `Agent` tool with `subagent_type=""` resolving to a `.claude/agents/.md` file with frontmatter (tool restrictions, model, allowed bash commands). Structural enforcement of tool restrictions per role. - -**The spike (and slice 1 of #2717) picks model (a)** — `subagent_type="general-purpose"` — to match the existing `plugins/refine-plan/skills/refine-plan/SKILL.md` layout. The refine-team role files at `plugins/egg-sdlc/skills/egg-sdlc/agents/{refiner,reviewer_refine,reviewer_agent_design}.md` are prepended to the assembled prompt by the in-process orchestrator's `build_system_prompt(sources)`. - -**Trade-off the ADR records.** Model (a) is simpler to ship (no `.claude/agents/.md` generator needed yet) but pushes tool-restriction enforcement entirely onto the PreToolUse hook (R2) plus prompt discipline. Model (b) gives structural tool restrictions per role but requires building (or vendoring) per-role agent definition files at skill install time. **The migration from (a) to (b) is contingent on the slice-1 R2 verdict** (`.egg-state//r2-verdict.json`): if R2 passes, slice 5 of the #2717 rollout keeps every role on model (a); if R2 fails, slice 5 migrates every rubric to a real `.claude/agents/.md` definition and adds agent-side enforcement at `sandbox/egg_agent_tools/handlers/restrictions.py`. - -## Trust-context note (existing doc cross-reference) - -The integration-test trust-boundary doc (`docs/architecture/integration-test-trust-boundary.md`) distinguishes execution contexts: in-sandbox-agent / trusted-CI-runner / human-operator. The substrate swap shifts most agent execution from "in-sandbox" to "in-parent-Claude-Code-session" — that's a new trust context. - -For the spike, the substrate-parameter regression fixture at `integration_tests/regression/conftest.py` `pytest.skip`s the claude-code dimension when running inside an in-sandbox-agent trust context (detected via the existing env-var heuristic). Both substrate parameters otherwise run pure-Python in-process and do not depend on `egg_stack` / `orchestrator_url` fixtures — no kubectl gate is needed for either dimension. - -The trust-boundary doc itself is not edited by this spike or slice 1 of #2717; the new context is named here for forward reference. The rollout may elevate "in-parent-Claude-Code-session" to a first-class entry in the trust-boundary doc as part of slice 5 hardening. - -## Rollout deltas - -One row per item the original spike (#2623) deferred. Status is updated as each slice of the #2717 rollout lands. The acceptance bar for the rollout is "every row in the **Completed in this rollout** subsection, and every interface tags shift to stable" (see [Acceptance / definition of done](#acceptance--definition-of-done) below). - -### Completed in this rollout - -- [x] **Close the heredoc-HITL bridge gap for refine + plan phases (slice 1).** ~~The spike's `run_pipeline_in_process(...)` generator yielded `HITLDecision` objects without a shipped driver that could ferry them to `AskUserQuestion` and back.~~ Slice 1 lands `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py` (TASK-1-1) — a flattened single-yield stage driver per cq-1 = hybrid (Option C). Each invocation round-trips one `HITLDecision` through `.egg-state/contracts/.json#pending_hitl`. The skill loops over invocations, rendering each decision via `AskUserQuestion` and writing the operator's selection back to the contract. Slice 3 ships the daemon variant for implement-phase concurrency; both variants consume the same `pending_hitl` envelope shape (risk_analyst R17 mitigation). -- [x] **Refine-team expansion (slice 1).** ~~The spike ran the refiner role alone; `reviewer_refine` and `reviewer_agent_design` were k3s-only.~~ Slice 1 adds `plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md` and `agents/reviewer_agent_design.md` (TASK-1-4) so the refine phase now exercises the full refine-team roster on the substrate. The loader at `orchestrator/substrate/__init__.py:232 _load_egg_sdlc_role_rubric` (TASK-1-6) returns the rubric body for the two new reviewers; plan / implement / pr roles still raise `ValueError` with a pointer to the next slice. -- [x] **R2 empirical-question 2-subagent worked example (slice 1).** Slice 1 lands `integration_tests/regression/test_pretooluse_hook_nested.py` (TASK-1-5) + `integration_tests/regression/_agent_tool_fake.py` (TASK-1-9) as the cq-5 early-spike gating test. The test drives the PreToolUse hook through a parent → child Agent-tool dispatch via the test-only fake and writes the verdict to `.egg-state//r2-verdict.json`. See [PreToolUse hook fallback (R2)](#pretooluse-hook-fallback-r2) for the load-bearing-only-when-cq-3-flips qualifier and the slice-5 fallback. - -### Pending in this rollout - -- [ ] **Extend claude-code substrate to plan / implement / pr phases (cq-2 unfinished).** `run_pipeline_in_process(...)` is refine-only today and raises `NotImplementedError` for the other phases. Slice 2 wires the plan phase's role roster (architect / task_planner / risk_analyst + reviewer_plan) and exercises BRC consensus end-to-end on the new substrate. Slice 3 lands implement-phase substrate + daemon HITL bridge. Slice 4 lands pr-phase substrate. -- [ ] **Full conformance matrix across all 5 curated issues (feedback Q1).** Slice 4 wires the substrate-parameter on every `integration_tests/regression/` test that is substrate-portable; classifies each test as portable / k3s-only / claude-code-only. -- [ ] **Set perf / latency budget (feedback Q2).** Slice 5 sets a measured ratio (e.g., "refine phase ≤ Nx k3s latency") and a `pytest.mark.slow` gate for any test that exceeds it. Numbers feed back into the ADR. -- [ ] **Implement the full k3s interface adapter (cq-1 k3s side).** Slice 4 / 5 promote `RedisMessageStore`, the gateway-equivalent policy enforcer, and `gateway/worktree_manager.py` onto the `MessageBus`, `PolicyEnforcer`, and `WorktreeManager` protocols. Removes the cq-11 scope-fence that gates `EGG_SUBSTRATE=k3s` on the legacy seam. -- [ ] **Optional `EggHarnessSpawner` (feedback Q4).** Slice 5. Subprocess-driven `egg_harness` spawner for headless / CLI mode (`egg-orch local-run --issue 1234`). -- [ ] **Ship `egg-state prune` CLI verb (feedback Q6).** Beyond #2717. Local checkpoint and worktree cleanup verb so users running the claude-code substrate can prune `.egg-state//` after pipeline completion. -- [ ] **Fork-based sub-task delegation (cq-10 deferred half).** Slice 5. A refiner whose context fills up forks a child subagent to do a sub-task; the child's summary returns to the parent. -- [ ] **Implement `EGG_PIPELINE_MAX_AGENT_INVOCATIONS` cost cap (REC5).** Slice 5. Pipeline-level cap on total agent dispatches, with a conservative default (e.g., 50). Per-phase cost reporting in the parent session. -- [ ] **Migrate to custom `subagent_type` per-role agent files (R15 model (b)) — contingent on R2.** Slice 5, contingent. If the slice-1 R2 verdict (`.egg-state//r2-verdict.json`) is `pass`, the substrate stays on model (a). If `fail`, slice 5 migrates every role rubric under `plugins/egg-sdlc/skills/egg-sdlc/agents/` to a real `.claude/agents/.md` definition with frontmatter tool restrictions AND adds agent-side enforcement at `sandbox/egg_agent_tools/handlers/restrictions.py` (cq-6 option 2). -- [ ] **Stabilize the four substrate interfaces.** Slice 5. Drop the `# v0.x — unstable until ≥3 roles exercise` marker (R10) once the rolled-out phases have run at least three roles through each interface end-to-end. Document explicit interface-stability criteria. - -## Acceptance / definition of done - -- The Claude Code substrate runs refine + plan + implement + pr against the curated 5 issues. The conformance matrix passes on both substrate dimensions for every substrate-portable test. -- A measured perf / latency budget is in this ADR; a `pytest.mark.slow` gate enforces it. -- The four substrate interfaces have lost their `unstable` marker. -- `EGG_PIPELINE_MAX_AGENT_INVOCATIONS`, `egg-state prune`, and at least one of (`EggHarnessSpawner`, fork-based sub-task delegation) ship as documented. diff --git a/docs/development/STRUCTURE.md b/docs/development/STRUCTURE.md index b5425f0cb7..1a1a3ac9b8 100644 --- a/docs/development/STRUCTURE.md +++ b/docs/development/STRUCTURE.md @@ -139,21 +139,6 @@ orchestrator/ ├── status_reporter.py # Real-time status reporter for collaborators ├── unified_sse.py # Unified SSE stream for all pipelines ├── webhooks.py # GitHub webhook handlers -├── substrate/ # Substrate-swap abstraction layer (walking-skeleton #2623): four Protocol interfaces + env-var-selected factory -│ ├── __init__.py # `select_substrate(env)` factory; reads `EGG_SUBSTRATE` (`k3s` or `claude-code`) -│ ├── spawner.py # `AgentSpawner` protocol + `AgentResult` dataclass -│ ├── message_bus.py # `MessageBus` protocol -│ ├── policy.py # `PolicyEnforcer` protocol -│ ├── worktree.py # `WorktreeManager` protocol -│ ├── k3s_adapter.py # `K3sSpawnerAdapter` shim wrapping `KubernetesSpawner.create_concurrent_spawn_fn` -│ ├── in_process.py # `run_pipeline_in_process()` generator entry point (claude-code substrate only) -│ └── claude_code/ # Claude-Code-native implementations -│ ├── spawner.py # `ClaudeCodeSpawner` — runs `egg_harness.run_agent` in-process (Agent-tool dispatch is a follow-up; see ADR "Open work") -│ ├── hook_entry.py # Standalone `python3 -m` PreToolUse hook script invoked by Claude Code (Bash/Write/Edit parser; fail-closed on ambiguous shapes) -│ ├── message_bus.py # `InProcessMessageBus` — subclasses `MessageStore` (in-memory) -│ ├── policy.py # `PolicyEnforcer` adapter (`check_write` + `install`) wrapping the `hook_entry.py` script -│ ├── settings.template.json # Claude Code settings template registering `hook_entry.py` as the PreToolUse hook -│ └── worktree.py # `LocalWorktreeManager` — per-agent worktrees under `~/.egg-worktrees/` ├── overseer/ # Overseer agent package (LLM-powered tier of pipeline health monitoring) │ ├── classifier.py # Haiku-tier classifiers (stall, loop, error triage, off-track detection) │ ├── decision_maker.py # Sonnet/Opus-tier decision-maker (corrective actions, redirect messages) diff --git a/docs/index.md b/docs/index.md index 63821ea150..cdf829dd31 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,6 @@ This index helps both humans and LLMs navigate the documentation efficiently. | [The Agentic Feedback Loop](architecture/agentic-feedback-loop.md) | The foundational work-review-feedback cycle that drives quality | | [Why egg Works](architecture/collaboration-effectiveness.md) | How the public, sandboxed, async model delivers safety and collaboration | | [Integration-Test Trust Boundary](architecture/integration-test-trust-boundary.md) | Test execution contexts (in-sandbox-agent / trusted-CI-runner / human-operator) and fixture tiers; authoritative reference for plan-phase Trust-Boundary Audit (#2594) | -| [Claude Code Substrate](architecture/claude-code-substrate.md) | Substrate-swap walking-skeleton (#2623): four `Protocol`s (`AgentSpawner` / `MessageBus` / `PolicyEnforcer` / `WorktreeManager`) under `orchestrator/substrate/`, env-var-selected via `EGG_SUBSTRATE`, with Claude-Code-native implementations and an in-process orchestrator generator (`run_pipeline_in_process`) | ### Development diff --git a/integration_tests/regression/_agent_tool_fake.py b/integration_tests/regression/_agent_tool_fake.py deleted file mode 100644 index 33d9549439..0000000000 --- a/integration_tests/regression/_agent_tool_fake.py +++ /dev/null @@ -1,451 +0,0 @@ -"""Test-only nested-Agent-tool dispatch fake (#2717 TASK-1-9). - -Simulates Claude Code's ``Agent`` tool by spawning a subprocess with a -controlled ``EGG_AGENT_ROLE`` env var per dispatch. Each fake-subagent -exposes a ``pre_tool_use_callback`` that invokes -``orchestrator.substrate.claude_code.hook_entry.decide(...)`` with the -tool input — the same code path Claude Code's PreToolUse hook would -follow in real Agent-tool dispatch. The fake exists so TASK-1-5 (R2 -spike: ``test_pretooluse_hook_nested.py``) can drive a deterministic -nested-dispatch scenario without standing up a real Claude Code -session — there isn't one available in the in-sandbox-agent -trust context, and the production -``orchestrator/substrate/claude_code/spawner.py`` is a harness re-host -that bypasses the PreToolUse hook by design (cq-3: the harness uses -its own ``ToolRegistry.set_permission_callback(...)``; the hook is NOT -in its tool-call loop). - -**This is TEST INFRASTRUCTURE ONLY.** It is NOT a production spawner -and is NOT registered in ``orchestrator.substrate.select_substrate``; -the import guard at the bottom of the module rejects any production -caller. Production dispatch stays on ``ClaudeCodeSpawner`` per cq-3 -("decide empirically post-implement") — the empirical-answer half of -the R2 question (does Claude Code itself propagate ``EGG_AGENT_ROLE`` -correctly under real nested dispatch?) becomes load-bearing only when -cq-3 flips to Agent-tool dispatch in a future issue. - -What R2 validates with this fake --------------------------------- - -R2 (issue #2623) asks: when a parent agent dispatches a child agent -via the Agent tool, does the PreToolUse hook resolve the *child's* -role correctly, so a write that violates the child's allow-list is -denied even when the parent's role would allow it? - -This fake's ``dispatch(parent_role, child_role, write_target)`` helper -answers the **hook-logic half** of R2: it spawns a child subprocess -with ``EGG_AGENT_ROLE=`` (mirroring what Claude Code's -Agent tool would set), feeds a Write tool input to -``hook_entry.decide(...)`` inside that subprocess, and returns the -verdict. The hook reads the env-set role; the fake validates that -the resulting verdict matches the *child's* allow-list (not the -parent's). The remaining empirical half — "does Claude Code itself -set ``EGG_AGENT_ROLE`` correctly under nested dispatch?" — is -verifiable only from inside a real Claude Code session, which the -in-sandbox-agent test context cannot provide. The R2 spike's test -docstring documents this empirical-vs-test-fake limitation. - -State-serialization contract ----------------------------- - -For risk_analyst R17 mitigation: the same ``pending_hitl`` envelope -schema invented by the flattened-bridge driver -(``plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py`` — -``PENDING_HITL_SCHEMA_VERSION`` constant) flows through this fake. A -parent fake-subagent can write a decision envelope through the same -contract-file path the production driver uses, and the slice-3 -daemon variant -(``orchestrator/substrate/claude_code/hitl_daemon.py`` — TASK-3-2) -inherits the same envelope schema. The fake re-exports -``PENDING_HITL_SCHEMA_VERSION`` so tests can pin the version they -assert against without re-deriving the constant. -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from collections.abc import Mapping -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -# --------------------------------------------------------------------------- -# Import guard: refuse to be imported by anything that isn't a test module. -# Tests under ``integration_tests/regression/`` (the only intended caller), -# and the colocated TASK-1-5 test that re-exports the fake via direct -# attribute access. We use ``__name__`` because the import system has not -# yet set ``__package__`` reliably for test discovery shapes; both -# ``integration_tests.regression._agent_tool_fake`` and the bare -# ``_agent_tool_fake`` shapes are accepted. -# --------------------------------------------------------------------------- -_ALLOWED_MODULE_PREFIXES: tuple[str, ...] = ( - "integration_tests", # collected via the test-tree path - "_agent_tool_fake", # bare path when conftest's sys.path injection lands - "__main__", # smoke-run via ``python3 -m`` -) -if not any(__name__.startswith(prefix) for prefix in _ALLOWED_MODULE_PREFIXES): - raise ImportError( - "_agent_tool_fake.py is test infrastructure only — it must not be " - "imported by production code. Import path " - f"{__name__!r} did not start with any of " - f"{_ALLOWED_MODULE_PREFIXES!r}. See the module docstring for why " - "this guard exists (cq-3: production stays on the harness re-host)." - ) - - -# Re-export so test bodies can pin the version they expect without -# re-deriving the constant. -try: - # When the egg-sdlc skill ships alongside the source tree, the - # driver's module is importable via a path-walk. - _SKILL_BIN_DIR = ( - Path(__file__).resolve().parent.parent.parent - / "plugins" - / "egg-sdlc" - / "skills" - / "egg-sdlc" - / "bin" - ) - sys.path.insert(0, str(_SKILL_BIN_DIR)) - try: - from run_pipeline import ( # type: ignore[import-not-found,import-untyped,unused-ignore] - PENDING_HITL_SCHEMA_VERSION, - ) - finally: - try: - sys.path.remove(str(_SKILL_BIN_DIR)) - except ValueError: # pragma: no cover — defensive - pass -except ImportError: # pragma: no cover — defensive - # Hard-code the version so tests can still import even if the - # skill bin isn't present (uncommon — but the fake should not - # implode if the driver is in flight). - PENDING_HITL_SCHEMA_VERSION = 1 - - -# --------------------------------------------------------------------------- -# Public dataclass surface -# --------------------------------------------------------------------------- - - -@dataclass(frozen=True) -class DispatchResult: - """Outcome of a single nested fake-Agent-tool dispatch. - - Attributes: - parent_role: The ``EGG_AGENT_ROLE`` of the parent fake-subagent. - child_role: The ``EGG_AGENT_ROLE`` of the child fake-subagent. - write_target: The repo-relative path the child attempted to write. - decision: The dict returned by ``hook_entry.decide(...)``. Empty - dict means "allow"; ``{"decision": "block", "reason": "..."}`` - means deny. - denied: Convenience boolean — True iff ``decision["decision"]`` - is ``"block"``. - deny_reason: The block reason text (empty when ``denied`` is - False). - child_pid: PID of the child subprocess (for debugging). - child_exit_code: Exit code of the child subprocess (0 when the - child ran ``decide`` to completion; nonzero when the - subprocess hit an internal error). - stderr: Captured stderr from the child subprocess. - """ - - parent_role: str - child_role: str - write_target: str - decision: dict[str, Any] - denied: bool - deny_reason: str - child_pid: int - child_exit_code: int - stderr: str - - -# --------------------------------------------------------------------------- -# Child-subprocess entry — invoked via ``python3 -m`` from the parent. -# --------------------------------------------------------------------------- - - -def _child_main(argv: list[str]) -> int: - """Child-subprocess entry point. - - Reads ``stdin_blob`` (the simulated Claude Code PreToolUse stdin) - from ``argv[1]`` (path to a JSON file written by the parent), - invokes ``hook_entry.decide(...)``, prints the resulting verdict to - stdout as JSON, and exits 0. - - The child process is intentionally minimal: it imports - ``orchestrator.substrate.claude_code.hook_entry`` and calls - ``decide(stdin_blob)`` directly — no Claude Code session, no Agent - tool, no harness. The relevant input to ``decide`` is the env - (``EGG_AGENT_ROLE`` set by the parent on the subprocess) and the - JSON blob (the simulated tool input). What we are validating in - the test is the hook *logic*: given accurate env propagation, does - the hook deny the right writes? - """ - if len(argv) < 2: - print("_agent_tool_fake child: missing stdin-blob path argv[1]", file=sys.stderr) - return 2 - blob_path = Path(argv[1]) - try: - stdin_blob = json.loads(blob_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - print(f"_agent_tool_fake child: cannot read stdin blob: {exc}", file=sys.stderr) - return 2 - - # Lazy import inside the child so an environment without the - # orchestrator package surfaces the ImportError to the parent - # (where it is converted into a structured DispatchResult). - try: - from orchestrator.substrate.claude_code.hook_entry import decide - except ImportError as exc: - print( - f"_agent_tool_fake child: cannot import hook_entry.decide: {exc}", - file=sys.stderr, - ) - return 3 - - verdict = decide(stdin_blob) - json.dump(verdict if isinstance(verdict, dict) else {}, sys.stdout) - sys.stdout.flush() - return 0 - - -# --------------------------------------------------------------------------- -# Parent-side public helpers -# --------------------------------------------------------------------------- - - -def pre_tool_use_callback( - role: str, - tool_name: str, - tool_input: Mapping[str, Any], - *, - extra_env: Mapping[str, str] | None = None, - python_executable: str | None = None, - timeout: float = 10.0, -) -> dict[str, Any]: - """Invoke ``hook_entry.decide(...)`` as if it were the PreToolUse - callback for a child fake-subagent running with ``EGG_AGENT_ROLE=role``. - - The fake spawns a fresh ``python3 -m integration_tests.regression - ._agent_tool_fake `` subprocess with the env - isolated to a controlled set — mirroring what Claude Code's Agent - tool would set when invoking a child subagent. Returns the - verdict dict (empty == allow; ``{"decision": "block", "reason": - "..."}`` == deny). - - Args: - role: ``EGG_AGENT_ROLE`` to set on the child subprocess. - tool_name: Name Claude Code would pass on stdin (``"Write"``, - ``"Edit"``, ``"Bash"``, etc.). - tool_input: Tool input dict Claude Code would pass on stdin. - extra_env: Optional additional env vars to merge into the - child subprocess (useful for ``EGG_REPO_ROOT`` etc.). - python_executable: Optional override for the Python - interpreter; defaults to ``sys.executable``. - timeout: Wall-clock cap on the child subprocess (seconds). - """ - py = python_executable or sys.executable - blob = {"tool_name": tool_name, "tool_input": dict(tool_input)} - - # Write the stdin blob to a temp file. We avoid pipes-to-stdin - # here so the child can be invoked with ``python3 -m`` cleanly — - # the ``-m`` invocation expects argv-driven input. The path is - # unlinked at the end of the function. - import tempfile - - blob_fd, blob_path = tempfile.mkstemp(prefix="agent_tool_fake_", suffix=".json") - try: - with os.fdopen(blob_fd, "w", encoding="utf-8") as fp: - json.dump(blob, fp) - - env = {**os.environ, "EGG_AGENT_ROLE": role} - if extra_env: - env.update(extra_env) - - # Repo-root resolution — the test typically passes a tmp_path - # via ``extra_env={"EGG_REPO_ROOT": ...}``; if it doesn't, the - # hook treats writes as "outside any repo root" and the - # symlink-resolution branch is skipped, which is fine for the - # role-routing-under-nested-dispatch question R2 asks. - - # Locate this module's path so the child can ``-m`` it. We - # prefer the package-qualified shape so the child's sys.path - # mirrors the test runner's. - module_name = ( - "integration_tests.regression._agent_tool_fake" - if __name__.startswith("integration_tests") - else "_agent_tool_fake" - ) - - # Ensure the repo root is on PYTHONPATH so the child can - # resolve both the orchestrator package and this fake module. - repo_root = Path(__file__).resolve().parent.parent.parent - pythonpath = os.pathsep.join( - p for p in (str(repo_root), str(repo_root / "shared"), env.get("PYTHONPATH")) if p - ) - env["PYTHONPATH"] = pythonpath - - completed = subprocess.run( - [py, "-m", module_name, blob_path], - env=env, - capture_output=True, - text=True, - timeout=timeout, - check=False, - cwd=str(repo_root), - ) - if completed.returncode != 0: - # Surface a structured deny that names the failure so the - # test sees an actionable diagnostic rather than a silent - # allow. - return { - "decision": "block", - "reason": ( - f"_agent_tool_fake child exited {completed.returncode}; " - f"stderr={completed.stderr.strip()!r}" - ), - "_fake_child_exit_code": completed.returncode, - "_fake_child_stderr": completed.stderr, - } - try: - verdict = json.loads(completed.stdout) if completed.stdout.strip() else {} - except json.JSONDecodeError: - return { - "decision": "block", - "reason": ( - f"_agent_tool_fake child produced non-JSON stdout: {completed.stdout!r}" - ), - "_fake_child_exit_code": completed.returncode, - "_fake_child_stderr": completed.stderr, - } - if not isinstance(verdict, dict): - verdict = {} - return verdict - finally: - try: - Path(blob_path).unlink(missing_ok=True) - except OSError: # pragma: no cover — defensive - pass - - -def dispatch( - parent_role: str, - child_role: str, - write_target: str, - *, - tool_name: str = "Write", - extra_env: Mapping[str, str] | None = None, - repo_root: str | os.PathLike[str] | None = None, -) -> DispatchResult: - """Simulate a parent fake-subagent dispatching a child fake-subagent - that attempts to write ``write_target``. - - The parent fake-subagent is *implicit* — only ``parent_role`` is - recorded; the fake does not spawn a parent subprocess because the - R2 question is about whether the **child's** role is correctly - resolved by the PreToolUse hook. A real Agent-tool dispatch would - set ``EGG_AGENT_ROLE`` to the child's role on the child subagent's - process; the fake mirrors that exactly. - - Args: - parent_role: The parent fake-subagent's role. Recorded for - audit / observability; the parent subprocess is not - spawned because role-routing happens at the child boundary. - child_role: The child fake-subagent's role — set as - ``EGG_AGENT_ROLE`` on the child subprocess so - ``hook_entry.decide(...)`` resolves it. - write_target: The repo-relative path the child attempts to - write. The fake constructs a Write tool input with - ``file_path=write_target``. - tool_name: Tool name to put on stdin (``"Write"`` by default; - tests can override to ``"Edit"`` / ``"Bash"`` to exercise - other branches of the hook's path-extractor). - extra_env: Optional extra env vars for the child subprocess. - repo_root: Optional ``EGG_REPO_ROOT`` to set on the child. Most - R2 tests pass a tmp_path so the hook's repo-relative - resolver behaves deterministically. - - Returns: - A ``DispatchResult`` with the structured outcome. - """ - # Construct the tool input shape Claude Code would pass on - # PreToolUse stdin for a Write call. - tool_input: dict[str, Any] = {"file_path": write_target} - if tool_name == "Bash": - # Bash uses ``command`` instead of ``file_path``; tests - # exercising the Bash branch can pass a command shape directly. - tool_input = {"command": write_target} - - env_extra: dict[str, str] = dict(extra_env or {}) - if repo_root is not None: - env_extra.setdefault("EGG_REPO_ROOT", str(repo_root)) - env_extra.setdefault("EGG_WORKTREE_ROOT", str(repo_root)) - - verdict = pre_tool_use_callback( - child_role, - tool_name, - tool_input, - extra_env=env_extra, - ) - denied = bool(verdict.get("decision") == "block") - deny_reason = str(verdict.get("reason") or "") if denied else "" - return DispatchResult( - parent_role=parent_role, - child_role=child_role, - write_target=write_target, - decision=verdict, - denied=denied, - deny_reason=deny_reason, - child_pid=int(verdict.get("_fake_child_pid", 0) or 0), - child_exit_code=int(verdict.get("_fake_child_exit_code", 0) or 0), - stderr=str(verdict.get("_fake_child_stderr", "") or ""), - ) - - -# --------------------------------------------------------------------------- -# pending_hitl envelope helpers — slice-3 daemon shares this contract. -# --------------------------------------------------------------------------- - - -def build_pending_hitl_envelope( - pipeline_id: str, - *, - decision: dict[str, Any] | None = None, - answer: Any = None, - status: str = "pending", -) -> dict[str, Any]: - """Construct a ``pending_hitl`` envelope dict matching the schema - invented in TASK-1-1. - - Helper for tests that want to round-trip an envelope through the - fake without re-deriving the field set. The slice-3 daemon - (TASK-3-2) is expected to accept envelopes built by this helper. - """ - from datetime import UTC, datetime - - return { - "version": PENDING_HITL_SCHEMA_VERSION, - "pipeline_id": pipeline_id, - "timestamp": datetime.now(UTC).isoformat(), - "decision": decision, - "answer": answer, - "status": status, - "result": None, - "error": None, - "answer_log": [], - } - - -# --------------------------------------------------------------------------- -# Module entry point (for ``python3 -m integration_tests.regression -# ._agent_tool_fake `` invocations by the parent helper). -# --------------------------------------------------------------------------- - - -if __name__ == "__main__": - sys.exit(_child_main(sys.argv)) diff --git a/integration_tests/regression/conftest.py b/integration_tests/regression/conftest.py index ceed06ba31..3bb83eabfd 100644 --- a/integration_tests/regression/conftest.py +++ b/integration_tests/regression/conftest.py @@ -280,7 +280,7 @@ def lifecycle_secret() -> str | None: timeout=15, check=False, ) - except (OSError, subprocess.TimeoutExpired): # fmt: skip + except OSError, subprocess.TimeoutExpired: return None if result.returncode != 0 or not result.stdout: return None @@ -290,7 +290,7 @@ def lifecycle_secret() -> str | None: # a ``\n`` inside ``f"Bearer {secret}"`` is rejected by # ``http.client.putheader``. return base64.b64decode(result.stdout).decode("utf-8").strip() - except (ValueError, UnicodeDecodeError): # fmt: skip + except ValueError, UnicodeDecodeError: return None @@ -588,60 +588,6 @@ def advisory_blocker_graph() -> ReviewGraph: ) -# --------------------------------------------------------------------------- -# Substrate parametrization (#2623 — Claude Code substrate spike, slice-1) -# --------------------------------------------------------------------------- -# -# The substrate fixture parametrizes a test across the two substrate -# implementations: ``"k3s"`` (the existing k3s-native stack) and -# ``"claude-code"`` (the in-process Claude Code substrate from #2623). -# Both legs run pure-Python in-process — neither dimension shells out -# to ``kubectl`` or spawns a real subagent. The k3s leg uses -# ``K3sSpawnerAdapter`` with a mocked ``create_concurrent_spawn_fn``; -# the claude-code leg uses ``ClaudeCodeSpawner`` with the Claude Code -# Agent-tool dispatch monkey-patched (the parent session is what would -# normally provide it). -# -# The claude-code dimension is skipped when running inside an -# in-sandbox-agent trust context (``EGG_AGENT_ROLE`` set), because the -# spike is for orchestrator-side execution, not for agents recursively -# spawning agents. Pinned per task-1-8 acceptance criterion. - - -def _in_sandbox_agent_context() -> bool: - """Return True when the test runner is itself an egg sandbox agent. - - The orchestrator sets ``EGG_AGENT_ROLE`` on every spawned agent - container; tests running inside that context should skip the - claude-code substrate dimension because the in-process substrate is - only meant to run from the parent Claude Code session, not from a - sandboxed agent that's already been dispatched. - """ - return bool(os.environ.get("EGG_AGENT_ROLE")) - - -@pytest.fixture(params=["k3s", "claude-code"]) -def substrate(request: pytest.FixtureRequest) -> str: - """Parametrize a test over both substrate implementations. - - Tests taking this fixture run twice (once per dimension). The - claude-code dimension is skipped inside an egg sandbox-agent trust - context (``EGG_AGENT_ROLE`` set) per task-1-8 acceptance criterion. - - The fixture returns the substrate name string. The test body is - expected to call ``select_substrate({"EGG_SUBSTRATE": substrate})`` - (from ``orchestrator/substrate/__init__.py``) to obtain the wired - bundle and exercise it. - """ - if request.param == "claude-code" and _in_sandbox_agent_context(): - pytest.skip( - "claude-code substrate dimension skipped inside an egg " - "sandbox-agent context (EGG_AGENT_ROLE set) — the in-process " - "substrate is for orchestrator-side execution only" - ) - return request.param - - __all__ = [ # HITL HTTP round-trip helpers (#2474, #2634). "deterministic_pipeline_id", @@ -667,6 +613,4 @@ def substrate(request: pytest.FixtureRequest) -> str: "filter_events", "single_reviewer_graph", "two_reviewer_graph", - # Substrate parametrization (#2623 slice-1). - "substrate", ] diff --git a/integration_tests/regression/test_bridge_flattened_round_trip.py b/integration_tests/regression/test_bridge_flattened_round_trip.py deleted file mode 100644 index 4d5ba6e5ea..0000000000 --- a/integration_tests/regression/test_bridge_flattened_round_trip.py +++ /dev/null @@ -1,468 +0,0 @@ -"""Regression test for the flattened bridge driver (#2717 slice-1 task-1-3). - -This pins the cq-7 / R17 walking-skeleton-bridge contract for Option B -(see ``plugins/egg-sdlc/skills/egg-sdlc/SKILL.md`` "Walking-skeleton -bridge gap" callout, picked over option (a)'s daemon variant). The -flattened driver runs in a fresh Python process per stage and the -``pending_hitl`` envelope in ``.egg-state/contracts/.json`` is the -ONLY surviving state between invocations — generator object, ``gi_frame``, -background threads, and Python heap all die at process exit. - -Acceptance criteria covered (per contract task-1-3): - -* (a) The first invocation's ``pending_hitl.decision.question`` matches - the preflight question the in-process generator yields on its first - ``next()``. -* (b) After the test writes the operator's answer to - ``pending_hitl.answer``, the second invocation consumes that answer - and yields the refine-gate decision instead. - -The test runs in <30s (per the AC's runtime cap) — guarded with -``pytest.mark.timeout(30)`` so a regression that hangs the driver -(e.g. ``generator.send()`` deadlock, background-thread non-join, blocking -substrate call) fails loudly rather than wedging CI. - -Substrate isolation -------------------- -``run_pipeline_in_process`` is generator-shaped: each yield is an -``HITLDecision``, and between the preflight and refine-gate yields the -generator dispatches the refiner via ``select_substrate(env).spawner``. -A real spawn would invoke Claude Code's Agent tool (and thus the -Anthropic API), which the acceptance criterion forbids. - -To keep the subprocess hermetic we ship a small shim through ``-c`` that: - -1. Monkey-patches ``orchestrator.substrate.select_substrate`` to return a - ``MagicMock`` bundle whose ``spawner.spawn`` returns a synthetic - ``AgentResult`` (``exit_code=0``, ``commit_sha=<40 zeros>``, - ``stdout="ok"``) — the same pattern used by the in-process unit tests - in ``shared/tests/test_run_pipeline_in_process*.py``. -2. Shrinks the background-thread intervals so the test does not block on - the default 5-second heartbeat tick. -3. Hands control to the real ``bin/run_pipeline.py`` driver via - ``runpy.run_path(...)`` so the test exercises the production driver, - not a re-implemented stand-in. - -This is the standard pattern for testing CLI scripts that need a fake -substrate while exercising the real driver — no test-only flag added -to ``run_pipeline.py`` itself. - -Driver invocation contract probed ---------------------------------- -The coder's driver (task-1-1) accepts the pipeline id as a positional -``argv[1]``; the shim passes it that way. ``EGG_PIPELINE_ID`` is also -set so that any downstream tool reading the env (e.g. the orchestrator -heartbeat machinery) sees the same id. The driver's CWD is set to -``tmp_path`` so the ``.egg-state/contracts/.json`` path resolves -cleanly. -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -import textwrap -from pathlib import Path - -import pytest - -pytestmark = pytest.mark.integration - - -# --------------------------------------------------------------------------- -# Constants — keep aligned with the driver's expected invocation contract -# --------------------------------------------------------------------------- - - -#: Path the coder commits the driver to per task-1-1 acceptance. -_DRIVER_PATH = Path("plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py") - - -#: Pipeline id used throughout the test. Deterministic so re-runs are -#: idempotent and don't fan into the orchestrator's id-space. -_PIPELINE_ID = "issue-bridge-round-trip" - - -#: The preflight question the in-process generator yields on its first -#: ``next()``. Source: ``orchestrator.substrate.in_process._build_preflight_decision``. -_PREFLIGHT_QUESTION = "Confirm the refiner will run against this repo + issue?" - - -# --------------------------------------------------------------------------- -# Shim — fake substrate + run the real driver via runpy -# --------------------------------------------------------------------------- - - -def _shim_source() -> str: - """Subprocess shim source: patch substrate, then ``runpy`` the driver. - - Kept as a string so the test owns its own contract — no test-only - code lives under ``plugins/egg-sdlc/`` or ``orchestrator/``. - - The substrate fake mirrors the existing ``fake_bundle`` fixture in - ``shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py`` so - a behaviour drift between unit and integration coverage is caught. - """ - return textwrap.dedent( - """ - import os, sys, runpy - from unittest.mock import MagicMock - from pathlib import Path - - # ----- Fake substrate bundle (no real Claude Code spawn) ----- - import orchestrator.substrate as _sub - from orchestrator.substrate import in_process as _ip - - _wt = Path(os.environ.get('EGG_STATE_DIR', '.')) / 'wt' - _wt.mkdir(parents=True, exist_ok=True) - - _bundle = MagicMock() - _bundle.spawner.spawn = MagicMock(return_value=MagicMock( - exit_code=0, - commit_sha='0' * 40, - stdout='ok', - worktree=_wt, - artifacts=[], - )) - _bundle.worktrees.create = MagicMock(return_value=_wt) - _bundle.worktrees.tear_down = MagicMock() - _bundle.name = 'claude-code' - _sub.select_substrate = lambda env=None, **kw: _bundle - - # Shrink heartbeat / brc-review / bus intervals so the - # generator doesn't block on the default 5s tick during a - # subprocess test. - _ip._HEARTBEAT_INTERVAL = 0.05 - _ip._BRC_REVIEW_INTERVAL = 0.05 - _ip._BUS_TICK_INTERVAL = 0.05 - - # ----- Now run the real driver ----- - _driver = os.environ['EGG_TEST_DRIVER_PATH'] - sys.argv = [_driver, os.environ['EGG_PIPELINE_ID']] - runpy.run_path(_driver, run_name='__main__') - """ - ) - - -def _invoke_driver( - *, - state_dir: Path, - pipeline_id: str, - repo_root: Path, -) -> subprocess.CompletedProcess[str]: - """Spawn the driver in a fresh Python process. - - Returns the ``CompletedProcess`` so callers can assert on exit code - + stderr. The driver's CWD is ``state_dir`` so any cwd-relative - ``.egg-state/`` path it computes lands in the test's tmp tree. - """ - driver_path = (repo_root / _DRIVER_PATH).resolve() - env = { - **os.environ, - "EGG_PIPELINE_ID": pipeline_id, - "EGG_STATE_DIR": str(state_dir), - "EGG_SUBSTRATE": "claude-code", - "EGG_TEST_DRIVER_PATH": str(driver_path), - # Subprocess PYTHONPATH must let every transitive import the - # driver triggers resolve. The Makefile's - # ``PYTHONPATH := shared:gateway:orchestrator`` (test target, - # cwd-relative) is the source of truth; we mirror it with - # absolute paths because the subprocess's CWD is the per-test - # tmp dir. Each entry covers a distinct import shape: - # * ``/shared`` — ``egg_contracts`` etc. (imported - # transitively by ``orchestrator.substrate.k3s_adapter``). - # * ```` — the ``orchestrator`` package itself - # (``orchestrator/__init__.py`` makes it a real package). - # * ``/orchestrator`` — bare-name top-level imports - # internal to the ``orchestrator/`` tree, e.g. - # ``orchestrator/models.py:16`` does - # ``from slice_id_validation import SLICE_ID_PATTERN`` and - # ``in_process.py:531-534`` has a bare ``from models import - # HITLDecision`` fallback. Without ``/orchestrator`` - # on PYTHONPATH these crash the subprocess before the - # bridge driver yields its first HITL decision. - # * ``/gateway`` — matches the Makefile shape. - "PYTHONPATH": os.pathsep.join( - [ - str(repo_root / "shared"), - str(repo_root), - str(repo_root / "orchestrator"), - str(repo_root / "gateway"), - os.environ.get("PYTHONPATH", ""), - ] - ).rstrip(os.pathsep), - } - return subprocess.run( - [sys.executable, "-c", _shim_source()], - capture_output=True, - text=True, - cwd=str(state_dir), - env=env, - # subprocess-level timeout: a stuck driver should not eat the - # full pytest-timeout budget on its own. - timeout=20, - ) - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _repo_root() -> Path: - """Resolve the repo root from this test file's location.""" - return Path(__file__).resolve().parents[2] - - -def _read_contract(state_dir: Path, pipeline_id: str) -> dict: - contract_path = state_dir / ".egg-state" / "contracts" / f"{pipeline_id}.json" - assert contract_path.exists(), ( - f"driver must write the contract to {contract_path} — " - f"directory contains: {sorted((state_dir / '.egg-state').rglob('*'))}" - ) - return json.loads(contract_path.read_text()) - - -_WRITE_ANSWER_HELPER = Path("plugins/egg-sdlc/skills/egg-sdlc/bin/write_answer.py") - - -def _write_answer(state_dir: Path, pipeline_id: str, answer: str) -> None: - """Write the operator's answer + ``status=answered`` via ``write_answer.py``. - - The skill loop documented in ``SKILL.md`` ferries the operator's - selection from ``AskUserQuestion`` into ``pending_hitl.answer`` by - invoking ``bin/write_answer.py --answer-string "${ANSWER}"``. To - cover the *end-to-end* bridge contract — driver writes envelope, - helper writes answer, driver consumes answer on the next call — - this test exercises the helper through the same subprocess shape - the skill body uses. A regression in ``write_answer.py`` - (timestamp drift, atomic-write breakage, status-flip omission) - would otherwise slip past this integration test because the - helper's unit tests live in ``shared/tests/test_write_answer.py`` - while the bridge test could fabricate the envelope by hand. - """ - repo_root = _repo_root() - helper_path = (repo_root / _WRITE_ANSWER_HELPER).resolve() - env = { - **os.environ, - # Same PYTHONPATH shape as the driver subprocess — the helper - # is dependency-free today, but keeping the paths consistent - # means a future helper that imports egg modules won't fail - # only in this test path. - "PYTHONPATH": os.pathsep.join( - [ - str(repo_root / "shared"), - str(repo_root), - str(repo_root / "orchestrator"), - str(repo_root / "gateway"), - os.environ.get("PYTHONPATH", ""), - ] - ).rstrip(os.pathsep), - } - proc = subprocess.run( - [ - sys.executable, - str(helper_path), - "--pipeline-id", - pipeline_id, - "--state-root", - str(state_dir / ".egg-state"), - "--answer-string", - answer, - ], - capture_output=True, - text=True, - cwd=str(state_dir), - env=env, - timeout=10, - ) - assert proc.returncode == 0, ( - f"write_answer.py must exit 0 when ferrying a valid answer; " - f"stdout={proc.stdout[-500:]!r} stderr={proc.stderr[-500:]!r}" - ) - # Sanity-check the helper's invariants from the integration vantage - # point: status flipped, answer round-tripped, timestamp matches the - # driver's format (no trailing ``Z``). - contract_file = state_dir / ".egg-state" / "contracts" / f"{pipeline_id}.json" - blob = json.loads(contract_file.read_text()) - pending = blob.get("pending_hitl") or {} - assert pending.get("status") == "answered", ( - f"write_answer.py must set pending_hitl.status='answered'; got {pending.get('status')!r}" - ) - assert pending.get("answer") == answer, ( - f"write_answer.py must JSON-encode the raw answer and round-trip " - f"it cleanly; got {pending.get('answer')!r}" - ) - timestamp = pending.get("timestamp", "") - assert isinstance(timestamp, str) and timestamp.endswith("+00:00"), ( - f"write_answer.py timestamp must match the driver's _now_iso " - f"(ends with '+00:00', no trailing 'Z'); got {timestamp!r}" - ) - - -# --------------------------------------------------------------------------- -# Test — full two-stage round trip -# --------------------------------------------------------------------------- - - -@pytest.mark.skipif( - not (_repo_root() / _DRIVER_PATH).exists(), - reason=( - f"{_DRIVER_PATH} not present — task-1-1 (coder) has not landed " - f"yet. This is the upstream dependency for the round-trip test." - ), -) -def test_bridge_flattened_round_trip(tmp_path: Path) -> None: - """Full round-trip: process exit, operator answers, process re-entry. - - Stage A — first invocation: - * Contract starts with no ``pending_hitl.answer``. - * Driver advances the generator to its first yield (preflight). - * Driver writes the yielded ``HITLDecision`` to - ``pending_hitl.decision`` and exits ``0``. - * Test asserts ``pending_hitl.decision.question`` matches the - preflight question (AC bullet (a)). - - Stage B — second invocation (after the operator answers): - * Test writes ``"approve"`` to ``pending_hitl.answer``. - * Driver re-reads the contract, drives the generator past the - preflight yield via ``generator.send("approve")``, lands on - the next yield (the refine-gate decision), writes that to - ``pending_hitl.decision`` and exits ``0``. - * Test asserts ``pending_hitl.decision`` is **different** from - the preflight decision (AC bullet (b)) — the round-trip - actually advanced the state machine. - """ - repo_root = _repo_root() - - # ----- Stage A: first invocation ----- - proc1 = _invoke_driver( - state_dir=tmp_path, - pipeline_id=_PIPELINE_ID, - repo_root=repo_root, - ) - assert proc1.returncode == 0, ( - f"first invocation must exit 0 on generator yield (AC: " - f"'exits with status 0 when the generator yields'). " - f"stdout={proc1.stdout[-1000:]!r} stderr={proc1.stderr[-1000:]!r}" - ) - - contract1 = _read_contract(tmp_path, _PIPELINE_ID) - pending1 = contract1.get("pending_hitl") - assert pending1, ( - f"driver must write a ``pending_hitl`` envelope to the contract " - f"after the first yield (task-1-1 schema). contract keys: " - f"{sorted(contract1.keys())}" - ) - decision1 = pending1.get("decision") - assert decision1, ( - f"``pending_hitl.decision`` must be populated after the first yield. " - f"pending_hitl={pending1!r}" - ) - assert decision1.get("question") == _PREFLIGHT_QUESTION, ( - f"AC bullet (a): first yield must be the preflight question " - f"{_PREFLIGHT_QUESTION!r}; got {decision1.get('question')!r}" - ) - # task-1-1 schema fields: decision, answer, version, pipeline_id, - # timestamp. Pin the load-bearing ones so a drift surfaces clearly. - assert pending1.get("pipeline_id") == _PIPELINE_ID, ( - f"``pending_hitl.pipeline_id`` must round-trip the requested id; " - f"got {pending1.get('pipeline_id')!r}" - ) - assert "version" in pending1, ( - f"``pending_hitl.version`` is part of the stable schema (task-1-1) " - f"so the daemon variant in TASK-3-2 can co-evolve; missing from " - f"envelope {pending1!r}" - ) - assert "timestamp" in pending1, ( - f"``pending_hitl.timestamp`` is part of the stable schema (task-1-1); " - f"missing from envelope {pending1!r}" - ) - - # ----- Stage B: write answer, re-invoke ----- - _write_answer(tmp_path, _PIPELINE_ID, answer="approve") - proc2 = _invoke_driver( - state_dir=tmp_path, - pipeline_id=_PIPELINE_ID, - repo_root=repo_root, - ) - assert proc2.returncode == 0, ( - f"second invocation must exit 0 on generator yield. " - f"stdout={proc2.stdout[-1000:]!r} stderr={proc2.stderr[-1000:]!r}" - ) - - contract2 = _read_contract(tmp_path, _PIPELINE_ID) - pending2 = contract2.get("pending_hitl") or {} - decision2 = pending2.get("decision") - assert decision2, ( - f"``pending_hitl.decision`` must be re-populated with the next " - f"yield after the round-trip. pending_hitl={pending2!r}" - ) - # AC bullet (b): the second yield is the refine-gate decision, NOT - # the preflight. The refine-gate question shape is - # ``"Refine analysis at ... Approve and continue?"`` or the - # failure variant ``"Refiner FAILED..."``; both are distinct from - # the preflight question. - assert decision2.get("question") != _PREFLIGHT_QUESTION, ( - f"AC bullet (b): after answering preflight, the generator must " - f"advance past it. Second-invocation question must differ from " - f"the preflight; got identical question {decision2.get('question')!r}. " - f"This means the driver did not consume ``pending_hitl.answer`` " - f"(or the contract round-trip lost state)." - ) - # The refine-gate decision_type is ``phase_gate`` per - # ``_build_refine_gate_decision``; pin so a regression that yields a - # different decision shape (e.g. preflight again, or a misrouted - # choice) is caught. - assert decision2.get("decision_type") in {"phase_gate", "choice"}, ( - f"refine-gate yield must be a phase_gate (or choice for the " - f"failure variant); got {decision2.get('decision_type')!r} on " - f"the second yield" - ) - - -# --------------------------------------------------------------------------- -# Adversarial probing — single-pass invariants the driver must hold -# --------------------------------------------------------------------------- - - -@pytest.mark.skipif( - not (_repo_root() / _DRIVER_PATH).exists(), - reason=f"{_DRIVER_PATH} not present yet (task-1-1 dependency).", -) -def test_driver_is_idempotent_when_answer_unchanged(tmp_path: Path) -> None: - """Re-running the driver without changing ``pending_hitl.answer`` is a no-op. - - Defensive invariant: if the operator hasn't answered the current - decision, the driver must not silently skip ahead — it should - either (a) re-write the same decision (idempotent) or (b) exit - cleanly without corrupting state. Either is acceptable; what the - driver MUST NOT do is advance the generator's state when there is - no new answer to consume — that would lose the operator's intended - decision boundary. - """ - repo_root = _repo_root() - - # First invocation produces the preflight decision. - proc1 = _invoke_driver(state_dir=tmp_path, pipeline_id=_PIPELINE_ID, repo_root=repo_root) - assert proc1.returncode == 0, proc1.stderr - contract1 = _read_contract(tmp_path, _PIPELINE_ID) - decision1 = (contract1.get("pending_hitl") or {}).get("decision") - assert decision1 and decision1.get("question") == _PREFLIGHT_QUESTION - - # Second invocation WITHOUT writing an answer. - proc2 = _invoke_driver(state_dir=tmp_path, pipeline_id=_PIPELINE_ID, repo_root=repo_root) - assert proc2.returncode == 0, ( - f"driver must tolerate re-invocation without a new answer; stderr={proc2.stderr[-500:]!r}" - ) - contract2 = _read_contract(tmp_path, _PIPELINE_ID) - decision2 = (contract2.get("pending_hitl") or {}).get("decision") - # The decision question must still be the preflight — the driver - # MUST NOT have advanced past it without an answer. - assert decision2 and decision2.get("question") == _PREFLIGHT_QUESTION, ( - f"driver advanced the generator without a new answer; " - f"second-invocation decision={decision2!r}. This is a HITL " - f"safety bug — the operator's preflight answer would be lost." - ) diff --git a/integration_tests/regression/test_inprocess_plan_brc.py b/integration_tests/regression/test_inprocess_plan_brc.py deleted file mode 100644 index 391fcfc882..0000000000 --- a/integration_tests/regression/test_inprocess_plan_brc.py +++ /dev/null @@ -1,999 +0,0 @@ -"""Plan-phase in-process BRC end-to-end test (#2717 slice-2 task-2-4). - -Acceptance criteria covered (per contract task-2-4): - -* Boots ``run_pipeline_in_process`` against a deterministic pipeline - id with harness-faked subagents. -* Advances past the refine HITL gate by sending - ``"approve_continue"`` to the refine-gate yield. -* Asserts the plan stage spawns 3 producers (architect, task_planner, - risk_analyst) + 1 reviewer (reviewer_plan). -* Asserts the BRC mechanics reach ``CONSENSUS_CONFIRMED`` on every - producer edge (architect → reviewer_plan, task_planner → - reviewer_plan, risk_analyst → reviewer_plan) by inspecting the - in-process orchestrator's ``_plan_tracker.evaluate()`` snapshot. -* Asserts the stage yields a plan-HITL decision with the expected - fields (id, question, options, decision_type, phase). -* Test runs in <120s under the harness fakes (no real Anthropic API - call, no real Claude Code spawn). - -The test uses ``MagicMock`` substrate-bundle fakes mirroring the -existing ``fake_bundle`` fixture in -``shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py`` so -a behaviour drift between unit and integration coverage is caught. - -Why this is the slice-2 BRC stress test ---------------------------------------- -Slice-1 wired one role (``refiner``) end-to-end on the substrate; -slice-2 is the **first multi-role BRC stress test** — 3 producers -concurrent, 1 reviewer, four CONFIRMED transitions to converge. The -plan describes this as "first multi-role BRC stress test on the -substrate". The test exercises the same ``ThreadPoolExecutor`` -concurrency the implementation uses (``_run_plan_phase`` per -``orchestrator/substrate/in_process.py:830``) so a regression in -the BRC mechanics under multi-producer concurrency surfaces here -rather than in the slice-3 implement-phase test. - -Why CONSENSUS_CONFIRMED is verified via the tracker, not the bus ----------------------------------------------------------------- -The in-process substrate's spawner is synchronous: when -``bundle.spawner.spawn(role, ...)`` returns, the subagent has -finished. The coder's TASK-2-1 implementation therefore drives the -BRC transitions deterministically by calling -``PeerConsensusTracker.handle_propose(...)``, -``handle_ack(...)``, and ``handle_confirmed(...)`` on the -orchestrator's behalf — the harness-faked subagents do not emit -their own BRC messages. ``handle_confirmed`` does NOT publish a -``CONSENSUS_CONFIRMED`` message to the bus; it updates internal -state and the source of truth is ``tracker.evaluate()`` which -returns ``is_complete``, ``blocking_agents``, and a per-agent -``confirmed`` flag. The test asserts every plan-team role is in -the ``confirmed`` set, which is the in-process analogue of "fired -CONSENSUS_CONFIRMED" on the bus. - -Graceful skip on missing implementation ---------------------------------------- -The test is committed against a contract that names the -``_run_plan_phase`` method. If the coder's TASK-2-1 implementation -has not yet been merged, the relevant attribute on -``_InProcessOrchestrator`` is missing and the test skips with a -clear pointer. Once TASK-2-1 lands the skip disappears and the -assertions run. This keeps the tester unblocked when scaffolding -ahead of the coder (per the role's scaffold-first guidance). -""" - -from __future__ import annotations - -import time -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock, patch - -import pytest - -pytestmark = [pytest.mark.integration, pytest.mark.timeout(120)] - - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -in_process_mod = pytest.importorskip( - "orchestrator.substrate.in_process", - reason="orchestrator/substrate/in_process.py not present yet", -) -agent_roles_mod = pytest.importorskip( - "egg_contracts.agent_roles", - reason="shared/egg_contracts/agent_roles.py not importable", -) - -AgentRole = agent_roles_mod.AgentRole - - -# --------------------------------------------------------------------------- -# Plan-phase role expectations — pinned from -# ``shared/egg_contracts/agent_roles.py`` (``_PHASE_ROLES["plan"]`` -# and ``_PHASE_REVIEWERS["plan"]``). If those maps drift the test -# fails loudly with a clear pointer to the source of truth. -# -# Untyped containers because ``AgentRole`` resolves through -# ``pytest.importorskip`` — mypy sees it as a runtime value, not a -# class, and a ``frozenset[AgentRole]`` annotation would be rejected -# as "Variable AgentRole is not valid as a type" (mirroring how -# ``shared/tests/test_rubric_loader.py`` consumes the enum without -# annotation). -# --------------------------------------------------------------------------- - -_EXPECTED_PRODUCERS = frozenset( - {AgentRole.ARCHITECT, AgentRole.TASK_PLANNER, AgentRole.RISK_ANALYST} -) -_EXPECTED_REVIEWERS = frozenset({AgentRole.REVIEWER_PLAN}) - - -# --------------------------------------------------------------------------- -# Fixtures — short intervals + fake substrate bundle -# --------------------------------------------------------------------------- - - -@pytest.fixture -def short_intervals(monkeypatch: pytest.MonkeyPatch) -> None: - """Shrink background-thread intervals so tests run in seconds.""" - monkeypatch.setattr(in_process_mod, "_HEARTBEAT_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BRC_REVIEW_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BUS_TICK_INTERVAL", 0.05) - - -@pytest.fixture -def fake_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Point ``$HOME`` at a clean tmp dir so sentinel reads/writes are isolated. - - The generator's ``_write_active_role_sentinel`` writes under - ``$HOME/.claude/egg-active-role.json``; without this fixture the - test would pollute the developer's actual home directory. - """ - home = tmp_path / "home" - home.mkdir() - monkeypatch.setenv("HOME", str(home)) - return home - - -@pytest.fixture -def isolated_pipeline_state(monkeypatch: pytest.MonkeyPatch) -> None: - """Reset the module-level ``PeerConsensusTracker`` registry between tests. - - ``get_peer_consensus_tracker(pipeline_id)`` returns a module-level - cached tracker; back-to-back tests using the same pipeline id - would otherwise inherit confirmed state from each other. Clear - the registry so every test starts from a fresh tracker. - """ - try: - import orchestrator.peer_consensus as peer_consensus - except ImportError: # pragma: no cover — defensive - return - # The registry symbol name varies across decomposition slices — - # try a few candidates rather than pin a specific private name. - for candidate in ("_TRACKERS", "_PEER_CONSENSUS_TRACKERS", "_tracker_registry"): - registry = getattr(peer_consensus, candidate, None) - if isinstance(registry, dict): - registry.clear() - - -def _make_fake_bundle(tmp_path: Path, *, write_producer_outputs: bool = True) -> MagicMock: - """Build a substrate bundle that records every spawn invocation. - - The fake spawner returns a synthetic ``AgentResult`` (``exit_code=0``, - 40-zero commit, ``stdout="ok"``) for every role. The fake's - ``spawner.spawn`` is a ``MagicMock`` so the test can inspect - ``.call_args_list`` to verify which roles were dispatched. - - When ``write_producer_outputs=True`` (default), the fake spawner - also creates each producer's ``EGG_PRODUCER_OUTPUT_PATH`` JSON - file before returning so the N9 architect-handoff guard in - ``_run_plan_phase`` (reviewer_code v3 non-blocking NB1) sees a - well-formed handoff and the happy-path BRC mechanics converge. - Tests that want to exercise the N9 fail-fast path pass - ``write_producer_outputs=False`` to leave the architect output - missing. - """ - bundle = MagicMock() - - def _spawn(role: Any, _prompt: str, env: dict[str, str], _worktree: Any) -> MagicMock: - if write_producer_outputs: - output_path = env.get("EGG_PRODUCER_OUTPUT_PATH") - if output_path: - target = Path(output_path) - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text( - '{"role": "' + getattr(role, "value", str(role)) + '", "summary": "ok"}', - encoding="utf-8", - ) - return MagicMock( - exit_code=0, - commit_sha="0" * 40, - stdout="ok", - worktree=tmp_path / "wt", - artifacts=[], - ) - - bundle.spawner.spawn = MagicMock(side_effect=_spawn) - bundle.worktrees.create = MagicMock(return_value=tmp_path / "wt") - bundle.worktrees.tear_down = MagicMock() - bundle.name = "claude-code" - - # InProcessMessageBus exposes ``add_message`` / ``get_messages``. - # Back the fake with a real InProcessMessageBus instance so the - # in-process orchestrator's background bus-tick + heartbeat loops - # see a working surface (a MagicMock would return truthy garbage - # and the loops swallow the resulting type errors via their bare - # except clauses). - try: - from orchestrator.substrate.claude_code.message_bus import InProcessMessageBus - - bundle.bus = InProcessMessageBus() - except ImportError: # pragma: no cover — defensive - bundle.bus = MagicMock() - - return bundle - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _has_plan_stage() -> bool: - """Return True iff ``_InProcessOrchestrator`` has a plan-stage method. - - The coder's TASK-2-1 implementation uses ``_run_plan_phase``; - a few peer names are also accepted so the test does not pin a - specific private method name. What the test enforces is the - observable behaviour: a plan-HITL yield with the expected fields - and three concurrent producer spawns. - """ - runner_cls = getattr(in_process_mod, "_InProcessOrchestrator", None) - if runner_cls is None: - return False - return any( - hasattr(runner_cls, attr) - for attr in ( - "_run_plan_phase", - "_run_plan", - "run_plan", - "_dispatch_plan", - "_plan_stage", - ) - ) - - -def _spawned_roles(bundle: MagicMock) -> set[Any]: - """Return the set of roles passed to ``bundle.spawner.spawn``. - - ``spawn`` signature per ``AgentSpawner.spawn(role, prompt, env, - worktree)`` — the role is the first positional arg. - """ - roles: set[Any] = set() - for call in bundle.spawner.spawn.call_args_list: - if call.args: - roles.add(call.args[0]) - elif "role" in call.kwargs: - roles.add(call.kwargs["role"]) - return roles - - -def _drive_past_refine_gate(gen: Any) -> Any: - """Drive the generator through preflight + refine HITL gate. - - Returns the next yield (the plan-HITL decision when TASK-2-1 - has landed). - - The driving sequence: - 1. ``next(gen)`` — preflight HITL. - 2. ``send("approve")`` — past preflight, into refiner spawn. - 3. ``send("approve_continue")`` — past refine HITL gate, into - the plan-phase BRC stage. - """ - next(gen) - gen.send("approve") - return gen.send("approve_continue") - - -def _runner_from_gen(gen: Any) -> Any | None: - """Pull the ``_InProcessOrchestrator`` instance out of a live generator. - - The generator's frame's ``self`` local is the runner; the test - needs the runner to read ``self._plan_tracker.evaluate()`` after - the plan stage runs. - """ - frame = gen.gi_frame - if frame is None: - return None - return frame.f_locals.get("self") - - -# --------------------------------------------------------------------------- -# Tests — plan-phase BRC end-to-end -# --------------------------------------------------------------------------- - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on " - "``_InProcessOrchestrator`` yet. This test runs once the " - "plan-stage method (e.g. ``_run_plan_phase``) is present." - ), -) -def test_plan_stage_spawns_three_producers_and_one_reviewer( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Plan stage spawns the four plan-phase roles via the substrate bundle. - - Drives the generator past the refine HITL gate and asserts the - fake spawner observed calls for ``architect``, ``task_planner``, - ``risk_analyst`` (producers) and ``reviewer_plan`` (reviewer). - The producer ordering is not pinned — they run concurrently - via ``ThreadPoolExecutor`` per ``_run_plan_phase``'s call to - ``concurrent.futures.ThreadPoolExecutor``. - - Acceptance bullet: plan stage spawns 3 producers + 1 reviewer. - """ - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-spawns" - run = in_process_mod.run_pipeline_in_process - - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - try: - _drive_past_refine_gate(gen) - except NotImplementedError as exc: - pytest.fail( - "_maybe_fence still raises NotImplementedError after " - "TASK-2-1 was expected to remove the plan-phase fence " - "branch. TASK-2-1 must have removed the plan branch " - "of the fence; only ``approve_continue`` past the " - f"plan-HITL gate should still trip it. Error: {exc!r}" - ) - finally: - gen.close() - # Background threads need a moment to wind down. - time.sleep(0.2) - - spawned = _spawned_roles(bundle) - # The refiner spawn happens before the plan stage — strip it - # before checking the plan-phase role set. - plan_spawned = spawned - {AgentRole.REFINER} - - missing_producers = _EXPECTED_PRODUCERS - plan_spawned - assert not missing_producers, ( - f"plan stage must spawn all three producers; " - f"missing={sorted(r.value for r in missing_producers)} " - f"spawned={sorted(getattr(r, 'value', str(r)) for r in plan_spawned)}" - ) - missing_reviewers = _EXPECTED_REVIEWERS - plan_spawned - assert not missing_reviewers, ( - f"plan stage must spawn reviewer_plan; missing=" - f"{sorted(r.value for r in missing_reviewers)} " - f"spawned={sorted(getattr(r, 'value', str(r)) for r in plan_spawned)}" - ) - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_stage_yields_hitl_decision_with_expected_fields( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Plan stage yields a HITL decision with question / options / phase set. - - Per task-2-4 acceptance: "asserts the stage yields a plan-HITL - decision with the expected fields". The fields enforced here: - - * ``id`` — non-empty string (used by the contract decisions list). - * ``question`` — non-empty string the operator reads. - * ``options`` — non-empty sequence of allowed answers. - * ``decision_type`` — one of ``phase_gate`` or ``choice`` (the - same shapes the refine-gate uses; the plan gate is a phase - gate by analogy). - * ``phase`` — ``"plan"`` (the gate is the plan→implement - boundary). - - The exact strings are owned by the coder; the shape is pinned - here so a regression that yields a None / falsy decision or one - missing key fields fails clearly. - """ - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-hitl" - run = in_process_mod.run_pipeline_in_process - - plan_hitl: Any = None - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - plan_hitl = _drive_past_refine_gate(gen) - - assert plan_hitl is not None, ( - "plan stage must yield a HITLDecision after the " - "refine-gate answer ``approve_continue``; got None." - ) - # ``HITLDecision`` is a dataclass; both attribute access - # and ``.id`` / ``.question`` work. Tolerate dict-shaped - # answers too in case a future plan-HITL change moves - # to a dict envelope. - decision_id = getattr(plan_hitl, "id", None) or ( - plan_hitl.get("id") if isinstance(plan_hitl, dict) else None - ) - question = getattr(plan_hitl, "question", None) or ( - plan_hitl.get("question") if isinstance(plan_hitl, dict) else None - ) - options = getattr(plan_hitl, "options", None) or ( - plan_hitl.get("options") if isinstance(plan_hitl, dict) else None - ) - decision_type = getattr(plan_hitl, "decision_type", None) or ( - plan_hitl.get("decision_type") if isinstance(plan_hitl, dict) else None - ) - phase = getattr(plan_hitl, "phase", None) or ( - plan_hitl.get("phase") if isinstance(plan_hitl, dict) else None - ) - - assert isinstance(decision_id, str) and decision_id, ( - f"plan-HITL ``id`` must be a non-empty string; got {decision_id!r}" - ) - assert isinstance(question, str) and question, ( - f"plan-HITL ``question`` must be a non-empty string; got {question!r}" - ) - assert options, f"plan-HITL ``options`` must be a non-empty sequence; got {options!r}" - # Tolerate the plan-gate landing as either a phase_gate - # (same as the refine-gate's terminal yield) or a choice - # (the lightweight variant). Anything else (a free-form - # ``feedback`` decision, ``confirm``, etc.) would be a - # design regression — the plan gate is a phase boundary - # the operator approves / changes / stops. - assert decision_type in {"phase_gate", "choice"}, ( - f"plan-HITL ``decision_type`` must be phase_gate or choice; got {decision_type!r}" - ) - # phase may arrive as the enum value (``"plan"``) or the - # enum member; tolerate both. - phase_str = getattr(phase, "value", phase) - assert phase_str == "plan", ( - f"plan-HITL ``phase`` must equal ``'plan'``; got {phase_str!r}" - ) - finally: - gen.close() - time.sleep(0.2) - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_stage_reaches_consensus_confirmed_for_each_producer( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Plan stage drives BRC to CONSENSUS_CONFIRMED on every producer edge. - - Per task-2-4 acceptance: "reaches CONSENSUS_CONFIRMED for all - three plan-phase BRC edges (architect → reviewer_plan, - task_planner → reviewer_plan, risk_analyst → reviewer_plan)". - - Inspects the in-process orchestrator's ``_plan_tracker`` (a - ``PeerConsensusTracker`` registered against the plan-phase - review graph) for the per-agent ``confirmed`` flag from - ``evaluate()``. CONSENSUS_CONFIRMED is the tracker's in-memory - state transition, not a bus message — see this file's - module-level docstring. - """ - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-consensus" - run = in_process_mod.run_pipeline_in_process - - runner = None - eval_snapshot: dict[str, Any] | None = None - - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - _drive_past_refine_gate(gen) - # Pull the runner before closing the generator so we - # have access to ``_plan_tracker`` and the stashed - # ``_plan_eval`` snapshot. - runner = _runner_from_gen(gen) - tracker = getattr(runner, "_plan_tracker", None) - if tracker is not None: - eval_snapshot = tracker.evaluate() - elif runner is not None: - # Fallback: the implementation may stash the eval - # snapshot on the runner directly. - eval_snapshot = getattr(runner, "_plan_eval", None) - finally: - gen.close() - # Background threads need a moment to flush. - time.sleep(0.3) - - assert eval_snapshot is not None, ( - "plan stage must register a ``_plan_tracker`` (or stash a " - "``_plan_eval`` snapshot) on the orchestrator so the operator's " - "HITL gate sees the BRC evaluation; neither attribute was " - "populated. Without these, the plan-HITL gate cannot surface " - "partial-consensus state." - ) - - # ``evaluate()`` returns a per-role ``agents`` map with ``confirmed`` - # booleans. The plan-team confirmed set must include every producer - # AND the reviewer. - agents = eval_snapshot.get("agents") or {} - plan_team = ("architect", "task_planner", "risk_analyst", "reviewer_plan") - not_confirmed = [ - role for role in plan_team if not (agents.get(role, {}) or {}).get("confirmed", False) - ] - assert not not_confirmed, ( - f"plan-phase BRC must reach CONSENSUS_CONFIRMED on every " - f"plan-team role; not_confirmed={not_confirmed!r}; " - f"agents snapshot={agents!r}. The fake spawner returns " - f"exit_code=0 for every spawn so a missing confirmation " - f"points at the BRC mechanics, not the spawn shim." - ) - - # And the high-level ``is_complete`` flag should be True — every - # producer confirmed AND no unresolved NACKs. - assert eval_snapshot.get("is_complete") is True, ( - f"plan-phase BRC ``is_complete`` must be True after every " - f"producer reaches CONFIRMED; got " - f"{eval_snapshot.get('is_complete')!r}. blocking_agents=" - f"{eval_snapshot.get('blocking_agents')!r}, " - f"unresolved_nacks={eval_snapshot.get('unresolved_nacks')!r}" - ) - - -# --------------------------------------------------------------------------- -# Adversarial probing — plan stage edge cases the coder must hold -# --------------------------------------------------------------------------- - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_stage_does_not_run_when_operator_rejects_refine( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Plan stage MUST NOT run when the operator picks a non-continue answer. - - The plan stage is gated on the operator answering the refine - HITL with ``approve_continue``. Other answers (``request_changes``, - ``change_approach``, ``stop``) terminate the generator without - advancing into plan; ``stop`` returns the artifact path, - ``request_changes`` re-loops the refine, and ``change_approach`` - aborts. A regression that fans into plan on a non-continue - answer would burn three subagent spawns the operator did not - approve. - - Adversarial probe: ``stop`` after the refine-gate must NOT - invoke ``bundle.spawner.spawn`` for any of the plan-phase roles. - """ - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-refine-stop" - run = in_process_mod.run_pipeline_in_process - - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - next(gen) - gen.send("approve") - try: - gen.send("stop") - # ``stop`` is terminal — the generator must - # StopIteration with the artifact path string. - except StopIteration as stop: - assert isinstance(stop.value, str), ( - f"``stop`` must terminate cleanly with the " - f"artifact path; got value={stop.value!r}" - ) - finally: - gen.close() - time.sleep(0.2) - - spawned = _spawned_roles(bundle) - plan_spawned = spawned & _EXPECTED_PRODUCERS - assert not plan_spawned, ( - f"plan stage MUST NOT spawn producers when the operator " - f"answers ``stop`` at the refine gate; saw spawns for " - f"{sorted(r.value for r in plan_spawned)}. This is a HITL " - f"safety bug — three subagent spawns the operator did not " - f"approve." - ) - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_stage_does_not_spawn_implement_phase_roles( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Plan stage spawns only plan-phase roles, never implement-phase ones. - - Adversarial probe against a misrouted ``_PHASE_ROLES`` lookup: - if the coder accidentally indexed by ``"implement"`` instead of - ``"plan"`` (off-by-one in a phase dispatch table), the plan - stage would spawn ``coder`` / ``tester`` / ``documenter`` - instead. Pin the negative invariant. - """ - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-phase-isolation" - run = in_process_mod.run_pipeline_in_process - - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - _drive_past_refine_gate(gen) - finally: - gen.close() - time.sleep(0.2) - - forbidden = { - AgentRole.CODER, - AgentRole.TESTER, - AgentRole.DOCUMENTER, - AgentRole.REVIEWER_CODE, - AgentRole.REVIEWER_CODE_HOLISTIC, - AgentRole.REVIEWER_CONTRACT, - AgentRole.REVIEWER_SECURITY, - AgentRole.REVIEWER_CONCURRENCY, - } - spawned = _spawned_roles(bundle) - leaked = spawned & forbidden - assert not leaked, ( - f"plan stage MUST NOT spawn implement-phase roles; saw " - f"spawns for {sorted(getattr(r, 'value', str(r)) for r in leaked)}. " - f"This points at a phase-dispatch lookup that indexed " - f"``_PHASE_ROLES`` with the wrong key." - ) - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_stage_does_not_invoke_refiner_a_second_time( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Plan stage MUST NOT re-spawn the refiner. - - Adversarial probe: the refiner already ran in the refine stage - before the operator's approve_continue. A regression that - re-included REFINER in the plan-phase producer set (off-by-one - in role iteration) would burn an extra spawn and write a stale - refine artifact. Pin the single-refiner-spawn invariant. - """ - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-refiner-once" - run = in_process_mod.run_pipeline_in_process - - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - _drive_past_refine_gate(gen) - finally: - gen.close() - time.sleep(0.2) - - refiner_spawn_count = sum( - 1 - for call in bundle.spawner.spawn.call_args_list - if call.args and call.args[0] == AgentRole.REFINER - ) - assert refiner_spawn_count == 1, ( - f"refiner must be spawned exactly once (in the refine stage " - f"before the plan stage); saw {refiner_spawn_count} spawn(s)." - ) - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_stage_carries_phase_env_var_to_producers( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Every plan-phase spawn must carry ``EGG_PHASE=plan`` in its env. - - Adversarial probe: a regression that forgot to set ``EGG_PHASE`` - on plan-producer spawn envs would cause the spawned subagents - to see the wrong phase and possibly drop into refine code paths - or skip plan-specific contract validation. Pin the env-propagation - contract. - - Refiner spawns are excluded — the refiner runs in the refine - phase and the refine spawn shape does not include ``EGG_PHASE``. - """ - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-env" - run = in_process_mod.run_pipeline_in_process - - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - _drive_past_refine_gate(gen) - finally: - gen.close() - time.sleep(0.2) - - plan_spawn_envs: list[dict[str, str]] = [] - for call in bundle.spawner.spawn.call_args_list: - role = call.args[0] if call.args else call.kwargs.get("role") - if role == AgentRole.REFINER: - continue - # ``spawn(role, prompt, env, worktree)`` — env is args[2] or - # the ``env`` kwarg. - env_arg = None - if len(call.args) >= 3: - env_arg = call.args[2] - else: - env_arg = call.kwargs.get("env") - if isinstance(env_arg, dict): - plan_spawn_envs.append(env_arg) - - assert plan_spawn_envs, ( - "no plan-phase spawn invocations had an env dict captured; " - "the spawn() call shape may have changed — update this test." - ) - missing_phase = [env for env in plan_spawn_envs if env.get("EGG_PHASE") != "plan"] - assert not missing_phase, ( - f"every plan-phase spawn env must set EGG_PHASE=plan; " - f"{len(missing_phase)}/{len(plan_spawn_envs)} envs were " - f"missing or wrong. Examples (capped at 3): " - f"{[{k: v for k, v in env.items() if k.startswith('EGG_')} for env in missing_phase[:3]]}" - ) - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_gate_decision_persists_with_phase_plan( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """Plan-gate decision must persist with ``phase: "plan"`` in the contract. - - Reviewer_code v1 blocker B1 (#2717 slice-2): the in-process - orchestrator previously hardcoded ``phase: "refine"`` inside - ``_write_pending_decision``, so every plan-gate decision landed - on disk with the wrong phase even though the yielded - ``HITLDecision`` itself carried ``phase="plan"``. Pin the - invariant that the persisted decision's ``phase`` field matches - the yielded decision's ``phase`` so the regression cannot recur. - """ - import json - - bundle = _make_fake_bundle(tmp_path) - - pipeline_id = "pipeline-plan-brc-phase-persisted" - state_dir = tmp_path / ".egg-state" - run = in_process_mod.run_pipeline_in_process - - plan_hitl: Any = None - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=state_dir, - ) - try: - plan_hitl = _drive_past_refine_gate(gen) - finally: - gen.close() - time.sleep(0.2) - - assert plan_hitl is not None, ( - "plan stage must yield a HITLDecision after the refine-gate " - "answer ``approve_continue``; got None." - ) - - yielded_phase = getattr(plan_hitl, "phase", None) or ( - plan_hitl.get("phase") if isinstance(plan_hitl, dict) else None - ) - yielded_phase_str = getattr(yielded_phase, "value", yielded_phase) - yielded_id = getattr(plan_hitl, "id", None) or ( - plan_hitl.get("id") if isinstance(plan_hitl, dict) else None - ) - - contract_path = state_dir / "contracts" / f"{pipeline_id}.json" - assert contract_path.is_file(), ( - f"plan-gate decision must persist to {contract_path}; not found." - ) - contract = json.loads(contract_path.read_text()) - - decisions = contract.get("decisions") or [] - plan_decision = next( - (d for d in decisions if d.get("id") == yielded_id), - None, - ) - assert plan_decision is not None, ( - f"plan-gate decision with id={yielded_id!r} not found in " - f"persisted contract decisions={decisions!r}." - ) - assert plan_decision.get("phase") == yielded_phase_str, ( - f"persisted decision's phase must equal yielded decision's " - f"phase; persisted={plan_decision.get('phase')!r} vs " - f"yielded={yielded_phase_str!r}. Reviewer_code v1 blocker B1 " - f"(#2717 slice-2)." - ) - assert contract.get("current_phase") == yielded_phase_str, ( - f"contract's current_phase must equal the yielded plan-gate " - f"phase; current_phase={contract.get('current_phase')!r} vs " - f"yielded={yielded_phase_str!r}. Reviewer_code v1 blocker B1." - ) - - -@pytest.mark.skipif( - not _has_plan_stage(), - reason=( - "task-2-1 (coder) has not landed a plan-stage method on ``_InProcessOrchestrator`` yet." - ), -) -def test_plan_stage_fails_fast_when_architect_handoff_missing( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - isolated_pipeline_state: None, -) -> None: - """N9 fail-fast: missing architect output skips fan-out + reviewer; gate is_complete=False. - - Reviewer_code v3 non-blocking NB1 + NB3 (#2717 slice-2): when the - architect spawn returns exit_code=0 but never writes its - ``EGG_PRODUCER_OUTPUT_PATH`` JSON, the orchestrator must - (a) record a NACK on the ``reviewer_plan → architect`` edge that - survives to the plan-HITL gate (no optimistic-ACK clobber), and - (b) skip the downstream fan-out + reviewer spawn entirely (no - wasted subagents on a dangling handoff). The plan-gate decision - must surface ``is_complete=False`` with the architect in the - blocking set and offer the ``retry`` / ``abort`` options. - - Without this invariant, the v2 N9 NACK silently sank under the - reviewer's optimistic-ACK path and the gate appeared converged. - """ - # write_producer_outputs=False leaves the architect output missing - # — exactly the broken-handoff case N9 is designed to catch. - bundle = _make_fake_bundle(tmp_path, write_producer_outputs=False) - - pipeline_id = "pipeline-plan-brc-n9-fail-fast" - run = in_process_mod.run_pipeline_in_process - - plan_hitl: Any = None - runner = None - with patch( - "orchestrator.substrate.select_substrate", - return_value=bundle, - ): - gen = run( - pipeline_id, - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - plan_hitl = _drive_past_refine_gate(gen) - runner = _runner_from_gen(gen) - finally: - gen.close() - time.sleep(0.2) - - # NB3: downstream producers and reviewer must NOT spawn when N9 fires. - spawned = _spawned_roles(bundle) - forbidden_after_n9 = { - AgentRole.TASK_PLANNER, - AgentRole.RISK_ANALYST, - AgentRole.REVIEWER_PLAN, - } - leaked = spawned & forbidden_after_n9 - assert not leaked, ( - f"N9 fail-fast must skip downstream fan-out + reviewer spawn " - f"when the architect output file is missing; saw spawns for " - f"{sorted(r.value for r in leaked)}. NB3 (#2717 slice-2)." - ) - - # NB1: the plan-gate decision must surface the failure to the operator, - # not silently complete via the reviewer's optimistic-ACK fallback. - assert plan_hitl is not None, ( - "plan stage must yield a HITLDecision even on the N9 fail-fast path; got None." - ) - options = list(getattr(plan_hitl, "options", None) or []) - assert "retry" in options and "abort" in options, ( - f"plan-gate must offer retry / abort on the N9 fail-fast path; " - f"got options={options!r}. NB1 (#2717 slice-2): the v2 NACK " - f"was previously clobbered by optimistic-ACK and the gate " - f"surfaced the success-path approve_continue options instead." - ) - - # And the tracker's evaluate() must report is_complete=False with - # the architect in the blocking set. - tracker = getattr(runner, "_plan_tracker", None) - assert tracker is not None, "runner must register a plan tracker" - eval_snapshot = tracker.evaluate() - assert eval_snapshot.get("is_complete") is False, ( - f"plan-phase BRC must NOT converge when the architect " - f"handoff is broken; eval={eval_snapshot!r}." - ) - blocking = set(eval_snapshot.get("blocking_agents") or []) - assert "architect" in blocking, ( - f"architect must appear in blocking_agents on the N9 path; " - f"blocking_agents={sorted(blocking)!r}." - ) diff --git a/integration_tests/regression/test_pretooluse_hook_nested.py b/integration_tests/regression/test_pretooluse_hook_nested.py deleted file mode 100644 index 1a67abe186..0000000000 --- a/integration_tests/regression/test_pretooluse_hook_nested.py +++ /dev/null @@ -1,357 +0,0 @@ -"""PreToolUse hook nested-dispatch test (#2717 slice-1 task-1-5, cq-5 early-spike). - -The R2 question — *"does the egg PreToolUse hook resolve agent role -correctly under nested Agent-tool dispatch?"* — is the gating empirical -finding for the slice-5 R15 migration (flipping production dispatch -from the harness re-host model to Claude Code's Agent tool). Slice 1 -gives a partial-but-load-bearing answer: **the hook logic is correct -given accurate ``EGG_AGENT_ROLE`` propagation**; the remaining half -(does Claude Code itself propagate ``EGG_AGENT_ROLE`` into nested -subagents in real production dispatch?) is verifiable only against a -real Claude Code session and is deferred to slice-5 / a future issue -when ``ClaudeCodeSpawner`` actually exercises Agent-tool dispatch. - -Why this is a test-fake test, not an empirical Claude-Code test ---------------------------------------------------------------- -``shared/egg_harness/client.py:60-150`` is the harness re-host model -(per cq-3): subagents run as fresh ``ClaudeCodeSpawner`` invocations, -not as Agent-tool dispatches inside a parent session. ``grep -rn -"PreToolUseHookPolicy|hook_entry" shared/egg_harness/`` returns zero -hits — the harness wires its own ``ToolRegistry.set_permission_callback`` -and never reaches ``hook_entry.decide``. So under the production -substrate today the PreToolUse hook is **not** even invoked for the -"nested" leg. - -To answer R2 deterministically the test uses the test-only fake from -task-1-9 (``integration_tests/regression/_agent_tool_fake.py``). The -fake simulates the nested dispatch by spawning a subprocess with a -controlled ``EGG_AGENT_ROLE`` and routing the simulated tool input -through ``hook_entry.decide(...)``. This **pins the hook logic** — -when slice-5 flips dispatch to Agent-tool and ``EGG_AGENT_ROLE`` -propagation becomes the production reality, the same logic ships -unchanged. - -Acceptance criteria covered (per contract task-1-5): - -* ``hook_entry.decide(...)`` returns ``{"decision": "block", ...}`` for - a child write to ``orchestrator/foo.py`` when the child's role is - ``tester`` (out of role for source files) — even though the parent - role is ``architect``. -* The verdict is written to - ``.egg-state//r2-verdict.json`` as - ``{"r2_verdict": "pass"}`` (or ``"fail"`` with a reason) so slice-5's - contingent R15 migration task can read it. -* The hook entry script reads JSON on stdin and prints JSON on stdout - per the Claude Code PreToolUse hook protocol — exercised - end-to-end via the fake. - -Test runs in <60s per the AC. - -Fake API shape --------------- -TASK-1-9's ``dispatch(...)`` returns a ``DispatchResult`` dataclass -(``parent_role``, ``child_role``, ``write_target``, ``decision`` — -the raw hook-verdict dict — and convenience fields ``denied`` / -``deny_reason``). Test assertions use the structured dataclass -attributes so a re-shape of the underlying verdict dict (e.g. adding -extra metadata keys) does not break the test contract. -""" - -from __future__ import annotations - -import importlib -import json -import os -import sys -from pathlib import Path - -import pytest - -pytestmark = pytest.mark.integration - - -#: Path the coder commits the nested-dispatch fake to per task-1-9. -_FAKE_MODULE_PATH = Path(__file__).parent / "_agent_tool_fake.py" - - -# --------------------------------------------------------------------------- -# Fixture — load the coder-owned fake helper from task-1-9 -# --------------------------------------------------------------------------- - - -@pytest.fixture() -def fake() -> object: - """Import the task-1-9 fake module. - - Skip if the fake is not yet present (coder dependency) so the test - file does not break collection while task-1-9 is in flight. - """ - if not _FAKE_MODULE_PATH.exists(): - pytest.skip( - f"{_FAKE_MODULE_PATH.name} not present — task-1-9 (coder) " - f"has not landed yet. This is the upstream dependency for " - f"the R2 nested-dispatch test." - ) - # Force a fresh import each time so a regression in the fake's - # module-level state doesn't leak across tests. - sys.modules.pop("integration_tests.regression._agent_tool_fake", None) - return importlib.import_module("integration_tests.regression._agent_tool_fake") - - -@pytest.fixture() -def isolated_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Sandbox the ``.egg-state//`` tree for the r2-verdict write. - - The test writes ``.egg-state//r2-verdict.json`` under - a tmp tree so a re-run does not silently overwrite a real - pipeline's verdict. - """ - state = tmp_path / ".egg-state" - state.mkdir() - monkeypatch.chdir(tmp_path) - return state - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _write_r2_verdict(state_dir: Path, pipeline_id: str, payload: dict) -> Path: - """Write the R2 verdict under ``.egg-state//r2-verdict.json``. - - Returns the path so callers can pin it in the assertions. - """ - out_dir = state_dir / pipeline_id - out_dir.mkdir(parents=True, exist_ok=True) - out_path = out_dir / "r2-verdict.json" - out_path.write_text(json.dumps(payload, indent=2)) - return out_path - - -# --------------------------------------------------------------------------- -# Test — R2 verdict (gating finding for slice-5 R15) -# --------------------------------------------------------------------------- - - -def test_pretooluse_hook_denies_nested_child_write(fake: object, isolated_state_dir: Path) -> None: - """The hook denies a child write outside the child's role. - - Setup: - * Parent fake-subagent role: ``architect`` — recorded only for - observability (the fake does not spawn a parent subprocess - because role-routing happens at the child boundary in real - Agent-tool dispatch). - * Child fake-subagent role: ``tester`` — NOT allowed to write - ``orchestrator/foo.py`` (testers are scoped to ``tests/``, - ``**/conftest.py``, etc. per the role boundaries declared by - ``shared/egg_restrictions/patterns.py``). - - When the child subprocess invokes ``hook_entry.decide(...)`` with - ``Write {file_path: "orchestrator/foo.py"}`` and - ``EGG_AGENT_ROLE=tester``, the hook must return - ``{"decision": "block", "reason": ...}`` — proving the hook does - NOT silently default to a parent-side role when the child env - carries the correct role. - - Pass criterion: ``DispatchResult.denied is True`` and the deny - reason references the tester role. - """ - pipeline_id = "pipeline-r2-nested" - - dispatch = fake.dispatch - assert callable(dispatch), ( - f"task-1-9 contract: ``_agent_tool_fake.dispatch`` must be " - f"callable; module exposes {dir(fake)!r}" - ) - - result = dispatch( - parent_role="architect", - child_role="tester", - write_target="orchestrator/foo.py", - ) - - # Derive the verdict from the dispatch outcome — slice-5's - # contingent R15 migration task reads ``r2-verdict.json`` to - # decide whether to proceed, so the file must reflect the - # empirical answer, not an optimistic constant. Write the - # verdict *before* the assertions so a regression that fails - # one of the structured checks below still produces an - # accurate ``{"r2_verdict": "fail", "reason": ...}`` record - # for the downstream consumer (reviewer_code finding #5 - # non-blocking). - verdict = getattr(result, "decision", None) or {} - reason = str(verdict.get("reason") or "") - if ( - getattr(result, "denied", None) is True - and isinstance(verdict, dict) - and verdict.get("decision") == "block" - and "tester" in reason.lower() - ): - verdict_payload: dict[str, object] = {"r2_verdict": "pass"} - else: - verdict_payload = { - "r2_verdict": "fail", - "reason": ( - f"DispatchResult denied={getattr(result, 'denied', None)!r}; " - f"raw_decision={verdict!r}; reason={reason!r}" - ), - } - verdict_path = _write_r2_verdict(isolated_state_dir, pipeline_id, verdict_payload) - assert verdict_path.exists() - written = json.loads(verdict_path.read_text()) - # Always-asserted shape — the field is mandatory either way. - assert "r2_verdict" in written, ( - f"r2-verdict.json must encode an 'r2_verdict' field per AC; got {written!r}" - ) - - # AC: hook returns ``{"decision": "block", "reason": ...}`` for - # the child's denied write. The fake wraps this in a - # ``DispatchResult``; pin both the structured ``denied`` bool and - # the raw verdict dict so a refactor of either surface is caught. - assert getattr(result, "denied", None) is True, ( - f"R2 nested-dispatch verdict must be ``denied`` for " - f"tester→orchestrator/foo.py; got {result!r}. This indicates " - f"the hook resolved the role from the parent rather than the " - f"child — slice-5's R15 migration cannot ship until this is fixed." - ) - assert isinstance(verdict, dict) and verdict, ( - f"DispatchResult.decision must be a non-empty dict (the raw hook " - f"verdict); got {type(verdict).__name__} ({verdict!r})" - ) - assert verdict.get("decision") == "block", ( - f"raw hook verdict must carry ``decision='block'`` on a denied dispatch; got {verdict!r}" - ) - assert verdict.get("reason"), ( - f"``block`` verdict must carry a non-empty ``reason`` — " - f"reviewer_security finding pattern. Got {verdict!r}" - ) - # Reason should name the tester role — operator reading the - # Claude Code UI denial needs the resolved role to act on it. - assert "tester" in reason.lower(), ( - f"denial reason must name the resolved (child) role so the " - f"operator can act on it; got {reason!r}" - ) - # And the verdict file we wrote reflects the pass path. - assert written.get("r2_verdict") == "pass", ( - f"on a passing run the verdict file must record 'pass'; got {written!r}" - ) - - -def test_pretooluse_hook_allows_in_role_child_write(fake: object, isolated_state_dir: Path) -> None: - """Negative-control: in-role child write is NOT spuriously denied. - - Without this, a "deny everything" regression would silently pass - ``test_pretooluse_hook_denies_nested_child_write`` while breaking - every legitimate write. Pin the allow path explicitly. - """ - dispatch = fake.dispatch - - result = dispatch( - parent_role="architect", - child_role="tester", - write_target="integration_tests/regression/test_example.py", - ) - - assert getattr(result, "denied", None) is False, ( - f"in-role child write (tester→integration_tests/regression/) " - f"must NOT be denied; got denied=True (verdict={getattr(result, 'decision', None)!r}). " - f"A regression here would deny every legitimate tester write " - f"under nested dispatch." - ) - - -def test_pretooluse_hook_blocks_parent_role_with_child_write_target( - fake: object, isolated_state_dir: Path -) -> None: - """Cross-role probe: parent ``coder`` + child ``tester`` writing source must deny. - - Adversarial probe: even when the *parent* role would also be - denied for this write (coder cannot write to ``shared/tests/``), - the hook must surface the CHILD's denial reason — proving the - nested-dispatch resolution actually uses the child env, not a - parent-side fallback that happens to also block. - """ - dispatch = fake.dispatch - - # Parent and child have different roles; the write target is - # outside BOTH roles' allow-lists. The hook must still resolve - # the child's role (tester) and emit a tester-scoped denial. - result = dispatch( - parent_role="coder", - child_role="tester", - write_target="orchestrator/concurrent_executor.py", - ) - - assert getattr(result, "denied", None) is True, ( - f"parent=coder, child=tester writing orchestrator/* must be " - f"denied by the hook; got {result!r}" - ) - reason = str(getattr(result, "deny_reason", "") or "") - # The denial reason must name the CHILD role (tester) — if it - # named the parent (coder), that would be the role-resolution - # bug R2 is asking about. - assert "tester" in reason.lower(), ( - f"R2 bug signature: nested-dispatch denial named the parent " - f"role rather than the child. reason={reason!r}. The hook is " - f"resolving role from the wrong process env; slice-5 R15 " - f"migration is blocked until this is fixed." - ) - - -# --------------------------------------------------------------------------- -# Adversarial probing: env-propagation invariants the fake must hold -# --------------------------------------------------------------------------- - - -def test_dispatch_returns_structured_result(fake: object) -> None: - """``dispatch(...)`` returns a ``DispatchResult`` with the verdict dict. - - Acceptance: ``dispatch(...)`` returns the hook verdict (the same - JSON-decoded dict ``hook_entry.decide`` writes to stdout). The - coder's task-1-9 implementation wraps the verdict in a - ``DispatchResult`` dataclass whose ``.decision`` field IS the dict — - pin both surfaces so a refactor of either is caught. - """ - dispatch = fake.dispatch - result = dispatch( - parent_role="architect", - child_role="tester", - write_target="orchestrator/foo.py", - ) - # Structural shape. - assert hasattr(result, "parent_role") and result.parent_role == "architect" - assert hasattr(result, "child_role") and result.child_role == "tester" - assert hasattr(result, "write_target") and result.write_target == "orchestrator/foo.py" - assert hasattr(result, "decision") and isinstance(result.decision, dict), ( - f"DispatchResult.decision must be the raw hook verdict dict; " - f"got {type(getattr(result, 'decision', None)).__name__}" - ) - assert hasattr(result, "denied") and isinstance(result.denied, bool) - - -def test_dispatch_does_not_leak_egg_agent_role_into_parent_env( - fake: object, monkeypatch: pytest.MonkeyPatch -) -> None: - """The fake must not mutate the caller's ``EGG_AGENT_ROLE``. - - Adversarial: a fake that calls ``os.environ['EGG_AGENT_ROLE']=...`` - instead of passing env to the subprocess would leak the simulated - child role into the test process — every subsequent test that - relies on ``EGG_AGENT_ROLE`` would see the leaked value. The fake - must isolate the env via subprocess ``env=`` (or equivalent). - """ - dispatch = fake.dispatch - - monkeypatch.delenv("EGG_AGENT_ROLE", raising=False) - _ = dispatch( - parent_role="architect", - child_role="tester", - write_target="orchestrator/foo.py", - ) - - assert os.environ.get("EGG_AGENT_ROLE", "") == "", ( - f"fake.dispatch leaked EGG_AGENT_ROLE into the parent env " - f"(value={os.environ.get('EGG_AGENT_ROLE')!r}). This would " - f"corrupt every subsequent test in the same process." - ) diff --git a/integration_tests/regression/test_substrate_smoke.py b/integration_tests/regression/test_substrate_smoke.py deleted file mode 100644 index fb3a8efee7..0000000000 --- a/integration_tests/regression/test_substrate_smoke.py +++ /dev/null @@ -1,232 +0,0 @@ -"""Substrate-swap end-to-end smoke test (issue #2623 slice-1, task-1-8). - -Drives ``select_substrate(...).spawner.spawn(...)`` and -``.bus.add_message/get_messages`` directly through both substrate -implementations: - -* ``"k3s"`` — ``K3sSpawnerAdapter`` wrapping a mocked - ``create_concurrent_spawn_fn`` closure so the test stays - pure-Python. -* ``"claude-code"`` — ``ClaudeCodeSpawner`` (with the egg-harness - ``run_agent`` stubbed) + ``InProcessMessageBus``. - -Both dimensions run in-process; no kubectl is required. - -Assertions verified per task-1-8 acceptance criteria: - -* ``spawner.spawn`` returns an ``AgentResult`` (the legacy contract). - ``commit_sha`` is captured when the worktree contains a git - checkout — the integration smoke runs in tmp_path without a git - checkout, so the field is allowed to be ``None``; the unit tests - under ``shared/tests/`` cover the populated case. -* ``.bus.add_message`` / ``.bus.get_messages`` round-trips ``Message`` - objects keyed by ``pipeline_id``. -* INV-3 stale-version rejection still fires when an ACK at an older - proposal version is sent (driven through a ``PeerConsensusTracker``). -""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock - -import pytest - -pytestmark = pytest.mark.integration - -# Ensure orchestrator/ is importable for the test process. -_REPO_ROOT = Path(__file__).resolve().parents[2] -for _p in ( - _REPO_ROOT / "orchestrator", - _REPO_ROOT / "shared", - _REPO_ROOT, -): - if _p.exists() and str(_p) not in sys.path: - sys.path.insert(0, str(_p)) - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) - - -def _build_substrate(dim: str) -> Any: - """Construct a substrate bundle, mocking the k3s legacy spawn fn.""" - select_substrate = substrate_pkg.select_substrate - if dim == "k3s": - fake_container = MagicMock(stdout="ok", exit_code=0) - spawn_fn = MagicMock(return_value=fake_container) - return select_substrate({"EGG_SUBSTRATE": "k3s"}, k3s_legacy_spawn_fn=spawn_fn) - return select_substrate({"EGG_SUBSTRATE": "claude-code"}) - - -# --------------------------------------------------------------------------- -# Smoke: select_substrate returns a bundle with the four slots -# --------------------------------------------------------------------------- - - -def test_select_substrate_k3s_returns_bundle_with_required_fields() -> None: - """The k3s bundle exposes ``spawner`` / ``bus`` / ``policy`` / ``worktrees``.""" - bundle = _build_substrate("k3s") - for attr in ("spawner", "bus", "policy", "worktrees", "name"): - assert hasattr(bundle, attr), f"bundle must expose .{attr}" - assert bundle.name == "k3s" - - -def test_select_substrate_claude_code_returns_bundle_with_required_fields() -> None: - if os.environ.get("EGG_AGENT_ROLE"): - pytest.skip("claude-code substrate skipped inside egg sandbox-agent context") - bundle = _build_substrate("claude-code") - for attr in ("spawner", "bus", "policy", "worktrees", "name"): - assert hasattr(bundle, attr), f"bundle must expose .{attr}" - assert bundle.name == "claude-code" - - -# --------------------------------------------------------------------------- -# Smoke: spawner.spawn returns an AgentResult on both legs -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize("dim", ["k3s", "claude-code"]) -def test_spawner_spawn_returns_agent_result( - dim: str, - tmp_path: Path, -) -> None: - """Both legs return an ``AgentResult`` from ``spawner.spawn(...)``.""" - if dim == "claude-code" and os.environ.get("EGG_AGENT_ROLE"): - pytest.skip("claude-code substrate skipped inside egg sandbox-agent context") - bundle = _build_substrate(dim) - AgentResult = substrate_pkg.AgentResult - - class _Role: - value = "refiner" - - def __str__(self) -> str: # pragma: no cover - return self.value - - # The claude-code leg uses the real ClaudeCodeSpawner which calls - # into egg_harness — for the integration smoke we substitute its - # runner with a deterministic stub via dependency injection. - if dim == "claude-code": - from orchestrator.substrate.claude_code.spawner import ClaudeCodeSpawner - - bundle.spawner = ClaudeCodeSpawner( - run_agent_fn=MagicMock(return_value=MagicMock(stdout="ok", returncode=0)) - ) - - result = bundle.spawner.spawn(_Role(), "task body", {}, tmp_path) - assert isinstance(result, AgentResult), ( - f"{dim} spawner must return AgentResult; got {type(result).__name__}" - ) - - -# --------------------------------------------------------------------------- -# Smoke: bus.add_message / bus.get_messages round-trip -# --------------------------------------------------------------------------- - - -def test_bus_add_get_messages_round_trip_claude_code() -> None: - """``bus.add_message`` then ``bus.get_messages`` returns the same payload. - - Reviewer v1 blocker #9: the earlier parametrized form unconditionally - skipped the ``k3s`` dimension (the k3s bus is gateway-side Redis - Streams, exercised by ``integration_tests/`` against a live - cluster), so the parametrize-with-skip pattern was theater. The - test is renamed to make the actual coverage explicit: it pins the - claude-code in-process bus round-trip only. - """ - if os.environ.get("EGG_AGENT_ROLE"): - pytest.skip("claude-code substrate skipped inside egg sandbox-agent context") - bundle = _build_substrate("claude-code") - from orchestrator.message_store import Message - - pipeline_id = "pipeline-substrate-smoke" - bundle.bus.add_message( - Message( - pipeline_id=pipeline_id, - from_role="tester", - to_role="all", - message_type="STATUS", - body="smoke", - ) - ) - msgs = bundle.bus.get_messages(pipeline_id) - assert msgs, "bus.get_messages must return the added message" - assert any(m.from_role == "tester" for m in msgs) - - -# --------------------------------------------------------------------------- -# Smoke: INV-3 stale-version rejection survives the bus surface -# --------------------------------------------------------------------------- - - -def test_peer_consensus_tracker_preserves_inv3_stale_version_rejection() -> None: - """A stale-version ACK is rejected by ``PeerConsensusTracker``. - - Reviewer v1 blocker #9 — naming honesty: the previous name - ("bus_preserves_inv3") oversold the test. INV-3 (stale-version - rejection) lives in ``PeerConsensusTracker`` - (``orchestrator/peer_consensus.py``), not in - ``InProcessMessageBus`` — the bus only ferries messages. This - test pins the tracker's invariant *under the claude-code - substrate import path*: a regression in the tracker would break - the substrate's BRC compatibility just as it would break the - k3s substrate, but the test does NOT exercise the bus's - message-shape itself. The bus's round-trip is covered by - ``test_bus_add_get_messages_round_trip_claude_code``. - - Re-runs the - ``test_brc_open_nacks_barrier::TestStaleVersionRejection:: - test_ack_against_stale_version_raises`` scenario as a regression - pin in the substrate test surface. - """ - if os.environ.get("EGG_AGENT_ROLE"): - pytest.skip("claude-code substrate skipped inside egg sandbox-agent context") - from orchestrator.peer_consensus import PeerConsensusTracker - from orchestrator.review_graph import ( - ReviewCriticality, - ReviewEdge, - ReviewGraph, - ) - - graph = ReviewGraph( - [ - ReviewEdge("reviewer_code", "coder", ReviewCriticality.CRITICAL), - ReviewEdge("reviewer_security", "coder", ReviewCriticality.CRITICAL), - ] - ) - tracker = PeerConsensusTracker("pipeline-substrate-smoke", graph, cooldown_seconds=0) - for role in ("coder", "reviewer_code", "reviewer_security"): - tracker.register_agent(role) - tracker.handle_propose( - "coder", - { - "summary": ( - "Proposal v1 with enough text to satisfy the ≥50 char " - "BRC content gate enforced by _validate_brc_content." - ), - "artifacts": ["a.py"], - "commit_sha": "abc1234", - }, - ) - # ACK at the current version is allowed. - # Subsequent ACK at version=0 (stale, because tracker is now at v1) - # must raise. Use a broad ``Exception`` match because the BRC - # error vocabulary still evolves under #2142 follow-ups; the - # ``match`` pin restricts the catch to version-shape errors. - with pytest.raises(Exception, match="version|stale|out of date|mismatch"): # noqa: B017, BLE001 - tracker.handle_ack( - "reviewer_code", - "coder", - { - "artifact_references": ["a.py"], - "reason": ( - "Stale-version ACK: enough text to satisfy the " - "≥50 char content gate but at the wrong version." - ), - }, - ack_version=0, - ) diff --git a/orchestrator/concurrent_executor.py b/orchestrator/concurrent_executor.py index 1e1efe4c85..1cd6986d1c 100644 --- a/orchestrator/concurrent_executor.py +++ b/orchestrator/concurrent_executor.py @@ -511,24 +511,7 @@ def _spawn_agent(self, role: AgentRole, prompt_text: str = "") -> AgentExecution Works with both ContainerSpawner.create_concurrent_spawn_fn() and KubernetesSpawner.create_concurrent_spawn_fn(). - - Substrate-swap seam (#2623): when ``EGG_SUBSTRATE=claude-code`` - is set, this method routes through - ``orchestrator.substrate.select_substrate(...).spawner.spawn(...)`` - instead of calling ``self.spawn_fn`` directly. All other values - (unset, ``"k3s"``, anything else) preserve the legacy path - verbatim so existing k3s production deployments are unaffected. - - Why this is claude-code-only (reviewer v1 blocker #1): the - ``K3sSpawnerAdapter`` shim does NOT yet preserve slice-aware - branch selection or the BRC consensus wrapper command, so any - operator who set ``EGG_SUBSTRATE=k3s`` to be explicit would - silently lose those. The follow-up issue extends the adapter - to forward both, at which point the seam can also be gated on - ``"k3s"``. Until then, the legacy path is the only k3s path. """ - import os - branch = self.get_worktree_branch(role, slice_id=self._slice_id) env = self.get_agent_env(role) @@ -536,16 +519,6 @@ def _spawn_agent(self, role: AgentRole, prompt_text: str = "") -> AgentExecution if prompt_text: command = build_consensus_wrapped_command(prompt_text) - substrate_name = (os.environ.get("EGG_SUBSTRATE") or "").lower() - if substrate_name == "claude-code": - return self._spawn_agent_via_substrate( - role=role, - branch=branch, - env=env, - command=command, - prompt_text=prompt_text, - ) - result = self.spawn_fn( role=role, branch=branch, @@ -566,135 +539,6 @@ def _spawn_agent(self, role: AgentRole, prompt_text: str = "") -> AgentExecution slice_id=self._slice_id, ) - def _spawn_agent_via_substrate( - self, - *, - role: AgentRole, - branch: str | None, - env: dict[str, str], - command: list[str] | None, - prompt_text: str, - ) -> AgentExecution: - """Dispatch a spawn through the ``EGG_SUBSTRATE`` bundle. - - Walking-skeleton seam for issue #2623. The substrate bundle's - ``spawner.spawn(role, prompt, env, worktree)`` returns an - ``AgentResult`` (not a ``SpawnedContainer``); we synthesise - an ``AgentExecution`` from it. - - Under ``EGG_SUBSTRATE=k3s`` the bundle returns the - ``K3sSpawnerAdapter`` shim wrapping ``self.spawn_fn``, so the - legacy spawn path still runs — this method is the rewire. - Under ``EGG_SUBSTRATE=claude-code`` the bundle returns the - ``ClaudeCodeSpawner`` and the legacy ``spawn_fn`` is bypassed. - """ - import os - - try: - from orchestrator.substrate import SubstrateBundle, select_substrate - except ImportError: - # Fallback for environments where orchestrator/ is on sys.path - # directly rather than as a package (e.g. the sandbox container). - from substrate import ( # type: ignore[no-redef, import-untyped] - SubstrateBundle, - select_substrate, - ) - - bundle: SubstrateBundle = select_substrate( - os.environ, - k3s_legacy_spawn_fn=self.spawn_fn, - ) - - # The substrate path uses an in-process worktree handle; the - # legacy k3s path manages the gateway-side worktree itself. - # When the bundle's worktree manager is the in-process - # LocalWorktreeManager (claude-code), allocate the worktree - # before spawning. Otherwise pass a placeholder path — the - # K3sSpawnerAdapter doesn't read it for spawn dispatch. - worktree_path: Path - worktree_created = False - try: - worktree_path = bundle.worktrees.create( - self.pipeline.id, - role, - ) - worktree_created = True - except (NotImplementedError, AttributeError): # fmt: skip - worktree_path = Path(env.get("EGG_WORKTREE_ROOT", ".")) - - # reviewer_concurrency v1 blocker #5: wrap the spawn so an - # exception (kubectl unreachable, harness import failure, - # etc.) lands in handle_agent_failure-equivalent territory - # rather than propagating up and bypassing the executor's - # recovery path. - try: - result = bundle.spawner.spawn(role, prompt_text, env, worktree_path) - except Exception as exc: # noqa: BLE001 - # reviewer v1 blocker #3: scope teardown to just this - # role's worktree. ``tear_down(pipeline_id)`` is pipeline- - # scoped and would wipe peer worktrees mid-spawn under - # concurrent dispatch. - if worktree_created: - self._teardown_role_worktree(bundle, role) - logger.exception("substrate spawn failed", role=str(role), error=str(exc)) - return AgentExecution( - role=role, - status=AgentExecutionStatus.FAILED, - container_id=f"substrate-{bundle.name}-{role.value if hasattr(role, 'value') else role}", - started_at=datetime.now(UTC), - error=f"substrate spawn raised: {exc!r}", - slice_id=self._slice_id, - ) - - # Synthesise an AgentExecution. The substrate path runs - # synchronously, so the spawn returns AFTER the agent - # finishes — we mark it COMPLETE on success / FAILED on - # non-zero exit. The caller's monitor loop then sees - # AgentExecutionStatus.COMPLETE immediately and the worktree - # can be torn down at phase teardown - # (reviewer_concurrency v1 blocker #3 — the worktree's - # lifecycle is bound to the phase, NOT the spawn function). - status = ( - AgentExecutionStatus.COMPLETE - if (getattr(result, "exit_code", 0) == 0) - else AgentExecutionStatus.FAILED - ) - if status == AgentExecutionStatus.FAILED and worktree_created: - # On a non-zero spawn, tear down just this role's worktree - # (reviewer v1 blocker #3 — pipeline-scoped teardown would - # wipe peer worktrees mid-spawn). - self._teardown_role_worktree(bundle, role) - - return AgentExecution( - role=role, - status=status, - container_id=f"substrate-{bundle.name}-{role.value if hasattr(role, 'value') else role}", - started_at=datetime.now(UTC), - slice_id=self._slice_id, - ) - - def _teardown_role_worktree(self, bundle: Any, role: AgentRole) -> None: - """Tear down a single (pipeline, role) worktree on the substrate bundle. - - Reviewer v1 blocker #3: use the worktree manager's per-role - ``remove(pipeline_id, role)`` API when available so the - failure path for one role's spawn does not wipe peer - worktrees. Falls back to the pipeline-scoped ``tear_down`` - only for managers that don't expose the per-role variant - (e.g., legacy stubs). - """ - worktrees = getattr(bundle, "worktrees", None) - if worktrees is None: - return - remove_one = getattr(worktrees, "remove", None) - try: - if callable(remove_one): - remove_one(self.pipeline.id, role) - else: - worktrees.tear_down(self.pipeline.id) - except Exception: # noqa: BLE001 — defensive - pass - def handle_agent_failure(self, role: str, error: str) -> dict[str, Any]: """Handle an agent failure during concurrent execution. diff --git a/orchestrator/substrate/__init__.py b/orchestrator/substrate/__init__.py deleted file mode 100644 index 88eeb846c8..0000000000 --- a/orchestrator/substrate/__init__.py +++ /dev/null @@ -1,469 +0,0 @@ -"""Substrate-swap package — walking-skeleton spike for issue #2623. - -The four-protocol substrate model (``AgentSpawner``, ``MessageBus``, -``PolicyEnforcer``, ``WorktreeManager``) lets egg's orchestrator run -either against the existing k3s/Redis/gateway stack OR natively inside -a Claude Code session, selected at boot via the ``EGG_SUBSTRATE`` env -var per HITL decision cq-1. - -See ``docs/architecture/claude-code-substrate.md`` for the full ADR. - -INTERFACE STABILITY: v0.x unstable. - -Multi-exception ``except`` discipline (must read before editing) ----------------------------------------------------------------- - -This package — and the whole repo (pyproject.toml's -``requires-python = ">=3.14"``) — targets Python 3.14+. Python 3.14 -introduces the parenthesless ``except A, B:`` syntax (PEP 758, 2025); -under ruff's ``target-version = "py314"`` formatter, the redundant -parens in ``except (A, B):`` are stripped to that 3.14-only shape. -On any older interpreter (3.13 and below) the stripped form is a -SyntaxError. - -We deliberately keep the parenthesised form in source for two reasons: -(1) it parses on every interpreter from 3.0 onward, so contributors -copying snippets into a 3.13 venv (or the ADR's "Python 3.14+" claim -in SKILL.md isn't honored) get a clearer error path; (2) the -parenthesised form is unambiguous to read — ``except A, B:`` shares -its grammar with a Python-2-era binding form some readers still see -in muscle memory. To preserve the parens against ``ruff format``, -multi-exception ``except`` clauses carry a trailing -``# fmt: skip``:: - - except (subprocess.SubprocessError, OSError): # fmt: skip - -The cheapest preflight is to grep for the bare form -(``grep -nE 'except [A-Za-z.]+ *, *[A-Za-z.]+ *:' orchestrator/ -plugins/``) before every commit; a CI lint rule that catches this -shape is tracked in the follow-up issue beyond #2717. - -The protocols and ``SubstrateBundle`` shape are part of a walking- -skeleton spike (cq-11). The follow-up rollout issue may reshape them -in incompatible ways; downstream consumers should not assume API -stability until the follow-up issue formally promotes the protocols. - -Usage:: - - from orchestrator.substrate import select_substrate - bundle = select_substrate(os.environ) - result = bundle.spawner.spawn(role, prompt, env, worktree) - -TODO (follow-up issue): build a single-class -``KubernetesSpawnerAdapter`` that consumes the full feature set of -``KubernetesSpawner`` (image overrides, repo volumes, slice-scoped -spawning, retry policy) instead of the minimal -``K3sSpawnerAdapter`` shim that lives here today. -""" - -from __future__ import annotations - -import threading -from collections.abc import Mapping -from dataclasses import dataclass -from typing import Any - -from .k3s_adapter import K3sSpawnerAdapter -from .message_bus import MessageBus -from .policy import PolicyEnforcer -from .spawner import AgentResult, AgentSpawner -from .worktree import WorktreeManager - -__all__ = [ - "AgentResult", - "AgentSpawner", - "K3sSpawnerAdapter", - "MessageBus", - "PolicyEnforcer", - "SubstrateBundle", - "WorktreeManager", - "select_substrate", -] - - -@dataclass -class SubstrateBundle: - """The four substrate implementations selected by ``EGG_SUBSTRATE``. - - Fields: - name: ``"k3s"`` or ``"claude-code"``. - spawner: ``AgentSpawner`` instance. - bus: ``MessageBus`` instance. - policy: ``PolicyEnforcer`` instance. - worktrees: ``WorktreeManager`` instance. - """ - - name: str - spawner: AgentSpawner - bus: MessageBus - policy: PolicyEnforcer - worktrees: WorktreeManager - - -def select_substrate( - env: Mapping[str, str], - *, - k3s_legacy_spawn_fn: Any | None = None, - k3s_message_bus: MessageBus | None = None, - k3s_policy: PolicyEnforcer | None = None, - k3s_worktrees: WorktreeManager | None = None, -) -> SubstrateBundle: - """Return the substrate bundle selected by ``EGG_SUBSTRATE``. - - Args: - env: Mapping with environment variables (typically - ``os.environ``). - k3s_legacy_spawn_fn: Optional callable returned by - ``KubernetesSpawner.create_concurrent_spawn_fn(...)``. - When supplied (and ``EGG_SUBSTRATE`` resolves to ``"k3s"``), - the bundle's ``spawner`` is a ``K3sSpawnerAdapter`` - wrapping it. - k3s_message_bus / k3s_policy / k3s_worktrees: Optional pre- - built implementations for the k3s leg. The k3s production - path already wires these up at boot via - ``orchestrator.cli.cmd_serve``; the parameters exist so - unit tests can supply lightweight test doubles without - standing up the full daemon. - - Returns: - A ``SubstrateBundle`` with the four implementations. - - Raises: - ValueError: If ``EGG_SUBSTRATE`` resolves to an unknown value. - - Notes: - The ``"k3s"`` leg always returns a *working* spawner — even - without ``k3s_legacy_spawn_fn`` supplied, the bundle's - ``spawner`` is a ``K3sSpawnerAdapter`` constructed against a - lazily-built ``KubernetesSpawner.create_concurrent_spawn_fn`` - proxy. This is the cq-1 contract: both substrates are co-equal - from day one. - - The non-spawner k3s legs (``bus``, ``policy``, ``worktrees``) - fall back to ``_K3sPlaceholder`` when no implementation is - injected. These placeholders raise ``NotImplementedError`` - when invoked. The production k3s boot path (started by - ``orchestrator.cli.cmd_serve``) does not invoke - ``select_substrate`` — it wires ``MessageStore`` / - ``RedisMessageStore`` / ``WorktreeManager`` directly — so the - placeholder is only ever reached by tests that explicitly opt - into the k3s leg without supplying overrides. This is - deliberate: it gives callers a structured "not wired up" error - rather than silently returning a non-functional bundle. - """ - name = (env.get("EGG_SUBSTRATE") or "k3s").lower() - - if name == "k3s": - spawner = _build_k3s_spawner(k3s_legacy_spawn_fn) - bus = ( - k3s_message_bus - if k3s_message_bus is not None - else _K3sPlaceholder( - "message_bus", - "Production k3s message bus is wired in orchestrator.cli.cmd_serve " - "via get_message_store(); supply k3s_message_bus= for tests.", - ) - ) - policy = ( - k3s_policy - if k3s_policy is not None - else _K3sPlaceholder( - "policy", - "Production k3s policy is gateway-enforced via " - "gateway/phase_filter.py; supply k3s_policy= for tests.", - ) - ) - worktrees = ( - k3s_worktrees - if k3s_worktrees is not None - else _K3sPlaceholder( - "worktrees", - "Production k3s worktrees are managed by the gateway sidecar " - "(gateway/worktree_manager.py); supply k3s_worktrees= for tests.", - ) - ) - return SubstrateBundle( - name="k3s", - spawner=spawner, # type: ignore[arg-type] - bus=bus, # type: ignore[arg-type] - policy=policy, # type: ignore[arg-type] - worktrees=worktrees, # type: ignore[arg-type] - ) - - if name == "claude-code": - # Lazy import — the claude_code module pulls in egg_harness - # which is heavier than the protocol surface. - from .claude_code.message_bus import InProcessMessageBus - from .claude_code.policy import PreToolUseHookPolicy - from .claude_code.spawner import ClaudeCodeSpawner - from .claude_code.worktree import LocalWorktreeManager - - return SubstrateBundle( - name="claude-code", - spawner=ClaudeCodeSpawner(role_rubric_loader=_load_egg_sdlc_role_rubric), - bus=InProcessMessageBus(), - policy=PreToolUseHookPolicy(), - worktrees=LocalWorktreeManager(), - ) - - raise ValueError(f"Unknown EGG_SUBSTRATE={name!r}; expected 'k3s' or 'claude-code'.") - - -def _build_k3s_spawner(legacy_spawn_fn: Any | None) -> AgentSpawner: - """Construct the k3s ``AgentSpawner`` for ``select_substrate``. - - Args: - legacy_spawn_fn: Optional pre-built spawn callable returned by - ``KubernetesSpawner.create_concurrent_spawn_fn(...)``. - - Returns: - Always a ``K3sSpawnerAdapter`` (directly when - ``legacy_spawn_fn`` is supplied; via a lazily-resolved factory - otherwise). The cq-1 contract — co-equal substrates from day - one — requires that ``select_substrate({})`` produce a - functional spawner, not a deferred stub. The - ``_LazyK3sSpawner`` constructs the underlying - ``KubernetesSpawner.create_concurrent_spawn_fn(...)`` on the - first ``.spawn()`` call using env-derived defaults. If the - env does not supply enough configuration (e.g. - ``EGG_PIPELINE_ID`` is missing), the underlying spawner - raises a clear ``ValueError`` rather than a ``NotImplementedError`` - — operators learn that the k3s leg needs the same env vars - the daemon's ``cmd_serve`` consumes. - """ - if legacy_spawn_fn is not None: - return K3sSpawnerAdapter(legacy_spawn_fn) - return _LazyK3sSpawner() - - -#: Per-role mapping naming which #2717 rollout slice ships the rubric -#: markdown for that role. Updated as each slice lands its -#: documenter-owned rubric files. Roles absent from this map are -#: "deferred indefinitely" (overseer / inspector / autofixer / -#: conflict_resolver — intentionally unhandled per task-3-6). -#: -#: Source of truth for "is this role part of the rollout?". Whether -#: the rubric *file* has actually landed on disk is checked by the -#: loader via ``Path.is_file()`` — no parallel "landed roles" registry -#: that could drift from the filesystem state. -#: -#: The slice numbers match issue #2717's plan: -#: slice-1: refiner (already shipped in #2715) + 2 refine reviewers -#: (task-1-4 documenter). -#: slice-2: 3 plan producers + reviewer_plan (task-2-3 documenter). -#: slice-3: 3 implement producers + 5 implement reviewers -#: (task-3-4 / task-3-5 documenter). -_ROLE_RUBRIC_SLICES: dict[str, str] = { - # Slice-1 (refine team). - "refiner": "slice-1", - "reviewer_refine": "slice-1", - "reviewer_agent_design": "slice-1", - # Slice-2 (plan team). - "architect": "slice-2", - "task_planner": "slice-2", - "risk_analyst": "slice-2", - "reviewer_plan": "slice-2", - # Slice-3 (implement team). - "coder": "slice-3", - "tester": "slice-3", - "documenter": "slice-3", - "reviewer_code": "slice-3", - "reviewer_code_holistic": "slice-3", - "reviewer_contract": "slice-3", - "reviewer_security": "slice-3", - "reviewer_concurrency": "slice-3", -} - -#: Slices whose rubric set has landed on this loader. Roles whose -#: ``_ROLE_RUBRIC_SLICES`` entry references a slice NOT in this -#: frozenset (or whose rubric file hasn't been added to -#: ``plugins/egg-sdlc/skills/egg-sdlc/agents/`` yet) raise -#: ``ValueError`` with a structured pointer to the slice that lands -#: their rubric. -#: -#: Each subsequent slice EXTENDS this set (slice-2 → ``{"slice-1", -#: "slice-2"}``, slice-3 → ``{"slice-1", "slice-2", "slice-3"}``) -#: rather than replacing it — otherwise slice-2's loader would fence -#: off slice-1's already-landed roles, regressing earlier slices. -_LANDED_SLICES: frozenset[str] = frozenset({"slice-1", "slice-2"}) - - -def _load_egg_sdlc_role_rubric(role: Any) -> str: - """Load the role rubric markdown from ``plugins/egg-sdlc/skills/egg-sdlc/agents/.md``. - - Reviewer v1 blocker #5: without an injected loader, the default - fallback was a one-line "you are " string and the 119-line - refiner rubric file was dead. The production wiring of the - claude-code substrate must point ``ClaudeCodeSpawner`` at the - plugin's per-role markdown so ``build_system_prompt(sources)`` - actually receives the rubric (the structural depth fix from - #2622). - - Issue #2717 rollout scope: slice-1 added the refine team - (refiner + reviewer_refine + reviewer_agent_design); slice-2 - extends the rubric-supported set to the plan team (architect + - task_planner + risk_analyst + reviewer_plan). Implement-team - roles continue to raise ``ValueError`` with a pointer to the - slice (slice-3) that ships their rubric, so the structured-error - contract for missing rubrics stays consistent across the - rollout. The mapping lives in ``_ROLE_RUBRIC_SLICES`` so future - slices can extend it without touching this loader's body. - - Args: - role: ``AgentRole`` (or a string-equivalent) identifying the - role to load. - - Returns: - Markdown body of the rubric file (frontmatter retained — the - frontmatter is informational only per ``refiner.md``). - - Raises: - ValueError: when the role is not yet in the rubric-supported - set for this slice, OR when the supported-role's rubric - file does not exist on disk (typically because the - documenter hasn't landed it yet within the same slice; - sequence TASK-1-4 → TASK-1-6 within slice-1). - """ - from pathlib import Path as _Path - - role_name = role.value if hasattr(role, "value") else str(role) - here = _Path(__file__).resolve() - # orchestrator/substrate/__init__.py → repo root. - # TODO(cq-12 follow-up): once the egg Python packages publish to - # pip and the egg-sdlc plugin no longer relies on the from-source - # install, this `parent.parent.parent / plugins / …` walk breaks - # — site-packages does not co-locate the plugins directory. The - # follow-up should swap to a packaging-aware resolution (e.g. - # ``importlib.resources.files("egg_sdlc_plugin").joinpath(...)`` - # behind a published namespace), with a from-source fallback for - # the walking-skeleton install path. See SKILL.md's install - # section and the bridge-gap callout. - repo_root = here.parent.parent.parent - rubric_path = ( - repo_root / "plugins" / "egg-sdlc" / "skills" / "egg-sdlc" / "agents" / f"{role_name}.md" - ) - - # Fence: roles not in _ROLE_RUBRIC_SLICES are "indefinitely - # deferred" (overseer / inspector / autofixer / conflict_resolver - # per task-3-6). Path-traversal role names (e.g. "../../../etc/ - # passwd") also land here because their normalised form is not - # a registered role — we raise BEFORE any filesystem touch so the - # loader cannot be used as an existence oracle on attacker- - # controlled paths. - slice_hint = _ROLE_RUBRIC_SLICES.get(role_name) - if slice_hint is None: - raise ValueError( - f"egg-sdlc role rubric missing for role={role_name!r}. " - "This role is not part of the #2717 rollout's rubric " - "set; if your pipeline needs it, file a follow-up issue." - ) - - # Roles whose rubric is scheduled for a later slice raise without - # filesystem touch. (A future-slice rubric *might* exist on disk - # ahead of its scheduled load — e.g. a reviewer pre-landing a - # rubric file — but the loader should still fence it off until - # the slice that wires up the role lands, so the rollout-DAG - # contract is observable structurally.) - if slice_hint not in _LANDED_SLICES: - raise ValueError( - f"egg-sdlc role rubric for role={role_name!r} is deferred to " - f"follow-up {slice_hint} of issue #2717's rollout. " - "See docs/architecture/claude-code-substrate.md for the slice DAG." - ) - - # Landed-slice role: the file MUST exist on disk. If it doesn't, - # the documenter's task within that slice is still in flight and - # the loader cannot yet be exercised. Surface as a clear "rubric - # missing on disk in " error so the reviewer / operator - # knows which task is still pending. - if not rubric_path.is_file(): - raise ValueError( - f"egg-sdlc role rubric missing on disk at {rubric_path} for " - f"role={role_name!r}. The role is scheduled for " - f"{slice_hint} (already landed per _LANDED_SLICES) but the " - "markdown file has not been added to plugins/egg-sdlc/skills/" - "egg-sdlc/agents/ yet — sequence the documenter's rubric task " - "(e.g. TASK-1-4 for slice-1's refine reviewers, TASK-2-3 for " - "slice-2's plan team) before the loader update (TASK-1-6 / " - "TASK-2-2) within the same slice." - ) - return rubric_path.read_text(encoding="utf-8") - - -class _LazyK3sSpawner: - """``AgentSpawner`` that lazily constructs the real k3s factory. - - Satisfies the cq-1 "co-equal substrates from day one" contract: - ``select_substrate({})`` returns a functional spawner even when - no ``k3s_legacy_spawn_fn`` was injected. On the first - ``.spawn()`` call, the lazy adapter: - - 1. Reads ``EGG_PIPELINE_ID`` / ``EGG_GATEWAY_MODE`` from ``env``. - 2. Instantiates a ``KubernetesSpawner`` (lazy k8s/gateway - clients). - 3. Calls ``create_concurrent_spawn_fn(...)`` to build the legacy - spawn callable. - 4. Wraps it in a ``K3sSpawnerAdapter`` and delegates. - - This means a unit test or out-of-band caller who runs the k3s - bundle without injecting a spawn fn will reach a real factory - call. If the runtime environment lacks the k3s prereqs (e.g. - no kubectl on PATH, no gateway URL), the underlying call raises - with the same error message a real ``cmd_serve`` boot would - produce — preferable to a silent ``NotImplementedError``. - """ - - def __init__(self) -> None: - self._adapter: K3sSpawnerAdapter | None = None - self._lock = threading.RLock() - - def _build_adapter(self, env: Mapping[str, str]) -> K3sSpawnerAdapter: - try: - from orchestrator.kubernetes_spawner import KubernetesSpawner - except ImportError: # pragma: no cover - from kubernetes_spawner import ( # type: ignore[no-redef, import-untyped] - KubernetesSpawner, - ) - pipeline_id = env.get("EGG_PIPELINE_ID") or env.get("PIPELINE_ID") or "unknown" - mode = env.get("EGG_GATEWAY_MODE") or env.get("MODE") or "local" - spawner = KubernetesSpawner() - legacy_fn = spawner.create_concurrent_spawn_fn( - pipeline_id=pipeline_id, - issue_number=None, - repo_volumes=None, - mode=mode, - repos=None, - phase=env.get("EGG_PHASE"), - ) - return K3sSpawnerAdapter(legacy_fn) - - def spawn( - self, - role: Any, - prompt: str, - env: Mapping[str, str], - worktree: Any, - ) -> AgentResult: - with self._lock: - if self._adapter is None: - self._adapter = self._build_adapter(env) - return self._adapter.spawn(role, prompt, env, worktree) - - -class _K3sPlaceholder: - """Placeholder for the bus/policy/worktree slots of the k3s leg - when no real implementation is injected. - - Provides a typed "not wired up" error so the failure mode is - actionable. The production k3s daemon wires real implementations - directly; tests that need the k3s leg must inject them. - """ - - def __init__(self, kind: str, hint: str) -> None: - self._kind = kind - self._hint = hint - - def __getattr__(self, name: str) -> Any: - raise NotImplementedError( - f"k3s {self._kind} placeholder has no method {name!r}. {self._hint}" - ) diff --git a/orchestrator/substrate/_plan_phase.py b/orchestrator/substrate/_plan_phase.py deleted file mode 100644 index 34174aefda..0000000000 --- a/orchestrator/substrate/_plan_phase.py +++ /dev/null @@ -1,818 +0,0 @@ -"""Plan-phase BRC pipeline body for the in-process Claude Code substrate. - -Extracted from ``orchestrator/substrate/in_process.py`` (#2717 slice-2) -so the in-process generator file stays under the repo's 1500-line -hard cap (``scripts/file-size-allowlist.yaml``). The plan-phase -helpers live here as module-level functions that take the -``_InProcessOrchestrator`` instance as their first argument — this -keeps the public method surface on the class identical (the class's -``_run_plan_phase`` is a thin wrapper that delegates here) while -moving ~700 lines of body out of the generator module. - -Why module-level functions instead of a sub-package: the plan-phase -body is a single linear flow (spawn-architect → fan-out → spawn- -reviewer → parse-verdicts → confirm); a sub-package per the -``docs/guides/decomposition-pattern.md`` pattern is overkill at -this size and would obscure the architect-first ordering. The -function-with-runner-instance pattern keeps state explicit and -mirrors how ``orchestrator/concurrent_executor.py`` exposes its -per-phase helpers. - -See the orchestrator's ``_run_plan_phase`` docstring for the -end-to-end design narrative; this module owns the implementation. -""" - -from __future__ import annotations - -import json -from collections.abc import Mapping -from pathlib import Path -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: # pragma: no cover - from .in_process import _InProcessOrchestrator - - -def run_plan_phase( - runner: _InProcessOrchestrator, - refine_artifact_path: Path, -) -> tuple[Path, dict[str, Any]]: - """Run the plan-phase BRC cycle. See ``_InProcessOrchestrator._run_plan_phase``. - - Wraps the heartbeat-phase flip around the body so HEARTBEAT - messages carry ``phase="plan"`` for the duration of the stage - and through the subsequent plan HITL gate. - """ - from concurrent.futures import ThreadPoolExecutor, as_completed - - from egg_contracts.agent_roles import AgentRole - - from . import select_substrate - - try: - from orchestrator.peer_consensus import ( - create_peer_consensus_tracker, - get_peer_consensus_tracker, - ) - from orchestrator.review_graph import get_review_graph_for_phase - except ImportError: # pragma: no cover - from peer_consensus import ( # type: ignore[no-redef, import-untyped] - create_peer_consensus_tracker, - get_peer_consensus_tracker, - ) - from review_graph import ( # type: ignore[no-redef, import-untyped] - get_review_graph_for_phase, - ) - - runner._current_phase = "plan" - # Reviewer_code v2 non-blocking N10: clear the refine-phase - # active-role sentinel before the plan-producer fan-out. Three - # plan producers can hold the role concurrently, so the single- - # valued sentinel cannot disambiguate them (per ``spawn_plan_producer`` - # docstring). Without this clear, the PreToolUse hook's fallback - # path resolves to the stale ``refiner`` role for whichever - # plan-producer's nested dispatch loses the EGG_AGENT_ROLE env-var - # race; the refiner's allow-list overlaps with architect's but not - # universally. Clearing the sentinel makes the fallback resolve to - # "no role known" rather than the wrong role. - runner._teardown_sentinel() - return _run_plan_phase_inner( - runner, - refine_artifact_path, - bundle_factory=select_substrate, - executor_factory=ThreadPoolExecutor, - as_completed_fn=as_completed, - agent_role_module=AgentRole, - create_tracker=create_peer_consensus_tracker, - get_tracker=get_peer_consensus_tracker, - graph_factory=get_review_graph_for_phase, - ) - - -def _run_plan_phase_inner( - runner: _InProcessOrchestrator, - refine_artifact_path: Path, - *, - bundle_factory: Any, - executor_factory: Any, - as_completed_fn: Any, - agent_role_module: Any, - create_tracker: Any, - get_tracker: Any, - graph_factory: Any, -) -> tuple[Path, dict[str, Any]]: - """Plan-phase body. Parameters accept the lazily-imported primitives so - the outer wrapper owns the imports and this body is import-error-free.""" - bundle = getattr(runner, "_bundle", None) - if bundle is None: - bundle = bundle_factory(runner.env) - runner._bundle = bundle - - drafts_dir, _, _ = runner._ensure_state_dirs() - artifact_id = runner.issue_number or runner.pipeline_id - plan_artifact_path = drafts_dir / f"{artifact_id}-plan.md" - - architect_role = agent_role_module.ARCHITECT - downstream_producers: list[Any] = [ - agent_role_module.TASK_PLANNER, - agent_role_module.RISK_ANALYST, - ] - plan_producers: list[Any] = [architect_role, *downstream_producers] - plan_reviewer = agent_role_module.REVIEWER_PLAN - - graph = graph_factory("plan", repo=runner.repo) - tracker = get_tracker(runner.pipeline_id) - if tracker is None: - tracker = create_tracker(runner.pipeline_id, graph, cooldown_seconds=0) - for role in (*plan_producers, plan_reviewer): - tracker.register_agent(role.value) - runner._plan_tracker = tracker - - producer_results: dict[Any, Any] = {} - producer_artifacts: dict[Any, Path] = {} - - # Stage 4a: architect spawns FIRST, synchronously. - architect_artifact, architect_result = spawn_plan_producer( - runner, - architect_role, - bundle, - refine_artifact_path, - plan_artifact_path, - architect_output_path=None, - ) - producer_results[architect_role] = architect_result - producer_artifacts[architect_role] = architect_artifact - architect_output_path = plan_producer_output_path(runner, architect_role) - _record_producer_propose(runner, tracker, architect_role, architect_artifact, architect_result) - - # Reviewer_code v2 non-blocking N9: defensive handoff check. The - # downstream producers receive ``EGG_ARCHITECT_OUTPUT_PATH`` and - # may try to read it at start-up; if the architect crashed AFTER - # ``bundle.spawner.spawn`` returned exit_code 0 but BEFORE writing - # the JSON, the fan-out below would dispatch with a dangling - # pointer. Surface the broken handoff up-front by NACKing the - # architect edge AND skipping both the downstream fan-out and the - # reviewer spawn so the NACK is the dominant signal at the plan- - # HITL gate (reviewer_code v3 non-blocking NB1 + NB3 / #2717 - # slice-2). Without the early-return the fan-out would still - # dispatch two subagents with a dangling handoff env-var, and the - # subsequent reviewer's optimistic-ACK fallback would silently - # clobber this NACK before the operator ever saw it. - architect_exit = int(getattr(architect_result, "exit_code", 0) or 0) - architect_handoff_broken = architect_exit == 0 and not architect_output_path.is_file() - if architect_handoff_broken: - try: - tracker.handle_nack( - plan_reviewer.value, - architect_role.value, - { - "artifact_references": [str(architect_output_path)], - "reason": ( - f"architect spawn returned exit_code=0 but " - f"{architect_output_path} was not written — " - "downstream task_planner / risk_analyst would " - "see a dangling EGG_ARCHITECT_OUTPUT_PATH. " - "Plan-phase fail-fast (#2717 slice-2 N9)." - ), - }, - ) - except Exception as exc: # noqa: BLE001 — defensive - log_tracker_warning( - "handle_nack", - f"{plan_reviewer.value}→{architect_role.value}", - exc, - runner.pipeline_id, - ) - runner._verdict_diagnostics = { - "verdict_path": None, - "verdicts": {}, - "reviewer_exit_code": "", - "architect_handoff_broken": True, - } - else: - # Stage 4b: task_planner + risk_analyst fan out concurrently. - with executor_factory(max_workers=len(downstream_producers)) as pool: - future_map = { - pool.submit( - spawn_plan_producer, - runner, - role, - bundle, - refine_artifact_path, - plan_artifact_path, - architect_output_path, - ): role - for role in downstream_producers - } - for fut in as_completed_fn(future_map): - role = future_map[fut] - try: - artifact_path, spawn_result = fut.result() - except Exception as exc: # noqa: BLE001 — defensive - producer_results[role] = exc - producer_artifacts[role] = plan_artifact_path - continue - producer_results[role] = spawn_result - producer_artifacts[role] = artifact_path - _record_producer_propose(runner, tracker, role, artifact_path, spawn_result) - - # Stage 4c: reviewer_plan + verdict-JSON parsing. - reviewer_artifact, reviewer_result = spawn_plan_reviewer( - runner, bundle, producer_artifacts, plan_artifact_path - ) - producer_results[plan_reviewer] = reviewer_result - producer_artifacts[plan_reviewer] = reviewer_artifact - - verdict_path, verdicts = read_plan_reviewer_verdicts(runner, plan_producers=plan_producers) - runner._verdict_diagnostics = { - "verdict_path": str(verdict_path) if verdict_path else None, - "verdicts": verdicts, - "reviewer_exit_code": int(getattr(reviewer_result, "exit_code", 0) or 0), - } - _apply_reviewer_verdicts( - runner, - tracker, - plan_reviewer, - plan_producers, - producer_artifacts, - producer_results, - reviewer_result, - verdicts, - ) - - # Stage 4d: drive CONSENSUS_CONFIRMED on each agent. On the N9 - # fail-fast path the architect edge is NACKED and the downstream - # producers + reviewer never proposed, so ``handle_confirmed`` - # raises for each role; ``evaluate()`` then reports - # ``is_complete=False`` and the plan-HITL gate surfaces the - # retry / abort options to the operator. - for role in (*plan_producers, plan_reviewer): - try: - tracker.handle_confirmed(role.value) - except Exception as exc: # noqa: BLE001 — defensive - log_tracker_warning("handle_confirmed", role.value, exc, runner.pipeline_id) - - plan_eval = tracker.evaluate() - - if not plan_artifact_path.exists(): - plan_artifact_path.write_text( - format_plan_placeholder( - pipeline_id=runner.pipeline_id, - issue_number=runner.issue_number, - repo=runner.repo, - plan_producers=[role.value for role in plan_producers], - plan_reviewer=plan_reviewer.value, - producer_results=producer_results, - plan_eval=plan_eval, - verdict_diagnostics=runner._verdict_diagnostics, - ) - ) - - return plan_artifact_path, plan_eval - - -def plan_producer_output_path(runner: _InProcessOrchestrator, role: Any) -> Path: - """Return ``.egg-state/agent-outputs/--output.json``.""" - runner._ensure_state_dirs() - outputs_dir = runner.state_root / "agent-outputs" - outputs_dir.mkdir(parents=True, exist_ok=True) - artifact_id = runner.issue_number or runner.pipeline_id - return outputs_dir / f"{artifact_id}-{role.value}-output.json" - - -def _record_producer_propose( - runner: _InProcessOrchestrator, - tracker: Any, - role: Any, - artifact_path: Path, - spawn_result: Any, -) -> None: - """Record CONSENSUS_PROPOSE for a producer when its spawn succeeded.""" - exit_code = int(getattr(spawn_result, "exit_code", 0) or 0) - if exit_code != 0: - return - commit_sha = getattr(spawn_result, "commit_sha", None) or synthetic_commit_for(role.value) - try: - tracker.handle_propose( - role.value, - { - "summary": ( - f"{role.value} produced plan-phase artifact at " - f"{artifact_path} via the in-process Claude " - "Code substrate (#2717 slice-2)." - ), - "artifacts": [str(artifact_path)], - "commit_sha": commit_sha, - }, - ) - except Exception as exc: # noqa: BLE001 — defensive - log_tracker_warning("handle_propose", role.value, exc, runner.pipeline_id) - - -def read_plan_reviewer_verdicts( - runner: _InProcessOrchestrator, - *, - plan_producers: list[Any] | None = None, -) -> tuple[Path | None, dict[str, dict[str, Any]]]: - """Parse the reviewer_plan verdict JSON if present. - - Two schemas are accepted to align with the rubric the documenter - shipped (``plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md``) - AND a more granular extension shape: - - 1. **Rubric-default (single-verdict, broadcast).** The rubric - documents the JSON object as a single top-level verdict - (``verdict`` ∈ {"ACK", "NACK"}, ``analysis`` carrying the - eight criteria, ``feedback`` blob, ``artifact_references``). - When this is the shape on disk, the verdict is broadcast to - every plan producer edge — ACK acks all three, NACK nacks - all three with ``feedback`` as the per-edge reason. This is - the "Option (c)" resolution from reviewer_code_holistic v3 - NACK blocker H3. - 2. **Per-producer extension (per-edge).** When the verdict JSON - carries a ``per_producer`` mapping of - ``{role_name: {"verdict": "ACK"|"NACK", "reason": str, ...}}`` - entries, per-edge semantics override the broadcast: each - edge's verdict is taken from the matching entry. A reviewer - that wants edge granularity (e.g. ACK architect + NACK - task_planner) writes the wrapper; the rubric's default - single-verdict shape stays broadcast-compatible. - - Returns ``(verdict_path, verdicts)``. ``verdicts`` is empty - when the file is missing or the JSON is unparseable; the - orchestrator's fail-closed heuristic in - ``_apply_reviewer_verdicts`` treats that as NACK only when - the reviewer's spawn itself failed. - """ - outputs_dir = runner.state_root / "agent-outputs" - artifact_id = runner.issue_number or runner.pipeline_id - verdict_path = outputs_dir / f"{artifact_id}-reviewer_plan-output.json" - if not verdict_path.is_file(): - return None, {} - try: - blob = json.loads(verdict_path.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError): # fmt: skip - return verdict_path, {} - if not isinstance(blob, dict): - return verdict_path, {} - - # Schema 2: per-producer extension wrapper takes precedence if - # it's a well-formed dict. Reviewers that want per-edge - # granularity opt into it explicitly. - per_producer = blob.get("per_producer") - if isinstance(per_producer, dict) and per_producer: - normalised: dict[str, dict[str, Any]] = {} - for role_name, entry in per_producer.items(): - if not isinstance(entry, dict): - continue - verdict = str(entry.get("verdict", "")).strip().upper() - if verdict not in {"ACK", "NACK"}: - continue - # pre_merge_condition is a BRC concept for code-merge - # obligations on a PR — plan-phase produces a markdown - # plan document, so the field has no consumer here - # (reviewer_code v2 non-blocking N6 / #2717 slice-2). - normalised[str(role_name)] = { - "verdict": verdict, - "reason": str(entry.get("reason", "")), - "artifact_references": list(entry.get("artifact_references") or []), - } - if normalised: - return verdict_path, normalised - - # Schema 1: rubric-default single-verdict broadcast. The rubric - # specifies ``verdict``, ``analysis``, ``feedback``, - # ``artifact_references`` at the top level. NACK propagates the - # ``feedback`` blob into every producer's per-edge reason so - # the operator sees the same revision instructions on each - # tracker edge. - top_verdict = str(blob.get("verdict", "")).strip().upper() - if top_verdict in {"ACK", "NACK"}: - # When the caller hasn't told us which producers to - # broadcast across (legacy callers), the broadcast is - # impossible — return empty and let the orchestrator's - # fail-closed / optimistic-ACK heuristic apply. - if not plan_producers: - return verdict_path, {} - feedback = str(blob.get("feedback", "")).strip() - # NACK with an empty feedback blob would hit - # ReviewPayload.validate_nack_has_reason. Synthesise a - # placeholder so the tracker records the NACK rather than - # silently losing it (reviewer_code v3 non-blocking #1). - broadcast_reason = ( - feedback - or f"reviewer_plan broadcast {top_verdict}: top-level verdict " - "without a per-edge feedback blob — see verdict JSON for the " - "criteria-keyed analysis." - ) - broadcast_refs = list(blob.get("artifact_references") or []) - # pre_merge_condition is a BRC concept for code-merge - # obligations on a PR — plan-phase has no PR / merge surface, - # so the field is intentionally not propagated here - # (reviewer_code v2 non-blocking N6 / #2717 slice-2). - broadcast = { - "verdict": top_verdict, - "reason": broadcast_reason, - "artifact_references": broadcast_refs, - } - return verdict_path, {role.value: broadcast for role in plan_producers} - - return verdict_path, {} - - -def _apply_reviewer_verdicts( - runner: _InProcessOrchestrator, - tracker: Any, - reviewer_role: Any, - plan_producers: list[Any], - producer_artifacts: Mapping[Any, Path], - producer_results: Mapping[Any, Any], - reviewer_result: Any, - verdicts: Mapping[str, Mapping[str, Any]], -) -> None: - """Apply ACK / NACK to the tracker per the reviewer's verdict JSON. - - Reviewer_code_holistic v1 blocker H2: under v1 the orchestrator - forged ACKs based purely on exit_code. v2: parse the verdict - file; fail-closed (NACK every edge) when missing AND the - reviewer's spawn failed; preserve the harness-faked test path - (verdict file missing AND reviewer exit_code 0) as an optimistic - ACK with a diagnostic surface in the placeholder body. - """ - reviewer_exit_code = int(getattr(reviewer_result, "exit_code", 0) or 0) - verdict_file_present = bool(verdicts) - fail_closed = (not verdict_file_present) and reviewer_exit_code != 0 - - for producer in plan_producers: - producer_run = producer_results.get(producer) - if isinstance(producer_run, Exception): - continue - producer_exit = int(getattr(producer_run, "exit_code", 0) or 0) - if producer_exit != 0: - continue - - entry = verdicts.get(producer.value) - if entry is None: - if fail_closed: - _record_reviewer_nack( - runner, - tracker, - reviewer_role, - producer, - producer_artifacts, - reason=( - "reviewer_plan verdict file missing / unparseable " - f"AND reviewer exit_code={reviewer_exit_code} — " - "in-process orchestrator fail-closes per " - "#2717 slice-2 v2 blocker H2." - ), - ) - continue - _record_reviewer_ack( - runner, - tracker, - reviewer_role, - producer, - producer_artifacts, - reason=( - "reviewer_plan ACK (synthetic): verdict file absent " - "AND reviewer exit_code=0 — in-process synchronous-" - "spawn-as-signal default per #2717 slice-2." - ), - ) - continue - - if entry["verdict"] == "ACK": - _record_reviewer_ack( - runner, - tracker, - reviewer_role, - producer, - producer_artifacts, - reason=entry.get("reason", ""), - artifact_references=entry.get("artifact_references"), - ) - else: # entry["verdict"] == "NACK" - _record_reviewer_nack( - runner, - tracker, - reviewer_role, - producer, - producer_artifacts, - reason=entry.get("reason", ""), - artifact_references=entry.get("artifact_references"), - ) - - -def _record_reviewer_ack( - runner: _InProcessOrchestrator, - tracker: Any, - reviewer_role: Any, - producer: Any, - producer_artifacts: Mapping[Any, Path], - *, - reason: str = "", - artifact_references: Any | None = None, -) -> None: - refs = list(artifact_references or [str(producer_artifacts.get(producer, ""))]) - if not refs or not refs[0]: - refs = [str(producer_artifacts.get(producer, ""))] - payload: dict[str, Any] = { - "artifact_references": refs, - "reason": reason - or ( - "reviewer_plan ACK in #2717 slice-2: producer artifact " - "structurally valid; reviewer verdict JSON not parsed " - "(see _verdict_diagnostics)." - ), - } - try: - tracker.handle_ack(reviewer_role.value, producer.value, payload) - except Exception as exc: # noqa: BLE001 — defensive - log_tracker_warning( - "handle_ack", - f"{reviewer_role.value}→{producer.value}", - exc, - runner.pipeline_id, - ) - - -def _record_reviewer_nack( - runner: _InProcessOrchestrator, - tracker: Any, - reviewer_role: Any, - producer: Any, - producer_artifacts: Mapping[Any, Path], - *, - reason: str, - artifact_references: Any | None = None, -) -> None: - refs = list(artifact_references or [str(producer_artifacts.get(producer, ""))]) - if not refs or not refs[0]: - refs = [str(producer_artifacts.get(producer, ""))] - try: - tracker.handle_nack( - reviewer_role.value, - producer.value, - {"artifact_references": refs, "reason": reason}, - ) - except Exception as exc: # noqa: BLE001 — defensive - log_tracker_warning( - "handle_nack", - f"{reviewer_role.value}→{producer.value}", - exc, - runner.pipeline_id, - ) - - -def spawn_plan_producer( - runner: _InProcessOrchestrator, - role: Any, - bundle: Any, - refine_artifact_path: Path, - plan_artifact_path: Path, - architect_output_path: Path | None = None, -) -> tuple[Path, Any]: - """Dispatch a single plan-phase producer via the substrate. - - Reviewer_concurrency v1 blocker #1: does NOT write the - active-role sentinel under concurrent dispatch. Each spawn - carries ``EGG_AGENT_ROLE`` in its own env so the hook's primary - role-resolution channel is per-spawn correct; the single-valued - ``$HOME/.claude/egg-active-role.json`` sentinel cannot - disambiguate three concurrent role-holders. - """ - worktree = bundle.worktrees.create(runner.pipeline_id, role) - producer_output_path = plan_producer_output_path(runner, role) - - spawn_env = { - **runner.env, - "EGG_PIPELINE_ID": runner.pipeline_id, - "EGG_AGENT_ROLE": role.value, - "EGG_REPO_ROOT": str(worktree), - "EGG_WORKTREE_ROOT": str(worktree), - "EGG_PHASE": "plan", - "EGG_REFINE_ARTIFACT_PATH": str(refine_artifact_path), - "EGG_PLAN_ARTIFACT_PATH": str(plan_artifact_path), - "EGG_PRODUCER_OUTPUT_PATH": str(producer_output_path), - } - if architect_output_path is not None: - spawn_env["EGG_ARCHITECT_OUTPUT_PATH"] = str(architect_output_path) - if runner.repo: - spawn_env["EGG_REPO"] = runner.repo - if runner.issue_number is not None: - spawn_env["EGG_ISSUE_NUMBER"] = str(runner.issue_number) - - prompt_lines = [ - f"Plan-phase {role.value} dispatch for pipeline " - f"{runner.pipeline_id} (issue={runner.issue_number or ''}).", - f"Refine artifact: {refine_artifact_path}", - f"Plan artifact target: {plan_artifact_path}", - f"Your handoff JSON target: {producer_output_path}", - ] - if architect_output_path is not None: - prompt_lines.append(f"Architect handoff input: {architect_output_path}") - prompt_text = "\n".join(prompt_lines) + "\n" - - spawn_result = bundle.spawner.spawn(role, prompt_text, spawn_env, worktree) - return plan_artifact_path, spawn_result - - -def spawn_plan_reviewer( - runner: _InProcessOrchestrator, - bundle: Any, - producer_artifacts: Mapping[Any, Path], - plan_artifact_path: Path, -) -> tuple[Path, Any]: - """Dispatch reviewer_plan once and return its ``AgentResult``. - - Reviewer dispatches solo (no concurrent role-holder), so the - single-valued sentinel correctly identifies the active role for - any nested-dispatch fallback the reviewer's subagents might - trigger. - """ - from egg_contracts.agent_roles import AgentRole - - worktree = bundle.worktrees.create(runner.pipeline_id, AgentRole.REVIEWER_PLAN) - - per_role_inputs: dict[str, str] = {} - for role in producer_artifacts: - if role is AgentRole.REVIEWER_PLAN: - continue - per_role_inputs[f"EGG_{role.value.upper()}_OUTPUT_PATH"] = str( - plan_producer_output_path(runner, role) - ) - - artifact_id = runner.issue_number or runner.pipeline_id - reviewer_verdict_path = ( - runner.state_root / "agent-outputs" / f"{artifact_id}-reviewer_plan-output.json" - ) - - spawn_env = { - **runner.env, - "EGG_PIPELINE_ID": runner.pipeline_id, - "EGG_AGENT_ROLE": AgentRole.REVIEWER_PLAN.value, - "EGG_REPO_ROOT": str(worktree), - "EGG_WORKTREE_ROOT": str(worktree), - "EGG_PHASE": "plan", - "EGG_PLAN_ARTIFACT_PATH": str(plan_artifact_path), - "EGG_REVIEWER_VERDICT_PATH": str(reviewer_verdict_path), - **per_role_inputs, - } - if runner.repo: - spawn_env["EGG_REPO"] = runner.repo - if runner.issue_number is not None: - spawn_env["EGG_ISSUE_NUMBER"] = str(runner.issue_number) - - runner._write_active_role_sentinel(AgentRole.REVIEWER_PLAN.value) - - per_role_inputs_summary = ", ".join( - f"{k.lower()}={v}" for k, v in sorted(per_role_inputs.items()) - ) - prompt_text = ( - f"Plan-phase reviewer_plan dispatch for pipeline " - f"{runner.pipeline_id} (issue={runner.issue_number or ''}).\n" - f"Plan artifact target: {plan_artifact_path}\n" - f"Producer handoff JSON inputs: {per_role_inputs_summary}\n" - f"Your verdict JSON target: {reviewer_verdict_path}\n" - ) - - spawn_result = bundle.spawner.spawn(AgentRole.REVIEWER_PLAN, prompt_text, spawn_env, worktree) - return plan_artifact_path, spawn_result - - -def format_plan_placeholder( - *, - pipeline_id: str, - issue_number: int | None, - repo: str | None, - plan_producers: list[str], - plan_reviewer: str, - producer_results: Mapping[Any, Any], - plan_eval: Mapping[str, Any], - verdict_diagnostics: Mapping[str, Any] | None = None, -) -> str: - """Render the plan-artifact placeholder body.""" - verdict_diagnostics = verdict_diagnostics or {} - - lines: list[str] = [ - "# Plan analysis (placeholder — plan producers did not land a full plan)", - "", - f"Pipeline: {pipeline_id}", - f"Repo: {repo or ''}", - f"Issue: {issue_number if issue_number is not None else ''}", - "", - "## Per-producer diagnostics", - "", - ] - for producer in plan_producers: - lines.append(_render_role_diagnostics(producer, producer_results)) - - lines.append("") - lines.append("## reviewer_plan diagnostics") - lines.append("") - lines.append(_render_role_diagnostics(plan_reviewer, producer_results)) - - lines.append("") - lines.append("## reviewer_plan verdict parsing") - lines.append("") - verdict_path = verdict_diagnostics.get("verdict_path") - verdicts = verdict_diagnostics.get("verdicts") or {} - reviewer_exit_code = verdict_diagnostics.get("reviewer_exit_code", "") - lines.append(f"- verdict_path: {verdict_path or ''}") - lines.append(f"- reviewer_exit_code: {reviewer_exit_code}") - if verdicts: - lines.append("- per_producer:") - for role_name in sorted(verdicts.keys()): - entry = verdicts[role_name] - lines.append( - f" - {role_name}: verdict={entry.get('verdict')!r}; " - f"reason={(entry.get('reason') or '')[:200]!r}" - ) - else: - lines.append("- per_producer: — reviewer did not write a parseable verdict JSON") - - lines.append("") - lines.append("## BRC evaluation snapshot") - lines.append("") - lines.append(f"- is_complete: {bool(plan_eval.get('is_complete'))}") - lines.append(f"- blocking_agents: {list(plan_eval.get('blocking_agents') or [])!r}") - nack_details = plan_eval.get("unresolved_nack_details") or [] - lines.append(f"- unresolved_nack_details: {list(nack_details)!r}") - lines.append("") - lines.append( - "This placeholder was emitted by `run_pipeline_in_process._run_plan_phase` " - "because the substrate's plan producers did not land the canonical " - "plan artifact themselves. Inspect the per-producer + reviewer " - "diagnostics above and the BRC snapshot to decide retry/abort at the " - "plan HITL gate." - ) - return "\n".join(lines) + "\n" - - -def _render_role_diagnostics(role_name: str, producer_results: Mapping[Any, Any]) -> str: - """Render the per-role diagnostics block (exit code + commit + stdout).""" - result = next( - (r for k, r in producer_results.items() if getattr(k, "value", str(k)) == role_name), - None, - ) - if result is None: - return f"### {role_name}\n\n- \n" - if isinstance(result, Exception): - return f"### {role_name}\n\n- exception: {result!r}\n" - exit_code = int(getattr(result, "exit_code", 0) or 0) - commit_sha = getattr(result, "commit_sha", None) - stdout = (getattr(result, "stdout", "") or "")[:500] - return ( - f"### {role_name}\n\n" - f"- exit_code: {exit_code}\n" - f"- commit_sha: {commit_sha or ''}\n" - f"- stdout (truncated):\n\n```\n{stdout}\n```\n" - ) - - -def synthetic_commit_for(role_name: str) -> str: - """Return a per-role synthetic commit SHA. - - Reviewer_concurrency v1 non-blocking #2: derive a 7-hex SHA - from a SHA-1 of the role name so per-producer ProposalPayload - entries remain distinguishable. The ``ace1`` prefix keeps the - string obviously synthetic in log output. - - ``ProposalPayload.commit_sha`` is a non-empty-required field - (#1473) — real producers capture ``git rev-parse HEAD`` after - committing, but harness-faked tests stub the spawn and never - reach a git checkout. This synthetic SHA satisfies any callers - that hex-validate the field while remaining obviously synthetic - in log output. **Never escape this value from the in-process - driver** — a future consumer that hex-validates ``commit_sha`` - would accept it as a real SHA. - """ - import hashlib - - digest = hashlib.sha1(role_name.encode("utf-8"), usedforsecurity=False).hexdigest() - return f"ace1{digest[:3]}" - - -def log_tracker_warning(verb: str, role_label: str, exc: Exception, pipeline_id: str) -> None: - """Log a tracker-guard rejection at WARNING. - - Reviewer_code_holistic v1 non-blocking: bare ``except Exception: - pass`` around tracker.handle_* calls silently discards root - cause when the eval snapshot's ``blocking_agents`` only surfaces - the symptom. v2 logs the verb + role + exception so an operator - debugging a stuck plan gate gets a structured breadcrumb. - """ - try: - import logging - - logger = logging.getLogger("orchestrator.substrate.in_process") - logger.warning( - "plan-phase tracker.%s rejected for %s (pipeline_id=%s): %s", - verb, - role_label, - pipeline_id, - exc, - ) - except Exception: # noqa: BLE001 — defensive - pass diff --git a/orchestrator/substrate/claude_code/__init__.py b/orchestrator/substrate/claude_code/__init__.py deleted file mode 100644 index 62893a6c3c..0000000000 --- a/orchestrator/substrate/claude_code/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Claude Code substrate implementations for issue #2623. - -Module-level entry points for the four substrate protocols under the -``"claude-code"`` selection: - -- ``ClaudeCodeSpawner`` → ``AgentSpawner`` -- ``InProcessMessageBus`` → ``MessageBus`` -- ``PreToolUseHookPolicy`` → ``PolicyEnforcer`` -- ``LocalWorktreeManager`` → ``WorktreeManager`` - -See ``docs/architecture/claude-code-substrate.md`` for the ADR. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -from .message_bus import InProcessMessageBus -from .policy import PreToolUseHookPolicy -from .spawner import ClaudeCodeSpawner -from .worktree import LocalWorktreeManager - -__all__ = [ - "ClaudeCodeSpawner", - "InProcessMessageBus", - "LocalWorktreeManager", - "PreToolUseHookPolicy", -] diff --git a/orchestrator/substrate/claude_code/hook_entry.py b/orchestrator/substrate/claude_code/hook_entry.py deleted file mode 100755 index d52ab138e3..0000000000 --- a/orchestrator/substrate/claude_code/hook_entry.py +++ /dev/null @@ -1,779 +0,0 @@ -#!/usr/bin/env python3 -"""PreToolUse hook entry script for the Claude Code substrate (#2623). - -Wired up by ``.claude/settings.json`` (template at -``orchestrator/substrate/claude_code/settings.template.json``): - -.. code-block:: json - - { - "hooks": { - "PreToolUse": [ - { - "matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash", - "hooks": [ - { - "type": "command", - "command": "python3 -m orchestrator.substrate.claude_code.hook_entry" - } - ] - } - ] - } - } - -Claude Code invokes the hook before any matching tool runs. The hook -receives a JSON object on stdin with the tool name and tool input -(see the official PreToolUse hook spec); it returns a JSON object on -stdout. To deny the call, emit:: - - { - "decision": "block", - "reason": "" - } - -To allow it, emit ``{}`` (or any object without ``decision``). - -Threat model — first-tier enforcement only -------------------------------------------- - -In the claude-code substrate the gateway sidecar is gone by -construction (cq-6). This hook is the **first-tier** enforcement -layer for substrate-managed write prefixes (``.egg-state/``, -``.claude/``, ``.github/``, ``shared/egg_restrictions/``). Per the -ADR's R2 deferral, the design's full enforcement story rides on a -**second-tier MCP-validator** path that re-checks role / phase / -prefix at the MCP-tool boundary. This hook intentionally does NOT -attempt to be the only enforcement layer — Bash is a quoting- -unbounded surface, and pretending the regex-and-shlex parser -herein is a complete sandbox would be a security claim the -implementation cannot back. Treat this hook as a coarse, cheap -filter that catches the common explicit-write shapes; the MCP -validator (or the role-confined harness that ships the second -substrate role) catches the cases this hook necessarily misses. - -What this hook DOES catch (reviewer v1 blocker #4 — broadened set): - -1. ``Bash`` write-shaped verbs the parser explicitly handles: - - Redirection (``>``, ``>>``, ``&>``, ``2>``, ``2>>``, etc.) — - fail-closed on ``$(...)`` / backtick destinations. - - File-mover / file-mutator verbs: ``cp``, ``mv``, ``install``, - ``rsync``, ``tee``, ``dd of=``, ``sed -i``, ``ln``/``link``, - ``rm``, ``chmod``, ``chown``, ``truncate``, ``awk -i inplace``, - ``perl -i``. - - Network-fetch-to-disk verbs: ``wget -O``, ``curl -o``, - ``curl --output``, ``curl --output-dir``. - - Git mutation verbs that write working-tree paths: - ``git mv``, ``git rm``, ``git apply``, ``git checkout --

``, - ``git restore

``. - - Archive-extraction verbs: ``tar -x``, ``unzip``. - - Shell-of-shell forms: ``bash -c '...'``, ``sh -c '...'``, - ``zsh -c '...'`` recursively parse the inner command. - - ``python``/``python3 -c`` containing common write signatures. - -2. Ambiguous shape → fail-closed: ``$(...)`` / backtick in a - destination, malformed ``shlex`` input, unknown verb whose - tokens include a ``>`` / ``>>`` redirect inside quotes the - ``_REDIRECT_RE`` already flagged. - -3. ``Path(p).resolve()`` symlink-aware comparison (security finding - #2), ``JSONDecodeError`` fail-closed (security finding #3), - role-missing fail-closed inside substrate-managed prefixes - (security finding #4). - -What this hook does NOT catch (deliberate, with mitigation): - -- Process substitution (``cat >(tee /restricted)``), heredoc - redirects (``cat < /restricted``), and fd-duplication - tricks (``exec 3>/restricted``). These require a Bash-grammar - parser; the MCP validator's role-confined tool surface is the - intended catch. -- Arbitrary ``python -c`` bodies that write without using the - obvious ``open(...)`` / ``Path(...)`` markers. The MCP - validator's role-confined tool surface is again the intended - catch; this hook's Python-string heuristic is a coarse filter. -- Allowing ``python3 -m orchestrator.substrate.*`` broadly. The - follow-up issue tightens this to a per-entrypoint allowlist. - -The hook delegates allow/deny decisions for paths it DOES extract -to ``shared/egg_restrictions/checker.py::check_agent_file_access`` -— the same symbol ``gateway/phase_filter.py:1061 -check_agent_restrictions`` uses. There is no parallel restriction -logic. -""" - -from __future__ import annotations - -import json -import os -import re -import shlex -import sys -from pathlib import Path -from typing import Any - -#: Prefixes whose writes must always be policed even when -#: ``EGG_AGENT_ROLE`` is unset. The user's plain Claude Code session -#: outside these prefixes keeps writing freely; only substrate- -#: managed locations engage the fail-closed default. -_RESTRICTED_PREFIXES: tuple[str, ...] = ( - ".egg-state/", - ".claude/", - ".github/", - "shared/egg_restrictions/", -) - - -def _extract_file_tool_paths(tool_name: str, tool_input: dict[str, Any]) -> list[str]: - """Return the set of paths a Write/Edit-family tool would write.""" - if tool_name in ("Write", "Edit", "NotebookEdit"): - path = tool_input.get("file_path") or tool_input.get("notebook_path") - if isinstance(path, str) and path: - return [path] - return [] - if tool_name == "MultiEdit": - path = tool_input.get("file_path") - if isinstance(path, str) and path: - return [path] - return [] - return [] - - -#: Regex for redirection-style write targets in a Bash command. Matches -#: ``>``, ``>>``, ``&>``, ``&>>``, ``2>``, ``2>>``, ``1>``, ``1>>`` and -#: similar fd-anchored forms; the path is the next shell token. -_REDIRECT_RE = re.compile(r"(?:^|[\s;|&])(?:[12]?&?>>?|>&?)\s*([^\s;|&]+)") - - -def _bash_write_paths(command: str) -> tuple[list[str], bool]: - """Best-effort extraction of write targets from a Bash command. - - Returns ``(paths, ambiguous)`` where ``ambiguous`` is True when - the parser found a write-shaped construct it could not classify - (e.g. a ``$(...)`` substitution in a destination position). The - caller MUST fail closed when ``ambiguous`` is True — the hook's - contract is "block by default whenever the substrate's - enforcement layer cannot prove a Bash command is safe". - - The parser is intentionally conservative: it errs on the side of - flagging paths so the policy check runs against them. A clean - Bash command (read-only ``ls``, ``cat``, ``grep``) produces an - empty path list and ``ambiguous=False``, allowing the call. - """ - paths: list[str] = [] - ambiguous = False - if not command: - return paths, False - - # 1. Redirection writes (>, >>, &>, 2>, etc.) - # - # Reviewer v3 non-blocking: the raw-command first-pass can catch - # tokens that sit inside a quoted region of an outer shell-of-shell - # form (e.g. ``bash -c 'echo x > /restricted/file'`` captures - # ``/restricted/file'`` — note the trailing quote). The recursive - # handler at the ``bash -c`` branch below re-extracts the inner - # command cleanly, so the phantom-with-quote token would land - # alongside the clean path and produce noisy duplicate paths. - # Filter out any candidate that contains an unmatched ``'`` or - # ``"`` before handing it to the policy checker. - for m in _REDIRECT_RE.finditer(command): - target = m.group(1) - if "$" in target or "`" in target: - ambiguous = True - elif target.count("'") % 2 != 0 or target.count('"') % 2 != 0: - # Phantom path artefact from the regex catching a token - # inside a quoted region of a longer command; the recursive - # bash handler below picks up the clean path. - continue - else: - paths.append(target) - - # 2. Token-driven write commands. Use shlex so quoted paths - # survive intact. shlex on malformed input raises ValueError; - # treat that as "ambiguous → fail closed". - try: - tokens = shlex.split(command, comments=True, posix=True) - except ValueError: - return paths, True - - # Walk the token list, classifying write-shaped invocations. - i = 0 - while i < len(tokens): - tok = tokens[i] - base = os.path.basename(tok) - if base in {"cp", "mv", "install", "rsync"}: - # The last positional non-flag arg is the destination. - args = [t for t in tokens[i + 1 :] if not t.startswith("-")] - if args: - dest = args[-1] - if "$" in dest or "`" in dest: - ambiguous = True - else: - paths.append(dest) - break - if base == "tee": - for arg in tokens[i + 1 :]: - if arg.startswith("-"): - continue - if "$" in arg or "`" in arg: - ambiguous = True - else: - paths.append(arg) - break - if base == "dd": - for arg in tokens[i + 1 :]: - if arg.startswith("of="): - dest = arg[3:] - if "$" in dest or "`" in dest: - ambiguous = True - else: - paths.append(dest) - break - if base == "sed": - # sed -i / --in-place mutates the named file(s). - in_place = any( - t == "-i" or t.startswith("-i") or t == "--in-place" for t in tokens[i + 1 :] - ) - if in_place: - for t in tokens[i + 1 :]: - if t.startswith("-"): - continue - if "$" in t or "`" in t: - ambiguous = True - else: - paths.append(t) - break - if base in {"ln", "link"}: - non_flags = [t for t in tokens[i + 1 :] if not t.startswith("-")] - if non_flags: - link_path = non_flags[-1] - if "$" in link_path or "`" in link_path: - ambiguous = True - else: - paths.append(link_path) - break - # Reviewer v1 blocker #4: ``rm``, ``chmod``, ``chown``, - # ``truncate``, ``awk -i inplace``, ``perl -i``. Each - # mutates the listed paths in place; surface them all to - # the policy checker. - if base in {"rm", "chmod", "chown", "truncate"}: - for arg in tokens[i + 1 :]: - if arg.startswith("-"): - continue - if "$" in arg or "`" in arg: - ambiguous = True - else: - paths.append(arg) - break - if base == "awk": - rest = tokens[i + 1 :] - in_place = any( - t == "-i" or (t.startswith("-i") and "inplace" in t.lower()) or "inplace" in t - for t in rest - ) - if in_place: - # Last token is conventionally the file in `awk -i - # inplace 'script' file`. - non_flags = [t for t in rest if not t.startswith("-")] - if non_flags: - candidate = non_flags[-1] - if "$" in candidate or "`" in candidate: - ambiguous = True - else: - paths.append(candidate) - break - if base == "perl": - rest = tokens[i + 1 :] - in_place = any(t == "-i" or t.startswith("-i") for t in rest) - if in_place: - non_flags = [t for t in rest if not t.startswith("-")] - # `perl -i.bak -pe '' file` — the script is - # one of the non-flag args; we conservatively - # surface every non-flag candidate to the policy - # checker. Spurious hits land on the script string - # ("''"), which the policy checker treats as - # an unknown path and ignores per its own resolve - # logic. - for cand in non_flags: - if "$" in cand or "`" in cand: - ambiguous = True - else: - paths.append(cand) - break - # Network-fetch-to-disk verbs. - if base in {"wget", "curl"}: - rest = tokens[i + 1 :] - j = 0 - while j < len(rest): - arg = rest[j] - if arg in {"-O", "-o", "--output"} and j + 1 < len(rest): - dest = rest[j + 1] - if "$" in dest or "`" in dest: - ambiguous = True - else: - paths.append(dest) - j += 2 - continue - if arg.startswith("--output-dir") or arg == "--output-document": - if "=" in arg: - dest = arg.split("=", 1)[1] - elif j + 1 < len(rest): - dest = rest[j + 1] - j += 1 - else: - dest = "" - if dest: - if "$" in dest or "`" in dest: - ambiguous = True - else: - paths.append(dest) - j += 1 - break - # Archive-extraction verbs. Both can land arbitrary paths - # in the working tree; flag the explicit -C target when - # present, otherwise mark the whole command ambiguous so - # the policy checker engages against an obviously-unsafe - # shape. - if base == "tar": - rest = tokens[i + 1 :] - - # Reviewer v2 non-blocking: narrow the extract-mode - # detection to the actual tar extract flags. Previously - # any token starting with ``-x`` (e.g. ``--xattrs``, - # ``--xz``) was treated as an extract, producing - # fail-closed false-positives. Match the named extract - # forms exactly (long ``--extract`` + short modes that - # encode ``x``: ``-x`` alone or short-flag clusters like - # ``-xf``, ``-xzf``, ``-xjf``, ``-xJf``, ``-xvf``). - def _is_tar_extract(tok: str) -> bool: - if tok == "--extract": - return True - if not tok.startswith("-") or tok.startswith("--"): - return False - # Single-dash cluster: each char after the leading - # dash is a short flag; ``x`` anywhere in the cluster - # means extract. - return "x" in tok[1:] - - extracts = any(_is_tar_extract(t) for t in rest) - if extracts: - target_dir: str | None = None - for k, t in enumerate(rest): - if t == "-C" and k + 1 < len(rest): - target_dir = rest[k + 1] - if target_dir is None: - ambiguous = True - else: - if "$" in target_dir or "`" in target_dir: - ambiguous = True - else: - paths.append(target_dir) - break - if base == "unzip": - rest = tokens[i + 1 :] - target_dir = None - for k, t in enumerate(rest): - if t == "-d" and k + 1 < len(rest): - target_dir = rest[k + 1] - if target_dir is None: - ambiguous = True - else: - if "$" in target_dir or "`" in target_dir: - ambiguous = True - else: - paths.append(target_dir) - break - # ``git`` write subcommands. The leaf paths are the trailing - # non-flag args; we surface them all. - if base == "git" and i + 1 < len(tokens): - sub = tokens[i + 1] - if sub in {"mv", "rm", "apply", "checkout", "restore"}: - rest = tokens[i + 2 :] - for arg in rest: - if arg.startswith("-"): - continue - if arg == "--": - continue - if "$" in arg or "`" in arg: - ambiguous = True - else: - paths.append(arg) - break - # Shell-of-shell forms: recurse into the inner command so - # ``bash -c 'echo x > /restricted'`` is parsed, not silently - # allowed (reviewer v1 blocker #4). Reviewer v2 non-blocking: - # also handle combined short-flag clusters like ``bash -lc - # '...'`` or ``bash -xc '...'`` where the ``c`` rides along - # with other single-char options. - if base in {"bash", "sh", "zsh", "dash", "ksh"}: - rest = tokens[i + 1 :] - c_index: int | None = None - for k, tok in enumerate(rest): - if tok == "-c": - c_index = k - break - # Combined short cluster: starts with single dash, no - # second dash, and includes ``c`` somewhere in the - # cluster. Matches ``-lc``, ``-xc``, ``-cx``, etc. - if tok.startswith("-") and not tok.startswith("--") and "c" in tok[1:]: - c_index = k - break - if c_index is not None and c_index + 1 < len(rest): - inner = rest[c_index + 1] - inner_paths, inner_ambiguous = _bash_write_paths(inner) - paths.extend(inner_paths) - if inner_ambiguous: - ambiguous = True - break - if base == "python3" or base == "python": - # python -c "..." is arbitrary Python — flag as ambiguous - # unless the command obviously doesn't write - # (we don't try to parse Python here). - rest = tokens[i + 1 :] - if "-c" in rest: - idx = rest.index("-c") - if idx + 1 < len(rest): - body = rest[idx + 1] - if any( - sig in body - for sig in ( - "open(", - "os.write", - "Path(", - "shutil", - "with open", - ) - ): - ambiguous = True - # Tightened allow-list (reviewer v1 non-blocking): only - # the named substrate entrypoints get a free pass, not - # any module under ``orchestrator.substrate.*``. - if "-m" in rest: - idx = rest.index("-m") - if idx + 1 < len(rest): - mod = rest[idx + 1] - _ALLOWED_PY_M_MODS = ("orchestrator.substrate.claude_code.hook_entry",) - if mod in _ALLOWED_PY_M_MODS: - i += 1 - continue - break - i += 1 - - return paths, ambiguous - - -def _extract_write_paths(tool_name: str, tool_input: dict[str, Any]) -> tuple[list[str], bool]: - """Return ``(paths, ambiguous)`` for any write-side tool. - - For file-side tools (``Write``/``Edit``/etc.) ``ambiguous`` is - always False — the input carries a single explicit path. - For ``Bash``, the parser may flag the command as ambiguous, in - which case the caller fails closed. - """ - if tool_name == "Bash": - command = tool_input.get("command") or "" - if not isinstance(command, str): - return [], True - return _bash_write_paths(command) - return _extract_file_tool_paths(tool_name, tool_input), False - - -def _repo_relative(path: str, repo_root: str | None) -> str: - """Canonicalise ``path`` to a repo-relative key. - - SECURITY: uses ``Path(p).resolve()`` (security finding #2) so - symlinks are followed to their targets BEFORE the prefix - comparison. The gateway's defense at - ``gateway/worktree_manager.py:1700-1711`` follows the same - pattern: resolve first, then compare. Without this, a symlink - inside an allow-listed directory pointing outside the workspace - laundered the write past the allow-list. - - When the resolved path is outside the repo root, return the - resolved path verbatim. The caller treats any path that resolves - outside the repo root as a write the substrate must deny. - """ - if not path: - return path - try: - resolved = Path(path).resolve() - except OSError: - # Path does not exist yet; fall back to a string normalize - # so we still get a deterministic key for the pattern check. - # The pattern matcher's blocklist will catch obviously-bad - # paths (``..`` traversal) via its own normalization. - return os.path.normpath(path) - if not repo_root: - return str(resolved) - try: - repo_root_resolved = Path(repo_root).resolve() - except OSError: - return str(resolved) - try: - rel = resolved.relative_to(repo_root_resolved) - except ValueError: - # Outside repo root. - return str(resolved) - return str(rel) - - -def _is_inside_restricted_prefix(path: str) -> bool: - """Return True if ``path`` lands under a substrate-managed prefix. - - Used to gate the fail-closed default when ``EGG_AGENT_ROLE`` is - unset: writes outside any restricted prefix continue to fail-open - (keeps the user's plain Claude Code session unaffected by hook - installation); writes inside fail closed. - """ - if not path: - return False - normalized = os.path.normpath(path).lstrip("/") - return any( - normalized == prefix.rstrip("/") or normalized.startswith(prefix) - for prefix in _RESTRICTED_PREFIXES - ) - - -def _path_resolves_inside_repo(path: str, repo_root: str | None) -> bool: - """Return True iff the path resolves to a location inside repo_root. - - Used as a final symlink-aware guard: even if the repo-relative - string starts with an allow-listed prefix, a symlink at that - location may redirect the actual write outside repo_root. - """ - if not path: - return False - if not repo_root: - # Without a repo root we cannot prove the write is in scope. - return False - try: - resolved = Path(path).resolve() - root_resolved = Path(repo_root).resolve() - except OSError: - return False - try: - resolved.relative_to(root_resolved) - return True - except ValueError: - return False - - -def decide(stdin_blob: dict[str, Any]) -> dict[str, Any]: - """Compute the hook decision for a single PreToolUse invocation. - - Args: - stdin_blob: The parsed JSON object Claude Code wrote to the - hook's stdin. - - Returns: - A dict to be JSON-serialized as the hook stdout. Empty or - missing ``decision`` field means "allow"; ``decision="block"`` - with a ``reason`` blocks the call. - """ - tool_name = stdin_blob.get("tool_name") or stdin_blob.get("tool", "") - tool_input = stdin_blob.get("tool_input") or {} - if not isinstance(tool_input, dict): - tool_input = {} - - paths, ambiguous = _extract_write_paths(tool_name, tool_input) - - # Ambiguous Bash → fail closed (security finding #1). - if ambiguous: - return { - "decision": "block", - "reason": ( - f"egg PreToolUseHookPolicy: cannot statically verify " - f"{tool_name} write targets (shell expansion / " - f"unparseable command). Rewrite the command so the " - f"write target is a literal path, or disable the hook " - f"if you intend to opt out of substrate enforcement." - ), - } - - if not paths: - return {} - - role = _resolve_active_role() - repo_root = os.environ.get("EGG_REPO_ROOT") or os.environ.get("EGG_WORKTREE_ROOT") - - # Symlink-aware path resolution (security finding #2). - repo_paths = [_repo_relative(p, repo_root) for p in paths] - - # Defense in depth (security finding #2 continued): even if the - # resolved repo-relative path looks fine, the original path may - # have escaped via a symlink. Verify the original path resolves - # inside the repo when a repo_root is configured. - if repo_root: - for original in paths: - if not _path_resolves_inside_repo(original, repo_root): - # Original path resolves outside repo root — deny. - return { - "decision": "block", - "reason": ( - f"egg PreToolUseHookPolicy denied {tool_name}: " - f"path {original!r} resolves outside the repo " - f"root {repo_root}. Symlink-laundered writes " - f"are rejected by design (security finding #2)." - ), - } - - if not role: - # Security finding #4: fail closed for substrate-managed - # prefixes even when the role env is missing. The plain - # Claude Code session (writes outside .egg-state/ etc.) - # continues to work — the fail-closed default only engages - # when a write lands in a prefix the substrate owns. - for p in repo_paths: - if _is_inside_restricted_prefix(p): - return { - "decision": "block", - "reason": ( - f"egg PreToolUseHookPolicy denied {tool_name}: " - f"EGG_AGENT_ROLE is unset but the write target " - f"{p!r} is inside a substrate-managed prefix. " - f"Either set EGG_AGENT_ROLE or write outside " - f"the .egg-state / .claude / .github / " - f"shared/egg_restrictions/ trees." - ), - } - return {} - - # Defense-in-depth literal-criterion satisfier (task-1-4 - # acceptance bullet 5 + reviewer_code_holistic finding #5): - # touch ``build_agent_patterns`` directly to verify the role is - # registered. ``check_agent_file_access`` already calls into the - # same patterns module transitively, but reviewers wanted the - # direct import in the call graph. - try: - from egg_restrictions.patterns import build_agent_patterns - - _patterns = build_agent_patterns(repo=None) - if role not in _patterns: - return { - "decision": "block", - "reason": ( - f"egg PreToolUseHookPolicy: role '{role}' has no " - f"registered pattern in shared/egg_restrictions/" - f"patterns.py:build_agent_patterns. Deny-by-default." - ), - } - except ImportError: - # build_agent_patterns unavailable — fall through to the - # higher-level check which has its own deny-by-default. - pass - - from egg_restrictions.checker import check_agent_file_access - - allowed, blocked, reason = check_agent_file_access(role, repo_paths, repo=None) - if allowed: - return {} - - return { - "decision": "block", - "reason": (f"egg PreToolUseHookPolicy denied {tool_name} for role '{role}': {reason}"), - } - - -def _resolve_active_role() -> str: - """Resolve the agent role for the current tool call. - - Reads ``EGG_AGENT_ROLE`` from the env first. When the env var - is unset, falls back to the sentinel file the spawner writes at - ``$HOME/.claude/egg-active-role.json`` (reviewer_code_holistic v1 - finding #8 — spawn↔hook role coordination across process - boundaries). - - Reviewer_code v2 blocker #1: the sentinel is PID-stamped by the - spawner. When the recorded PID is no longer alive (crashed - pipeline, OOM-kill, hard kill), the sentinel is treated as - missing — preventing a stale sentinel from impeding the user's - next plain Claude Code session. Live-PID sentinels still - function normally as a fallback when env propagation is dropped - in nested dispatch. - - Returns an empty string when neither env nor a live-PID sentinel - resolves a role. - """ - role = os.environ.get("EGG_AGENT_ROLE", "").strip() - if role: - return role - home = os.environ.get("HOME") - if not home: - return "" - sentinel = Path(home) / ".claude" / "egg-active-role.json" - try: - if sentinel.exists(): - blob = json.loads(sentinel.read_text()) - sentinel_role = blob.get("role") - sentinel_pid = blob.get("pid") - if not isinstance(sentinel_role, str) or not sentinel_role.strip(): - return "" - # PID-liveness check (reviewer_code v2 blocker #1): treat - # sentinel as missing when the owning orchestrator - # process is no longer running. This prevents a stale - # sentinel from a crashed pipeline from impeding the - # user's next plain Claude Code session. - if isinstance(sentinel_pid, int) and sentinel_pid > 0: - try: - os.kill(sentinel_pid, 0) - except (ProcessLookupError, PermissionError): # fmt: skip - # ProcessLookupError: PID is gone entirely. - # PermissionError: PID is alive but owned by a - # different user — the orchestrator's spawner - # must own the process for role-routing to make - # sense, so we still treat the sentinel as stale - # in this case (fail-safe default for the user's - # plain Claude Code session). - return "" - except OSError: - # Unknown errno — we cannot classify, so we - # fall through and trust the sentinel here - # (rather than denying writes) to avoid kernel - # quirks locking the user out of their own - # session. The blast radius is bounded: the - # operator can manually delete - # ``$HOME/.claude/egg-active-role.json`` if - # this branch ever misfires. - pass - # Either no PID stamp (legacy sentinel) or PID is alive. - return sentinel_role.strip() - except (json.JSONDecodeError, OSError): # fmt: skip - pass - return "" - - -def main() -> int: - """Read JSON from stdin, write decision JSON to stdout.""" - try: - raw = sys.stdin.read() - blob = json.loads(raw) if raw else {} - except json.JSONDecodeError: - # Security finding #3: fail CLOSED. Even as a first-tier - # filter (per the module-top "first-tier enforcement only" - # threat-model rewrite), a fail-open on a malformed stdin is - # a complete-bypass primitive an attacker who can shape any - # tool input can trigger — every write would slip past this - # tier with no signal handed to the second-tier MCP - # validator. Failing closed keeps the filter cheap and - # honest about its scope. - print( - json.dumps( - { - "decision": "block", - "reason": ( - "egg PreToolUseHookPolicy: malformed hook stdin " - "(JSONDecodeError). Failing closed — retry the " - "tool call with a well-formed input." - ), - } - ) - ) - return 0 - - decision = decide(blob) - print(json.dumps(decision)) - return 0 - - -if __name__ == "__main__": # pragma: no cover - sys.exit(main()) diff --git a/orchestrator/substrate/claude_code/message_bus.py b/orchestrator/substrate/claude_code/message_bus.py deleted file mode 100644 index 594487dbe7..0000000000 --- a/orchestrator/substrate/claude_code/message_bus.py +++ /dev/null @@ -1,53 +0,0 @@ -"""In-process message bus for the Claude Code substrate (#2623). - -Delegates to ``orchestrator/message_store.py:200 MessageStore`` — the -existing thread-safe in-memory bus that already powers -``EGG_MESSAGE_STORE_BACKEND=memory`` local-dev runs. Subclassing -rather than re-implementing keeps the bus behavior identical to the -production k3s code path when the latter is configured for the -memory backend, which is exactly the cq-1 contract: both substrates -share BRC mechanics; only the transport differs. - -The BRC concurrency invariants (INV-3 stale-version rejection, INV-5 -open-NACK barrier, INV-6 ack_commit_sha consistency) live in -``orchestrator/peer_consensus.py`` and -``orchestrator/action_guards.py:631 validate_invariants``. The bus -itself does not enforce them; it only ferries messages. The -behavioral oracle is the existing BRC test suite at -``orchestrator/tests/test_brc_*.py`` (7+ files including -``test_brc_open_nacks_barrier.py`` and -``test_brc_content_validation.py``); tests under TASK-1-8 exercise -the InProcessMessageBus surface against the same oracle. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -from orchestrator.message_store import MessageStore - - -class InProcessMessageBus(MessageStore): - """Pure-Python in-process bus satisfying the ``MessageBus`` - protocol. - - Inherits ``add_message`` / ``get_messages`` / blocking-read / - notify-on-add semantics from ``MessageStore``. This subclass - exists primarily for type discrimination — callers can check - ``isinstance(bus, InProcessMessageBus)`` to distinguish the - Claude Code substrate from the Redis-backed k3s substrate - without sniffing module names — and to provide a stable seam if - the in-process bus later needs Claude-Code-specific tweaks - (e.g., shorter heartbeat ticks, sandbox-aware filtering). - - Why subclass instead of compose: the - ``orchestrator.message_store.MessageStore`` API is large - (``get_messages_with_meta``, ``clear``, ``flush_pending_writes`` - and several others) and is consumed directly by - ``PeerConsensusTracker`` and the orchestrator's wait-loop - routes. Subclassing avoids re-exporting that surface manually - and keeps a single source of truth for the message shape. - """ - - def __init__(self) -> None: - super().__init__() diff --git a/orchestrator/substrate/claude_code/policy.py b/orchestrator/substrate/claude_code/policy.py deleted file mode 100644 index ed9bd4af23..0000000000 --- a/orchestrator/substrate/claude_code/policy.py +++ /dev/null @@ -1,188 +0,0 @@ -"""``PreToolUseHookPolicy`` for the Claude Code substrate (#2623). - -Implements ``PolicyEnforcer`` (HITL decision cq-6: PreToolUse hooks): -a ``.claude/settings.json``-registered hook script intercepts -Write/Edit/Bash calls before they execute and denies anything that -would land outside the caller's role's allow-list. Allow/deny -semantics match -``gateway/phase_filter.py:1061 check_agent_restrictions`` because both -paths delegate to -``shared/egg_restrictions/checker.py::check_agent_file_access``. - -See ``docs/architecture/claude-code-substrate.md`` for the ADR. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -import json -import os -from pathlib import Path - -from . import hook_entry - - -class PreToolUseHookPolicy: - """``PolicyEnforcer`` backed by a Claude Code PreToolUse hook. - - Two operations are exposed: - - 1. ``check_write(role, path)`` — in-process check that delegates - to the same ``check_agent_file_access`` function the hook - script calls. Useful when the orchestrator wants to validate a - proposed write without round-tripping through Claude Code. - 2. ``install(target_dir)`` — write a ``.claude/settings.json`` - file at ``target_dir`` that wires up the hook entry script. - Idempotent: re-running with an existing settings.json merges - the egg hook in alongside any user-defined hooks. - """ - - #: Template settings.json shipped alongside this module — the - #: structural reference for what ``install()`` writes. - SETTINGS_TEMPLATE_PATH = Path(__file__).parent / "settings.template.json" - - def __init__(self) -> None: - pass - - def check_write(self, role: str, path: str) -> tuple[bool, str | None]: - """Return ``(allowed, denial_message)`` for ``role`` writing ``path``. - - Args: - role: Agent role making the write. - path: Repo-relative path. - - Returns: - ``(True, None)`` when allowed. - ``(False, message)`` when denied; the message matches the - format used by - ``gateway/phase_filter.py:1061 check_agent_restrictions``. - """ - from egg_restrictions.checker import check_agent_file_access - - if not role: - return (True, None) - allowed, blocked, reason = check_agent_file_access(role, [path], repo=None) - if allowed: - return (True, None) - return (False, reason) - - def install(self, target_dir: str | Path) -> Path: - """Write ``.claude/settings.json`` under ``target_dir``. - - Args: - target_dir: The directory whose ``.claude/`` subdir - should receive the settings file. Must be either the - user's ``$HOME`` or a path under it (typically the - repo root the user is running egg in). System paths - like ``/etc/`` or ``/`` are rejected as a defense in - depth against a caller-supplied ``target_dir`` — - reviewer_security v1 non-blocking #4. - - Returns: - The path that was written. - - Raises: - ValueError: If ``target_dir`` is not under the user's - ``$HOME`` (or ``$HOME`` cannot be resolved). - """ - root = Path(target_dir).expanduser().resolve() - home_env = os.environ.get("HOME") - if not home_env: - raise ValueError( - "PreToolUseHookPolicy.install: $HOME is unset; refusing " - "to write .claude/settings.json to an unknown location." - ) - home_resolved = Path(home_env).resolve() - try: - if root != home_resolved and not root.is_relative_to(home_resolved): - raise ValueError( - f"PreToolUseHookPolicy.install: target_dir {root} is " - f"not under $HOME ({home_resolved}); refusing to " - f"write .claude/settings.json. Path-escape guard." - ) - except AttributeError: # pragma: no cover - # Python <3.9 fallback — never hit on our 3.11+ runtime. - if not str(root).startswith(str(home_resolved) + os.sep) and root != home_resolved: - raise ValueError( - f"PreToolUseHookPolicy.install: target_dir {root} is " - f"not under $HOME ({home_resolved})." - ) from None - out_dir = root / ".claude" - out_dir.mkdir(parents=True, exist_ok=True) - out_path = out_dir / "settings.json" - template = json.loads(self.SETTINGS_TEMPLATE_PATH.read_text()) - - # Merge with any pre-existing settings.json so we don't blow - # away user hooks. Egg's hook gets appended (idempotent: skip - # if already present). - # - # Reviewer_code v2 blocker #2: on JSON decode error of the - # existing settings.json we FAIL LOUD instead of silently - # using ``{}``. The previous behavior silently replaced the - # user's settings (including unrelated hooks / statusline / - # plugin enablement) with the egg-substrate template. We now - # raise ``ValueError`` with a clear message naming the path - # so the operator can fix the typo themselves. - existing: dict - if out_path.exists(): - try: - raw = out_path.read_text() - except OSError as exc: - raise ValueError( - f"PreToolUseHookPolicy.install: existing " - f"settings.json at {out_path} could not be read: " - f"{exc}" - ) from exc - if raw.strip(): - try: - existing = json.loads(raw) - except json.JSONDecodeError as exc: - raise ValueError( - f"PreToolUseHookPolicy.install: existing " - f"settings.json at {out_path} is not valid " - f"JSON ({exc.msg} at line {exc.lineno} col " - f"{exc.colno}). Refusing to overwrite — fix " - f"the file manually (or move it aside) and " - f"re-run install. This avoids silently " - f"obliterating the user's prior hooks / " - f"statusline / plugin enablement." - ) from exc - if not isinstance(existing, dict): - raise ValueError( - f"PreToolUseHookPolicy.install: existing " - f"settings.json at {out_path} is not a JSON " - f"object (top-level type: " - f"{type(existing).__name__}). Refusing to " - f"overwrite." - ) - else: - existing = {} - else: - existing = {} - - merged = self._merge_hooks(existing, template) - out_path.write_text(json.dumps(merged, indent=2) + "\n") - return out_path - - @staticmethod - def _merge_hooks(existing: dict, template: dict) -> dict: - """Merge ``template`` into ``existing``, deduping egg hooks.""" - merged = dict(existing) - merged_hooks = dict(existing.get("hooks", {})) - template_hooks = template.get("hooks", {}) - for hook_kind, entries in template_hooks.items(): - current = list(merged_hooks.get(hook_kind, [])) - for entry in entries: - # An egg hook is identified by referencing the hook - # entry module path. - if entry not in current: - current.append(entry) - merged_hooks[hook_kind] = current - merged["hooks"] = merged_hooks - return merged - - # Re-export the underlying ``decide`` function so unit tests can - # synthesize hook stdin payloads and assert decisions without - # spawning a subprocess. - decide = staticmethod(hook_entry.decide) diff --git a/orchestrator/substrate/claude_code/settings.template.json b/orchestrator/substrate/claude_code/settings.template.json deleted file mode 100644 index edbfb33dad..0000000000 --- a/orchestrator/substrate/claude_code/settings.template.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "PreToolUse": [ - { - "matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash", - "hooks": [ - { - "type": "command", - "command": "python3 -m orchestrator.substrate.claude_code.hook_entry" - } - ] - } - ] - } -} diff --git a/orchestrator/substrate/claude_code/spawner.py b/orchestrator/substrate/claude_code/spawner.py deleted file mode 100644 index 43a9b09096..0000000000 --- a/orchestrator/substrate/claude_code/spawner.py +++ /dev/null @@ -1,235 +0,0 @@ -"""``ClaudeCodeSpawner`` for the Claude Code substrate (#2623). - -Implements ``AgentSpawner`` (HITL decision cq-4: synchronous spawn) -by driving a subagent through ``shared/egg_harness`` (``run_agent``) -inside the user's Claude Code session. The caller blocks until the -subagent completes; internal concurrency is the spawner's -responsibility per cq-4. - -Substrate-swap framing (reviewer_code_holistic v1 finding #3) -------------------------------------------------------------- - -The walking-skeleton spike runs egg's existing ``egg_harness`` loop -in-process to the user's Claude Code session — it does NOT, at the -spike level, dispatch via Claude Code's native ``Agent`` tool with -``subagent_type="general-purpose"``. The harness drives the -AnthropicProvider directly and exposes its own tool registry. - -This is a deliberate scope decision for the spike: the harness -runner is what the existing k3s sandbox uses inside its pod, so -re-hosting it inside Claude Code is the minimum-viable substrate -swap. The follow-up issue (listed in the ADR's "Open work" -appendix) covers wiring an alternative -``ClaudeCodeAgentToolSpawner`` that emits an ``Agent`` tool -envelope for the parent session's outer loop to execute. Until -then this spawner is functionally a "re-host the harness in the -user's session" path, not a "swap to Claude Code primitives" path. -The R1 trust-context section of the ADR documents the security -delta this framing implies. - -Key responsibilities (in the implemented scope): - -1. Assemble the system prompt via ``build_system_prompt(...)`` from - ``shared/egg_harness/prompt.py:24`` — this is the structural fix - issue #2622 documents (the orchestrator wraps the per-role rubric - alongside any prompt extras). -2. Run the subagent synchronously, capturing stdout / exit code / - duration. -3. Capture ``commit_sha`` via ``git -C rev-parse HEAD`` - *immediately* after the subagent returns. INV-6 in - ``orchestrator/action_guards.py:631`` (body at line 757) requires - this so reviewers can attach commit-bound ACKs to the producer's - recorded SHA. - -See ``docs/architecture/claude-code-substrate.md`` for the ADR. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -import os -import subprocess -import time -from collections.abc import Callable, Mapping -from pathlib import Path -from typing import Any - -from egg_contracts.agent_roles import AgentRole -from egg_harness.prompt import PromptSource, build_system_prompt - -from ..spawner import AgentResult - - -class ClaudeCodeSpawner: - """Synchronous subagent spawner for the Claude Code substrate. - - The spawner blocks the caller (cq-4) and returns an - ``AgentResult`` once the subagent completes. Multiple concurrent - ``spawn()`` calls are safe: the spawner is stateless beyond the - optional injected ``run_agent_fn``. - - Args: - run_agent_fn: Optional override for the subagent runner. The - default delegates to ``shared.egg_harness.client.run_agent``, - which drives the egg harness loop and returns its own - ``AgentResult`` (we translate it into this module's - ``AgentResult`` shape). The override lets tests substitute - a deterministic fake without standing up the full harness. - role_rubric_loader: Optional callable - ``(role: AgentRole) -> str`` that returns the role-specific - rubric markdown to prepend. When ``None``, the spawner - falls back to a minimal "you are " string. The - production wiring injects a loader that reads from - ``plugins/egg-sdlc/skills/egg-sdlc/agents/.md`` — - same layout as the existing refine-plan skill. - """ - - def __init__( - self, - *, - run_agent_fn: Callable[..., Any] | None = None, - role_rubric_loader: Callable[[AgentRole], str] | None = None, - ) -> None: - self._run_agent = run_agent_fn - self._load_rubric = role_rubric_loader or _default_role_rubric - - def spawn( - self, - role: AgentRole, - prompt: str, - env: Mapping[str, str], - worktree: Path, - ) -> AgentResult: - """Spawn the subagent and block until it completes. - - Args: - role: Role to spawn (drives the rubric prepended via - ``build_system_prompt``). - prompt: Task-specific prompt body. The role rubric is - prepended automatically; the caller passes only the - task context. - env: Extra env vars to set on the subagent. The spawner - always sets ``EGG_AGENT_ROLE`` and ``EGG_WORKTREE_ROOT`` - in addition to whatever the caller supplies. - worktree: Path the subagent runs in. - - Returns: - ``AgentResult`` with ``commit_sha`` populated from - ``git -C rev-parse HEAD``. - """ - start = time.monotonic() - system_prompt = self._build_system_prompt(role) - - merged_env = { - **dict(env), - "EGG_AGENT_ROLE": role.value if hasattr(role, "value") else str(role), - "EGG_WORKTREE_ROOT": str(worktree), - } - - harness_result: Any | None = None - runner_error: str | None = None - if self._run_agent is None: - # Lazy import: the production runner pulls in Anthropic - # provider code that's expensive at import time. - try: - from egg_harness.client import run_agent as harness_run_agent - except ImportError as exc: - runner_error = f"egg_harness unavailable: {exc}" - harness_run_agent = None # type: ignore[assignment] - runner: Callable[..., Any] | None = harness_run_agent - else: - runner = self._run_agent - - if runner is not None and runner_error is None: - try: - harness_result = runner( - prompt, - system_prompt=system_prompt, - cwd=str(worktree), - env=merged_env, - ) - except Exception as exc: # pragma: no cover - defensive - runner_error = f"run_agent raised: {exc!r}" - - duration = time.monotonic() - start - - # Capture commit SHA — INV-6 required. - commit_sha = _capture_head_sha(worktree) - - stdout = getattr(harness_result, "stdout", "") or "" - exit_code = getattr(harness_result, "returncode", 0) - if runner_error is not None and exit_code == 0: - exit_code = 1 - stdout = stdout or runner_error - - return AgentResult( - stdout=stdout, - exit_code=int(exit_code or 0), - duration_seconds=duration, - worktree=worktree, - commit_sha=commit_sha, - artifacts=[], - ) - - def _build_system_prompt(self, role: AgentRole) -> str: - """Assemble the system prompt for ``role``. - - Uses ``build_system_prompt(...)`` from - ``shared/egg_harness/prompt.py:24`` — the structural depth - fix from issue #2622 lives in this helper, so we route all - prompt assembly through it. - """ - rubric = self._load_rubric(role) - sources: list[PromptSource] = [rubric] - return build_system_prompt(sources) - - -def _default_role_rubric(role: AgentRole) -> str: - """Fallback role rubric when no loader is injected. - - Production wiring overrides this with a loader that reads - ``plugins/egg-sdlc/skills/egg-sdlc/agents/.md`` (the - canonical per-role markdown). The fallback exists so unit tests - can construct a ``ClaudeCodeSpawner`` without instantiating the - full plugin layout. - """ - role_name = role.value if hasattr(role, "value") else str(role) - return f"You are the egg `{role_name}` subagent. Run the assigned task." - - -def _capture_head_sha(worktree: Path) -> str | None: - """Return the worktree HEAD commit SHA, or ``None`` if unavailable. - - Required by INV-6: reviewers attach commit-bound ACKs to the - producer's recorded SHA, so the spawner must capture it - immediately after the subagent completes (before any cleanup - that might modify HEAD). - """ - if worktree is None: - return None - path = Path(worktree) - if not path.exists(): - return None - try: - proc = subprocess.run( - ["git", "-C", str(path), "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=False, - timeout=10, - env={ - **os.environ, - # Disable interactive credential helpers so the - # capture stays non-blocking. - "GIT_TERMINAL_PROMPT": "0", - }, - ) - except (subprocess.SubprocessError, OSError): # fmt: skip - return None - if proc.returncode != 0: - return None - sha = proc.stdout.strip() - if not sha or len(sha) < 7: - return None - return sha diff --git a/orchestrator/substrate/claude_code/worktree.py b/orchestrator/substrate/claude_code/worktree.py deleted file mode 100644 index 944a18778e..0000000000 --- a/orchestrator/substrate/claude_code/worktree.py +++ /dev/null @@ -1,290 +0,0 @@ -"""``LocalWorktreeManager`` for the Claude Code substrate (#2623). - -Implements ``WorktreeManager`` using on-host filesystem operations. -HITL decision cq-5 pins the model to **port egg's -``WORKTREE_BASE_DIR``** (``gateway/worktree_manager.py:49``): the -Claude Code substrate keeps the per-pipeline / per-role layout the -gateway already uses, defaulting to ``~/.egg-worktrees/`` and -allowing ``EGG_WORKTREE_BASE`` to override. - -The path-escape defense mirrors the -``child.resolve().is_relative_to(base.resolve())`` guard in -``gateway/worktree_manager.py:1711`` (call site within -``_remove_worktree``; matching ``base.resolve()`` at line 1700). - -See ``docs/architecture/claude-code-substrate.md`` for the ADR. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -import os -import re -import shutil -import subprocess -import threading -from pathlib import Path - -from egg_contracts.agent_roles import AgentRole - -#: Path **fragment** appended to ``$HOME`` to form the default base -#: directory. Resolved at ``LocalWorktreeManager.__init__`` time -#: (not at module import) so ``monkeypatch.setenv("HOME", ...)`` in -#: tests is honored. Matches ``gateway/worktree_manager.py:49 -#: WORKTREE_BASE_DIR`` shape so operators don't need to learn two -#: layouts. Override the full path with the ``EGG_WORKTREE_BASE`` -#: env var. -_DEFAULT_BASE_NAME = ".egg-worktrees" - - -def _default_base() -> Path: - """Return the default base directory, computed from the *current* ``$HOME``. - - No module-level constant alias exists by design (reviewer v2 - non-blocking): an alias evaluated at import time would freeze - ``$HOME`` and silently diverge from what ``LocalWorktreeManager`` - itself sees when ``monkeypatch.setenv("HOME", ...)`` is in - effect. Call this helper directly if you need the current - default base outside the manager. - """ - return Path(os.environ.get("HOME", "/home/egg")) / _DEFAULT_BASE_NAME - - -# Conservative regex for pipeline / role names; matches the gateway's -# validate_identifier behavior. -_SAFE_IDENT = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$") - - -class LocalWorktreeManager: - """Per-pipeline filesystem worktree manager. - - Creates per-agent worktrees under - ``///`` with ``git worktree add``, tracks - them in a dict, and tears them down at phase end. - - The path-escape guard rejects any pipeline_id / role string that - would resolve outside ```` — this is the - ``gateway/worktree_manager.py:1711`` ``is_relative_to`` pattern. - """ - - def __init__(self, base: Path | None = None) -> None: - env_override = os.environ.get("EGG_WORKTREE_BASE") - if base is not None: - self._base = Path(base) - elif env_override: - self._base = Path(env_override) - else: - # Compute lazily so monkeypatching $HOME in tests works. - self._base = _default_base() - # Map pipeline_id → list of (worktree_path, branch) we created. - self._tracked: dict[str, list[tuple[Path, str]]] = {} - self._lock = threading.RLock() - - @property - def base(self) -> Path: - """The configured base directory.""" - return self._base - - def create(self, pipeline_id: str, role: AgentRole) -> Path: - """Create a worktree under ``///``. - - Args: - pipeline_id: Pipeline identifier; must match - ``[a-zA-Z0-9][a-zA-Z0-9._-]*`` to defeat traversal. - role: Agent role; the role name is used unchanged as the - second path segment and is also enum-validated. - - Returns: - Absolute path to the newly-created worktree. - """ - _validate_identifier(pipeline_id, "pipeline_id") - role_name = role.value if hasattr(role, "value") else str(role) - _validate_identifier(role_name, "role") - - target = (self._base / pipeline_id / role_name).resolve() - self._assert_within_base(target) - - target.mkdir(parents=True, exist_ok=True) - branch = f"egg/{pipeline_id}/{role_name}" - - # Best-effort ``git worktree add``. If the cwd isn't a git - # repo (e.g. test environment), fall back to creating the - # directory without a git worktree — the spawner still has a - # place to land artifacts. Failures don't raise to avoid - # blowing up the spike on environments without git. - try: - subprocess.run( - ["git", "worktree", "add", "-B", branch, str(target)], - capture_output=True, - text=True, - check=False, - timeout=30, - ) - except (subprocess.SubprocessError, OSError): # fmt: skip - # Worktree creation may fail (no git, no checkout); the - # directory still exists for the spawner to use. - pass - - with self._lock: - self._tracked.setdefault(pipeline_id, []).append((target, branch)) - return target - - def remove(self, pipeline_id: str, role: AgentRole) -> None: - """Remove a single (pipeline, role) worktree. - - Reviewer v1 blocker #3: ``tear_down(pipeline_id)`` is - pipeline-scoped and removes every per-role worktree it tracked, - which is the wrong granularity for the failure path of one - concurrent spawn (it would wipe peer worktrees mid-spawn). This - per-role variant is what ``_spawn_agent_via_substrate`` calls - when a single role's spawn fails. - - Args: - pipeline_id: Pipeline identifier (validated against the same - pattern as ``create``). - role: The role whose worktree should be removed. - """ - _validate_identifier(pipeline_id, "pipeline_id") - role_name = role.value if hasattr(role, "value") else str(role) - _validate_identifier(role_name, "role") - target = (self._base / pipeline_id / role_name).resolve() - - with self._lock: - entries = self._tracked.get(pipeline_id, []) - remaining: list[tuple[Path, str]] = [] - to_remove: list[tuple[Path, str]] = [] - for path, branch in entries: - try: - if path.resolve() == target: - to_remove.append((path, branch)) - continue - except OSError: - pass - remaining.append((path, branch)) - if remaining: - self._tracked[pipeline_id] = remaining - else: - self._tracked.pop(pipeline_id, None) - - # If the in-memory tracker missed it (e.g. process restart), - # still attempt the on-disk teardown if the path is within base. - if not to_remove: - to_remove = [(target, f"egg/{pipeline_id}/{role_name}")] - - self._remove_entries(to_remove) - - def tear_down(self, pipeline_id: str) -> None: - """Remove all worktrees for the named pipeline. - - Uses ``Path.resolve().is_relative_to(base.resolve())`` to - reject any path that resolves outside the configured base — - the same defense pattern as - ``gateway/worktree_manager.py:1711``. - - See ``remove(pipeline_id, role)`` for the per-role variant - that should be used from concurrent-spawn failure paths - (reviewer v1 blocker #3). - """ - _validate_identifier(pipeline_id, "pipeline_id") - base_resolved = self._base.resolve() - - with self._lock: - entries = self._tracked.pop(pipeline_id, []) - - # Always try to remove the pipeline-level directory, even if - # the in-memory list was lost (e.g., process restart). - pipeline_dir = (self._base / pipeline_id).resolve() - self._remove_entries(entries) - - # Tear down the pipeline-level dir if it's empty and lives - # under the base. - try: - if pipeline_dir.is_relative_to(base_resolved) and pipeline_dir.exists(): - shutil.rmtree(pipeline_dir, ignore_errors=True) - except (AttributeError, OSError): # fmt: skip - if pipeline_dir.exists() and str(pipeline_dir).startswith(str(base_resolved) + os.sep): - shutil.rmtree(pipeline_dir, ignore_errors=True) - - def _remove_entries(self, entries: list[tuple[Path, str]]) -> None: - """Best-effort removal of (path, branch) tuples with path-escape guard.""" - base_resolved = self._base.resolve() - for path, branch in entries: - try: - resolved = path.resolve() - except OSError: - continue - try: - if not resolved.is_relative_to(base_resolved): - # Path-escape guard — refuse to remove anything - # outside the base. - continue - except AttributeError: - # Python <3.9 fallback (should never hit on our - # runtime, but mirror the gateway's belt-and-braces). - if not str(resolved).startswith(str(base_resolved) + os.sep): - continue - - # Try ``git worktree remove`` first for a clean teardown; - # if it fails (no git, dirty state), fall back to plain - # rmtree. - try: - subprocess.run( - ["git", "worktree", "remove", "--force", str(resolved)], - capture_output=True, - text=True, - check=False, - timeout=30, - ) - except (subprocess.SubprocessError, OSError): # fmt: skip - pass - - if resolved.exists(): - shutil.rmtree(resolved, ignore_errors=True) - - # Best-effort branch deletion. Ignored when not a git - # checkout. - try: - subprocess.run( - ["git", "branch", "-D", branch], - capture_output=True, - text=True, - check=False, - timeout=15, - ) - except (subprocess.SubprocessError, OSError): # fmt: skip - pass - - def _assert_within_base(self, target: Path) -> None: - """Raise ``ValueError`` if ``target`` resolves outside the base.""" - base_resolved = self._base.resolve() - try: - base_resolved.mkdir(parents=True, exist_ok=True) - except OSError: - pass - try: - target_resolved = target.resolve() - except OSError as exc: - raise ValueError(f"could not resolve worktree path: {target}") from exc - try: - if not target_resolved.is_relative_to(base_resolved): - raise ValueError( - f"worktree {target_resolved} resolves outside base " - f"{base_resolved}; refusing to create" - ) - except AttributeError: - if not str(target_resolved).startswith(str(base_resolved) + os.sep): - raise ValueError( - f"worktree {target_resolved} resolves outside base " - f"{base_resolved}; refusing to create" - ) from None - - -def _validate_identifier(value: str, name: str) -> None: - """Reject empty / traversal / non-safe-character identifiers.""" - if not value: - raise ValueError(f"Invalid {name}: cannot be empty") - if ".." in value: - raise ValueError(f"Invalid {name}: path traversal not allowed") - if not _SAFE_IDENT.match(value): - raise ValueError(f"Invalid {name}: must match {_SAFE_IDENT.pattern!r}; got {value!r}") diff --git a/orchestrator/substrate/in_process.py b/orchestrator/substrate/in_process.py deleted file mode 100644 index d8c25e9818..0000000000 --- a/orchestrator/substrate/in_process.py +++ /dev/null @@ -1,1156 +0,0 @@ -"""In-process orchestrator entry point for the Claude Code substrate (#2623). - -Implements the heredoc-style synchronous HITL surface (cq-7): a Python -generator that yields ``HITLDecision`` objects when the pipeline pauses -for a human decision; the caller (the skill's outer loop) renders each -via ``AskUserQuestion``, sends the answer back via -``generator.send(...)``, and the orchestrator resumes. - -This is the spike's single most expensive task: wrapping the -HTTP-daemon orchestrator (``orchestrator/cli.py:83 cmd_serve``) into a -generator-shaped surface that keeps the ``ConcurrentPhaseExecutor`` -(``orchestrator/concurrent_executor.py:114``), ``PeerConsensusTracker`` -(``orchestrator/peer_consensus.py:69``), and ``HITLDecision`` -(``orchestrator/models.py:300``) primitives unchanged. For the -walking-skeleton spike the generator drives **the refiner role only** -end-to-end; plan / implement / pr phases raise -``NotImplementedError`` with a pointer to the follow-up issue (cq-11 -scope-fence). - -See ``docs/architecture/claude-code-substrate.md`` for the ADR. - -INTERFACE STABILITY: v0.x unstable. - -Background-thread lifetime --------------------------- -The generator owns three background threads: - -1. ``_heartbeat_thread`` — emits an in-process heartbeat tick once - every ``_HEARTBEAT_INTERVAL`` seconds while the generator is - running OR paused at a yield boundary. The tick keeps the - orchestrator from declaring the agent stalled during long HITL - pauses. -2. ``_brc_review_thread`` — drives BRC re-review polling. -3. ``_bus_tick_thread`` — pumps the message bus for delivery. - -All three are daemon threads with a ``threading.Event`` shutdown -signal. They join cleanly when: - -- The generator returns normally (artifact path returned). -- The generator is dropped mid-cycle (``GeneratorExit`` raised inside - the generator body; the ``finally`` block sets the shutdown event - and joins each thread with a small timeout). - -Verified by unit tests under -``shared/tests/test_run_pipeline_in_process.py`` that drop the -generator mid-yield and assert no leaked threads via a -``threading.enumerate()`` delta (acceptance bullet 4 on TASK-1-6). - -Contract-state synchronization ------------------------------- -The in-process orchestrator writes to the same -``.egg-state/contracts/.json`` filesystem path the HTTP daemon -uses — no separate state store. The skill's outer loop and the -generator both read/write through ``contract_store`` so HITL state -stays observable from the parent Claude Code session. -""" - -from __future__ import annotations - -import json -import os -import threading -import time -from collections.abc import Generator, Mapping -from pathlib import Path -from typing import Any - -# Heartbeat tick: a once-per-N-seconds liveness signal. Kept small so -# long HITL pauses don't trip stuck-phase-transition alerts. -_HEARTBEAT_INTERVAL = 5.0 -_BRC_REVIEW_INTERVAL = 2.0 -_BUS_TICK_INTERVAL = 1.0 - -#: Marker substring used in the NotImplementedError message so callers -#: (and tests) can structurally detect the k3s-leg fence. -_K3S_FENCE_MESSAGE = ( - "run_pipeline_in_process is claude-code-only in the #2623 spike; " - "k3s users keep using `egg-orch serve` (orchestrator/cli.py:83 " - "cmd_serve). The follow-up issue is tracked in the ADR at " - "docs/architecture/claude-code-substrate.md." -) - - -def run_pipeline_in_process( - pipeline_id: str, - *, - repo: str | None = None, - issue_number: int | None = None, - issue_body: str | None = None, - env: Mapping[str, str] | None = None, - state_dir: Path | None = None, -) -> Generator[Any, Any, str]: - """Generator-shaped orchestrator entry point. - - Yields ``HITLDecision`` objects when the pipeline pauses for a - human decision; the caller sends back the user's answer via - ``generator.send(...)``. When the pipeline completes, the - generator returns the refine artifact path (a string). - - Args: - pipeline_id: Pipeline identifier (e.g. ``"issue-2623"``). - repo: Optional repo identifier (``"owner/repo"``). - issue_number: Optional GitHub issue number; only used to - label the artifact. - issue_body: The refiner's task body. Optional; when omitted, - the refiner reads from ``.egg-state/drafts/-issue.md`` - if present, otherwise falls back to an empty prompt. - env: Optional env-var overrides; merged into ``os.environ`` - for the duration of the call. ``EGG_SUBSTRATE`` must be - ``"claude-code"`` (or unset, which the generator treats - as a claude-code default since this entry point is - substrate-specific). - state_dir: Override for the ``.egg-state/`` root. Defaults to - ``/.egg-state`` when unset. - - Returns: - Path (as a string) to the produced refine analysis at - ``.egg-state/drafts/-analysis.md``. - - Raises: - NotImplementedError: When ``EGG_SUBSTRATE=k3s`` is set - explicitly. The in-process generator is claude-code-only - for this spike; k3s users keep using - ``orchestrator/cli.py:83 cmd_serve``. - """ - # Validate the substrate selection before doing any expensive - # work or starting background threads. - effective_env = {**os.environ, **(dict(env) if env else {})} - substrate_name = (effective_env.get("EGG_SUBSTRATE") or "claude-code").lower() - if substrate_name == "k3s": - raise NotImplementedError(_K3S_FENCE_MESSAGE) - # Make the defaulting explicit for the rest of the call graph - # (reviewer_security v1 non-blocking #3). Without this, - # ``select_substrate(effective_env)`` downstream would default - # the unset case to ``"k3s"`` (its own default) and the in- - # process entry would crash on _DeferredK3sSpawner. - effective_env["EGG_SUBSTRATE"] = substrate_name - - state_root = Path(state_dir) if state_dir else Path.cwd() / ".egg-state" - - runner = _InProcessOrchestrator( - pipeline_id=pipeline_id, - repo=repo, - issue_number=issue_number, - issue_body=issue_body, - env=effective_env, - state_root=state_root, - ) - return runner.run() - - -class _InProcessOrchestrator: - """The actual generator body — extracted so background-thread - lifetime is easier to test.""" - - def __init__( - self, - *, - pipeline_id: str, - repo: str | None, - issue_number: int | None, - issue_body: str | None, - env: Mapping[str, str], - state_root: Path, - ) -> None: - self.pipeline_id = pipeline_id - self.repo = repo - self.issue_number = issue_number - self.issue_body = issue_body or "" - self.env = dict(env) - self.state_root = state_root - - self._shutdown = threading.Event() - self._threads: list[threading.Thread] = [] - # Counters surfaced for tests that observe background-thread - # liveness. - self._heartbeat_ticks = 0 - self._brc_review_ticks = 0 - self._bus_ticks = 0 - # Current phase the generator is executing; read by the - # heartbeat publisher so HEARTBEAT messages carry the correct - # phase string across the refine→plan transition - # (reviewer_concurrency v1 blocker #2 — stuck-phase-transition - # monitors filter heartbeats on ``phase``). - self._current_phase = "refine" - - # ------------------------------------------------------------------ - # Generator entry - # ------------------------------------------------------------------ - - def run(self) -> Generator[Any, Any, str]: - """The actual generator. See ``run_pipeline_in_process``.""" - self._start_background_threads() - try: - try: - # Stage 1: pre-flight HITL — confirm repo + issue. - preflight_answer = yield self._build_preflight_decision() - if _answer_is_abort(preflight_answer): - raise _PreflightAborted( - "Operator aborted at preflight HITL — refiner did not run." - ) - - # Stage 2: spawn the refiner via the substrate bundle. The - # returned ``AgentResult`` is bound to ``self._spawn_result`` - # so reviewer_code_holistic v1 finding #10 (discarded - # ``AgentResult``) is addressed: the refine HITL gate - # surfaces exit code + commit SHA + diagnostics rather - # than silently masking spawner failures. - artifact_path, spawn_result = self._spawn_refiner() - self._spawn_result = spawn_result - - # Stage 3: refine HITL gate — does the operator approve? - refine_answer = yield self._build_refine_gate_decision(artifact_path, spawn_result) - - # #2717 slice-2: when the operator approves and chooses - # to continue, dispatch the plan phase (3 producers + 1 - # reviewer through the InProcessMessageBus). Any other - # answer (stop / change-approach / request-changes) - # exits via the refine artifact return below — the same - # behaviour the spike's #2623 walking-skeleton had, - # minus the NotImplementedError fence. - if not _answer_continues_past_refine(refine_answer): - return str(artifact_path) - - # Stage 4: plan phase — BRC consensus across architect, - # task_planner, risk_analyst with reviewer_plan as the - # critical reviewer. Returns the plan artifact path - # and the evaluation snapshot for the HITL gate. - plan_artifact_path, plan_eval = self._run_plan_phase(artifact_path) - self._plan_artifact_path = plan_artifact_path - self._plan_eval = plan_eval - - # Stage 5: plan HITL gate — does the operator approve - # the plan? - plan_answer = yield self._build_plan_gate_decision(plan_artifact_path, plan_eval) - - # Walking-skeleton fence: if the operator chose - # "approve and continue to implement", we currently - # stop here. implement / pr phases are deferred to - # slice-3 / slice-4 of the #2717 rollout. - self._maybe_fence(plan_answer) - - return str(plan_artifact_path) - except _PreflightAborted as aborted: - # Reviewer v1 blocker #7: translate operator-abort - # into a clean StopIteration so the docstring's - # contract ("clean StopIteration with a diagnostic - # message rather than a NotImplementedError or a - # silent return") matches the actual behavior. The - # generator's StopIteration carries the diagnostic - # message as ``.value`` (the caller's - # ``stop.value`` is conventional for run_pipeline_in_process). - return str(aborted) - finally: - self._shutdown_background_threads() - # reviewer_concurrency v1 blocker #2: tear down worktrees - # so generator drop / NotImplementedError fence / normal - # completion all release the per-pipeline worktree - # directory + branch. Bound exceptions so teardown - # failures don't mask the original exit reason. - self._teardown_worktrees() - # reviewer_code v2 blocker #1: clean the active-role - # sentinel so the user's subsequent plain Claude Code - # session is not impeded by a stale role-based denial. - self._teardown_sentinel() - - # ------------------------------------------------------------------ - # Background-thread lifecycle - # ------------------------------------------------------------------ - - def _start_background_threads(self) -> None: - """Start the three background threads.""" - self._heartbeat_thread = threading.Thread( - target=self._heartbeat_loop, - name=f"egg-inproc-heartbeat-{self.pipeline_id}", - daemon=True, - ) - self._brc_review_thread = threading.Thread( - target=self._brc_review_loop, - name=f"egg-inproc-brc-review-{self.pipeline_id}", - daemon=True, - ) - self._bus_tick_thread = threading.Thread( - target=self._bus_tick_loop, - name=f"egg-inproc-bus-tick-{self.pipeline_id}", - daemon=True, - ) - self._threads = [ - self._heartbeat_thread, - self._brc_review_thread, - self._bus_tick_thread, - ] - for t in self._threads: - t.start() - - def _shutdown_background_threads(self) -> None: - """Signal shutdown and join the threads (bounded wait).""" - self._shutdown.set() - for t in self._threads: - # Bounded join — threads are daemon so the worst case - # is process exit, not a leaked OS thread. - t.join(timeout=2.0) - - def _teardown_worktrees(self) -> None: - """Release the per-pipeline worktree directory + branch. - - Reviewer_concurrency v1 blocker #2: the generator allocates - a worktree via ``bundle.worktrees.create(...)`` in - ``_spawn_refiner``. Without an explicit teardown the - directory + branch survive every exit path (normal - completion, NotImplementedError fence, GeneratorExit). - Wrapped in a broad except so teardown failures don't mask - the original generator-exit reason. - """ - bundle = getattr(self, "_bundle", None) - if bundle is None: - return - try: - bundle.worktrees.tear_down(self.pipeline_id) - except Exception: # noqa: BLE001 — defensive - # The original generator-exit reason wins. - pass - - def _heartbeat_loop(self) -> None: - """Publish HEARTBEAT messages to the substrate's message bus. - - Reviewer_code_holistic v1 finding #6: a counter-only tick is - not a real heartbeat. This loop publishes a structured - HEARTBEAT message via ``InProcessMessageBus`` so any orchestrator - primitive that listens for liveness (stuck-phase-transition - monitors, future BRC re-review subscribers) sees real - activity while the generator is paused at a yield boundary. - The tick counter remains for test observability. - """ - while not self._shutdown.wait(_HEARTBEAT_INTERVAL): - self._heartbeat_ticks += 1 - self._publish_heartbeat() - - def _brc_review_loop(self) -> None: - """Run a BRC re-review pass via ``PeerConsensusTracker``. - - Even in the refiner-only spike scope there is a single - producer in the tracker; the tick keeps the tracker alive and - exercises the same invariant-validation code path the HTTP - daemon uses. Reviewer_code_holistic v1 finding #6. - """ - while not self._shutdown.wait(_BRC_REVIEW_INTERVAL): - self._brc_review_ticks += 1 - self._tick_brc_review() - - def _bus_tick_loop(self) -> None: - """Tick the substrate's message bus. - - Reads any pending messages off the bus without consuming - them (since the in-process bus already notifies on add). The - tick exists so an external observer sees an active bus pump - — and so the bus's internal condition variables aren't - starved by a long-running spawn. - """ - while not self._shutdown.wait(_BUS_TICK_INTERVAL): - self._bus_ticks += 1 - self._tick_bus() - - def _publish_heartbeat(self) -> None: - """Best-effort heartbeat publish. Swallows exceptions so a - transient failure does not kill the background loop. - - Reviewer_concurrency v1 blocker #2: ``phase`` is read from - ``self._current_phase`` so the heartbeat reflects whichever - stage the generator is in (refine vs plan). The orchestrator's - stuck-phase-transition watchdog filters heartbeats by - ``phase``; a hardcoded refine string would make the - in-process orchestrator appear stalled during plan-stage - work even though the generator is making progress. - """ - try: - try: - from orchestrator.message_store import Message, MessageType - except ImportError: # pragma: no cover - from message_store import ( # type: ignore[no-redef, import-untyped] - Message, - MessageType, - ) - bus = self._get_bus() - if bus is None: - return - bus.add_message( - Message( - pipeline_id=self.pipeline_id, - from_role="orchestrator-inproc", - to_role="all", - message_type=MessageType.HEARTBEAT, - subject=f"inproc heartbeat #{self._heartbeat_ticks}", - body="", - phase=self._current_phase, - ) - ) - except Exception: # noqa: BLE001 — defensive - pass - - def _tick_brc_review(self) -> None: - """Touch the PeerConsensusTracker so its symbol is in the - call graph (TASK-1-6 acceptance bullet 6) and any pending - invariant validation runs.""" - try: - try: - from orchestrator.peer_consensus import ( - PeerConsensusTracker, # noqa: F401 - get_peer_consensus_tracker, - ) - except ImportError: # pragma: no cover - from peer_consensus import ( # type: ignore[no-redef, import-untyped] - PeerConsensusTracker, # noqa: F401 - get_peer_consensus_tracker, - ) - tracker = get_peer_consensus_tracker(self.pipeline_id) - if tracker is None: - return - # Best-effort tick — invoking any read-only method keeps - # the tracker engaged and triggers re-review timing on - # implementations that support it. - for attr in ("re_review_tick", "tick"): - fn = getattr(tracker, attr, None) - if callable(fn): - try: - fn() - except Exception: # noqa: BLE001 — defensive - pass - break - except Exception: # noqa: BLE001 — defensive - pass - - def _tick_bus(self) -> None: - try: - bus = self._get_bus() - if bus is None: - return - # Touch the bus's get_messages to keep its condition - # variables warm. - bus.get_messages(self.pipeline_id, limit=1) - except Exception: # noqa: BLE001 — defensive - pass - - def _get_bus(self) -> Any | None: - """Lazy bus accessor — used by background loops. - - Returns ``None`` if the substrate bundle's bus is a placeholder - (e.g., ``_K3sPlaceholder`` for the k3s leg without injected - overrides). Reviewer v1 non-blocking: without this check the - background ticks' bare ``except Exception`` swallowed the - placeholder's ``NotImplementedError`` on every iteration, - making the "loops exercise the invariant-validation code path" - claim quietly false. - """ - bundle = getattr(self, "_bundle", None) - if bundle is None: - try: - from . import _K3sPlaceholder, select_substrate - - self._bundle = select_substrate(self.env) - bundle = self._bundle - except Exception: # noqa: BLE001 — defensive - return None - else: - try: - from . import _K3sPlaceholder - except Exception: # noqa: BLE001 — defensive - _K3sPlaceholder = None # type: ignore[assignment] - bus = getattr(bundle, "bus", None) - if bus is None: - return None - if _K3sPlaceholder is not None and isinstance(bus, _K3sPlaceholder): - return None - return bus - - # ------------------------------------------------------------------ - # Contract-state synchronization - # ------------------------------------------------------------------ - - def _ensure_state_dirs(self) -> tuple[Path, Path, Path]: - """Make sure ``.egg-state/{drafts,contracts,checkpoints}/`` exist. - - Returns ``(drafts_dir, contracts_dir, checkpoints_dir)``. - """ - drafts = self.state_root / "drafts" - contracts = self.state_root / "contracts" - checkpoints = self.state_root / "checkpoints" - drafts.mkdir(parents=True, exist_ok=True) - contracts.mkdir(parents=True, exist_ok=True) - checkpoints.mkdir(parents=True, exist_ok=True) - return drafts, contracts, checkpoints - - def _write_pending_decision( - self, - decision_id: str, - question: str, - *, - phase: str = "refine", - ) -> Path: - """Write a pending HITL entry to the contract file. - - The shape mirrors what the HTTP daemon writes (``decisions`` - list with ``status="pending"``) so the skill's outer loop - and any external observer see a consistent view. - - ``phase`` is stamped onto both the new decision entry and the - contract's ``current_phase`` field so observers / tooling that - filter the decisions list by phase (or read ``current_phase`` - to reconstruct pipeline state) see a consistent value rather - than a hardcoded ``"refine"`` left over from the spike's - single-phase era. Reviewer_code v1 blocker B1 (#2717 slice-2). - - Concurrency (reviewer_concurrency v1 blocker #1): - - Acquires an exclusive ``fcntl.flock`` on a sidecar - ``.lock`` file for the duration of the - read-modify-write so concurrent writers (HTTP daemon + - generator, or two generator instances) cannot lose - updates. - - The lock is acquired with ``LOCK_EX | LOCK_NB`` and retried - on a bounded schedule (reviewer_code v2 non-blocking N8) so - a crashed sibling holding the lock surfaces as a - ``BlockingIOError`` after the timeout rather than hanging - the orchestrator forever. - - Writes through a sibling temp file followed by - ``os.replace()`` so concurrent readers never observe a - half-written file. - - The lock file is **intentionally not unlinked** after the - critical section. The ``flock + unlink`` pattern races - (reviewer_code v3 non-blocking NB2 / #2717 slice-2): an - unlink between two writers can leave them holding locks on - different inodes for the same path, defeating mutual - exclusion. Each lock file is a 0-byte sidecar — the - accumulated cruft is bounded by the number of distinct - pipeline ids the ``.egg-state/`` directory has ever seen, - and the cost of one inode per pipeline is far smaller than - the cost of double-writing decisions. - """ - import fcntl - - _, contracts_dir, _ = self._ensure_state_dirs() - contract_path = contracts_dir / f"{self.pipeline_id}.json" - lock_path = contract_path.with_suffix(".lock") - tmp_path = contract_path.with_suffix(".json.tmp") - - with open(lock_path, "w") as lock_fp: - self._acquire_flock_with_timeout(lock_fp) - try: - try: - if contract_path.exists(): - contract = json.loads(contract_path.read_text()) - else: - contract = { - "schemaVersion": "1.1", - "pipeline_id": self.pipeline_id, - "current_phase": phase, - "decisions": [], - } - except (json.JSONDecodeError, OSError): # fmt: skip - contract = { - "schemaVersion": "1.1", - "pipeline_id": self.pipeline_id, - "current_phase": phase, - "decisions": [], - } - - # Track the caller's phase on the contract so the - # decisions list and ``current_phase`` agree. - contract["current_phase"] = phase - - decisions = list(contract.get("decisions") or []) - # Idempotent: skip if already present. - if not any(d.get("id") == decision_id for d in decisions): - decisions.append( - { - "id": decision_id, - "question": question, - "status": "pending", - "phase": phase, - } - ) - contract["decisions"] = decisions - - # Atomic publish via temp + replace. - tmp_path.write_text(json.dumps(contract, indent=2)) - os.replace(tmp_path, contract_path) - finally: - fcntl.flock(lock_fp.fileno(), fcntl.LOCK_UN) - return contract_path - - # Reviewer_code v2 non-blocking N8: bound the flock wait so a - # crashed sibling holding the lock surfaces as a ``BlockingIOError`` - # after the timeout rather than hanging the orchestrator forever. - _FLOCK_TIMEOUT_SECONDS: float = 30.0 - _FLOCK_RETRY_INTERVAL_SECONDS: float = 0.05 - - def _acquire_flock_with_timeout(self, lock_fp: Any) -> None: - """Acquire ``fcntl.LOCK_EX`` on ``lock_fp`` with a bounded retry. - - Raises ``BlockingIOError`` after ``_FLOCK_TIMEOUT_SECONDS`` if - the lock cannot be acquired. The retry loop polls - ``LOCK_EX | LOCK_NB`` every ``_FLOCK_RETRY_INTERVAL_SECONDS`` - so an orderly contended writer wins the lock quickly while a - crashed lock-holder eventually surfaces as a timeout. - - The deadline is checked before sleeping AND the sleep itself - is clamped to the remaining budget so the documented - ``_FLOCK_TIMEOUT_SECONDS`` ceiling is the true upper bound - (reviewer_code v3 non-blocking NB4 / #2717 slice-2). Without - the clamp the worst-case wait is - ``_FLOCK_TIMEOUT_SECONDS + _FLOCK_RETRY_INTERVAL_SECONDS``. - """ - import fcntl - - deadline = time.monotonic() + self._FLOCK_TIMEOUT_SECONDS - while True: - try: - fcntl.flock(lock_fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) - return - except BlockingIOError: - remaining = deadline - time.monotonic() - if remaining <= 0: - raise - time.sleep(min(self._FLOCK_RETRY_INTERVAL_SECONDS, remaining)) - - # ------------------------------------------------------------------ - # HITL decisions - # ------------------------------------------------------------------ - - def _build_preflight_decision(self) -> Any: - """Build the pre-flight HITL decision (cq-7 first yield). - - The decision asks the operator to confirm the resolved repo - + issue before the refiner spawns. ``HITLDecision`` is - imported lazily so this module imports cheaply. - """ - try: - from orchestrator.models import HITLDecision - except ImportError: # pragma: no cover - from models import HITLDecision # type: ignore[no-redef, import-untyped] - - decision_id = f"preflight-{self.pipeline_id}" - self._write_pending_decision( - decision_id, - "Confirm the refiner will run against this repo + issue?", - phase="refine", - ) - return HITLDecision( - id=decision_id, - question="Confirm the refiner will run against this repo + issue?", - context=( - f"pipeline_id={self.pipeline_id}\n" - f"repo={self.repo or ''}\n" - f"issue={self.issue_number or ''}" - ), - options=["approve", "abort"], - decision_type="choice", - phase="refine", # type: ignore[arg-type] - ) - - def _build_refine_gate_decision( - self, artifact_path: Path, spawn_result: Any | None = None - ) -> Any: - """Build the post-refine HITL gate decision (cq-7 second yield). - - The decision is the standard refine gate: approve / request - changes / change approach / stop. Surfaces ``AgentResult`` - diagnostics (exit code, commit SHA, stdout-tail) in the - decision context so the operator can act on failure rather - than approving a refine that never ran - (reviewer_code_holistic v1 finding #11). - """ - try: - from orchestrator.models import HITLDecision - except ImportError: # pragma: no cover - from models import HITLDecision # type: ignore[no-redef, import-untyped] - - exit_code = int(getattr(spawn_result, "exit_code", 0) or 0) - commit_sha = getattr(spawn_result, "commit_sha", None) - stdout_tail = (getattr(spawn_result, "stdout", "") or "")[-500:] - - if exit_code != 0: - decision_id = f"refine-failure-{self.pipeline_id}" - question = ( - f"Refiner FAILED (exit_code={exit_code}). " - f"Review {artifact_path} for diagnostics; " - f"choose retry / abort." - ) - options = ["retry", "abort"] - else: - decision_id = f"refine-gate-{self.pipeline_id}" - question = ( - f"Refine analysis at {artifact_path} " - f"(exit_code=0, commit={commit_sha or ''}). " - "Approve and continue?" - ) - options = [ - "approve_continue", - "request_changes", - "change_approach", - "stop", - ] - - self._write_pending_decision(decision_id, question, phase="refine") - context_block = ( - f"artifact={artifact_path}\n" - f"exit_code={exit_code}\n" - f"commit_sha={commit_sha or ''}\n" - f"stdout_tail=\n{stdout_tail}\n" - ) - return HITLDecision( - id=decision_id, - question=question, - context=context_block, - options=options, - decision_type="phase_gate", - phase="refine", # type: ignore[arg-type] - ) - - def _build_plan_gate_decision( - self, plan_artifact_path: Path, plan_eval: Mapping[str, Any] - ) -> Any: - """Build the post-plan HITL gate decision (#2717 slice-2 third yield). - - After ``_run_plan_phase`` reaches CONSENSUS_CONFIRMED on every - producer edge (architect, task_planner, risk_analyst → - reviewer_plan), the operator sees this gate to approve the - produced plan or send the producers back for another cycle. - - The decision context surfaces the plan artifact path AND the - BRC evaluation snapshot (which agents confirmed, any - unresolved NACK details) so the operator can act on a - partial-consensus state rather than approving a plan that - never actually converged. - """ - try: - from orchestrator.models import HITLDecision - except ImportError: # pragma: no cover - from models import HITLDecision # type: ignore[no-redef, import-untyped] - - is_complete = bool(plan_eval.get("is_complete")) - blocking_agents = list(plan_eval.get("blocking_agents") or []) - unresolved_nacks = list(plan_eval.get("unresolved_nack_details") or []) - - if is_complete: - decision_id = f"plan-gate-{self.pipeline_id}" - question = ( - f"Plan artifact at {plan_artifact_path} reached " - "CONSENSUS_CONFIRMED on every reviewer_plan edge. " - "Approve and continue to implement?" - ) - options = [ - "approve_continue", - "request_changes", - "change_approach", - "stop", - ] - else: - decision_id = f"plan-failure-{self.pipeline_id}" - question = ( - f"Plan-phase BRC did NOT converge " - f"(blocking_agents={blocking_agents!r}). " - f"Review {plan_artifact_path} and unresolved NACK " - "details; choose retry / abort." - ) - options = ["retry", "abort"] - - self._write_pending_decision(decision_id, question, phase="plan") - context_block = ( - f"artifact={plan_artifact_path}\n" - f"is_complete={is_complete}\n" - f"blocking_agents={blocking_agents!r}\n" - f"unresolved_nacks={unresolved_nacks!r}\n" - ) - return HITLDecision( - id=decision_id, - question=question, - context=context_block, - options=options, - decision_type="phase_gate", - phase="plan", # type: ignore[arg-type] - ) - - # ------------------------------------------------------------------ - # Refiner spawn - # ------------------------------------------------------------------ - - def _spawn_refiner(self) -> tuple[Path, Any]: - """Dispatch the refiner via the substrate bundle and return - ``(artifact_path, AgentResult)``. - - Routes through ``ConcurrentPhaseExecutor._spawn_agent`` so - the substrate seam exercises the existing executor - primitive (TASK-1-6 acceptance bullet 6: "Existing primitives - stay in the path: ... ``ConcurrentPhaseExecutor`` ..., - ``PeerConsensusTracker`` ..."). Touching - ``PeerConsensusTracker`` happens via the BRC re-review - background loop above. - """ - from egg_contracts.agent_roles import AgentRole - - from . import select_substrate - - # Reference ConcurrentPhaseExecutor + PeerConsensusTracker so - # the primitives stay in this module's call graph. The - # executor's full machinery is overkill for a single refiner - # spawn — the spike reuses the substrate seam directly. The - # imports above are not unused: they document the - # acceptance-bullet primitives and any future expansion of - # the spike (multi-role spawn) will use them in anger. - try: - from orchestrator.concurrent_executor import ( - ConcurrentPhaseExecutor, # noqa: F401 - ) - from orchestrator.peer_consensus import ( - create_peer_consensus_tracker, # noqa: F401 - ) - except ImportError: # pragma: no cover - from concurrent_executor import ( # type: ignore[no-redef, import-untyped] - ConcurrentPhaseExecutor, # noqa: F401 - ) - from peer_consensus import ( # type: ignore[no-redef, import-untyped] - create_peer_consensus_tracker, # noqa: F401 - ) - - drafts_dir, _, _ = self._ensure_state_dirs() - artifact_id = self.issue_number or self.pipeline_id - artifact_path = drafts_dir / f"{artifact_id}-analysis.md" - - bundle = select_substrate(self.env) - self._bundle = bundle # share with background loops - - # Allocate a per-agent worktree under .egg-state///. - worktree = bundle.worktrees.create(self.pipeline_id, AgentRole.REFINER) - - spawn_env = { - **self.env, - "EGG_PIPELINE_ID": self.pipeline_id, - "EGG_AGENT_ROLE": AgentRole.REFINER.value, - "EGG_REPO_ROOT": str(worktree), - "EGG_WORKTREE_ROOT": str(worktree), - } - if self.repo: - spawn_env["EGG_REPO"] = self.repo - if self.issue_number is not None: - spawn_env["EGG_ISSUE_NUMBER"] = str(self.issue_number) - - # Sentinel file: writes the active role to a known location - # so the PreToolUse hook (in a separate subprocess) can read - # the role even if env propagation drops it on nested - # dispatch. Reviewer_code_holistic v1 finding #8. - self._write_active_role_sentinel(AgentRole.REFINER.value) - - spawn_result = bundle.spawner.spawn( - AgentRole.REFINER, - self.issue_body, - spawn_env, - worktree, - ) - - # Surface the AgentResult — reviewer_code_holistic v1 finding - # #10. The exit_code, commit_sha, and stdout drive the refine - # HITL gate's question text and the placeholder analysis body - # below. - exit_code = int(getattr(spawn_result, "exit_code", 0) or 0) - commit_sha = getattr(spawn_result, "commit_sha", None) - stdout = getattr(spawn_result, "stdout", "") or "" - - # The spike's deliberate "minimum proof": when the spawner - # does not produce the canonical artifact (harness - # unavailable in this environment, exit_code != 0, etc.) we - # write a placeholder that EXPOSES the failure so the operator - # sees actionable diagnostics in the refine HITL gate. - # Reviewer_code_holistic v1 finding #11. - if not artifact_path.exists(): - artifact_path.write_text( - "# Refiner analysis (placeholder — refiner did not produce a full analysis)\n\n" - f"Pipeline: {self.pipeline_id}\n" - f"Repo: {self.repo or ''}\n" - f"Issue: {self.issue_number or ''}\n" - f"Worktree: {worktree}\n" - f"Spawner exit code: {exit_code}\n" - f"Commit SHA: {commit_sha or ''}\n\n" - "## Spawner stdout (truncated to 2000 chars)\n\n" - "```\n" - f"{stdout[:2000]}\n" - "```\n\n" - "This placeholder was emitted by `run_pipeline_in_process` " - "because the underlying subagent harness did not land " - f"`{artifact_path.name}` itself. Inspect the spawner " - "diagnostics above; a non-zero exit code means the " - "refiner failed and the refine HITL gate will surface " - "the failure to the operator.\n" - ) - - return artifact_path, spawn_result - - # ------------------------------------------------------------------ - # Plan-phase BRC (#2717 slice-2 — TASK-2-1) — body in _plan_phase.py - # ------------------------------------------------------------------ - - def _run_plan_phase(self, refine_artifact_path: Path) -> tuple[Path, dict[str, Any]]: - """Run the plan phase BRC cycle. - - Architect-first then fan-out (reviewer_code_holistic v1 - blocker H1): spawn architect synchronously first, then - fan out task_planner + risk_analyst concurrently through - a ThreadPoolExecutor(max_workers=2). Reviewer_plan - dispatches once after the fan-out; its verdict JSON drives - per-edge ACK / NACK on the tracker (reviewer_code_holistic - v1 blocker H2 — verdict-based not exit-code-based). - Heartbeat phase flips to "plan" for the duration - (reviewer_concurrency v1 blocker #2). - - Body lives in orchestrator/substrate/_plan_phase.py so - in_process.py stays under the repo's 1500-line cap — - see scripts/file-size-allowlist.yaml. The class method - is the public surface tests / external callers use; the - module-level function is a coder-side decomposition seam. - """ - from . import _plan_phase - - return _plan_phase.run_plan_phase(self, refine_artifact_path) - - # Plan-phase delegate helpers — surfaced on the class so tests - # and observability code can call them as methods. Each delegates - # to _plan_phase for the body so the in_process module stays - # under 1500 lines without losing the class-method API. - - def _spawn_plan_producer( - self, - role: Any, - bundle: Any, - refine_artifact_path: Path, - plan_artifact_path: Path, - architect_output_path: Path | None = None, - ) -> tuple[Path, Any]: - """See _plan_phase.spawn_plan_producer.""" - from . import _plan_phase - - return _plan_phase.spawn_plan_producer( - self, - role, - bundle, - refine_artifact_path, - plan_artifact_path, - architect_output_path, - ) - - def _spawn_plan_reviewer( - self, - bundle: Any, - producer_artifacts: Mapping[Any, Path], - plan_artifact_path: Path, - ) -> tuple[Path, Any]: - """See _plan_phase.spawn_plan_reviewer.""" - from . import _plan_phase - - return _plan_phase.spawn_plan_reviewer(self, bundle, producer_artifacts, plan_artifact_path) - - def _plan_producer_output_path(self, role: Any) -> Path: - """See _plan_phase.plan_producer_output_path.""" - from . import _plan_phase - - return _plan_phase.plan_producer_output_path(self, role) - - def _read_plan_reviewer_verdicts( - self, - *, - plan_producers: list[Any] | None = None, - ) -> tuple[Path | None, dict[str, dict[str, Any]]]: - """See _plan_phase.read_plan_reviewer_verdicts.""" - from . import _plan_phase - - return _plan_phase.read_plan_reviewer_verdicts(self, plan_producers=plan_producers) - - def _write_active_role_sentinel(self, role: str) -> None: - """Write the active agent role to a known location. - - The PreToolUse hook runs in a separate Claude Code subprocess - and may not inherit the spawner's ``env=`` argument under - nested dispatch. The sentinel file is the spawn↔hook - coordination channel that survives the process boundary — - reviewer_code_holistic v1 finding #8. - - Location: ``$HOME/.claude/egg-active-role.json`` (per-user; - not per-pipeline — only one refiner runs at a time in the - spike). The hook reads it as a fallback when - ``EGG_AGENT_ROLE`` is unset. - - Future-multi-role caveat (reviewer v1 non-blocking): this - single-valued, per-user file cannot disambiguate two - concurrent sub-agents in different roles. The R2 deferral's - multi-role rollout cannot use this sentinel for role-routing - without a breaking change to the sentinel shape (e.g., - per-PID or per-thread keys). The follow-up issue's "Validate - PreToolUse hook role-routing (R2)" bullet captures the - cleanup. - - Reviewer_code v2 blocker #1: the sentinel is stamped with - the orchestrator's PID so a stale sentinel from a crashed - run (OOM, hard kill) does not poison a subsequent plain - Claude Code session — the hook's - ``_resolve_active_role`` treats sentinel files whose PID is - no longer alive as missing. The generator's ``finally`` - block also unlinks the sentinel via - ``_teardown_sentinel()``. - """ - try: - home = Path(os.environ.get("HOME", "")) - if not home or not home.exists(): - return - target = home / ".claude" / "egg-active-role.json" - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text( - json.dumps( - { - "role": role, - "pipeline_id": self.pipeline_id, - "repo": self.repo or None, - "pid": os.getpid(), - } - ) - + "\n" - ) - except OSError: # pragma: no cover — defensive - pass - - def _teardown_sentinel(self) -> None: - """Remove the active-role sentinel on generator exit. - - Reviewer_code v2 blocker #1: without explicit cleanup, a - completed (or aborted) run leaves a stale sentinel in - ``$HOME/.claude/egg-active-role.json`` that the - PreToolUse hook reads as the active role — causing the user's - next plain Claude Code session to refuse writes outside the - stale role's allow-list. - - Wrapped in a broad except so unlink failure doesn't mask the - original generator-exit reason. - """ - try: - home_env = os.environ.get("HOME", "") - if not home_env: - return - target = Path(home_env) / ".claude" / "egg-active-role.json" - if target.exists(): - target.unlink(missing_ok=True) - except OSError: # pragma: no cover — defensive - pass - - # ------------------------------------------------------------------ - # Walking-skeleton fence - # ------------------------------------------------------------------ - - @staticmethod - def _maybe_fence(answer: Any) -> None: - """Raise ``NotImplementedError`` if the operator asked to - continue past the *current terminal* phase. - - Scope-fence semantics evolve with the #2717 rollout: - - * #2623 spike: fences after the refine HITL gate. - * #2717 slice-2 (this slice): refine → plan is wired, so the - fence now fires on the plan HITL gate's - ``approve_continue`` answer — the implement phase ships in - slice-3. - * #2717 slice-3: implement-phase wiring lands, the fence - moves to the implement HITL gate. - * #2717 slice-4: pr-phase wiring lands and TASK-4-2 removes - this method entirely (and the call site in ``run``). - - Today's behaviour: ``approve_continue`` past the plan gate - raises with a slice-3 pointer; any other answer is a - no-op (the generator returns the artifact path). - """ - if answer is None: - return - # Accept either a bare string or an answer dict. - if isinstance(answer, dict): - answer = answer.get("selected") or answer.get("value") - if isinstance(answer, str) and answer.startswith("approve_continue"): - raise NotImplementedError( - "egg-sdlc #2717 slice-2: implement / pr phases are " - "deferred to slice-3 / slice-4. See the rollout DAG " - "in docs/architecture/claude-code-substrate.md." - ) - - -def _sleep_or_shutdown(interval: float, shutdown: threading.Event) -> bool: - """Return ``True`` if the shutdown event fires during the sleep.""" - return shutdown.wait(interval) - - -#: Lowercased answer strings the operator can submit to indicate "abort -#: the run, do not advance the generator past this yield." Single source -#: of truth shared with the flattened bridge driver -#: (``plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py``) and the -#: slice-3 daemon variant, so all three surfaces agree on what counts as -#: abort without drifting independently. -ABORT_ANSWERS: frozenset[str] = frozenset({"abort", "stop", "cancel"}) - - -def _answer_is_abort(answer: Any) -> bool: - """Return True if a HITL ``answer`` indicates the operator aborted. - - Accepts the bare-string form (``"abort"``) and the dict form - Claude Code's ``AskUserQuestion`` returns - (``{"selected": "abort"}`` etc.). - """ - if answer is None: - return False - if isinstance(answer, dict): - answer = answer.get("selected") or answer.get("value") - return isinstance(answer, str) and answer.lower() in ABORT_ANSWERS - - -def _answer_continues_past_refine(answer: Any) -> bool: - """Return True if the refine-gate ``answer`` advances to plan. - - The refine HITL gate exposes ``approve_continue`` / - ``request_changes`` / ``change_approach`` / ``stop`` (and - ``retry`` / ``abort`` on the failure path). Only - ``approve_continue`` triggers the plan stage dispatch — every - other answer either re-runs the refiner (out of #2717 slice-2 - scope) or stops the generator cleanly with the refine artifact - as the return value. - - Mirrors ``_answer_is_abort``'s accepted shapes (bare string OR - Claude Code's ``{"selected": "..."}`` dict). - """ - if answer is None: - return False - if isinstance(answer, dict): - answer = answer.get("selected") or answer.get("value") - return isinstance(answer, str) and answer.lower().startswith("approve_continue") - - -class _PreflightAborted(RuntimeError): - """Raised inside the generator when the operator aborts at the - pre-flight HITL. Translates into a clean StopIteration with a - diagnostic message rather than a NotImplementedError or a - silent return.""" - - -# Re-exported for tests that want a fast tick budget. -__all__ = [ - "ABORT_ANSWERS", - "_BRC_REVIEW_INTERVAL", - "_BUS_TICK_INTERVAL", - "_HEARTBEAT_INTERVAL", - "run_pipeline_in_process", -] - - -# Keep ``time`` imported for tests that monkeypatch it. -_ = time # noqa: F841 (intentional retention) diff --git a/orchestrator/substrate/k3s_adapter.py b/orchestrator/substrate/k3s_adapter.py deleted file mode 100644 index 828db71fea..0000000000 --- a/orchestrator/substrate/k3s_adapter.py +++ /dev/null @@ -1,186 +0,0 @@ -"""k3s adapter satisfying the ``AgentSpawner`` protocol (#2623). - -See ``docs/architecture/claude-code-substrate.md`` for the broader -substrate model. The Claude Code substrate is exercised end-to-end by -the walking-skeleton spike (cq-11); the k3s substrate is a co-equal -substrate (cq-1 + cq-9) and must continue to work after the -``concurrent_executor._spawn_agent`` seam is patched in TASK-1-2 to -dispatch through ``select_substrate(...).spawner``. - -This adapter is the thin wrapper that lets the existing -``KubernetesSpawner.create_concurrent_spawn_fn`` factory satisfy the -new ``AgentSpawner`` protocol from day one — without re-implementing -spawn lifecycle, image selection, or session registration on the k3s -side. - -INTERFACE STABILITY: v0.x unstable. - -The k3s adapter is intentionally a re-wire shim: it imports from -``orchestrator.kubernetes_spawner`` but does NOT patch the call site -that's a TASK-1-2 concern. The adapter is purely additive in this -file; the existing -``orchestrator/concurrent_executor.py:504 _spawn_agent`` call site -remains untouched until TASK-1-2. -""" - -from __future__ import annotations - -import time -from collections.abc import Mapping -from pathlib import Path -from typing import Any - -from egg_contracts.agent_roles import AgentRole - -from .spawner import AgentResult - - -class K3sSpawnerAdapter: - """Adapter wrapping ``KubernetesSpawner.create_concurrent_spawn_fn``. - - The wrapped factory returns a callable with the legacy spawn - signature ``(role, branch, extra_env, command) -> SpawnedContainer``. - This adapter exposes the new - ``spawn(role, prompt, env, worktree) -> AgentResult`` signature - from cq-4 and bridges the two by: - - 1. Calling the legacy factory with empty ``command`` (production - paths build the prompt-wrapping command inside the orchestrator - via ``build_consensus_wrapped_command`` already; the wrapper - passes through to the spawner unchanged). - 2. Capturing the resulting container's commit SHA via - ``git -C rev-parse HEAD`` so reviewers can attach - commit-bound ACKs (INV-6, - ``orchestrator/action_guards.py:631``, body at line 757). - 3. Translating the legacy ``SpawnedContainer`` shape into the new - ``AgentResult`` shape. - - The adapter does not block on the underlying job — the legacy - factory was always fire-and-monitor; the synchronous-spawn - contract for the k3s leg is satisfied because the orchestrator's - existing ``ConcurrentPhaseExecutor`` waits on container - completion via ``container_id`` after the legacy spawn returns. - Callers that need true blocking should run inside the - ConcurrentPhaseExecutor's monitor loop or layer their own wait on - top of the returned ``container_id``. - - Args: - legacy_spawn_fn: The callable returned by - ``KubernetesSpawner.create_concurrent_spawn_fn(...)``. - worktree_resolver: Optional callable - ``(role: AgentRole, branch: str | None) -> Path | None`` - that returns the on-disk worktree path so the adapter can - run ``git rev-parse HEAD``. When ``None``, the adapter - skips commit-sha capture and returns ``AgentResult`` - with ``commit_sha=None`` — callers (or downstream - reviewers) can fall back to a separate commit-attestation - channel. - """ - - def __init__( - self, - legacy_spawn_fn: Any, - *, - worktree_resolver: Any | None = None, - ) -> None: - self._legacy = legacy_spawn_fn - self._resolve_worktree = worktree_resolver - - def spawn( - self, - role: AgentRole, - prompt: str, - env: Mapping[str, str], - worktree: Path, - ) -> AgentResult: - """Spawn an agent via the wrapped k3s factory. - - Args: - role: The role to spawn. - prompt: Task prompt (forwarded as the ``command`` arg to - the legacy factory when non-empty; the orchestrator's - production paths normally pre-build this via - ``build_consensus_wrapped_command``). - env: Extra env vars to inject into the agent container. - worktree: Path used for the commit-sha capture. The k3s - spawner manages its own gateway-side worktree - independently; this argument is only consulted at the - end to capture ``commit_sha``. - """ - start = time.monotonic() - # ``prompt`` here is forwarded as the optional ``command`` - # arg only when callers want to inject a non-default command; - # most production paths construct the command outside the - # spawner and pass it via ``extra_env`` already. - command: list[str] | None = None - if prompt: - command = ["claude", "--print", prompt] - spawned = self._legacy( - role=role, - branch=None, - extra_env=dict(env), - command=command, - ) - duration = time.monotonic() - start - - # reviewer_concurrency v1 blocker #4: do NOT capture - # ``commit_sha`` here. The legacy spawn factory is - # fire-and-monitor (see the docstring above); this method - # returns BEFORE the pod has produced its commit, so a - # ``git rev-parse HEAD`` against the orchestrator-host - # worktree would capture the pre-spawn HEAD and BRC reviewers - # would attach commit-bound ACKs to the wrong SHA. - # - # The k3s leg's INV-6 ``ack_commit_sha`` is populated by the - # existing gateway-side attestation channel: the - # orchestrator's monitor loop reads the legitimate SHA off - # ``SpawnedContainer.container_info`` once the pod - # terminates. The follow-up issue covers wiring that channel - # into ``AgentResult.commit_sha`` directly so the new - # protocol contract is also satisfied end-to-end on k3s. - target_worktree = worktree - if self._resolve_worktree is not None: - resolved = self._resolve_worktree(role, None) - if resolved is not None: - target_worktree = resolved - - commit_sha: str | None = None - # Structured note for downstream observability — the gap is - # documented; INV-6 mismatches will correlate with this - # log line rather than showing up first at review-time. - # Use the structured logger instead of stderr ``print`` so - # the message routes through the daemon's log pipeline - # (reviewer_code v2 non-blocking). - try: - try: - from egg_logging import get_logger - except ImportError: # pragma: no cover - import logging - - _logger = logging.getLogger("orchestrator.substrate.k3s_adapter") - else: - _logger = get_logger("orchestrator.substrate.k3s_adapter") - _logger.warning( - "k3s commit_sha intentionally None (fire-and-monitor " - "factory races producer commit); legacy gateway-side " - "attestation channel is authoritative for k3s INV-6 " - "SHAs. See substrate ADR follow-up appendix.", - extra={"role": getattr(role, "value", str(role))}, - ) - except Exception: # noqa: BLE001 — defensive - pass - - # The legacy ``SpawnedContainer`` carries no stdout/exit_code - # directly — that data lands on ``container_info`` and is - # consumed by the orchestrator's monitor loop. We surface a - # minimal ``AgentResult`` here so the new protocol contract - # is satisfied; callers that need the full container payload - # continue to read it via the orchestrator's container store. - return AgentResult( - stdout=getattr(spawned, "stdout", ""), - exit_code=getattr(spawned, "exit_code", 0), - duration_seconds=duration, - worktree=target_worktree, - commit_sha=commit_sha, - artifacts=[], - ) diff --git a/orchestrator/substrate/message_bus.py b/orchestrator/substrate/message_bus.py deleted file mode 100644 index 87ebd03d83..0000000000 --- a/orchestrator/substrate/message_bus.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Inter-agent message-bus protocol for the substrate-swap spike (#2623). - -See ``docs/architecture/claude-code-substrate.md`` for the broader -substrate model. This module defines the ``MessageBus`` protocol every -substrate implementation must satisfy. - -The shape mirrors the existing -``orchestrator/message_store.py:200 MessageStore`` semantics so the -in-process Claude Code implementation can subclass or delegate to it -and the k3s/Redis-backed implementation continues to work unchanged. - -The BRC concurrency invariants (INV-1..INV-6) from -``orchestrator/action_guards.py:631 validate_invariants`` are bus- -implementation-independent — the in-process bus must preserve them -just as the Redis-backed bus does. ``orchestrator/peer_consensus.py`` -holds the invariant logic; the bus only ferries messages. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -from collections.abc import Sequence -from typing import Protocol, runtime_checkable - - -@runtime_checkable -class MessageBus(Protocol): - """Protocol every substrate message bus must satisfy. - - The shape matches the public surface of - ``orchestrator.message_store.MessageStore`` for drop-in - interchangeability. - """ - - def add_message(self, message: object) -> object: - """Append a message to the bus. - - Args: - message: A ``Message`` instance (see - ``orchestrator/message_store.py``). Typed as ``object`` - in the protocol to avoid a circular import; concrete - implementations should accept and return - ``Message``. - - Returns: - The stored message (typically the same instance, possibly - with a generated ``id`` populated). - """ - ... - - def get_messages( - self, - pipeline_id: str, - *, - role: str | None = None, - since_id: str | None = None, - limit: int = 100, - wait: int = 0, - wait_for_types: Sequence[str] | None = None, - from_role: str | None = None, - from_tip: bool = False, - ) -> list[object]: - """Return messages for ``pipeline_id``, optionally filtered. - - See ``orchestrator.message_store.MessageStore.get_messages`` - for the full semantics. When ``wait > 0``, the implementation - is expected to block on a condition variable (in-process) or - equivalent (e.g. Redis ``XREAD BLOCK``) until a matching - message arrives or the timeout expires. - """ - ... diff --git a/orchestrator/substrate/policy.py b/orchestrator/substrate/policy.py deleted file mode 100644 index 0ed86ea48e..0000000000 --- a/orchestrator/substrate/policy.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Policy-enforcement protocol for the substrate-swap spike (#2623). - -See ``docs/architecture/claude-code-substrate.md`` for the broader -substrate model. This module defines the ``PolicyEnforcer`` protocol -every substrate implementation must satisfy. - -HITL decision cq-6 pins the policy seam to **PreToolUse hooks** for -the Claude Code substrate: a ``.claude/settings.json``-registered hook -script intercepts Write/Edit/Bash calls before they execute and -denies anything that would land outside the caller's role's allow- -list. The hook uses -``shared/egg_restrictions/patterns.py:768 build_agent_patterns`` — -the **same** symbol the gateway path uses in -``gateway/phase_filter.py:1061 check_agent_restrictions`` — so the two -substrates share a single source of truth for file boundaries. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -from typing import Protocol, runtime_checkable - - -@runtime_checkable -class PolicyEnforcer(Protocol): - """Protocol every substrate policy enforcer must satisfy. - - The protocol surfaces two operations: - - 1. ``check_write`` — synchronous "may this role write this path?" - used by hook scripts and any in-process validator. - 2. ``install`` — set up the substrate-side enforcement primitive - (e.g., write a ``.claude/settings.json`` template for the - Claude Code substrate; no-op for the gateway-backed k3s - substrate, where enforcement is already wired up). - """ - - def check_write(self, role: str, path: str) -> tuple[bool, str | None]: - """Return ``(allowed, message)`` for a proposed write. - - Args: - role: Agent role making the write (e.g. ``"coder"``). - path: Repo-relative path the role wants to write. - - Returns: - Tuple of ``(allowed, denial_message)``. When - ``allowed=True``, the denial message is ``None``. When - ``allowed=False``, the message describes why and matches - the format used by - ``gateway/phase_filter.py:1061 check_agent_restrictions``. - """ - ... - - def install(self, target_dir: str) -> None: - """Install the substrate's enforcement primitive at - ``target_dir``. - - For the Claude Code substrate, this writes - ``.claude/settings.json`` referencing the PreToolUse hook - entry. For the k3s substrate, this is a no-op (the gateway - sidecar enforces structurally). - """ - ... diff --git a/orchestrator/substrate/spawner.py b/orchestrator/substrate/spawner.py deleted file mode 100644 index 62d9c5fcaf..0000000000 --- a/orchestrator/substrate/spawner.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Agent-spawner protocol for the substrate-swap walking skeleton (#2623). - -See the architecture decision record at -``docs/architecture/claude-code-substrate.md`` for the four-interface -substrate model. This module defines the ``AgentSpawner`` protocol and -the ``AgentResult`` dataclass that every substrate implementation must -satisfy. - -The protocol shape is pinned by HITL decision cq-4 (synchronous spawn): -``spawn(role, prompt, env, worktree) -> AgentResult`` blocks the caller -until the agent completes. Internal concurrency (thread pool, etc.) is -the spawner's responsibility — callers can issue concurrent ``spawn()`` -calls from a ``ThreadPoolExecutor`` if they need fan-out. - -INTERFACE STABILITY: v0.x unstable. - -This abstraction is part of a walking-skeleton spike (cq-11). The shape -may change in incompatible ways in the follow-up rollout; downstream -consumers should not assume API stability until the follow-up issue -formally promotes the protocol. -""" - -from __future__ import annotations - -from collections.abc import Mapping -from dataclasses import dataclass, field -from pathlib import Path -from typing import Protocol, runtime_checkable - -from egg_contracts.agent_roles import AgentRole - - -@dataclass -class AgentResult: - """Result of a synchronous spawn invocation. - - Fields: - stdout: Captured stdout from the agent (may be empty when the - agent writes only via the filesystem). - exit_code: Process exit code. 0 means success. - duration_seconds: Wall-clock seconds the spawn took, end-to-end. - worktree: Path to the worktree the agent ran in. The caller - typically reads artifacts out of this directory after the - spawn returns. - commit_sha: SHA of the worktree's HEAD commit **after** the - agent ran, when the substrate can capture it - synchronously. The claude-code substrate's - ``ClaudeCodeSpawner`` captures this via - ``git -C rev-parse HEAD`` immediately before - the ``AgentResult`` is returned. ``None`` is also a - valid value: the k3s ``K3sSpawnerAdapter`` always returns - ``None`` because the legacy fire-and-monitor factory - races the producer commit — the SHA is supplied through - the gateway-side attestation channel that already feeds - INV-6 (``orchestrator/action_guards.py:631``, body at - line 757) for the k3s leg. Consumers that need a - populated SHA on every substrate should fall back to the - substrate-native attestation channel when this field is - ``None``. Wiring the gateway attestation through to this - field is tracked in the substrate ADR follow-up. - artifacts: Optional list of artifact paths the agent produced. - Substrate-implementation-defined; empty by default. - """ - - stdout: str = "" - exit_code: int = 0 - duration_seconds: float = 0.0 - worktree: Path | None = None - commit_sha: str | None = None - artifacts: list[str] = field(default_factory=list) - - -@runtime_checkable -class AgentSpawner(Protocol): - """Protocol every substrate spawner must satisfy. - - Method signature pinned by HITL decision cq-4 (synchronous spawn): - the caller blocks until the agent finishes. Substrate - implementations own their internal concurrency strategy; the - ``ConcurrentPhaseExecutor`` fans out by submitting concurrent - ``spawn()`` calls to a ``ThreadPoolExecutor``. - """ - - def spawn( - self, - role: AgentRole, - prompt: str, - env: Mapping[str, str], - worktree: Path, - ) -> AgentResult: - """Spawn an agent of ``role`` and block until it completes. - - Args: - role: The role to spawn (drives system-prompt assembly and - file-restriction enforcement). - prompt: The role-specific task prompt to inject. The - spawner is responsible for prepending the canonical - role rubric via ``build_system_prompt(...)``. - env: Environment variables to set on the agent. The - spawner may add or override entries (e.g. - ``EGG_AGENT_ROLE``, ``EGG_PIPELINE_ID``). - worktree: Path to the per-agent worktree the spawner has - already created via ``WorktreeManager.create(...)``. - - Returns: - An ``AgentResult`` with the spawn's outputs. The - ``commit_sha`` field SHOULD be populated when the - substrate can capture the producer's HEAD SHA - synchronously. ``None`` is allowed for substrates whose - spawn factory cannot do so without racing the producer - commit (e.g. k3s's fire-and-monitor adapter); those - substrates supply INV-6's ``ack_commit_sha`` through - their native attestation channel instead. - """ - ... diff --git a/orchestrator/substrate/worktree.py b/orchestrator/substrate/worktree.py deleted file mode 100644 index d40da46a70..0000000000 --- a/orchestrator/substrate/worktree.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Worktree-manager protocol for the substrate-swap spike (#2623). - -See ``docs/architecture/claude-code-substrate.md`` for the broader -substrate model. This module defines the ``WorktreeManager`` protocol -every substrate implementation must satisfy. - -HITL decision cq-5 pins the worktree model to **port egg's -``WORKTREE_BASE_DIR``** (``gateway/worktree_manager.py:49``): the -Claude Code substrate keeps the per-pipeline / per-role layout the -gateway already uses, so agents continue to run in isolated worktrees -without changing what egg's filesystem looks like. - -INTERFACE STABILITY: v0.x unstable. -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Protocol, runtime_checkable - -from egg_contracts.agent_roles import AgentRole - - -@runtime_checkable -class WorktreeManager(Protocol): - """Protocol every substrate worktree manager must satisfy.""" - - def create(self, pipeline_id: str, role: AgentRole) -> Path: - """Create a per-agent worktree. - - Args: - pipeline_id: Pipeline identifier (e.g. ``"issue-2623"``). - role: Agent role; used to name the worktree branch. - - Returns: - Absolute path to the worktree the spawner will run the - agent inside. - """ - ... - - def tear_down(self, pipeline_id: str) -> None: - """Remove all worktrees for the named pipeline. - - Implementations MUST use a path-escape guard equivalent to the - ``child.resolve().is_relative_to(base.resolve())`` defense in - ``gateway/worktree_manager.py:1711`` so a malicious pipeline - id cannot delete files outside the configured base. - """ - ... diff --git a/plugins/egg-sdlc/.claude-plugin/plugin.json b/plugins/egg-sdlc/.claude-plugin/plugin.json deleted file mode 100644 index 6890348406..0000000000 --- a/plugins/egg-sdlc/.claude-plugin/plugin.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "egg-sdlc", - "description": "Walking-skeleton substrate-swap (#2623) — runs the real egg_orchestrator stack natively in Claude Code with no k3s, no Redis, no Docker, no gateway sidecar. Spike scope: refiner role only. Plan / implement / pr phases are out of scope and deferred to the follow-up issue listed in the substrate ADR.", - "version": "0.1.0", - "author": { - "name": "egg maintainers", - "url": "https://github.com/jwbron/egg/issues" - }, - "homepage": "https://github.com/jwbron/egg/tree/main/plugins/egg-sdlc", - "repository": "https://github.com/jwbron/egg", - "license": "MIT", - "keywords": [ - "egg", - "sdlc", - "claude-code-substrate", - "walking-skeleton", - "refiner", - "in-process-orchestrator" - ], - "egg": { - "install_source": "from-source", - "install_instructions": "Clone the egg repo and install it from its pyproject.toml (no PyPI package is published yet). Example: `git clone https://github.com/jwbron/egg.git && cd egg && pip install . && export PYTHONPATH=\"$PWD:$PWD/shared:$PYTHONPATH\"`. The repo requires Python >=3.14 (see pyproject.toml). The follow-up issue listed in docs/architecture/claude-code-substrate.md tracks publishing a `pip install`-able package (cq-12 deferral).", - "substrate_scope": "refiner-only", - "substrate_selector_env_var": "EGG_SUBSTRATE", - "substrate_selector_value": "claude-code", - "follow_up_issue_doc": "docs/architecture/claude-code-substrate.md" - } -} diff --git a/plugins/egg-sdlc/skills/egg-sdlc/SKILL.md b/plugins/egg-sdlc/skills/egg-sdlc/SKILL.md deleted file mode 100644 index c996844fc6..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/SKILL.md +++ /dev/null @@ -1,329 +0,0 @@ ---- -name: egg-sdlc -description: "Run the full egg SDLC stack natively in Claude Code (substrate-swap rollout from #2623 → #2717). Target shape: boot the real `egg_orchestrator` in-process, dispatch role subagents via Claude Code's Agent tool, enforce role file-write restrictions via a PreToolUse hook, and render HITL decisions through `AskUserQuestion`. Refine-phase scope landed in slice 1 of the #2717 rollout (refiner + reviewer_refine + reviewer_agent_design); plan-phase scope landed in slice 2 (architect + task_planner + risk_analyst + reviewer_plan). Both phases are driven by the flattened `bin/run_pipeline.py` stage driver that ferries a single `pending_hitl` envelope through `.egg-state/contracts/.json` per skill→Python round-trip. Implement / pr phases land in later slices of the rollout." -disable-model-invocation: true -argument-hint: "[issue# | issue-url] [--repo owner/name]" -allowed-tools: Agent Read AskUserQuestion Bash(gh issue view:*) Bash(gh issue list:*) Bash(git -C * remote:*) Bash(git remote:*) Bash(mkdir:*) Bash(ls:*) Bash(test:*) Bash(find:*) Bash(python3 plugins/egg-sdlc/skills/egg-sdlc/bin/*:*) Bash(cat:*) Bash(cp:*) ---- - -# egg-sdlc — full egg SDLC stack inside Claude Code - -This skill is the **claude-code-substrate** entry point for the real `egg_orchestrator` stack — the user-facing entry point for the [substrate-swap ADR](../../../../docs/architecture/claude-code-substrate.md) seeded by the walking-skeleton spike [#2623](https://github.com/jwbron/egg/issues/2623) and being rolled out under [#2717](https://github.com/jwbron/egg/issues/2717). It is **not** a parallel Markdown approximation of egg's BRC like `plugins/refine-plan/`; it is the real orchestrator running in-process to the parent Claude Code session. - -> **Rollout status (slices 1 + 2 of #2717 landed).** The refine and plan phases now exercise their full role rosters on this substrate: -> -> - **Refine** — `refiner` + `reviewer_refine` + `reviewer_agent_design` (the third is spawned only when the target repo is `jwbron/egg`). _(Slice 1.)_ -> - **Plan** — `architect` (runs solo first) + `task_planner` and `risk_analyst` (run concurrently downstream of the architect) + `reviewer_plan` (ACKs / NACKs each of the three producer edges). The stage yields a plan-HITL decision after `CONSENSUS_CONFIRMED` lands on every producer edge — see "Plan phase" below. _(Slice 2.)_ -> -> The heredoc-HITL bridge gap that the original spike deferred is **closed for refine + plan** via the flattened `bin/run_pipeline.py` stage driver (see "How the flattened bridge works" below). The implement / pr phases — and their role rosters (`coder`, `tester`, `documenter`, `reviewer_code`, `reviewer_code_holistic`, `reviewer_contract`, `reviewer_security`, `reviewer_concurrency`) — land in later slices of the #2717 rollout (slice 3 = implement + daemon HITL bridge, slice 4 = pr + the rest of the conformance matrix, slice 5 = hardening). If you call this skill with anything beyond refine or plan today, expect `NotImplementedError` and a pointer to the next slice. - -## What this gets you - -- Real `egg_orchestrator` running in-process to your Claude Code session — no k3s, no Redis, no Docker, no gateway sidecar. -- Refine- and plan-team subagents run via Claude Code's `Agent` tool with `subagent_type: "general-purpose"` and a system prompt assembled by the real `build_system_prompt(sources)` (`shared/egg_harness/prompt.py:24`) — the structural depth fix from #2622. Each role (`refiner`, `reviewer_refine`, `reviewer_agent_design`, `architect`, `task_planner`, `risk_analyst`, `reviewer_plan`) picks up its rubric from `agents/.md` automatically. -- Role file-write restrictions are enforced at write time by a PreToolUse hook that imports `build_agent_patterns` from `shared/egg_restrictions/patterns.py:768` — the same source of truth the gateway uses for `403 restricted_path_modified`. -- HITL decisions surface through the parent session via `AskUserQuestion` and resume the orchestrator from where it paused — the flattened `bin/run_pipeline.py` stage driver round-trips each `HITLDecision` through `.egg-state/contracts/.json#pending_hitl` so the skill can drive a generator-yielding orchestrator from Bash steps without keeping a Python process alive across yields. -- Refine + plan artifacts land at the canonical egg paths: `.egg-state/drafts/-analysis.md` (refine) and `.egg-state/drafts/-plan.md` (plan); each role's handoff JSON / verdict JSON lands at `.egg-state/agent-outputs/--output.json`. These paths match the k3s substrate's writes. - -## Install - -The skill depends on the egg Python packages. **Until cq-12 resolves and publishes a pip-installable package, the install is from source.** The plugin metadata's `egg.install_instructions` field carries the same from-source command the preflight prints on import failure — both surfaces stay in sync via the same source of truth. - -```bash -git clone https://github.com/jwbron/egg.git -cd egg -pip install . -export PYTHONPATH="$PWD:$PWD/shared:$PYTHONPATH" -``` - -The skill's pre-flight check imports `orchestrator.substrate.in_process.run_pipeline_in_process`; if that import fails, the skill emits the same from-source instructions and exits — it does NOT try to recover silently. **The install-error message in the pre-flight helper reads from the same `plugin.json` field this section documents** so the two surfaces remain consistent (TASK-1-7 acceptance). The follow-up issue (see the substrate ADR) tracks publishing a `pip install`-able package; until then, the from-source path is the only supported install. - -**Python version.** Egg requires Python **3.14+** (per `pyproject.toml`'s `requires-python = ">=3.14"`). `pip install .` will refuse to install on older interpreters. If your Claude Code session resolves to an older Python, re-run the install command in a 3.14+ venv (e.g. `python3.14 -m venv .venv && source .venv/bin/activate && pip install .`). - -**Marketplace footprint** stays well under the soft ~100 MB cap (feedback Q3). No new third-party dependencies were introduced for this substrate beyond what egg already declares. - -## Trust-context note (read this before running) - -This skill **runs egg in-process to your Claude Code session**. The session holds the real Anthropic API key, and every subagent spawned by the orchestrator inherits the session's credential context. In the k3s substrate the sandbox NEVER sees the real key — the gateway injects credentials server-side. In this substrate that boundary moves. - -**Threat model**. The substrate is intended for **repos you already trust to commit on**. The threat is not "agent from a randomly-encountered issue"; it is your own SDLC pipeline. A subagent compromised via prompt injection (untrusted issue body, malicious PR content) can in principle read or exfiltrate the API key from environment / disk / network — the PreToolUse hook gates *the agent's own tool calls* but cannot prevent the agent from reading env vars. - -If you need the gateway-isolated credential boundary, **keep using the k3s substrate** — it remains supported indefinitely (cq-9). Set `EGG_SUBSTRATE=k3s` (the default) and run the orchestrator HTTP daemon as before. - -See the ADR's [Trust-context shift (R1)](../../../../docs/architecture/claude-code-substrate.md#trust-context-shift-r1) section for the full discussion of what changed, why it's accepted, and what's mitigated. - -## Usage - -```bash -/egg-sdlc 1234 # GitHub issue number (curated rollout target) -/egg-sdlc #1234 # same -/egg-sdlc 1234 --repo jwbron/egg -``` - -### What the skill does - -1. **Pre-flight check**. Imports `egg_orchestrator`. If the import fails, prints the install instruction (verbatim from the section above) and exits. _(`bin/preflight.py`.)_ -2. **Resolve repo + issue**. Picks up the repo from `--repo`, falls back to `git -C "$EGG_REPO_PATH" remote get-url origin`, falls back to cwd. Fetches the issue body once with `gh issue view `. -3. **Boot the in-process orchestrator** by invoking the flattened stage driver `bin/run_pipeline.py` for the first time with the pipeline id as a positional arg plus `--repo` / `--issue-number` flags. The driver imports `run_pipeline_in_process(...)` from `orchestrator/substrate/in_process.py`, advances a fresh generator to its first `HITLDecision` yield, serialises the decision into `.egg-state/contracts/.json#pending_hitl`, and exits 0. -4. **Render the decision**. The skill reads `pending_hitl.decision` and `pending_hitl.status` from the contract (the status branch goes through `bin/read_status.py --field status`; the decision itself is read with the `Read` tool against the contract path); when `status == "pending"` it surfaces the decision via `AskUserQuestion`. The operator's selected option is written back to `pending_hitl.answer` (and `status` is set to `answered`) by `bin/write_answer.py --answer-string "${ANSWER}"`, which JSON-encodes the operator's selection internally so shell quoting cannot mis-encode it — see "How the flattened bridge works" below. -5. **Resume the orchestrator**. The skill re-invokes `bin/run_pipeline.py` with the same args. The driver promotes `pending_hitl.answer` into `answer_log`, replays the full `answer_log` into a fresh generator (deterministic replay — see "Generator state across invocations" below), advances to the next yield (or to `StopIteration`), serialises the next decision, and exits. The skill loops back to step 4 until `pending_hitl.status ∈ {completed, aborted, error}`. -6. **Refine subagents run inside step 3 / 5.** The `ClaudeCodeSpawner` dispatches the three refine-team roles via the `Agent` tool with `subagent_type: "general-purpose"`. Each subagent runs inside a worktree under `///` (default base `~/.egg-worktrees/`), the refiner writes its analysis to `.egg-state/drafts/-analysis.md`, each reviewer writes its verdict to `.egg-state/agent-outputs/--output.json`. The orchestrator coordinates ACK / NACK / re-propose cycles via the in-process message bus before pausing at the refine HITL gate. -7. **Refine HITL gate**. The skill surfaces a refine-gate `HITLDecision` (approve / request changes / change approach / stop) alongside the refiner's recommended option, the top open questions, and each reviewer's ACK or NACK summary. -8. **Plan subagents run inside the next driver invocation.** When the operator chooses "approve and continue to plan" at the refine gate, the next `bin/run_pipeline.py` invocation enters the plan stage. The `ClaudeCodeSpawner` dispatches `architect` solo first; once its handoff lands, `task_planner` and `risk_analyst` are spawned concurrently. `reviewer_plan` is spawned for the ACK / NACK cycle after each `CONSENSUS_PROPOSE`; the in-process message bus runs the open-NACK barrier the same way the k3s substrate does. The plan document lands at `.egg-state/drafts/-plan.md`; each role's handoff or verdict lands at `.egg-state/agent-outputs/--output.json`. -9. **Plan HITL gate.** Once `CONSENSUS_CONFIRMED` fires on all three producer edges (`architect → reviewer_plan`, `task_planner → reviewer_plan`, `risk_analyst → reviewer_plan`), the stage yields a plan-gate `HITLDecision` (approve / request changes / change approach / stop) alongside the architect's approach summary, the task_planner's slice DAG, the risk_analyst's top-3 risks and blocking concerns, and each per-edge reviewer verdict. -10. **Phase fence.** If the operator chooses "approve and continue to implement", the skill currently raises `NotImplementedError` with a pointer to slice 3 of the #2717 rollout — implement / pr phases land in slices 3 / 4 of the rollout. - -### How the flattened bridge works - -The orchestrator's `run_pipeline_in_process(...)` is a Python generator that pauses at each HITL boundary by **yielding** an `HITLDecision`. A Claude Code skill cannot keep a single long-lived Python process alive across multiple `AskUserQuestion` round-trips — every `python3` invocation from a Bash skill step is a fresh process whose generator state dies at exit. Per cq-1 = hybrid (Option C), this skill picks the **flattened** option for refine and plan phases (the daemon variant lives in slice 3 for implement-phase concurrency): a hand-shaped sequence of `python3 bin/run_pipeline.py` invocations that thread decisions and answers through `.egg-state/contracts/.json#pending_hitl`. - -The single-yield carrier is the **`pending_hitl` envelope**. Its shape is the load-bearing state-serialization contract between this driver and the future daemon variant — the daemon-mode driver in slice 3 (TASK-3-2) consumes the same envelope shape, so reviewers can compare contract files across the two bridges 1:1. The driver's top-of-file comment at `plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:20-46` is the source of truth; this section mirrors it. - -```json -{ - "pending_hitl": { - "version": 1, - "pipeline_id": "issue-1234", - "timestamp": "", - "decision": { - "question": "...", - "options": [{"label": "...", "description": "..."}, ...], - "phase": "refine", - "...": "..." - }, - "answer": null, - "status": "pending", - "result": null, - "error": null, - "answer_log": [] - } -} -``` - -Field semantics: - -- `version` — schema version (currently `1`). Do not bump without coordinating with the slice-3 daemon variant; the field exists so a future schema bump can be detected by both bridges. -- `pipeline_id` — echoes `contract.pipeline_id` for sanity-checking. -- `timestamp` — ISO-8601 UTC of the last driver write. -- `decision` — the most recently yielded `HITLDecision`, serialised via `.model_dump(mode="json")` (pydantic) or `dict()` (fallback). `null` before the generator yields, and `null` again on `StopIteration`. -- `answer` — the operator's response to the current `decision`. The skill body writes this after rendering `AskUserQuestion`; the driver consumes it on its next invocation (promotes it into `answer_log` and clears `answer` back to `null`). -- `status` — **the skill's loop predicate**. One of: - - `pending` — `decision` is set and waiting for an answer. Render via `AskUserQuestion` and write the answer back. - - `answered` — the skill body wrote `answer` and the driver hasn't been re-invoked yet. (You'll only see this transiently, written by the skill body.) - - `completed` — the generator returned (StopIteration). `result` holds the return value (the refine analysis path for slice-1 runs; the plan-document path for slice-2 runs that walked through both phases). Skill loop exits cleanly. - - `aborted` — the operator chose an abort-style answer (`abort` / `stop` / `cancel`). Skill loop exits cleanly. - - `error` — the driver hit an internal error. `error` holds the diagnostic. Driver exited 1. -- `result` — generator return value when `status == "completed"`. For a refine-only run this is the analysis path; for a run that walked through both refine and plan, the value depends on how the operator answered the plan HITL gate (e.g. the plan document path on `approve`). -- `error` — diagnostic message when `status == "error"`. -- `answer_log` — the operator's accumulated answer history. The driver replays this list on every invocation (see "Generator state across invocations" below); the slice-3 daemon variant inherits this field unchanged. - -**The full 9-field envelope is a stable cross-bridge contract.** The slice-3 daemon variant in `orchestrator/substrate/claude_code/hitl_daemon.py` (TASK-3-2) consumes every field; do not drop or rename any field without bumping `version`. - -#### Generator state across invocations (replay semantics) - -Each `python3 bin/run_pipeline.py` invocation is a fresh process — generator frames cannot persist across processes. To resume at the right yield boundary across invocations, the driver **replays** the operator's answers from `answer_log` on every call: it spawns a fresh `run_pipeline_in_process(...)` generator, calls `next()` to land on the first yield, then loops `generator.send(replay)` over each historical answer to fast-forward to the next un-answered yield. This works because the generator is deterministic — the same `(pipeline_id, repo, issue_number, issue_body)` inputs combined with the same answer sequence reach the same yield boundary every time. - -Practical consequence: **side effects (refiner subagent dispatch, worktree create / teardown, artifact write) re-run on every invocation.** For the walking-skeleton refine + plan phases this is acceptable (each subagent's worktree is idempotent and the artifact write overwrites). The implement phase has too many concurrent yields for replay to be practical, which is why slice 3 ships the daemon variant for implement-phase concurrency instead. - -**Cost note.** Each re-spawn is a real Anthropic API call: tokens, plus 10–60 s of wall-clock per subagent. For slice 1's 2-yield refine phase this means the refiner (and both refine reviewers, when the target is `jwbron/egg`) spawn **twice** — once when the operator first sees the refine-gate, again when they answer it. Slice 2's plan phase (4 yields, per the ADR) compounds: stage B = 2 spawns, stage C = 4, stage D = 6, stage E = 8 — eight spawns just to reach the plan-gate's final yield. For a real `jwbron/egg` issue this is on the order of tens of dollars in Anthropic API spend per pipeline run before the slice-3 daemon variant lands and eliminates replay. If cost matters to your run, prefer the k3s substrate (no replay) until slice 3 lands; the cost cap (`EGG_PIPELINE_MAX_AGENT_INVOCATIONS`, slice 5) does not apply to this substrate until then. - -#### The skill loop - -Run the driver, read `status` and `decision`, render via `AskUserQuestion`, write the answer back to `pending_hitl.answer` (and bump `status` to `answered`), re-invoke the driver. Loop until `status ∈ {completed, aborted, error}`: - -```bash -ISSUE=1234 -REPO="jwbron/egg" -PIPELINE_ID="issue-${ISSUE}" -CONTRACT_PATH=".egg-state/contracts/${PIPELINE_ID}.json" - -# Iteration N — ask the orchestrator for the next decision (positional -# pipeline_id; --repo / --issue-number flags match the driver's argparse -# at plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py:355-402). -python3 plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py \ - "${PIPELINE_ID}" \ - --repo "${REPO}" \ - --issue-number "${ISSUE}" - -# Read status out of the contract via the read_status helper. Each -# subcommand in the loop is a single `python3 plugins/.../bin/.py` -# invocation, so the skill's `allowed-tools` pattern -# `Bash(python3 plugins/egg-sdlc/skills/egg-sdlc/bin/*:*)` fences the -# whole loop without needing a separate `Bash(python3 -c *)` rule (and -# without leaving a prompt-injection door open). -STATUS=$(python3 plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py \ - --pipeline-id "${PIPELINE_ID}" \ - --state-root "$(dirname "$(dirname "${CONTRACT_PATH}")")" \ - --field status) - -# Note: the `case` has no `*)` default arm by design. `read_status.py` -# prints an empty string + exits 0 when the contract has no -# `pending_hitl` envelope yet (i.e. the driver hasn't been run yet, or -# the envelope was hand-cleared); ``${STATUS}`` is then empty, no arm -# matches, the `case` exits 0, and the skill's outer iteration loops -# back to the next `python3 .../run_pipeline.py` invocation — which -# is the recover path (re-materialise the envelope). Don't add a `*)` -# arm that exits non-zero; the fall-through is intentional. -case "${STATUS}" in - pending) - # Read pending_hitl.decision via the Read tool against - # ${CONTRACT_PATH} and render via AskUserQuestion (an LLM-side - # tool — outside Bash). The skill body collects the operator's - # selection into shell variable ${ANSWER}. - # Then write the answer back to the envelope via write_answer.py. - # `--answer-string` takes the raw selection; the helper JSON-encodes - # it internally (so shell quoting cannot mis-encode `approve` into - # a Python NameError), uses datetime.now(UTC) — matching the - # driver's _now_iso() at run_pipeline.py:103 — and writes the - # contract atomically via tmp + os.replace (matching - # _write_contract at run_pipeline.py:154-162). Failure to ferry the - # answer exits non-zero so the skill loop notices. - python3 plugins/egg-sdlc/skills/egg-sdlc/bin/write_answer.py \ - --pipeline-id "${PIPELINE_ID}" \ - --state-root "$(dirname "$(dirname "${CONTRACT_PATH}")")" \ - --answer-string "${ANSWER}" - # Loop: re-invoke run_pipeline.py with the same args. The driver - # promotes pending_hitl.answer → answer_log, clears answer to null, - # replays the full answer_log into a fresh generator, and writes - # the next pending_hitl.decision. - ;; - completed|aborted) - # Read pending_hitl.result for the artifact path (completed) or the - # abort diagnostic (aborted) via - # `python3 plugins/.../bin/read_status.py --field result`. Skill - # exits cleanly. - ;; - error) - # Read pending_hitl.error for the diagnostic via - # `python3 plugins/.../bin/read_status.py --field error`. Driver - # exited 1. - ;; -esac -``` - -The skill body's `allowed-tools` frontmatter scopes `python3` to `plugins/egg-sdlc/skills/egg-sdlc/bin/*` so the skill cannot be coerced (via a prompt-injected issue body, say) into running arbitrary `python3 -c "..."` snippets. The four helpers under `bin/` — `preflight.py`, `run_pipeline.py`, `read_status.py`, `write_answer.py` — are the entire Python surface the skill can invoke; all four ship in this PR and are read-reviewable next to `SKILL.md`. Every subcommand in the documented loop body is a single `python3 plugins/.../bin/.py` invocation (no `python3 -c` snippets, no `printf` pipes), so each subcommand matches the allowed-tools pattern independently per [Claude Code's compound-command permission rules](https://code.claude.com/docs/en/permissions#compound-commands). No separate `Write` permission is needed — `write_answer.py` is the only path that writes `pending_hitl.answer`, and `--answer-string` JSON-encodes the operator's selection internally so shell quoting cannot mis-encode it. - -While the generator is paused at a yield boundary inside a single `bin/run_pipeline.py` invocation, the orchestrator's background threads (heartbeat poll, BRC re-review, message-bus tick) keep running so a long-paused HITL does not cause stuck-phase-transition alerts within that invocation. Dropping the generator (process exit) joins the background threads cleanly via `GeneratorExit` — no leaked threads across the skill→Python boundary. - -### Worktree layout - -Per cq-5 the substrate ports egg's `WORKTREE_BASE_DIR` model. There are two filesystem trees: **worktrees** (per-agent git checkouts) and **state** (drafts, contracts, agent-outputs, checkpoints). They live under separate roots by default. - -**Worktrees** default to `~/.egg-worktrees///` (matching the shape at `gateway/worktree_manager.py:49`, which hardcodes `/home/egg/.egg-worktrees` for the gateway container — the substrate's `LocalWorktreeManager` expands `~` against the calling user's `$HOME`). Each per-role worktree gets its own `git worktree` checked out on `egg//`. `EGG_WORKTREE_BASE` overrides the root; the typical override is to point it at `./.egg-state/` so worktrees and state live in one tree. - -``` -# Default layout (no EGG_WORKTREE_BASE override) -~/.egg-worktrees/ - / - / # per-role worktree on branch egg// - -.egg-state/ # state files (relative to the in-process orchestrator's CWD) - drafts/ - -analysis.md - contracts/ - .json - agent-outputs/ - --output.json - brc-history/ - -.{md,json} - checkpoints/ - / # per-pipeline checkpoint shard - ... -``` - -``` -# Typical override: EGG_WORKTREE_BASE=./.egg-state/ -.egg-state/ - / - / # per-role worktrees moved alongside state - drafts/ - contracts/ - agent-outputs/ - brc-history/ - checkpoints/ - / -``` - -Path-escape safety mirrors the existing `is_relative_to` + `resolve()` defense in the gateway (`gateway/worktree_manager.py:1700-1711`, within `list_orphan_worktree_dirs`) so a malicious pipeline ID can't escape the base. - -### PreToolUse hook (file-write restrictions) - -The substrate ships a Python hook entry script at `orchestrator/substrate/claude_code/hook_entry.py` and a `.claude/settings.json` template at `orchestrator/substrate/claude_code/settings.template.json`. You activate the hook by **copying** the template into your own `.claude/settings.json` — it is **not** silently activated by the plugin install. - -What the hook does: - -1. Reads tool name + tool input from stdin (per the Claude Code PreToolUse contract). -2. Imports `build_agent_patterns` from `shared/egg_restrictions/patterns.py:768`. -3. Emits `deny` + `message` JSON to stdout when the write target is outside the caller's role's allow-list. The `message` mirrors the gateway's `check_agent_restrictions` denial format (`gateway/phase_filter.py:1061`) so the error you see in the Claude Code UI matches what k3s users see in their gateway logs. - -The hook reads the calling role from `EGG_AGENT_ROLE` in the env. **R2 — nested-dispatch role-routing**: slice 1 of #2717 lands a 2-subagent worked example at `integration_tests/regression/test_pretooluse_hook_nested.py` (TASK-1-5) that drives the hook through a parent → child Agent-tool dispatch via the test-only fake at `integration_tests/regression/_agent_tool_fake.py` (TASK-1-9). The verdict is recorded to `.egg-state//r2-verdict.json`. Note that the production substrate runs subagents through the harness re-host (`ClaudeCodeSpawner` per cq-3) rather than Agent-tool dispatch, so R2 today validates hook *logic* (given accurate `EGG_AGENT_ROLE` propagation) and becomes load-bearing only if cq-3 flips to Agent-tool dispatch in a future issue. If the verdict is `fail`, slice 5 wires the documented fallback — **MCP-validator-side enforcement** (cq-6 option 2) — the substrate keeps `patterns.py` as the source of truth and adds agent-side policy enforcement at `sandbox/egg_agent_tools/handlers/restrictions.py`. - -> **Open question for slice-5 sequencing.** The slice-1 R2 verdict file (`r2-verdict.json`) records *only* the hook-logic half of R2 — it is **not** a green-light for the R15 model-(b) migration on its own. Before slice 5 reads the verdict as "ship Agent-tool dispatch," an empirical Claude-Code-side test must land that exercises real nested Agent-tool dispatch and observes `EGG_AGENT_ROLE` propagation in the child. Slice 5's R15 task should treat the verdict file as a necessary-but-not-sufficient input. Tracked in the slice-5 plan; this caveat is duplicated in `integration_tests/regression/test_pretooluse_hook_nested.py`'s module docstring so a reader of either surface sees the same constraint. - -### Plan phase (landed in slice 2 of #2717) - -The plan stage runs **three producers reviewed by one reviewer**: - -| Role | When it spawns | Output | -|---|---|---| -| `architect` | First, solo | `.egg-state/agent-outputs/-architect-output.json` — approach summary + key design decisions + ordering constraints | -| `task_planner` | Concurrently with `risk_analyst`, downstream of the architect | `.egg-state/drafts/-plan.md` + `.egg-state/agent-outputs/-task_planner-output.json` — slice DAG with role-typed tasks | -| `risk_analyst` | Concurrently with `task_planner` | `.egg-state/agent-outputs/-risk_analyst-output.json` — risks with evidence, top-3, blocking concerns | -| `reviewer_plan` | After each producer's `CONSENSUS_PROPOSE` | `.egg-state/agent-outputs/-reviewer_plan-output.json` — ACK / NACK per producer edge | - -Each role's rubric lives at `agents/.md` and is prepended to the per-task prompt by `build_system_prompt(sources)`. The plan stage advances through three BRC edges (`architect → reviewer_plan`, `task_planner → reviewer_plan`, `risk_analyst → reviewer_plan`); the orchestrator's open-NACK barrier applies per edge. - -**Plan HITL gate.** Once `CONSENSUS_CONFIRMED` fires on all three producer edges, the stage yields a plan-gate `HITLDecision` with these standard options: - -- `approve_continue` — would advance to the implement phase. Currently fenced: the generator raises `NotImplementedError` with a pointer to slice 3 of the #2717 rollout (the implement phase ships in slice 3, pr ships in slice 4). -- `request_changes` — **not implemented in slice 2.** The option is surfaced for forward-compatibility, but the slice-2 generator treats every non-`approve_continue` answer as "stop and return the plan artifact path"; the producer / reviewer re-spawn loop lands in a later slice of the rollout (tracked in the [`#2717` plan](https://github.com/jwbron/egg/issues/2717)). -- `change_approach` — **not implemented in slice 2.** Same caveat as `request_changes`: surfaced but treated as stop. Kicking the pipeline back to the refine phase for a fresh refiner cycle lands in a later slice. -- `stop` — terminate the run cleanly; the skill loop exits with `pending_hitl.status = completed` and `result` pointing at the plan artifact path. - -If the plan-phase BRC did **not** reach `CONSENSUS_CONFIRMED` (`_run_plan_phase` returned `is_complete=False`), the gate surfaces a different option set instead: - -- `retry` — would re-run the failed producers; **not implemented in slice 2** (same forward-compatibility caveat as `request_changes` above — treated as stop today). -- `abort` — terminate the run cleanly with the partial plan artifact path as the return value. - -The skill surfaces the architect's `approach_summary`, the task_planner's slice DAG shape, the risk_analyst's top-3 risks + blocking concerns, and each per-edge `reviewer_plan` verdict alongside the decision so the operator decides with the full plan-team context in view. - -### What's NOT in this skill (yet) - -A non-exhaustive list of capabilities that the substrate-swap rollout targets but slices 1 + 2 of #2717 have not landed: - -- **Implement / pr phases.** Slice 3 lands the implement-phase substrate (3 producers + 5 reviewers) + daemon HITL bridge; slice 4 lands the pr-phase substrate + the rest of the conformance matrix. If you advance past the plan HITL gate today, the skill raises `NotImplementedError` with a pointer to the active slice. -- **Cost cap (`EGG_PIPELINE_MAX_AGENT_INVOCATIONS`).** Recommended in the ADR (REC5); lands in slice 5 of the #2717 rollout. -- **Custom `subagent_type` per-role agent definitions in `.claude/agents/.md` (R15 model (b)).** The skill uses `subagent_type: "general-purpose"` for now; per-role tool restrictions rely on the PreToolUse hook + prompt discipline. Migration is **contingent on the R2 verdict** (TASK-1-5): if the hook reliably resolves role under nested dispatch, slice 5 stays on model (a); if not, slice 5 migrates every role rubric to a real `.claude/agents/.md` definition and adds agent-side policy enforcement (cq-6 option 2). The R2 verdict file at `.egg-state//r2-verdict.json` records the empirical result. -- **`EggHarnessSpawner` for headless / CLI mode (feedback Q4).** Lands in slice 5. -- **`egg-state prune` verb for local checkpoint cleanup (feedback Q6).** Reserved for the follow-up issue beyond #2717. -- **Fork-based sub-task delegation (cq-10's deferred half).** Lands in slice 5. - -For each of these, see the [Substrate-swap ADR](../../../../docs/architecture/claude-code-substrate.md) and the [`#2717` plan](https://github.com/jwbron/egg/issues/2717) for the slice-DAG breakdown. - -## Compatibility with the k3s substrate - -`EGG_SUBSTRATE` selects the substrate at orchestrator boot: - -| `EGG_SUBSTRATE` | What you get | -|---|---| -| unset, `""`, or `"k3s"` | The k3s pipeline as before (`KubernetesSpawner` + `RedisMessageStore` + gateway sidecar). The HTTP daemon entry (`orchestrator/cli.py:83 cmd_serve`) remains the boot path | -| `"claude-code"` | This skill's in-process model (`ClaudeCodeSpawner` + `InProcessMessageBus` + `PreToolUseHookPolicy` + `LocalWorktreeManager`). `run_pipeline_in_process(...)` is the boot path | - -The contract schema (`shared/egg_contracts/models.py::Contract` v1.1), BRC history (`.egg-state/brc-history/-.json`), agent outputs (`.egg-state/agent-outputs/`), and drafts (`.egg-state/drafts/-analysis.md`) are all filesystem-native and substrate-portable. A pipeline started on one substrate can have its contract read by the other. - -## Failure modes and diagnostics - -- **`ImportError: No module named 'egg_orchestrator'`**: the pre-flight check failed. Re-run the pip install command above. -- **`NotImplementedError: egg-sdlc #2717 slice-2: implement / pr phases are deferred to slice-3 / slice-4. See the rollout DAG in docs/architecture/claude-code-substrate.md.`**: you answered `approve_continue` at the plan HITL gate. Slices 1 + 2 of the #2717 rollout cover refine + plan; implement / pr land in slices 3 / 4 of the same rollout. -- **`NotImplementedError: EGG_SUBSTRATE=k3s requires the HTTP daemon`** (raised from `run_pipeline_in_process`): you set `EGG_SUBSTRATE=k3s` while running this in-process skill. k3s users use `orchestrator/cli.py:83 cmd_serve`, not the skill. -- **PreToolUse hook denies a write the role *should* be allowed**: the `settings.template.json` is wired against a stale or wrong `EGG_AGENT_ROLE`. The hook prints which role it saw — re-check the spawn env. -- **HITL takes a long time and you see no progress**: the orchestrator's background threads keep running inside each `bin/run_pipeline.py` invocation while the generator is paused on a yield; the heartbeat-during-HITL acceptance criterion guarantees this within an invocation. Between invocations (i.e. while the skill is rendering `AskUserQuestion` and waiting on the operator), the Python process has exited and the orchestrator state lives only in `.egg-state/contracts/.json#pending_hitl`. If you genuinely want to abandon the run, close the session; the next invocation of `bin/run_pipeline.py` will resume from the contract file, or you can delete the contract file to discard the run entirely. - -- **`pending_hitl.status` is `completed`, `aborted`, or `error`** after a `bin/run_pipeline.py` invocation: the loop is done. `completed` → read `pending_hitl.result` for the artifact path. `aborted` → the operator chose an abort-style answer; `pending_hitl.result` holds the abort diagnostic. `error` → read `pending_hitl.error` for the driver's diagnostic string; the driver exited 1. The skill loop should exit in all three cases, not call `bin/run_pipeline.py` again. - -## Where this fits - -- [Substrate-swap ADR](../../../../docs/architecture/claude-code-substrate.md) — the canonical reference for the four interfaces, the implementations, the eleven cq decisions, and the deferred-vs-landed status of each rollout item. -- [`plugins/refine-plan/`](../../../refine-plan/) — the earlier Markdown-only approximation of egg's refine + plan phases. This skill **supersedes** that for solo-developer use of the real orchestrator; `refine-plan` remains as a portable Python-deps-free alternative. -- [Concurrent execution guide](../../../../docs/guides/concurrent-execution.md) — the BRC protocol the substrate preserves. -- [Integration-test trust boundary](../../../../docs/architecture/integration-test-trust-boundary.md) — names "in-parent-Claude-Code-session" as a new trust context (R1). diff --git a/plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md b/plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md deleted file mode 100644 index bcbe297991..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/agents/architect.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -# Role data file. NOT a Claude Code subagent definition — the in-process -# orchestrator's `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) -# reads this file's markdown body and prepends it to the per-task prompt before -# dispatching the architect via the Agent tool with subagent_type: -# "general-purpose". The frontmatter is informational only. -# -# Layout mirrors plugins/refine-plan/skills/refine-plan/agents/architect.md so -# the in-process orchestrator can read it without per-skill custom logic. The -# body is what the agent sees as its role rubric. -name: architect -description: Recommends a high-level implementation approach based on the refine analysis. First producer in the plan phase; runs solo before task_planner and risk_analyst. Plan-team role landed by slice 2 of the #2717 rollout on the claude-code substrate. ---- - -# Architect — egg-sdlc Claude Code substrate - -You are the **architect** running on the **Claude Code substrate** of egg's SDLC pipeline. You execute the same plan-phase rubric as the k3s-substrate architect — the substrate swap is structurally invisible to your role. Your output schema, your evidence discipline, and your handoff format are unchanged. - -What IS different (so you can adjust your tool usage accordingly): you are a Claude Code subagent dispatched by the in-process orchestrator's `ClaudeCodeSpawner`, not a k8s Job pod. You inherit your parent Claude Code session's tool surface and credential context. Read [the substrate ADR](../../../../docs/architecture/claude-code-substrate.md) once if you want the full picture; it is not required reading to do your job. - -## What you do - -Recommend a high-level implementation **approach** for the work described in the refine analysis. You run first, solo, before `task_planner` and `risk_analyst` (which fan out in parallel based on your output). You do **not** write the task breakdown (that's `task_planner`) or the risk register (that's `risk_analyst`). Your job is the architectural shape: key design decisions, components touched, ordering constraints, alternatives rejected. - -## Inputs - -The Task context will provide absolute paths via env vars / arguments: - -- `analysis_path` — the refine-phase analysis document (scope source of truth) -- `repo` — owner/name of the target repo -- `architect_output_path` — where to write your handoff JSON - -## Output - -Write a single JSON file to `architect_output_path`: - -```json -{ - "approach_summary": "2-3 sentence high-level approach", - "key_design_decisions": [ - { - "decision": "What is being decided", - "rationale": "Why this over alternatives, grounded in the analysis constraints", - "alternatives_rejected": ["alt name — one-line reason"] - } - ], - "components_touched": ["gateway/", "orchestrator/routes/", "shared/egg_contracts/", "..."], - "ordering_constraints": [ - "X must land before Y because " - ], - "open_questions_for_planner": [ - "Specific questions task_planner / risk_analyst need answered to do their jobs" - ] -} -``` - -## Process - -1. Read the analysis at `analysis_path` in full. The analysis's Recommended Approach is your starting point — your job is to translate it into an architectural shape. -2. Research the components named in the analysis. Cite files in `components_touched`. -3. Name the key design decisions (typically 3–7). For each, briefly state alternatives rejected — this gives the reviewer something to challenge. -4. Identify ordering constraints — what has to land first, why? This shapes the slice-DAG that `task_planner` will produce. - -## What you do not do - -- Do not enumerate phases, slices, or `TASK-N-M` task IDs -- Do not write a Risk Assessment table — that's `risk_analyst` -- Do not write or modify any production code or tests -- Do not deviate from the Recommended Approach in the analysis without surfacing the divergence as an `open_question_for_planner` - -## On revision - -If `prior_nacks` is provided (`reviewer_plan` NACKed an earlier plan cycle citing architectural problems), revisit the design decisions named in those NACKs. Address them concretely or escalate as open questions. - -## Report back - -3-bullet summary: (1) approach in one sentence, (2) the most consequential design decision, (3) the riskiest ordering constraint. - -## Substrate-specific notes (read these once, then forget them) - -These are the only operational differences between this architect and the k3s-substrate architect. None of them changes WHAT you produce — they affect HOW you operate. - -- **Your worktree** lives at `//architect/` on the user's local filesystem (per cq-5), not in a k8s persistent volume — one worktree per role under each pipeline, on branch `egg//architect`. `` defaults to `~/.egg-worktrees/`; operators commonly override it to `./.egg-state/` so worktrees and state files live in one tree. `git` operations behave normally; `git rev-parse HEAD` after you commit captures your `commit_sha` for the orchestrator's `AgentResult`. -- **File-write restrictions** are enforced by a PreToolUse hook (per cq-6) calling the same `shared/egg_restrictions/patterns.py:768 build_agent_patterns` the gateway uses. The architect's allow-list mirrors the k3s gateway: `.egg-state/drafts/` and `.egg-state/agent-outputs/`. Writes outside that allow-list are denied at the hook layer with the same message format the gateway emits at push time. -- **HITL surfaces through `AskUserQuestion`** in the parent Claude Code session (per cq-7). You do not call `AskUserQuestion` yourself; you write your handoff JSON and the operator sees the plan-HITL gate after the full plan-team roster has reached `CONSENSUS_CONFIRMED` (you + `task_planner` + `risk_analyst` reviewed by `reviewer_plan`). -- **Concurrent peers in this slice.** Slice 2 of the #2717 rollout adds `architect`, `task_planner`, and `risk_analyst` as plan-phase producers plus `reviewer_plan` as the critical reviewer. You run first, solo; `task_planner` and `risk_analyst` are spawned in parallel after your handoff lands. Implement-team and pr-team roles land in later slices. -- **Output path stability**: the orchestrator writes your handoff to `.egg-state/agent-outputs/-architect-output.json` — same filesystem-native path as the k3s substrate. diff --git a/plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md b/plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md deleted file mode 100644 index fd7bdd1171..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/agents/refiner.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -# Role data file. NOT a Claude Code subagent definition — the in-process -# orchestrator's `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) -# reads this file's markdown body and prepends it to the per-task prompt before -# dispatching the refiner via the Agent tool with subagent_type: "general-purpose". -# The frontmatter is informational only. -# -# Layout mirrors plugins/refine-plan/skills/refine-plan/agents/refiner.md so the -# in-process orchestrator can read it without per-skill custom logic. The body -# is what the agent sees as its role rubric. -name: refiner -description: Researches the codebase and produces a structured requirements analysis. Producer role in the refine phase. Walking-skeleton scope under #2623 — the only role this substrate exercises. ---- - -# Refiner (egg-sdlc — Claude Code substrate) - -You are the **refiner** running on the **Claude Code substrate** of egg's SDLC pipeline. You execute the same refine-phase rubric as the k3s-substrate refiner — the substrate swap is structurally invisible to your role. Your output schema, your evidence discipline, and your handoff format are unchanged. - -What IS different (so you can adjust your tool usage accordingly): you are a Claude Code subagent dispatched by the in-process orchestrator's `ClaudeCodeSpawner`, not a k8s Job pod. You inherit your parent Claude Code session's tool surface and credential context. Read [the substrate ADR](../../../../docs/architecture/claude-code-substrate.md) once if you want the full picture; it is not required reading to do your job. - -## Walking-skeleton scope (#2623) - -This refiner is the **only role exercised by the spike**. The k3s substrate runs additional refine-phase roles (`reviewer_refine`, and `reviewer_agent_design` for the egg repo) — those reviewers do NOT run on this substrate yet. Your output goes straight to the refine HITL gate; the operator's approval (or request-for-changes) is the only review feedback you receive in this spike. - -This means **your evidence discipline matters more, not less**, than on the k3s substrate. There is no concurrent reviewer to catch a thin Open Questions section or a weakly-justified Recommended Approach. Write the analysis as if a single reviewer with no agenda is reading it cold — because in this spike that is exactly what happens at the HITL gate. - -The follow-up issue (named in the ADR) extends the substrate to the full refine-team roster and the plan / implement / pr phases. - -## What you do - -Analyze the task brief, research the relevant code, evaluate approaches, and produce a structured analysis document. You **do not** produce an implementation plan — that is the plan phase's job. Stay focused on understanding the problem, surfacing options, and naming questions for the human to answer. - -## Outputs - -The orchestrator will provide absolute paths via the Task context. You must write both: - -### 1. Analysis document — markdown - -Structure (mirrors `docs/templates/analysis.md`): - -```markdown -# Analysis: - -> Issue: #<n> | Phase: refine (or `Task: <id> | Phase: refine` if no issue) - -## Problem Statement -What is broken or missing? What is the desired outcome? - -## Current Behavior -How the relevant code works today, with `file:line` citations. - -## Constraints -- Technical (compatibility, performance, security) -- Business (timeline, scope) -- Dependency (other systems / features) - -## Options Considered - -### Option A: <Name> -**Approach**: ... -**Pros**: ... -**Cons**: ... - -### Option B: <Name> -(at least two options; meaningfully different) - -## Recommended Approach -Which option, and why. Reference one of the listed options. - -## Open Questions -Each question must be specific enough for a human to answer in one decision. -``` - -### 2. Handoff JSON - -```json -{ - "analysis_path": "<absolute path>", - "recommended_option": "<name of recommended option>", - "files_researched": ["path/to/file.py:42-58", "..."], - "options_considered": [{"name": "...", "summary": "one line"}], - "open_questions": ["one-line summary of each question"], - "external_research_done": true -} -``` - -## Process - -1. If the target repo is egg, start with `docs/index.md` for navigation. -2. Research the codebase — open files, read functions, follow references — *before* drafting. -3. For third-party libraries / APIs / integrations, use WebSearch and WebFetch. -4. Identify at least two meaningfully different options (not three flavors of the same idea). -5. Recommend one option with explicit justification grounded in the constraints. -6. Surface every uncertainty as an Open Question — do not self-limit. - -## What you do not do - -- Do not write implementation phases, slices, or task breakdowns. -- Do not modify source code, tests, or docs in this phase. -- Do not propose changes you have not verified are necessary by reading the relevant code. - -## Substrate-specific notes (read these once, then forget them) - -These are the only operational differences between this refiner and the k3s-substrate refiner. None of them changes WHAT you produce — they affect HOW you operate. - -- **Your worktree** lives at `<EGG_WORKTREE_BASE>/<pipeline_id>/refiner/` on the user's local filesystem (per cq-5), not in a k8s persistent volume — one worktree per role under each pipeline, on branch `egg/<pipeline_id>/refiner`. `<EGG_WORKTREE_BASE>` defaults to `~/.egg-worktrees/` (matching the gateway shape at `gateway/worktree_manager.py:49`); operators commonly override it to `./.egg-state/` so worktrees and state files live in one tree. `git` operations behave normally; `git rev-parse HEAD` after you commit captures your `commit_sha` for the orchestrator's `AgentResult` (per INV-6). -- **File-write restrictions** are enforced by a PreToolUse hook (per cq-6) calling the same `shared/egg_restrictions/patterns.py:768 build_agent_patterns` the gateway uses. The refiner's allow-list is exactly the two paths in `REFINER_PATTERNS.allowed_patterns` (`shared/egg_restrictions/patterns.py:491-494`): **`.egg-state/drafts/`** and **`.egg-state/agent-outputs/`**. Any `Write` or `Edit` outside that allow-list is denied — same paths the k3s substrate's gateway would reject at push time, just enforced earlier. The error message format mirrors the gateway's `check_agent_restrictions` denial. (`docs/templates/analysis.md` referenced above is **read-only** for the refiner — you can `Read` the template; you cannot `Write` or `Edit` there.) -- **Context budget**. Per cq-10's hybrid (checkpoint half landed; fork half deferred), if you anticipate hitting context limits on a deep refine, write intermediate findings to a checkpoint file under `.egg-state/checkpoints/<pipeline_id>/` and the orchestrator can re-invoke you with the checkpoint summary on the next cycle. **The fork primitive is NOT available in this spike** — do not assume you can spawn a sub-subagent for a sub-task. If checkpoints prove inadequate, accept a smaller-than-1000 turn budget and lean harder on the role rubric. -- **HITL surfaces through `AskUserQuestion`** in the parent Claude Code session (per cq-7 heredoc-HITL). You do not call `AskUserQuestion` directly; you write your Open Questions into the analysis, and the operator answers them at the refine HITL gate. -- **No agent-side reviewer dialog**. The k3s substrate spawns `reviewer_refine` in parallel with you; this substrate does not (yet). The operator at the HITL gate is your reviewer. - -## On revision - -If the Task context includes `prior_nacks`, treat each NACK as a blocking issue to address. Verify the reviewer's `artifact_references` and either fix the underlying problem or — if you believe the NACK is wrong — explain why in the analysis under a `## Open Questions` entry (do not silently ignore it). On this substrate the most likely source of `prior_nacks` is operator request-for-changes feedback from a prior HITL cycle; treat it the same way you would treat a reviewer NACK. - -## Report back - -After writing both files, return a 3-bullet summary: (1) recommended option, (2) the most important open question, (3) the most surprising thing you learned from the codebase. diff --git a/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md b/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md deleted file mode 100644 index 9104d6ec07..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_agent_design.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -# Role data file. NOT a Claude Code subagent definition — the in-process -# orchestrator's `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) -# reads this file's markdown body and prepends it to the per-task prompt before -# dispatching reviewer_agent_design via the Agent tool with subagent_type: -# "general-purpose". The frontmatter is informational only. -# -# Layout mirrors plugins/refine-plan/skills/refine-plan/agents/reviewer-agent-design.md -# so the in-process orchestrator can read it without per-skill custom logic. -# The body is what the agent sees as its role rubric. -name: reviewer_agent_design -description: Reviews refine-phase analysis for agent-mode design alignment and anti-patterns. Spawned only when the target repo is egg itself (jwbron/egg). Reviewer in the refine phase; runs concurrently with the refiner on the claude-code substrate per slice 1 of the #2717 rollout. ---- - -# Reviewer (agent design) — egg-sdlc Claude Code substrate - -You are the **reviewer_agent_design** running on the **Claude Code substrate** of egg's SDLC pipeline. You are spawned **only** when the target repo is egg itself (`jwbron/egg`). You execute the same agent-design-alignment review rubric as the k3s-substrate `reviewer_agent_design` — the substrate swap is structurally invisible to your role. Your four review criteria, your evidence discipline, and your verdict JSON shape are unchanged. - -What IS different (so you can adjust your tool usage accordingly): you are a Claude Code subagent dispatched by the in-process orchestrator's `ClaudeCodeSpawner`, not a k8s Job pod. You inherit your parent Claude Code session's tool surface and credential context. Read [the substrate ADR](../../../../docs/architecture/claude-code-substrate.md) once if you want the full picture; it is not required reading to do your job. - -## Read first - -Ground your verdict in egg's design principles: - -- `docs/guides/agent-mode-design.md` -- `docs/architecture/sdlc-pipeline.md` -- `docs/design/capability-removal.md` - -## What you do - -Open the refiner's analysis document at the path supplied in the Task context, evaluate it against the four rubric criteria below, and emit a single verdict JSON object. Open the egg-canonical docs above (or the analysis-cited source files) and **cite at least one of them** in your `artifact_references` — your NACK is only as good as the doc you ground it in. - -## Rubric - -Use these exact keys in `analysis`: - -1. **structural_enforcement** — Does the analysis lean on structural / infrastructure enforcement (file permissions, gateway filters, role boundaries, container isolation) rather than prompt-based rules? Flag any "we'll tell the agent to be careful about X" patterns that should instead be fenced at the infrastructure layer. -2. **role_alignment** — If the analysis names roles, phases, or pipeline concepts, do they match egg's actual taxonomy (refiner, architect, task_planner, risk_analyst, coder, tester, documenter, reviewers)? Does it respect the slice-DAG implement model (forest of independently-implementable slices in waves), or does it treat phases as "N sequential PRs"? -3. **anti_patterns** — Flag: bundled cleanup, speculative scope expansion, agent-trust where structural constraints would work, "agent will validate" where a gateway filter could enforce, recommendations that re-invent parallel mechanisms instead of using existing infrastructure. -4. **prior_art_referenced** — Does the analysis reference relevant existing egg infrastructure (gateway endpoints, MCP tools, agent roles, BRC, slice scheduler) instead of proposing parallel mechanisms? - -## Verdict rules - -- **ACK** only if every criterion passes. Non-blocking polish in `suggestions`. -- **NACK** if any criterion fails. `feedback` must name the anti-pattern concretely and point at the egg infrastructure that should be used instead. -- `artifact_references` **must be non-empty**. Each entry must be a file or doc you actually opened (either in the analysis under review, in egg's docs, or in egg's source). **At least one reference should be an egg-canonical doc** (the structural-enforcement / slice-DAG / agent-roles references above). - -## Verdict JSON shape - -Final response = one JSON object, no surrounding prose. Also written to `verdict_path`: - -```json -{ - "verdict": "ACK" | "NACK", - "summary": "...", - "analysis": { - "structural_enforcement": "...", - "role_alignment": "...", - "anti_patterns": "...", - "prior_art_referenced": "..." - }, - "suggestions": ["..."], - "artifact_references": ["docs/guides/agent-mode-design.md:#…", "..."], - "feedback": "concrete revision instructions (empty on ACK)", - "timestamp": "<ISO-8601 UTC>" -} -``` - -## On revision cycles - -If `prior_nacks` is provided, verify each prior cycle's agent-design NACK is now resolved. Unresolved prior NACKs → NACK again with the same artifact references plus "unresolved from cycle N". - -## Substrate-specific notes (read these once, then forget them) - -These are the only operational differences between this reviewer and the k3s-substrate `reviewer_agent_design`. None of them changes WHAT you produce — they affect HOW you operate. - -- **Your worktree** lives at `<EGG_WORKTREE_BASE>/<pipeline_id>/reviewer_agent_design/` on the user's local filesystem (per cq-5), not in a k8s persistent volume — one worktree per role under each pipeline, on branch `egg/<pipeline_id>/reviewer_agent_design`. `<EGG_WORKTREE_BASE>` defaults to `~/.egg-worktrees/`; operators commonly override it to `./.egg-state/` so worktrees and state files live in one tree. -- **File-write restrictions** are enforced by a PreToolUse hook (per cq-6) calling the same `shared/egg_restrictions/patterns.py:768 build_agent_patterns` the gateway uses. The reviewer's allow-list mirrors the k3s gateway. Writes outside that allow-list are denied at the hook layer with the same message format the gateway emits at push time. -- **HITL surfaces through `AskUserQuestion`** in the parent Claude Code session (per cq-7). You do not call `AskUserQuestion` yourself; you write the verdict JSON and the operator sees your ACK / NACK at the refine HITL gate alongside the refiner's analysis and the `reviewer_refine` verdict. -- **Spawn scope**. You are spawned only when the target repo is `jwbron/egg`. The orchestrator filters the refine-team roster against repo identity before dispatching. -- **Verdict path stability**: the orchestrator writes your verdict to `.egg-state/agent-outputs/<issue>-reviewer_agent_design-output.json` — same filesystem-native path as the k3s substrate. diff --git a/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md b/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md deleted file mode 100644 index 1f8657404f..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_plan.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -# Role data file. NOT a Claude Code subagent definition — the in-process -# orchestrator's `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) -# reads this file's markdown body and prepends it to the per-task prompt before -# dispatching reviewer_plan via the Agent tool with subagent_type: -# "general-purpose". The frontmatter is informational only. -# -# Layout mirrors plugins/refine-plan/skills/refine-plan/agents/reviewer-plan.md -# so the in-process orchestrator can read it without per-skill custom logic. -# The body is what the agent sees as its role rubric. -name: reviewer_plan -description: Reviews the plan document, YAML appendix, and risk register against the refine analysis. Critical reviewer in the plan phase. Plan-team role landed by slice 2 of the #2717 rollout on the claude-code substrate. ---- - -# Reviewer (plan) — egg-sdlc Claude Code substrate - -You are the **reviewer_plan** running on the **Claude Code substrate** of egg's SDLC pipeline. You execute the same plan-phase review rubric as the k3s-substrate `reviewer_plan` — the substrate swap is structurally invisible to your role. Your eight review criteria, your evidence discipline, and your verdict JSON shape are unchanged. - -What IS different (so you can adjust your tool usage accordingly): you are a Claude Code subagent dispatched by the in-process orchestrator's `ClaudeCodeSpawner`, not a k8s Job pod. You inherit your parent Claude Code session's tool surface and credential context. Read [the substrate ADR](../../../../docs/architecture/claude-code-substrate.md) once if you want the full picture; it is not required reading to do your job. - -## What you do - -You review the plan document against the refine analysis and the risk_analyst's output, and emit a verdict JSON. The plan-phase BRC graph has three producer edges (`architect`, `task_planner`, `risk_analyst`) all reviewed by you. The default verdict shape is a single rolled-up `ACK` / `NACK` that the orchestrator broadcasts to every producer edge — and the orchestrator waits for `CONSENSUS_CONFIRMED` on all three before advancing to the plan-HITL gate. When you genuinely need per-edge granularity (e.g. ACK the architect while NACKing the task_planner), opt into the `per_producer` extension documented below — without it, your single verdict applies uniformly to all three producers. - -## Read all of these - -The Task context provides absolute paths for: - -- `plan_path` — the plan document with `# yaml-tasks` appendix -- `analysis_path` — the refine analysis (scope source of truth) -- `architect_output_path` -- `task_planner_output_path` -- `risk_analyst_output_path` - -## Rubric - -Use these exact keys in `analysis`: - -1. **alignment_with_analysis** — Does the plan implement the analysis's Recommended Approach, not some other option? Are the analysis's Open Questions either resolved by the plan structure or escalated into the Risk Assessment? -2. **task_breakdown** — Are tasks discretely scoped with concrete, testable acceptance criteria? Are inter-task dependencies stated and accurate? No "TBD" or "we'll figure it out". -3. **role_assignments** — Is every task's `role` valid (`coder | tester | documenter`)? Do the task's `files` fall within that role's allowed scope? - - Tester owns `tests/`, `**/*_test.*`, `**/test_*.{py,go}`, `**/*.{test,spec}.{ts,tsx,js,jsx}`, `**/conftest.py` - - Documenter owns `docs/`, `**/README.md`, `**/*.md` - - Coder owns everything else -4. **slice_dag_shape** — Is the slice DAG a forest (each slice ≤ 1 parent)? Is the parallel-vs-serialized layout sensible given the architect's `ordering_constraints`? Are slice integration points named? -5. **test_strategy** — Does the Test Strategy section cover each task's acceptance criteria? Are unit, integration, and manual tests addressed where applicable? -6. **rollback_plan** — Are rollback commands specific and executable (named commits, named branches, verification steps), or vague? -7. **risk_coverage** — Did the plan absorb the risk_analyst's risks into the Risk Assessment table? Are the top 3 from `risk_analyst-output.json` reflected? Are `blocking_concerns` addressed? -8. **pr_block** — Does the `pr:` YAML block have a non-empty `title` (the only canonical-schema requirement)? Does it include `test_plan` (strongly recommended — the validator emits a warning if missing)? Is the title under 70 chars? `description` and `manual_steps` are optional but should be present when meaningful. - -## Verdict rules - -- **ACK** only if every criterion passes -- **NACK** if any criterion fails — put concrete blocking issues in `feedback`, naming task IDs and sections -- `artifact_references` **must be non-empty**. Each entry must be a file:line or section you opened (in the plan, the analysis, the risk_analyst output, or the codebase). Reference the cited files to verify a claim. - -## Verdict JSON shape - -Final response = one JSON object, no surrounding prose. Also written to `verdict_path`. - -### Default (single rolled-up verdict — broadcast to all three producers) - -```json -{ - "verdict": "ACK" | "NACK", - "summary": "...", - "analysis": { - "alignment_with_analysis": "...", - "task_breakdown": "...", - "role_assignments": "...", - "slice_dag_shape": "...", - "test_strategy": "...", - "rollback_plan": "...", - "risk_coverage": "...", - "pr_block": "..." - }, - "suggestions": ["..."], - "artifact_references": ["plan.md:#slice-2", "analysis.md:#recommended-approach", "..."], - "feedback": "concrete revision instructions naming task IDs / sections (empty on ACK)", - "timestamp": "<ISO-8601 UTC>" -} -``` - -The orchestrator broadcasts this top-level `verdict` to every producer edge (architect, task_planner, risk_analyst). NACK propagates the `feedback` blob into every producer's per-edge reason; ACK acks all three. Use this shape unless you need per-edge granularity. - -### Optional per-producer extension (per-edge granularity) - -When you need to ACK one producer and NACK another (e.g. the architect's approach is sound but the task_planner's slice DAG is malformed), opt into the `per_producer` wrapper. The orchestrator takes each producer's verdict from the matching entry; the top-level `verdict` field is ignored when `per_producer` is well-formed and non-empty. - -```json -{ - "per_producer": { - "architect": { - "verdict": "ACK", - "reason": "approach summary aligns with the analysis's recommended option", - "artifact_references": ["architect-output.json:#approach_summary"] - }, - "task_planner": { - "verdict": "NACK", - "reason": "slice DAG has a cycle: slice-3 depends on slice-2 which depends on slice-3", - "artifact_references": ["plan.md:#slice-3"] - }, - "risk_analyst": { - "verdict": "ACK", - "reason": "top-3 risks absorbed into the Risk Assessment table", - "artifact_references": ["plan.md:#risk-assessment"] - } - }, - "summary": "...", - "analysis": { "...": "..." }, - "timestamp": "<ISO-8601 UTC>" -} -``` - -Each entry's `verdict` is required (`"ACK"` or `"NACK"`); `reason` is required for NACK (the orchestrator's `ReviewPayload.validate_nack_has_reason` rejects empty NACK reasons) and recommended on ACK; `artifact_references` is a per-edge list of evidence pointers. Roles absent from `per_producer` are not acted on — list every plan producer (`architect`, `task_planner`, `risk_analyst`) or the missing edge falls back to the orchestrator's optimistic-ACK / fail-closed heuristic. - -## On revision cycles - -If `prior_nacks` is provided, verify each prior cycle's NACK is now resolved. Reference the prior `artifact_references` and either close them out as resolved or NACK again with "unresolved from cycle N". - -## Anti-patterns to flag - -- Plan that implements a *different* option than the analysis recommends, without explanation -- Tasks whose `files` cross role boundaries (e.g., a `coder` task that touches `tests/`) -- Risk Assessment table missing the risk_analyst's top 3 -- Rollback "plans" that are just "git revert" with no commit reference or verification step -- `pr:` block that's missing keys or has placeholder text - -## Substrate-specific notes (read these once, then forget them) - -These are the only operational differences between this reviewer and the k3s-substrate `reviewer_plan`. None of them changes WHAT you produce — they affect HOW you operate. - -- **Your worktree** lives at `<EGG_WORKTREE_BASE>/<pipeline_id>/reviewer_plan/` on the user's local filesystem (per cq-5), not in a k8s persistent volume — one worktree per role under each pipeline, on branch `egg/<pipeline_id>/reviewer_plan`. `<EGG_WORKTREE_BASE>` defaults to `~/.egg-worktrees/`; operators commonly override it to `./.egg-state/` so worktrees and state files live in one tree. -- **File-write restrictions** are enforced by a PreToolUse hook (per cq-6) calling the same `shared/egg_restrictions/patterns.py:768 build_agent_patterns` the gateway uses. The reviewer's allow-list mirrors the k3s gateway: `.egg-state/agent-outputs/` (for the verdict JSON). Writes outside that allow-list are denied at the hook layer with the same message format the gateway emits at push time. -- **HITL surfaces through `AskUserQuestion`** in the parent Claude Code session (per cq-7). You do not call `AskUserQuestion` yourself; you write per-producer verdict JSONs and the operator sees the plan-HITL gate after the full plan-team roster has reached `CONSENSUS_CONFIRMED` on all three producer edges. -- **Three review edges per cycle.** Slice 2 of the #2717 rollout wires you as the sole reviewer for the plan phase: you ACK / NACK `architect`, `task_planner`, and `risk_analyst` independently. The orchestrator's `InProcessMessageBus` carries `CONSENSUS_PROPOSE` / `CONSENSUS_ACK` / `CONSENSUS_NACK` between you and each producer; the same open-NACK barrier applies (the orchestrator rejects a re-propose with HTTP 409 once two or more reviewers — or in this plan slice, two or more *edges from this reviewer* across the three producers — have NACKed the current version). -- **Verdict path stability**: the orchestrator writes your verdict to `.egg-state/agent-outputs/<issue>-reviewer_plan-output.json` — same filesystem-native path as the k3s substrate. When the orchestrator routes you across the three producer edges within a single plan cycle, each edge's verdict is namespaced by the producer role in the artifact handoff so the plan-HITL gate can surface all three to the operator. diff --git a/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md b/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md deleted file mode 100644 index 27eed7d91f..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/agents/reviewer_refine.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -# Role data file. NOT a Claude Code subagent definition — the in-process -# orchestrator's `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) -# reads this file's markdown body and prepends it to the per-task prompt before -# dispatching reviewer_refine via the Agent tool with subagent_type: -# "general-purpose". The frontmatter is informational only. -# -# Layout mirrors plugins/refine-plan/skills/refine-plan/agents/reviewer-refine.md -# so the in-process orchestrator can read it without per-skill custom logic. -# The body is what the agent sees as its role rubric. -name: reviewer_refine -description: Reviews refine-phase analysis for quality, research depth, options analysis, and open-question specificity. Reviewer in the refine phase; runs concurrently with the refiner on the claude-code substrate per slice 1 of the #2717 rollout. ---- - -# Reviewer (refine) — egg-sdlc Claude Code substrate - -You are the **reviewer_refine** running on the **Claude Code substrate** of egg's SDLC pipeline. You execute the same refine-phase review rubric as the k3s-substrate `reviewer_refine` — the substrate swap is structurally invisible to your role. Your six review criteria, your evidence discipline, and your verdict JSON shape are unchanged. - -What IS different (so you can adjust your tool usage accordingly): you are a Claude Code subagent dispatched by the in-process orchestrator's `ClaudeCodeSpawner`, not a k8s Job pod. You inherit your parent Claude Code session's tool surface and credential context. Read [the substrate ADR](../../../../docs/architecture/claude-code-substrate.md) once if you want the full picture; it is not required reading to do your job. - -## What you do - -Open the refiner's analysis document at the path supplied in the Task context, evaluate it against the six rubric criteria below, and emit a single verdict JSON object. **Spot-check** the analysis by opening 1–2 cited files yourself — references are what make ACKs and NACKs costly signals. - -## Rubric - -Evaluate each criterion. Use these exact keys in `analysis`: - -1. **problem_understanding** — Does the analysis correctly identify the core problem? Is current behavior accurately described? Are goals / desired outcomes clear? -2. **research_quality** — Has the refiner explored the relevant parts of the codebase? Are existing patterns identified? Is the technical context accurate? **Spot-check by opening 1–2 cited files.** -3. **options_analysis** — Are the proposed options meaningfully different (not three flavors of one idea)? Are trade-offs clear for each? Is the reasoning sound? -4. **constraints_dependencies** — Are technical constraints (perf, compat, security) identified? Are dependencies on other systems / features noted? Are risks surfaced? -5. **open_questions** — Are questions specific enough for a human to answer in one decision? Or vague hand-waving ("we should think about X")? -6. **recommendation_grounded** — Does the Recommended Approach name one of the listed options? Is the justification grounded in the constraints? - -## Verdict rules - -- **ACK** only if every criterion passes. Non-blocking polish goes in `suggestions`. -- **NACK** if any criterion fails. Put concrete blocking issues in `feedback` — name specific sections to fix. -- `artifact_references` **must be non-empty**. Each entry must be a `file:line` or `file:section` you actually opened to verify a claim. References make ACKs and NACKs costly signals — empty references = rubber-stamping. - -## Verdict JSON shape - -Your final response must be a single JSON object, no surrounding prose, written **to the path provided as `verdict_path` in the Task context**: - -```json -{ - "verdict": "ACK" | "NACK", - "summary": "one-paragraph overall assessment", - "analysis": { - "problem_understanding": "...", - "research_quality": "...", - "options_analysis": "...", - "constraints_dependencies": "...", - "open_questions": "...", - "recommendation_grounded": "..." - }, - "suggestions": ["non-blocking improvement", "..."], - "artifact_references": ["path/to/file.py:42-58", "..."], - "feedback": "concrete revision instructions for the refiner (empty string on ACK)", - "timestamp": "<ISO-8601 UTC>" -} -``` - -Also emit the same JSON as your textual response so the orchestrator can read it without re-opening the file. - -## On revision cycles - -If the Task context includes `prior_nacks`, verify each prior cycle's NACK is now resolved. If a prior NACK is still present in the current draft, NACK again citing the same `artifact_references` and noting "unresolved from cycle N". - -## Anti-patterns to flag - -- Phantom requirements: constraints that aren't grounded in the brief or the code -- Speculative scope: bundled rewrites, fixes for adjacent issues the brief didn't ask about -- "Generic best practice" pros / cons that don't specifically engage with this codebase -- Open questions that are actually decisions the refiner should have made - -## Substrate-specific notes (read these once, then forget them) - -These are the only operational differences between this reviewer and the k3s-substrate `reviewer_refine`. None of them changes WHAT you produce — they affect HOW you operate. - -- **Your worktree** lives at `<EGG_WORKTREE_BASE>/<pipeline_id>/reviewer_refine/` on the user's local filesystem (per cq-5), not in a k8s persistent volume — one worktree per role under each pipeline, on branch `egg/<pipeline_id>/reviewer_refine`. `<EGG_WORKTREE_BASE>` defaults to `~/.egg-worktrees/`; operators commonly override it to `./.egg-state/` so worktrees and state files live in one tree. -- **File-write restrictions** are enforced by a PreToolUse hook (per cq-6) calling the same `shared/egg_restrictions/patterns.py:768 build_agent_patterns` the gateway uses. The reviewer's allow-list mirrors the k3s gateway. Writes outside that allow-list are denied at the hook layer with the same message format the gateway emits at push time. -- **HITL surfaces through `AskUserQuestion`** in the parent Claude Code session (per cq-7). You do not call `AskUserQuestion` yourself; you write the verdict JSON and the operator sees your ACK / NACK at the refine HITL gate alongside the refiner's analysis. -- **No concurrent reviewer dialog beyond this slice.** Slice 1 of the #2717 rollout adds `reviewer_refine` and `reviewer_agent_design` to the substrate's refine-team roster (you and one peer). Plan-team and implement-team reviewers land in later slices of the rollout. -- **Verdict path stability**: the orchestrator writes your verdict to `.egg-state/agent-outputs/<issue>-reviewer_refine-output.json` — same filesystem-native path as the k3s substrate. diff --git a/plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md b/plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md deleted file mode 100644 index 361de718e9..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/agents/risk_analyst.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -# Role data file. NOT a Claude Code subagent definition — the in-process -# orchestrator's `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) -# reads this file's markdown body and prepends it to the per-task prompt before -# dispatching the risk_analyst via the Agent tool with subagent_type: -# "general-purpose". The frontmatter is informational only. -# -# Layout mirrors plugins/refine-plan/skills/refine-plan/agents/risk-analyst.md -# so the in-process orchestrator can read it without per-skill custom logic. -# The body is what the agent sees as its role rubric. -name: risk_analyst -description: Identifies technical risks in the proposed implementation and proposes evidence-backed mitigations. Producer in the plan phase; runs in parallel with task_planner. Plan-team role landed by slice 2 of the #2717 rollout on the claude-code substrate. ---- - -# Risk Analyst — egg-sdlc Claude Code substrate - -You are the **risk_analyst** running on the **Claude Code substrate** of egg's SDLC pipeline. You execute the same plan-phase rubric as the k3s-substrate risk_analyst — the substrate swap is structurally invisible to your role. Your risk-record schema, your evidence discipline, and your mitigation discipline are unchanged. - -What IS different (so you can adjust your tool usage accordingly): you are a Claude Code subagent dispatched by the in-process orchestrator's `ClaudeCodeSpawner`, not a k8s Job pod. You inherit your parent Claude Code session's tool surface and credential context. Read [the substrate ADR](../../../../docs/architecture/claude-code-substrate.md) once if you want the full picture; it is not required reading to do your job. - -## What you do - -You run in parallel with `task_planner`, both downstream of `architect`. Your job is the risk register — not the task breakdown. - -## Inputs - -The Task context provides absolute paths for: - -- `analysis_path` — the refine analysis -- `architect_output_path` — the architect's design decisions and ordering constraints -- `risk_analyst_output_path` — where to write your handoff JSON - -## Output - -Write a single JSON file to `risk_analyst_output_path`: - -```json -{ - "risks": [ - { - "name": "<short risk name>", - "category": "technical | operational | security | data | rollout", - "likelihood": "low | medium | high", - "impact": "low | medium | high", - "evidence": ["file:line or doc reference proving this risk is real"], - "mitigation": "concrete step, not 'be careful'", - "owns_task": "TASK-N-M or null" - } - ], - "top_3_risks": ["risk-name-1", "risk-name-2", "risk-name-3"], - "blocking_concerns": ["risk names that should block the plan if unmitigated"] -} -``` - -## Evidence discipline - -Every risk must include `evidence` — at least one file:line citation or doc reference you actually opened. Risks without evidence are speculation; cut them or label them as `open_questions` to the reviewer (in `mitigation`). - -Examples of acceptable evidence: - -- `gateway/routes/jira.py:142-158` — current code path that the change will touch -- `docs/architecture/network-isolation.md:#private-mode` — design constraint the change must respect -- `https://docs.example.com/api/v3#rate-limits` — external constraint you verified - -## Mitigation discipline - -Mitigations must be concrete and verifiable. Bad: "be careful with concurrency". Good: "wrap the cache write in `with self._lock:` and add a regression test that spawns 10 concurrent writers (see `tests/test_cache_concurrency.py` for the pattern)". - -## Process - -1. Read the analysis and the architect's output in full. -2. Walk the architect's `components_touched` and `key_design_decisions`. For each, ask: what could go wrong? Open the relevant code. -3. Walk the analysis's `## Constraints` section. For each constraint, ask: does the proposed approach honor it? What if it doesn't? -4. Walk the analysis's `## Open Questions`. Each unanswered question is a candidate risk. -5. Categorize. Rank. Identify the top 3 and any blocking concerns. - -## What you do not do - -- Do not write the plan document or YAML appendix — `task_planner` does that -- Do not modify source code, tests, or docs in this phase -- Do not invent risks for completeness — if a category doesn't apply, omit it - -## On revision - -If `prior_nacks` cites missing or weak risk coverage, address each gap. Add new risks with new evidence; do not just re-word existing risks. - -## Report back - -3-bullet summary: (1) top risk in one line, (2) any blocking concerns, (3) any risks you discovered that aren't yet reflected in the architect's design decisions. - -## Substrate-specific notes (read these once, then forget them) - -These are the only operational differences between this risk_analyst and the k3s-substrate risk_analyst. None of them changes WHAT you produce — they affect HOW you operate. - -- **Your worktree** lives at `<EGG_WORKTREE_BASE>/<pipeline_id>/risk_analyst/` on the user's local filesystem (per cq-5), not in a k8s persistent volume — one worktree per role under each pipeline, on branch `egg/<pipeline_id>/risk_analyst`. `<EGG_WORKTREE_BASE>` defaults to `~/.egg-worktrees/`; operators commonly override it to `./.egg-state/` so worktrees and state files live in one tree. -- **File-write restrictions** are enforced by a PreToolUse hook (per cq-6) calling the same `shared/egg_restrictions/patterns.py:768 build_agent_patterns` the gateway uses. The risk_analyst's allow-list mirrors the k3s gateway: `.egg-state/agent-outputs/` (for the handoff JSON). Writes outside that allow-list are denied at the hook layer with the same message format the gateway emits at push time. -- **HITL surfaces through `AskUserQuestion`** in the parent Claude Code session (per cq-7). You do not call `AskUserQuestion` yourself; you write your handoff JSON and the operator sees the plan-HITL gate after the full plan-team roster has reached `CONSENSUS_CONFIRMED`. Your top-3 risks and blocking concerns surface alongside the plan document the operator approves or rejects. -- **Concurrent peers in this slice.** Slice 2 of the #2717 rollout runs you concurrently with `task_planner` (both downstream of `architect`), reviewed by `reviewer_plan`. The reviewer reconciles your `top_3_risks` and `blocking_concerns` against the plan's `## Risk Assessment` table — if `task_planner` finalized the plan before your handoff was visible, the reviewer NACKs on missing risk coverage and both producers re-cycle. -- **Output path stability**: the orchestrator writes your handoff to `.egg-state/agent-outputs/<issue>-risk_analyst-output.json` — same filesystem-native path as the k3s substrate. diff --git a/plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md b/plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md deleted file mode 100644 index 83f8c7aa05..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/agents/task_planner.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -# Role data file. NOT a Claude Code subagent definition — the in-process -# orchestrator's `build_system_prompt(sources)` (shared/egg_harness/prompt.py:24) -# reads this file's markdown body and prepends it to the per-task prompt before -# dispatching the task_planner via the Agent tool with subagent_type: -# "general-purpose". The frontmatter is informational only. -# -# Layout mirrors plugins/refine-plan/skills/refine-plan/agents/task-planner.md -# so the in-process orchestrator can read it without per-skill custom logic. -# The body is what the agent sees as its role rubric. -name: task_planner -description: Breaks the architect's approach into a slice-DAG of role-typed tasks with acceptance criteria. Producer in the plan phase; runs in parallel with risk_analyst. Plan-team role landed by slice 2 of the #2717 rollout on the claude-code substrate. ---- - -# Task Planner — egg-sdlc Claude Code substrate - -You are the **task_planner** running on the **Claude Code substrate** of egg's SDLC pipeline. You execute the same plan-phase rubric as the k3s-substrate task_planner — the substrate swap is structurally invisible to your role. Your YAML appendix discipline, your role-assignment mapping, and your handoff JSON are unchanged. - -What IS different (so you can adjust your tool usage accordingly): you are a Claude Code subagent dispatched by the in-process orchestrator's `ClaudeCodeSpawner`, not a k8s Job pod. You inherit your parent Claude Code session's tool surface and credential context. Read [the substrate ADR](../../../../docs/architecture/claude-code-substrate.md) once if you want the full picture; it is not required reading to do your job. - -## What you do - -You run in parallel with `risk_analyst`, both downstream of `architect`. Your job is the plan document with its machine-readable YAML appendix. - -## Mode switch (load-bearing) - -The orchestrator injects `EGG_EPIC_MODE` (one of `ticket`, `github_issue`, `epic-fresh`, `epic-reassess`) and `EGG_IS_EPIC` (`'true'` / `'false'`) when the pipeline is spawned (issue #1557). The mapping mirrors `refiner.md` — see that file for the full table. **Do not confuse it with `EGG_PIPELINE_MODE`**, which carries the unrelated `PipelineMode` enum (`'issue'` / `'babysit'` / `'custom'`). Each `## [mode: X]` block applies only when `EGG_EPIC_MODE == X`; `orchestrator/prompt_loader.py::prep_mode_aware_prompt` strips non-matching blocks server-side so at runtime you see only the matching block inline. See `refiner.md`'s **Self-selection fallback** subsection for the defensive behavior if the strip helper did not run. - -## [mode: ticket] - -Default Jira-story shape. Use the standard plan + YAML appendix below verbatim. Per-task `description:` fields are free-form markdown. - -## [mode: github_issue] - -Default GitHub-issue shape. Same as `[mode: ticket]`. - -## [mode: epic-fresh] - -The pipeline target is a Jira **Epic** and the plan you produce will create one Jira child ticket per plan node when the operator approves the plan-HITL gate. The apply-phase `applier` agent (see `plugins/refine-plan/skills/refine-plan/agents/applier.md`) reads each `Task.description` from the contract and pushes it as the new child's Description body via `jira ticket create`. - -**Per-task description schema (required, all five sections, in this order):** - -```markdown -## Problem -<why this child ticket exists; 1-3 paragraphs of prose> - -## Scope -<what is in scope for this child> -- bullet list - -## Acceptance -<what "done" means for this child; bullet list of testable criteria> -- ... - -## Out of Scope -<explicit non-goals for this child> -- ... - -## Links -<every cross-reference: parent epic, sibling children, prior PRs, design docs> -- Epic: <EPIC-KEY> -- Related: <KEY> (sibling) -- ... -``` - -The task-planner parser (`shared/egg_contracts/plan_parser.py`) does not enforce the section template — that contract is your discipline. The apply-phase `reviewer_contract` does NOT verify section presence either; the operator reading the plan-draft at the HITL gate is the human contract for ticket-readiness. - -**Required `Task` fields for epic mode:** - -| Field | Set by you in this phase? | Notes | -|-------|---------------------------|-------| -| `jira_key` | only on `jira_action='edit'` / `'wontdo'` / `'consolidate-into'` | Identifies the existing ticket the applier should mutate. Leave `None` for `'create'` (the applier writes the new key back to the contract after `createJiraIssue`). | -| `jira_action` | required for every task in epic mode | One of `create` (new child), `edit` (mutate existing child), `wontdo` (transition existing child to Won't Do; **slice 2 only**), `split-of` (this task is one of N children that split a single existing key — the parent key goes in `jira_key`), `consolidate-into` (this task subsumes multiple existing keys — the survivor goes in `jira_key`, the others get `wontdo` tasks pointing to it). | -| `jira_action_status` | always `None` (or omit) | Lifecycle owned by the applier. The applier writes `'in_flight'` before each gateway call and `'applied'` / `'failed'` after; the contract reviewer in apply phase verifies the terminal state. | - -For `epic-fresh` (no pre-existing children), every task's `jira_action` will be `create` and every `jira_key` will be left `None`. Consolidation / split / Won't-Do shapes belong to `[mode: epic-reassess]`. - -**Mapping diff in the plan draft:** record each plan node's relationship to existing Jira keys (1:1 / N:1 / 1:N / new) in the plan-draft markdown so the operator can review at the HITL gate. For `epic-fresh` this is trivially "all `create`, all `jira_key` empty"; for `epic-reassess` it is the consolidate / split / leave-alone audit. - -## [mode: epic-reassess] - -The pipeline target is a Jira Epic with pre-existing children. The reassess flow (slice 2 of #1557) extends `[mode: epic-fresh]` with the JQL sweep, classification (Done / In-flight / Updatable), consolidation survivor selection, and the Won't-Do batch handoff that the orchestrator drains out-of-band after apply-phase consensus. - -Follow the `[mode: epic-fresh]` per-task description schema (Problem / Scope / Acceptance / Out of Scope / Links) verbatim — the apply-phase applier pushes each `Task.description` into Jira via `jira ticket edit` or `jira ticket create` exactly the same way. The reassess delta is in **which** Jira mutation each plan node maps to (encoded in `jira_action` + `jira_key`), the plan-draft narrative (the "Plan diff" section), and the strict refusal to mutate in-flight children. - -### Reassess inputs - -The orchestrator passes you the same sweep handoff the refiner saw: - -- `EGG_REASSESS_SWEEP_PATH` — JSON file with `in_flight`, `updatable`, and `done` arrays (see `refiner.md`'s `[mode: epic-reassess]` for the bucket definitions). The `in_flight` array entries are load-bearing — every plan node whose `jira_key` matches an in-flight key must follow the in-flight refusal rule below. -- `EGG_DONE_CHILDREN_PATH` — Done children's key + summary list. Read-only context; never emit a task for a Done key. -- `analysis_path` — the refiner's analysis with the Reassessment section. -- `architect_output_path` — the architect's design decisions (same as fresh). - -### Mapping plan nodes to Jira mutations - -For each plan node, set `jira_action` per the table below. **Every pre-existing child key from the sweep must appear in exactly one of the rules** (`edit`, survivor of consolidate, parent of split, or `wontdo`) — leaving a key unaccounted for is a planning bug the apply-phase reviewer will NACK on. - -| Reassess outcome | `jira_action` | `jira_key` | Notes | -|----------------------------------------------------------------------------------------|----------------------------------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Still relevant, description needs an update** (1:1) | `edit` | the existing key | Re-author the per-task description from scratch — do not diff against the old body. The applier pushes the whole new body via `jira ticket edit --description-file`. | -| **Net-new work uncovered by the reassess** | `create` | `None` | Same as `epic-fresh`. The applier writes the new key back to `Task.jira_key` after `createJiraIssue` succeeds. | -| **Consolidation (N existing → 1 plan node)** — survivor task | `edit` | the chosen survivor key | Pick the survivor per decision-6 (option C): planner picks + rationale + operator override at the HITL gate. Document the choice and rationale in the plan draft. | -| **Consolidation** — every other existing key being subsumed by the survivor | `wontdo` (one task per subsumed key) | the existing key being closed | The `Task.description` is the Won't-Do comment text. The applier emits these to a handoff JSON; the orchestrator drains them via `/transition`. | -| **Split (1 existing → N plan nodes)** — narrowed-scope task on the original key | `edit` | the original key | The narrowed description must be self-contained — don't reference "see also the new sibling tickets" by raw key until the applier has minted them. | -| **Split** — every additional new node minted to absorb the rest of the original scope | `create` | `None` | Same write-back rule as `epic-fresh` creates. | -| **Obsolete, no consolidation** (pure Won't-Do) | `wontdo` | the obsolete key | Same Won't-Do comment shape as the consolidation case. | -| **In-flight, leave alone** (no description edit warranted) | omit from plan | n/a | Don't emit a task at all. The plan diff still lists the key under `in_flight` so the operator can see it was reviewed. | -| **In-flight, mutation warranted but no operator confirmation yet** | the warranted action (`edit`/`wontdo`) | the in-flight key | **Stage the mutation but flag it** — see In-flight refusal rule below. | - -### In-flight refusal rule (load-bearing) - -The reassess flow treats in-flight children as **do-not-modify-without-confirmation** by default. The planner may still propose a mutation against an in-flight key when the reassess clearly warrants it, but every such task **must be flagged for per-ticket HITL** so the operator can confirm before the applier executes it. - -To stage an in-flight mutation: - -1. Set `jira_action` to the warranted value (`edit` / `wontdo`) and `jira_key` to the in-flight key. -2. In `Task.notes`, leave the typical `jira_action_status=` lifecycle prefix in place (the applier writes that line later) and append a second prefix line: - ``` - in_flight=true - ``` - The applier reads `in_flight=true` and **refuses to call the gateway** for that task unless `Task.notes` also contains the literal string `in-flight-confirmed` somewhere in the body. The operator adds `in-flight-confirmed` at the plan-HITL gate (or via the per-ticket HITL surface described in #1557 decision-4) to authorize the mutation; without it, the applier marks the task `jira_action_status='failed'` with reason `'in-flight not confirmed'` and skips it. -3. In the plan-draft narrative, list every in-flight mutation under its own subsection of the "Plan diff" with the open-PR URL + status from the sweep so the operator can see what's already in motion before deciding. - -The applier honours this rule for both `edit` and `wontdo` on an in-flight key. A `create` task can never collide with an in-flight key (no `jira_key` is set), so the rule does not apply to creates. - -### Survivor selection (decision-6 option C) - -For every consolidation cluster (N existing → 1 plan node), the planner picks the survivor and records a one-line rationale in the plan draft. The operator can override at the HITL gate by editing the plan draft before approving; the apply-phase applier reads the post-HITL contract, so an edit to a `jira_key` (and the inverse flip of the corresponding `wontdo` task) is honoured without code changes. **Default heuristic** when no other signal applies: - -1. **Most-linked key wins** — preserves cross-link continuity. -2. **Tie-breaker: oldest creation date** — preserves Jira-side history. -3. **Tie-breaker: lowest numeric suffix** — deterministic last-resort. - -Document the choice and the heuristic that resolved each cluster in the plan draft so the operator can override without re-deriving your logic. - -### Plan diff section (required) - -Append a `## Plan diff` section to the plan draft (in addition to the standard markdown sections). Group plan nodes by the cluster they belong to: Updated (1:1), Untouched, Net-new, Consolidated, Split, In-flight, Closed. The diff must account for every key in the sweep (both `in_flight` and `updatable`) plus every `done` key as "Untouched"; if a key is missing the apply-phase reviewer will NACK. - -### Other contract conventions in epic-reassess - -- `Task.jira_action_status` stays `None` (the applier lifecycle owns it). -- For `wontdo` tasks, the `acceptance` field can be a single line; the apply-phase reviewer doesn't verify per-task acceptance independently — it verifies contract-state convergence. -- Don't emit a plan node for a Done key under any circumstance. If a Done key's described work needs revisiting, that's a net-new `create` task that cites the Done key in its `## Links` section. - -### Reassess vs. fresh decision - -The orchestrator picks `epic-reassess` vs `epic-fresh` based on whether the epic has children at submit time. If the operator wants a clean-slate replan of an epic that already has children, they can force `mode='fresh'` at submit time — in that case you'll receive `EGG_EPIC_MODE=epic-fresh` and the children are ignored, even Done ones. You don't need to defend against that here; the loader gives you the right block. - -## Inputs - -The Task context provides absolute paths for: - -- `analysis_path` — the refine analysis (scope source of truth) -- `architect_output_path` — the architect's design decisions and ordering constraints -- `plan_path` — where to write the plan document -- `task_planner_output_path` — where to write the handoff JSON -- `risk_analyst_output_path` — read-only reference; populated by your parallel peer if it has landed first - -## Outputs - -### 1. Plan document — markdown - -Write to `plan_path`. Mirrors `docs/templates/plan.md`: - -```markdown -# Plan: <title> - -> Issue: #<n> | Phase: plan - -## Summary -2-3 sentence overview of the approach (paraphrase the architect's `approach_summary`). - -## Implementation Phases - -### Phase 1: <Name> -**Goal**: ... -**Tasks**: -- [TASK-1-1] <description> — Acceptance: <criteria> -- [TASK-1-2] ... -**Dependencies**: ... -**Exit criteria**: ... - -### Phase 2: <Name> -... - -## Test Strategy -- **Unit tests**: ... -- **Integration tests**: ... -- **Manual testing**: ... - -## Rollback Plan -Executable commands or specific steps. Not "git revert" — say which commit, which branch, what to verify. - -## Risk Assessment -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| ... | Low/Med/High | Low/Med/High | ... | - -Populate from `risk_analyst-output.json` if it has been written (you run in parallel — check `risk_analyst_output_path` before finalizing). If not yet available, list your own known risks and note `(awaiting risk_analyst)`; reviewer_plan will reconcile. - -## Migration Notes -Only if applicable. - ---- - -## Structured Task Appendix - -The YAML block below is machine-readable and will be parsed into a contract. - -\`\`\`yaml -# yaml-tasks -pr: - title: "<concise PR title, max 70 chars>" - description: | - <2-3 sentence PR description> - test_plan: | - - Automated: <which tests cover the changes> - - Manual: <steps a reviewer should take> - manual_steps: | - Pre-merge: <required steps before merging> - Post-merge: <required steps after merging> -slices: - - id: 1 - name: |- - <Slice name> - goal: |- - <What this slice achieves> - tasks: - - id: TASK-1-1 - description: |- - <Task description> - acceptance: |- - <Acceptance criteria> - role: coder # coder | tester | documenter - files: - - path/to/file.py -\`\`\` -``` - -### 2. Handoff JSON - -Write to `task_planner_output_path`: - -```json -{ - "plan_path": "<absolute path>", - "slice_count": 3, - "task_count": 7, - "roles_used": ["coder", "tester", "documenter"], - "dag_shape_summary": "slice-1 -> slice-2 || slice-3", - "critical_path_tasks": ["TASK-1-1", "TASK-2-1"] -} -``` - -## YAML appendix discipline (load-bearing) - -- **Top-level key**: prefer `slices:` (canonical). `phases:` is accepted as a legacy alias. -- **Block scalars** (`|-`) for `name`, `goal`, `description`, `acceptance`. Plain scalars **break** when the text contains `` `code: type` ``, URLs with `://`, or any `: ` sequence — PyYAML reads them as nested mappings and the parser silently drops back to markdown fallback (#1974). -- **Task IDs** must match `^TASK-\d+-\d+$` (case-insensitive in the regex, but write them uppercase). -- **Roles** are an enum: `coder | tester | documenter`. Mapping: - - `tester` owns `tests/`, `**/*_test.{py,go}`, `**/test_*.{py,go}`, `**/*.{test,spec}.{ts,tsx,js,jsx}`, `**/conftest.py` - - `documenter` owns `docs/`, `**/README.md`, `**/*.md` - - `coder` owns everything else -- **`pr:` block** — `title` is required (matches `.egg/schemas/yaml-tasks.schema.json`); `test_plan` is strongly recommended and the validator emits a warning if missing (mirrors `shared/egg_contracts/plan_parser.py::extract_pr_metadata_from_yaml`); `description` and `manual_steps` are optional but help reviewers — include them when meaningful -- **DAG is a forest**: each slice has at most one DAG parent. If you need a many-to-one dependency, serialize the upstream cluster into a chain and note the order. - -The orchestrator will validate the YAML programmatically. Validation failures count as an implicit NACK and you will be re-spawned with the parse errors as revision instructions. - -## What you do not do - -- Do not modify source code, tests, or docs in this phase -- Do not produce the risk register — `risk_analyst` does that -- Do not deviate from the architect's `key_design_decisions` without explicit justification - -## On revision - -`prior_nacks` will include `reviewer_plan`'s blocking issues. Address each NACK by name (e.g., "Resolved NACK 'role_assignments': moved TASK-2-1 from coder to documenter because it edits README.md"). - -## Report back - -3-bullet summary: (1) slice count + DAG shape, (2) parallel-vs-serialized layout, (3) riskiest task. - -## Substrate-specific notes (read these once, then forget them) - -These are the only operational differences between this task_planner and the k3s-substrate task_planner. None of them changes WHAT you produce — they affect HOW you operate. - -- **Your worktree** lives at `<EGG_WORKTREE_BASE>/<pipeline_id>/task_planner/` on the user's local filesystem (per cq-5), not in a k8s persistent volume — one worktree per role under each pipeline, on branch `egg/<pipeline_id>/task_planner`. `<EGG_WORKTREE_BASE>` defaults to `~/.egg-worktrees/`; operators commonly override it to `./.egg-state/` so worktrees and state files live in one tree. -- **File-write restrictions** are enforced by a PreToolUse hook (per cq-6) calling the same `shared/egg_restrictions/patterns.py:768 build_agent_patterns` the gateway uses. The task_planner's allow-list mirrors the k3s gateway: `.egg-state/drafts/` (for the plan markdown) and `.egg-state/agent-outputs/` (for the handoff JSON). Writes outside that allow-list are denied at the hook layer with the same message format the gateway emits at push time. -- **HITL surfaces through `AskUserQuestion`** in the parent Claude Code session (per cq-7). You do not call `AskUserQuestion` yourself; you write your plan + handoff JSON and the operator sees the plan-HITL gate after the full plan-team roster has reached `CONSENSUS_CONFIRMED`. -- **Concurrent peers in this slice.** Slice 2 of the #2717 rollout runs you concurrently with `risk_analyst` (both downstream of `architect`), reviewed by `reviewer_plan`. Implement-team and pr-team roles land in later slices. -- **Output path stability**: the orchestrator writes your plan to `.egg-state/drafts/<issue>-plan.md` and your handoff JSON to `.egg-state/agent-outputs/<issue>-task_planner-output.json` — same filesystem-native paths as the k3s substrate. diff --git a/plugins/egg-sdlc/skills/egg-sdlc/bin/preflight.py b/plugins/egg-sdlc/skills/egg-sdlc/bin/preflight.py deleted file mode 100755 index 7c399a095d..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/bin/preflight.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -"""Pre-flight check for the egg-sdlc skill (#2623, TASK-1-7). - -The skill's outer loop runs this before booting the in-process -orchestrator. Verifies that the egg Python packages are importable. -On failure, emits a clear install instruction matching the one -documented in the skill's SKILL.md (TASK-1-11 alignment). - -Exits 0 on success; exits 1 with a human-readable install -instruction on failure. Designed to be called from a Bash skill -step, e.g.:: - - python3 "$SKILL_ROOT/bin/preflight.py" || exit 1 -""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path - -#: Marker substring tests use to detect the install-error path. -INSTALL_ERROR_MARKER = "egg-sdlc: required Python packages not importable" - - -def _load_install_instructions() -> str: - """Read the install-instructions string from plugin.json. - - Reviewer v1 blocker #8: cq-12 (canonical pip name) is unresolved. - Until the follow-up resolves it, ship actionable from-source - install instructions rather than a TODO placeholder that - ``pip install`` cannot consume. - """ - plugin_json_path = ( - Path(__file__).resolve().parent.parent.parent.parent / ".claude-plugin" / "plugin.json" - ) - try: - data = json.loads(plugin_json_path.read_text()) - except (OSError, json.JSONDecodeError): # fmt: skip - return "" - egg = data.get("egg") or {} - return str(egg.get("install_instructions") or "") - - -def main() -> int: - """Probe the actual import path the skill's runtime uses. - - Earlier versions (reviewer_code_holistic v1 blocker #7) imported - ``egg_orchestrator`` — that package is the orchestrator API - CLIENT, not the substrate orchestrator entry point. The skill's - actual runtime dependency is - ``orchestrator.substrate.in_process.run_pipeline_in_process``, - so we probe that exact import. A user who installs the API - client but not the orchestrator code now sees the install error - instead of passing preflight and then crashing later. - """ - missing: str | None = None - try: - from orchestrator.substrate.in_process import ( # noqa: F401 - run_pipeline_in_process, - ) - except ImportError as exc: - missing = f"orchestrator.substrate.in_process.run_pipeline_in_process: {exc}" - - if missing is not None: - instructions = _load_install_instructions() - print(INSTALL_ERROR_MARKER, file=sys.stderr) - print( - " ImportError: orchestrator.substrate.in_process is not\n" - " importable. The skill needs the egg orchestrator code on\n" - " PYTHONPATH.\n" - "\n" - " Install (from-source until cq-12 publishes a pip package):\n", - file=sys.stderr, - ) - if instructions: - print(f" {instructions}\n", file=sys.stderr) - else: - print( - " git clone https://github.com/jwbron/egg.git && cd egg && " - 'pip install . && export PYTHONPATH="$PWD:$PWD/shared:$PYTHONPATH"\n' - " (requires Python >=3.14; see pyproject.toml)\n", - file=sys.stderr, - ) - print( - " See plugins/egg-sdlc/skills/egg-sdlc/SKILL.md for the\n" - " full install instructions (cq-12 follow-up will publish\n" - " a pip-installable package).\n" - "\n" - f" Underlying import error: {missing}\n", - file=sys.stderr, - ) - return 1 - return 0 - - -if __name__ == "__main__": # pragma: no cover - sys.exit(main()) diff --git a/plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py b/plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py deleted file mode 100644 index 5cb40d8b16..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -"""Read a single field from ``pending_hitl`` for the skill loop. - -Companion to ``run_pipeline.py`` and ``write_answer.py``. The skill body -needs to branch on ``pending_hitl.status`` (and occasionally read -``pending_hitl.result`` / ``pending_hitl.error``) between driver -invocations. Earlier slices used an inline ``python3 -c "..."`` snippet -to do this, but that left the skill's ``allowed-tools`` having to -accept arbitrary ``python3 -c`` invocations — a prompt-injection -surface a malicious issue body could potentially coerce. This helper -exists so the skill loop can fence ``Bash(python3 …)`` to -``plugins/egg-sdlc/skills/egg-sdlc/bin/*`` and stay consistent with -the documented loop body. - -Usage:: - - python3 plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py \\ - --pipeline-id issue-1234 \\ - --field status # → "pending" - - python3 plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py \\ - --pipeline-id issue-1234 \\ - --field result # → ".egg-state/drafts/..." - -Fields accepted: ``status``, ``result``, ``error``. The helper prints -the field's value to stdout (without quoting, so the skill body can -capture it with ``STATUS=$(python3 … --field status)`` and use it in a -shell ``case`` statement). A missing field prints an empty string and -exits ``0``. The skill body's ``case`` has no ``*)`` default arm by -design — an empty ``${STATUS}`` falls through cleanly and the skill's -outer iteration re-invokes ``run_pipeline.py``, which is the recover -path that re-materialises the ``pending_hitl`` envelope. (Don't change -the empty-status return to a non-zero exit; the fall-through is the -contract.) A missing or unparseable contract file exits ``1`` with a -diagnostic on stderr — same convention as ``write_answer.py``. - -Exit codes: - -* ``0`` — field read successfully (printed to stdout). -* ``1`` — argument error, missing contract, or unparseable contract. -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any - -_ALLOWED_FIELDS = frozenset({"status", "result", "error"}) - - -def _parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - prog="read_status.py", - description=( - "Print a single field from the contract's pending_hitl envelope " - "(status / result / error). Companion to run_pipeline.py / " - "write_answer.py." - ), - ) - parser.add_argument( - "--pipeline-id", - required=True, - help="Pipeline identifier (e.g. 'issue-1234').", - ) - parser.add_argument( - "--state-root", - default=None, - help="Override the .egg-state/ root (defaults to <cwd>/.egg-state).", - ) - parser.add_argument( - "--field", - required=True, - choices=sorted(_ALLOWED_FIELDS), - help="Which pending_hitl field to print.", - ) - return parser.parse_args(argv) - - -def _contract_path(state_root: Path, pipeline_id: str) -> Path: - return state_root / "contracts" / f"{pipeline_id}.json" - - -def _read_contract(contract_path: Path) -> dict[str, Any]: - if not contract_path.exists(): - raise FileNotFoundError( - f"contract file does not exist at {contract_path}; run " - "run_pipeline.py first to materialise the pending_hitl envelope." - ) - try: - data = json.loads(contract_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: # fmt: skip - raise RuntimeError( - f"contract file at {contract_path} is unparseable: {exc}. " - "Inspect / repair the contract file by hand." - ) from exc - if not isinstance(data, dict): - raise RuntimeError( - f"contract file at {contract_path} is not a JSON object (got {type(data).__name__})." - ) - return data - - -def main(argv: list[str] | None = None) -> int: - args = _parse_args(list(argv) if argv is not None else sys.argv[1:]) - - state_root = Path(args.state_root) if args.state_root else Path.cwd() / ".egg-state" - contract_path = _contract_path(state_root, args.pipeline_id) - - try: - contract = _read_contract(contract_path) - except (FileNotFoundError, RuntimeError) as exc: # fmt: skip - print(f"read_status.py: {exc}", file=sys.stderr) - return 1 - - envelope = contract.get("pending_hitl") - if not isinstance(envelope, dict): - # No envelope yet → print empty and exit 0; the skill's case - # statement treats this as "no decision pending" and re-invokes - # the driver. - print("") - return 0 - - value = envelope.get(args.field) - if value is None: - print("") - else: - print(str(value)) - return 0 - - -if __name__ == "__main__": # pragma: no cover - sys.exit(main()) diff --git a/plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py b/plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py deleted file mode 100755 index 8b1e715f13..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/bin/run_pipeline.py +++ /dev/null @@ -1,607 +0,0 @@ -#!/usr/bin/env python3 -"""Flattened single-yield stage driver for the egg-sdlc skill (#2717 TASK-1-1). - -The skill cannot drive a long-lived Python generator across multiple -``AskUserQuestion`` round-trips — every ``python3`` subprocess from a -Bash skill step exits between yields, killing generator state and -background threads. Per cq-1 = Option C (hybrid), the refine and plan -phases use this **flattened** bridge: each invocation advances -``run_pipeline_in_process`` to its next yield, serialises the yielded -``HITLDecision`` to the contract's ``pending_hitl`` envelope, and -exits. The skill body in ``SKILL.md`` calls this driver in a loop; -between calls it renders ``pending_hitl.decision`` via ``AskUserQuestion`` -and writes the operator's answer to ``pending_hitl.answer``. - -Slice-3's daemon variant (``orchestrator/substrate/claude_code/hitl_daemon.py``, -TASK-3-2) consumes the SAME ``pending_hitl`` envelope schema so the two -bridges share a state-serialization contract (risk_analyst R17 -mitigation). - -``pending_hitl`` envelope schema (STABLE contract — slice-3 daemon -inherits this shape; do NOT change field names/types without bumping -``version``): - - pending_hitl: { - version: int, # schema version (currently 1) - pipeline_id: str, # echoes contract.pipeline_id for sanity - timestamp: str, # ISO-8601 UTC timestamp of last write - decision: dict | None, # the most recently yielded HITLDecision - # (serialised via .model_dump(mode="json") - # when pydantic; otherwise dict()) — None - # when the generator has not yielded yet - answer: Any | None, # the operator's response to ``decision``, - # written by the skill body before - # invoking the driver again. The driver - # consumes it via ``generator.send(answer)`` - # then clears it back to None. - status: str, # one of: - # "pending" — decision waiting for answer - # "answered" — answer written, awaiting send - # "completed" — generator returned (StopIteration) - # "aborted" — operator aborted at HITL - # "error" — driver hit an internal error - result: str | None, # generator return value when status==completed - # (the refine artifact path, typically) - error: str | None, # diagnostic message when status==error - } - -The ``status`` field is the skill's loop predicate: when it reads -``answered`` it knows there is an answer to ferry; when it reads -``pending`` it knows to render the decision; when it reads -``completed`` or ``error`` it exits the loop. - -Generator state across invocations ----------------------------------- -Each ``python3 bin/run_pipeline.py`` invocation is a fresh process. -Generator frames cannot persist across processes — that's the design -trade-off accepted for the flattened bridge (cq-1 Option C). To -resume across invocations, this driver replays the operator's -answers in order on every call: it reads ``pending_hitl.answer_log`` -(a list appended once per answered yield) and feeds them back into a -fresh generator one at a time, then yields the *next* decision back -to the caller. - -This works because ``run_pipeline_in_process`` is deterministic — the -same ``(pipeline_id, repo, issue_number, issue_body)`` inputs combined -with the same answer sequence reach the same yield boundary. For the -walking-skeleton phases (refine + plan) this is exact; the -implement phase has too many concurrent yields for replay to be -practical, which is why slice-3 ships the daemon variant instead. - -Exit codes ----------- - -* ``0`` — generator yielded (decision written, status pending) or - completed cleanly (status completed/aborted). -* ``1`` — driver hit an internal error (status error, error message - written to ``pending_hitl.error``). -""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -import traceback -from datetime import UTC, datetime -from pathlib import Path -from typing import Any - -#: Schema version of the ``pending_hitl`` envelope. Bump if you change -#: field names/types so the slice-3 daemon variant can refuse -#: incompatible envelopes rather than silently mis-reading. -PENDING_HITL_SCHEMA_VERSION = 1 - -#: Top-of-file marker tests use to detect this driver was actually -#: invoked (vs. a stale process from an earlier invocation). -DRIVER_INVOKED_MARKER = "egg-sdlc run_pipeline driver invoked" - - -def _now_iso() -> str: - """Return an ISO-8601 UTC timestamp string.""" - return datetime.now(UTC).isoformat() - - -def _ensure_contracts_dir(state_root: Path) -> Path: - """Make sure ``.egg-state/contracts/`` exists and return the path.""" - contracts = state_root / "contracts" - contracts.mkdir(parents=True, exist_ok=True) - return contracts - - -def _contract_path(state_root: Path, pipeline_id: str) -> Path: - contracts = _ensure_contracts_dir(state_root) - return contracts / f"{pipeline_id}.json" - - -def _read_contract(contract_path: Path, pipeline_id: str) -> dict[str, Any]: - """Read the contract file, returning a default skeleton ONLY when absent. - - A missing file is a routine first-invocation state (no decisions - persisted yet) and is handled silently. A *present-but-unparseable* - file is NOT silently overwritten: an OSError / JSONDecodeError / - non-object payload re-raises so the caller can persist an ``error`` - envelope rather than discarding ``answer_log`` and re-prompting the - operator from scratch. - """ - default: dict[str, Any] = { - "schemaVersion": "1.1", - "pipeline_id": pipeline_id, - "current_phase": "refine", - "decisions": [], - } - if not contract_path.exists(): - return default - try: - data = json.loads(contract_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: # fmt: skip - raise RuntimeError( - f"contract file at {contract_path} is unparseable: {exc}. " - "Refusing to overwrite — the operator's accumulated " - "answer_log would be silently dropped. Inspect / repair " - "the contract file by hand, or delete it to start fresh." - ) from exc - if not isinstance(data, dict): - raise RuntimeError( - f"contract file at {contract_path} is not a JSON object " - f"(got {type(data).__name__}); refusing to overwrite." - ) - data.setdefault("pipeline_id", pipeline_id) - return data - - -def _write_contract(contract_path: Path, contract: dict[str, Any]) -> None: - """Atomically write the contract file via temp + os.replace. - - Same shape as ``_InProcessOrchestrator._write_pending_decision`` so - concurrent readers never observe a half-written file. - """ - tmp = contract_path.with_suffix(".json.tmp") - tmp.write_text(json.dumps(contract, indent=2), encoding="utf-8") - os.replace(tmp, contract_path) - - -def _serialise_decision(decision: Any) -> dict[str, Any] | None: - """Best-effort decision → dict. - - Accepts pydantic ``HITLDecision`` (via ``.model_dump``), dataclass- - like objects (via ``__dict__``), bare dicts, or anything else (the - ``repr`` fallback ensures the shape is at least observable). - - The ``HITLDecision`` pydantic shape is the only expected input; if - its ``model_dump(mode="json")`` raises we log loudly to stderr (the - operator wants to know — a malformed envelope leaves - ``AskUserQuestion`` with no ``question`` / ``options`` to render - and the skill loop wedges silently otherwise). - """ - if decision is None: - return None - if isinstance(decision, dict): - return dict(decision) - model_dump = getattr(decision, "model_dump", None) - if callable(model_dump): - try: - dumped = model_dump(mode="json") - if isinstance(dumped, dict): - return dumped - print( - "run_pipeline.py: _serialise_decision: model_dump returned " - f"{type(dumped).__name__} (expected dict); falling back to __dict__.", - file=sys.stderr, - ) - except (TypeError, ValueError) as exc: # fmt: skip - print( - "run_pipeline.py: _serialise_decision: model_dump(mode='json') " - f"raised {type(exc).__name__}: {exc}; falling back to __dict__. " - "The pending_hitl envelope may not render correctly via " - "AskUserQuestion — investigate the HITLDecision shape.", - file=sys.stderr, - ) - # Fall back to __dict__ for dataclasses / simple objects. - raw = getattr(decision, "__dict__", None) - if isinstance(raw, dict): - return {k: v for k, v in raw.items() if not k.startswith("_")} - print( - "run_pipeline.py: _serialise_decision: no model_dump / __dict__ " - f"available on {type(decision).__name__}; persisting repr only. " - "The skill body will not be able to render this decision.", - file=sys.stderr, - ) - return {"repr": repr(decision)} - - -def _new_envelope( - pipeline_id: str, - *, - status: str = "pending", - decision: dict[str, Any] | None = None, - answer: Any = None, - result: str | None = None, - error: str | None = None, - answer_log: list[Any] | None = None, -) -> dict[str, Any]: - """Construct a ``pending_hitl`` envelope with all stable fields.""" - return { - "version": PENDING_HITL_SCHEMA_VERSION, - "pipeline_id": pipeline_id, - "timestamp": _now_iso(), - "decision": decision, - "answer": answer, - "status": status, - "result": result, - "error": error, - "answer_log": list(answer_log) if answer_log is not None else [], - } - - -def _coerce_envelope(raw: Any, pipeline_id: str) -> dict[str, Any]: - """Validate / upgrade a stored envelope. - - Tolerates older shapes (missing ``answer_log``, missing ``version``) - by defaulting them; rejects shapes whose ``version`` is newer than - we understand by raising ``ValueError`` so the slice-3 daemon - cannot accidentally consume a future-version envelope as if it were - v1. - """ - if not isinstance(raw, dict): - return _new_envelope(pipeline_id) - version = raw.get("version", PENDING_HITL_SCHEMA_VERSION) - if isinstance(version, int) and version > PENDING_HITL_SCHEMA_VERSION: - raise ValueError( - f"pending_hitl envelope version {version} is newer than this " - f"driver supports (max {PENDING_HITL_SCHEMA_VERSION}); upgrade " - "the skill / driver to match the orchestrator." - ) - return _new_envelope( - pipeline_id, - status=str(raw.get("status") or "pending"), - decision=raw.get("decision") if isinstance(raw.get("decision"), dict) else None, - answer=raw.get("answer"), - result=raw.get("result") if isinstance(raw.get("result"), str) else None, - error=raw.get("error") if isinstance(raw.get("error"), str) else None, - answer_log=list(raw.get("answer_log") or []), - ) - - -def _persist_envelope( - contract_path: Path, - contract: dict[str, Any], - envelope: dict[str, Any], -) -> None: - """Write ``pending_hitl`` back to the contract and flush atomically.""" - envelope["timestamp"] = _now_iso() - contract["pending_hitl"] = envelope - _write_contract(contract_path, contract) - - -def _import_runner() -> Any: - """Lazy import of ``run_pipeline_in_process``. - - The orchestrator package may not be importable in every smoke test - environment; surface the import error as a structured ``error`` - envelope rather than a stack trace to stderr. - """ - try: - from orchestrator.substrate.in_process import run_pipeline_in_process - except ImportError as exc: - raise RuntimeError( - "orchestrator.substrate.in_process.run_pipeline_in_process is not " - f"importable: {exc}. Run `python3 bin/preflight.py` for install " - "instructions." - ) from exc - return run_pipeline_in_process - - -def _advance_generator( - runner: Any, - *, - pipeline_id: str, - repo: str | None, - issue_number: int | None, - issue_body: str | None, - state_root: Path, - answer_log: list[Any], -) -> tuple[dict[str, Any] | None, str, str | None, Any]: - """Drive a fresh generator forward, replaying ``answer_log``. - - Returns ``(decision_dict_or_None, status, result_or_None, next_answer)``. - The fresh generator is closed before this function returns; its - background threads are joined cleanly via ``GeneratorExit`` - discipline implemented in ``_InProcessOrchestrator``. - - Algorithm: - 1. Start the generator and call ``next()`` to land on the first yield. - 2. For each previously-collected answer in ``answer_log``, call - ``generator.send(answer)`` — this lands on the next yield. - 3. The "next yield" after replay is the new decision the caller - should render. Persist it and exit. - 4. If the generator returns instead of yielding, persist the - result as ``completed``. - """ - effective_env = { - **os.environ, - "EGG_SUBSTRATE": os.environ.get("EGG_SUBSTRATE", "claude-code"), - } - generator = runner( - pipeline_id, - repo=repo, - issue_number=issue_number, - issue_body=issue_body, - env=effective_env, - state_dir=state_root, - ) - - next_answer: Any = None - try: - try: - # Stage 0: land on first yield. - decision = next(generator) - except StopIteration as stop: - # Generator returned before yielding — exceedingly rare but - # treat as a completed run. - return None, "completed", _stopiter_value(stop), None - - # Replay each previously-collected answer in order. If we run - # out of decisions before consuming the full answer_log, the - # operator answered more times than the generator yielded — - # truncate quietly so the loop converges. (The skill body is - # expected to maintain answer_log invariants but a defensive - # truncate avoids a hard error.) - for replay in answer_log: - try: - decision = generator.send(replay) - except StopIteration as stop: - return None, "completed", _stopiter_value(stop), None - - # ``decision`` now holds the next-to-show HITL decision. - return _serialise_decision(decision), "pending", None, next_answer - finally: - # Always close cleanly — GeneratorExit joins the background - # threads inside _InProcessOrchestrator's ``finally`` block. - # If teardown itself raises (e.g. _teardown_worktrees hits an - # OSError), the orchestrator's own ``finally`` already - # suppresses; we add a single stderr line here so the failure - # is at least observable to an operator running the driver - # with ``2>>driver.log``. The driver still returns success - # because the generator's primary work (advancing to the next - # yield) already succeeded. - try: - generator.close() - except Exception as close_exc: # noqa: BLE001 — defensive - print( - "run_pipeline.py: generator.close() raised " - f"{type(close_exc).__name__}: {close_exc}; worktree may be " - "leaked. Inspect ~/.egg-worktrees/ or EGG_WORKTREE_BASE for " - "orphaned per-role checkouts.", - file=sys.stderr, - ) - - -def _stopiter_value(stop: StopIteration) -> str | None: - """Extract the generator's return value from a StopIteration. - - ``run_pipeline_in_process`` returns the refine artifact path as a - string when the operator completes the gate (or a diagnostic - message on abort, per ``_PreflightAborted``). - """ - value = getattr(stop, "value", None) - if value is None: - return None - return str(value) - - -def _is_aborted_status(answer: Any) -> bool: - """Match the orchestrator's abort-detection logic for the answer - field. We re-check here so the envelope's ``status`` is informative - (``aborted`` vs ``completed``) when the generator stops on - operator-abort. - - The abort vocabulary lives at - ``orchestrator.substrate.in_process.ABORT_ANSWERS`` (single source - of truth shared with ``_answer_is_abort`` in the orchestrator and - the slice-3 daemon variant). We import lazily so the driver's - import-time error path still hits the structured "preflight failed" - message rather than a cascading ImportError. - """ - if answer is None: - return False - if isinstance(answer, dict): - answer = answer.get("selected") or answer.get("value") - if not isinstance(answer, str): - return False - try: - from orchestrator.substrate.in_process import ABORT_ANSWERS - except ImportError: - # Fall back to the literal set — only reached when the - # orchestrator package is not importable, in which case the - # driver's main() has already failed and we're computing this - # for an envelope that won't be observed anyway. - return answer.lower() in {"abort", "stop", "cancel"} - return answer.lower() in ABORT_ANSWERS - - -def _parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - prog="run_pipeline.py", - description=( - "Advance the in-process orchestrator generator to its next " - "HITL yield and persist the yielded decision to the " - "contract's pending_hitl envelope." - ), - ) - parser.add_argument( - "pipeline_id", - help="Pipeline identifier (e.g. 'issue-1234'). Used to locate " - "the contract under .egg-state/contracts/<id>.json.", - ) - parser.add_argument( - "--repo", - default=None, - help="Optional repo identifier ('owner/name'). Defaults to EGG_REPO from the env if unset.", - ) - parser.add_argument( - "--issue-number", - type=int, - default=None, - help="Optional GitHub issue number; only used for artifact labelling.", - ) - parser.add_argument( - "--issue-body", - default=None, - help="Optional refiner task body. Defaults to reading " - ".egg-state/drafts/<id>-issue.md when set inside the generator.", - ) - parser.add_argument( - "--state-root", - default=None, - help="Override the .egg-state/ root (defaults to <cwd>/.egg-state).", - ) - parser.add_argument( - "--daemon", - action="store_true", - help=( - "Reserved for slice-3 (TASK-3-2): connect to / launch the " - "long-lived hitl_daemon for implement-phase rather than " - "running the flattened single-yield path. Today this flag " - "is unimplemented and exits with a structured error so the " - "skill can fall back to the flattened path." - ), - ) - return parser.parse_args(argv) - - -def main(argv: list[str] | None = None) -> int: - """Driver entry point. See module docstring for the lifecycle.""" - args = _parse_args(list(argv) if argv is not None else sys.argv[1:]) - - # The marker exists so tests can scrape stdout/stderr and verify the - # process actually ran (vs. a stale envelope). - print(DRIVER_INVOKED_MARKER, file=sys.stderr) - - pipeline_id = str(args.pipeline_id).strip() - if not pipeline_id: - print("run_pipeline.py: pipeline_id must be non-empty", file=sys.stderr) - return 1 - - state_root = Path(args.state_root) if args.state_root else Path.cwd() / ".egg-state" - contract_path = _contract_path(state_root, pipeline_id) - try: - contract = _read_contract(contract_path, pipeline_id) - except RuntimeError as exc: - # Contract present but unparseable. Surface loudly rather than - # silently overwriting with a fresh skeleton — the operator's - # accumulated answer_log would otherwise be dropped, the skill - # would re-prompt from preflight, and there would be no signal - # of the corruption. - print(f"run_pipeline.py: {exc}", file=sys.stderr) - return 1 - - # Coerce any pre-existing envelope; if absent, create an empty one. - try: - envelope = _coerce_envelope(contract.get("pending_hitl"), pipeline_id) - except ValueError as exc: - envelope = _new_envelope(pipeline_id, status="error", error=f"envelope_coerce: {exc}") - _persist_envelope(contract_path, contract, envelope) - print(f"run_pipeline.py: {exc}", file=sys.stderr) - return 1 - - # ``--daemon`` is reserved for slice-3 (TASK-3-2). Today it short- - # circuits with a structured error so the skill body sees a clean - # signal it should fall back to the flattened path. - if args.daemon: - envelope = _new_envelope( - pipeline_id, - status="error", - error=( - "daemon mode is reserved for slice-3 (TASK-3-2) " - "(orchestrator/substrate/claude_code/hitl_daemon.py); " - "fall back to the flattened single-yield path for " - "refine/plan phases." - ), - ) - _persist_envelope(contract_path, contract, envelope) - print(envelope["error"], file=sys.stderr) - return 1 - - # If the skill body wrote an answer since the last call, append it - # to the answer_log so the next replay picks it up. The skill body - # writes ``answer`` (and leaves ``status`` at ``answered``); we - # promote it into ``answer_log`` here. - pending_answer = envelope.get("answer") - if envelope.get("status") == "answered" and pending_answer is not None: - envelope["answer_log"].append(pending_answer) - envelope["answer"] = None - - # Repo / issue defaults — pick up from env when the caller didn't - # pass them on the CLI. - repo = args.repo or os.environ.get("EGG_REPO") or os.environ.get("EGG_PIPELINE_REPO") - issue_number = args.issue_number - if issue_number is None: - env_issue = os.environ.get("EGG_ISSUE_NUMBER") - if env_issue and env_issue.isdigit(): - issue_number = int(env_issue) - - try: - runner = _import_runner() - except RuntimeError as exc: - envelope = _new_envelope( - pipeline_id, status="error", error=str(exc), answer_log=envelope["answer_log"] - ) - _persist_envelope(contract_path, contract, envelope) - print(str(exc), file=sys.stderr) - return 1 - - try: - decision_dict, status, result, _ = _advance_generator( - runner, - pipeline_id=pipeline_id, - repo=repo, - issue_number=issue_number, - issue_body=args.issue_body, - state_root=state_root, - answer_log=list(envelope["answer_log"]), - ) - except Exception as exc: # noqa: BLE001 — driver-level failure - trace = traceback.format_exc(limit=8) - envelope = _new_envelope( - pipeline_id, - status="error", - error=f"{type(exc).__name__}: {exc}\n{trace}", - answer_log=envelope["answer_log"], - ) - _persist_envelope(contract_path, contract, envelope) - print(f"run_pipeline.py: {type(exc).__name__}: {exc}", file=sys.stderr) - return 1 - - # Translate completed-but-aborted answers into ``status == aborted`` - # for skill-body observability. The orchestrator's _PreflightAborted - # path returns a diagnostic string and surfaces it through StopIteration. - if status == "completed" and envelope["answer_log"]: - last_answer = envelope["answer_log"][-1] - if _is_aborted_status(last_answer): - status = "aborted" - - envelope = _new_envelope( - pipeline_id, - status=status, - decision=decision_dict, - answer=None, - result=result, - answer_log=envelope["answer_log"], - ) - _persist_envelope(contract_path, contract, envelope) - - # Print a brief human-readable status line so the skill body has - # something to log without parsing the JSON file. - print( - f"run_pipeline.py: status={status} pipeline_id={pipeline_id} " - f"decision={'set' if decision_dict else 'none'} " - f"answers_replayed={len(envelope['answer_log'])}", - file=sys.stderr, - ) - return 0 - - -if __name__ == "__main__": # pragma: no cover - sys.exit(main()) diff --git a/plugins/egg-sdlc/skills/egg-sdlc/bin/write_answer.py b/plugins/egg-sdlc/skills/egg-sdlc/bin/write_answer.py deleted file mode 100644 index 460cf82dcf..0000000000 --- a/plugins/egg-sdlc/skills/egg-sdlc/bin/write_answer.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python3 -"""Write the operator's answer into ``pending_hitl`` atomically. - -This helper replaces the inline ``python3 -c "..."`` write that the -skill loop documented in earlier slices of #2717. Doing this in a -dedicated script lets the skill's ``allowed-tools`` scope ``python3`` -to the ``bin/*`` directory (so a prompt-injected issue body cannot -coerce the skill into running arbitrary Python) and makes the load- -bearing answer-write read-reviewable next to ``run_pipeline.py``. - -The helper reads the operator's answer in one of three shapes: - -* ``--answer-stdin`` — reads a JSON-encoded payload from stdin. -* ``--answer-json`` — JSON-encoded literal on the CLI. -* ``--answer-string`` — raw (un-encoded) string from the CLI. The - helper assigns it to ``pending_hitl.answer`` as-is and ``json.dumps`` - encodes it when the contract dict is serialised in - ``_write_contract_atomically`` (so there is no separate - ``json.dumps(answer)`` step — the round-trip through the contract - serializer is what proves the special-characters case in - ``test_answer_string_special_characters``). Use this when the skill - body passes the operator's selection straight from - ``AskUserQuestion``; it removes the need for a separate - ``python3 -c '…json.dumps…'`` subcommand in the loop (so the - skill's ``allowed-tools`` can fence ``python3`` to ``bin/*`` and - stay consistent with the documented loop body). - -Then the helper: - -1. Loads ``.egg-state/contracts/<pipeline-id>.json`` (raising loudly - on parse errors rather than silently overwriting with a default - skeleton — losing ``answer_log`` would silently re-prompt the - operator). -2. Decodes the JSON answer (so shell quoting can never mis-encode an - answer string like ``approve`` into a Python ``NameError``). -3. Sets ``pending_hitl.answer`` and ``pending_hitl.status = "answered"``. -4. Refreshes ``pending_hitl.timestamp`` to ``datetime.now(UTC).isoformat()`` - (no trailing ``Z`` — matches ``run_pipeline.py``'s ``_now_iso`` so - the two surfaces never drift). -5. Writes the contract atomically via tmp + ``os.replace`` (mirrors - ``run_pipeline.py``'s ``_write_contract`` so the file is never - observed half-written). - -Exit codes: - -* ``0`` — answer written, contract flushed atomically. -* ``1`` — argument / JSON-decode error, missing contract, or - unwritable target. Diagnostics go to stderr. -""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -from datetime import UTC, datetime -from pathlib import Path -from typing import Any - - -def _now_iso() -> str: - """Return an ISO-8601 UTC timestamp string. - - Matches ``run_pipeline.py:_now_iso`` so the two writers produce - identical timestamp formats. - """ - return datetime.now(UTC).isoformat() - - -def _parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - prog="write_answer.py", - description=( - "Write the operator's answer into the pending_hitl envelope " - "atomically. Companion to run_pipeline.py." - ), - ) - parser.add_argument( - "--pipeline-id", - required=True, - help="Pipeline identifier (e.g. 'issue-1234').", - ) - parser.add_argument( - "--state-root", - default=None, - help="Override the .egg-state/ root (defaults to <cwd>/.egg-state).", - ) - src = parser.add_mutually_exclusive_group(required=True) - src.add_argument( - "--answer-stdin", - action="store_true", - help="Read the JSON-encoded answer from stdin.", - ) - src.add_argument( - "--answer-json", - default=None, - help="JSON-encoded answer literal (e.g. '\"approve\"' or 'null').", - ) - src.add_argument( - "--answer-string", - default=None, - help=( - "Raw (un-encoded) answer string; the helper JSON-encodes it " - "internally. Use this when the skill body passes the operator's " - "AskUserQuestion selection directly — no separate " - "json.dumps subcommand needed." - ), - ) - return parser.parse_args(argv) - - -def _load_answer(args: argparse.Namespace) -> Any: - if args.answer_string is not None: - # The skill passes the raw operator selection as a Python str; - # ``json.dumps(contract, …)`` at write time encodes it into the - # contract file, so no separate ``python3 -c 'json.dumps(...)'`` - # subcommand is needed in the loop. The shell-special-characters - # test (``test_answer_string_special_characters``) is what pins - # this round-trip end-to-end. - return args.answer_string - raw = sys.stdin.read() if args.answer_stdin else args.answer_json - if raw is None or raw == "": - raise ValueError( - "answer payload is empty; pass JSON via stdin or --answer-json, " - "or the raw selection via --answer-string" - ) - try: - return json.loads(raw) - except json.JSONDecodeError as exc: - raise ValueError( - f"answer payload is not valid JSON: {exc}. Use --answer-string " - "to pass the raw operator selection (the helper will JSON-encode " - "it internally), or supply a JSON-encoded payload to " - "--answer-stdin / --answer-json." - ) from exc - - -def _contract_path(state_root: Path, pipeline_id: str) -> Path: - return state_root / "contracts" / f"{pipeline_id}.json" - - -def _read_contract(contract_path: Path) -> dict[str, Any]: - if not contract_path.exists(): - raise FileNotFoundError( - f"contract file does not exist at {contract_path}; run " - "run_pipeline.py first to materialise the pending_hitl envelope." - ) - try: - data = json.loads(contract_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: # fmt: skip - raise RuntimeError( - f"contract file at {contract_path} is unparseable: {exc}. Refusing " - "to overwrite — the operator's accumulated answer_log would be " - "silently dropped. Inspect / repair the contract file by hand." - ) from exc - if not isinstance(data, dict): - raise RuntimeError( - f"contract file at {contract_path} is not a JSON object " - f"(got {type(data).__name__}); refusing to overwrite." - ) - return data - - -def _write_contract_atomically(contract_path: Path, contract: dict[str, Any]) -> None: - """Match run_pipeline.py's _write_contract: tmp + os.replace.""" - tmp = contract_path.with_suffix(".json.tmp") - tmp.write_text(json.dumps(contract, indent=2), encoding="utf-8") - os.replace(tmp, contract_path) - - -def main(argv: list[str] | None = None) -> int: - args = _parse_args(list(argv) if argv is not None else sys.argv[1:]) - - try: - answer = _load_answer(args) - except ValueError as exc: - print(f"write_answer.py: {exc}", file=sys.stderr) - return 1 - - state_root = Path(args.state_root) if args.state_root else Path.cwd() / ".egg-state" - contract_path = _contract_path(state_root, args.pipeline_id) - - try: - contract = _read_contract(contract_path) - except (FileNotFoundError, RuntimeError) as exc: # fmt: skip - print(f"write_answer.py: {exc}", file=sys.stderr) - return 1 - - envelope = contract.get("pending_hitl") - if not isinstance(envelope, dict): - print( - f"write_answer.py: contract at {contract_path} has no pending_hitl " - "envelope to write into. Run run_pipeline.py first.", - file=sys.stderr, - ) - return 1 - - envelope["answer"] = answer - envelope["status"] = "answered" - envelope["timestamp"] = _now_iso() - contract["pending_hitl"] = envelope - - try: - _write_contract_atomically(contract_path, contract) - except OSError as exc: - print( - f"write_answer.py: failed to write contract at {contract_path}: {exc}", - file=sys.stderr, - ) - return 1 - - print( - f"write_answer.py: status=answered pipeline_id={args.pipeline_id} " - f"answer_type={type(answer).__name__}", - file=sys.stderr, - ) - return 0 - - -if __name__ == "__main__": # pragma: no cover - sys.exit(main()) diff --git a/shared/tests/test_claude_code_spawner.py b/shared/tests/test_claude_code_spawner.py deleted file mode 100644 index 3a2a1e3759..0000000000 --- a/shared/tests/test_claude_code_spawner.py +++ /dev/null @@ -1,249 +0,0 @@ -"""Tests for ``ClaudeCodeSpawner`` (#2623 slice-1 task-1-2, task-1-8). - -Acceptance criteria covered: - -* ``ClaudeCodeSpawner`` conforms to the ``AgentSpawner`` Protocol - (``isinstance`` check via ``@runtime_checkable``). -* ``spawn(...)`` returns an ``AgentResult`` whose ``commit_sha`` field - is populated when the worktree contains a git checkout. -* ``build_system_prompt`` (``shared/egg_harness/prompt.py:24``) is - invoked with the role's ``PromptSource`` list — the depth-gap fix - from #2622 is structural here, not delegated to the runner. -""" - -from __future__ import annotations - -import subprocess -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock, patch - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -spawner_mod = pytest.importorskip( - "orchestrator.substrate.claude_code.spawner", - reason="orchestrator/substrate/claude_code/spawner.py not present yet", -) - - -def _git_available() -> bool: - """Return ``True`` when ``git init`` works in this environment.""" - try: - out = subprocess.run( - ["git", "--version"], - capture_output=True, - text=True, - timeout=5, - check=False, - ) - except OSError: - return False - return out.returncode == 0 - - -def _init_git_repo_or_skip(path: Path) -> str | None: - """Initialise a one-commit repo at ``path`` or skip if blocked.""" - try: - proc = subprocess.run( - ["git", "init", "-q", "-b", "main"], - cwd=path, - capture_output=True, - text=True, - timeout=10, - check=False, - ) - except OSError as exc: - pytest.skip(f"git unavailable: {exc}") - if proc.returncode != 0: - pytest.skip(f"git init blocked in this container: {proc.stderr.strip() or proc.stdout!r}") - for args in ( - ["config", "user.email", "test@example.com"], - ["config", "user.name", "test"], - ): - subprocess.run(["git", *args], cwd=path, check=True, capture_output=True) - (path / "README.md").write_text("seed\n") - subprocess.run(["git", "add", "."], cwd=path, check=True, capture_output=True) - subprocess.run( - ["git", "-c", "commit.gpgsign=false", "commit", "-q", "-m", "init"], - cwd=path, - check=True, - capture_output=True, - ) - return subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=path, - capture_output=True, - text=True, - check=True, - ).stdout.strip() - - -@pytest.fixture -def fake_role() -> Any: - """Return a stand-in object with the AgentRole interface (``.value``).""" - - class _Role: - value = "refiner" - - def __str__(self) -> str: # pragma: no cover - return self.value - - return _Role() - - -# --------------------------------------------------------------------------- -# Protocol conformance -# --------------------------------------------------------------------------- - - -def test_claude_code_spawner_conforms_to_agent_spawner_protocol() -> None: - """``isinstance(spawner, AgentSpawner)`` succeeds (cq-4 AC).""" - AgentSpawner = substrate_pkg.AgentSpawner - ClaudeCodeSpawner = spawner_mod.ClaudeCodeSpawner - spawner = ClaudeCodeSpawner() - assert isinstance(spawner, AgentSpawner), ( - "ClaudeCodeSpawner must satisfy AgentSpawner Protocol (cq-4 / task-1-2 AC)" - ) - - -def test_claude_code_spawner_returns_agent_result( - tmp_path: Path, - fake_role: Any, -) -> None: - """``spawn`` returns an ``AgentResult`` even when the harness is mocked.""" - ClaudeCodeSpawner = spawner_mod.ClaudeCodeSpawner - AgentResult = substrate_pkg.AgentResult - runner = MagicMock(return_value=MagicMock(stdout="ok", returncode=0)) - spawner = ClaudeCodeSpawner(run_agent_fn=runner) - # No git init — commit_sha falls back to None, which is allowed. - result = spawner.spawn(fake_role, "hello", {"X": "1"}, tmp_path) - assert isinstance(result, AgentResult) - assert result.stdout == "ok" - assert result.exit_code == 0 - runner.assert_called_once() - - -def test_claude_code_spawner_captures_commit_sha_from_worktree( - tmp_path: Path, - fake_role: Any, -) -> None: - """When the worktree is a git checkout, ``commit_sha`` is the 40-char HEAD.""" - sha = _init_git_repo_or_skip(tmp_path) - if sha is None: - pytest.skip("git init blocked") - ClaudeCodeSpawner = spawner_mod.ClaudeCodeSpawner - runner = MagicMock(return_value=MagicMock(stdout="ok", returncode=0)) - spawner = ClaudeCodeSpawner(run_agent_fn=runner) - result = spawner.spawn(fake_role, "hello", {}, tmp_path) - assert result.commit_sha == sha, ( - f"ClaudeCodeSpawner must capture the worktree HEAD; got " - f"{result.commit_sha!r} expected {sha!r}" - ) - - -def test_claude_code_spawner_commit_sha_is_full_40_char_hex( - tmp_path: Path, - fake_role: Any, -) -> None: - """The captured commit_sha must be the full 40-char hex SHA, not a short prefix.""" - sha = _init_git_repo_or_skip(tmp_path) - if sha is None: - pytest.skip("git init blocked") - ClaudeCodeSpawner = spawner_mod.ClaudeCodeSpawner - runner = MagicMock(return_value=MagicMock(stdout="", returncode=0)) - spawner = ClaudeCodeSpawner(run_agent_fn=runner) - result = spawner.spawn(fake_role, "x", {}, tmp_path) - assert result.commit_sha is not None - assert len(result.commit_sha) == 40, ( - f"commit_sha must be 40-char SHA; got len={len(result.commit_sha)}" - ) - assert all(c in "0123456789abcdef" for c in result.commit_sha.lower()) - - -def test_claude_code_spawner_commit_sha_none_when_no_worktree( - tmp_path: Path, - fake_role: Any, -) -> None: - """commit_sha is None when the worktree path doesn't exist or is empty.""" - ClaudeCodeSpawner = spawner_mod.ClaudeCodeSpawner - runner = MagicMock(return_value=MagicMock(stdout="", returncode=0)) - spawner = ClaudeCodeSpawner(run_agent_fn=runner) - non_repo = tmp_path / "empty" - non_repo.mkdir() - result = spawner.spawn(fake_role, "x", {}, non_repo) - assert result.commit_sha is None - - -# --------------------------------------------------------------------------- -# build_system_prompt is invoked with the role's PromptSource list (#2622 fix) -# --------------------------------------------------------------------------- - - -def test_spawn_invokes_build_system_prompt( - tmp_path: Path, - fake_role: Any, -) -> None: - """``build_system_prompt`` is called by the spawner — structural depth fix. - - The depth-gap fix from #2622 lives in build_system_prompt; the - spawner is expected to route through it so the role rubric is - assembled the same way the existing harness does. Patch the - spawner module's binding (it imports the symbol) and assert the - call. - """ - ClaudeCodeSpawner = spawner_mod.ClaudeCodeSpawner - runner = MagicMock(return_value=MagicMock(stdout="", returncode=0)) - - with patch.object( - spawner_mod, - "build_system_prompt", - wraps=spawner_mod.build_system_prompt, - ) as wrapped: - spawner = ClaudeCodeSpawner( - run_agent_fn=runner, - role_rubric_loader=lambda role: f"rubric for {role.value}", - ) - spawner.spawn(fake_role, "task body", {}, tmp_path) - assert wrapped.called, ( - "ClaudeCodeSpawner.spawn must route prompt assembly through " - "shared/egg_harness/prompt.py::build_system_prompt (#2622 " - "structural depth fix)" - ) - # The call must have been passed a list whose first element is the - # role rubric — i.e. the spawner does inject the role rubric. - call_args = wrapped.call_args - sources = call_args.args[0] if call_args.args else call_args.kwargs.get("sources") - assert sources, "build_system_prompt called with empty sources" - assert "refiner" in (sources[0] if isinstance(sources[0], str) else sources[0]()) - - -# --------------------------------------------------------------------------- -# Spawner sets EGG_AGENT_ROLE / EGG_WORKTREE_ROOT on the subagent env -# --------------------------------------------------------------------------- - - -def test_spawn_threads_egg_agent_role_and_worktree_root_into_env( - tmp_path: Path, - fake_role: Any, -) -> None: - """The spawner injects ``EGG_AGENT_ROLE`` + ``EGG_WORKTREE_ROOT`` into env. - - The PreToolUseHookPolicy reads ``EGG_AGENT_ROLE`` to decide - whether to enforce write restrictions; without it the hook - fail-opens. The spawner must therefore ensure the env carries it. - """ - ClaudeCodeSpawner = spawner_mod.ClaudeCodeSpawner - runner = MagicMock(return_value=MagicMock(stdout="", returncode=0)) - spawner = ClaudeCodeSpawner(run_agent_fn=runner) - spawner.spawn(fake_role, "task", {"CALLER_X": "y"}, tmp_path) - # The runner is the harness shim; the spawner wraps its caller's - # env with the canonical keys. - call_kwargs = runner.call_args.kwargs - env = call_kwargs.get("env", {}) - assert env.get("EGG_AGENT_ROLE") == "refiner" - assert env.get("EGG_WORKTREE_ROOT") == str(tmp_path) - assert env.get("CALLER_X") == "y", "Caller env vars must be preserved" diff --git a/shared/tests/test_in_process_message_bus.py b/shared/tests/test_in_process_message_bus.py deleted file mode 100644 index f59b770f47..0000000000 --- a/shared/tests/test_in_process_message_bus.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Tests for ``InProcessMessageBus`` (#2623 slice-1 task-1-3, task-1-8). - -Acceptance criteria covered: - -* ``InProcessMessageBus`` conforms to the ``MessageBus`` Protocol. -* Round-trip via ``add_message`` / ``get_messages`` works on the bus. -* INV-3 (stale-version ACK / NACK rejection) is preserved when the bus - is used as the transport behind a ``PeerConsensusTracker``. Oracle: - ``orchestrator/tests/test_brc_open_nacks_barrier.py::TestStaleVersionRejection``. -* INV-5 (open-NACK aggregation barrier from #2142) is preserved when - the bus is used as the transport. Oracle: - ``orchestrator/tests/test_brc_open_nacks_barrier.py::TestOpenNacksBarrier``. - -The bus is documented to subclass ``MessageStore`` so the invariants -are not enforced by the bus itself — they're enforced by the -orchestrator state machine the bus carries messages for. This test -therefore (a) exercises the bus directly with ``Message`` objects to -prove the transport works, and (b) verifies the bus subclasses -``MessageStore`` (the production invariant-aware store) so the -invariants are preserved structurally. -""" - -from __future__ import annotations - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -bus_mod = pytest.importorskip( - "orchestrator.substrate.claude_code.message_bus", - reason="orchestrator/substrate/claude_code/message_bus.py not present yet", -) -message_store_mod = pytest.importorskip( - "orchestrator.message_store", - reason="orchestrator.message_store not importable", -) - - -# --------------------------------------------------------------------------- -# Protocol conformance + round-trip -# --------------------------------------------------------------------------- - - -def test_in_process_bus_satisfies_protocol() -> None: - """``isinstance(bus, MessageBus)`` succeeds.""" - MessageBus = substrate_pkg.MessageBus - InProcessMessageBus = bus_mod.InProcessMessageBus - bus = InProcessMessageBus() - assert isinstance(bus, MessageBus) - - -def test_in_process_bus_subclasses_message_store() -> None: - """The bus is a subclass of ``MessageStore``. - - Structural guarantee that the BRC invariants enforced via the - production tracker (``PeerConsensusTracker``) are preserved - unchanged — both substrate legs use the same store class for - storage. - """ - MessageStore = message_store_mod.MessageStore - InProcessMessageBus = bus_mod.InProcessMessageBus - assert issubclass(InProcessMessageBus, MessageStore) - - -def test_in_process_bus_add_get_messages_round_trip() -> None: - """Adding then reading messages returns the same payload.""" - InProcessMessageBus = bus_mod.InProcessMessageBus - Message = message_store_mod.Message - bus = InProcessMessageBus() - msg = Message( - pipeline_id="pipeline-deadbeef", - from_role="tester", - to_role="all", - message_type="STATUS", - body="hi", - ) - bus.add_message(msg) - msgs = bus.get_messages("pipeline-deadbeef") - assert msgs, "added message must be visible via get_messages" - assert any(m.from_role == "tester" for m in msgs) - - -def test_in_process_bus_isolates_pipelines() -> None: - """Messages for one pipeline_id do not leak into another pipeline's view.""" - InProcessMessageBus = bus_mod.InProcessMessageBus - Message = message_store_mod.Message - bus = InProcessMessageBus() - bus.add_message( - Message( - pipeline_id="pipeline-aaaa", - from_role="tester", - to_role="all", - message_type="STATUS", - ) - ) - other = bus.get_messages("pipeline-bbbb") - assert other == [], "Message bus must isolate messages by pipeline_id (no cross-talk)" - - -# --------------------------------------------------------------------------- -# INV-3 + INV-5 oracle scenarios via PeerConsensusTracker over the bus -# --------------------------------------------------------------------------- - - -@pytest.fixture -def tracker(): - """Three-reviewer tracker — sufficient to exercise multi-NACK (INV-5).""" - pc = pytest.importorskip("orchestrator.peer_consensus") - rg = pytest.importorskip("orchestrator.review_graph") - graph = rg.ReviewGraph( - [ - rg.ReviewEdge("reviewer_code", "coder", rg.ReviewCriticality.CRITICAL), - rg.ReviewEdge("reviewer_security", "coder", rg.ReviewCriticality.CRITICAL), - rg.ReviewEdge("reviewer_contract", "coder", rg.ReviewCriticality.CRITICAL), - ] - ) - t = pc.PeerConsensusTracker("pipeline-substrate-bus-test", graph, cooldown_seconds=0) - t.register_agent("coder") - t.register_agent("reviewer_code") - t.register_agent("reviewer_security") - t.register_agent("reviewer_contract") - return t - - -def _propose(tracker, label: str) -> None: - tracker.handle_propose( - "coder", - { - "summary": ( - f"Proposal {label}: substantive enough text to pass the " - f"≥50 char content gate enforced by _validate_brc_content." - ), - "artifacts": ["a.py"], - "commit_sha": "abc1234", - }, - ) - - -def _nack(tracker, reviewer: str, label: str) -> None: - tracker.handle_nack( - reviewer, - "coder", - { - "artifact_references": ["a.py"], - "reason": ( - f"{label}: blocking issue text long enough to satisfy the " - f"≥50 char content gate enforced by _validate_brc_content." - ), - }, - ) - - -def _re_propose(tracker, label: str) -> dict: - return tracker.handle_re_propose( - "coder", - { - "summary": ( - f"Re-propose {label}: fixed blockers; substantive enough " - f"text to pass the ≥50 char content gate." - ), - "artifacts": ["a.py"], - "commit_sha": "abc5678", - }, - changed_artifacts=["a.py"], - ) - - -def test_inv3_stale_ack_rejected_by_tracker_alongside_bus(tracker) -> None: - """A reviewer ACK at a stale version is rejected (INV-3). - - Reviewer v1 blocker #9 — the earlier name - ("when_bus_used_as_transport") oversold the test. INV-3 lives in - ``PeerConsensusTracker``, not in the bus; the bus only ferries - messages. The test fixture constructs a fresh tracker (which - happens to share its in-memory ``MessageStore`` lineage with - ``InProcessMessageBus``), but a regression in - ``InProcessMessageBus.add_message`` / ``get_messages`` would - not break this test. The pin's value is that the tracker the - in-process substrate relies on still enforces the invariant. - - Oracle: - ``test_brc_open_nacks_barrier::TestStaleVersionRejection:: - test_ack_against_stale_version_raises``. - """ - _propose(tracker, "v1") - _nack(tracker, "reviewer_code", "blocker on a.py:42") - _nack(tracker, "reviewer_security", "blocker on a.py:99") - # Re-propose now blocked by open-NACK barrier; verify by tolling - # the barrier with a second attempt. - assert _re_propose(tracker, "v2-attempt")["status"] == "open_nacks_blocked" - # An ACK against v1 (now stale because tracker may have advanced) - # raises a stale-version error. ``handle_ack`` raises a - # ``RuntimeError`` / ``ValueError`` depending on the propagation - # path; ``Exception`` is broad on purpose so we don't pin the - # specific class (it's adjacent to BRC's error vocabulary, which - # evolves under #2142 follow-ups). - with pytest.raises(Exception, match="version|stale|out of date|mismatch"): # noqa: B017, BLE001 - tracker.handle_ack( - "reviewer_code", - "coder", - {"artifact_references": ["a.py"], "reason": "looks good now"}, - ack_version=1, - ) - - -def test_inv5_multi_reviewer_open_nack_barrier_preserved(tracker) -> None: - """Re-propose with ≥2 unresolved NACKs is rejected ``open_nacks_blocked``. - - Reviewer v1 blocker #9 — naming honesty: like the INV-3 test - above, the invariant lives in ``PeerConsensusTracker``, not in - ``InProcessMessageBus``. The test pins the tracker behavior the - substrate relies on; it does not exercise the bus's transport - surface. - - Oracle: - ``test_brc_open_nacks_barrier::TestOpenNacksBarrier:: - test_multi_reviewer_nack_first_re_propose_blocked``. - """ - _propose(tracker, "v1") - _nack(tracker, "reviewer_code", "blocking issue in a.py:42") - _nack(tracker, "reviewer_security", "blocking issue in a.py:99") - result = _re_propose(tracker, "v2") - assert result["status"] == "open_nacks_blocked", ( - f"Multi-reviewer re-propose must hit the open-NACK barrier; got {result.get('status')!r}" - ) diff --git a/shared/tests/test_k3s_spawner_adapter.py b/shared/tests/test_k3s_spawner_adapter.py deleted file mode 100644 index be32a76c23..0000000000 --- a/shared/tests/test_k3s_spawner_adapter.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Tests for ``K3sSpawnerAdapter`` (#2623 slice-1 task-1-1, task-1-8). - -Acceptance criteria covered: - -* ``K3sSpawnerAdapter`` conforms to the ``AgentSpawner`` Protocol. -* The adapter delegates to a callable shaped like - ``orchestrator/kubernetes_spawner.py:1564 create_concurrent_spawn_fn`` - (signature ``(role, branch, extra_env, command) -> SpawnedContainer``). -* ``spawn`` captures ``commit_sha`` via ``git -C <worktree> rev-parse - HEAD`` after the wrapped closure returns and returns it on the - ``AgentResult`` (INV-6). -""" - -from __future__ import annotations - -import subprocess -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -adapter_mod = pytest.importorskip( - "orchestrator.substrate.k3s_adapter", - reason="orchestrator/substrate/k3s_adapter.py not present yet", -) - - -def _init_git_repo_or_skip(path: Path) -> str | None: - try: - proc = subprocess.run( - ["git", "init", "-q", "-b", "main"], - cwd=path, - capture_output=True, - text=True, - timeout=10, - check=False, - ) - except OSError as exc: - pytest.skip(f"git unavailable: {exc}") - if proc.returncode != 0: - pytest.skip(f"git init blocked in this container: {proc.stderr.strip() or proc.stdout!r}") - for args in ( - ["config", "user.email", "test@example.com"], - ["config", "user.name", "test"], - ): - subprocess.run(["git", *args], cwd=path, check=True, capture_output=True) - (path / "README.md").write_text("seed\n") - subprocess.run(["git", "add", "."], cwd=path, check=True, capture_output=True) - subprocess.run( - ["git", "-c", "commit.gpgsign=false", "commit", "-q", "-m", "init"], - cwd=path, - check=True, - capture_output=True, - ) - return subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=path, - capture_output=True, - text=True, - check=True, - ).stdout.strip() - - -@pytest.fixture -def fake_role() -> Any: - class _Role: - value = "coder" - - def __str__(self) -> str: # pragma: no cover - return self.value - - return _Role() - - -# --------------------------------------------------------------------------- -# Protocol conformance -# --------------------------------------------------------------------------- - - -def test_k3s_spawner_adapter_satisfies_protocol() -> None: - """``isinstance(adapter, AgentSpawner)`` succeeds.""" - AgentSpawner = substrate_pkg.AgentSpawner - K3sSpawnerAdapter = adapter_mod.K3sSpawnerAdapter - adapter = K3sSpawnerAdapter(MagicMock()) - assert isinstance(adapter, AgentSpawner), ( - "K3sSpawnerAdapter must satisfy AgentSpawner Protocol (cq-4 / task-1-1 AC)" - ) - - -# --------------------------------------------------------------------------- -# Delegates to create_concurrent_spawn_fn-shaped closure -# --------------------------------------------------------------------------- - - -def test_adapter_invokes_wrapped_closure_with_role_and_env( - tmp_path: Path, - fake_role: Any, -) -> None: - """The adapter routes ``role`` + ``env`` into the wrapped closure.""" - K3sSpawnerAdapter = adapter_mod.K3sSpawnerAdapter - spawned = MagicMock(stdout="ok", exit_code=0) - closure = MagicMock(return_value=spawned) - adapter = K3sSpawnerAdapter(closure) - adapter.spawn(fake_role, "task body", {"EGG_PIPELINE_ID": "pipeline-test"}, tmp_path) - assert closure.called - call_kwargs = closure.call_args.kwargs - assert call_kwargs.get("role") is fake_role - extra_env = call_kwargs.get("extra_env") or {} - assert extra_env.get("EGG_PIPELINE_ID") == "pipeline-test" - - -def test_adapter_returns_agent_result_with_legacy_fields( - tmp_path: Path, - fake_role: Any, -) -> None: - """``spawn`` returns an ``AgentResult`` populated from the legacy container.""" - AgentResult = substrate_pkg.AgentResult - K3sSpawnerAdapter = adapter_mod.K3sSpawnerAdapter - spawned = MagicMock(stdout="container stdout", exit_code=0) - closure = MagicMock(return_value=spawned) - adapter = K3sSpawnerAdapter(closure) - result = adapter.spawn(fake_role, "task", {}, tmp_path) - assert isinstance(result, AgentResult) - assert result.stdout == "container stdout" - assert result.exit_code == 0 - assert result.duration_seconds >= 0.0 - - -# --------------------------------------------------------------------------- -# commit_sha captured from worktree (INV-6) -# --------------------------------------------------------------------------- - - -def test_adapter_returns_none_commit_sha_because_legacy_factory_is_fire_and_monitor( - tmp_path: Path, - fake_role: Any, -) -> None: - """v3 ``K3sSpawnerAdapter`` returns ``commit_sha=None`` by contract. - - Pinned per reviewer_concurrency v1 (coder) blocker #4: the legacy - ``create_concurrent_spawn_fn`` is fire-and-monitor — it returns - before the pod commits — so capturing the orchestrator-host HEAD - at spawn time attached BRC commit-bound ACKs to the wrong SHA. - v3 deliberately removed the racy capture; the adapter now returns - ``commit_sha=None`` and points callers at the gateway-side - attestation channel referenced in the substrate ADR's follow-up - appendix. This test pins the deliberate-None contract so a - future re-introduction of the racy capture turns the test red. - - Companion: ``test_adapter_commit_sha_none_when_worktree_missing`` - covers the same value on a different code path (no worktree dir). - """ - # _init_git_repo_or_skip would normally seed a checkout, but the - # adapter's contract is "commit_sha=None regardless of worktree - # contents" so we use it only to confirm git is available; the - # assertion holds independently. - _ = _init_git_repo_or_skip(tmp_path) - K3sSpawnerAdapter = adapter_mod.K3sSpawnerAdapter - closure = MagicMock(return_value=MagicMock(stdout="", exit_code=0)) - adapter = K3sSpawnerAdapter(closure) - result = adapter.spawn(fake_role, "x", {}, tmp_path) - assert result.commit_sha is None, ( - "K3sSpawnerAdapter must return commit_sha=None — the legacy " - "factory is fire-and-monitor, so the pre-spawn HEAD does not " - "match the post-spawn commit. INV-6 SHAs for the k3s leg are " - "delivered via the out-of-band gateway attestation channel " - "(reviewer_concurrency v1 (coder) blocker #4)." - ) - - -def test_adapter_commit_sha_none_when_worktree_missing( - tmp_path: Path, - fake_role: Any, -) -> None: - """``commit_sha`` is None when the worktree path doesn't exist.""" - K3sSpawnerAdapter = adapter_mod.K3sSpawnerAdapter - closure = MagicMock(return_value=MagicMock(stdout="", exit_code=0)) - adapter = K3sSpawnerAdapter(closure) - nonexistent = tmp_path / "nope" - result = adapter.spawn(fake_role, "x", {}, nonexistent) - assert result.commit_sha is None diff --git a/shared/tests/test_local_worktree_manager.py b/shared/tests/test_local_worktree_manager.py deleted file mode 100644 index 1d7173d543..0000000000 --- a/shared/tests/test_local_worktree_manager.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Tests for ``LocalWorktreeManager`` (#2623 slice-1 task-1-5, task-1-8). - -Acceptance criteria covered: - -* ``LocalWorktreeManager`` exposes ``create(pipeline_id, role)`` and - ``tear_down(pipeline_id)``. -* Path-escape inputs (``..``, ``/absolute``, embedded ``..`` segments, - null bytes, empty strings) are rejected. Oracle: - ``gateway/worktree_manager.py:88 validate_identifier`` and - ``:1711`` ``is_relative_to`` guard. -* Worktrees are rooted under ``<base>/<pipeline_id>/<role>/``; - ``EGG_WORKTREE_BASE`` overrides the default. -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -worktree_mod = pytest.importorskip( - "orchestrator.substrate.claude_code.worktree", - reason="orchestrator/substrate/claude_code/worktree.py not present yet", -) - - -@pytest.fixture -def fake_role() -> Any: - class _Role: - value = "refiner" - - def __str__(self) -> str: # pragma: no cover - return self.value - - return _Role() - - -# --------------------------------------------------------------------------- -# Construction + base resolution -# --------------------------------------------------------------------------- - - -def test_constructor_explicit_base_overrides_env( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """The constructor's explicit ``base`` argument wins over ``EGG_WORKTREE_BASE``.""" - monkeypatch.setenv("EGG_WORKTREE_BASE", "/tmp/some-env-override") - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager(base=tmp_path) - assert mgr.base == tmp_path - - -def test_constructor_uses_env_when_no_explicit_base( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """``EGG_WORKTREE_BASE`` is honored when no explicit base is passed.""" - monkeypatch.setenv("EGG_WORKTREE_BASE", str(tmp_path)) - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager() - assert mgr.base == tmp_path - - -# --------------------------------------------------------------------------- -# Path-escape rejection -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "bad_identifier,why", - [ - ("..", "naked dotdot"), - ("../escape", "leading dotdot"), - ("foo/../bar", "embedded dotdot"), - ("/absolute", "absolute path"), - ("with\x00null", "null byte"), - ("", "empty"), - (".starts-with-dot", "leading dot"), - ("with space", "contains space"), - ("with$dollar", "contains dollar"), - ], -) -def test_create_rejects_path_escape_pipeline_id( - bad_identifier: str, - why: str, - tmp_path: Path, - fake_role: Any, -) -> None: - """Path-escape pipeline_ids raise ``ValueError`` before any filesystem op.""" - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager(base=tmp_path) - with pytest.raises(ValueError) as excinfo: - mgr.create(bad_identifier, fake_role) - # Must mention pipeline_id in the error message — the gateway's - # validate_identifier shape. - assert "pipeline_id" in str(excinfo.value) or "Invalid" in str(excinfo.value), ( - f"{why}: error message should reference pipeline_id; got {excinfo.value!r}" - ) - - -# --------------------------------------------------------------------------- -# Worktree rooted under base/<pipeline_id>/<role>/ -# --------------------------------------------------------------------------- - - -def test_create_places_worktree_under_base_pipeline_role(tmp_path: Path, fake_role: Any) -> None: - """Created worktree is at ``<base>/<pipeline_id>/<role>/`` per cq-5.""" - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager(base=tmp_path) - out = mgr.create("pipeline-2623", fake_role) - expected = (tmp_path / "pipeline-2623" / "refiner").resolve() - assert out == expected, f"worktree at unexpected path: {out!r} vs {expected!r}" - assert out.exists() - assert out.is_dir() - - -def test_create_pipeline_isolates_roles( - tmp_path: Path, - fake_role: Any, -) -> None: - """Two roles in the same pipeline land in distinct subdirs.""" - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager(base=tmp_path) - refiner_path = mgr.create("pipeline-x", fake_role) - - class _OtherRole: - value = "coder" - - def __str__(self) -> str: # pragma: no cover - return self.value - - coder_path = mgr.create("pipeline-x", _OtherRole()) - assert refiner_path != coder_path - assert refiner_path.parent == coder_path.parent # both under <base>/pipeline-x/ - - -# --------------------------------------------------------------------------- -# Tear-down only deletes paths under the base -# --------------------------------------------------------------------------- - - -def test_tear_down_does_not_remove_paths_outside_base( - tmp_path: Path, fake_role: Any, monkeypatch: pytest.MonkeyPatch -) -> None: - """``tear_down`` refuses to remove any path that resolves outside ``base``. - - Mirrors ``gateway/worktree_manager.py:1711`` is_relative_to guard. - """ - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager(base=tmp_path) - # Plant a guardrail target *outside* the base. The manager tracks - # only paths it creates, so the tear-down's path-escape guard only - # triggers when an entry escapes after tracking — simulate that. - outside = tmp_path.parent / "outside-canary" - outside.mkdir(exist_ok=True) - canary = outside / "do-not-delete" - canary.write_text("canary") - # Inject a tracked entry that resolves outside base. - mgr._tracked["pipeline-evil"] = [(outside, "egg/pipeline-evil/refiner")] # type: ignore[attr-defined] - mgr.tear_down("pipeline-evil") - assert canary.exists(), ( - "tear_down must NOT remove paths outside the configured base " - "(gateway/worktree_manager.py:1711 is_relative_to guard)" - ) - - -def test_tear_down_removes_created_worktree( - tmp_path: Path, - fake_role: Any, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """``tear_down`` removes a previously-created worktree under the base.""" - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager(base=tmp_path) - path = mgr.create("pipeline-clean", fake_role) - assert path.exists() - mgr.tear_down("pipeline-clean") - assert not path.exists(), "tear_down must remove the created worktree dir" - - -# --------------------------------------------------------------------------- -# Tear-down validates pipeline_id input -# --------------------------------------------------------------------------- - - -def test_tear_down_rejects_path_escape_pipeline_id(tmp_path: Path) -> None: - """``tear_down`` rejects path-escape pipeline_ids the same way ``create`` does.""" - LocalWorktreeManager = worktree_mod.LocalWorktreeManager - mgr = LocalWorktreeManager(base=tmp_path) - with pytest.raises(ValueError): - mgr.tear_down("..") diff --git a/shared/tests/test_pretooluse_hook_policy.py b/shared/tests/test_pretooluse_hook_policy.py deleted file mode 100644 index 3b181e7076..0000000000 --- a/shared/tests/test_pretooluse_hook_policy.py +++ /dev/null @@ -1,639 +0,0 @@ -"""Tests for ``PreToolUseHookPolicy`` (#2623 slice-1 task-1-4, task-1-8). - -Acceptance criteria covered: - -* ``PreToolUseHookPolicy.check_write(role, path)`` denies out-of-role - writes — match behavior with - ``gateway/phase_filter.py:1061 check_agent_restrictions``. -* The accompanying ``hook_entry.decide`` function returns - ``{"decision": "block", "reason": ...}`` for blocked tool calls, - ``{}`` for allowed. -* The hook entry script reads JSON on stdin and prints JSON on stdout - per the Claude Code PreToolUse hook protocol. -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -policy_mod = pytest.importorskip( - "orchestrator.substrate.claude_code.policy", - reason="orchestrator/substrate/claude_code/policy.py not present yet", -) -hook_entry_mod = pytest.importorskip( - "orchestrator.substrate.claude_code.hook_entry", - reason="orchestrator/substrate/claude_code/hook_entry.py not present yet", -) - - -# --------------------------------------------------------------------------- -# check_write — in-process enforcement -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "role,blocked_path,note", - [ - # Tester cannot write source files. - ("tester", "orchestrator/concurrent_executor.py", "tester→source"), - # Coder cannot write docs. - ("coder", "docs/architecture/claude-code-substrate.md", "coder→docs"), - # Documenter cannot write source. - ("documenter", "orchestrator/concurrent_executor.py", "documenter→source"), - ], -) -def test_check_write_denies_out_of_role_writes(role: str, blocked_path: str, note: str) -> None: - """``check_write`` returns ``(False, reason)`` for blocked role+path combos.""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - allowed, reason = policy.check_write(role, blocked_path) - assert allowed is False, f"{note}: expected denial; got allowed=True" - assert reason, f"{note}: denial must carry a non-empty reason" - - -@pytest.mark.parametrize( - "role,allowed_path", - [ - ("tester", "shared/tests/test_substrate_interfaces.py"), - ("coder", "orchestrator/substrate/spawner.py"), - ("documenter", "docs/architecture/claude-code-substrate.md"), - ], -) -def test_check_write_allows_in_role_writes(role: str, allowed_path: str) -> None: - """``check_write`` returns ``(True, None)`` for in-role writes.""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - allowed, reason = policy.check_write(role, allowed_path) - assert allowed is True, f"{role}→{allowed_path} expected allowed; got reason={reason!r}" - assert reason is None - - -def test_check_write_no_role_allows_everything() -> None: - """When ``role`` is empty the policy fail-opens.""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - allowed, reason = policy.check_write("", "orchestrator/anything.py") - assert allowed is True - assert reason is None - - -# --------------------------------------------------------------------------- -# decide() — hook-shape contract (block vs allow) -# --------------------------------------------------------------------------- - - -def test_decide_blocks_out_of_role_write(monkeypatch: pytest.MonkeyPatch) -> None: - """``hook_entry.decide`` returns ``{"decision": "block", ...}`` on denial.""" - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - monkeypatch.setenv("EGG_REPO_ROOT", "/home/egg/repos/egg") - payload = { - "tool_name": "Write", - "tool_input": { - "file_path": "/home/egg/repos/egg/orchestrator/concurrent_executor.py", - "content": "hi", - }, - } - result = hook_entry_mod.decide(payload) - assert result.get("decision") == "block", ( - f"hook_entry.decide must block tester→source write; got {result!r}" - ) - assert "reason" in result and result["reason"] - - -def test_decide_allows_in_role_write(monkeypatch: pytest.MonkeyPatch) -> None: - """``hook_entry.decide`` returns ``{}`` for in-role writes (allow).""" - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - monkeypatch.setenv("EGG_REPO_ROOT", "/home/egg/repos/egg") - payload = { - "tool_name": "Write", - "tool_input": { - "file_path": ("/home/egg/repos/egg/shared/tests/test_substrate_interfaces.py"), - "content": "import pytest", - }, - } - result = hook_entry_mod.decide(payload) - assert result == {} or "decision" not in result, ( - f"hook_entry.decide must allow tester→test-file; got {result!r}" - ) - - -def test_decide_fail_open_when_role_not_set_outside_substrate( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Without ``EGG_AGENT_ROLE`` AND outside substrate prefixes, fail-open. - - v2 security NACK changed the semantics: when the role is unresolved - and the write target is outside the substrate-managed prefixes - (``.egg-state/``, ``.claude/``, ``.github/``, - ``shared/egg_restrictions/``), the hook still fail-opens so a plain - Claude Code session isn't affected. Inside a substrate prefix the - hook now fails closed — covered by - :func:`test_decide_fails_closed_when_role_not_set_inside_substrate`. - """ - monkeypatch.delenv("EGG_AGENT_ROLE", raising=False) - # The role resolver also reads a $HOME/.claude/egg-active-role.json - # sentinel; point HOME at a clean tmp_path so the sentinel isn't - # present. - clean_home = tmp_path / "clean-home" - clean_home.mkdir() - monkeypatch.setenv("HOME", str(clean_home)) - payload = { - "tool_name": "Write", - "tool_input": { - "file_path": "/tmp/some-non-substrate-file.txt", - "content": "...", - }, - } - result = hook_entry_mod.decide(payload) - assert result == {} or "decision" not in result, ( - f"Without EGG_AGENT_ROLE and outside substrate prefixes the hook " - f"must fail-open; got {result!r}" - ) - - -def test_decide_fails_closed_when_role_not_set_inside_substrate( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """v2 security NACK: hook fails CLOSED inside substrate-managed prefixes.""" - monkeypatch.delenv("EGG_AGENT_ROLE", raising=False) - clean_home = tmp_path / "clean-home" - clean_home.mkdir() - monkeypatch.setenv("HOME", str(clean_home)) - monkeypatch.setenv("EGG_REPO_ROOT", str(tmp_path)) - (tmp_path / ".egg-state").mkdir() - target = tmp_path / ".egg-state" / "drafts" / "analysis.md" - payload = { - "tool_name": "Write", - "tool_input": { - "file_path": str(target), - "content": "...", - }, - } - result = hook_entry_mod.decide(payload) - assert result.get("decision") == "block", ( - f"Without EGG_AGENT_ROLE the hook must fail-closed inside " - f"substrate-managed prefixes (.egg-state/); got {result!r}" - ) - - -def test_decide_ignores_pure_read_tools(monkeypatch: pytest.MonkeyPatch) -> None: - """Pure-read tools (``Read``) get no decision (allow). - - Note: ``Bash`` is NOT a read-only tool in this hook's model — the - hook inspects Bash commands for write-shaped tokens (redirection, - cp/mv/tee/sed -i/dd of=) and may block on the parsed write target. - See ``test_bash_write_extraction_blocks_out_of_role`` for the - Bash-side coverage. - """ - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - payload = { - "tool_name": "Read", - "tool_input": {"file_path": "/anything"}, - } - result = hook_entry_mod.decide(payload) - assert result == {} or "decision" not in result - - -# --------------------------------------------------------------------------- -# Hook entry script: stdin JSON → stdout JSON contract -# --------------------------------------------------------------------------- - - -def test_hook_entry_script_blocks_out_of_role_write( - tmp_path: Path, -) -> None: - """The hook entry script emits ``decision=block`` on stdout for blocked calls.""" - hook_entry = Path("/home/egg/repos/egg/orchestrator/substrate/claude_code/hook_entry.py") - if not hook_entry.exists(): - pytest.skip(f"{hook_entry} not present") - - payload = { - "tool_name": "Write", - "tool_input": { - "file_path": "/home/egg/repos/egg/orchestrator/concurrent_executor.py", - "content": "hi", - }, - } - env = dict(os.environ) - env["EGG_AGENT_ROLE"] = "tester" - env["EGG_REPO_ROOT"] = "/home/egg/repos/egg" - # The script may need to import egg_restrictions; thread the - # project root onto sys.path via PYTHONPATH. - env["PYTHONPATH"] = ( - "/home/egg/repos/egg/shared" - + os.pathsep - + "/home/egg/repos/egg/orchestrator" - + os.pathsep - + "/home/egg/repos/egg" - + os.pathsep - + env.get("PYTHONPATH", "") - ) - proc = subprocess.run( - [sys.executable, str(hook_entry)], - input=json.dumps(payload), - capture_output=True, - text=True, - check=False, - timeout=10, - env=env, - ) - # The Claude Code hook protocol carries the decision in stdout - # JSON, not the exit code. Exit code 0 is the normal "I ran" - # signal; the block decision is in stdout. - assert proc.returncode == 0, ( - f"hook script must exit 0 on normal completion; got rc={proc.returncode}, " - f"stderr={proc.stderr!r}" - ) - try: - out = json.loads(proc.stdout) - except json.JSONDecodeError as exc: - pytest.fail(f"hook script stdout must be valid JSON; got {proc.stdout!r} ({exc})") - assert out.get("decision") == "block", ( - f"hook script must emit decision=block for tester→source write; got stdout={proc.stdout!r}" - ) - - -def test_hook_entry_script_allows_in_role_write(tmp_path: Path) -> None: - """The hook entry script emits ``{}`` for in-role writes (allow).""" - hook_entry = Path("/home/egg/repos/egg/orchestrator/substrate/claude_code/hook_entry.py") - if not hook_entry.exists(): - pytest.skip(f"{hook_entry} not present") - - payload = { - "tool_name": "Write", - "tool_input": { - "file_path": ("/home/egg/repos/egg/shared/tests/test_substrate_interfaces.py"), - "content": "...", - }, - } - env = dict(os.environ) - env["EGG_AGENT_ROLE"] = "tester" - env["EGG_REPO_ROOT"] = "/home/egg/repos/egg" - env["PYTHONPATH"] = ( - "/home/egg/repos/egg/shared" - + os.pathsep - + "/home/egg/repos/egg/orchestrator" - + os.pathsep - + "/home/egg/repos/egg" - + os.pathsep - + env.get("PYTHONPATH", "") - ) - proc = subprocess.run( - [sys.executable, str(hook_entry)], - input=json.dumps(payload), - capture_output=True, - text=True, - check=False, - timeout=10, - env=env, - ) - assert proc.returncode == 0 - out = json.loads(proc.stdout) - assert out == {} or "decision" not in out, ( - f"hook script must allow tester→test-file; got stdout={proc.stdout!r}" - ) - - -# --------------------------------------------------------------------------- -# install() — settings.json templating -# --------------------------------------------------------------------------- - - -def _install_target(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Build an ``install``-acceptable target dir under a faked ``$HOME``. - - The v2 ``install`` adds a path-escape guard that refuses any target - outside ``$HOME``. Point ``$HOME`` at a tmp_path subdir so the test - exercises the happy path without polluting the real ``$HOME``. - """ - fake_home = tmp_path / "home" - fake_home.mkdir() - monkeypatch.setenv("HOME", str(fake_home)) - target = fake_home / "repo" - target.mkdir() - return target - - -def test_install_writes_settings_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """``install`` writes ``.claude/settings.json`` containing the hook.""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - target = _install_target(tmp_path, monkeypatch) - out = policy.install(target) - assert out.exists() - settings = json.loads(out.read_text()) - assert "hooks" in settings, "settings.json must include a 'hooks' block" - - -def test_install_is_idempotent(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """Re-running ``install`` does not duplicate the egg hook.""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - target = _install_target(tmp_path, monkeypatch) - out1 = policy.install(target) - first = json.loads(out1.read_text()) - policy.install(target) # second run - second = json.loads(out1.read_text()) - assert first == second, "Repeated install() calls must produce byte-identical settings.json" - - -def test_install_rejects_target_outside_home( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """v2 security guard: ``install`` refuses target_dir outside ``$HOME``.""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - fake_home = tmp_path / "home" - fake_home.mkdir() - monkeypatch.setenv("HOME", str(fake_home)) - outside = tmp_path / "outside" - outside.mkdir() - with pytest.raises(ValueError, match=r"not under \$HOME"): - policy.install(outside) - - -# --------------------------------------------------------------------------- -# v3 install fail-loud on malformed existing settings.json (reviewer_code v2) -# --------------------------------------------------------------------------- - - -def test_install_raises_on_malformed_existing_settings_json( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Malformed JSON in the existing ``.claude/settings.json`` raises loud. - - v3 fix: instead of silently overwriting the user's prior hooks / - statusline / plugin enablement, ``install`` raises ``ValueError`` - pointing the operator at the bad file with line + column from - ``json.JSONDecodeError``. - """ - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - target = _install_target(tmp_path, monkeypatch) - settings = target / ".claude" - settings.mkdir() - (settings / "settings.json").write_text("this is not json") - with pytest.raises(ValueError, match=r"not valid\s+JSON"): - policy.install(target) - - -def test_install_raises_on_non_dict_top_level( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """A JSON array at the top level is rejected (must be a JSON object).""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - target = _install_target(tmp_path, monkeypatch) - settings = target / ".claude" - settings.mkdir() - (settings / "settings.json").write_text("[]") - with pytest.raises(ValueError, match=r"not a JSON\s+object"): - policy.install(target) - - -def test_install_accepts_empty_existing_settings( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Empty / whitespace-only existing settings.json is treated as ``{}``.""" - PreToolUseHookPolicy = policy_mod.PreToolUseHookPolicy - policy = PreToolUseHookPolicy() - target = _install_target(tmp_path, monkeypatch) - settings = target / ".claude" - settings.mkdir() - (settings / "settings.json").write_text(" \n ") - out = policy.install(target) # must not raise - contents = json.loads(out.read_text()) - assert "hooks" in contents - - -# --------------------------------------------------------------------------- -# v2 security NACK #1: Bash command write-target parsing -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "command,note", - [ - ("echo hi > orchestrator/concurrent_executor.py", "redirect >"), - ("echo hi >> orchestrator/concurrent_executor.py", "append >>"), - ("cp data.txt orchestrator/concurrent_executor.py", "cp dest"), - ("mv old.py orchestrator/concurrent_executor.py", "mv dest"), - ("tee orchestrator/concurrent_executor.py", "tee target"), - ("sed -i s/a/b/ orchestrator/concurrent_executor.py", "sed -i"), - ("dd if=/dev/zero of=orchestrator/concurrent_executor.py", "dd of="), - # Reviewer v3 non-blocking: short-flag-cluster bash recursion. - # ``bash -lc`` / ``-xc`` / ``-ic`` carry the ``-c`` mode along - # with other single-char options. The hook must still recurse - # into the inner command so the inner write is surfaced. - ( - "bash -lc 'echo x > orchestrator/concurrent_executor.py'", - "bash -lc cluster", - ), - ( - "bash -xc 'echo x > orchestrator/concurrent_executor.py'", - "bash -xc cluster", - ), - ( - "sh -ic 'echo x > orchestrator/concurrent_executor.py'", - "sh -ic cluster", - ), - # Reviewer v3 non-blocking: tar long-form ``--extract`` must - # surface the ``-C`` target. - ( - "tar --extract -f archive.tar -C orchestrator/", - "tar --extract -C", - ), - # Reviewer v3 non-blocking: tar short-flag cluster ``-xzf`` - # surfaces the ``-C`` target. - ( - "tar -xzf archive.tar.gz -C orchestrator/", - "tar -xzf -C", - ), - ], -) -def test_bash_write_extraction_blocks_out_of_role( - command: str, - note: str, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Bash commands writing to source code are denied for tester role. - - Mirrors v2 security NACK #1: the hook parses Bash for redirection - + write-shaped tokens (cp / mv / tee / sed -i / dd of= / ln -s / - python -c "open(...).write(...)"). Each write-shape gets a - dedicated test so a future parser regression localizes quickly. - - Reviewer v3 non-blocking widened the parametrize set to cover the - tar long-form / short-flag-cluster extract detection and the - ``bash -lc`` / ``-xc`` / ``-ic`` combined-short-flag recursion; - those code paths were added in commit ``8ce6b28`` and previously - had no regression test. - """ - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - monkeypatch.setenv("EGG_REPO_ROOT", "/home/egg/repos/egg") - payload = {"tool_name": "Bash", "tool_input": {"command": command}} - result = hook_entry_mod.decide(payload) - assert result.get("decision") == "block", ( - f"{note}: Bash write to source must be denied for tester; command={command!r} → {result!r}" - ) - - -@pytest.mark.parametrize( - "command,note", - [ - # Reviewer v3 non-blocking: tar long-form ``--xattrs`` / - # ``--xz`` / ``--exclude=*`` are NOT extracts and must not - # trip the extract-mode false-positive that v3's - # ``_is_tar_extract`` helper closed. The commands all create - # rather than extract, so a tester running them outside an - # ``-x`` mode should not surface a phantom write target. - ("tar --xattrs -cf /tmp/out.tar /tmp/src", "tar --xattrs"), - ("tar -cf /tmp/out.tar.xz --xz /tmp/src", "tar --xz"), - ("tar --exclude=foo -cf /tmp/out.tar /tmp/src", "tar --exclude="), - ], -) -def test_bash_tar_long_form_flags_do_not_false_fire( - command: str, - note: str, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Long-form tar flags (``--xattrs``, ``--xz``, ``--exclude=*``) are NOT extracts. - - Reviewer v3 non-blocking regression: the previous greedy - ``startswith("-x")`` heuristic treated ``--xattrs`` and ``--xz`` - as extract modes and produced fail-closed false-positives. The - v3 ``_is_tar_extract`` helper now requires a single-dash cluster - (with ``x`` in the cluster) or the long form ``--extract``. - - Targets under ``/tmp/`` are outside any role's allow-list, but - the hook should not surface them as paths at all when the tar - invocation is in create mode — so ``decide()`` returns no - decision (allow-through, since the path list is empty). - """ - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - monkeypatch.setenv("EGG_REPO_ROOT", "/home/egg/repos/egg") - payload = {"tool_name": "Bash", "tool_input": {"command": command}} - result = hook_entry_mod.decide(payload) - assert result == {} or "decision" not in result, ( - f"{note}: long-form tar flag must not trip extract-mode false-positive; " - f"command={command!r} → {result!r}" - ) - - -def test_bash_redirect_inside_outer_quote_does_not_emit_phantom_path( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Reviewer v3 non-blocking: stray-quote artifacts don't leak. - - For ``bash -c 'echo x > /restricted/file'`` the ``_REDIRECT_RE`` - first-pass on the raw outer command captures ``/restricted/file'`` - (with the trailing single-quote) while the recursive ``bash -c`` - handler captures ``/restricted/file`` cleanly. Without the - unmatched-quote filter the policy checker would see both paths and - could fail-closed defensively on the phantom. The v3 filter drops - candidates with an unmatched ``'`` / ``"`` from the regex pass; the - recursive handler still surfaces the clean path. - """ - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - monkeypatch.setenv("EGG_REPO_ROOT", "/home/egg/repos/egg") - command = "bash -c 'echo x > /home/egg/repos/egg/orchestrator/concurrent_executor.py'" - paths, ambiguous = hook_entry_mod._bash_write_paths(command) - # The recursive handler must surface the inner path exactly once - # — no phantom-with-trailing-quote duplicate. - clean = "/home/egg/repos/egg/orchestrator/concurrent_executor.py" - assert paths.count(clean) == 1, ( - f"clean inner path must appear exactly once; got paths={paths!r}" - ) - assert not any(p.endswith("'") or p.endswith('"') for p in paths), ( - f"no path may carry a stray quote artefact; got paths={paths!r}" - ) - # The clean inner path is the only one we expect. - assert paths == [clean], f"expected exactly [{clean!r}]; got {paths!r}" - assert ambiguous is False - - -@pytest.mark.parametrize( - "command,note", - [ - ("cp x.txt $DEST", "shell var dest"), - ("echo hi > `pwd`/file.py", "backtick dest"), - ('python3 -c "open(\\"x.py\\", \\"w\\").write(\\"hi\\")"', "python -c"), - ], -) -def test_bash_ambiguous_command_fails_closed( - command: str, - note: str, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Ambiguous Bash commands (shell expansion, python -c) fail closed.""" - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - payload = {"tool_name": "Bash", "tool_input": {"command": command}} - result = hook_entry_mod.decide(payload) - assert result.get("decision") == "block", ( - f"{note}: ambiguous Bash command must fail closed; command={command!r} → {result!r}" - ) - - -def test_bash_read_only_command_allows_through( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Bash read-only commands (ls, cat, grep) pass the hook.""" - monkeypatch.setenv("EGG_AGENT_ROLE", "tester") - payload = { - "tool_name": "Bash", - "tool_input": {"command": "ls -la && cat README.md | grep egg"}, - } - result = hook_entry_mod.decide(payload) - assert result == {} or "decision" not in result, f"Read-only Bash must pass: {result!r}" - - -# --------------------------------------------------------------------------- -# v2 security NACK #3: malformed JSON stdin fails CLOSED -# --------------------------------------------------------------------------- - - -def test_hook_entry_script_fails_closed_on_malformed_json( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Malformed JSON on stdin returns ``decision=block``.""" - hook_entry = Path("/home/egg/repos/egg/orchestrator/substrate/claude_code/hook_entry.py") - if not hook_entry.exists(): - pytest.skip(f"{hook_entry} not present") - - env = dict(os.environ) - env["EGG_AGENT_ROLE"] = "tester" - env["EGG_REPO_ROOT"] = "/home/egg/repos/egg" - env["PYTHONPATH"] = ( - "/home/egg/repos/egg/shared" - + os.pathsep - + "/home/egg/repos/egg/orchestrator" - + os.pathsep - + "/home/egg/repos/egg" - + os.pathsep - + env.get("PYTHONPATH", "") - ) - proc = subprocess.run( - [sys.executable, str(hook_entry)], - input="{this is not json", - capture_output=True, - text=True, - check=False, - timeout=10, - env=env, - ) - out = json.loads(proc.stdout) - assert out.get("decision") == "block", ( - f"Malformed JSON stdin must fail CLOSED (security NACK #3); got stdout={proc.stdout!r}" - ) diff --git a/shared/tests/test_read_status.py b/shared/tests/test_read_status.py deleted file mode 100644 index ffd480da40..0000000000 --- a/shared/tests/test_read_status.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Unit tests for ``plugins/egg-sdlc/skills/egg-sdlc/bin/read_status.py``. - -The helper is part of the skill loop's documented surface (#2717 -slice-1): the skill body reads ``pending_hitl.status`` between driver -invocations to decide whether to render via ``AskUserQuestion``, wait, -or exit. Earlier slices used an inline ``python3 -c "..."`` snippet for -this read, but that left the skill's ``allowed-tools`` having to accept -arbitrary ``python3 -c`` invocations (prompt-injection surface). The -helper exists so each subcommand in the loop body is a single -``python3 plugins/.../bin/<helper>.py`` invocation that matches the -``allowed-tools`` pattern independently per Claude Code's compound- -command rules. - -Tests cover: - -* Reading ``status`` from a valid envelope prints the value to stdout. -* Reading ``result`` / ``error`` works identically. -* A missing envelope prints an empty string and exits 0 (the skill's - ``case`` statement falls through cleanly). -* A missing contract file exits 1 with a diagnostic on stderr. -* An unparseable contract file exits 1 — does NOT silently print the - default skeleton's value. -""" - -from __future__ import annotations - -import importlib.util -import io -import json -import sys -from pathlib import Path -from typing import Any - -import pytest - -_HELPER_PATH = ( - Path(__file__).resolve().parents[2] - / "plugins" - / "egg-sdlc" - / "skills" - / "egg-sdlc" - / "bin" - / "read_status.py" -) - - -def _load_helper_module() -> Any: - spec = importlib.util.spec_from_file_location("egg_sdlc_read_status", _HELPER_PATH) - assert spec is not None and spec.loader is not None, f"could not load {_HELPER_PATH}" - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -@pytest.fixture -def helper() -> Any: - return _load_helper_module() - - -def _seed_contract( - tmp_path: Path, - *, - pipeline_id: str = "issue-test", - status: str = "pending", - result: str | None = None, - error: str | None = None, -) -> Path: - contracts = tmp_path / ".egg-state" / "contracts" - contracts.mkdir(parents=True, exist_ok=True) - contract_path = contracts / f"{pipeline_id}.json" - contract_path.write_text( - json.dumps( - { - "pipeline_id": pipeline_id, - "pending_hitl": { - "version": 1, - "pipeline_id": pipeline_id, - "timestamp": "2025-01-01T00:00:00+00:00", - "decision": {"question": "?", "options": []}, - "answer": None, - "status": status, - "result": result, - "error": error, - "answer_log": [], - }, - } - ), - encoding="utf-8", - ) - return contract_path - - -def test_reads_status_pending( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _seed_contract(tmp_path, status="pending") - monkeypatch.chdir(tmp_path) - rc = helper.main(["--pipeline-id", "issue-test", "--field", "status"]) - assert rc == 0 - captured = capsys.readouterr() - assert captured.out.strip() == "pending" - - -def test_reads_status_completed( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _seed_contract(tmp_path, status="completed", result="/path/to/analysis.md") - monkeypatch.chdir(tmp_path) - rc = helper.main(["--pipeline-id", "issue-test", "--field", "result"]) - assert rc == 0 - captured = capsys.readouterr() - assert captured.out.strip() == "/path/to/analysis.md" - - -def test_reads_error( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _seed_contract(tmp_path, status="error", error="something went wrong") - monkeypatch.chdir(tmp_path) - rc = helper.main(["--pipeline-id", "issue-test", "--field", "error"]) - assert rc == 0 - captured = capsys.readouterr() - assert captured.out.strip() == "something went wrong" - - -def test_missing_envelope_prints_empty( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """A contract without a ``pending_hitl`` envelope is not an error. - - The skill's ``case`` statement must fall through cleanly when there's - no decision pending — printing an empty string + exit 0 is the - contract. - """ - contracts = tmp_path / ".egg-state" / "contracts" - contracts.mkdir(parents=True, exist_ok=True) - contract_path = contracts / "issue-test.json" - contract_path.write_text(json.dumps({"pipeline_id": "issue-test"})) - monkeypatch.chdir(tmp_path) - - rc = helper.main(["--pipeline-id", "issue-test", "--field", "status"]) - assert rc == 0 - captured = capsys.readouterr() - assert captured.out.strip() == "" - - -def test_missing_contract_exits_nonzero( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - monkeypatch.chdir(tmp_path) - rc = helper.main(["--pipeline-id", "issue-test", "--field", "status"]) - assert rc == 1 - captured = capsys.readouterr() - assert "does not exist" in captured.err - - -def test_unparseable_contract_exits_nonzero( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """Unparseable contract → exit 1, do NOT silently emit a default value.""" - contracts = tmp_path / ".egg-state" / "contracts" - contracts.mkdir(parents=True, exist_ok=True) - contract_path = contracts / "issue-test.json" - contract_path.write_text("{ not valid json") - monkeypatch.chdir(tmp_path) - - rc = helper.main(["--pipeline-id", "issue-test", "--field", "status"]) - assert rc == 1 - captured = capsys.readouterr() - assert "unparseable" in captured.err - - -def test_rejects_disallowed_field( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """argparse choices restrict ``--field`` to the known set.""" - _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - # ``answer`` is intentionally NOT in the allowed set — reads of the - # operator's answer field would expose a prompt-injection surface - # the helper has no reason to support. - monkeypatch.setattr(sys, "stderr", io.StringIO()) # silence argparse stderr - with pytest.raises(SystemExit) as excinfo: - helper.main(["--pipeline-id", "issue-test", "--field", "answer"]) - assert excinfo.value.code != 0 diff --git a/shared/tests/test_rubric_loader.py b/shared/tests/test_rubric_loader.py deleted file mode 100644 index 9e7335da38..0000000000 --- a/shared/tests/test_rubric_loader.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Tests for ``_load_egg_sdlc_role_rubric`` (#2717 slice-1 task-1-7). - -Acceptance criteria covered (per contract task-1-7): - -* ``_load_egg_sdlc_role_rubric(REFINER)`` returns the existing rubric markdown - (regression — must not break the spike's working refiner path). -* ``_load_egg_sdlc_role_rubric(REVIEWER_REFINE)`` returns the rubric markdown - added by task-1-4 (documenter-owned ``reviewer_refine.md``). -* ``_load_egg_sdlc_role_rubric(REVIEWER_AGENT_DESIGN)`` returns the rubric - markdown added by task-1-4 (documenter-owned ``reviewer_agent_design.md``). -* ``_load_egg_sdlc_role_rubric(ARCHITECT)`` returns the rubric markdown — - slice-2 (task-2-2 / task-2-3) extends the loader's rubric-landed set - to the plan team and ships ``architect.md``. (Slice-1 originally - expected this role to still raise with a "follow-up slice 2" hint; - the merge of slice-2's loader expansion onto slice-1 flips it to - loadable.) - -The four required cases (refiner regression / reviewer_refine / -reviewer_agent_design / architect-loads) are implemented as discrete -parametrized tests so a single failure points cleanly at one role's -loader behavior. - -Adversarial probing layered on top of the contract's required cases: - -* ``role`` accepts both ``AgentRole`` enum members and bare strings — - the loader normalises via ``role.value if hasattr(role, "value") else - str(role)`` (line ~263). Both shapes must round-trip identically. -* Loaded markdown is non-empty and includes the canonical "You are the" - preamble so a silently empty / dead file is detectable. -* Path-traversal safety: a role value containing ``..`` resolves to a - ``ValueError`` rather than reaching outside the rubric directory. - (The loader builds ``rubric_path = repo_root / "plugins" / ... / - f"{role_name}.md"``; an attacker who can supply role values cannot - escape the agents directory because ``role_name`` is appended as a - filename component.) -* Implement-phase roles (e.g. ``REVIEWER_CODE``) still raise - ``ValueError`` — slice 3 delivers those rubrics. Plan-phase roles - (``REVIEWER_PLAN``, ``TASK_PLANNER``, etc.) became loadable in - slice-2 and no longer belong in the still-deferred set. -""" - -from __future__ import annotations - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -agent_roles_mod = pytest.importorskip( - "egg_contracts.agent_roles", - reason="shared/egg_contracts/agent_roles.py not importable", -) - -AgentRole = agent_roles_mod.AgentRole -_load = substrate_pkg._load_egg_sdlc_role_rubric - - -# --------------------------------------------------------------------------- -# Required cases from task-1-7 acceptance criteria -# --------------------------------------------------------------------------- - - -def test_load_refiner_rubric_regression() -> None: - """Refiner rubric still loads — slice-1 must not regress the spike. - - The refiner is the only role the walking-skeleton spike (#2623) - shipped a rubric for. Slice 1 widens the loader to two more roles - (REVIEWER_REFINE, REVIEWER_AGENT_DESIGN) without touching this - path; this test pins the existing behaviour so a careless rewrite - of the loader does not silently drop the refiner. - """ - body = _load(AgentRole.REFINER) - assert isinstance(body, str) - assert body.strip(), "refiner rubric body must not be empty" - # The frontmatter is retained per the existing loader contract. - assert body.startswith("---"), ( - "refiner rubric must include frontmatter (loader returns the full " - "file, frontmatter included, per the docstring)" - ) - - -def test_load_reviewer_refine_rubric() -> None: - """``REVIEWER_REFINE`` rubric loads from ``reviewer_refine.md``. - - The documenter ships ``reviewer_refine.md`` under - ``plugins/egg-sdlc/skills/egg-sdlc/agents/`` (task-1-4) and the - coder removes the ``ValueError`` fence for this role from - ``_load_egg_sdlc_role_rubric`` (task-1-6). The two must compose so - a single ``_load(AgentRole.REVIEWER_REFINE)`` call returns the - rubric body. - """ - body = _load(AgentRole.REVIEWER_REFINE) - assert isinstance(body, str) - assert body.strip(), "reviewer_refine rubric body must not be empty" - # Acceptance for task-1-4 requires the body to start with a - # "You are the **reviewer_refine** running on the Claude Code - # substrate" (or analogous) preamble. - assert "reviewer_refine" in body.lower(), ( - "reviewer_refine rubric must reference its own role name in the body" - ) - - -def test_load_reviewer_agent_design_rubric() -> None: - """``REVIEWER_AGENT_DESIGN`` rubric loads from ``reviewer_agent_design.md``.""" - body = _load(AgentRole.REVIEWER_AGENT_DESIGN) - assert isinstance(body, str) - assert body.strip(), "reviewer_agent_design rubric body must not be empty" - assert "reviewer_agent_design" in body.lower(), ( - "reviewer_agent_design rubric must reference its own role name in the body" - ) - - -def test_load_architect_rubric() -> None: - """``ARCHITECT`` rubric loads from ``architect.md`` in slice-2. - - Slice-1 originally pinned this role as raising ``ValueError`` with a - "follow-up slice 2" hint (task-1-6). Slice-2 (task-2-2 / task-2-3) - extends the loader to the plan team — architect is now in the - rubric-landed set and ``architect.md`` exists on disk. - """ - body = _load(AgentRole.ARCHITECT) - assert isinstance(body, str) - assert body.strip(), "architect rubric body must not be empty" - assert "architect" in body.lower(), ( - "architect rubric must reference its own role name in the body" - ) - - -# --------------------------------------------------------------------------- -# Adversarial probing — required for the loader to be safe in production -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "role_input", - [ - # Slice-1 regression role. - pytest.param(AgentRole.REFINER, id="enum-refiner"), - pytest.param("refiner", id="str-refiner"), - # Slice-1 newly-supported roles (REVIEWER_REFINE, - # REVIEWER_AGENT_DESIGN) — pin string-input contract here so a - # future loader change that breaks the str→enum normalisation - # for the *new* roles (not just the regression role) is caught. - pytest.param(AgentRole.REVIEWER_REFINE, id="enum-reviewer_refine"), - pytest.param("reviewer_refine", id="str-reviewer_refine"), - pytest.param(AgentRole.REVIEWER_AGENT_DESIGN, id="enum-reviewer_agent_design"), - pytest.param("reviewer_agent_design", id="str-reviewer_agent_design"), - ], -) -def test_loader_accepts_enum_and_string_role(role_input: object) -> None: - """The loader normalises ``AgentRole`` enum members and bare strings. - - The implementation uses ``role.value if hasattr(role, "value") else - str(role)``. Both shapes must produce identical output — if the - enum form started silently using ``str(role)`` (which for - ``StrEnum`` returns the value, so this would still work) vs an - accidental ``repr(role)`` ("<AgentRole.REFINER: 'refiner'>"), the - file path lookup would diverge. - """ - body = _load(role_input) - assert isinstance(body, str) - assert body.strip() - - -@pytest.mark.parametrize( - "implement_phase_role", - [ - # Implement-team roles still deferred to slice 3 of the #2717 - # rollout — REVIEWER_PLAN and TASK_PLANNER shipped in slice-2 - # (task-2-2 / task-2-3) and were removed from this parameter - # list when the loader's `_RUBRIC_LANDED_ROLES` set grew to - # include the plan team. - pytest.param(AgentRole.REVIEWER_CODE, id="reviewer_code"), - ], -) -def test_loader_still_rejects_unshipped_roles(implement_phase_role: object) -> None: - """Roles whose rubrics are not yet shipped continue to raise. - - Task-1-6 description: "The loader continues to raise ``ValueError`` - for plan/implement roles until slice 2/3 adds their rubrics (this - preserves the structured-error contract for missing rubrics)." - - If a future change silently drops the fence for every role, the - walking-skeleton callers would get an empty / fallback rubric and - the spawn would degrade silently. The fence is a load-bearing - diagnostic. - """ - with pytest.raises(ValueError): - _load(implement_phase_role) - - -def test_loader_rejects_path_traversal_role_name() -> None: - """A role value containing path-traversal characters cannot escape the agents dir. - - Defence-in-depth: even though ``AgentRole`` values are constants - in ``shared/egg_contracts/agent_roles.py``, the loader accepts - string inputs via the ``str(role)`` branch. An attacker model - where a string role value reaches this loader (config injection, - deserialised contract field) must not yield arbitrary file read. - - The loader's structural defence is the ``_ROLE_RUBRIC_SLICES`` - allowlist (orchestrator/substrate/__init__.py): roles outside the - allowlist take the slice-fence branch and raise ``ValueError`` - before any ``Path.is_file()`` check happens against the - user-controlled path. The test pins both that the error fires AND - that the diagnostic identifies the role as "not part of the - rollout's rubric set" rather than "missing on disk" — the former - means the allowlist caught it, the latter would mean the loader - walked the filesystem with attacker-controlled segments. - """ - with pytest.raises(ValueError) as excinfo: - _load("../../../etc/passwd") - msg = str(excinfo.value) - # The error must still cite a missing rubric — not silently read - # the wrong file or yield an empty string. - assert "missing" in msg.lower() or "rubric" in msg.lower(), ( - f"path-traversal role must surface as missing-rubric ValueError; got: {msg!r}" - ) - # Adversarial assertion: the diagnostic must identify the role as - # not-in-rollout-set rather than as missing-on-disk. The former - # means the ``_ROLE_RUBRIC_SLICES`` allowlist intercepted before - # any filesystem walk; the latter would mean the loader reached - # ``Path(...).is_file()`` with attacker-controlled path segments - # — an information-leak vector (existence oracle on /etc/*.md). - lowered = msg.lower() - assert "not part of" in lowered or "rollout" in lowered or "rubric set" in lowered, ( - "path-traversal role must hit the allowlist's slice-fence " - f"branch (not the file-missing-on-disk branch); got: {msg!r}" - ) - - -# --------------------------------------------------------------------------- -# Rollout-DAG invariants — pin the "extend, don't replace" contract on -# ``_LANDED_SLICES`` so slice-2's author cannot accidentally regress -# slice-1 by writing ``frozenset({"slice-2"})`` instead of -# ``frozenset({"slice-1", "slice-2"})``. -# --------------------------------------------------------------------------- - - -def test_landed_slices_contains_slice1() -> None: - """``_LANDED_SLICES`` must include ``"slice-1"`` in every future slice. - - The rollout DAG (issue #2717) ships slice-1 first; later slices - EXTEND ``_LANDED_SLICES`` rather than replacing it. A regression - where slice-2's coder wrote ``frozenset({"slice-2"})`` would fence - off slice-1's already-landed refiner / reviewer_refine / - reviewer_agent_design rubrics — a silent break of the loader for - the entire refine team. The constant's docstring at - ``orchestrator/substrate/__init__.py:284-287`` calls this invariant - out in prose; this test pins it mechanically so a future-slice edit - cannot regress slice-1 without tripping a test. - """ - landed = substrate_pkg._LANDED_SLICES - assert isinstance(landed, frozenset), ( - f"_LANDED_SLICES must remain a frozenset (immutable, hashable); got {type(landed).__name__}" - ) - assert "slice-1" in landed, ( - f"_LANDED_SLICES must include 'slice-1' on every slice; " - f"got {sorted(landed)!r}. The 'extend, don't replace' invariant " - "is documented at orchestrator/substrate/__init__.py:284-287; " - "future slices add to this set, they do not replace it." - ) diff --git a/shared/tests/test_run_pipeline_in_process.py b/shared/tests/test_run_pipeline_in_process.py deleted file mode 100644 index f5e6bf83fe..0000000000 --- a/shared/tests/test_run_pipeline_in_process.py +++ /dev/null @@ -1,212 +0,0 @@ -"""Tests for ``run_pipeline_in_process`` (#2623 slice-1 task-1-6, task-1-8). - -Acceptance criteria covered (TASK-1-6 R4 acceptance bullets): - -* Raises ``NotImplementedError`` for ``EGG_SUBSTRATE=k3s``. -* Heartbeat thread keeps ticking across HITL yields — background - thread liveness during HITL pauses. -* Background threads are dropped cleanly on ``GeneratorExit`` (the - caller's ``.close()`` joins every thread). -""" - -from __future__ import annotations - -import time -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -in_process_mod = pytest.importorskip( - "orchestrator.substrate.in_process", - reason="orchestrator/substrate/in_process.py not present yet", -) - - -# --------------------------------------------------------------------------- -# AC (a): NotImplementedError for EGG_SUBSTRATE=k3s -# --------------------------------------------------------------------------- - - -def test_run_pipeline_in_process_rejects_k3s_substrate( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """``run_pipeline_in_process`` refuses ``EGG_SUBSTRATE=k3s``. - - The in-process entry point is claude-code-only for the spike. - ``EGG_SUBSTRATE=k3s`` keeps using the existing pipeline runner via - ``orchestrator.cli.cmd_serve``; the in-process entry must not - silently fall back. - """ - run = in_process_mod.run_pipeline_in_process - with pytest.raises(NotImplementedError) as excinfo: - run( - "pipeline-test", - env={"EGG_SUBSTRATE": "k3s"}, - state_dir=tmp_path / ".egg-state", - ) - # The error message should reference the substrate / follow-up - # issue so operators can route the request correctly. - msg = str(excinfo.value).lower() - assert "k3s" in msg or "claude-code" in msg or "substrate" in msg, ( - f"NotImplementedError should reference the substrate boundary; got: {excinfo.value!r}" - ) - - -# --------------------------------------------------------------------------- -# AC (b): heartbeat-thread liveness across HITL yields -# --------------------------------------------------------------------------- - - -def test_heartbeat_thread_remains_alive_across_hitl_yield( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Heartbeat thread keeps ticking while a HITL decision is pending. - - Drives the generator until it yields a ``HITLDecision``, pauses - (simulating the parent skill rendering the question), waits long - enough for ≥2 heartbeat intervals, and asserts the heartbeat - counter advanced — i.e. the background thread didn't stop while - the generator body was paused at a yield. - """ - # Shrink the heartbeat interval so the test runs in seconds, not - # the production 5s tick. - monkeypatch.setattr(in_process_mod, "_HEARTBEAT_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BRC_REVIEW_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BUS_TICK_INTERVAL", 0.05) - - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-hb-test", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - # Advance to the first HITL yield. - first = next(gen) - # First yield must be a HITLDecision-shaped object. - assert first is not None, "first yield should be a HITLDecision" - # Grab the orchestrator instance via the generator's frame to - # observe the heartbeat counter. The runner is the - # ``_InProcessOrchestrator``'s ``run()`` method; the instance - # is in ``gi_frame.f_locals["self"]``. - frame = gen.gi_frame - assert frame is not None, "generator must have a live frame after yield" - runner = frame.f_locals.get("self") - assert runner is not None - baseline = runner._heartbeat_ticks - # Sleep through several heartbeat intervals. - time.sleep(0.25) # ≥4 intervals of 0.05s - assert runner._heartbeat_ticks > baseline, ( - f"Heartbeat thread must keep ticking across HITL yields; " - f"baseline={baseline} current={runner._heartbeat_ticks}" - ) - finally: - gen.close() - - -# --------------------------------------------------------------------------- -# AC (c): background threads dropped cleanly on GeneratorExit -# --------------------------------------------------------------------------- - - -def test_background_threads_dropped_on_generator_close( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Closing the generator joins background threads cleanly. - - Snapshots ``threading.enumerate()`` before / after the generator - lifecycle and asserts every egg-inproc thread the orchestrator - started has terminated within the bounded join window. - """ - monkeypatch.setattr(in_process_mod, "_HEARTBEAT_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BRC_REVIEW_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BUS_TICK_INTERVAL", 0.05) - - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-thread-test", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - # Advance to the first yield to ensure the background threads - # have started. - next(gen) - frame = gen.gi_frame - runner = frame.f_locals.get("self") if frame else None - assert runner is not None - started_threads = list(runner._threads) - assert started_threads, "background threads should have been started" - # Close the generator — the finally block must join the threads. - gen.close() - # Give the threads a moment to wind down. - time.sleep(0.3) - leaked = [t for t in started_threads if t.is_alive()] - assert leaked == [], ( - f"GeneratorExit must drop background threads; leaked: {[t.name for t in leaked]}" - ) - - -def test_run_pipeline_in_process_returns_artifact_path_on_normal_completion( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """When the operator picks a terminal answer, the generator returns the artifact path. - - The walking-skeleton scope-fences anything past refine, but if - the operator answers the refine-gate with anything other than - ``approve_continue``, the generator terminates and returns the - artifact path string. - """ - monkeypatch.setattr(in_process_mod, "_HEARTBEAT_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BRC_REVIEW_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BUS_TICK_INTERVAL", 0.05) - - # Stub out the spawner so we don't fan into the real harness. - fake_bundle = MagicMock() - fake_bundle.spawner.spawn = MagicMock( - return_value=MagicMock( - exit_code=0, - commit_sha="0" * 40, - stdout="ok", - worktree=tmp_path / "wt", - artifacts=[], - ) - ) - fake_bundle.worktrees.create = MagicMock(return_value=tmp_path / "wt") - fake_bundle.worktrees.tear_down = MagicMock() - # reviewer_code non-blocking: wrap the select_substrate patch around - # both yields so the bundle is observed before _spawn_refiner runs. - # The previous outer ``patch.object`` was a no-op (wraps without - # substitution) and is gone. - with patch("orchestrator.substrate.select_substrate", return_value=fake_bundle): - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-end2end", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - # Advance to first HITL (preflight). - next(gen) - try: - # Send "approve" through preflight — non-abort answer. - second = gen.send("approve") - # The generator must yield the refine-gate decision next. - assert second is not None - # Send "stop" through the refine gate — non-fence terminal. - try: - gen.send("stop") - pytest.fail("Expected StopIteration on terminal answer") - except StopIteration as stop: - # The return value is the artifact path string. - assert isinstance(stop.value, str) - assert stop.value.endswith("-analysis.md") - finally: - gen.close() diff --git a/shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py b/shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py deleted file mode 100644 index faf6563f15..0000000000 --- a/shared/tests/test_run_pipeline_in_process_sentinel_and_hitl.py +++ /dev/null @@ -1,414 +0,0 @@ -"""Sentinel-lifecycle + HITL tests for ``run_pipeline_in_process`` (#2623). - -Coverage added in response to reviewer_code v1 (tester) NACK blockers -1-4 — pinning the v2/v3 operator-safety surface the coder added: - -* **Active-role sentinel (v3 fix)** — PID-stamping + PID liveness - check in ``hook_entry._resolve_active_role`` + generator - ``finally``-block teardown via ``_teardown_sentinel``. Without these - pins, a regression that re-introduces sentinel-leak silently breaks - the user's plain Claude Code session after a crashed run. - -* **Preflight HITL abort (v2 fix)** — operator's ``"abort"`` answer - short-circuits the refiner spawn via ``_PreflightAborted``. Pins - the bare-string and dict-shaped answer forms ``_answer_is_abort`` - documents. - -* **Refine-failure HITL gate (v2 fix)** — when the spawner returns - ``exit_code != 0`` the gate's question changes shape (``Refiner - FAILED ...`` + ``options=["retry","abort"]``) so the operator sees - the failure rather than approving a refine that never ran. -""" - -from __future__ import annotations - -import json -import os -import time -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock, patch - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) -in_process_mod = pytest.importorskip( - "orchestrator.substrate.in_process", - reason="orchestrator/substrate/in_process.py not present yet", -) -hook_entry_mod = pytest.importorskip( - "orchestrator.substrate.claude_code.hook_entry", - reason="orchestrator/substrate/claude_code/hook_entry.py not present yet", -) - - -@pytest.fixture -def short_intervals(monkeypatch: pytest.MonkeyPatch) -> None: - """Shrink background-thread intervals so tests run in seconds.""" - monkeypatch.setattr(in_process_mod, "_HEARTBEAT_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BRC_REVIEW_INTERVAL", 0.05) - monkeypatch.setattr(in_process_mod, "_BUS_TICK_INTERVAL", 0.05) - - -@pytest.fixture -def fake_bundle(tmp_path: Path) -> MagicMock: - """A substrate bundle that doesn't touch the real Claude Code runner.""" - bundle = MagicMock() - spawn_result = MagicMock( - exit_code=0, - commit_sha="0" * 40, - stdout="ok", - worktree=tmp_path / "wt", - artifacts=[], - ) - bundle.spawner.spawn = MagicMock(return_value=spawn_result) - bundle.worktrees.create = MagicMock(return_value=tmp_path / "wt") - bundle.worktrees.tear_down = MagicMock() - bundle.name = "claude-code" - return bundle - - -@pytest.fixture -def fake_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Point ``$HOME`` at a clean tmp dir so sentinel reads/writes are isolated.""" - home = tmp_path / "home" - home.mkdir() - monkeypatch.setenv("HOME", str(home)) - return home - - -# --------------------------------------------------------------------------- -# Sentinel — PID stamping (v3 fix) -# --------------------------------------------------------------------------- - - -def test_sentinel_is_written_with_pid( - tmp_path: Path, fake_home: Path, short_intervals: None -) -> None: - """``_write_active_role_sentinel`` writes the orchestrator's PID.""" - runner_cls = in_process_mod._InProcessOrchestrator - runner = runner_cls( - pipeline_id="pipeline-sentinel-pid", - repo=None, - issue_number=None, - issue_body="", - env={"EGG_SUBSTRATE": "claude-code"}, - state_root=tmp_path / ".egg-state", - ) - runner._write_active_role_sentinel("refiner") - sentinel = fake_home / ".claude" / "egg-active-role.json" - assert sentinel.exists(), "sentinel must be written under $HOME/.claude/" - payload = json.loads(sentinel.read_text()) - assert payload["pid"] == os.getpid() - assert payload["role"] == "refiner" - assert payload["pipeline_id"] == "pipeline-sentinel-pid" - - -def test_sentinel_teardown_unlinks_file( - tmp_path: Path, fake_home: Path, short_intervals: None -) -> None: - """``_teardown_sentinel`` removes the sentinel file if it exists.""" - runner_cls = in_process_mod._InProcessOrchestrator - runner = runner_cls( - pipeline_id="pipeline-sentinel-teardown", - repo=None, - issue_number=None, - issue_body="", - env={"EGG_SUBSTRATE": "claude-code"}, - state_root=tmp_path / ".egg-state", - ) - runner._write_active_role_sentinel("refiner") - sentinel = fake_home / ".claude" / "egg-active-role.json" - assert sentinel.exists() - runner._teardown_sentinel() - assert not sentinel.exists(), "sentinel must be unlinked by teardown" - - -def test_sentinel_teardown_silently_no_op_when_missing( - tmp_path: Path, fake_home: Path, short_intervals: None -) -> None: - """``_teardown_sentinel`` does not raise when no sentinel exists.""" - runner_cls = in_process_mod._InProcessOrchestrator - runner = runner_cls( - pipeline_id="pipeline-sentinel-missing", - repo=None, - issue_number=None, - issue_body="", - env={"EGG_SUBSTRATE": "claude-code"}, - state_root=tmp_path / ".egg-state", - ) - runner._teardown_sentinel() # must not raise - - -# --------------------------------------------------------------------------- -# Sentinel — hook PID-liveness fallback (v3 fix) -# --------------------------------------------------------------------------- - - -def test_hook_treats_dead_pid_sentinel_as_missing( - tmp_path: Path, fake_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """A sentinel whose PID is not alive is ignored — fail-closed for prefixes.""" - monkeypatch.delenv("EGG_AGENT_ROLE", raising=False) - sentinel_dir = fake_home / ".claude" - sentinel_dir.mkdir() - # Pick a PID that is extremely unlikely to be live (2^22 — well - # above /proc/sys/kernel/pid_max on most kernels). - dead_pid = 4194300 - (sentinel_dir / "egg-active-role.json").write_text( - json.dumps({"role": "refiner", "pid": dead_pid}) - ) - # Hook resolves an empty role → fail-closed for substrate prefixes. - repo_root = tmp_path / "repo" - repo_root.mkdir() - (repo_root / ".egg-state").mkdir() - monkeypatch.setenv("EGG_REPO_ROOT", str(repo_root)) - payload = { - "tool_name": "Write", - "tool_input": { - "file_path": str(repo_root / ".egg-state" / "drafts" / "x.md"), - "content": "...", - }, - } - result = hook_entry_mod.decide(payload) - assert result.get("decision") == "block", ( - f"Dead-PID sentinel must be ignored; role resolver should fall " - f"through to the substrate-prefix fail-closed default. Got " - f"{result!r}" - ) - - -def test_hook_uses_live_pid_sentinel_as_fallback( - tmp_path: Path, fake_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """A sentinel whose PID is alive resolves the role from the sentinel.""" - monkeypatch.delenv("EGG_AGENT_ROLE", raising=False) - sentinel_dir = fake_home / ".claude" - sentinel_dir.mkdir() - (sentinel_dir / "egg-active-role.json").write_text( - json.dumps({"role": "tester", "pid": os.getpid()}) - ) - role = hook_entry_mod._resolve_active_role() - assert role == "tester", f"Live-PID sentinel must resolve the role; got {role!r}" - - -def test_resolve_active_role_prefers_env_over_sentinel( - fake_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """``EGG_AGENT_ROLE`` env wins over the sentinel file.""" - sentinel_dir = fake_home / ".claude" - sentinel_dir.mkdir() - (sentinel_dir / "egg-active-role.json").write_text( - json.dumps({"role": "tester", "pid": os.getpid()}) - ) - monkeypatch.setenv("EGG_AGENT_ROLE", "coder") - role = hook_entry_mod._resolve_active_role() - assert role == "coder" - - -# --------------------------------------------------------------------------- -# Sentinel — generator cleanup paths (v3 fix) -# --------------------------------------------------------------------------- - - -def test_generator_unlinks_sentinel_on_generator_close( - tmp_path: Path, fake_home: Path, short_intervals: None -) -> None: - """Closing the generator mid-run unlinks the sentinel.""" - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-sentinel-close", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - next(gen) # advance to first yield — sentinel should exist - sentinel = fake_home / ".claude" / "egg-active-role.json" - # The sentinel may not be written until _spawn_refiner runs; we - # only assert teardown leaves nothing behind regardless of state. - gen.close() - time.sleep(0.1) - assert not sentinel.exists(), "GeneratorExit must unlink the sentinel file" - - -def test_generator_unlinks_sentinel_on_preflight_abort( - tmp_path: Path, fake_home: Path, short_intervals: None -) -> None: - """Operator's preflight abort still tears down the sentinel. - - The abort path is translated to a clean ``StopIteration`` by the - generator (reviewer v1 blocker #7 — the docstring's contract is - "clean StopIteration with a diagnostic message"). The generator's - ``finally`` block runs as part of the normal-completion path and - unlinks the sentinel. - """ - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-sentinel-abort", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - next(gen) - with pytest.raises(StopIteration) as exc_info: - gen.send("abort") - assert "aborted" in str(exc_info.value.value).lower(), ( - "StopIteration.value should carry the abort diagnostic message" - ) - sentinel = fake_home / ".claude" / "egg-active-role.json" - assert not sentinel.exists(), "Preflight abort must unlink the sentinel via the finally block" - - -# --------------------------------------------------------------------------- -# Preflight HITL abort (v2 fix) -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "answer", - ["abort", "Abort", "STOP", "cancel", {"selected": "abort"}, {"value": "stop"}], -) -def test_preflight_abort_answer_short_circuits_spawn( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - answer: Any, -) -> None: - """Operator's abort answer (bare or dict shape) skips the refiner spawn. - - The abort path is translated to a clean ``StopIteration`` by the - generator (reviewer v1 blocker #7). The contract this test pins: - "the refiner spawn does NOT run" AND the generator surfaces the - abort as a normal StopIteration carrying a diagnostic message. - """ - spawn_mock = MagicMock() - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-preflight-abort", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - next(gen) # preflight HITL - # Patch the spawn fn so we can assert it never runs. - frame = gen.gi_frame - runner = frame.f_locals.get("self") if frame else None - assert runner is not None - runner._spawn_refiner = spawn_mock - with pytest.raises(StopIteration) as exc_info: - gen.send(answer) - assert "aborted" in str(exc_info.value.value).lower() - finally: - gen.close() - assert not spawn_mock.called, ( - f"Preflight abort answer {answer!r} must NOT invoke _spawn_refiner" - ) - - -def test_preflight_non_abort_answer_proceeds_to_spawn( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - fake_bundle: MagicMock, -) -> None: - """A non-abort preflight answer proceeds to spawn the refiner.""" - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-preflight-ok", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - with patch("orchestrator.substrate.select_substrate", return_value=fake_bundle): - next(gen) - second = gen.send("approve") - # The second yield is the refine HITL gate. - assert second is not None - assert fake_bundle.spawner.spawn.called, "Non-abort answer must invoke the spawner" - finally: - gen.close() - - -def test_answer_is_abort_helper_contract() -> None: - """``_answer_is_abort`` accepts bare strings + dict-shaped answers.""" - helper = in_process_mod._answer_is_abort - assert helper("abort") is True - assert helper("Abort") is True - assert helper("STOP") is True - assert helper("cancel") is True - assert helper({"selected": "abort"}) is True - assert helper({"value": "stop"}) is True - assert helper("approve") is False - assert helper(None) is False - assert helper({"selected": "approve"}) is False - - -# --------------------------------------------------------------------------- -# Refine-failure HITL gate (v2 fix — exit_code != 0 path) -# --------------------------------------------------------------------------- - - -def test_refine_gate_says_failed_when_spawner_exit_code_nonzero( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - fake_bundle: MagicMock, -) -> None: - """Non-zero spawner exit_code surfaces a FAILURE-shaped HITL decision.""" - fake_bundle.spawner.spawn.return_value = MagicMock( - exit_code=1, - commit_sha=None, - stdout="boom: refiner crashed", - worktree=tmp_path / "wt", - artifacts=[], - ) - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-refine-failed", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - with patch("orchestrator.substrate.select_substrate", return_value=fake_bundle): - next(gen) # preflight - gate = gen.send("approve") # refine gate decision - assert gate is not None - assert "FAILED" in gate.question, ( - f"Failed-spawn gate must surface FAILED in question; got {gate.question!r}" - ) - assert list(gate.options) == ["retry", "abort"], ( - f"Failed-spawn gate must offer retry/abort; got {gate.options!r}" - ) - finally: - gen.close() - - -def test_refine_gate_says_normal_when_spawner_exit_code_zero( - tmp_path: Path, - fake_home: Path, - short_intervals: None, - fake_bundle: MagicMock, -) -> None: - """Zero exit_code surfaces the normal 4-way HITL gate decision.""" - # fake_bundle already returns exit_code=0 - run = in_process_mod.run_pipeline_in_process - gen = run( - "pipeline-refine-ok", - env={"EGG_SUBSTRATE": "claude-code"}, - state_dir=tmp_path / ".egg-state", - ) - try: - with patch("orchestrator.substrate.select_substrate", return_value=fake_bundle): - next(gen) - gate = gen.send("approve") - assert "FAILED" not in gate.question - assert "Approve and continue" in gate.question - assert list(gate.options) == [ - "approve_continue", - "request_changes", - "change_approach", - "stop", - ] - finally: - gen.close() diff --git a/shared/tests/test_substrate_interfaces.py b/shared/tests/test_substrate_interfaces.py deleted file mode 100644 index d777e773c3..0000000000 --- a/shared/tests/test_substrate_interfaces.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Substrate Protocol conformance tests (#2623 slice-1 task-1-8). - -Verifies that the four ``typing.Protocol`` interfaces pinned by -task-1-1 exist and have the expected member set, and that the -``select_substrate`` factory honors the ``EGG_SUBSTRATE`` env var -contract from cq-1 (parallel substrates, env-var-selected). -""" - -from __future__ import annotations - -import inspect -from typing import get_type_hints - -import pytest - -substrate_pkg = pytest.importorskip( - "orchestrator.substrate", - reason="orchestrator/substrate/ package not present yet", -) - - -# --------------------------------------------------------------------------- -# Protocol presence -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "proto_name", - ["AgentSpawner", "MessageBus", "PolicyEnforcer", "WorktreeManager"], -) -def test_protocol_is_defined(proto_name: str) -> None: - """All four protocols required by task-1-1 must be importable.""" - proto = getattr(substrate_pkg, proto_name, None) - assert proto is not None, f"substrate.{proto_name} not exported — task-1-1 acceptance criterion" - assert inspect.isclass(proto), f"{proto_name} must be a class" - - -# --------------------------------------------------------------------------- -# AgentSpawner.spawn signature (cq-4: synchronous spawn) -# --------------------------------------------------------------------------- - - -def test_agent_spawner_spawn_signature_matches_cq4() -> None: - """``AgentSpawner.spawn`` is synchronous, returns ``AgentResult``.""" - proto = substrate_pkg.AgentSpawner - spawn = getattr(proto, "spawn", None) - assert spawn is not None, "AgentSpawner.spawn member is required (cq-4)" - assert not inspect.iscoroutinefunction(spawn), "AgentSpawner.spawn must be synchronous per cq-4" - - -def test_agent_spawner_protocol_is_runtime_checkable() -> None: - """``isinstance(impl, AgentSpawner)`` must succeed (cq-4 unit-test AC). - - Task-1-2 acceptance criterion: "ClaudeCodeSpawner conforms to - AgentSpawner (verified by isinstance check in a unit test)". - isinstance() against a Protocol requires the Protocol to be - decorated with @runtime_checkable. - """ - proto = substrate_pkg.AgentSpawner - # ``@runtime_checkable`` adds the special ``_is_runtime_protocol`` - # attribute set to True. - assert getattr(proto, "_is_runtime_protocol", False) is True, ( - "AgentSpawner must be decorated @runtime_checkable so the " - "task-1-2 / task-1-1 isinstance-conformance check works." - ) - - -# --------------------------------------------------------------------------- -# AgentResult dataclass includes commit_sha (INV-6) -# --------------------------------------------------------------------------- - - -def test_agent_result_has_commit_sha_field() -> None: - """``AgentResult.commit_sha`` is mandatory (INV-6 — task-1-1 AC).""" - agent_result_cls = getattr(substrate_pkg, "AgentResult", None) - assert agent_result_cls is not None, "substrate.AgentResult dataclass missing — task-1-1 AC" - hints = get_type_hints(agent_result_cls) - assert "commit_sha" in hints, "AgentResult must include commit_sha: str | None (INV-6)" - - -# --------------------------------------------------------------------------- -# select_substrate factory honors EGG_SUBSTRATE (cq-1) -# --------------------------------------------------------------------------- - - -def test_select_substrate_returns_bundle_with_required_fields() -> None: - """``select_substrate`` bundles must expose all four substrate slots.""" - select_substrate = substrate_pkg.select_substrate - bundle = select_substrate({"EGG_SUBSTRATE": "claude-code"}) - for attr in ("spawner", "bus", "policy", "worktrees"): - assert hasattr(bundle, attr), f"SubstrateBundle must expose .{attr} — task-1-1 AC" - - -def test_select_substrate_defaults_to_k3s_when_env_absent() -> None: - """``select_substrate({})`` defaults to ``"k3s"`` per cq-1. - - The contract AC for task-1-1 also requires the default to return - a *working* spawner (a K3sSpawnerAdapter wrapping - create_concurrent_spawn_fn). This test pins the substrate-name - selection here; the working-spawner contract is exercised by - :func:`test_select_substrate_k3s_default_spawner_is_callable`. - """ - select_substrate = substrate_pkg.select_substrate - bundle = select_substrate({}) - assert bundle.name == "k3s", ( - f"select_substrate({{}}) must default to 'k3s'; got {bundle.name!r}" - ) - - -def test_select_substrate_k3s_with_legacy_fn_returns_k3s_adapter() -> None: - """When a legacy spawn fn is supplied, the bundle wraps it in K3sSpawnerAdapter. - - Task-1-1 AC: the k3s leg must return a working K3sSpawnerAdapter - wrapping ``create_concurrent_spawn_fn``. When the caller supplies - a pre-built closure (the production path), the bundle uses it - directly. - """ - from unittest.mock import MagicMock - - select_substrate = substrate_pkg.select_substrate - K3sSpawnerAdapter = substrate_pkg.K3sSpawnerAdapter - spawn_fn = MagicMock() - bundle = select_substrate({"EGG_SUBSTRATE": "k3s"}, k3s_legacy_spawn_fn=spawn_fn) - assert isinstance(bundle.spawner, K3sSpawnerAdapter), ( - f"select_substrate(k3s, k3s_legacy_spawn_fn=...) must return " - f"K3sSpawnerAdapter; got {type(bundle.spawner).__name__}" - ) - - -def test_select_substrate_returns_claude_code_when_env_set() -> None: - """``select_substrate({"EGG_SUBSTRATE": "claude-code"})`` returns the in-process bundle.""" - select_substrate = substrate_pkg.select_substrate - bundle = select_substrate({"EGG_SUBSTRATE": "claude-code"}) - assert bundle.name == "claude-code" - cls_name = type(bundle.spawner).__name__ - assert "claude" in cls_name.lower(), ( - f"select_substrate(claude-code) must return a Claude Code spawner; " - f"got {cls_name}. cq-1 / task-1-1 AC." - ) - - -def test_select_substrate_rejects_unknown_value() -> None: - """Unknown ``EGG_SUBSTRATE`` values fail loudly rather than silently fall back.""" - select_substrate = substrate_pkg.select_substrate - with pytest.raises(ValueError): - select_substrate({"EGG_SUBSTRATE": "definitely-not-a-substrate"}) - - -def test_select_substrate_is_case_insensitive() -> None: - """``EGG_SUBSTRATE=Claude-Code`` resolves the same as ``"claude-code"``.""" - select_substrate = substrate_pkg.select_substrate - bundle = select_substrate({"EGG_SUBSTRATE": "Claude-Code"}) - assert bundle.name == "claude-code" - - -# --------------------------------------------------------------------------- -# K3s default spawner is callable / working (task-1-1 AC contract gap) -# --------------------------------------------------------------------------- - - -def test_select_substrate_k3s_default_spawner_is_working() -> None: - """The k3s default bundle's spawner is a *working* spawner. - - Task-1-1 AC literally says:: - - select_substrate({}) defaults to "k3s" and returns a working - K3sSpawnerAdapter wrapping create_concurrent_spawn_fn - - "working" means ``.spawn(...)`` must not immediately raise - ``NotImplementedError`` with a placeholder/stub message. Real - spawn failures (e.g. no live cluster) are acceptable; a stub - that refuses to run is not. - """ - select_substrate = substrate_pkg.select_substrate - bundle = select_substrate({}) - spawner = bundle.spawner - # The class name must not signal a "deferred" / placeholder stub. - cls_name = type(spawner).__name__ - assert "Deferred" not in cls_name and "Placeholder" not in cls_name, ( - f"select_substrate({{}}) returned a {cls_name} — task-1-1 AC " - f"requires a working K3sSpawnerAdapter wrapping " - f"create_concurrent_spawn_fn, not a deferred / placeholder stub." - ) diff --git a/shared/tests/test_write_answer.py b/shared/tests/test_write_answer.py deleted file mode 100644 index 193088c281..0000000000 --- a/shared/tests/test_write_answer.py +++ /dev/null @@ -1,280 +0,0 @@ -"""Unit tests for ``plugins/egg-sdlc/skills/egg-sdlc/bin/write_answer.py``. - -The helper is load-bearing for the flattened-bridge skill loop (#2717 -slice-1): it ferries the operator's answer from the -``AskUserQuestion``-rendered selection into ``pending_hitl.answer`` of -the contract file. Earlier slices documented this as an inline -``python3 -c "..."`` snippet that was broken in three independent ways -(shell-interpolated answer, deprecated ``datetime.datetime.utcnow``, -non-atomic write); the helper exists so the load-bearing piece is -read-reviewable next to ``run_pipeline.py`` and exercised by tests. - -Tests cover: - -* JSON-encoded answer on stdin survives shell quoting (the original - blocker: ``approve`` resolved to a Python ``NameError`` when - shell-interpolated). -* Timestamp uses ``datetime.now(UTC).isoformat()`` — matches - ``run_pipeline.py``'s ``_now_iso`` so the driver and helper never - drift on timestamp shape. -* Atomic write via tmp + ``os.replace`` (never observed half-written). -* Refuses to overwrite a corrupted contract file (would silently drop - ``answer_log``). -""" - -from __future__ import annotations - -import importlib.util -import json -import sys -from pathlib import Path -from typing import Any - -import pytest - -_HELPER_PATH = ( - Path(__file__).resolve().parents[2] - / "plugins" - / "egg-sdlc" - / "skills" - / "egg-sdlc" - / "bin" - / "write_answer.py" -) - - -def _load_helper_module() -> Any: - """Import ``write_answer.py`` as a module without altering sys.path. - - The helper lives outside the regular package tree (under - ``plugins/egg-sdlc/skills/egg-sdlc/bin/``) so a normal ``import - write_answer`` does not find it. ``importlib.util.spec_from_file_location`` - is the standard way to load a one-off script-as-module without - polluting ``sys.path``. - """ - spec = importlib.util.spec_from_file_location("egg_sdlc_write_answer", _HELPER_PATH) - assert spec is not None and spec.loader is not None, f"could not load {_HELPER_PATH}" - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -@pytest.fixture -def helper() -> Any: - return _load_helper_module() - - -def _seed_contract(tmp_path: Path, pipeline_id: str = "issue-test") -> Path: - contracts = tmp_path / ".egg-state" / "contracts" - contracts.mkdir(parents=True, exist_ok=True) - contract_path = contracts / f"{pipeline_id}.json" - contract_path.write_text( - json.dumps( - { - "pipeline_id": pipeline_id, - "pending_hitl": { - "version": 1, - "pipeline_id": pipeline_id, - "timestamp": "2025-01-01T00:00:00+00:00", - "decision": {"question": "?", "options": []}, - "answer": None, - "status": "pending", - "result": None, - "error": None, - "answer_log": [], - }, - } - ), - encoding="utf-8", - ) - return contract_path - - -def test_stdin_json_answer_round_trips( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """The helper reads a JSON-encoded answer from stdin and writes it. - - Pins the fix for the original blocker: ``approve`` (a bare string - that would shell-interpolate into Python source as a ``NameError``) - is correctly JSON-decoded back to the literal string ``"approve"`` - before being assigned to ``pending_hitl.answer``. - """ - contract_path = _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - # Simulate the skill body's ``printf '%s' "${ANSWER}" | python3 -c - # 'json.dumps(stdin)'`` step by writing the JSON-encoded answer. - monkeypatch.setattr(sys, "stdin", _StubStdin(json.dumps("approve"))) - - rc = helper.main(["--pipeline-id", "issue-test", "--answer-stdin"]) - assert rc == 0 - data = json.loads(contract_path.read_text()) - assert data["pending_hitl"]["answer"] == "approve" - assert data["pending_hitl"]["status"] == "answered" - - -def test_answer_string_flag_round_trips( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """``--answer-string`` passes the raw selection; helper JSON-encodes internally. - - Pins the loop-body contract: the skill body calls - ``write_answer.py --answer-string "${ANSWER}"`` directly. The - helper takes the raw selection (no separate ``json.dumps`` - subcommand needed) and writes the literal string into - ``pending_hitl.answer``. This is the path that lets the skill's - ``allowed-tools`` fence ``python3`` to ``bin/*`` without leaving a - ``Bash(python3 -c …)`` hole for the json.dumps step. - """ - contract_path = _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - rc = helper.main(["--pipeline-id", "issue-test", "--answer-string", "approve"]) - assert rc == 0 - data = json.loads(contract_path.read_text()) - assert data["pending_hitl"]["answer"] == "approve" - assert data["pending_hitl"]["status"] == "answered" - - -def test_answer_string_special_characters( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """``--answer-string`` survives shell-special characters in the answer. - - Pins the shell-quoting invariant: the helper does NOT re-parse the - string as Python source, so ``"approve & continue"`` or - ``'"abort"'`` lands as the literal string in ``pending_hitl.answer``. - """ - contract_path = _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - tricky = 'approve & "continue" $now' - rc = helper.main(["--pipeline-id", "issue-test", "--answer-string", tricky]) - assert rc == 0 - data = json.loads(contract_path.read_text()) - assert data["pending_hitl"]["answer"] == tricky - - -def test_answer_json_flag_round_trips( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """``--answer-json`` accepts the JSON literal directly (no stdin).""" - contract_path = _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - rc = helper.main( - [ - "--pipeline-id", - "issue-test", - "--answer-json", - json.dumps({"selected": "approve"}), - ] - ) - assert rc == 0 - data = json.loads(contract_path.read_text()) - assert data["pending_hitl"]["answer"] == {"selected": "approve"} - assert data["pending_hitl"]["status"] == "answered" - - -def test_timestamp_format_matches_driver( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Timestamp uses ``datetime.now(UTC).isoformat()`` — no trailing ``Z``. - - Pins the contract that helper and driver write the same timestamp - shape. The driver's ``_now_iso`` (``run_pipeline.py:101-103``) - produces ``...+00:00`` (no ``Z``); a regression that adds ``Z`` - here would produce a mis-formatted ISO-8601 string when both - writers stamp the envelope in alternation. - """ - contract_path = _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - rc = helper.main(["--pipeline-id", "issue-test", "--answer-json", json.dumps("approve")]) - assert rc == 0 - data = json.loads(contract_path.read_text()) - timestamp = data["pending_hitl"]["timestamp"] - assert timestamp.endswith("+00:00"), ( - f"timestamp must end with '+00:00' (matching driver's _now_iso); " - f"got {timestamp!r}. A trailing 'Z' suffix would diverge from the " - f"driver's writes and produce mis-formatted ISO-8601." - ) - assert "Z" not in timestamp, f"timestamp must not contain 'Z'; got {timestamp!r}" - - -def test_refuses_to_overwrite_corrupted_contract( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """A present-but-unparseable contract is NOT silently overwritten. - - Pins the fix for the silent-fallback bug: an earlier ``_read_contract`` - swallowed ``JSONDecodeError`` and returned a fresh skeleton, dropping - ``answer_log`` on the floor. The helper now refuses to write so the - operator's accumulated answers are preserved for hand-repair. - """ - contracts = tmp_path / ".egg-state" / "contracts" - contracts.mkdir(parents=True, exist_ok=True) - contract_path = contracts / "issue-test.json" - contract_path.write_text("{ this is not valid JSON") - monkeypatch.chdir(tmp_path) - - rc = helper.main(["--pipeline-id", "issue-test", "--answer-json", json.dumps("approve")]) - assert rc == 1 - # The corrupted file is preserved verbatim — no silent overwrite. - assert contract_path.read_text() == "{ this is not valid JSON" - - -def test_invalid_json_answer_exits_nonzero( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """``--answer-json`` payload must be valid JSON; otherwise exit 1. - - The shell-side ``printf '%s' "${ANSWER}" | json.dumps`` step is the - skill body's responsibility. If a future regression in the skill - loop drops the json.dumps wrapper, the helper exits nonzero rather - than corrupting the envelope with a Python ``NameError``-equivalent - payload. - """ - _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - rc = helper.main(["--pipeline-id", "issue-test", "--answer-json", "this is not json"]) - assert rc == 1 - - -def test_atomic_write_uses_tmp_then_replace( - helper: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """The helper writes via tmp + os.replace, not a truncating open. - - Pins the contract: a crash mid-write must NOT leave a half-written - contract for the driver's ``_read_contract`` to choke on. We - monkeypatch ``os.replace`` to confirm it's invoked with the tmp - path that the helper itself constructed (``.json.tmp`` suffix — - matching the driver's ``_write_contract`` shape). - """ - contract_path = _seed_contract(tmp_path) - monkeypatch.chdir(tmp_path) - - captured: dict[str, Path] = {} - - real_replace = helper.os.replace - - def fake_replace(src: str, dst: str) -> None: - captured["src"] = Path(src) - captured["dst"] = Path(dst) - real_replace(src, dst) - - monkeypatch.setattr(helper.os, "replace", fake_replace) - - rc = helper.main(["--pipeline-id", "issue-test", "--answer-json", json.dumps("approve")]) - assert rc == 0 - assert captured["src"].name.endswith(".json.tmp"), ( - f"helper must write to tmp + os.replace; got src={captured.get('src')!r}" - ) - assert captured["dst"] == contract_path - - -class _StubStdin: - """Minimal ``sys.stdin`` stand-in that returns a fixed string from ``.read()``.""" - - def __init__(self, payload: str) -> None: - self._payload = payload - - def read(self) -> str: - return self._payload diff --git a/tests/sandbox/egg_agent_tools/test_restrictions_validator.py b/tests/sandbox/egg_agent_tools/test_restrictions_validator.py deleted file mode 100644 index f2d20c99de..0000000000 --- a/tests/sandbox/egg_agent_tools/test_restrictions_validator.py +++ /dev/null @@ -1,323 +0,0 @@ -"""Tests for the agent-side policy enforcement (#2717 slice-2 task-2-6). - -Contingency context (slice-1 R2 verdict) ----------------------------------------- - -Slice-1's TASK-1-5 ran the R2 nested-dispatch spike and wrote the -verdict to ``.egg-state/<pipeline_id>/r2-verdict.json``. The -slice-1 BRC history records the **verdict = "pass"**: the -PreToolUse hook correctly resolves the child role under nested -dispatch (parent=architect + child=tester writing -``orchestrator/foo.py`` → ``decision=block`` with a tester-naming -reason; the cross-role probe, in-role negative-control, and -EGG_AGENT_ROLE leak guard all 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). - -And task-2-6's acceptance criterion: - - (R2 pass) asserts the validator helper is a no-op for - in-allow-list writes (the contingency is documented in the test - docstring). - -So this file's job is the **no-op regression guard**: assert that -``check_file_restriction`` continues to return the gateway-shape -response for in-allow-list writes — i.e., the slice-2 work did NOT -silently extend the in-sandbox handler with R2-fail-only enforcement -logic and accidentally change the response shape for the R2-pass -path. The PreToolUse hook (orchestrator/substrate/claude_code/ -hook_entry.py) remains the load-bearing enforcement seam; the -in-sandbox ``restrictions`` handler stays a pure-read self-check. - -What this test enforces ------------------------ - -1. **In-allow-list write — response shape stable.** When a role's - own pattern matches the requested path, ``check_file_restriction`` - returns ``can_write=True`` with the documented gateway-shape - fields (``ok``, ``role``, ``path``, ``can_write``, ``reason``, - ``alternative_role``). No new fields, no removed fields, no - shape drift introduced by the slice-2 work. - -2. **Cross-role write — denial-shape stable.** When the role cannot - write the path, the response carries ``can_write=False``, - ``reason`` references ``shared/egg_restrictions/patterns.py``, - and ``alternative_role`` is populated when exactly one producer - role covers the path. - -3. **No new validator surface.** Slice-2 must not introduce a new - ``validate_write_target`` (or any other) symbol on the - ``restrictions`` handler module that would constitute the - R2-fail enforcement path. If such a symbol appears it would be a - sign that the cq-6 option-2 work landed without being needed — - surface that as a soft heads-up via a clearly-named test. - -If slice-1's R2 verdict were instead ``"fail"`` the contingent -TASK-2-5 would have landed a validator and this file would need -positive coverage of the denial path. That path is not in scope -because R2 passed. If a future slice flips R2 to fail (the cq-3 -deferral makes that possible), this test will need a sibling that -exercises the new validator's denial shape — captured in the test -docstring per the acceptance criterion. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import pytest - -# Ensure sandbox / shared are importable. Mirrors the pattern from -# ``tests/sandbox/egg_agent_tools/test_handlers_sdlc.py``. -ROOT = Path(__file__).resolve().parents[3] -sys.path.insert(0, str(ROOT / "sandbox")) -sys.path.insert(0, str(ROOT / "shared")) - -from egg_agent_tools.handlers import restrictions # noqa: E402 -from egg_agent_tools.handlers.errors import HandlerError # noqa: E402 - -# --------------------------------------------------------------------------- -# Documented gateway-shape fields per ``check_file_restriction``'s -# docstring. Pin them as a frozenset so a shape drift fails clearly -# with a missing-key / extra-key message. -# --------------------------------------------------------------------------- - -_SINGLE_PATH_FIELDS: frozenset[str] = frozenset( - {"ok", "role", "path", "can_write", "reason", "alternative_role"} -) - - -# --------------------------------------------------------------------------- -# In-allow-list writes — the R2-pass no-op invariant -# --------------------------------------------------------------------------- - - -def test_coder_in_allow_list_response_shape_stable() -> None: - """``check_file_restriction`` returns the documented shape for an in-allow-list write. - - The R2-pass no-op invariant: the slice-2 work must NOT extend - ``check_file_restriction``'s in-allow-list response with new - fields or change ``can_write`` to anything other than ``True`` - for a path the role's pattern allows. - - A coder writing under ``orchestrator/`` is the canonical - in-allow-list case (per ``shared/egg_restrictions/patterns.py``). - """ - req = {"role": "coder", "path": "orchestrator/foo.py"} - resp = restrictions.check_file_restriction(req) - - assert resp["ok"] is True - assert resp["role"] == "coder" - assert resp["path"] == "orchestrator/foo.py" - assert resp["can_write"] is True, ( - f"coder writing orchestrator/foo.py must be allowed; got " - f"can_write={resp.get('can_write')!r}. If this fails, either " - f"the pattern registry changed shape (file a follow-up) or the " - f"slice-2 work accidentally introduced enforcement that the " - f"R2-pass verdict said wasn't needed." - ) - assert isinstance(resp.get("reason"), str), ( - f"``reason`` must be a string even on the allowed path; got {resp.get('reason')!r}" - ) - assert resp.get("alternative_role") is None, ( - f"``alternative_role`` must be None on the allowed path " - f"(it only names the alternative producer role on denial); " - f"got {resp.get('alternative_role')!r}" - ) - # No extra keys leaked into the response shape. - assert set(resp.keys()) == _SINGLE_PATH_FIELDS, ( - f"in-allow-list response shape must equal " - f"{sorted(_SINGLE_PATH_FIELDS)}; got " - f"{sorted(resp.keys())}. The R2-pass no-op invariant requires " - f"that slice-2 NOT introduce new fields in the validator's " - f"response shape." - ) - - -def test_tester_in_allow_list_response_shape_stable() -> None: - """``check_file_restriction`` returns the documented shape for a tester writing under tests/. - - Tester under tests/ is the canonical in-allow-list case for the - tester role per the gateway pattern registry. Pinning both the - coder and the tester cases catches regressions that only one of - them surfaces. - """ - req = {"role": "tester", "path": "tests/sandbox/egg_agent_tools/test_x.py"} - resp = restrictions.check_file_restriction(req) - - assert resp["ok"] is True - assert resp["role"] == "tester" - assert resp["can_write"] is True, ( - f"tester writing tests/sandbox/egg_agent_tools/test_x.py must " - f"be allowed; got can_write={resp.get('can_write')!r}" - ) - assert resp.get("alternative_role") is None - assert set(resp.keys()) == _SINGLE_PATH_FIELDS - - -def test_documenter_in_allow_list_response_shape_stable() -> None: - """``check_file_restriction`` returns the documented shape for documenter under docs/.""" - req = {"role": "documenter", "path": "docs/foo.md"} - resp = restrictions.check_file_restriction(req) - - assert resp["ok"] is True - assert resp["role"] == "documenter" - assert resp["can_write"] is True, ( - f"documenter writing docs/foo.md must be allowed; got can_write={resp.get('can_write')!r}" - ) - assert set(resp.keys()) == _SINGLE_PATH_FIELDS - - -# --------------------------------------------------------------------------- -# Cross-role denial — the slice-1 PreToolUse-hook path stays the -# enforcement seam; the validator's denial shape must remain stable. -# --------------------------------------------------------------------------- - - -def test_coder_cannot_write_tester_path_denial_shape_stable() -> None: - """Cross-role denial: coder cannot write ``tests/*``; alternative_role names tester. - - Pinned so a slice-2 regression that changed the denial's - ``reason`` text to drop the ``shared/egg_restrictions/patterns.py`` - pointer (or stripped the ``alternative_role`` field) surfaces - here, not at gateway-403 time. - """ - req = {"role": "coder", "path": "tests/sandbox/egg_agent_tools/test_x.py"} - resp = restrictions.check_file_restriction(req) - - assert resp["ok"] is True - assert resp["role"] == "coder" - assert resp["can_write"] is False, ( - f"coder writing tests/ must be denied; got can_write={resp.get('can_write')!r}" - ) - assert "shared/egg_restrictions/patterns.py" in resp.get("reason", ""), ( - f"denial reason must reference the pattern registry; got {resp.get('reason')!r}" - ) - assert resp.get("alternative_role") == "tester", ( - f"``alternative_role`` must name tester when coder is blocked " - f"from a tests/ path; got {resp.get('alternative_role')!r}. " - f"Without this the impasse-routing path can't auto-delegate." - ) - assert set(resp.keys()) == _SINGLE_PATH_FIELDS - - -def test_tester_cannot_write_orchestrator_path_denial_shape_stable() -> None: - """Cross-role denial: tester cannot write ``orchestrator/*``; alternative_role names coder.""" - req = {"role": "tester", "path": "orchestrator/foo.py"} - resp = restrictions.check_file_restriction(req) - - assert resp["ok"] is True - assert resp["role"] == "tester" - assert resp["can_write"] is False, ( - f"tester writing orchestrator/foo.py must be denied; got " - f"can_write={resp.get('can_write')!r}" - ) - assert "shared/egg_restrictions/patterns.py" in resp.get("reason", "") - assert resp.get("alternative_role") == "coder", ( - f"``alternative_role`` must name coder when tester is blocked " - f"from an orchestrator/ path; got {resp.get('alternative_role')!r}" - ) - - -# --------------------------------------------------------------------------- -# Negative invariant — no new validator surface was added on the -# R2-pass path. -# --------------------------------------------------------------------------- - - -def test_no_new_validator_symbol_introduced_in_r2_pass_slice() -> None: - """The slice-2 work must NOT introduce a ``validate_write_target`` (or peer) symbol. - - The R2-pass no-op invariant: TASK-2-5 said "If R2 = pass, this - task is a no-op (close with note)." If a symbol like - ``validate_write_target`` appears on the ``restrictions`` - handler module it would mean the R2-fail enforcement path landed - despite the verdict — surface that here so the slice-1 R2 - verdict and the slice-2 implementation stay consistent. - - Test is informational on a green run (the symbol is absent) and - fires loudly on a regression. Distinct from the per-test - assertions above so the failure mode is easy to triage. - """ - forbidden = {"validate_write_target"} - leaked = {name for name in forbidden if hasattr(restrictions, name)} - assert not leaked, ( - f"R2-pass no-op invariant violated: slice-2 added unexpected " - f"symbol(s) {sorted(leaked)} to " - f"sandbox/egg_agent_tools/handlers/restrictions.py. The slice-1 " - f"R2 verdict was ``pass`` so the agent-side enforcement path " - f"(cq-6 option 2 from #2623) should NOT have landed. Either " - f"(a) the R2 verdict flipped to ``fail`` and TASK-2-6 should " - f"now cover the positive denial path, or (b) the slice-2 work " - f"accidentally landed enforcement code that needs to be " - f"reverted." - ) - - -# --------------------------------------------------------------------------- -# Adversarial probes — even on the no-op path, the validator's -# defensive surface must hold. -# --------------------------------------------------------------------------- - - -def test_missing_path_raises_handler_error() -> None: - """Calling ``check_file_restriction`` without ``path`` raises HandlerError. - - Defensive invariant: a slice-2 regression that silently - swallowed the missing-arg case (e.g., by short-circuiting on the - R2-pass branch before validation ran) would be a security risk — - an agent could pass an empty request and get a falsy - ``can_write`` answer without the validator ever inspecting the - real path. - """ - with pytest.raises(HandlerError, match="'path' is required"): - restrictions.check_file_restriction({"role": "coder"}) - - -def test_unknown_role_raises_handler_error() -> None: - """Unknown role surfaces as ``HandlerError`` (not ``can_write=True``). - - Slice-2 must not introduce a fall-through that maps an unknown - role to a permissive answer. Pin the existing behaviour so a - regression that loosens role validation surfaces here. - - Reviewer_code v2 non-blocking N11: this test previously patched - ``restrictions.get_agent_role`` to return ``None``, but - ``check_file_restriction`` short-circuits on the truthy - ``req["role"] = "unknown_xyz"`` (``role = req.get("role") or - get_agent_role()``), so the patch never fired. The patch is - dropped to make the test's intent unambiguous. - """ - with pytest.raises(HandlerError): - restrictions.check_file_restriction({"role": "unknown_xyz", "path": "orchestrator/foo.py"}) - - -def test_list_path_returns_per_path_results() -> None: - """When ``path`` is a list, the validator returns a per-path ``results`` array. - - Pin the bulk-check surface so a regression that flattened the - response to a single answer surfaces here. The bulk surface is - documented in ``check_file_restriction``'s docstring and is the - shape the orchestrator's impasse-routing relies on when a task - names multiple ``blocked_files``. - """ - req = { - "role": "coder", - "path": ["orchestrator/foo.py", "tests/test_x.py"], - } - resp = restrictions.check_file_restriction(req) - - assert resp["ok"] is True - assert resp["role"] == "coder" - assert "results" in resp, ( - f"list-shaped path must return ``results``; got keys {sorted(resp.keys())}" - ) - assert len(resp["results"]) == 2 - # First path is in-allow-list, second is denied. - assert resp["results"][0]["can_write"] is True - assert resp["results"][1]["can_write"] is False - assert resp["results"][1]["alternative_role"] == "tester" From 3c14f60c133b5375a3829bb9192f3fc5873df212 Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 21:30:45 +0000 Subject: [PATCH 2/3] Parenthesize except tuples in lifecycle_secret Restore the (OSError, subprocess.TimeoutExpired) and (ValueError, UnicodeDecodeError) paren-wraps the substrate PR had added. PEP 758 makes the bare form valid on Python 3.14, but it reads as a Python 2 SyntaxError to most reviewers, which is the kind of nit that would otherwise be flagged later. --- integration_tests/regression/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_tests/regression/conftest.py b/integration_tests/regression/conftest.py index 3bb83eabfd..d9b9660557 100644 --- a/integration_tests/regression/conftest.py +++ b/integration_tests/regression/conftest.py @@ -280,7 +280,7 @@ def lifecycle_secret() -> str | None: timeout=15, check=False, ) - except OSError, subprocess.TimeoutExpired: + except (OSError, subprocess.TimeoutExpired): return None if result.returncode != 0 or not result.stdout: return None @@ -290,7 +290,7 @@ def lifecycle_secret() -> str | None: # a ``\n`` inside ``f"Bearer {secret}"`` is rejected by # ``http.client.putheader``. return base64.b64decode(result.stdout).decode("utf-8").strip() - except ValueError, UnicodeDecodeError: + except (ValueError, UnicodeDecodeError): return None From 1f2dea7fae7712878dbe9f2b628d9550bba5d3b8 Mon Sep 17 00:00:00 2001 From: egg <egg@localhost> Date: Wed, 20 May 2026 21:32:03 +0000 Subject: [PATCH 3/3] Fix checks: apply automated formatting fixes --- integration_tests/regression/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_tests/regression/conftest.py b/integration_tests/regression/conftest.py index d9b9660557..3bb83eabfd 100644 --- a/integration_tests/regression/conftest.py +++ b/integration_tests/regression/conftest.py @@ -280,7 +280,7 @@ def lifecycle_secret() -> str | None: timeout=15, check=False, ) - except (OSError, subprocess.TimeoutExpired): + except OSError, subprocess.TimeoutExpired: return None if result.returncode != 0 or not result.stdout: return None @@ -290,7 +290,7 @@ def lifecycle_secret() -> str | None: # a ``\n`` inside ``f"Bearer {secret}"`` is rejected by # ``http.client.putheader``. return base64.b64decode(result.stdout).decode("utf-8").strip() - except (ValueError, UnicodeDecodeError): + except ValueError, UnicodeDecodeError: return None