diff --git a/.egg-state/agent-outputs/coder-to-tester-1557-test-followups.md b/.egg-state/agent-outputs/coder-to-tester-1557-test-followups.md deleted file mode 100644 index 29f9534afc..0000000000 --- a/.egg-state/agent-outputs/coder-to-tester-1557-test-followups.md +++ /dev/null @@ -1,51 +0,0 @@ -# Coder → Tester handoff: test follow-ons for issue #1557 (apply-phase scheduler + APPLIER + epic_link_field) - -The coder commit `4cf20c886` (`implement(#1557): apply-phase scheduler + wontdo drain + test fixes`) introduces three production changes that have mechanical follow-on test deltas. Per the gateway's file-restriction policy -(`shared/egg_restrictions/patterns.py`), tests under `gateway/tests/`, -`orchestrator/tests/`, and `shared/tests/` are tester scope — the -coder role cannot push them. The patch below captures those deltas -verbatim; please apply them on the slice-2 integration branch and -re-ACK my proposal. - -## Files affected - -| Path | Why the test needs updating | -|---------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `gateway/tests/test_phase_transition.py` | `PHASE_TRANSITIONS[PLAN]` grew from `[IMPLEMENT]` to `[IMPLEMENT, APPLY]`; a new `APPLY → IMPLEMENT` edge needs coverage. | -| `gateway/tests/test_jira_routes.py` | NEW: task-1-6 acceptance — two tests verifying the ticket-create route propagates `JiraPolicy.epic_link_field()` verbatim to `JiraClient.create_issue` for both `parent` (default) and `customfield_10014`. | -| `orchestrator/tests/test_advance_phase_thread.py` | The auto-advance source-inspection block window was 3000 chars; the new applier-handoff + Won't-Do drain hooks push the `_spawn_pipeline_run_thread` call past that window. Widen to 5000. | -| `orchestrator/tests/test_models.py` | `AgentRole` count moved from 19 → 20 (APPLIER added); `PipelinePhase` declaration order now has APPLY between PLAN and IMPLEMENT. | -| `shared/tests/test_egg_restrictions.py` | Same registry-count bump on the `AGENT_PATTERNS` parity assertions. | - -## How to apply - -```bash -git apply .egg-state/agent-outputs/coder-to-tester-1557-test-followups.patch -git add gateway/tests/test_jira_routes.py \ - gateway/tests/test_phase_transition.py \ - orchestrator/tests/test_advance_phase_thread.py \ - orchestrator/tests/test_models.py \ - shared/tests/test_egg_restrictions.py -git commit -m 'test(#1557): follow-on assertions for APPLY phase + APPLIER role + epic_link_field' -``` - -The patch is mechanical — it only adjusts assertions that have hard-coded counts / ordering / window-sizes the coder's production change shifted. There are no behavioral test rewrites and no new fixture infrastructure. - -## Validation that the patch passes locally - -Each touched test file was run against the coder's production diff -before the test files were extracted from the commit: - -- `gateway/tests/test_jira_routes.py` — 104 tests pass (including the - two new `test_epic_link_dispatches_via_{parent_field,customfield}`). -- `gateway/tests/test_phase_transition.py` — 29 tests pass (including - the new `test_apply_to_implement`). -- `orchestrator/tests/test_advance_phase_thread.py` — 15 tests pass. -- `orchestrator/tests/test_models.py` — 85 tests pass. -- `shared/tests/test_egg_restrictions.py` — 211 tests pass. - -## Reviewer pointer - -Pair with my proposal's `pre_merge_condition`: the human reviewer -(or you, with `mcp__brc__resolve_obligation`) closes the obligation -once this patch is on the integration branch. diff --git a/.egg-state/agent-outputs/coder-to-tester-1557-test-followups.patch b/.egg-state/agent-outputs/coder-to-tester-1557-test-followups.patch deleted file mode 100644 index 929c099055..0000000000 --- a/.egg-state/agent-outputs/coder-to-tester-1557-test-followups.patch +++ /dev/null @@ -1,219 +0,0 @@ -diff --git a/gateway/tests/test_jira_routes.py b/gateway/tests/test_jira_routes.py -index ed7a96492..759a65377 100644 ---- a/gateway/tests/test_jira_routes.py -+++ b/gateway/tests/test_jira_routes.py -@@ -979,6 +979,86 @@ class TestTicketCreate: - kwargs = fake_client.create_issue.call_args.kwargs - assert kwargs["description"] == adf - -+ # ------------------------------------------------------------------- -+ # Issue #1557 task-1-6 — per-project ``epic_link_field`` dispatch. -+ # ------------------------------------------------------------------- -+ # -+ # The dispatch from the ``epicLink`` shorthand to either ``parent`` -+ # (next-gen / company-managed projects, default) or -+ # ``customfield_10014`` (classic / team-managed projects) is wired -+ # at ``gateway/gateway.py:6097`` — the route reads -+ # ``JiraPolicy.epic_link_field`` and passes it to -+ # ``JiraClient.create_issue``. ``JiraClient.create_issue``'s wire -+ # translation is covered by -+ # ``gateway/tests/test_jira_client.py::TestCreateIssue::test_epic_ -+ # link_with_{parent,customfield}_dispatch``. The tests below close -+ # the route-layer half: they assert the gateway route reads the -+ # policy and propagates the resolved field name verbatim to the -+ # JiraClient call. Together the two sides verify the operator- -+ # managed ``epic_link_field`` setting (refine decision-3) is -+ # exercised end-to-end before the epic pipeline relies on it for -+ # child-ticket creation. -+ -+ def test_epic_link_dispatches_via_parent_field( -+ self, client, private_headers, allow_eng, captured_audit, monkeypatch -+ ): -+ """Default ``epic_link_field='parent'`` (next-gen / company-managed -+ sites) → the route hands ``epic_link_field='parent'`` to -+ ``JiraClient.create_issue``, which then writes -+ ``fields: {parent: {key: }}`` on the Atlassian wire. -+ Verified at the JiraClient layer by -+ ``test_epic_link_with_parent_dispatch`` in test_jira_client.py.""" -+ monkeypatch.setattr(gateway, "jira_epic_link_field", lambda: "parent") -+ fake_client = MagicMock() -+ fake_client.create_issue.return_value = ( -+ 201, -+ {"id": "1", "key": "ENG-2", "self": "https://e.atlassian.net/rest/api/3/issue/1"}, -+ False, -+ ) -+ with patch.object(gateway, "get_jira_client", return_value=fake_client): -+ resp = client.post( -+ self.PATH, -+ headers=private_headers, -+ data=json.dumps({**self._valid_body(), "epicLink": "ENG-1"}), -+ content_type="application/json", -+ ) -+ assert resp.status_code == 200, resp.data -+ kwargs = fake_client.create_issue.call_args.kwargs -+ # The route must forward both the requested epic link AND the -+ # operator-configured dispatch field — the JiraClient layer -+ # then translates ``epic_link_field='parent'`` into -+ # ``fields.parent: {key: }`` (covered in test_jira_client.py). -+ assert kwargs["epic_link"] == "ENG-1" -+ assert kwargs["epic_link_field"] == "parent" -+ -+ def test_epic_link_dispatches_via_customfield( -+ self, client, private_headers, allow_eng, captured_audit, monkeypatch -+ ): -+ """``epic_link_field='customfield_10014'`` (classic / team-managed -+ sites) → the route hands the customfield name to -+ ``JiraClient.create_issue``, which writes -+ ``fields: {customfield_10014: }`` on the wire. Verified at -+ the JiraClient layer by ``test_epic_link_with_customfield_ -+ dispatch`` in test_jira_client.py.""" -+ monkeypatch.setattr(gateway, "jira_epic_link_field", lambda: "customfield_10014") -+ fake_client = MagicMock() -+ fake_client.create_issue.return_value = ( -+ 201, -+ {"id": "1", "key": "ENG-2", "self": "https://e.atlassian.net/rest/api/3/issue/1"}, -+ False, -+ ) -+ with patch.object(gateway, "get_jira_client", return_value=fake_client): -+ resp = client.post( -+ self.PATH, -+ headers=private_headers, -+ data=json.dumps({**self._valid_body(), "epicLink": "ENG-1"}), -+ content_type="application/json", -+ ) -+ assert resp.status_code == 200, resp.data -+ kwargs = fake_client.create_issue.call_args.kwargs -+ assert kwargs["epic_link"] == "ENG-1" -+ assert kwargs["epic_link_field"] == "customfield_10014" -+ - def test_upstream_error_passes_through( - self, client, private_headers, allow_eng, captured_audit - ): -diff --git a/gateway/tests/test_phase_transition.py b/gateway/tests/test_phase_transition.py -index f664696d9..a53adda6f 100644 ---- a/gateway/tests/test_phase_transition.py -+++ b/gateway/tests/test_phase_transition.py -@@ -96,9 +96,34 @@ class TestValidTransitions: - assert len(VALID_TRANSITIONS[PipelinePhase.REFINE]) == 1 - - def test_plan_to_implement(self): -- """Plan can only transition to implement.""" -+ """Plan can transition to implement (and to apply for epic pipelines). -+ -+ Issue #1557: ``PLAN`` gained ``APPLY`` as a second valid -+ successor so epic-mode pipelines can route Jira mutations -+ through a dedicated APPLY phase between PLAN and IMPLEMENT. -+ Non-epic pipelines continue to use the IMPLEMENT edge — -+ ``IMPLEMENT`` is listed first so ``get_next_phase`` keeps the -+ pre-#1557 default.""" - assert PipelinePhase.IMPLEMENT in VALID_TRANSITIONS[PipelinePhase.PLAN] -- assert len(VALID_TRANSITIONS[PipelinePhase.PLAN]) == 1 -+ assert PipelinePhase.APPLY in VALID_TRANSITIONS[PipelinePhase.PLAN] -+ assert len(VALID_TRANSITIONS[PipelinePhase.PLAN]) == 2 -+ # Default-first ordering invariant: epic-aware schedulers pick -+ # APPLY by name; non-epic flows that take ``next_phases[0]`` -+ # must still see IMPLEMENT. -+ assert VALID_TRANSITIONS[PipelinePhase.PLAN][0] == PipelinePhase.IMPLEMENT -+ -+ def test_apply_to_implement(self): -+ """Apply (Jira-epic phase) advances only to implement. -+ -+ Issue #1557: the new ``APPLY`` phase is the second step in the -+ epic-mode pipeline (PLAN → APPLY → IMPLEMENT). The orchestrator- -+ side scheduler in ``orchestrator.routes.pipelines. -+ _next_phases_for_epic`` picks APPLY only when ``Pipeline.is_epic`` -+ is true; this transition is what carries the pipeline back into -+ the standard IMPLEMENT phase once the applier has driven all Jira -+ mutations and BRC consensus has confirmed.""" -+ assert PipelinePhase.IMPLEMENT in VALID_TRANSITIONS[PipelinePhase.APPLY] -+ assert len(VALID_TRANSITIONS[PipelinePhase.APPLY]) == 1 - - def test_implement_to_pr(self): - """Implement can only transition to PR.""" -diff --git a/orchestrator/tests/test_advance_phase_thread.py b/orchestrator/tests/test_advance_phase_thread.py -index 6a3de2df2..ff73bf07b 100644 ---- a/orchestrator/tests/test_advance_phase_thread.py -+++ b/orchestrator/tests/test_advance_phase_thread.py -@@ -278,7 +278,10 @@ class TestAutoAdvanceRespawnsThread: - ) - idx = source.index(self._BLOCK_MARKER) - # Take a generous window so the block including the return is included. -- return source[idx : idx + 3000] -+ # Widened from 3000 to 5000 in issue #1557 to absorb the epic-mode -+ # applier-handoff write + Won't-Do drain hook the auto-advance -+ # block now performs before respawning the next-phase thread. -+ return source[idx : idx + 5000] - - def test_auto_advance_bumps_run_epoch(self): - block = self._auto_advance_block() -diff --git a/orchestrator/tests/test_models.py b/orchestrator/tests/test_models.py -index af463e384..d8bde8aee 100644 ---- a/orchestrator/tests/test_models.py -+++ b/orchestrator/tests/test_models.py -@@ -819,6 +819,10 @@ class TestAgentRole: - assert AgentRole.CODER in roles - assert AgentRole.TESTER in roles - assert AgentRole.DOCUMENTER in roles -+ # Issue #1557 — APPLIER joined the registry for Jira-epic -+ # SDLC support (drives gateway Jira mutations after HITL -+ # approval on epic-mode pipelines). -+ assert AgentRole.APPLIER in roles - assert AgentRole.ARCHITECT in roles - assert AgentRole.TASK_PLANNER in roles - assert AgentRole.RISK_ANALYST in roles -@@ -835,7 +839,7 @@ class TestAgentRole: - assert AgentRole.OVERSEER in roles - assert AgentRole.AUTOFIXER in roles - assert AgentRole.CONFLICT_RESOLVER in roles -- assert len(roles) == 19 -+ assert len(roles) == 20 - - - class TestBackwardCompatibility: -@@ -898,9 +902,18 @@ class TestPipelinePhase: - """Tests for PipelinePhase enum.""" - - def test_phase_order(self): -- """Test phases are defined in SDLC order.""" -+ """Test phases are defined in SDLC order. -+ -+ Issue #1557 inserted ``APPLY`` between ``PLAN`` and ``IMPLEMENT`` -+ — the new phase runs only on epic-mode pipelines (gated by -+ ``Pipeline.is_epic`` in the orchestrator-side scheduler) so the -+ enum declaration order reflects the SDLC reading order for an -+ epic pipeline; non-epic pipelines skip APPLY entirely via -+ ``orchestrator.routes.pipelines._next_phases_for_epic``. -+ """ - phases = list(PipelinePhase) - assert phases[0] == PipelinePhase.REFINE - assert phases[1] == PipelinePhase.PLAN -- assert phases[2] == PipelinePhase.IMPLEMENT -- assert phases[3] == PipelinePhase.PR -+ assert phases[2] == PipelinePhase.APPLY -+ assert phases[3] == PipelinePhase.IMPLEMENT -+ assert phases[4] == PipelinePhase.PR -diff --git a/shared/tests/test_egg_restrictions.py b/shared/tests/test_egg_restrictions.py -index 23a7c7765..0097fd278 100644 ---- a/shared/tests/test_egg_restrictions.py -+++ b/shared/tests/test_egg_restrictions.py -@@ -77,14 +77,18 @@ class TestAgentRole: - - - class TestAgentPatterns: -- def test_registry_has_all_19_roles(self): -- assert len(AGENT_PATTERNS) == 19 -+ def test_registry_has_all_20_roles(self): -+ # Issue #1557 — APPLIER joined the registry (Jira-epic SDLC -+ # support); the count grew from 19 to 20. -+ assert len(AGENT_PATTERNS) == 20 - - def test_registry_keys_match_role_constants(self): - expected_roles = { - AgentRole.CODER, - AgentRole.TESTER, - AgentRole.DOCUMENTER, -+ # Issue #1557 — Jira-epic SDLC pipeline support. -+ AgentRole.APPLIER, - AgentRole.ARCHITECT, - AgentRole.TASK_PLANNER, - AgentRole.RISK_ANALYST, diff --git a/.egg-state/brc-history/2769-implement-unattributed.json b/.egg-state/brc-history/2769-implement-unattributed.json new file mode 100644 index 0000000000..6285c0d943 --- /dev/null +++ b/.egg-state/brc-history/2769-implement-unattributed.json @@ -0,0 +1,76 @@ +[ + { + "id": "4739cde4-1a36-4c", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "OVERSEER_ALERT", + "subject": "agent-heartbeat-stall [medium]", + "body": "coder (container 8fcde009) has emitted zero heartbeats in 644s, exceeding the 600s silent-agent threshold \u2014 no CONSENSUS_PROPOSE yet\n\nDetail:\nPipeline issue-2769 / slice-1, implement phase. Coder started 2026-05-22T05:33:10Z and has never sent a heartbeat or proposed. Silent threshold (overseer_silent_agent_threshold_seconds=600) is exceeded by ~44s. Downstream impact: tester is WAITING_ON_ROLE:coder with scaffold tests committed; reviewer_code_holistic, reviewer_security, reviewer_contract, reviewer_concurrency all blocking on CONSENSUS_PROPOSE from coder. Documenter BRC cycle is healthy (proposed task-1-12, reviewer_code is reviewing). Coder container status=running \u2014 process is alive but has not communicated. Recommend: inspect coder checkpoint logs to determine if it is blocked on a gateway call, LLM call, or file operation.\n\nRecommended action:\nRun `egg-checkpoint show` on the most recent coder checkpoint for pipeline issue-2769/slice-1 to inspect progress. If the coder is stuck on a gateway credential or LLM call timeout, consider a targeted restart of just the coder container.", + "metadata": {}, + "timestamp": "2026-05-22T05:44:25.893876+00:00", + "phase": "implement" + }, + { + "id": "70976add-0201-4b", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "OVERSEER_ALERT", + "subject": "agent-heartbeat-stall [high]", + "body": "coder silent for 833s \u2014 all code-reviewers and tester blocked; pipeline issue-2769/slice-1 requires human decision on coder restart\n\nDetail:\nCoder container 8fcde009 started at 05:33:10Z and has emitted ZERO heartbeats, checkpoints, or proposals after 833s. Silent threshold (600s) exceeded by 233s. The coder did not respond to the prior medium-priority overseer alert (4739cde4) because it was not in a message wait loop. Downstream blocked: reviewer_code_holistic, reviewer_security, reviewer_contract, reviewer_concurrency all await coder CONSENSUS_PROPOSE; tester committed scaffold tests locally and awaits coder commits on origin/egg/issue-2769/slice-1. Documenter proposal (task-1-12, commit 8c68062f) is at 1 ACK (reviewer_code), stalled because other reviewers are scoped to coder output. overseer_auto_file_issues_mode=shadow so no auto-action was taken. Phase long-running threshold is 3600s (not yet hit). Coder is doing a large multi-file gateway refactor (gateway/upstream_registry.py new file + edits to gateway/gateway.py, anthropic_credentials.py, session_manager.py, orchestrator/gateway_client.py, k8s manifests) which could legitimately take 15-20 min, but zero communication is anomalous.\n\nRecommended action:\nOption A (Restart): Restart coder container 8fcde009 on pipeline issue-2769/slice-1 to unblock reviewers and tester. Option B (Wait): Allow up to 20 more minutes (total ~35 min) given large scope of gateway refactor. Option C (Inspect): Attach to coder container directly to check if it is mid-LLM-call or stuck on a gateway credential fetch before deciding.", + "metadata": {}, + "timestamp": "2026-05-22T05:48:03.998630+00:00", + "phase": "implement" + }, + { + "id": "119f13c4-f5ac-4e", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "Overseer monitoring pipeline issue-2769/slice-1 implement phase. Coder (container 8fcde009) has been silent for 1421s \u2014 zero heartbeats, checkpoints, or proposals. Two overseer alerts broadcast (medium at 644s, high at 833s). All 7 other agents healthy. Pipeline not terminal. Advisor gate now open. Continuing monitoring.", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T05:57:21.157495+00:00", + "phase": "implement" + }, + { + "id": "040be711-e749-48", + "pipeline_id": "issue-2769", + "from_role": "tester", + "to_role": "coder", + "message_type": "HANDOFF", + "subject": "make lint failing on gateway/gateway.py format", + "body": "ruff format --check gateway/gateway.py fails at lines 8678-8683 (the new Invalid upstream make_error call). Fix: ruff format gateway/gateway.py \u2014 the diff is a one-liner. NACK already sent on your v1 proposal (see CONSENSUS_NACK). I will re-review and ACK once you re-propose with the format fix.", + "metadata": {}, + "timestamp": "2026-05-22T06:18:17.690083+00:00", + "phase": "implement" + }, + { + "id": "1f8c0a1d-d07f-41", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "OVERSEER_ALERT", + "subject": "stuck-phase-transition [medium]", + "body": "BRC confirmation livelock: 5 reviewers have ACKed + tried confirm (all false); coder and tester never called confirm\n\nDetail:\nAll 3 producers have proposed (documenter, coder v3 commit f5076a866, tester v1 commit bf857bf8b). All 5 reviewers have ACKed and attempted CONSENSUS_CONFIRMED between 06:28:57\u201306:31:58 (all consensus_reached: false). Coder is frozen in CONSENSUS_ACK wait since 06:26:07 (600s+) without calling mcp__brc__confirm. Tester is frozen in CONSENSUS_ACK wait since 06:31:30 without calling confirm. reviewer_contract is frozen since 06:25:00 and did not wake on tester proposal (producer-scoped event subscription suspected). The BRC ACK events from reviewers may not be reaching the coder/tester event streams, preventing them from learning they have been ACKed and should call confirm. This OVERSEER_ALERT is broadcast to unblock the coder and tester wait_loops so they can check BRC state and proceed to confirm. Human operator: no restart needed; pipeline is healthy but needs coder/tester to call mcp__brc__confirm.\n\nRecommended action:\nCoder and tester should check BRC state and call mcp__brc__confirm. If reviewer_contract is blocking confirmation (needs to ACK tester v1), human may need to NACK reviewer_contract's current wait or send it a direct RE_REVIEW signal for the tester proposal.", + "metadata": {}, + "timestamp": "2026-05-22T06:37:44.928932+00:00", + "phase": "implement" + }, + { + "id": "a26d1a90-5b18-4d", + "pipeline_id": "issue-2769", + "from_role": "tester", + "to_role": "coder", + "message_type": "HANDOFF", + "subject": "make test failing on slice-2 \u2014 repositories.yaml regression + missing TASK-2-6", + "body": "Two blocking issues from my v1 review (full reasoning in the NACK):\n\n(1) gateway/gateway.py is untouched by your commit but TASK-2-6 explicitly lives there. The cq-5 mitigation has TWO halves: the orchestrator-side decision (which is correctly threaded through register_session \u2014 well done on that) AND the gateway-side body rewrite that translates the on-the-wire 'opus' alias to the upstream model name before forwarding to LiteLLM. Without _rewrite_upstream_model in gateway.py:9870-ish, the LiteLLM proxy receives 'opus' and has no idea which backend to dispatch to.\n\n(2) make test is currently failing because resolve_agent_model unconditionally loads repositories.yaml whenever pipeline.repo is set, even when agent_models is empty. Three pre-existing concurrent_executor tests broke as a result (TestSpawnPropagatesContainerInfo, TestRolesOverride, TestSpawnSpecificRoles), so the 'no-op by default' invariant is not actually achieved. Cleanest fix: catch FileNotFoundError in get_default_agent_model and return None.\n\nI'll re-review the moment you re-propose. The non-gateway slice of your diff looks solid, so this is a tightly-scoped follow-up.", + "metadata": {}, + "timestamp": "2026-05-22T07:03:30.123975+00:00", + "phase": "implement" + } +] \ No newline at end of file diff --git a/.egg-state/brc-history/2769-implement-unattributed.md b/.egg-state/brc-history/2769-implement-unattributed.md new file mode 100644 index 0000000000..8d466ab42b --- /dev/null +++ b/.egg-state/brc-history/2769-implement-unattributed.md @@ -0,0 +1,85 @@ +# BRC Consensus History — implement phase, cross-cutting (unattributed) + +Generated: 2026-05-22T07:03:30Z +Pipeline: issue-2769 +Section: cross-cutting (unattributed) + +### [2026-05-22T05:44:25Z] overseer (OVERSEER_ALERT): agent-heartbeat-stall [medium] + +coder (container 8fcde009) has emitted zero heartbeats in 644s, exceeding the 600s silent-agent threshold — no CONSENSUS_PROPOSE yet + +Detail: +Pipeline issue-2769 / slice-1, implement phase. Coder started 2026-05-22T05:33:10Z and has never sent a heartbeat or proposed. Silent threshold (overseer_silent_agent_threshold_seconds=600) is exceeded by ~44s. Downstream impact: tester is WAITING_ON_ROLE:coder with scaffold tests committed; reviewer_code_holistic, reviewer_security, reviewer_contract, reviewer_concurrency all blocking on CONSENSUS_PROPOSE from coder. Documenter BRC cycle is healthy (proposed task-1-12, reviewer_code is reviewing). Coder container status=running — process is alive but has not communicated. Recommend: inspect coder checkpoint logs to determine if it is blocked on a gateway call, LLM call, or file operation. + +Recommended action: +Run `egg-checkpoint show` on the most recent coder checkpoint for pipeline issue-2769/slice-1 to inspect progress. If the coder is stuck on a gateway credential or LLM call timeout, consider a targeted restart of just the coder container. + +````yaml +id: 4739cde4-1a36-4c +phase: implement +```` + +### [2026-05-22T05:48:03Z] overseer (OVERSEER_ALERT): agent-heartbeat-stall [high] + +coder silent for 833s — all code-reviewers and tester blocked; pipeline issue-2769/slice-1 requires human decision on coder restart + +Detail: +Coder container 8fcde009 started at 05:33:10Z and has emitted ZERO heartbeats, checkpoints, or proposals after 833s. Silent threshold (600s) exceeded by 233s. The coder did not respond to the prior medium-priority overseer alert (4739cde4) because it was not in a message wait loop. Downstream blocked: reviewer_code_holistic, reviewer_security, reviewer_contract, reviewer_concurrency all await coder CONSENSUS_PROPOSE; tester committed scaffold tests locally and awaits coder commits on origin/egg/issue-2769/slice-1. Documenter proposal (task-1-12, commit 8c68062f) is at 1 ACK (reviewer_code), stalled because other reviewers are scoped to coder output. overseer_auto_file_issues_mode=shadow so no auto-action was taken. Phase long-running threshold is 3600s (not yet hit). Coder is doing a large multi-file gateway refactor (gateway/upstream_registry.py new file + edits to gateway/gateway.py, anthropic_credentials.py, session_manager.py, orchestrator/gateway_client.py, k8s manifests) which could legitimately take 15-20 min, but zero communication is anomalous. + +Recommended action: +Option A (Restart): Restart coder container 8fcde009 on pipeline issue-2769/slice-1 to unblock reviewers and tester. Option B (Wait): Allow up to 20 more minutes (total ~35 min) given large scope of gateway refactor. Option C (Inspect): Attach to coder container directly to check if it is mid-LLM-call or stuck on a gateway credential fetch before deciding. + +````yaml +id: 70976add-0201-4b +phase: implement +```` + +### [2026-05-22T05:57:21Z] overseer (HEARTBEAT): heartbeat: WORKING + +Overseer monitoring pipeline issue-2769/slice-1 implement phase. Coder (container 8fcde009) has been silent for 1421s — zero heartbeats, checkpoints, or proposals. Two overseer alerts broadcast (medium at 644s, high at 833s). All 7 other agents healthy. Pipeline not terminal. Advisor gate now open. Continuing monitoring. + +````yaml +id: 119f13c4-f5ac-4e +phase: implement +metadata: + state: WORKING +```` + +### [2026-05-22T06:18:17Z] tester → coder (HANDOFF): make lint failing on gateway/gateway.py format + +ruff format --check gateway/gateway.py fails at lines 8678-8683 (the new Invalid upstream make_error call). Fix: ruff format gateway/gateway.py — the diff is a one-liner. NACK already sent on your v1 proposal (see CONSENSUS_NACK). I will re-review and ACK once you re-propose with the format fix. + +````yaml +id: 040be711-e749-48 +phase: implement +```` + +### [2026-05-22T06:37:44Z] overseer (OVERSEER_ALERT): stuck-phase-transition [medium] + +BRC confirmation livelock: 5 reviewers have ACKed + tried confirm (all false); coder and tester never called confirm + +Detail: +All 3 producers have proposed (documenter, coder v3 commit f5076a866, tester v1 commit bf857bf8b). All 5 reviewers have ACKed and attempted CONSENSUS_CONFIRMED between 06:28:57–06:31:58 (all consensus_reached: false). Coder is frozen in CONSENSUS_ACK wait since 06:26:07 (600s+) without calling mcp__brc__confirm. Tester is frozen in CONSENSUS_ACK wait since 06:31:30 without calling confirm. reviewer_contract is frozen since 06:25:00 and did not wake on tester proposal (producer-scoped event subscription suspected). The BRC ACK events from reviewers may not be reaching the coder/tester event streams, preventing them from learning they have been ACKed and should call confirm. This OVERSEER_ALERT is broadcast to unblock the coder and tester wait_loops so they can check BRC state and proceed to confirm. Human operator: no restart needed; pipeline is healthy but needs coder/tester to call mcp__brc__confirm. + +Recommended action: +Coder and tester should check BRC state and call mcp__brc__confirm. If reviewer_contract is blocking confirmation (needs to ACK tester v1), human may need to NACK reviewer_contract's current wait or send it a direct RE_REVIEW signal for the tester proposal. + +````yaml +id: 1f8c0a1d-d07f-41 +phase: implement +```` + +### [2026-05-22T07:03:30Z] tester → coder (HANDOFF): make test failing on slice-2 — repositories.yaml regression + missing TASK-2-6 + +Two blocking issues from my v1 review (full reasoning in the NACK): + +(1) gateway/gateway.py is untouched by your commit but TASK-2-6 explicitly lives there. The cq-5 mitigation has TWO halves: the orchestrator-side decision (which is correctly threaded through register_session — well done on that) AND the gateway-side body rewrite that translates the on-the-wire 'opus' alias to the upstream model name before forwarding to LiteLLM. Without _rewrite_upstream_model in gateway.py:9870-ish, the LiteLLM proxy receives 'opus' and has no idea which backend to dispatch to. + +(2) make test is currently failing because resolve_agent_model unconditionally loads repositories.yaml whenever pipeline.repo is set, even when agent_models is empty. Three pre-existing concurrent_executor tests broke as a result (TestSpawnPropagatesContainerInfo, TestRolesOverride, TestSpawnSpecificRoles), so the 'no-op by default' invariant is not actually achieved. Cleanest fix: catch FileNotFoundError in get_default_agent_model and return None. + +I'll re-review the moment you re-propose. The non-gateway slice of your diff looks solid, so this is a tightly-scoped follow-up. + +````yaml +id: a26d1a90-5b18-4d +phase: implement +```` diff --git a/.egg-state/brc-history/2769-plan.json b/.egg-state/brc-history/2769-plan.json new file mode 100644 index 0000000000..c7c7c19467 --- /dev/null +++ b/.egg-state/brc-history/2769-plan.json @@ -0,0 +1,1534 @@ +[ + { + "id": "35aef3af-00b8-43", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "Overseer monitoring plan phase. 4 agents active (architect, task_planner, reviewer_plan, risk_analyst) at ~2min elapsed. No alerts. Nominal.", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:21:14.334066+00:00", + "phase": "plan" + }, + { + "id": "83c6031a-7abf-44", + "pipeline_id": "issue-2769", + "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-22T03:22:34.673673+00:00" + }, + "timestamp": "2026-05-22T03:22:34.720746+00:00", + "phase": "plan" + }, + { + "id": "a12cf2f1-0f0d-4a", + "pipeline_id": "issue-2769", + "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-22T03:22:34.673673+00:00" + }, + "timestamp": "2026-05-22T03:23:34.876211+00:00", + "phase": "plan" + }, + { + "id": "7102e57b-bf5e-44", + "pipeline_id": "issue-2769", + "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-22T03:22:34.673673+00:00" + }, + "timestamp": "2026-05-22T03:24:35.004185+00:00", + "phase": "plan" + }, + { + "id": "83c13591-c54c-4c", + "pipeline_id": "issue-2769", + "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-22T03:22:34.673673+00:00" + }, + "timestamp": "2026-05-22T03:25:35.085681+00:00", + "phase": "plan" + }, + { + "id": "2b60b75b-cf76-4c", + "pipeline_id": "issue-2769", + "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-22T03:22:34.673673+00:00" + }, + "timestamp": "2026-05-22T03:26:35.186814+00:00", + "phase": "plan" + }, + { + "id": "355c4439-56c8-46", + "pipeline_id": "issue-2769", + "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-22T03:22:34.673673+00:00" + }, + "timestamp": "2026-05-22T03:27:35.282194+00:00", + "phase": "plan" + }, + { + "id": "054df3da-2bb2-42", + "pipeline_id": "issue-2769", + "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-22T03:22:34.673673+00:00" + }, + "timestamp": "2026-05-22T03:28:50.070699+00:00", + "phase": "plan" + }, + { + "id": "1ed8d22d-503c-47", + "pipeline_id": "issue-2769", + "from_role": "architect", + "to_role": "all", + "message_type": "CONSENSUS_PROPOSE", + "subject": "Proposal from architect", + "body": "Architecture analysis for non-Claude models via LiteLLM proxy. Recommends Option A (gateway-side UpstreamRegistry keyed by per-agent session metadata; LiteLLM as sibling Deployment in egg-system) over options B (LiteLLM-fronts-everything, rejected \u2014 violates no-regression-on-Claude), C (route on body, rejected \u2014 conflicts with compaction-math mitigation), and D (egg_agent SDK path, rejected \u2014 cq-5 forbids). Decomposes work into two dependent slices: slice-1 ships the gateway router + LiteLLM topology as a no-op by default; slice-2 adds PipelineConfig.agent_models + repositories.yaml default_model + resolve_model_for_role() helper that drives BOTH the consensus_wrapper --model flag (recognized Claude alias, keeps compaction math sane) AND the gateway's session.agent_upstream/agent_model_litellm fields (decides actual backend). Honors every cq-* HITL: cq-1 separate Deployment+Service, cq-2 per-agent session metadata, cq-3 PipelineConfig + repositories.yaml, cq-4 no in-pipeline validation, cq-5 keep Claude Code harness, cq-6 hosted Qwen, cq-7 gateway holds master key, cq-8 fail-closed, cq-9 uniform tool-strip, cq-11 leave opus[1m]. Surfaces every runtime primitive with file:line evidence and explicit purpose / execution-context labels per #2594 (gateway pod vs orchestrator pod vs in-sandbox-agent). Surfaces seven open risks (R1-R7) for the risk_analyst and seeds nine acceptance criteria for the task_planner.", + "metadata": { + "payload": { + "summary": "Architecture analysis for non-Claude models via LiteLLM proxy. Recommends Option A (gateway-side UpstreamRegistry keyed by per-agent session metadata; LiteLLM as sibling Deployment in egg-system) over options B (LiteLLM-fronts-everything, rejected \u2014 violates no-regression-on-Claude), C (route on body, rejected \u2014 conflicts with compaction-math mitigation), and D (egg_agent SDK path, rejected \u2014 cq-5 forbids). Decomposes work into two dependent slices: slice-1 ships the gateway router + LiteLLM topology as a no-op by default; slice-2 adds PipelineConfig.agent_models + repositories.yaml default_model + resolve_model_for_role() helper that drives BOTH the consensus_wrapper --model flag (recognized Claude alias, keeps compaction math sane) AND the gateway's session.agent_upstream/agent_model_litellm fields (decides actual backend). Honors every cq-* HITL: cq-1 separate Deployment+Service, cq-2 per-agent session metadata, cq-3 PipelineConfig + repositories.yaml, cq-4 no in-pipeline validation, cq-5 keep Claude Code harness, cq-6 hosted Qwen, cq-7 gateway holds master key, cq-8 fail-closed, cq-9 uniform tool-strip, cq-11 leave opus[1m]. Surfaces every runtime primitive with file:line evidence and explicit purpose / execution-context labels per #2594 (gateway pod vs orchestrator pod vs in-sandbox-agent). Surfaces seven open risks (R1-R7) for the risk_analyst and seeds nine acceptance criteria for the task_planner.", + "attestation": {}, + "artifacts": [ + ".egg-state/agent-outputs/2769-architect-output.json" + ], + "risk_considered": "No-regression-on-Claude is preserved by making the Anthropic upstream byte-for-byte identical under the new UpstreamRegistry (same base_url, timeouts, limits, credential injection, SSE accumulator, retry policy, tool-strip). Inert-by-default: without an opt-in via PipelineConfig.agent_models or repositories.yaml default_model, no LiteLLM request ever fires. Drift between Claude Code's --model and gateway's session.agent_model_litellm is the primary residual risk (R3) \u2014 mitigated by making resolve_model_for_role the SINGLE function both callers consume, with an end-to-end test asserting the same tuple flows to both sites. Supply-chain risk (R4, refine feedback Q3) mitigated by pinning the LiteLLM image to a digest in k8s manifest and by the UpstreamRegistry seam being one-entry-swap-out. Backward-compat across rolling updates (R7) handled by defaulting agent_upstream=None to the anthropic upstream.", + "commit_sha": "851ef4393", + "files_changed": [ + ".egg-state/agent-outputs/2769-architect-output.json" + ], + "tests_run": [], + "tasks_satisfied": [] + }, + "version": 1, + "commit_sha": "851ef4393" + }, + "timestamp": "2026-05-22T03:29:15.387766+00:00", + "phase": "plan" + }, + { + "id": "3bc16a6d-9f0a-42", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:29:15.427620+00:00", + "phase": "plan" + }, + { + "id": "70448dcc-070a-4e", + "pipeline_id": "issue-2769", + "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-22T03:30:09.076660+00:00" + }, + "timestamp": "2026-05-22T03:30:09.191266+00:00", + "phase": "plan" + }, + { + "id": "c248c1f7-3ac5-49", + "pipeline_id": "issue-2769", + "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-22T03:30:11.830823+00:00" + }, + "timestamp": "2026-05-22T03:30:11.878456+00:00", + "phase": "plan" + }, + { + "id": "67d7eaf4-8872-45", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:30:39.390126+00:00", + "phase": "plan" + }, + { + "id": "1d548e27-40c4-44", + "pipeline_id": "issue-2769", + "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-22T03:30:11.830823+00:00" + }, + "timestamp": "2026-05-22T03:31:12.039405+00:00", + "phase": "plan" + }, + { + "id": "44b52bf9-ca0b-41", + "pipeline_id": "issue-2769", + "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-22T03:30:11.830823+00:00" + }, + "timestamp": "2026-05-22T03:32:12.102441+00:00", + "phase": "plan" + }, + { + "id": "7dec4b0d-6925-48", + "pipeline_id": "issue-2769", + "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-22T03:30:11.830823+00:00" + }, + "timestamp": "2026-05-22T03:33:12.160470+00:00", + "phase": "plan" + }, + { + "id": "f6650463-d51a-48", + "pipeline_id": "issue-2769", + "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-22T03:30:11.830823+00:00" + }, + "timestamp": "2026-05-22T03:34:12.228034+00:00", + "phase": "plan" + }, + { + "id": "81904061-5005-4c", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "to_role": "all", + "message_type": "CONSENSUS_PROPOSE", + "subject": "Proposal from task_planner", + "body": "Plan for #2769 (non-Claude models via LiteLLM proxy). Two dependent slices per cq-10: slice-1 lands the gateway UpstreamRegistry seam + Session.upstream/upstream_model + upstream-aware credential injection + LiteLLM k8s manifests with empty model_list (Claude path byte-identical, no-op by default); slice-2 adds PipelineConfig.agent_models + repositories.yaml default_agent_model + a precedence-aware resolver + body rewrite that keeps Claude Code seeing the recognised alias while LiteLLM sees the real upstream model name (cq-5 mitigation). 21 tasks across coder/tester/documenter roles. cq-4 empirical agent flip explicitly out of scope. Primitives table cites every existing symbol the plan touches with current file:line and marks the two NEW primitives (UpstreamRegistry, PipelineConfig.agent_models) against their creating tasks.", + "metadata": { + "payload": { + "summary": "Plan for #2769 (non-Claude models via LiteLLM proxy). Two dependent slices per cq-10: slice-1 lands the gateway UpstreamRegistry seam + Session.upstream/upstream_model + upstream-aware credential injection + LiteLLM k8s manifests with empty model_list (Claude path byte-identical, no-op by default); slice-2 adds PipelineConfig.agent_models + repositories.yaml default_agent_model + a precedence-aware resolver + body rewrite that keeps Claude Code seeing the recognised alias while LiteLLM sees the real upstream model name (cq-5 mitigation). 21 tasks across coder/tester/documenter roles. cq-4 empirical agent flip explicitly out of scope. Primitives table cites every existing symbol the plan touches with current file:line and marks the two NEW primitives (UpstreamRegistry, PipelineConfig.agent_models) against their creating tasks.", + "attestation": {}, + "artifacts": [ + ".egg-state/drafts/2769-plan.md" + ], + "risk_considered": "R1 gateway file-size discipline (#2261) \u2014 new symbols land in gateway/upstream_registry.py and the body-rewrite helper sits next to _filter_blocked_tools; net delta to gateway.py is small. R2 Session-field back-compat \u2014 Session.from_persistence already tolerates absent fields; new upstream defaults to \"anthropic\". R3 credential drift \u2014 registry pairs client and resolver together so the wrong credential cannot reach the wrong upstream. R4 LiteLLM supply-chain blast radius \u2014 confined by topology (separate Deployment, not sidecar) per cq-1 and by UpstreamRegistry seam per feedback Q3. R5 Claude Code compaction math \u2014 cq-5 mitigation: Claude Code always sees a recognised Claude alias (default opus) for LiteLLM-routed agents; body rewrite happens gateway-side after _filter_blocked_tools.", + "commit_sha": "5ef34fba2e6a51549b80245f7a977720dddcc1bd", + "files_changed": [ + ".egg-state/drafts/2769-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-1-10", + "TASK-1-11", + "TASK-1-12", + "TASK-2-1", + "TASK-2-2", + "TASK-2-3", + "TASK-2-4", + "TASK-2-5", + "TASK-2-6", + "TASK-2-7", + "TASK-2-8", + "TASK-2-9" + ] + }, + "version": 1, + "commit_sha": "5ef34fba2e6a51549b80245f7a977720dddcc1bd" + }, + "timestamp": "2026-05-22T03:34:20.184003+00:00", + "phase": "plan" + }, + { + "id": "f196876c-371b-42", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:34:41.677573+00:00", + "phase": "plan" + }, + { + "id": "ea2b2e4d-b5cd-49", + "pipeline_id": "issue-2769", + "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-22T03:30:11.830823+00:00" + }, + "timestamp": "2026-05-22T03:35:12.302740+00:00", + "phase": "plan" + }, + { + "id": "b869de68-c93f-42", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:35:41.201331+00:00", + "phase": "plan" + }, + { + "id": "46b66267-16ab-4f", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "architect", + "message_type": "CONSENSUS_NACK", + "subject": "NACK from reviewer_plan for architect", + "body": "\nI read the full architect JSON (`.egg-state/agent-outputs/2769-architect-output.json`), spot-grepped every cited primitive (all 25 exist at the cited file:line \u2014 see audit table below), and cross-checked the design against the HITL resolutions and feedback answers. The structural shape is right and the no-op-by-default framing is well-defended. **One blocking architectural gap and a small bundle of non-blocking items follow.**\n\n### Primitive-Existence Audit (#2594) \u2014 all PASS\n\n| primitive | grep | result |\n|-----------|------|--------|\n| `_anthropic_client` / `get_anthropic_client()` | `grep -n 'def get_anthropic_client' gateway/gateway.py` | `gateway/gateway.py:9320` \u2713 |\n| `proxy_anthropic_messages` POST `/v1/messages` | `grep -n 'def proxy_anthropic_messages' gateway/gateway.py` | `gateway/gateway.py:9752` \u2713 |\n| `proxy_count_tokens` POST `/v1/messages/count_tokens` | `grep -n 'def proxy_count_tokens' gateway/gateway.py` | `gateway/gateway.py:10019` \u2713 |\n| `_inject_anthropic_credentials` | `grep -n '_inject_anthropic_credentials' gateway/gateway.py` | `gateway/gateway.py:9355` \u2713 |\n| `_filter_blocked_tools` | `grep -n 'def _filter_blocked_tools' gateway/gateway.py` | `gateway/gateway.py:9410` \u2713 |\n| `_SSEAccumulator` | `grep -n 'class _SSEAccumulator' gateway/gateway.py` | `gateway/gateway.py:9552` \u2713 |\n| `get_session_by_ip` | `grep -n 'def get_session_by_ip' gateway/session_manager.py` | `gateway/session_manager.py:741` \u2713 |\n| `Session` dataclass / `agent_role` field | `grep -n '@dataclass\\\\|agent_role' gateway/session_manager.py` | `gateway/session_manager.py:288, agent_role at :314` \u2713 |\n| `AnthropicCredential` (header_name / header_value) | `grep -n 'class AnthropicCredential' gateway/anthropic_credentials.py` | `gateway/anthropic_credentials.py:36\u201349` \u2713 |\n| `register_session()` | `grep -n 'def register_session' gateway/session_manager.py` | `gateway/session_manager.py:548` \u2713 |\n| `POST /api/v1/sessions/create` | `grep -n '/api/v1/sessions/create' gateway/gateway.py` | `gateway/gateway.py:8507` \u2713 |\n| `build_consensus_wrapped_command(model='opus')` | `grep -n 'def build_consensus_wrapped_command' orchestrator/consensus_wrapper.py` | `orchestrator/consensus_wrapper.py:620, default model='opus' at :622, '--model' arg at :654` \u2713 |\n| Call sites with NO model arg | `grep -n 'build_consensus_wrapped_command' orchestrator/concurrent_executor.py orchestrator/routes/pipelines.py` | `concurrent_executor.py:454, routes/pipelines.py:2704` \u2713 |\n| `PipelineConfig.overseer_decision_maker_model` / `overseer_advisor_model` | `grep -n 'overseer_decision_maker_model\\\\|overseer_advisor_model' orchestrator/models.py` | `orchestrator/models.py:546, :620` \u2713 |\n| `_PROTECTED_ENV_KEYS` | `grep -n '_PROTECTED_ENV_KEYS' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:138` \u2713 |\n| `ANTHROPIC_BASE_URL = GATEWAY_K8S_URL` injection | `grep -n 'ANTHROPIC_BASE_URL' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:807; GATEWAY_K8S_URL at :124` \u2713 |\n| `setup_anthropic_api()` | `grep -n 'def setup_anthropic_api' sandbox/entrypoint.py` | `sandbox/entrypoint.py:737` \u2713 |\n| `DEFAULT_MODEL = 'opus[1m]'` | `grep -n 'DEFAULT_MODEL' shared/egg_agent/client.py` | `shared/egg_agent/client.py:62` \u2713 |\n| `--model` default `opus[1m]` | `grep -n '\\\"--model\\\"' shared/egg_agent/__main__.py` | `shared/egg_agent/__main__.py:35` \u2713 |\n| `['--model','opus[1m]']` in legacy runner | `grep -n 'opus\\\\[1m\\\\]' sandbox/llm/runner.py` | `sandbox/llm/runner.py:49` \u2713 |\n| `allowed_domains.txt` Anthropic excluded | `grep -n 'anthropic' gateway/allowed_domains.txt` | `gateway/allowed_domains.txt:9\u201317` \u2713 (explicitly comments rationale) |\n| `k8s/base/gateway-deployment.yaml` `/secrets` mount | inspected file | mount at `:142\u2013155`, Secret `gateway-secrets` \u2713 |\n| `config/repo_config.py` `get_repo_setting` | `grep -n 'def get_repo_setting' config/repo_config.py` | `config/repo_config.py:248` \u2713 |\n| `config/repositories.yaml` (live file the helper reads) | `ls config/repositories.yaml*` | **`config/repositories.yaml.example` only \u2014 live file is operator-supplied at runtime** (see non-blocker #4 below) |\n\nAll NEW primitives the architect introduces (`UpstreamRegistry`, `LiteLLMCredentialsManager`, `Session.agent_upstream`, `Session.agent_model_litellm`, `k8s/base/litellm-deployment.yaml`, `k8s/base/litellm-service.yaml`, `LITELLM_MASTER_KEY` secret entry, `PipelineConfig.agent_models`, `resolve_model_for_role`, `config/repo_config.get_default_model`, `gateway/tests/test_upstream_router.py`, `orchestrator/tests/test_model_resolver.py`) are unambiguously net-new and the existing slice-1/slice-2 components create them \u2014 no false-NACK on missing-grep evidence per the \u00a79 exception rule.\n\n### Trust-Boundary Audit (\u00a710) \u2014 PASS\n\nAll proposed tests live under `gateway/tests/` and `orchestrator/tests/`, both unit-test execution contexts (`make test` / pytest from the trusted-CI-runner). The architect explicitly avoids any test that needs a live LiteLLM endpoint (`httpx.MockTransport`-driven). No `integration_tests/` work is proposed that would hit the in-sandbox-agent vs trusted-CI-runner fixture trap. No `@require_lifecycle_secret` route invocations from in-sandbox-agent contexts. No `ScriptedProvider` references. The cq-4 \"no in-pipeline validation\" boundary is honored.\n\n### Slice-Sizing Advisory (#2137 opt-2 advisory only \u2014 non-blocking)\n\n- **slice-1** (gateway router + LiteLLM topology, 7 components + 2 test files): rough estimate ~600\u2013900 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n- **slice-2** (per-agent model config + plumbing, 6 components + 3 test files): rough estimate ~400\u2013700 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n\n### Blocking\n\n1. **The design records `Session.agent_model_litellm` but does not specify how the gateway consumes it on a LiteLLM-bound request \u2014 the request body's `model` field is left unaltered.** This is a load-bearing ambiguity that fundamentally changes the operator-facing contract and the test surface.\n\n Concrete evidence of the gap, from the architect JSON:\n - Slice-1 component **\"Upstream-aware credential injection\"** (`gateway/gateway.py:9355`) mutates headers only.\n - Slice-1 component **\"Session-driven upstream resolver\"** returns an upstream name only.\n - Slice-1 component **\"Session-storage extensions\"** stores `agent_model_litellm: str|None (e.g. 'qwen-2.5-coder' \u2014 the model alias LiteLLM's model_list will map)`.\n - Slice-1 AC-2 only verifies \"request lands at the litellm client (verify base_url and Authorization: Bearer )\" \u2014 *not* what the body's `model` field looks like on the wire.\n - Slice-2 AC-6 only asserts the *recorded session metadata*, not what the gateway forwards to LiteLLM.\n - The compaction-math mitigation (the issue's primary risk per analysis lines 49\u201359) requires Claude Code to *see* a recognized Claude alias in its `--model` flag \u2014 so the body's `model` field is `opus` (or whatever recognized alias `resolve_model_for_role` returns as the first tuple element).\n\n The architect needs to commit to one of two semantics for the LiteLLM-bound proxy path. Both are buildable; the design implications are very different:\n\n **Semantics A \u2014 \"body is dispatch, agent_model_litellm is metadata\":**\n - Gateway forwards the request body byte-unchanged. LiteLLM dispatches on `body[\"model\"]`.\n - Operator MUST configure LiteLLM's `model_list` with the Claude alias as the dispatch key, e.g. `model_list: [{model_name: \"opus\", litellm_params: {model: \"openrouter/qwen/qwen3-coder\", ...}}]`. The string `\"opus\"` in the LiteLLM config does NOT mean Opus; it is the alias the operator's gateway sends.\n - `Session.agent_model_litellm` is operational metadata (logging, observability, future cost-tracking #2769-followup) and is not used at request time.\n - Acceptance test must verify the request body forwarded to LiteLLM has `model=\"opus\"` byte-for-byte.\n - Operator docs must call out the \"LiteLLM model_list keys are Claude aliases\" constraint loud and early \u2014 any operator who configures `model_name: \"qwen3-coder\"` in `model_list` will see immediate breakage.\n\n **Semantics B \u2014 \"gateway rewrites body.model before forwarding\":**\n - A NEW component (currently missing from slice-1) sits between the credential injection and `client.send(http_req, stream=True)` at `gateway/gateway.py:9827` and, when `upstream='litellm'`, rewrites `body[\"model\"]` from the Claude alias to `session.agent_model_litellm`.\n - Operator configures LiteLLM's `model_list` with the natural model names: `model_list: [{model_name: \"qwen3-coder\", litellm_params: {...}}]`.\n - `Session.agent_model_litellm` is functional and required at request time.\n - Acceptance test must verify the request body is mutated (`body[\"model\"]` changes from `\"opus\"` to `\"qwen3-coder\"`) before reaching LiteLLM.\n - Same rewrite is required for `proxy_count_tokens` so the token count returned to Claude Code matches the model that actually runs.\n\n **Fix:** Re-propose with one semantics chosen explicitly. If A, add a short \"LiteLLM model_list operator constraint\" sub-section under `key_constraints_carried_from_refine` and re-label `agent_model_litellm` as \"operational metadata\" in the Session-storage component. If B, add a slice-1 component \"Request-body model rewriter for LiteLLM-bound requests\" (`gateway/gateway.py`, between credential injection and `client.send`), bump AC-2 to assert body mutation, and add an equivalent assertion for `proxy_count_tokens`. Either path satisfies the refine HITL resolutions; both are buildable; the planner cannot convert components to tasks until this is pinned.\n\n Why this is blocking now (not a planner-detail): the choice changes (a) what test fixtures slice-1 needs, (b) whether the LiteLLM Deployment manifest needs a sample `model_list` that demonstrates the alias-as-key pattern (Semantics A) vs the natural-key pattern (Semantics B), (c) whether `Session.agent_model_litellm` should even exist as a typed field (under A it could be a free-form `metadata` blob), and (d) the operator-facing documentation surface. Letting the planner pick produces incoherent task boundaries.\n\n### Non-blocking\n\n- **R8 (recommended addition to `open_risks_for_risk_analyst`)** \u2014 **`/v1/messages/count_tokens` semantics on the LiteLLM path with Semantics A.** Even after #1 is resolved, the count_tokens response from LiteLLM under Semantics A is *for the Claude alias Claude Code passed*, not for the actual backend. Claude Code uses this count to drive compaction. If the LiteLLM model_list maps `\"opus\" \u2192 qwen3-coder`, LiteLLM may compute tokens with the wrong tokenizer (Claude tokenizer vs Qwen tokenizer have different boundaries). Under Semantics B the rewrite ensures LiteLLM uses the right tokenizer. Worth surfacing for the risk_analyst to weigh.\n\n- **`config/repositories.yaml` is not in-repo today** \u2014 only `config/repositories.yaml.example` exists (operator drops the live file at runtime via the EGG_REPO_CONFIG / EGG_SECRETS_PATH mount). The slice-2 `get_default_model(repo)` helper must handle the missing-file case the same way `get_repo_setting` does today (defaults to None silently). Add an AC: \"AC-10: When `config/repositories.yaml` is absent or contains no `default_model` for the queried repo, `get_default_model(repo)` returns `None` and the resolver falls back to the built-in `'opus'` default.\" Without this, dev environments without an opted-in repo config break loudly the moment slice-2 ships.\n\n- **`LITELLM_BASE_URL` env-var injection into the gateway pod is not in `key_files_changed`.** The architect mentions \"base_url from env LITELLM_BASE_URL, default http://litellm.egg-system.svc.cluster.local:4000\" but doesn't list `k8s/base/gateway-deployment.yaml` as edited for slice-1. The env var has to be declared on the gateway pod (under `env:` in the container spec) for the default to be overridable. Add `k8s/base/gateway-deployment.yaml` to `production_code_slice_1` with a one-liner noting \"add `LITELLM_BASE_URL` env var (optional, defaults to in-cluster service DNS)\". If the architect's intent is \"no env var, hard-coded default\", say so explicitly \u2014 but then the seam loses its swap-out flexibility for the refine-Q3 supply-chain mitigation.\n\n- **`PipelineConfig.agent_models: dict[str, str]` should be typed against the AgentRole enum.** A free-string key invites typos (`'reviewer-refine'` vs `'reviewer_refine'` vs `'refiner_review'`) that silently never resolve. The existing per-phase consensus-timeout pattern at `orchestrator/models.py:452\u2013474` uses *separate fields* per phase, not a dict, precisely because Pydantic validation cannot easily restrict dict keys to an enum without a `field_validator`. Recommendation: either (a) typed as `dict[AgentRole, str]` with a Pydantic v2 `field_validator` that coerces and validates string keys, or (b) split into per-role explicit fields following the overseer-model precedent. Planner can pick, but the architect should call out that key-validation is required (not assumed).\n\n- **Cost-tracking interaction.** `max_llm_cost_per_hour` envelope at `orchestrator/overseer/self_monitor.py:30\u2013130` assumes Anthropic pricing. Refine feedback Q4 deferred extension to a follow-up, which the architect correctly notes. But: what is the *behavior* when a LiteLLM-bound agent's tokens flow through the existing cost tracker today? Does it crash, no-op silently, or mis-price? Even an `# explicitly out of scope but documented behavior: cost-tracker will mis-price non-Anthropic tokens until follow-up #XXXX` note would prevent a downstream operator surprise. Add a one-line entry under `out_of_scope_explicitly` calling out the *observable behavior* (e.g. \"cost tracker continues to apply Anthropic pricing to LiteLLM token counts; the tracked dollar figure will be incorrect for non-Claude agents until #XXXX lands \u2014 does NOT block the agent from running\").\n\n- **NetworkPolicy egress allowlist is left vague.** \"Egress to allowlisted backend hosts only\" \u2014 for the cq-6 hosted-Qwen-first first cut, *which hosts*? The architect should at least name an example (`api.together.xyz`, `api.fireworks.ai`, etc.) and clarify that the NetworkPolicy egress list is *operator-supplied per backend choice* (probably via a kustomize overlay), not hard-coded in `k8s/base/network-policies.yaml`. Otherwise a reader of the slice-1 PR will not know what to put in the egress block.\n\n- **`config/repo_config.py` helper barrel-decomposition cross-check.** The architect places the new `get_default_model(repo)` near `get_repo_setting` at `config/repo_config.py:248`. Sanity-check that this file isn't itself slated for in-flight decomposition (#2261) \u2014 a quick grep of `config/CLAUDE.md` if present, or a check in `orchestrator/CLAUDE.md`. Not blocking; just a sanity check the planner should make before placing the helper.\n\n- **Slice-1 SSE acceptance test (AC-2 part (e)) is good but should include a \"client disconnect mid-stream\" path against the LiteLLM upstream.** The existing pre-stream retry (`gateway/gateway.py:9850`) and mid-stream synthetic SSE error frame (`gateway/gateway.py:9920ish`) are exercised against `httpx.MockTransport` for both upstreams; explicitly include the client-disconnect mid-stream case to assert the `_SSEAccumulator` flushes the partial response identically. Marginal cost in the slice-1 test budget; high value because this is exactly the path that's hardest to debug post-deploy.\n\n### Summary\n\nDesign is sound on the architectural axis the analysis prescribed (Option A with cq-1..11 resolutions respected) and the cited primitives all exist. The one blocking gap (#1) is the missing commitment on how `agent_model_litellm` reaches the wire \u2014 that single decision drives slice-1's test plan, the operator's LiteLLM config, the LiteLLM Deployment's sample manifest, and the documentation surface. Address #1 and re-propose; the non-blocking items can roll into the re-propose or be picked up by the planner.\n", + "metadata": { + "payload": { + "reason": "\nI read the full architect JSON (`.egg-state/agent-outputs/2769-architect-output.json`), spot-grepped every cited primitive (all 25 exist at the cited file:line \u2014 see audit table below), and cross-checked the design against the HITL resolutions and feedback answers. The structural shape is right and the no-op-by-default framing is well-defended. **One blocking architectural gap and a small bundle of non-blocking items follow.**\n\n### Primitive-Existence Audit (#2594) \u2014 all PASS\n\n| primitive | grep | result |\n|-----------|------|--------|\n| `_anthropic_client` / `get_anthropic_client()` | `grep -n 'def get_anthropic_client' gateway/gateway.py` | `gateway/gateway.py:9320` \u2713 |\n| `proxy_anthropic_messages` POST `/v1/messages` | `grep -n 'def proxy_anthropic_messages' gateway/gateway.py` | `gateway/gateway.py:9752` \u2713 |\n| `proxy_count_tokens` POST `/v1/messages/count_tokens` | `grep -n 'def proxy_count_tokens' gateway/gateway.py` | `gateway/gateway.py:10019` \u2713 |\n| `_inject_anthropic_credentials` | `grep -n '_inject_anthropic_credentials' gateway/gateway.py` | `gateway/gateway.py:9355` \u2713 |\n| `_filter_blocked_tools` | `grep -n 'def _filter_blocked_tools' gateway/gateway.py` | `gateway/gateway.py:9410` \u2713 |\n| `_SSEAccumulator` | `grep -n 'class _SSEAccumulator' gateway/gateway.py` | `gateway/gateway.py:9552` \u2713 |\n| `get_session_by_ip` | `grep -n 'def get_session_by_ip' gateway/session_manager.py` | `gateway/session_manager.py:741` \u2713 |\n| `Session` dataclass / `agent_role` field | `grep -n '@dataclass\\\\|agent_role' gateway/session_manager.py` | `gateway/session_manager.py:288, agent_role at :314` \u2713 |\n| `AnthropicCredential` (header_name / header_value) | `grep -n 'class AnthropicCredential' gateway/anthropic_credentials.py` | `gateway/anthropic_credentials.py:36\u201349` \u2713 |\n| `register_session()` | `grep -n 'def register_session' gateway/session_manager.py` | `gateway/session_manager.py:548` \u2713 |\n| `POST /api/v1/sessions/create` | `grep -n '/api/v1/sessions/create' gateway/gateway.py` | `gateway/gateway.py:8507` \u2713 |\n| `build_consensus_wrapped_command(model='opus')` | `grep -n 'def build_consensus_wrapped_command' orchestrator/consensus_wrapper.py` | `orchestrator/consensus_wrapper.py:620, default model='opus' at :622, '--model' arg at :654` \u2713 |\n| Call sites with NO model arg | `grep -n 'build_consensus_wrapped_command' orchestrator/concurrent_executor.py orchestrator/routes/pipelines.py` | `concurrent_executor.py:454, routes/pipelines.py:2704` \u2713 |\n| `PipelineConfig.overseer_decision_maker_model` / `overseer_advisor_model` | `grep -n 'overseer_decision_maker_model\\\\|overseer_advisor_model' orchestrator/models.py` | `orchestrator/models.py:546, :620` \u2713 |\n| `_PROTECTED_ENV_KEYS` | `grep -n '_PROTECTED_ENV_KEYS' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:138` \u2713 |\n| `ANTHROPIC_BASE_URL = GATEWAY_K8S_URL` injection | `grep -n 'ANTHROPIC_BASE_URL' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:807; GATEWAY_K8S_URL at :124` \u2713 |\n| `setup_anthropic_api()` | `grep -n 'def setup_anthropic_api' sandbox/entrypoint.py` | `sandbox/entrypoint.py:737` \u2713 |\n| `DEFAULT_MODEL = 'opus[1m]'` | `grep -n 'DEFAULT_MODEL' shared/egg_agent/client.py` | `shared/egg_agent/client.py:62` \u2713 |\n| `--model` default `opus[1m]` | `grep -n '\\\"--model\\\"' shared/egg_agent/__main__.py` | `shared/egg_agent/__main__.py:35` \u2713 |\n| `['--model','opus[1m]']` in legacy runner | `grep -n 'opus\\\\[1m\\\\]' sandbox/llm/runner.py` | `sandbox/llm/runner.py:49` \u2713 |\n| `allowed_domains.txt` Anthropic excluded | `grep -n 'anthropic' gateway/allowed_domains.txt` | `gateway/allowed_domains.txt:9\u201317` \u2713 (explicitly comments rationale) |\n| `k8s/base/gateway-deployment.yaml` `/secrets` mount | inspected file | mount at `:142\u2013155`, Secret `gateway-secrets` \u2713 |\n| `config/repo_config.py` `get_repo_setting` | `grep -n 'def get_repo_setting' config/repo_config.py` | `config/repo_config.py:248` \u2713 |\n| `config/repositories.yaml` (live file the helper reads) | `ls config/repositories.yaml*` | **`config/repositories.yaml.example` only \u2014 live file is operator-supplied at runtime** (see non-blocker #4 below) |\n\nAll NEW primitives the architect introduces (`UpstreamRegistry`, `LiteLLMCredentialsManager`, `Session.agent_upstream`, `Session.agent_model_litellm`, `k8s/base/litellm-deployment.yaml`, `k8s/base/litellm-service.yaml`, `LITELLM_MASTER_KEY` secret entry, `PipelineConfig.agent_models`, `resolve_model_for_role`, `config/repo_config.get_default_model`, `gateway/tests/test_upstream_router.py`, `orchestrator/tests/test_model_resolver.py`) are unambiguously net-new and the existing slice-1/slice-2 components create them \u2014 no false-NACK on missing-grep evidence per the \u00a79 exception rule.\n\n### Trust-Boundary Audit (\u00a710) \u2014 PASS\n\nAll proposed tests live under `gateway/tests/` and `orchestrator/tests/`, both unit-test execution contexts (`make test` / pytest from the trusted-CI-runner). The architect explicitly avoids any test that needs a live LiteLLM endpoint (`httpx.MockTransport`-driven). No `integration_tests/` work is proposed that would hit the in-sandbox-agent vs trusted-CI-runner fixture trap. No `@require_lifecycle_secret` route invocations from in-sandbox-agent contexts. No `ScriptedProvider` references. The cq-4 \"no in-pipeline validation\" boundary is honored.\n\n### Slice-Sizing Advisory (#2137 opt-2 advisory only \u2014 non-blocking)\n\n- **slice-1** (gateway router + LiteLLM topology, 7 components + 2 test files): rough estimate ~600\u2013900 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n- **slice-2** (per-agent model config + plumbing, 6 components + 3 test files): rough estimate ~400\u2013700 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n\n### Blocking\n\n1. **The design records `Session.agent_model_litellm` but does not specify how the gateway consumes it on a LiteLLM-bound request \u2014 the request body's `model` field is left unaltered.** This is a load-bearing ambiguity that fundamentally changes the operator-facing contract and the test surface.\n\n Concrete evidence of the gap, from the architect JSON:\n - Slice-1 component **\"Upstream-aware credential injection\"** (`gateway/gateway.py:9355`) mutates headers only.\n - Slice-1 component **\"Session-driven upstream resolver\"** returns an upstream name only.\n - Slice-1 component **\"Session-storage extensions\"** stores `agent_model_litellm: str|None (e.g. 'qwen-2.5-coder' \u2014 the model alias LiteLLM's model_list will map)`.\n - Slice-1 AC-2 only verifies \"request lands at the litellm client (verify base_url and Authorization: Bearer )\" \u2014 *not* what the body's `model` field looks like on the wire.\n - Slice-2 AC-6 only asserts the *recorded session metadata*, not what the gateway forwards to LiteLLM.\n - The compaction-math mitigation (the issue's primary risk per analysis lines 49\u201359) requires Claude Code to *see* a recognized Claude alias in its `--model` flag \u2014 so the body's `model` field is `opus` (or whatever recognized alias `resolve_model_for_role` returns as the first tuple element).\n\n The architect needs to commit to one of two semantics for the LiteLLM-bound proxy path. Both are buildable; the design implications are very different:\n\n **Semantics A \u2014 \"body is dispatch, agent_model_litellm is metadata\":**\n - Gateway forwards the request body byte-unchanged. LiteLLM dispatches on `body[\"model\"]`.\n - Operator MUST configure LiteLLM's `model_list` with the Claude alias as the dispatch key, e.g. `model_list: [{model_name: \"opus\", litellm_params: {model: \"openrouter/qwen/qwen3-coder\", ...}}]`. The string `\"opus\"` in the LiteLLM config does NOT mean Opus; it is the alias the operator's gateway sends.\n - `Session.agent_model_litellm` is operational metadata (logging, observability, future cost-tracking #2769-followup) and is not used at request time.\n - Acceptance test must verify the request body forwarded to LiteLLM has `model=\"opus\"` byte-for-byte.\n - Operator docs must call out the \"LiteLLM model_list keys are Claude aliases\" constraint loud and early \u2014 any operator who configures `model_name: \"qwen3-coder\"` in `model_list` will see immediate breakage.\n\n **Semantics B \u2014 \"gateway rewrites body.model before forwarding\":**\n - A NEW component (currently missing from slice-1) sits between the credential injection and `client.send(http_req, stream=True)` at `gateway/gateway.py:9827` and, when `upstream='litellm'`, rewrites `body[\"model\"]` from the Claude alias to `session.agent_model_litellm`.\n - Operator configures LiteLLM's `model_list` with the natural model names: `model_list: [{model_name: \"qwen3-coder\", litellm_params: {...}}]`.\n - `Session.agent_model_litellm` is functional and required at request time.\n - Acceptance test must verify the request body is mutated (`body[\"model\"]` changes from `\"opus\"` to `\"qwen3-coder\"`) before reaching LiteLLM.\n - Same rewrite is required for `proxy_count_tokens` so the token count returned to Claude Code matches the model that actually runs.\n\n **Fix:** Re-propose with one semantics chosen explicitly. If A, add a short \"LiteLLM model_list operator constraint\" sub-section under `key_constraints_carried_from_refine` and re-label `agent_model_litellm` as \"operational metadata\" in the Session-storage component. If B, add a slice-1 component \"Request-body model rewriter for LiteLLM-bound requests\" (`gateway/gateway.py`, between credential injection and `client.send`), bump AC-2 to assert body mutation, and add an equivalent assertion for `proxy_count_tokens`. Either path satisfies the refine HITL resolutions; both are buildable; the planner cannot convert components to tasks until this is pinned.\n\n Why this is blocking now (not a planner-detail): the choice changes (a) what test fixtures slice-1 needs, (b) whether the LiteLLM Deployment manifest needs a sample `model_list` that demonstrates the alias-as-key pattern (Semantics A) vs the natural-key pattern (Semantics B), (c) whether `Session.agent_model_litellm` should even exist as a typed field (under A it could be a free-form `metadata` blob), and (d) the operator-facing documentation surface. Letting the planner pick produces incoherent task boundaries.\n\n### Non-blocking\n\n- **R8 (recommended addition to `open_risks_for_risk_analyst`)** \u2014 **`/v1/messages/count_tokens` semantics on the LiteLLM path with Semantics A.** Even after #1 is resolved, the count_tokens response from LiteLLM under Semantics A is *for the Claude alias Claude Code passed*, not for the actual backend. Claude Code uses this count to drive compaction. If the LiteLLM model_list maps `\"opus\" \u2192 qwen3-coder`, LiteLLM may compute tokens with the wrong tokenizer (Claude tokenizer vs Qwen tokenizer have different boundaries). Under Semantics B the rewrite ensures LiteLLM uses the right tokenizer. Worth surfacing for the risk_analyst to weigh.\n\n- **`config/repositories.yaml` is not in-repo today** \u2014 only `config/repositories.yaml.example` exists (operator drops the live file at runtime via the EGG_REPO_CONFIG / EGG_SECRETS_PATH mount). The slice-2 `get_default_model(repo)` helper must handle the missing-file case the same way `get_repo_setting` does today (defaults to None silently). Add an AC: \"AC-10: When `config/repositories.yaml` is absent or contains no `default_model` for the queried repo, `get_default_model(repo)` returns `None` and the resolver falls back to the built-in `'opus'` default.\" Without this, dev environments without an opted-in repo config break loudly the moment slice-2 ships.\n\n- **`LITELLM_BASE_URL` env-var injection into the gateway pod is not in `key_files_changed`.** The architect mentions \"base_url from env LITELLM_BASE_URL, default http://litellm.egg-system.svc.cluster.local:4000\" but doesn't list `k8s/base/gateway-deployment.yaml` as edited for slice-1. The env var has to be declared on the gateway pod (under `env:` in the container spec) for the default to be overridable. Add `k8s/base/gateway-deployment.yaml` to `production_code_slice_1` with a one-liner noting \"add `LITELLM_BASE_URL` env var (optional, defaults to in-cluster service DNS)\". If the architect's intent is \"no env var, hard-coded default\", say so explicitly \u2014 but then the seam loses its swap-out flexibility for the refine-Q3 supply-chain mitigation.\n\n- **`PipelineConfig.agent_models: dict[str, str]` should be typed against the AgentRole enum.** A free-string key invites typos (`'reviewer-refine'` vs `'reviewer_refine'` vs `'refiner_review'`) that silently never resolve. The existing per-phase consensus-timeout pattern at `orchestrator/models.py:452\u2013474` uses *separate fields* per phase, not a dict, precisely because Pydantic validation cannot easily restrict dict keys to an enum without a `field_validator`. Recommendation: either (a) typed as `dict[AgentRole, str]` with a Pydantic v2 `field_validator` that coerces and validates string keys, or (b) split into per-role explicit fields following the overseer-model precedent. Planner can pick, but the architect should call out that key-validation is required (not assumed).\n\n- **Cost-tracking interaction.** `max_llm_cost_per_hour` envelope at `orchestrator/overseer/self_monitor.py:30\u2013130` assumes Anthropic pricing. Refine feedback Q4 deferred extension to a follow-up, which the architect correctly notes. But: what is the *behavior* when a LiteLLM-bound agent's tokens flow through the existing cost tracker today? Does it crash, no-op silently, or mis-price? Even an `# explicitly out of scope but documented behavior: cost-tracker will mis-price non-Anthropic tokens until follow-up #XXXX` note would prevent a downstream operator surprise. Add a one-line entry under `out_of_scope_explicitly` calling out the *observable behavior* (e.g. \"cost tracker continues to apply Anthropic pricing to LiteLLM token counts; the tracked dollar figure will be incorrect for non-Claude agents until #XXXX lands \u2014 does NOT block the agent from running\").\n\n- **NetworkPolicy egress allowlist is left vague.** \"Egress to allowlisted backend hosts only\" \u2014 for the cq-6 hosted-Qwen-first first cut, *which hosts*? The architect should at least name an example (`api.together.xyz`, `api.fireworks.ai`, etc.) and clarify that the NetworkPolicy egress list is *operator-supplied per backend choice* (probably via a kustomize overlay), not hard-coded in `k8s/base/network-policies.yaml`. Otherwise a reader of the slice-1 PR will not know what to put in the egress block.\n\n- **`config/repo_config.py` helper barrel-decomposition cross-check.** The architect places the new `get_default_model(repo)` near `get_repo_setting` at `config/repo_config.py:248`. Sanity-check that this file isn't itself slated for in-flight decomposition (#2261) \u2014 a quick grep of `config/CLAUDE.md` if present, or a check in `orchestrator/CLAUDE.md`. Not blocking; just a sanity check the planner should make before placing the helper.\n\n- **Slice-1 SSE acceptance test (AC-2 part (e)) is good but should include a \"client disconnect mid-stream\" path against the LiteLLM upstream.** The existing pre-stream retry (`gateway/gateway.py:9850`) and mid-stream synthetic SSE error frame (`gateway/gateway.py:9920ish`) are exercised against `httpx.MockTransport` for both upstreams; explicitly include the client-disconnect mid-stream case to assert the `_SSEAccumulator` flushes the partial response identically. Marginal cost in the slice-1 test budget; high value because this is exactly the path that's hardest to debug post-deploy.\n\n### Summary\n\nDesign is sound on the architectural axis the analysis prescribed (Option A with cq-1..11 resolutions respected) and the cited primitives all exist. The one blocking gap (#1) is the missing commitment on how `agent_model_litellm` reaches the wire \u2014 that single decision drives slice-1's test plan, the operator's LiteLLM config, the LiteLLM Deployment's sample manifest, and the documentation surface. Address #1 and re-propose; the non-blocking items can roll into the re-propose or be picked up by the planner.\n", + "artifact_references": [ + ".egg-state/agent-outputs/2769-architect-output.json", + ".egg-state/drafts/2769-analysis.md", + "gateway/gateway.py", + "gateway/session_manager.py", + "gateway/anthropic_credentials.py", + "gateway/allowed_domains.txt", + "orchestrator/consensus_wrapper.py", + "orchestrator/concurrent_executor.py", + "orchestrator/routes/pipelines.py", + "orchestrator/models.py", + "orchestrator/kubernetes_spawner.py", + "k8s/base/gateway-deployment.yaml", + "config/repositories.yaml.example", + "shared/egg_restrictions/patterns.py" + ], + "nack_version": 1 + }, + "reason": "\nI read the full architect JSON (`.egg-state/agent-outputs/2769-architect-output.json`), spot-grepped every cited primitive (all 25 exist at the cited file:line \u2014 see audit table below), and cross-checked the design against the HITL resolutions and feedback answers. The structural shape is right and the no-op-by-default framing is well-defended. **One blocking architectural gap and a small bundle of non-blocking items follow.**\n\n### Primitive-Existence Audit (#2594) \u2014 all PASS\n\n| primitive | grep | result |\n|-----------|------|--------|\n| `_anthropic_client` / `get_anthropic_client()` | `grep -n 'def get_anthropic_client' gateway/gateway.py` | `gateway/gateway.py:9320` \u2713 |\n| `proxy_anthropic_messages` POST `/v1/messages` | `grep -n 'def proxy_anthropic_messages' gateway/gateway.py` | `gateway/gateway.py:9752` \u2713 |\n| `proxy_count_tokens` POST `/v1/messages/count_tokens` | `grep -n 'def proxy_count_tokens' gateway/gateway.py` | `gateway/gateway.py:10019` \u2713 |\n| `_inject_anthropic_credentials` | `grep -n '_inject_anthropic_credentials' gateway/gateway.py` | `gateway/gateway.py:9355` \u2713 |\n| `_filter_blocked_tools` | `grep -n 'def _filter_blocked_tools' gateway/gateway.py` | `gateway/gateway.py:9410` \u2713 |\n| `_SSEAccumulator` | `grep -n 'class _SSEAccumulator' gateway/gateway.py` | `gateway/gateway.py:9552` \u2713 |\n| `get_session_by_ip` | `grep -n 'def get_session_by_ip' gateway/session_manager.py` | `gateway/session_manager.py:741` \u2713 |\n| `Session` dataclass / `agent_role` field | `grep -n '@dataclass\\\\|agent_role' gateway/session_manager.py` | `gateway/session_manager.py:288, agent_role at :314` \u2713 |\n| `AnthropicCredential` (header_name / header_value) | `grep -n 'class AnthropicCredential' gateway/anthropic_credentials.py` | `gateway/anthropic_credentials.py:36\u201349` \u2713 |\n| `register_session()` | `grep -n 'def register_session' gateway/session_manager.py` | `gateway/session_manager.py:548` \u2713 |\n| `POST /api/v1/sessions/create` | `grep -n '/api/v1/sessions/create' gateway/gateway.py` | `gateway/gateway.py:8507` \u2713 |\n| `build_consensus_wrapped_command(model='opus')` | `grep -n 'def build_consensus_wrapped_command' orchestrator/consensus_wrapper.py` | `orchestrator/consensus_wrapper.py:620, default model='opus' at :622, '--model' arg at :654` \u2713 |\n| Call sites with NO model arg | `grep -n 'build_consensus_wrapped_command' orchestrator/concurrent_executor.py orchestrator/routes/pipelines.py` | `concurrent_executor.py:454, routes/pipelines.py:2704` \u2713 |\n| `PipelineConfig.overseer_decision_maker_model` / `overseer_advisor_model` | `grep -n 'overseer_decision_maker_model\\\\|overseer_advisor_model' orchestrator/models.py` | `orchestrator/models.py:546, :620` \u2713 |\n| `_PROTECTED_ENV_KEYS` | `grep -n '_PROTECTED_ENV_KEYS' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:138` \u2713 |\n| `ANTHROPIC_BASE_URL = GATEWAY_K8S_URL` injection | `grep -n 'ANTHROPIC_BASE_URL' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:807; GATEWAY_K8S_URL at :124` \u2713 |\n| `setup_anthropic_api()` | `grep -n 'def setup_anthropic_api' sandbox/entrypoint.py` | `sandbox/entrypoint.py:737` \u2713 |\n| `DEFAULT_MODEL = 'opus[1m]'` | `grep -n 'DEFAULT_MODEL' shared/egg_agent/client.py` | `shared/egg_agent/client.py:62` \u2713 |\n| `--model` default `opus[1m]` | `grep -n '\\\"--model\\\"' shared/egg_agent/__main__.py` | `shared/egg_agent/__main__.py:35` \u2713 |\n| `['--model','opus[1m]']` in legacy runner | `grep -n 'opus\\\\[1m\\\\]' sandbox/llm/runner.py` | `sandbox/llm/runner.py:49` \u2713 |\n| `allowed_domains.txt` Anthropic excluded | `grep -n 'anthropic' gateway/allowed_domains.txt` | `gateway/allowed_domains.txt:9\u201317` \u2713 (explicitly comments rationale) |\n| `k8s/base/gateway-deployment.yaml` `/secrets` mount | inspected file | mount at `:142\u2013155`, Secret `gateway-secrets` \u2713 |\n| `config/repo_config.py` `get_repo_setting` | `grep -n 'def get_repo_setting' config/repo_config.py` | `config/repo_config.py:248` \u2713 |\n| `config/repositories.yaml` (live file the helper reads) | `ls config/repositories.yaml*` | **`config/repositories.yaml.example` only \u2014 live file is operator-supplied at runtime** (see non-blocker #4 below) |\n\nAll NEW primitives the architect introduces (`UpstreamRegistry`, `LiteLLMCredentialsManager`, `Session.agent_upstream`, `Session.agent_model_litellm`, `k8s/base/litellm-deployment.yaml`, `k8s/base/litellm-service.yaml`, `LITELLM_MASTER_KEY` secret entry, `PipelineConfig.agent_models`, `resolve_model_for_role`, `config/repo_config.get_default_model`, `gateway/tests/test_upstream_router.py`, `orchestrator/tests/test_model_resolver.py`) are unambiguously net-new and the existing slice-1/slice-2 components create them \u2014 no false-NACK on missing-grep evidence per the \u00a79 exception rule.\n\n### Trust-Boundary Audit (\u00a710) \u2014 PASS\n\nAll proposed tests live under `gateway/tests/` and `orchestrator/tests/`, both unit-test execution contexts (`make test` / pytest from the trusted-CI-runner). The architect explicitly avoids any test that needs a live LiteLLM endpoint (`httpx.MockTransport`-driven). No `integration_tests/` work is proposed that would hit the in-sandbox-agent vs trusted-CI-runner fixture trap. No `@require_lifecycle_secret` route invocations from in-sandbox-agent contexts. No `ScriptedProvider` references. The cq-4 \"no in-pipeline validation\" boundary is honored.\n\n### Slice-Sizing Advisory (#2137 opt-2 advisory only \u2014 non-blocking)\n\n- **slice-1** (gateway router + LiteLLM topology, 7 components + 2 test files): rough estimate ~600\u2013900 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n- **slice-2** (per-agent model config + plumbing, 6 components + 3 test files): rough estimate ~400\u2013700 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n\n### Blocking\n\n1. **The design records `Session.agent_model_litellm` but does not specify how the gateway consumes it on a LiteLLM-bound request \u2014 the request body's `model` field is left unaltered.** This is a load-bearing ambiguity that fundamentally changes the operator-facing contract and the test surface.\n\n Concrete evidence of the gap, from the architect JSON:\n - Slice-1 component **\"Upstream-aware credential injection\"** (`gateway/gateway.py:9355`) mutates headers only.\n - Slice-1 component **\"Session-driven upstream resolver\"** returns an upstream name only.\n - Slice-1 component **\"Session-storage extensions\"** stores `agent_model_litellm: str|None (e.g. 'qwen-2.5-coder' \u2014 the model alias LiteLLM's model_list will map)`.\n - Slice-1 AC-2 only verifies \"request lands at the litellm client (verify base_url and Authorization: Bearer )\" \u2014 *not* what the body's `model` field looks like on the wire.\n - Slice-2 AC-6 only asserts the *recorded session metadata*, not what the gateway forwards to LiteLLM.\n - The compaction-math mitigation (the issue's primary risk per analysis lines 49\u201359) requires Claude Code to *see* a recognized Claude alias in its `--model` flag \u2014 so the body's `model` field is `opus` (or whatever recognized alias `resolve_model_for_role` returns as the first tuple element).\n\n The architect needs to commit to one of two semantics for the LiteLLM-bound proxy path. Both are buildable; the design implications are very different:\n\n **Semantics A \u2014 \"body is dispatch, agent_model_litellm is metadata\":**\n - Gateway forwards the request body byte-unchanged. LiteLLM dispatches on `body[\"model\"]`.\n - Operator MUST configure LiteLLM's `model_list` with the Claude alias as the dispatch key, e.g. `model_list: [{model_name: \"opus\", litellm_params: {model: \"openrouter/qwen/qwen3-coder\", ...}}]`. The string `\"opus\"` in the LiteLLM config does NOT mean Opus; it is the alias the operator's gateway sends.\n - `Session.agent_model_litellm` is operational metadata (logging, observability, future cost-tracking #2769-followup) and is not used at request time.\n - Acceptance test must verify the request body forwarded to LiteLLM has `model=\"opus\"` byte-for-byte.\n - Operator docs must call out the \"LiteLLM model_list keys are Claude aliases\" constraint loud and early \u2014 any operator who configures `model_name: \"qwen3-coder\"` in `model_list` will see immediate breakage.\n\n **Semantics B \u2014 \"gateway rewrites body.model before forwarding\":**\n - A NEW component (currently missing from slice-1) sits between the credential injection and `client.send(http_req, stream=True)` at `gateway/gateway.py:9827` and, when `upstream='litellm'`, rewrites `body[\"model\"]` from the Claude alias to `session.agent_model_litellm`.\n - Operator configures LiteLLM's `model_list` with the natural model names: `model_list: [{model_name: \"qwen3-coder\", litellm_params: {...}}]`.\n - `Session.agent_model_litellm` is functional and required at request time.\n - Acceptance test must verify the request body is mutated (`body[\"model\"]` changes from `\"opus\"` to `\"qwen3-coder\"`) before reaching LiteLLM.\n - Same rewrite is required for `proxy_count_tokens` so the token count returned to Claude Code matches the model that actually runs.\n\n **Fix:** Re-propose with one semantics chosen explicitly. If A, add a short \"LiteLLM model_list operator constraint\" sub-section under `key_constraints_carried_from_refine` and re-label `agent_model_litellm` as \"operational metadata\" in the Session-storage component. If B, add a slice-1 component \"Request-body model rewriter for LiteLLM-bound requests\" (`gateway/gateway.py`, between credential injection and `client.send`), bump AC-2 to assert body mutation, and add an equivalent assertion for `proxy_count_tokens`. Either path satisfies the refine HITL resolutions; both are buildable; the planner cannot convert components to tasks until this is pinned.\n\n Why this is blocking now (not a planner-detail): the choice changes (a) what test fixtures slice-1 needs, (b) whether the LiteLLM Deployment manifest needs a sample `model_list` that demonstrates the alias-as-key pattern (Semantics A) vs the natural-key pattern (Semantics B), (c) whether `Session.agent_model_litellm` should even exist as a typed field (under A it could be a free-form `metadata` blob), and (d) the operator-facing documentation surface. Letting the planner pick produces incoherent task boundaries.\n\n### Non-blocking\n\n- **R8 (recommended addition to `open_risks_for_risk_analyst`)** \u2014 **`/v1/messages/count_tokens` semantics on the LiteLLM path with Semantics A.** Even after #1 is resolved, the count_tokens response from LiteLLM under Semantics A is *for the Claude alias Claude Code passed*, not for the actual backend. Claude Code uses this count to drive compaction. If the LiteLLM model_list maps `\"opus\" \u2192 qwen3-coder`, LiteLLM may compute tokens with the wrong tokenizer (Claude tokenizer vs Qwen tokenizer have different boundaries). Under Semantics B the rewrite ensures LiteLLM uses the right tokenizer. Worth surfacing for the risk_analyst to weigh.\n\n- **`config/repositories.yaml` is not in-repo today** \u2014 only `config/repositories.yaml.example` exists (operator drops the live file at runtime via the EGG_REPO_CONFIG / EGG_SECRETS_PATH mount). The slice-2 `get_default_model(repo)` helper must handle the missing-file case the same way `get_repo_setting` does today (defaults to None silently). Add an AC: \"AC-10: When `config/repositories.yaml` is absent or contains no `default_model` for the queried repo, `get_default_model(repo)` returns `None` and the resolver falls back to the built-in `'opus'` default.\" Without this, dev environments without an opted-in repo config break loudly the moment slice-2 ships.\n\n- **`LITELLM_BASE_URL` env-var injection into the gateway pod is not in `key_files_changed`.** The architect mentions \"base_url from env LITELLM_BASE_URL, default http://litellm.egg-system.svc.cluster.local:4000\" but doesn't list `k8s/base/gateway-deployment.yaml` as edited for slice-1. The env var has to be declared on the gateway pod (under `env:` in the container spec) for the default to be overridable. Add `k8s/base/gateway-deployment.yaml` to `production_code_slice_1` with a one-liner noting \"add `LITELLM_BASE_URL` env var (optional, defaults to in-cluster service DNS)\". If the architect's intent is \"no env var, hard-coded default\", say so explicitly \u2014 but then the seam loses its swap-out flexibility for the refine-Q3 supply-chain mitigation.\n\n- **`PipelineConfig.agent_models: dict[str, str]` should be typed against the AgentRole enum.** A free-string key invites typos (`'reviewer-refine'` vs `'reviewer_refine'` vs `'refiner_review'`) that silently never resolve. The existing per-phase consensus-timeout pattern at `orchestrator/models.py:452\u2013474` uses *separate fields* per phase, not a dict, precisely because Pydantic validation cannot easily restrict dict keys to an enum without a `field_validator`. Recommendation: either (a) typed as `dict[AgentRole, str]` with a Pydantic v2 `field_validator` that coerces and validates string keys, or (b) split into per-role explicit fields following the overseer-model precedent. Planner can pick, but the architect should call out that key-validation is required (not assumed).\n\n- **Cost-tracking interaction.** `max_llm_cost_per_hour` envelope at `orchestrator/overseer/self_monitor.py:30\u2013130` assumes Anthropic pricing. Refine feedback Q4 deferred extension to a follow-up, which the architect correctly notes. But: what is the *behavior* when a LiteLLM-bound agent's tokens flow through the existing cost tracker today? Does it crash, no-op silently, or mis-price? Even an `# explicitly out of scope but documented behavior: cost-tracker will mis-price non-Anthropic tokens until follow-up #XXXX` note would prevent a downstream operator surprise. Add a one-line entry under `out_of_scope_explicitly` calling out the *observable behavior* (e.g. \"cost tracker continues to apply Anthropic pricing to LiteLLM token counts; the tracked dollar figure will be incorrect for non-Claude agents until #XXXX lands \u2014 does NOT block the agent from running\").\n\n- **NetworkPolicy egress allowlist is left vague.** \"Egress to allowlisted backend hosts only\" \u2014 for the cq-6 hosted-Qwen-first first cut, *which hosts*? The architect should at least name an example (`api.together.xyz`, `api.fireworks.ai`, etc.) and clarify that the NetworkPolicy egress list is *operator-supplied per backend choice* (probably via a kustomize overlay), not hard-coded in `k8s/base/network-policies.yaml`. Otherwise a reader of the slice-1 PR will not know what to put in the egress block.\n\n- **`config/repo_config.py` helper barrel-decomposition cross-check.** The architect places the new `get_default_model(repo)` near `get_repo_setting` at `config/repo_config.py:248`. Sanity-check that this file isn't itself slated for in-flight decomposition (#2261) \u2014 a quick grep of `config/CLAUDE.md` if present, or a check in `orchestrator/CLAUDE.md`. Not blocking; just a sanity check the planner should make before placing the helper.\n\n- **Slice-1 SSE acceptance test (AC-2 part (e)) is good but should include a \"client disconnect mid-stream\" path against the LiteLLM upstream.** The existing pre-stream retry (`gateway/gateway.py:9850`) and mid-stream synthetic SSE error frame (`gateway/gateway.py:9920ish`) are exercised against `httpx.MockTransport` for both upstreams; explicitly include the client-disconnect mid-stream case to assert the `_SSEAccumulator` flushes the partial response identically. Marginal cost in the slice-1 test budget; high value because this is exactly the path that's hardest to debug post-deploy.\n\n### Summary\n\nDesign is sound on the architectural axis the analysis prescribed (Option A with cq-1..11 resolutions respected) and the cited primitives all exist. The one blocking gap (#1) is the missing commitment on how `agent_model_litellm` reaches the wire \u2014 that single decision drives slice-1's test plan, the operator's LiteLLM config, the LiteLLM Deployment's sample manifest, and the documentation surface. Address #1 and re-propose; the non-blocking items can roll into the re-propose or be picked up by the planner.\n", + "revision_count": 1 + }, + "timestamp": "2026-05-22T03:36:21.472930+00:00", + "phase": "plan" + }, + { + "id": "770e7849-652e-4e", + "pipeline_id": "issue-2769", + "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-22T03:30:11.830823+00:00" + }, + "timestamp": "2026-05-22T03:36:21.566955+00:00", + "phase": "plan" + }, + { + "id": "6cc5495c-d4d8-45", + "pipeline_id": "issue-2769", + "from_role": "architect", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:36:21.577527+00:00", + "phase": "plan" + }, + { + "id": "90c8605e-8c84-4f", + "pipeline_id": "issue-2769", + "from_role": "risk_analyst", + "to_role": "all", + "message_type": "CONSENSUS_PROPOSE", + "subject": "Proposal from risk_analyst", + "body": "Risk assessment for #2769 LiteLLM proxy integration. 15 risks recorded with severity, likelihood, impact, mitigation, residual risk, and human-review flag. Anchored on concrete external research: LiteLLM March 2026 PyPI supply-chain incident (TeamPCP backdoored 1.82.7/1.82.8) and April-May 2026 CVE cluster (CVE-2026-42208 CVSS 9.3 SQLi, CVE-2026-35029 RCE, CVE-2026-42271 subprocess spawn), all fixed in v1.83.7+; LiteLLM /v1/messages streaming SSE tool_use input_json_delta drop bugs against non-Anthropic backends (open issues #25561 vertex_ai/gemini, #25321 v1.82.x regression, #24765 GitHub Copilot path) \u2014 the exact configuration egg picked in cq-5 (Claude Code + LiteLLM + non-Anthropic); Claude Code auto-compaction heuristic on alias name + recognised model window; Qwen3 vLLM streaming/reasoning parser bugs (deferred per cq-6 hosted-provider-first). Audits 12 runtime primitives per #2594 (session.upstream/.model_alias missing, register_session() missing fields, PipelineConfig.agent_models missing, build_consensus_wrapped_command callers don't pass model arg, LiteLLM Deployment + LITELLM_MASTER_KEY missing, AnthropicCredential extensibility, SSE accumulator hardcoded Anthropic event names, max_llm_cost_per_hour breaks silently on LiteLLM path, ANTHROPIC_BASE_URL not in _PROTECTED_ENV_KEYS, max_turns hardcoded). Documents 5 trust-boundary surfaces (sandbox->gateway unchanged; gateway->LiteLLM and LiteLLM->hosted-provider are new). Rollback plan: unset PipelineConfig.agent_models[role] -> structural fall-back to Claude singleton (no-op by default per cq-4 deferred validation). Recommendation: PROCEED_WITH_MITIGATIONS. Overall risk HIGH driven by external dependencies (LiteLLM, Claude Code heuristic, hosted provider), not by egg-side code change. Three risks flagged requires_human_review (R1 supply chain controls, R2 LiteLLM streaming tool_use bug, R13 deferred validation residual).", + "metadata": { + "payload": { + "summary": "Risk assessment for #2769 LiteLLM proxy integration. 15 risks recorded with severity, likelihood, impact, mitigation, residual risk, and human-review flag. Anchored on concrete external research: LiteLLM March 2026 PyPI supply-chain incident (TeamPCP backdoored 1.82.7/1.82.8) and April-May 2026 CVE cluster (CVE-2026-42208 CVSS 9.3 SQLi, CVE-2026-35029 RCE, CVE-2026-42271 subprocess spawn), all fixed in v1.83.7+; LiteLLM /v1/messages streaming SSE tool_use input_json_delta drop bugs against non-Anthropic backends (open issues #25561 vertex_ai/gemini, #25321 v1.82.x regression, #24765 GitHub Copilot path) \u2014 the exact configuration egg picked in cq-5 (Claude Code + LiteLLM + non-Anthropic); Claude Code auto-compaction heuristic on alias name + recognised model window; Qwen3 vLLM streaming/reasoning parser bugs (deferred per cq-6 hosted-provider-first). Audits 12 runtime primitives per #2594 (session.upstream/.model_alias missing, register_session() missing fields, PipelineConfig.agent_models missing, build_consensus_wrapped_command callers don't pass model arg, LiteLLM Deployment + LITELLM_MASTER_KEY missing, AnthropicCredential extensibility, SSE accumulator hardcoded Anthropic event names, max_llm_cost_per_hour breaks silently on LiteLLM path, ANTHROPIC_BASE_URL not in _PROTECTED_ENV_KEYS, max_turns hardcoded). Documents 5 trust-boundary surfaces (sandbox->gateway unchanged; gateway->LiteLLM and LiteLLM->hosted-provider are new). Rollback plan: unset PipelineConfig.agent_models[role] -> structural fall-back to Claude singleton (no-op by default per cq-4 deferred validation). Recommendation: PROCEED_WITH_MITIGATIONS. Overall risk HIGH driven by external dependencies (LiteLLM, Claude Code heuristic, hosted provider), not by egg-side code change. Three risks flagged requires_human_review (R1 supply chain controls, R2 LiteLLM streaming tool_use bug, R13 deferred validation residual).", + "attestation": {}, + "artifacts": [ + ".egg-state/agent-outputs/2769-risk_analyst-output.json" + ], + "risk_considered": "All risks documented in the artifact itself; this proposal is the risk assessment. See R1 (CRITICAL supply chain), R2 (CRITICAL LiteLLM SSE bug), R3 (HIGH compaction math), R6-R7 (runtime-primitive gaps), R13 (deferred validation residual).", + "commit_sha": "dfe5ffb0f04cd0094021fd4bb005592352cf896d", + "files_changed": [ + ".egg-state/agent-outputs/2769-risk_analyst-output.json" + ], + "tests_run": [], + "tasks_satisfied": [] + }, + "version": 1, + "commit_sha": "dfe5ffb0f04cd0094021fd4bb005592352cf896d" + }, + "timestamp": "2026-05-22T03:36:37.124813+00:00", + "phase": "plan" + }, + { + "id": "4408dba3-db1c-49", + "pipeline_id": "issue-2769", + "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-22T03:36:37.124900+00:00", + "phase": "plan" + }, + { + "id": "6ec1d121-f2d4-4b", + "pipeline_id": "issue-2769", + "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-22T03:36:37.446421+00:00" + }, + "timestamp": "2026-05-22T03:36:37.515612+00:00", + "phase": "plan" + }, + { + "id": "225a0bf2-38c7-42", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:36:58.802005+00:00", + "phase": "plan" + }, + { + "id": "a65b6d33-2731-4d", + "pipeline_id": "issue-2769", + "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-22T03:37:03.057171+00:00" + }, + "timestamp": "2026-05-22T03:37:03.115681+00:00", + "phase": "plan" + }, + { + "id": "c93be0bb-3060-4f", + "pipeline_id": "issue-2769", + "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-22T03:36:37.446421+00:00" + }, + "timestamp": "2026-05-22T03:37:37.583358+00:00", + "phase": "plan" + }, + { + "id": "af788b75-df7d-4d", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:37:56.348849+00:00", + "phase": "plan" + }, + { + "id": "3da702f8-1f0c-44", + "pipeline_id": "issue-2769", + "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-22T03:37:03.057171+00:00" + }, + "timestamp": "2026-05-22T03:38:03.246097+00:00", + "phase": "plan" + }, + { + "id": "5f3f143a-68c0-4a", + "pipeline_id": "issue-2769", + "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-22T03:36:37.446421+00:00" + }, + "timestamp": "2026-05-22T03:38:37.707867+00:00", + "phase": "plan" + }, + { + "id": "3148eed6-4811-40", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:38:56.434319+00:00", + "phase": "plan" + }, + { + "id": "9f186746-0b56-4d", + "pipeline_id": "issue-2769", + "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-22T03:37:03.057171+00:00" + }, + "timestamp": "2026-05-22T03:39:03.315138+00:00", + "phase": "plan" + }, + { + "id": "364f79c3-aebc-48", + "pipeline_id": "issue-2769", + "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-22T03:36:37.446421+00:00" + }, + "timestamp": "2026-05-22T03:39:37.867704+00:00", + "phase": "plan" + }, + { + "id": "9a2d90ac-6417-42", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:39:56.507719+00:00", + "phase": "plan" + }, + { + "id": "65c0dcee-8f3c-44", + "pipeline_id": "issue-2769", + "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-22T03:37:03.057171+00:00" + }, + "timestamp": "2026-05-22T03:40:03.421957+00:00", + "phase": "plan" + }, + { + "id": "b7c4efc0-24fb-40", + "pipeline_id": "issue-2769", + "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-22T03:36:37.446421+00:00" + }, + "timestamp": "2026-05-22T03:40:37.973869+00:00", + "phase": "plan" + }, + { + "id": "0653dc14-ca88-4a", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:40:56.663860+00:00", + "phase": "plan" + }, + { + "id": "4f965b0c-91f6-49", + "pipeline_id": "issue-2769", + "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-22T03:37:03.057171+00:00" + }, + "timestamp": "2026-05-22T03:41:03.512295+00:00", + "phase": "plan" + }, + { + "id": "0577b33a-645e-47", + "pipeline_id": "issue-2769", + "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-22T03:36:37.446421+00:00" + }, + "timestamp": "2026-05-22T03:41:38.110447+00:00", + "phase": "plan" + }, + { + "id": "5d7db74a-9ea9-47", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:41:56.726174+00:00", + "phase": "plan" + }, + { + "id": "57861637-0f63-46", + "pipeline_id": "issue-2769", + "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 302s. 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": 302, + "source": "health_monitor" + }, + "timestamp": "2026-05-22T03:41:57.685759+00:00", + "phase": "plan" + }, + { + "id": "8f86dabe-e2c1-4a", + "pipeline_id": "issue-2769", + "from_role": "risk_analyst", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:41:57.899584+00:00", + "phase": "plan" + }, + { + "id": "4043b187-f996-44", + "pipeline_id": "issue-2769", + "from_role": "risk_analyst", + "to_role": "all", + "message_type": "CONSENSUS_CONFIRMED", + "subject": "Confirmed by risk_analyst", + "body": "", + "metadata": { + "consensus_reached": false + }, + "timestamp": "2026-05-22T03:42:01.736815+00:00", + "phase": "plan" + }, + { + "id": "12301449-fb9d-4b", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:42:01.829281+00:00", + "phase": "plan" + }, + { + "id": "d50d7e82-7eb1-41", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:42:06.750596+00:00", + "phase": "plan" + }, + { + "id": "5aed2ecd-5926-43", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:42:56.812948+00:00", + "phase": "plan" + }, + { + "id": "5407c11c-9eb8-42", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:43:06.808683+00:00", + "phase": "plan" + }, + { + "id": "fd890fe5-c7a8-44", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:43:56.873141+00:00", + "phase": "plan" + }, + { + "id": "d955d0a0-5ece-45", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:44:06.876137+00:00", + "phase": "plan" + }, + { + "id": "31f4d35f-8bbe-41", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:45:01.256673+00:00", + "phase": "plan" + }, + { + "id": "f3b84924-fb74-4c", + "pipeline_id": "issue-2769", + "from_role": "architect", + "to_role": "all", + "message_type": "CONSENSUS_PROPOSE", + "subject": "Proposal from architect", + "body": "v2 architecture analysis: addresses reviewer_plan NACK item #1 by pinning Semantics B (gateway rewrites body['model'] from the Claude alias to the natural LiteLLM model name BEFORE forwarding to LiteLLM, on both /v1/messages and /v1/messages/count_tokens), rejecting Semantics A (forward unchanged with LiteLLM model_list keyed on Claude aliases) because Semantics A breaks Claude Code's compaction math via tokenizer mismatch on count_tokens (new R8). Adds the body-rewriter component to slice-1 with exact insertion points (gateway/gateway.py between line 9767 credential injection and line 9778 tool-strip, plus the sibling site in proxy_count_tokens at line 10028). Bumps AC-2 to assert the body mutation on the wire, adds AC-10 (repositories.yaml absence handled by get_default_model returning None silently) and AC-11 (PipelineConfig.agent_models key-validation forbidden to be tolerant of typos). Addresses every non-blocking reviewer item: adds k8s/base/gateway-deployment.yaml LITELLM_BASE_URL env var declaration; flags PipelineConfig.agent_models key-validation as a hard design requirement; documents observable cost-tracker mis-pricing on LiteLLM-bound agents as out-of-scope-but-known; makes NetworkPolicy egress operator-supplied per backend via kustomize overlay; cross-checks config/repo_config.py is not in #2261 decomposition; expands the slice-1 SSE test with a client-disconnect mid-stream case against LiteLLM. The slice DAG is unchanged (slice-1 gateway no-op \u2192 slice-2 model config); component count grows by 2 and AC count grows from 9 to 11.\n\n**Adversarial re-review**\n\n**Your v2 review has TWO equal-weight mandates:**\n\n1. **Verify named v1 blockers were addressed** \u2014 confirm the producer fixed what you NACK'd.\n2. **Audit the v2 delta as a fresh reviewer** \u2014 ignore your v1 NACK history. Read the v2 diff as if you'd never seen v1. Apply your lens (security threat-model, concurrency races, contract AC, line-by-line bugs, silent-fallback shapes \u2014 whichever your role owns) to the v2 delta itself, not to whether your previous concerns were satisfied.\n\nBoth mandates have equal weight. If (1) passes but (2) finds new issues, you NACK. ACK requires both pass.\n\n**The named-blockers anchor is a known trap. Every reviewer lens has a mandate-2 in its own territory** \u2014 security has v2-introduced threat surfaces, concurrency has v2-introduced races, contract has v2-introduced AC drift, code has v2-introduced line-by-line bugs. The four issues that escaped PR #2724 to the GitHub bot were all of code-lens shape (`${ANSWER}` as bare Python, deprecated `datetime.utcnow()`, non-atomic write, bare `except: pass`) \u2014 the persistent reviewer correctly answered mandate 1 (\"did v1 issues get fixed? yes\") and skipped mandate 2 (\"does v2 introduce new issues? actually yes\"). The shape generalizes: whatever your lens, the v2 delta can introduce issues your prior NACK didn't name. Watching the producer deliver a targeted fix pulls strongly toward \"verify my fix-request landed \u2192 ACK.\" Recognize the pull and do mandate 2 anyway.\n\n**How to execute mandate 2:**\n\n- Read each new hunk as an operator who's about to copy-paste / run / integrate it. Would this code execute as written? Would these docs send a copy-paster down a working path?\n- Apply every rubric pass to the new hunks. New issues outside the scope of your prior NACK are blocking; your prior NACK does not bound this re-review.\n- **Fresh-reviewer simulation.** Before issuing your v2 verdict, ask: would a reviewer who has only seen the v2 diff with no NACK history ACK this? If you can't argue yes from the v2 diff alone, NACK.\n- **External-bot anchor.** Imagine `egg-reviewer[bot]` reads only your v2 diff with no NACK context. What would it flag? Anything it'd flag, you should NACK first.\n\n**Your v2 verdict must enumerate both halves** so mandate 2 doesn't silently disappear from the record:\n\n- (a) Which v1 blockers you verified-fixed (mandate 1).\n- (b) What new issues you audited-and-did-not-find (mandate 2). Name the specific shapes you checked \u2014 not \"reviewed thoroughly,\" but \"checked for silent fallbacks, doc-snippet executability, API-deprecation, atomicity of file writes.\" If you can't enumerate (b), you haven't done mandate 2.\n\n**Re-reviews are cheap by design.** Your amortized context means the work is \"read the delta, apply your rubric, decide\" \u2014 minutes, not hours. NACK without hesitance; the orchestrator absorbs cycles. Two NACKs on the same producer where the second names new findings is the correct trajectory, not goalpost-moving. The downstream GitHub reviewer should find nothing in this delta. Anything it catches that lives in this cycle's diff is a miss attributable to this re-review.", + "metadata": { + "payload": { + "summary": "v2 architecture analysis: addresses reviewer_plan NACK item #1 by pinning Semantics B (gateway rewrites body['model'] from the Claude alias to the natural LiteLLM model name BEFORE forwarding to LiteLLM, on both /v1/messages and /v1/messages/count_tokens), rejecting Semantics A (forward unchanged with LiteLLM model_list keyed on Claude aliases) because Semantics A breaks Claude Code's compaction math via tokenizer mismatch on count_tokens (new R8). Adds the body-rewriter component to slice-1 with exact insertion points (gateway/gateway.py between line 9767 credential injection and line 9778 tool-strip, plus the sibling site in proxy_count_tokens at line 10028). Bumps AC-2 to assert the body mutation on the wire, adds AC-10 (repositories.yaml absence handled by get_default_model returning None silently) and AC-11 (PipelineConfig.agent_models key-validation forbidden to be tolerant of typos). Addresses every non-blocking reviewer item: adds k8s/base/gateway-deployment.yaml LITELLM_BASE_URL env var declaration; flags PipelineConfig.agent_models key-validation as a hard design requirement; documents observable cost-tracker mis-pricing on LiteLLM-bound agents as out-of-scope-but-known; makes NetworkPolicy egress operator-supplied per backend via kustomize overlay; cross-checks config/repo_config.py is not in #2261 decomposition; expands the slice-1 SSE test with a client-disconnect mid-stream case against LiteLLM. The slice DAG is unchanged (slice-1 gateway no-op \u2192 slice-2 model config); component count grows by 2 and AC count grows from 9 to 11.", + "attestation": {}, + "artifacts": [ + ".egg-state/agent-outputs/2769-architect-output.json" + ], + "risk_considered": "v2 pins Semantics B explicitly, eliminating the v1 ambiguity reviewer_plan flagged as blocking. The chief residual risk (R3) \u2014 drift between Claude Code's --model alias and the gateway's session.agent_model_litellm \u2014 is mitigated by making resolve_model_for_role the SINGLE source of truth both callers consume, with AC-6 asserting all three derived values (consensus_wrapper --model, gateway session metadata, and the body-rewritten wire model) flow from the same resolver call. The tokenizer-mismatch risk (R8) that motivated rejecting Semantics A is eliminated under Semantics B because the rewrite extends to /v1/messages/count_tokens, so LiteLLM tokenizes against the actual backend model. The body rewriter only touches body['model']; every other field (messages, tools, system, max_tokens) is preserved byte-for-byte, and on JSON-parse failure the body is forwarded unchanged with a warning (defensive). The Anthropic path is unchanged: the rewriter only fires on upstream=='litellm'.", + "commit_sha": "fce400144", + "files_changed": [ + ".egg-state/agent-outputs/2769-architect-output.json" + ], + "tests_run": [], + "tasks_satisfied": [] + }, + "version": 2, + "commit_sha": "fce400144" + }, + "timestamp": "2026-05-22T03:45:01.633908+00:00", + "phase": "plan" + }, + { + "id": "99270965-bc54-4e", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:45:25.675325+00:00", + "phase": "plan" + }, + { + "id": "127cc3b8-2429-49", + "pipeline_id": "issue-2769", + "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-22T03:45:12.237332+00:00" + }, + "timestamp": "2026-05-22T03:45:25.720698+00:00", + "phase": "plan" + }, + { + "id": "3b3a9b66-14c5-46", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:46:01.322088+00:00", + "phase": "plan" + }, + { + "id": "fdbee8bb-d9d9-46", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:46:21.949274+00:00", + "phase": "plan" + }, + { + "id": "604ea25d-f28a-48", + "pipeline_id": "issue-2769", + "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-22T03:45:12.237332+00:00" + }, + "timestamp": "2026-05-22T03:46:25.797673+00:00", + "phase": "plan" + }, + { + "id": "e4bae38a-fc65-41", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:47:01.430989+00:00", + "phase": "plan" + }, + { + "id": "c9d2521b-ea2c-49", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:47:22.121969+00:00", + "phase": "plan" + }, + { + "id": "89007796-3697-40", + "pipeline_id": "issue-2769", + "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-22T03:45:12.237332+00:00" + }, + "timestamp": "2026-05-22T03:47:25.893677+00:00", + "phase": "plan" + }, + { + "id": "eaf10202-2f80-46", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:48:01.525601+00:00", + "phase": "plan" + }, + { + "id": "5afedd7c-a59e-40", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:48:22.211911+00:00", + "phase": "plan" + }, + { + "id": "7fcaf4cf-56e4-43", + "pipeline_id": "issue-2769", + "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-22T03:45:12.237332+00:00" + }, + "timestamp": "2026-05-22T03:48:25.971344+00:00", + "phase": "plan" + }, + { + "id": "69943ab4-206f-47", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:49:01.699007+00:00", + "phase": "plan" + }, + { + "id": "7e6dc3b4-6c3e-4d", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:49:22.278182+00:00", + "phase": "plan" + }, + { + "id": "d8022c57-4953-48", + "pipeline_id": "issue-2769", + "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-22T03:45:12.237332+00:00" + }, + "timestamp": "2026-05-22T03:49:26.066474+00:00", + "phase": "plan" + }, + { + "id": "44d32a41-8b0e-44", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "architect", + "message_type": "CONSENSUS_ACK", + "subject": "ACK from reviewer_plan for architect", + "body": "\n### v1 blocker verification (mandate #1)\n\nThe v1 blocker was \"design records `Session.agent_model_litellm` but does not specify how the gateway consumes it on a LiteLLM-bound request.\" v2 resolves it completely:\n\n- **`litellm_dispatch_semantics.choice = \"Semantics B\"`** is pinned explicitly in the recommended_approach (lines 25\u201337). The architect commits to \"gateway rewrites body.model before forwarding to LiteLLM,\" not the ambiguous \"metadata-only\" reading I called out.\n- **New slice-1 component \"Body model rewriter for LiteLLM-bound requests\"** (lines 72\u201377) names exact insertion points: `proxy_anthropic_messages` between `_inject_anthropic_credentials` at `gateway/gateway.py:9767` and `_filter_blocked_tools` at `:9778`; `proxy_count_tokens` between `_inject_anthropic_credentials` at `:10028` and `client.post` at `:10035`. JSON-parse failure mode is specified (log + leave unchanged + proceed \u2014 defensive). Ordering invariant (rewriter runs before tool-strip so the tool-strip sees the canonical rewritten body) is articulated explicitly in `key_constraints_carried_from_refine.tool_strip_unchanged_in_private_mode` (line 395).\n- **AC-2 (slice-1) bumped** to assert body mutation on the wire for BOTH `/v1/messages` and `/v1/messages/count_tokens` (acceptance_criteria_seeds AC-2 part (b) and (c)).\n- **R8 added** to `open_risks_for_risk_analyst` \u2014 the tokenizer-mismatch failure mode on count_tokens is the load-bearing technical reason Semantics A was rejected. This is the strongest possible justification for the choice and reads as honest engineering, not post-hoc rationalization.\n- **`what_we_explicitly_rejected_semantics_a`** (lines 34\u201337) names the alternative and the rejection reasons, so future readers don't re-litigate.\n\n### Non-blocker verification (all addressed)\n\n- **`config/repositories.yaml` missing-file handling** \u2192 AC-10 added (acceptance_criteria_seeds line 457), with explicit \"no exception, no error log\" + \"dev environments without an opted-in repo config continue to work as today\" rationale.\n- **`LITELLM_BASE_URL` env-var declaration** \u2192 new slice-1 component \"Gateway pod LITELLM_BASE_URL env var declaration\" (lines 103\u2013107), with exact placement (`k8s/base/gateway-deployment.yaml` after the existing port-env block at lines 77\u201382). `k8s/base/gateway-deployment.yaml` now appears in `production_code_slice_1` (line 176).\n- **`PipelineConfig.agent_models` key-validation** \u2192 \"key-validation is required\" flagged as non-negotiable in the component description (line 134); planner gets shape choice (`dict[AgentRole, str]` with Pydantic `field_validator` vs explicit per-role fields) but free-string `dict[str, str]` with no validator is forbidden. AC-11 added (line 458) for typo'd-role-key rejection.\n- **Cost-tracker observable behavior** \u2192 surfaced in `out_of_scope_explicitly` (line 462) and the runtime_primitives `max_llm_cost_per_hour` plan_action (line 370): documents that the tracker continues to apply Anthropic pricing to LiteLLM tokens and the dollar figure will be wrong on the LiteLLM path \u2014 DOES NOT block the agent.\n- **NetworkPolicy egress concreteness** \u2192 new slice-1 component \"NetworkPolicy egress overlay\" (lines 109\u2013112) commits to k8s/base shipping ZERO default egress for the LiteLLM pod (defense-in-depth); operator supplies egress via a kustomize overlay; example hosts named (`api.together.xyz`, `api.fireworks.ai`, `openrouter.ai`, `api.deepinfra.com`); example overlay scaffold path noted (`k8s/overlays/example/`). AC-7 (line 454) asserts the zero-default-egress behavior.\n- **`config/repo_config.py` decomposition cross-check** \u2192 done explicitly: \"836 lines, not in any #2261 decomposition row (verified against scripts/file-size-allowlist.yaml and CLAUDE.md tables). Helper placement near get_repo_setting (line 248) is safe.\" Now baked into the runtime_primitives entry (line 378).\n- **Slice-1 SSE test expansion (client-disconnect mid-stream against LiteLLM)** \u2192 AC-2 part (g) added (line 449) \u2014 explicitly exercises the `gateway/gateway.py:9909` mid-stream synthetic-error-frame branch against the LiteLLM upstream.\n\n### v2 delta audit (mandate #2 \u2014 fresh-reviewer pass)\n\nI read the v2 delta with no NACK history and looked for new issues. None blocking:\n\n- **Body-rewriter ordering (BEFORE tool-strip)**: rewriter touches only `body['model']`; `_filter_blocked_tools` touches only `body['tools']`. The orderings are commutative for correctness. The architect's explicit choice (rewriter first \u2192 tool-strip sees canonical body) is the cleaner invariant and is documented. OK.\n- **JSON-parse failure handling for the rewriter**: leave unchanged + log warning + proceed. Under Semantics B with operator-configured natural-keys, an un-rewritten body with `model='opus'` will fail at LiteLLM with a \"model not found\" \u2014 i.e. fail-closed at the upstream layer rather than the gateway. Acceptable; LiteLLM's error reaches the agent and surfaces the misconfig.\n- **`litellm_model_list_operator_pattern_semantics_b`** constraint (line 399) \u2014 new top-level invariant documenting the operator-facing config pattern. Clear and unambiguous. This is exactly the \"operator UX commitment\" the v1 ambiguity prevented.\n- **R8 framing**: the tokenizer-mismatch argument depends on LiteLLM's tokenizer registry mapping `model_name \u2192 tokenizer`. Under Semantics A, LiteLLM would use the tokenizer associated with whichever model it dispatched to (which IS the real backend), not the alias \u2014 so the tokenizer-mismatch claim is slightly overstated *for the dispatch path*. BUT for the count_tokens path, the Anthropic-tokenizer-mapped count is a real concern, since count_tokens may be tokenizer-only without involving the dispatch path. Net: R8 is still a strong argument for Semantics B even if the framing slightly overgeneralizes. Non-blocking observation only.\n- **Slice-1 LOC remains under 1000** with the added body-rewriter (~50 LOC) and env-var declaration (~3 LOC). Slice-2 is unchanged. Both well within the #2137 advisory threshold.\n- **Primitive-existence audit**: All new primitives (body-rewriter, LITELLM_BASE_URL env var declaration, NetworkPolicy egress overlay) are clearly NEW. All cited existing primitives match the v1 audit I already verified.\n- **Trust boundary**: Body-rewriter runs gateway-side in the gateway pod (same trust scope as `_filter_blocked_tools` and the existing credential injection). No sandbox-side credential or trust-boundary crossing introduced.\n\n### Non-blocking \u2014 for the planner / risk_analyst follow-through (not blocking the architect)\n\n- **Body-rewriter's downstream impact on the SSE accumulator transcript-capture path**: `_SSEAccumulator` captures the response, not the request. So the rewriter (which only touches the request) does not change anything downstream sees in the transcript. Worth a one-line note in the operator docs so future readers don't go looking. Non-blocking.\n\n- **R8's count_tokens tokenizer mapping nuance**: LiteLLM's `count_tokens` implementation may itself dispatch to the model_list to pick the tokenizer, in which case Semantics A's count would be correct *for the model_list-resolved backend*, NOT incorrect for the alias. The R8 framing is correct only if LiteLLM tokenizes against the alias verbatim. Either way Semantics B is the cleaner choice (no operator footgun), so the rejection of Semantics A stands; just worth being precise in the future docs.\n\n### Summary\n\nArchitect v2 resolves the v1 blocker comprehensively and addresses every non-blocking item I raised, with new acceptance criteria (AC-10, AC-11), a new risk (R8), a new component (body-rewriter), and updated k8s/component placements. The Semantics-B pin is the right call for the same reasons (decouples Claude Code's compaction math from LiteLLM's dispatch, gives operators natural model_list keys). Reviewed the v2 delta as a fresh reviewer with no NACK history \u2014 no new blocking issues. ACK.\n", + "metadata": { + "payload": { + "artifact_references": [ + ".egg-state/agent-outputs/2769-architect-output.json", + "gateway/gateway.py", + "gateway/session_manager.py", + "gateway/anthropic_credentials.py", + "k8s/base/gateway-deployment.yaml", + "config/repo_config.py", + "orchestrator/models.py" + ], + "reason": "\n### v1 blocker verification (mandate #1)\n\nThe v1 blocker was \"design records `Session.agent_model_litellm` but does not specify how the gateway consumes it on a LiteLLM-bound request.\" v2 resolves it completely:\n\n- **`litellm_dispatch_semantics.choice = \"Semantics B\"`** is pinned explicitly in the recommended_approach (lines 25\u201337). The architect commits to \"gateway rewrites body.model before forwarding to LiteLLM,\" not the ambiguous \"metadata-only\" reading I called out.\n- **New slice-1 component \"Body model rewriter for LiteLLM-bound requests\"** (lines 72\u201377) names exact insertion points: `proxy_anthropic_messages` between `_inject_anthropic_credentials` at `gateway/gateway.py:9767` and `_filter_blocked_tools` at `:9778`; `proxy_count_tokens` between `_inject_anthropic_credentials` at `:10028` and `client.post` at `:10035`. JSON-parse failure mode is specified (log + leave unchanged + proceed \u2014 defensive). Ordering invariant (rewriter runs before tool-strip so the tool-strip sees the canonical rewritten body) is articulated explicitly in `key_constraints_carried_from_refine.tool_strip_unchanged_in_private_mode` (line 395).\n- **AC-2 (slice-1) bumped** to assert body mutation on the wire for BOTH `/v1/messages` and `/v1/messages/count_tokens` (acceptance_criteria_seeds AC-2 part (b) and (c)).\n- **R8 added** to `open_risks_for_risk_analyst` \u2014 the tokenizer-mismatch failure mode on count_tokens is the load-bearing technical reason Semantics A was rejected. This is the strongest possible justification for the choice and reads as honest engineering, not post-hoc rationalization.\n- **`what_we_explicitly_rejected_semantics_a`** (lines 34\u201337) names the alternative and the rejection reasons, so future readers don't re-litigate.\n\n### Non-blocker verification (all addressed)\n\n- **`config/repositories.yaml` missing-file handling** \u2192 AC-10 added (acceptance_criteria_seeds line 457), with explicit \"no exception, no error log\" + \"dev environments without an opted-in repo config continue to work as today\" rationale.\n- **`LITELLM_BASE_URL` env-var declaration** \u2192 new slice-1 component \"Gateway pod LITELLM_BASE_URL env var declaration\" (lines 103\u2013107), with exact placement (`k8s/base/gateway-deployment.yaml` after the existing port-env block at lines 77\u201382). `k8s/base/gateway-deployment.yaml` now appears in `production_code_slice_1` (line 176).\n- **`PipelineConfig.agent_models` key-validation** \u2192 \"key-validation is required\" flagged as non-negotiable in the component description (line 134); planner gets shape choice (`dict[AgentRole, str]` with Pydantic `field_validator` vs explicit per-role fields) but free-string `dict[str, str]` with no validator is forbidden. AC-11 added (line 458) for typo'd-role-key rejection.\n- **Cost-tracker observable behavior** \u2192 surfaced in `out_of_scope_explicitly` (line 462) and the runtime_primitives `max_llm_cost_per_hour` plan_action (line 370): documents that the tracker continues to apply Anthropic pricing to LiteLLM tokens and the dollar figure will be wrong on the LiteLLM path \u2014 DOES NOT block the agent.\n- **NetworkPolicy egress concreteness** \u2192 new slice-1 component \"NetworkPolicy egress overlay\" (lines 109\u2013112) commits to k8s/base shipping ZERO default egress for the LiteLLM pod (defense-in-depth); operator supplies egress via a kustomize overlay; example hosts named (`api.together.xyz`, `api.fireworks.ai`, `openrouter.ai`, `api.deepinfra.com`); example overlay scaffold path noted (`k8s/overlays/example/`). AC-7 (line 454) asserts the zero-default-egress behavior.\n- **`config/repo_config.py` decomposition cross-check** \u2192 done explicitly: \"836 lines, not in any #2261 decomposition row (verified against scripts/file-size-allowlist.yaml and CLAUDE.md tables). Helper placement near get_repo_setting (line 248) is safe.\" Now baked into the runtime_primitives entry (line 378).\n- **Slice-1 SSE test expansion (client-disconnect mid-stream against LiteLLM)** \u2192 AC-2 part (g) added (line 449) \u2014 explicitly exercises the `gateway/gateway.py:9909` mid-stream synthetic-error-frame branch against the LiteLLM upstream.\n\n### v2 delta audit (mandate #2 \u2014 fresh-reviewer pass)\n\nI read the v2 delta with no NACK history and looked for new issues. None blocking:\n\n- **Body-rewriter ordering (BEFORE tool-strip)**: rewriter touches only `body['model']`; `_filter_blocked_tools` touches only `body['tools']`. The orderings are commutative for correctness. The architect's explicit choice (rewriter first \u2192 tool-strip sees canonical body) is the cleaner invariant and is documented. OK.\n- **JSON-parse failure handling for the rewriter**: leave unchanged + log warning + proceed. Under Semantics B with operator-configured natural-keys, an un-rewritten body with `model='opus'` will fail at LiteLLM with a \"model not found\" \u2014 i.e. fail-closed at the upstream layer rather than the gateway. Acceptable; LiteLLM's error reaches the agent and surfaces the misconfig.\n- **`litellm_model_list_operator_pattern_semantics_b`** constraint (line 399) \u2014 new top-level invariant documenting the operator-facing config pattern. Clear and unambiguous. This is exactly the \"operator UX commitment\" the v1 ambiguity prevented.\n- **R8 framing**: the tokenizer-mismatch argument depends on LiteLLM's tokenizer registry mapping `model_name \u2192 tokenizer`. Under Semantics A, LiteLLM would use the tokenizer associated with whichever model it dispatched to (which IS the real backend), not the alias \u2014 so the tokenizer-mismatch claim is slightly overstated *for the dispatch path*. BUT for the count_tokens path, the Anthropic-tokenizer-mapped count is a real concern, since count_tokens may be tokenizer-only without involving the dispatch path. Net: R8 is still a strong argument for Semantics B even if the framing slightly overgeneralizes. Non-blocking observation only.\n- **Slice-1 LOC remains under 1000** with the added body-rewriter (~50 LOC) and env-var declaration (~3 LOC). Slice-2 is unchanged. Both well within the #2137 advisory threshold.\n- **Primitive-existence audit**: All new primitives (body-rewriter, LITELLM_BASE_URL env var declaration, NetworkPolicy egress overlay) are clearly NEW. All cited existing primitives match the v1 audit I already verified.\n- **Trust boundary**: Body-rewriter runs gateway-side in the gateway pod (same trust scope as `_filter_blocked_tools` and the existing credential injection). No sandbox-side credential or trust-boundary crossing introduced.\n\n### Non-blocking \u2014 for the planner / risk_analyst follow-through (not blocking the architect)\n\n- **Body-rewriter's downstream impact on the SSE accumulator transcript-capture path**: `_SSEAccumulator` captures the response, not the request. So the rewriter (which only touches the request) does not change anything downstream sees in the transcript. Worth a one-line note in the operator docs so future readers don't go looking. Non-blocking.\n\n- **R8's count_tokens tokenizer mapping nuance**: LiteLLM's `count_tokens` implementation may itself dispatch to the model_list to pick the tokenizer, in which case Semantics A's count would be correct *for the model_list-resolved backend*, NOT incorrect for the alias. The R8 framing is correct only if LiteLLM tokenizes against the alias verbatim. Either way Semantics B is the cleaner choice (no operator footgun), so the rejection of Semantics A stands; just worth being precise in the future docs.\n\n### Summary\n\nArchitect v2 resolves the v1 blocker comprehensively and addresses every non-blocking item I raised, with new acceptance criteria (AC-10, AC-11), a new risk (R8), a new component (body-rewriter), and updated k8s/component placements. The Semantics-B pin is the right call for the same reasons (decouples Claude Code's compaction math from LiteLLM's dispatch, gives operators natural model_list keys). Reviewed the v2 delta as a fresh reviewer with no NACK history \u2014 no new blocking issues. ACK.\n", + "ack_version": 2 + }, + "version": 2 + }, + "timestamp": "2026-05-22T03:49:27.026721+00:00", + "phase": "plan" + }, + { + "id": "11211b24-b13e-44", + "pipeline_id": "issue-2769", + "from_role": "orchestrator", + "to_role": "architect", + "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-22T03:49:27.027322+00:00", + "phase": "plan" + }, + { + "id": "ba988b3a-253e-45", + "pipeline_id": "issue-2769", + "from_role": "architect", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:49:27.147820+00:00", + "phase": "plan" + }, + { + "id": "181186ba-7063-4a", + "pipeline_id": "issue-2769", + "from_role": "architect", + "to_role": "all", + "message_type": "CONSENSUS_CONFIRMED", + "subject": "Confirmed by architect", + "body": "", + "metadata": { + "consensus_reached": false + }, + "timestamp": "2026-05-22T03:49:30.494653+00:00", + "phase": "plan" + }, + { + "id": "4a66a564-0d6c-4a", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WAITING_ON_ROLE", + "body": "", + "metadata": { + "state": "WAITING_ON_ROLE", + "waiting_on": "reviewer_plan" + }, + "timestamp": "2026-05-22T03:49:33.818190+00:00", + "phase": "plan" + }, + { + "id": "382e08c8-e0c2-4f", + "pipeline_id": "issue-2769", + "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-22T03:49:38.773629+00:00" + }, + "timestamp": "2026-05-22T03:49:38.846128+00:00", + "phase": "plan" + }, + { + "id": "93b2a4ca-54d8-44", + "pipeline_id": "issue-2769", + "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-22T03:34:26.073888+00:00" + }, + "timestamp": "2026-05-22T03:50:01.786199+00:00", + "phase": "plan" + }, + { + "id": "82b38ffb-f039-45", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:50:22.341414+00:00", + "phase": "plan" + }, + { + "id": "089e0100-e836-4a", + "pipeline_id": "issue-2769", + "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-22T03:49:38.773629+00:00" + }, + "timestamp": "2026-05-22T03:50:38.948254+00:00", + "phase": "plan" + }, + { + "id": "68a2986a-6e91-4d", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "task_planner", + "message_type": "CONSENSUS_ACK", + "subject": "ACK from reviewer_plan for task_planner", + "body": "\nReviewed the full `2769-plan.md` (1027 lines), ran the planner's role\u2194files validator (`OK - no violations`), spot-grepped every cited primitive, and cross-referenced against the architect v2 component breakdown and the risk_analyst's 15-risk artifact. The plan is internally consistent, picks **Semantics B** explicitly (TASK-2-6 adds `_rewrite_upstream_model`), and decomposes cleanly into two dependent slices that match the architect's recommended option-3 shape.\n\n### Primitive-Existence Audit (#2594) \u2014 all PASS\n\n| primitive | grep | result |\n|-----------|------|--------|\n| All gateway/orchestrator primitives cited in the Primitives tables (lines 56\u2013116) | re-grepped against current HEAD | every cite resolves to the line number the plan lists \u2713 |\n| `AgentRole` enum at `shared/egg_contracts/agent_roles.py:46` (cited for TASK-2-1 validator) | `grep -n 'class AgentRole' shared/egg_contracts/agent_roles.py` | exists \u2713 |\n| `parse_env_file` at `gateway/anthropic_credentials.py:52` (cited for TASK-1-2) | verified | exists \u2713 |\n| `SECRETS_PATH` env var at `gateway/anthropic_credentials.py:31` (cited for TASK-1-2) | verified | exists \u2713 |\n| `load_repo_pattern_override` at `shared/egg_restrictions/patterns.py:854` (cited as pattern for TASK-2-2) | verified | exists \u2713 |\n| `get_repo_setting` at `config/repo_config.py:248` (cited for TASK-2-2) | verified | exists \u2713 |\n| `_get_forwarded_headers` / `_filter_response_headers` at gateway/gateway.py:9343 / :9348 | verified | exists \u2713 |\n| Existing test file paths (gateway/tests/test_session_manager.py, tests/gateway/test_anthropic_proxy.py, etc.) | `ls` | exist \u2713 |\n| `config/repositories.yaml.example` | exists; the live `config/repositories.yaml` is operator-supplied \u2014 TASK-2-2's helper inherits the missing-file-returns-None behavior from `get_repo_setting` | OK \u2713 |\n\nAll NEW primitives are clearly tagged with `(NEW \u2014 task TASK-X-Y)` per the \u00a79 exception (e.g. line 77 `UpstreamRegistry`, line 97 `PipelineConfig.agent_models`), and the downstream task consuming each new primitive has an explicit dependency on the creating task.\n\n### Trust-Boundary Audit (\u00a710) \u2014 PASS\n\nThe plan's \"Trust-boundary scope checks\" sub-section (lines 118\u2013141) is the cleanest such audit I've seen in a plan. Every gateway-side new primitive runs in the gateway pod; every orchestrator-side new primitive runs in the orchestrator pod (trusted-CI-runner); no in-sandbox-agent test fixture is added that would hit the parent-vs-sibling-conftest trap. All test files live under `tests/gateway/`, `gateway/tests/`, or `orchestrator/tests/` \u2014 all reachable from `make test` in the trusted-CI-runner. The plan explicitly defers live-LiteLLM end-to-end validation to the operator-driven smoke test (cq-4), so the in-sandbox-agent vs trusted-CI-runner mismatch never arises.\n\n### Role\u2194Files Alignment (\u00a78) \u2014 PASS\n\n`python3 -c \"from egg_contracts.plan_parser import parse_plan_file, validate_task_role_alignment as v; \u2026\"` returned `OK - no violations` against the current plan file. Every coder task targets files coder can write (gateway/, orchestrator/, config/, k8s/base/), every tester task targets files tester can write (gateway/tests/, tests/gateway/, orchestrator/tests/), every documenter task targets docs/ or `.md` files documenter can write.\n\n### Slice-Sizing Advisory (#2137 opt-2 advisory only \u2014 non-blocking)\n\nThe plan's own LOC estimate (lines 343\u2013355) is **slice-1 ~700 LOC, slice-2 ~600 LOC** \u2014 both well within the 1,000-LOC soft target. **No advisory needed.** Spot-check: 12 tasks in slice-1 across 9 files + 3 test files + 1 doc, with the LiteLLM-deployment YAML and the body-routing-credential extensions being the largest. 9 tasks in slice-2 across 6 files + 2 test files + 1 doc.\n\n### Slice-DAG Forest-Constraint Check (#2137) \u2014 PASS\n\nThe `yaml-tasks` block (lines 524\u20131027) declares `slice-2.dependencies = [slice-1]` and `slice-1` has no `dependencies` field. Single-parent, no cycles, forest-valid. The plan's \"Slice DAG\" prose (lines 325\u2013339) confirms the constraint. No `forest_violation` discriminator on the contract.\n\n### Coherence with Architect v2\n\nCross-checked the plan against the architect v2 proposal (which I just ACKed). They converge on Semantics B and on slice boundaries, with two minor differences I record as non-blockers below (body-rewriter slice placement; field naming) \u2014 both planner-level judgment calls and either choice ships a working integration.\n\n### Non-blocking\n\n- **TASK-2-3 hardcodes `claude_code_alias = \"opus\"` for every non-Claude model**, with no per-pipeline override of the alias itself. The risk_analyst's R3 (Claude Code compaction math) is explicit: \"the alias presented to Claude Code MUST have a context window \u2264 the real backend's window. real Qwen3 128K \u2192 present 'sonnet' alias (200K) is WRONG; present a 100K-window alias (or set context_token_threshold explicitly) is RIGHT.\" `opus` resolves to a 200K window \u2014 which exceeds Qwen3's 128K context. An operator who flips a role to a sub-200K backend hits the compaction wedge at validation time. Three reasonable fixes \u2014 any one is enough; the planner can pick at implementation time, but the plan should note that the operator's escape hatch exists:\n - (a) Make the alias configurable: change `PipelineConfig.agent_models` values from a string to a `(claude_code_alias, litellm_model)` tuple/object so the operator can pick `(\"haiku\", \"qwen3-coder-30b\")`.\n - (b) Add a small per-window lookup: when the LiteLLM model's window is known to be <200K, the resolver picks a smaller alias automatically.\n - (c) Set Claude Code's `context_token_threshold` SDK option explicitly for LiteLLM-bound agents (separate lever from the model name).\n - **Why non-blocking**: cq-4 puts empirical validation on the operator post-merge. The operator can patch the resolver as a stopgap if validation surfaces the issue. Plus the issue is already enumerated in the risk_analyst's R3, so the operator has been warned. But the plan as written has no operator-controllable escape hatch \u2014 it requires a source-code patch. Worth a doc note in TASK-2-9 (\"known limitation: if your real backend has a context window < ~190K, you must patch the resolver to use a smaller Claude alias or set `context_token_threshold` \u2014 see follow-up issue #XXXX\").\n\n- **TASK-2-6 places the body-rewriter in slice-2**, while the architect v2 places it in slice-1. Both work; planner's split keeps slice-1 purely additive at the routing level (the registry can resolve to LiteLLM but no body is rewritten until slice-2 ships the `_rewrite_upstream_model` helper). This means a hypothetical slice-1-only deployment that an operator somehow points at LiteLLM (by setting `Session.upstream='litellm'` out-of-band) would forward the body byte-unchanged with `model='opus'` to LiteLLM, which would fail at LiteLLM with \"model not found\" \u2014 fail-closed, just at the upstream layer instead of in the gateway. Acceptable. Implementer should be aware of this when reviewing the slice-1 PR.\n\n- **TASK-2-6 ordering: rewriter AFTER `_filter_blocked_tools`** (vs architect v2's \"BEFORE\"). Functionally commutative \u2014 rewriter touches `body['model']`, tool-strip touches `body['tools']`, no shared keys. Either order produces the same bytes on the wire. The architect's \"BEFORE\" preserves the \"canonical body downstream\" invariant slightly better (everything downstream sees one body shape). The planner's \"AFTER\" is slightly more efficient (skips one re-serialization if no rewrite). Implementer can pick; either is correct. Worth one line in the implementation note explaining the choice and rationale.\n\n- **Field naming: `Session.upstream` / `Session.upstream_model` (planner) vs `Session.agent_upstream` / `Session.agent_model_litellm` (architect v2)**. Both are valid; planner's names are shorter. Implementer needs to pick one and be consistent across `Session`, `register_session`, `/api/v1/sessions/create` payload, `GatewayClient.register_session`, and the test assertions. The plan and the architect v2 docs both refer to these fields by their own naming, so the implementer needs to reconcile \u2014 a one-line note in either re-propose would prevent confusion downstream.\n\n- **TASK-1-1 acceptance: \"the existing Anthropic credential resolver (preserves the `# noqa: EGG200` annotation pattern at `gateway/gateway.py:9325`)\"** \u2014 good attention to detail (lifting the noqa with the singleton). Worth verifying in code review that the noqa migrates intact to wherever `UpstreamRegistry` lives.\n\n- **TASK-1-12 doc** mentions cq-1/cq-2/cq-5/cq-7/cq-8 but not cq-9 (tool-strip uniformity) or cq-11 (`opus[1m]` left alone). These are also load-bearing decisions; worth one bullet each. Non-blocking \u2014 implementer can include during writing.\n\n- **TASK-1-8 / TASK-1-9 don't include adding `LITELLM_BASE_URL` to `gateway-deployment.yaml`'s `env:` block**. The architect v2 explicitly recommends this (so an operator can repoint via kustomize overlay without setting an env var the base manifest hasn't declared). Functionally the system works without it (the registry's hard-coded default `http://litellm.egg-system.svc.cluster.local:4000` matches the in-cluster Service DNS, and a kustomize strategic-merge overlay can still inject the env var). But declaring it in the base manifest is the \"discoverable optional config knob\" pattern. Worth folding into TASK-1-1 or TASK-1-8 as a small addition (3 lines of YAML). Non-blocking \u2014 the default works and overlay overrides function regardless.\n\n- **TASK-1-2 acceptance: \"With `LITELLM_MASTER_KEY` unset, the resolver returns `None` and does not warn at startup.\"** \u2014 combined with TASK-1-3 (\"Missing credentials for either upstream return a 401 with the same JSON body shape as today\"), the failure mode for \"session declares LiteLLM but no key\" is a 401 from `_inject_upstream_credentials`. But cq-8 / AC-8 (per architect v2) call for a 502 (upstream-unreachable / misconfig), not a 401 (auth). 401 implies \"your credential is wrong\"; 502 implies \"the upstream is broken.\" For a missing master key, 502 (or 500) is the more truthful status because the *operator* misconfigured the gateway, not the agent. Worth aligning. Non-blocking \u2014 the error reaches the agent either way, the agent's failure mode is the same.\n\n- **R5 in the planner-view risks (\"Empirical Claude Code compaction-math compatibility\")** says \"Slice 2 keeps Claude Code's `--model` flag set to a recognised Claude alias (`opus`) for all LiteLLM-bound agents, per cq-5, so Claude Code's compaction math stays sane.\" This claim is only true when the backend's window \u2265 `opus`'s window (200K). For backends <200K (Qwen3 128K), compaction math is NOT sane. The risk text overstates the mitigation. Tied to the first non-blocker above; would resolve when that follow-up lands. Non-blocking.\n\n- **Test-path inconsistency**: TASK-1-10 places new tests under `tests/gateway/` (matching the layout of `test_anthropic_proxy.py`); TASK-1-11 places extensions under `gateway/tests/` (matching `test_session_manager.py`). Both directories exist in the repo today (sister layouts \u2014 the canonical-test-layout cleanup is a separate concern). The planner correctly mirrors the existing file homes rather than relocating, so this is the right call. Worth one line in TASK-1-10 or 1-11 acknowledging the dual-layout convention so future planners don't try to consolidate one of them without warning.\n\n- **TASK-2-2's `default_agent_model` schema field** \u2014 the plan documents it in `config/repositories.yaml.example` but doesn't specify whether the field lives at the per-repo level (i.e. inside a `repos: :` block) or at a top-level config (applies to all repos). The plan's resolver text says \"applies to every role not explicitly overridden\" \u2014 per-repo only? Or per-repo with a top-level fallback? Worth pinning during implementation. Non-blocking.\n\n- **Risk_analyst R11 (`ANTHROPIC_BASE_URL` not in `_PROTECTED_ENV_KEYS`) defense-in-depth gap** is not addressed in the plan. The risk_analyst rated this LOW severity / LOW likelihood with NEGLIGIBLE residual after mitigation. Worth a one-line addition to TASK-1-7 or a new task (\"Add `ANTHROPIC_BASE_URL` to `_PROTECTED_ENV_KEYS` so per-agent `extra_env` cannot redirect agent LLM traffic away from the gateway\"). Non-blocking \u2014 current behavior is no caller sets `ANTHROPIC_BASE_URL` via extra_env, so the surface is theoretical.\n\n- **Risk_analyst R12 (max_turns hardcoded to 1000)** is also not addressed. Risk_analyst rated LOW severity. Worth a one-line follow-up tracker. Non-blocking.\n\n### Summary\n\nPlan is shippable. Task decomposition is clean, acceptance criteria are specific and testable, role\u2194files alignment is validated, primitive-existence audit passes, trust-boundary audit passes, no slice-sizing advisory, slice-DAG forest constraint holds. The single architectural choice that diverges from architect v2 (body-rewriter slice placement) is a defensible planner judgment call. The risk_analyst's R3 (compaction-window mismatch) is the most operationally significant residual \u2014 the plan would be strengthened by adding an operator escape hatch (configurable alias OR per-window lookup OR explicit `context_token_threshold`) and a doc note in TASK-2-9, but cq-4's \"operator validates post-merge\" framing makes this a follow-up concern rather than a merge blocker. ACK.\n", + "metadata": { + "payload": { + "artifact_references": [ + ".egg-state/drafts/2769-plan.md", + ".egg-state/agent-outputs/2769-risk_analyst-output.json", + ".egg-state/agent-outputs/2769-architect-output.json", + "gateway/gateway.py", + "gateway/session_manager.py", + "gateway/anthropic_credentials.py", + "orchestrator/models.py", + "orchestrator/consensus_wrapper.py", + "orchestrator/concurrent_executor.py", + "orchestrator/routes/pipelines.py", + "orchestrator/kubernetes_spawner.py", + "orchestrator/gateway_client.py", + "config/repo_config.py", + "shared/egg_contracts/agent_roles.py", + "shared/egg_restrictions/patterns.py", + "k8s/base/gateway-deployment.yaml" + ], + "reason": "\nReviewed the full `2769-plan.md` (1027 lines), ran the planner's role\u2194files validator (`OK - no violations`), spot-grepped every cited primitive, and cross-referenced against the architect v2 component breakdown and the risk_analyst's 15-risk artifact. The plan is internally consistent, picks **Semantics B** explicitly (TASK-2-6 adds `_rewrite_upstream_model`), and decomposes cleanly into two dependent slices that match the architect's recommended option-3 shape.\n\n### Primitive-Existence Audit (#2594) \u2014 all PASS\n\n| primitive | grep | result |\n|-----------|------|--------|\n| All gateway/orchestrator primitives cited in the Primitives tables (lines 56\u2013116) | re-grepped against current HEAD | every cite resolves to the line number the plan lists \u2713 |\n| `AgentRole` enum at `shared/egg_contracts/agent_roles.py:46` (cited for TASK-2-1 validator) | `grep -n 'class AgentRole' shared/egg_contracts/agent_roles.py` | exists \u2713 |\n| `parse_env_file` at `gateway/anthropic_credentials.py:52` (cited for TASK-1-2) | verified | exists \u2713 |\n| `SECRETS_PATH` env var at `gateway/anthropic_credentials.py:31` (cited for TASK-1-2) | verified | exists \u2713 |\n| `load_repo_pattern_override` at `shared/egg_restrictions/patterns.py:854` (cited as pattern for TASK-2-2) | verified | exists \u2713 |\n| `get_repo_setting` at `config/repo_config.py:248` (cited for TASK-2-2) | verified | exists \u2713 |\n| `_get_forwarded_headers` / `_filter_response_headers` at gateway/gateway.py:9343 / :9348 | verified | exists \u2713 |\n| Existing test file paths (gateway/tests/test_session_manager.py, tests/gateway/test_anthropic_proxy.py, etc.) | `ls` | exist \u2713 |\n| `config/repositories.yaml.example` | exists; the live `config/repositories.yaml` is operator-supplied \u2014 TASK-2-2's helper inherits the missing-file-returns-None behavior from `get_repo_setting` | OK \u2713 |\n\nAll NEW primitives are clearly tagged with `(NEW \u2014 task TASK-X-Y)` per the \u00a79 exception (e.g. line 77 `UpstreamRegistry`, line 97 `PipelineConfig.agent_models`), and the downstream task consuming each new primitive has an explicit dependency on the creating task.\n\n### Trust-Boundary Audit (\u00a710) \u2014 PASS\n\nThe plan's \"Trust-boundary scope checks\" sub-section (lines 118\u2013141) is the cleanest such audit I've seen in a plan. Every gateway-side new primitive runs in the gateway pod; every orchestrator-side new primitive runs in the orchestrator pod (trusted-CI-runner); no in-sandbox-agent test fixture is added that would hit the parent-vs-sibling-conftest trap. All test files live under `tests/gateway/`, `gateway/tests/`, or `orchestrator/tests/` \u2014 all reachable from `make test` in the trusted-CI-runner. The plan explicitly defers live-LiteLLM end-to-end validation to the operator-driven smoke test (cq-4), so the in-sandbox-agent vs trusted-CI-runner mismatch never arises.\n\n### Role\u2194Files Alignment (\u00a78) \u2014 PASS\n\n`python3 -c \"from egg_contracts.plan_parser import parse_plan_file, validate_task_role_alignment as v; \u2026\"` returned `OK - no violations` against the current plan file. Every coder task targets files coder can write (gateway/, orchestrator/, config/, k8s/base/), every tester task targets files tester can write (gateway/tests/, tests/gateway/, orchestrator/tests/), every documenter task targets docs/ or `.md` files documenter can write.\n\n### Slice-Sizing Advisory (#2137 opt-2 advisory only \u2014 non-blocking)\n\nThe plan's own LOC estimate (lines 343\u2013355) is **slice-1 ~700 LOC, slice-2 ~600 LOC** \u2014 both well within the 1,000-LOC soft target. **No advisory needed.** Spot-check: 12 tasks in slice-1 across 9 files + 3 test files + 1 doc, with the LiteLLM-deployment YAML and the body-routing-credential extensions being the largest. 9 tasks in slice-2 across 6 files + 2 test files + 1 doc.\n\n### Slice-DAG Forest-Constraint Check (#2137) \u2014 PASS\n\nThe `yaml-tasks` block (lines 524\u20131027) declares `slice-2.dependencies = [slice-1]` and `slice-1` has no `dependencies` field. Single-parent, no cycles, forest-valid. The plan's \"Slice DAG\" prose (lines 325\u2013339) confirms the constraint. No `forest_violation` discriminator on the contract.\n\n### Coherence with Architect v2\n\nCross-checked the plan against the architect v2 proposal (which I just ACKed). They converge on Semantics B and on slice boundaries, with two minor differences I record as non-blockers below (body-rewriter slice placement; field naming) \u2014 both planner-level judgment calls and either choice ships a working integration.\n\n### Non-blocking\n\n- **TASK-2-3 hardcodes `claude_code_alias = \"opus\"` for every non-Claude model**, with no per-pipeline override of the alias itself. The risk_analyst's R3 (Claude Code compaction math) is explicit: \"the alias presented to Claude Code MUST have a context window \u2264 the real backend's window. real Qwen3 128K \u2192 present 'sonnet' alias (200K) is WRONG; present a 100K-window alias (or set context_token_threshold explicitly) is RIGHT.\" `opus` resolves to a 200K window \u2014 which exceeds Qwen3's 128K context. An operator who flips a role to a sub-200K backend hits the compaction wedge at validation time. Three reasonable fixes \u2014 any one is enough; the planner can pick at implementation time, but the plan should note that the operator's escape hatch exists:\n - (a) Make the alias configurable: change `PipelineConfig.agent_models` values from a string to a `(claude_code_alias, litellm_model)` tuple/object so the operator can pick `(\"haiku\", \"qwen3-coder-30b\")`.\n - (b) Add a small per-window lookup: when the LiteLLM model's window is known to be <200K, the resolver picks a smaller alias automatically.\n - (c) Set Claude Code's `context_token_threshold` SDK option explicitly for LiteLLM-bound agents (separate lever from the model name).\n - **Why non-blocking**: cq-4 puts empirical validation on the operator post-merge. The operator can patch the resolver as a stopgap if validation surfaces the issue. Plus the issue is already enumerated in the risk_analyst's R3, so the operator has been warned. But the plan as written has no operator-controllable escape hatch \u2014 it requires a source-code patch. Worth a doc note in TASK-2-9 (\"known limitation: if your real backend has a context window < ~190K, you must patch the resolver to use a smaller Claude alias or set `context_token_threshold` \u2014 see follow-up issue #XXXX\").\n\n- **TASK-2-6 places the body-rewriter in slice-2**, while the architect v2 places it in slice-1. Both work; planner's split keeps slice-1 purely additive at the routing level (the registry can resolve to LiteLLM but no body is rewritten until slice-2 ships the `_rewrite_upstream_model` helper). This means a hypothetical slice-1-only deployment that an operator somehow points at LiteLLM (by setting `Session.upstream='litellm'` out-of-band) would forward the body byte-unchanged with `model='opus'` to LiteLLM, which would fail at LiteLLM with \"model not found\" \u2014 fail-closed, just at the upstream layer instead of in the gateway. Acceptable. Implementer should be aware of this when reviewing the slice-1 PR.\n\n- **TASK-2-6 ordering: rewriter AFTER `_filter_blocked_tools`** (vs architect v2's \"BEFORE\"). Functionally commutative \u2014 rewriter touches `body['model']`, tool-strip touches `body['tools']`, no shared keys. Either order produces the same bytes on the wire. The architect's \"BEFORE\" preserves the \"canonical body downstream\" invariant slightly better (everything downstream sees one body shape). The planner's \"AFTER\" is slightly more efficient (skips one re-serialization if no rewrite). Implementer can pick; either is correct. Worth one line in the implementation note explaining the choice and rationale.\n\n- **Field naming: `Session.upstream` / `Session.upstream_model` (planner) vs `Session.agent_upstream` / `Session.agent_model_litellm` (architect v2)**. Both are valid; planner's names are shorter. Implementer needs to pick one and be consistent across `Session`, `register_session`, `/api/v1/sessions/create` payload, `GatewayClient.register_session`, and the test assertions. The plan and the architect v2 docs both refer to these fields by their own naming, so the implementer needs to reconcile \u2014 a one-line note in either re-propose would prevent confusion downstream.\n\n- **TASK-1-1 acceptance: \"the existing Anthropic credential resolver (preserves the `# noqa: EGG200` annotation pattern at `gateway/gateway.py:9325`)\"** \u2014 good attention to detail (lifting the noqa with the singleton). Worth verifying in code review that the noqa migrates intact to wherever `UpstreamRegistry` lives.\n\n- **TASK-1-12 doc** mentions cq-1/cq-2/cq-5/cq-7/cq-8 but not cq-9 (tool-strip uniformity) or cq-11 (`opus[1m]` left alone). These are also load-bearing decisions; worth one bullet each. Non-blocking \u2014 implementer can include during writing.\n\n- **TASK-1-8 / TASK-1-9 don't include adding `LITELLM_BASE_URL` to `gateway-deployment.yaml`'s `env:` block**. The architect v2 explicitly recommends this (so an operator can repoint via kustomize overlay without setting an env var the base manifest hasn't declared). Functionally the system works without it (the registry's hard-coded default `http://litellm.egg-system.svc.cluster.local:4000` matches the in-cluster Service DNS, and a kustomize strategic-merge overlay can still inject the env var). But declaring it in the base manifest is the \"discoverable optional config knob\" pattern. Worth folding into TASK-1-1 or TASK-1-8 as a small addition (3 lines of YAML). Non-blocking \u2014 the default works and overlay overrides function regardless.\n\n- **TASK-1-2 acceptance: \"With `LITELLM_MASTER_KEY` unset, the resolver returns `None` and does not warn at startup.\"** \u2014 combined with TASK-1-3 (\"Missing credentials for either upstream return a 401 with the same JSON body shape as today\"), the failure mode for \"session declares LiteLLM but no key\" is a 401 from `_inject_upstream_credentials`. But cq-8 / AC-8 (per architect v2) call for a 502 (upstream-unreachable / misconfig), not a 401 (auth). 401 implies \"your credential is wrong\"; 502 implies \"the upstream is broken.\" For a missing master key, 502 (or 500) is the more truthful status because the *operator* misconfigured the gateway, not the agent. Worth aligning. Non-blocking \u2014 the error reaches the agent either way, the agent's failure mode is the same.\n\n- **R5 in the planner-view risks (\"Empirical Claude Code compaction-math compatibility\")** says \"Slice 2 keeps Claude Code's `--model` flag set to a recognised Claude alias (`opus`) for all LiteLLM-bound agents, per cq-5, so Claude Code's compaction math stays sane.\" This claim is only true when the backend's window \u2265 `opus`'s window (200K). For backends <200K (Qwen3 128K), compaction math is NOT sane. The risk text overstates the mitigation. Tied to the first non-blocker above; would resolve when that follow-up lands. Non-blocking.\n\n- **Test-path inconsistency**: TASK-1-10 places new tests under `tests/gateway/` (matching the layout of `test_anthropic_proxy.py`); TASK-1-11 places extensions under `gateway/tests/` (matching `test_session_manager.py`). Both directories exist in the repo today (sister layouts \u2014 the canonical-test-layout cleanup is a separate concern). The planner correctly mirrors the existing file homes rather than relocating, so this is the right call. Worth one line in TASK-1-10 or 1-11 acknowledging the dual-layout convention so future planners don't try to consolidate one of them without warning.\n\n- **TASK-2-2's `default_agent_model` schema field** \u2014 the plan documents it in `config/repositories.yaml.example` but doesn't specify whether the field lives at the per-repo level (i.e. inside a `repos: :` block) or at a top-level config (applies to all repos). The plan's resolver text says \"applies to every role not explicitly overridden\" \u2014 per-repo only? Or per-repo with a top-level fallback? Worth pinning during implementation. Non-blocking.\n\n- **Risk_analyst R11 (`ANTHROPIC_BASE_URL` not in `_PROTECTED_ENV_KEYS`) defense-in-depth gap** is not addressed in the plan. The risk_analyst rated this LOW severity / LOW likelihood with NEGLIGIBLE residual after mitigation. Worth a one-line addition to TASK-1-7 or a new task (\"Add `ANTHROPIC_BASE_URL` to `_PROTECTED_ENV_KEYS` so per-agent `extra_env` cannot redirect agent LLM traffic away from the gateway\"). Non-blocking \u2014 current behavior is no caller sets `ANTHROPIC_BASE_URL` via extra_env, so the surface is theoretical.\n\n- **Risk_analyst R12 (max_turns hardcoded to 1000)** is also not addressed. Risk_analyst rated LOW severity. Worth a one-line follow-up tracker. Non-blocking.\n\n### Summary\n\nPlan is shippable. Task decomposition is clean, acceptance criteria are specific and testable, role\u2194files alignment is validated, primitive-existence audit passes, trust-boundary audit passes, no slice-sizing advisory, slice-DAG forest constraint holds. The single architectural choice that diverges from architect v2 (body-rewriter slice placement) is a defensible planner judgment call. The risk_analyst's R3 (compaction-window mismatch) is the most operationally significant residual \u2014 the plan would be strengthened by adding an operator escape hatch (configurable alias OR per-window lookup OR explicit `context_token_threshold`) and a doc note in TASK-2-9, but cq-4's \"operator validates post-merge\" framing makes this a follow-up concern rather than a merge blocker. ACK.\n", + "ack_version": 1 + }, + "version": 1 + }, + "timestamp": "2026-05-22T03:50:58.837342+00:00", + "phase": "plan" + }, + { + "id": "a2441044-c6dd-4d", + "pipeline_id": "issue-2769", + "from_role": "orchestrator", + "to_role": "task_planner", + "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-22T03:50:58.837417+00:00", + "phase": "plan" + }, + { + "id": "21769c19-eb66-4c", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:50:58.904695+00:00", + "phase": "plan" + }, + { + "id": "1e3717fa-6888-41", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "to_role": "all", + "message_type": "CONSENSUS_CONFIRMED", + "subject": "Confirmed by task_planner", + "body": "", + "metadata": { + "consensus_reached": false + }, + "timestamp": "2026-05-22T03:51:02.814920+00:00", + "phase": "plan" + }, + { + "id": "e4ddcf79-9187-45", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "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-22T03:51:13.663337+00:00" + }, + "timestamp": "2026-05-22T03:51:13.718330+00:00", + "phase": "plan" + }, + { + "id": "c490a35f-aa86-42", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:51:22.418743+00:00", + "phase": "plan" + }, + { + "id": "bc91191f-d77e-46", + "pipeline_id": "issue-2769", + "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-22T03:49:38.773629+00:00" + }, + "timestamp": "2026-05-22T03:51:39.033994+00:00", + "phase": "plan" + }, + { + "id": "17cffbc9-5d4a-4d", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "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-22T03:51:13.663337+00:00" + }, + "timestamp": "2026-05-22T03:52:13.816264+00:00", + "phase": "plan" + }, + { + "id": "cdcf8aed-2bc0-47", + "pipeline_id": "issue-2769", + "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-22T03:42:06.692615+00:00" + }, + "timestamp": "2026-05-22T03:52:22.580390+00:00", + "phase": "plan" + }, + { + "id": "a6dd2396-e1a5-44", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "risk_analyst", + "message_type": "CONSENSUS_ACK", + "subject": "ACK from reviewer_plan for risk_analyst", + "body": "\nReviewed the full risk_analyst JSON (`.egg-state/agent-outputs/2769-risk_analyst-output.json`, 459 lines). The artifact is comprehensive and operationally honest: HIGH overall rating with PROCEED_WITH_MITIGATIONS, 15 risks (R1\u2013R15) each with severity, likelihood, impact, description, affected_files, mitigation strategy + effort + residual, and `requires_human_review` flag. The Primitive-Existence and Trust-Boundary sub-sections are explicit and convergent with what I separately verified.\n\n### Risk-coverage audit\n\nCross-checked the risk list against the architect's design, the planner's task breakdown, and my own preparation walk through the codebase. Every meaningful failure surface is covered:\n\n- **R1 LiteLLM supply-chain (CRITICAL/MEDIUM)** \u2014 six layered controls (pinned digest, cosign, NetworkPolicy, ServiceAccount RBAC, no JWT auth, kill-switch via `agent_models = {}`). The March 2026 PyPI incident citation and the subsequent CVE-2026-42208 / -35029 / -42271 list are accurately recent and bound the recommended pin floor (>= 1.83.7). The recommended controls map cleanly onto the architect v2's NetworkPolicy-egress-overlay component and the planner's TASK-1-8 manifest acceptance criteria (image-pinning specifically; planner's AC says \"pinned LiteLLM image\" but doesn't yet specify \"digest, not floating tag\" \u2014 worth a one-line tightening at implement time; non-blocking).\n\n- **R2 LiteLLM streaming tool_use drop (CRITICAL/HIGH)** \u2014 accurately cites the GitHub issue chain (#25561, #25321, #24765) and identifies the egg-specific worst case (cq-5 + Claude Code + non-Anthropic backend is exactly the configuration these bugs were filed against). The mitigation correctly defers correctness to the operator's acceptance-test smoke run (cq-4) and recommends an optional defensive sentinel in the gateway. The empty-input-detection sentinel idea is a clean future addition; correctly noted as out-of-scope-for-merge.\n\n- **R3 Claude Code compaction math drift (HIGH/MEDIUM)** \u2014 the most operationally consequential risk for the architecture chosen. Correctly identifies that the cq-5 recognized-alias mitigation only works if the alias's window \u2264 the real backend's window, and explicitly calls out the Qwen3 128K vs Claude `opus` 200K case as the failure mode. My ACK to the task_planner v1 cited R3 as the basis for a non-blocking observation \u2014 the plan's `claude_code_alias = \"opus\"` hardcoding doesn't give operators an in-config escape hatch. The risk_analyst's recommended mitigations (\"(1) explicit invariant, (2) per-(alias, real-model) lookup table, (3) explicit context_token_threshold\") are all viable and align with my non-blocker.\n\n- **R4 vLLM / Qwen3 self-hosted bugs (MEDIUM/HIGH)** \u2014 correctly scoped out of this issue (cq-6 deferred self-hosted vLLM); documented for the eventual self-hosted cut. Cites vLLM #21565, #17655, #23992, #20611, #39056 \u2014 all real.\n\n- **R5 Hosted provider as new credential + supply-chain surface (MEDIUM/MEDIUM)** \u2014 the operational implications of cq-6 (hosted Qwen first) are surfaced honestly. The mitigation correctly defers to provider-published policies and to the existing cq-7 LITELLM_MASTER_KEY-as-front pattern; cq-9 tool-strip stays uniform.\n\n- **R6 Session schema gap (MEDIUM/CERTAIN)** \u2014 exactly the gap the architect's slice-1 components 4\u20135 (\"Session-storage extensions\" and \"Orchestrator \u2192 gateway session-create payload extension\") address. The risk_analyst's mitigation prose maps line-by-line onto the planner's TASK-1-4 / TASK-1-5 / TASK-1-7. Convergent across all three producers.\n\n- **R7 `build_consensus_wrapped_command` model arg drift (MEDIUM/CERTAIN)** \u2014 exactly the gap the planner's TASK-2-3 / TASK-2-4 / TASK-2-5 address. The risk_analyst's \"defensive assert in the consensus wrapper that errors if model and the session's model_alias disagree\" is a nice belt-and-braces addition the planner did not include \u2014 worth folding in as a follow-up enhancement (non-blocking; the single-source-of-truth resolver pattern is itself the primary defense).\n\n- **R8 Claude Code harness assumption durability (MEDIUM/MEDIUM)** \u2014 orthogonal to the body-rewrite Semantics A vs B debate I had with the architect (architect's R8 is the tokenizer-mismatch argument; risk_analyst's R8 is the Claude Code closed-source heuristic durability). Both are real; named-collisions on the \"R8\" label across the two artifacts is mildly confusing but not substantive. Risk_analyst's mitigation (record + bound the Claude Code version + scheduled CI smoke test) is the right shape; correctly noted as out-of-scope-for-merge per cq-4.\n\n- **R9 max_llm_cost_per_hour silent break (MEDIUM/CERTAIN)** \u2014 exactly the observable-behavior gap I raised against architect v1 (architect v2 then surfaced it explicitly in `out_of_scope_explicitly`). Risk_analyst's mitigation (\"create a follow-up issue + document the regression in the deployment YAML / config README\") is the right framing. Convergent.\n\n- **R10 SSE accumulator hardcoded to Anthropic event names (MEDIUM/LOW)** \u2014 cheap defensive log on unknown event_type is a good, low-cost mitigation. Worth folding into the implementer's slice-1 work (one logger.warning line in `_SSEAccumulator`); the planner did not break this out as a task. Non-blocking \u2014 the bug surface is well-bounded.\n\n- **R11 `ANTHROPIC_BASE_URL` not in `_PROTECTED_ENV_KEYS` (LOW/LOW)** \u2014 accurate defense-in-depth gap. One-line fix. I flagged this as a non-blocker in my task_planner ACK; risk_analyst flagged it more formally with negligible-residual-after-mitigation framing. Convergent.\n\n- **R12 max_turns=1000 hardcoded (LOW/MEDIUM)** \u2014 correctly scoped as a tuning concern for follow-up; per-model max_turns is reasonable as a future PipelineConfig.agent_models extension or a separate `max_turns_per_role` field.\n\n- **R13 Empirical validation deferred (MEDIUM/CERTAIN)** \u2014 the structural-vs-nominal \"no-op by default\" guarantee is exactly what `requires_human_review: true` should flag. The risk_analyst's mitigation (\"Reviewer should verify this by inspection: searching for 'litellm' in the diff should yield only conditional / opt-in code paths\") is the right reviewer test at implement-PR time. I noted this as my reviewer mandate for the implement-phase PR.\n\n- **R14 UpstreamRegistry abstraction needed per feedback Q3 (LOW/CERTAIN)** \u2014 exactly the architect's slice-1 component 1. Convergent across all three producers.\n\n- **R15 Fail-closed visibility requires monitoring (LOW/MEDIUM)** \u2014 operational trade-off correctly recorded as known-and-accepted per cq-8, not as discoverable behavior. Operator-side acknowledgement is the right framing.\n\n### Runtime-Primitive Audit (#2594) \u2014 convergent\n\nThe risk_analyst's `runtime_primitive_audit_per_2594` table (lines 308\u2013372) identifies the same MISSING / EXISTING / EXTENDABLE classifications I derived independently. Notable matches:\n- `Session.upstream / .model_alias`: MISSING \u2713\n- `register_session(upstream=..., model_alias=...)`: MISSING \u2713\n- `PipelineConfig.agent_models[role]`: MISSING \u2713\n- `build_consensus_wrapped_command(model=...)`: EXISTS but UNUSED \u2713\n- `LiteLLM Deployment + LITELLM_MASTER_KEY`: MISSING \u2713\n- `Internal cluster DNS allowlisting`: NOT NEEDED (correctly notes that LiteLLM is reached via the gateway's httpx client, not via Squid \u2014 the analyst correctly avoids the trap of trying to allowlist `*.svc.cluster.local` in `gateway/allowed_domains.txt`) \u2713\n- `AnthropicCredential extension for LiteLLM master key`: EXTENDABLE (matches the architect's \"sibling LiteLLMCredentialsManager\" recommendation) \u2713\n- `Claude Code recognised-alias compaction math`: DOCUMENTED EXTERNALLY (correctly noted as a heuristic dependency) \u2713\n- `_SSEAccumulator (Anthropic event names)`: EXISTS, EVENT-NAME-HARDCODED (matches my exploration) \u2713\n- `max_llm_cost_per_hour`: EXISTS but UNINSTRUMENTED FOR LITELLM \u2713\n- `_PROTECTED_ENV_KEYS for ANTHROPIC_BASE_URL`: MISSING \u2713\n- `max_turns per model`: HARDCODED \u2713\n\n### Trust-boundary audit \u2014 convergent\n\nThe risk_analyst's `trust_boundary_audit` (lines 374\u2013398) names all five boundaries (Sandbox\u2192Gateway, Gateway\u2192Anthropic, Gateway\u2192LiteLLM, LiteLLM\u2192Hosted Qwen, Orchestrator\u2192Gateway register_session). The \"Gateway \u2192 Anthropic UNCHANGED\" call-out and the \"LiteLLM \u2192 Hosted Qwen NEW: sees full request bodies\" are the right shape for an operator reviewing this design.\n\n### Rollback plan\n\nThe `rollback_plan` (lines 400\u2013423) names four scenarios with corresponding actions:\n- Per-agent flip exposes bug \u2192 unset `agent_models[role]` (config-only, structural)\n- Future LiteLLM CVE \u2192 scale Deployment to 0, unset cluster-wide, rotate key (operational)\n- LiteLLM pinned image regresses \u2192 re-pin to last-known-good sha256 (operational, git history dependent)\n- Hosted provider outage \u2192 unset agent_models, revoke provider key\n\nAll four rollback paths are real and align with cq-8's fail-closed framing.\n\n### Non-blocking\n\n- **R8 label collision with architect v2**: the architect's R8 (tokenizer-mismatch on count_tokens, used to reject Semantics A) and the risk_analyst's R8 (Claude Code heuristic durability) share the label but address different concerns. Not blocking \u2014 implementer's audit notes should disambiguate. Worth a one-line renumber if either artifact gets a re-propose.\n\n- **R1 mitigation \"(2) Enable cosign signature verification at the imagePullPolicy layer or via a Kyverno/Cosign admission policy\"**: cosign / Kyverno admission policies are operator-cluster-dependent and may not be present in every egg deployment. Worth softening to \"where a cluster admission controller is available, enable cosign verification; otherwise the digest pin is the minimum bar.\" Non-blocking; the digest pin alone covers the primary attack surface.\n\n- **The `requires_human_review` flags on R1, R2, R13** are all accurate \u2014 these are the three risks where operator-side awareness materially changes acceptance. The plan reviewer's mandate (which I already addressed in the task_planner ACK) is convergent: the plan-reviewer must confirm the no-op-by-default structural guarantee in the implement-phase PR; the operator must read R1 and R2 before flipping a role to LiteLLM at validation time.\n\n- **R3 alias-window mitigation** \u2014 I raised this as the single most operationally consequential non-blocker in my task_planner ACK; the risk_analyst flagged it independently with the same severity rating (HIGH). Convergent across both reviews. The fix is small (operator-controllable claude_code_alias OR per-window lookup OR explicit context_token_threshold). Worth a follow-up issue if not addressed in the implement phase.\n\n### Summary\n\nRisk assessment is thorough and operationally honest. Coverage is convergent with the architect's design and the planner's tasks (where the same gap is named by all three artifacts: R6 \u2194 architect Session-storage extensions \u2194 planner TASK-1-4/5; R7 \u2194 architect resolver \u2194 planner TASK-2-3/4/5). The HIGH overall rating is driven by EXTERNAL risks (LiteLLM supply-chain, LiteLLM SSE bugs, Claude Code compaction math) \u2014 not by the egg-side code change, which is correctly characterized as low-risk because of the no-op-by-default framing. PROCEED_WITH_MITIGATIONS is the right call. No blocking concerns. ACK.\n", + "metadata": { + "payload": { + "artifact_references": [ + ".egg-state/agent-outputs/2769-risk_analyst-output.json", + ".egg-state/drafts/2769-analysis.md", + ".egg-state/drafts/2769-plan.md", + ".egg-state/agent-outputs/2769-architect-output.json", + "gateway/gateway.py", + "gateway/session_manager.py", + "gateway/anthropic_credentials.py", + "orchestrator/kubernetes_spawner.py", + "orchestrator/consensus_wrapper.py", + "orchestrator/models.py", + "orchestrator/concurrent_executor.py", + "orchestrator/routes/pipelines.py", + "orchestrator/overseer/self_monitor.py", + "shared/egg_agent/client.py", + "k8s/base/gateway-deployment.yaml" + ], + "reason": "\nReviewed the full risk_analyst JSON (`.egg-state/agent-outputs/2769-risk_analyst-output.json`, 459 lines). The artifact is comprehensive and operationally honest: HIGH overall rating with PROCEED_WITH_MITIGATIONS, 15 risks (R1\u2013R15) each with severity, likelihood, impact, description, affected_files, mitigation strategy + effort + residual, and `requires_human_review` flag. The Primitive-Existence and Trust-Boundary sub-sections are explicit and convergent with what I separately verified.\n\n### Risk-coverage audit\n\nCross-checked the risk list against the architect's design, the planner's task breakdown, and my own preparation walk through the codebase. Every meaningful failure surface is covered:\n\n- **R1 LiteLLM supply-chain (CRITICAL/MEDIUM)** \u2014 six layered controls (pinned digest, cosign, NetworkPolicy, ServiceAccount RBAC, no JWT auth, kill-switch via `agent_models = {}`). The March 2026 PyPI incident citation and the subsequent CVE-2026-42208 / -35029 / -42271 list are accurately recent and bound the recommended pin floor (>= 1.83.7). The recommended controls map cleanly onto the architect v2's NetworkPolicy-egress-overlay component and the planner's TASK-1-8 manifest acceptance criteria (image-pinning specifically; planner's AC says \"pinned LiteLLM image\" but doesn't yet specify \"digest, not floating tag\" \u2014 worth a one-line tightening at implement time; non-blocking).\n\n- **R2 LiteLLM streaming tool_use drop (CRITICAL/HIGH)** \u2014 accurately cites the GitHub issue chain (#25561, #25321, #24765) and identifies the egg-specific worst case (cq-5 + Claude Code + non-Anthropic backend is exactly the configuration these bugs were filed against). The mitigation correctly defers correctness to the operator's acceptance-test smoke run (cq-4) and recommends an optional defensive sentinel in the gateway. The empty-input-detection sentinel idea is a clean future addition; correctly noted as out-of-scope-for-merge.\n\n- **R3 Claude Code compaction math drift (HIGH/MEDIUM)** \u2014 the most operationally consequential risk for the architecture chosen. Correctly identifies that the cq-5 recognized-alias mitigation only works if the alias's window \u2264 the real backend's window, and explicitly calls out the Qwen3 128K vs Claude `opus` 200K case as the failure mode. My ACK to the task_planner v1 cited R3 as the basis for a non-blocking observation \u2014 the plan's `claude_code_alias = \"opus\"` hardcoding doesn't give operators an in-config escape hatch. The risk_analyst's recommended mitigations (\"(1) explicit invariant, (2) per-(alias, real-model) lookup table, (3) explicit context_token_threshold\") are all viable and align with my non-blocker.\n\n- **R4 vLLM / Qwen3 self-hosted bugs (MEDIUM/HIGH)** \u2014 correctly scoped out of this issue (cq-6 deferred self-hosted vLLM); documented for the eventual self-hosted cut. Cites vLLM #21565, #17655, #23992, #20611, #39056 \u2014 all real.\n\n- **R5 Hosted provider as new credential + supply-chain surface (MEDIUM/MEDIUM)** \u2014 the operational implications of cq-6 (hosted Qwen first) are surfaced honestly. The mitigation correctly defers to provider-published policies and to the existing cq-7 LITELLM_MASTER_KEY-as-front pattern; cq-9 tool-strip stays uniform.\n\n- **R6 Session schema gap (MEDIUM/CERTAIN)** \u2014 exactly the gap the architect's slice-1 components 4\u20135 (\"Session-storage extensions\" and \"Orchestrator \u2192 gateway session-create payload extension\") address. The risk_analyst's mitigation prose maps line-by-line onto the planner's TASK-1-4 / TASK-1-5 / TASK-1-7. Convergent across all three producers.\n\n- **R7 `build_consensus_wrapped_command` model arg drift (MEDIUM/CERTAIN)** \u2014 exactly the gap the planner's TASK-2-3 / TASK-2-4 / TASK-2-5 address. The risk_analyst's \"defensive assert in the consensus wrapper that errors if model and the session's model_alias disagree\" is a nice belt-and-braces addition the planner did not include \u2014 worth folding in as a follow-up enhancement (non-blocking; the single-source-of-truth resolver pattern is itself the primary defense).\n\n- **R8 Claude Code harness assumption durability (MEDIUM/MEDIUM)** \u2014 orthogonal to the body-rewrite Semantics A vs B debate I had with the architect (architect's R8 is the tokenizer-mismatch argument; risk_analyst's R8 is the Claude Code closed-source heuristic durability). Both are real; named-collisions on the \"R8\" label across the two artifacts is mildly confusing but not substantive. Risk_analyst's mitigation (record + bound the Claude Code version + scheduled CI smoke test) is the right shape; correctly noted as out-of-scope-for-merge per cq-4.\n\n- **R9 max_llm_cost_per_hour silent break (MEDIUM/CERTAIN)** \u2014 exactly the observable-behavior gap I raised against architect v1 (architect v2 then surfaced it explicitly in `out_of_scope_explicitly`). Risk_analyst's mitigation (\"create a follow-up issue + document the regression in the deployment YAML / config README\") is the right framing. Convergent.\n\n- **R10 SSE accumulator hardcoded to Anthropic event names (MEDIUM/LOW)** \u2014 cheap defensive log on unknown event_type is a good, low-cost mitigation. Worth folding into the implementer's slice-1 work (one logger.warning line in `_SSEAccumulator`); the planner did not break this out as a task. Non-blocking \u2014 the bug surface is well-bounded.\n\n- **R11 `ANTHROPIC_BASE_URL` not in `_PROTECTED_ENV_KEYS` (LOW/LOW)** \u2014 accurate defense-in-depth gap. One-line fix. I flagged this as a non-blocker in my task_planner ACK; risk_analyst flagged it more formally with negligible-residual-after-mitigation framing. Convergent.\n\n- **R12 max_turns=1000 hardcoded (LOW/MEDIUM)** \u2014 correctly scoped as a tuning concern for follow-up; per-model max_turns is reasonable as a future PipelineConfig.agent_models extension or a separate `max_turns_per_role` field.\n\n- **R13 Empirical validation deferred (MEDIUM/CERTAIN)** \u2014 the structural-vs-nominal \"no-op by default\" guarantee is exactly what `requires_human_review: true` should flag. The risk_analyst's mitigation (\"Reviewer should verify this by inspection: searching for 'litellm' in the diff should yield only conditional / opt-in code paths\") is the right reviewer test at implement-PR time. I noted this as my reviewer mandate for the implement-phase PR.\n\n- **R14 UpstreamRegistry abstraction needed per feedback Q3 (LOW/CERTAIN)** \u2014 exactly the architect's slice-1 component 1. Convergent across all three producers.\n\n- **R15 Fail-closed visibility requires monitoring (LOW/MEDIUM)** \u2014 operational trade-off correctly recorded as known-and-accepted per cq-8, not as discoverable behavior. Operator-side acknowledgement is the right framing.\n\n### Runtime-Primitive Audit (#2594) \u2014 convergent\n\nThe risk_analyst's `runtime_primitive_audit_per_2594` table (lines 308\u2013372) identifies the same MISSING / EXISTING / EXTENDABLE classifications I derived independently. Notable matches:\n- `Session.upstream / .model_alias`: MISSING \u2713\n- `register_session(upstream=..., model_alias=...)`: MISSING \u2713\n- `PipelineConfig.agent_models[role]`: MISSING \u2713\n- `build_consensus_wrapped_command(model=...)`: EXISTS but UNUSED \u2713\n- `LiteLLM Deployment + LITELLM_MASTER_KEY`: MISSING \u2713\n- `Internal cluster DNS allowlisting`: NOT NEEDED (correctly notes that LiteLLM is reached via the gateway's httpx client, not via Squid \u2014 the analyst correctly avoids the trap of trying to allowlist `*.svc.cluster.local` in `gateway/allowed_domains.txt`) \u2713\n- `AnthropicCredential extension for LiteLLM master key`: EXTENDABLE (matches the architect's \"sibling LiteLLMCredentialsManager\" recommendation) \u2713\n- `Claude Code recognised-alias compaction math`: DOCUMENTED EXTERNALLY (correctly noted as a heuristic dependency) \u2713\n- `_SSEAccumulator (Anthropic event names)`: EXISTS, EVENT-NAME-HARDCODED (matches my exploration) \u2713\n- `max_llm_cost_per_hour`: EXISTS but UNINSTRUMENTED FOR LITELLM \u2713\n- `_PROTECTED_ENV_KEYS for ANTHROPIC_BASE_URL`: MISSING \u2713\n- `max_turns per model`: HARDCODED \u2713\n\n### Trust-boundary audit \u2014 convergent\n\nThe risk_analyst's `trust_boundary_audit` (lines 374\u2013398) names all five boundaries (Sandbox\u2192Gateway, Gateway\u2192Anthropic, Gateway\u2192LiteLLM, LiteLLM\u2192Hosted Qwen, Orchestrator\u2192Gateway register_session). The \"Gateway \u2192 Anthropic UNCHANGED\" call-out and the \"LiteLLM \u2192 Hosted Qwen NEW: sees full request bodies\" are the right shape for an operator reviewing this design.\n\n### Rollback plan\n\nThe `rollback_plan` (lines 400\u2013423) names four scenarios with corresponding actions:\n- Per-agent flip exposes bug \u2192 unset `agent_models[role]` (config-only, structural)\n- Future LiteLLM CVE \u2192 scale Deployment to 0, unset cluster-wide, rotate key (operational)\n- LiteLLM pinned image regresses \u2192 re-pin to last-known-good sha256 (operational, git history dependent)\n- Hosted provider outage \u2192 unset agent_models, revoke provider key\n\nAll four rollback paths are real and align with cq-8's fail-closed framing.\n\n### Non-blocking\n\n- **R8 label collision with architect v2**: the architect's R8 (tokenizer-mismatch on count_tokens, used to reject Semantics A) and the risk_analyst's R8 (Claude Code heuristic durability) share the label but address different concerns. Not blocking \u2014 implementer's audit notes should disambiguate. Worth a one-line renumber if either artifact gets a re-propose.\n\n- **R1 mitigation \"(2) Enable cosign signature verification at the imagePullPolicy layer or via a Kyverno/Cosign admission policy\"**: cosign / Kyverno admission policies are operator-cluster-dependent and may not be present in every egg deployment. Worth softening to \"where a cluster admission controller is available, enable cosign verification; otherwise the digest pin is the minimum bar.\" Non-blocking; the digest pin alone covers the primary attack surface.\n\n- **The `requires_human_review` flags on R1, R2, R13** are all accurate \u2014 these are the three risks where operator-side awareness materially changes acceptance. The plan reviewer's mandate (which I already addressed in the task_planner ACK) is convergent: the plan-reviewer must confirm the no-op-by-default structural guarantee in the implement-phase PR; the operator must read R1 and R2 before flipping a role to LiteLLM at validation time.\n\n- **R3 alias-window mitigation** \u2014 I raised this as the single most operationally consequential non-blocker in my task_planner ACK; the risk_analyst flagged it independently with the same severity rating (HIGH). Convergent across both reviews. The fix is small (operator-controllable claude_code_alias OR per-window lookup OR explicit context_token_threshold). Worth a follow-up issue if not addressed in the implement phase.\n\n### Summary\n\nRisk assessment is thorough and operationally honest. Coverage is convergent with the architect's design and the planner's tasks (where the same gap is named by all three artifacts: R6 \u2194 architect Session-storage extensions \u2194 planner TASK-1-4/5; R7 \u2194 architect resolver \u2194 planner TASK-2-3/4/5). The HIGH overall rating is driven by EXTERNAL risks (LiteLLM supply-chain, LiteLLM SSE bugs, Claude Code compaction math) \u2014 not by the egg-side code change, which is correctly characterized as low-risk because of the no-op-by-default framing. PROCEED_WITH_MITIGATIONS is the right call. No blocking concerns. ACK.\n", + "ack_version": 1 + }, + "version": 1 + }, + "timestamp": "2026-05-22T03:52:36.323820+00:00", + "phase": "plan" + }, + { + "id": "e782e87a-f4ac-47", + "pipeline_id": "issue-2769", + "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-22T03:49:38.773629+00:00" + }, + "timestamp": "2026-05-22T03:52:39.108264+00:00", + "phase": "plan" + }, + { + "id": "87cf1d9e-86ea-40", + "pipeline_id": "issue-2769", + "from_role": "reviewer_plan", + "to_role": "all", + "message_type": "CONSENSUS_CONFIRMED", + "subject": "Confirmed by reviewer_plan", + "body": "", + "metadata": { + "consensus_reached": true + }, + "timestamp": "2026-05-22T03:52:39.896667+00:00", + "phase": "plan" + }, + { + "id": "42f1a681-0130-48", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:52:40.078182+00:00", + "phase": "plan" + }, + { + "id": "2eb393b0-1905-44", + "pipeline_id": "issue-2769", + "from_role": "risk_analyst", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:52:40.094002+00:00", + "phase": "plan" + }, + { + "id": "1e6dbb0a-4d4c-4d", + "pipeline_id": "issue-2769", + "from_role": "architect", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T03:52:40.106148+00:00", + "phase": "plan" + }, + { + "id": "a510f3ca-18a5-4b", + "pipeline_id": "issue-2769", + "from_role": "task_planner", + "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-22T03:52:44.501632+00:00" + }, + "timestamp": "2026-05-22T03:52:44.591468+00:00", + "phase": "plan" + } +] \ No newline at end of file diff --git a/.egg-state/brc-history/2769-plan.md b/.egg-state/brc-history/2769-plan.md new file mode 100644 index 0000000000..6f1bc9e4e6 --- /dev/null +++ b/.egg-state/brc-history/2769-plan.md @@ -0,0 +1,2473 @@ +# BRC Consensus History — plan phase + +Generated: 2026-05-22T03:52:44Z +Pipeline: issue-2769 + +### [2026-05-22T03:21:14Z] overseer (HEARTBEAT): heartbeat: WORKING + +Overseer monitoring plan phase. 4 agents active (architect, task_planner, reviewer_plan, risk_analyst) at ~2min elapsed. No alerts. Nominal. + +````yaml +id: 35aef3af-00b8-43 +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:22:34Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 83c6031a-7abf-44 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:22:34.673673+00:00' +```` + +### [2026-05-22T03:23:34Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: a12cf2f1-0f0d-4a +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:22:34.673673+00:00' +```` + +### [2026-05-22T03:24:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 7102e57b-bf5e-44 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:22:34.673673+00:00' +```` + +### [2026-05-22T03:25:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 83c13591-c54c-4c +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:22:34.673673+00:00' +```` + +### [2026-05-22T03:26:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 2b60b75b-cf76-4c +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:22:34.673673+00:00' +```` + +### [2026-05-22T03:27:35Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 355c4439-56c8-46 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:22:34.673673+00:00' +```` + +### [2026-05-22T03:28:50Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 054df3da-2bb2-42 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:22:34.673673+00:00' +```` + +### [2026-05-22T03:29:15Z] architect (CONSENSUS_PROPOSE): Proposal from architect + +Architecture analysis for non-Claude models via LiteLLM proxy. Recommends Option A (gateway-side UpstreamRegistry keyed by per-agent session metadata; LiteLLM as sibling Deployment in egg-system) over options B (LiteLLM-fronts-everything, rejected — violates no-regression-on-Claude), C (route on body, rejected — conflicts with compaction-math mitigation), and D (egg_agent SDK path, rejected — cq-5 forbids). Decomposes work into two dependent slices: slice-1 ships the gateway router + LiteLLM topology as a no-op by default; slice-2 adds PipelineConfig.agent_models + repositories.yaml default_model + resolve_model_for_role() helper that drives BOTH the consensus_wrapper --model flag (recognized Claude alias, keeps compaction math sane) AND the gateway's session.agent_upstream/agent_model_litellm fields (decides actual backend). Honors every cq-* HITL: cq-1 separate Deployment+Service, cq-2 per-agent session metadata, cq-3 PipelineConfig + repositories.yaml, cq-4 no in-pipeline validation, cq-5 keep Claude Code harness, cq-6 hosted Qwen, cq-7 gateway holds master key, cq-8 fail-closed, cq-9 uniform tool-strip, cq-11 leave opus[1m]. Surfaces every runtime primitive with file:line evidence and explicit purpose / execution-context labels per #2594 (gateway pod vs orchestrator pod vs in-sandbox-agent). Surfaces seven open risks (R1-R7) for the risk_analyst and seeds nine acceptance criteria for the task_planner. + +````yaml +id: 1ed8d22d-503c-47 +phase: plan +metadata: + payload: + summary: "Architecture analysis for non-Claude models via LiteLLM proxy. Recommends\ + \ Option A (gateway-side UpstreamRegistry keyed by per-agent session metadata;\ + \ LiteLLM as sibling Deployment in egg-system) over options B (LiteLLM-fronts-everything,\ + \ rejected \u2014 violates no-regression-on-Claude), C (route on body, rejected\ + \ \u2014 conflicts with compaction-math mitigation), and D (egg_agent SDK path,\ + \ rejected \u2014 cq-5 forbids). Decomposes work into two dependent slices:\ + \ slice-1 ships the gateway router + LiteLLM topology as a no-op by default;\ + \ slice-2 adds PipelineConfig.agent_models + repositories.yaml default_model\ + \ + resolve_model_for_role() helper that drives BOTH the consensus_wrapper --model\ + \ flag (recognized Claude alias, keeps compaction math sane) AND the gateway's\ + \ session.agent_upstream/agent_model_litellm fields (decides actual backend).\ + \ Honors every cq-* HITL: cq-1 separate Deployment+Service, cq-2 per-agent session\ + \ metadata, cq-3 PipelineConfig + repositories.yaml, cq-4 no in-pipeline validation,\ + \ cq-5 keep Claude Code harness, cq-6 hosted Qwen, cq-7 gateway holds master\ + \ key, cq-8 fail-closed, cq-9 uniform tool-strip, cq-11 leave opus[1m]. Surfaces\ + \ every runtime primitive with file:line evidence and explicit purpose / execution-context\ + \ labels per #2594 (gateway pod vs orchestrator pod vs in-sandbox-agent). Surfaces\ + \ seven open risks (R1-R7) for the risk_analyst and seeds nine acceptance criteria\ + \ for the task_planner." + attestation: {} + artifacts: + - .egg-state/agent-outputs/2769-architect-output.json + risk_considered: "No-regression-on-Claude is preserved by making the Anthropic\ + \ upstream byte-for-byte identical under the new UpstreamRegistry (same base_url,\ + \ timeouts, limits, credential injection, SSE accumulator, retry policy, tool-strip).\ + \ Inert-by-default: without an opt-in via PipelineConfig.agent_models or repositories.yaml\ + \ default_model, no LiteLLM request ever fires. Drift between Claude Code's\ + \ --model and gateway's session.agent_model_litellm is the primary residual\ + \ risk (R3) \u2014 mitigated by making resolve_model_for_role the SINGLE function\ + \ both callers consume, with an end-to-end test asserting the same tuple flows\ + \ to both sites. Supply-chain risk (R4, refine feedback Q3) mitigated by pinning\ + \ the LiteLLM image to a digest in k8s manifest and by the UpstreamRegistry\ + \ seam being one-entry-swap-out. Backward-compat across rolling updates (R7)\ + \ handled by defaulting agent_upstream=None to the anthropic upstream." + commit_sha: 851ef4393 + files_changed: + - .egg-state/agent-outputs/2769-architect-output.json + tests_run: [] + tasks_satisfied: [] + version: 1 + commit_sha: 851ef4393 +```` + +### [2026-05-22T03:29:15Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 3bc16a6d-9f0a-42 +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:30:09Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 70448dcc-070a-4e +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:09.076660+00:00' +```` + +### [2026-05-22T03:30:11Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: c248c1f7-3ac5-49 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:11.830823+00:00' +```` + +### [2026-05-22T03:30:39Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 67d7eaf4-8872-45 +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:31:12Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 1d548e27-40c4-44 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:11.830823+00:00' +```` + +### [2026-05-22T03:32:12Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 44b52bf9-ca0b-41 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:11.830823+00:00' +```` + +### [2026-05-22T03:33:12Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 7dec4b0d-6925-48 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:11.830823+00:00' +```` + +### [2026-05-22T03:34:12Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: f6650463-d51a-48 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:11.830823+00:00' +```` + +### [2026-05-22T03:34:20Z] task_planner (CONSENSUS_PROPOSE): Proposal from task_planner + +Plan for #2769 (non-Claude models via LiteLLM proxy). Two dependent slices per cq-10: slice-1 lands the gateway UpstreamRegistry seam + Session.upstream/upstream_model + upstream-aware credential injection + LiteLLM k8s manifests with empty model_list (Claude path byte-identical, no-op by default); slice-2 adds PipelineConfig.agent_models + repositories.yaml default_agent_model + a precedence-aware resolver + body rewrite that keeps Claude Code seeing the recognised alias while LiteLLM sees the real upstream model name (cq-5 mitigation). 21 tasks across coder/tester/documenter roles. cq-4 empirical agent flip explicitly out of scope. Primitives table cites every existing symbol the plan touches with current file:line and marks the two NEW primitives (UpstreamRegistry, PipelineConfig.agent_models) against their creating tasks. + +````yaml +id: 81904061-5005-4c +phase: plan +metadata: + payload: + summary: 'Plan for #2769 (non-Claude models via LiteLLM proxy). Two dependent + slices per cq-10: slice-1 lands the gateway UpstreamRegistry seam + Session.upstream/upstream_model + + upstream-aware credential injection + LiteLLM k8s manifests with empty model_list + (Claude path byte-identical, no-op by default); slice-2 adds PipelineConfig.agent_models + + repositories.yaml default_agent_model + a precedence-aware resolver + body + rewrite that keeps Claude Code seeing the recognised alias while LiteLLM sees + the real upstream model name (cq-5 mitigation). 21 tasks across coder/tester/documenter + roles. cq-4 empirical agent flip explicitly out of scope. Primitives table cites + every existing symbol the plan touches with current file:line and marks the + two NEW primitives (UpstreamRegistry, PipelineConfig.agent_models) against their + creating tasks.' + attestation: {} + artifacts: + - .egg-state/drafts/2769-plan.md + risk_considered: "R1 gateway file-size discipline (#2261) \u2014 new symbols land\ + \ in gateway/upstream_registry.py and the body-rewrite helper sits next to _filter_blocked_tools;\ + \ net delta to gateway.py is small. R2 Session-field back-compat \u2014 Session.from_persistence\ + \ already tolerates absent fields; new upstream defaults to \"anthropic\". R3\ + \ credential drift \u2014 registry pairs client and resolver together so the\ + \ wrong credential cannot reach the wrong upstream. R4 LiteLLM supply-chain\ + \ blast radius \u2014 confined by topology (separate Deployment, not sidecar)\ + \ per cq-1 and by UpstreamRegistry seam per feedback Q3. R5 Claude Code compaction\ + \ math \u2014 cq-5 mitigation: Claude Code always sees a recognised Claude alias\ + \ (default opus) for LiteLLM-routed agents; body rewrite happens gateway-side\ + \ after _filter_blocked_tools." + commit_sha: 5ef34fba2e6a51549b80245f7a977720dddcc1bd + files_changed: + - .egg-state/drafts/2769-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-1-10 + - TASK-1-11 + - TASK-1-12 + - TASK-2-1 + - TASK-2-2 + - TASK-2-3 + - TASK-2-4 + - TASK-2-5 + - TASK-2-6 + - TASK-2-7 + - TASK-2-8 + - TASK-2-9 + version: 1 + commit_sha: 5ef34fba2e6a51549b80245f7a977720dddcc1bd +```` + +### [2026-05-22T03:34:41Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: f196876c-371b-42 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:35:12Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: ea2b2e4d-b5cd-49 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:11.830823+00:00' +```` + +### [2026-05-22T03:35:41Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: b869de68-c93f-42 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:36:21Z] reviewer_plan → architect (CONSENSUS_NACK): NACK from reviewer_plan for architect + + +I read the full architect JSON (`.egg-state/agent-outputs/2769-architect-output.json`), spot-grepped every cited primitive (all 25 exist at the cited file:line — see audit table below), and cross-checked the design against the HITL resolutions and feedback answers. The structural shape is right and the no-op-by-default framing is well-defended. **One blocking architectural gap and a small bundle of non-blocking items follow.** + +### Primitive-Existence Audit (#2594) — all PASS + +| primitive | grep | result | +|-----------|------|--------| +| `_anthropic_client` / `get_anthropic_client()` | `grep -n 'def get_anthropic_client' gateway/gateway.py` | `gateway/gateway.py:9320` ✓ | +| `proxy_anthropic_messages` POST `/v1/messages` | `grep -n 'def proxy_anthropic_messages' gateway/gateway.py` | `gateway/gateway.py:9752` ✓ | +| `proxy_count_tokens` POST `/v1/messages/count_tokens` | `grep -n 'def proxy_count_tokens' gateway/gateway.py` | `gateway/gateway.py:10019` ✓ | +| `_inject_anthropic_credentials` | `grep -n '_inject_anthropic_credentials' gateway/gateway.py` | `gateway/gateway.py:9355` ✓ | +| `_filter_blocked_tools` | `grep -n 'def _filter_blocked_tools' gateway/gateway.py` | `gateway/gateway.py:9410` ✓ | +| `_SSEAccumulator` | `grep -n 'class _SSEAccumulator' gateway/gateway.py` | `gateway/gateway.py:9552` ✓ | +| `get_session_by_ip` | `grep -n 'def get_session_by_ip' gateway/session_manager.py` | `gateway/session_manager.py:741` ✓ | +| `Session` dataclass / `agent_role` field | `grep -n '@dataclass\\|agent_role' gateway/session_manager.py` | `gateway/session_manager.py:288, agent_role at :314` ✓ | +| `AnthropicCredential` (header_name / header_value) | `grep -n 'class AnthropicCredential' gateway/anthropic_credentials.py` | `gateway/anthropic_credentials.py:36–49` ✓ | +| `register_session()` | `grep -n 'def register_session' gateway/session_manager.py` | `gateway/session_manager.py:548` ✓ | +| `POST /api/v1/sessions/create` | `grep -n '/api/v1/sessions/create' gateway/gateway.py` | `gateway/gateway.py:8507` ✓ | +| `build_consensus_wrapped_command(model='opus')` | `grep -n 'def build_consensus_wrapped_command' orchestrator/consensus_wrapper.py` | `orchestrator/consensus_wrapper.py:620, default model='opus' at :622, '--model' arg at :654` ✓ | +| Call sites with NO model arg | `grep -n 'build_consensus_wrapped_command' orchestrator/concurrent_executor.py orchestrator/routes/pipelines.py` | `concurrent_executor.py:454, routes/pipelines.py:2704` ✓ | +| `PipelineConfig.overseer_decision_maker_model` / `overseer_advisor_model` | `grep -n 'overseer_decision_maker_model\\|overseer_advisor_model' orchestrator/models.py` | `orchestrator/models.py:546, :620` ✓ | +| `_PROTECTED_ENV_KEYS` | `grep -n '_PROTECTED_ENV_KEYS' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:138` ✓ | +| `ANTHROPIC_BASE_URL = GATEWAY_K8S_URL` injection | `grep -n 'ANTHROPIC_BASE_URL' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:807; GATEWAY_K8S_URL at :124` ✓ | +| `setup_anthropic_api()` | `grep -n 'def setup_anthropic_api' sandbox/entrypoint.py` | `sandbox/entrypoint.py:737` ✓ | +| `DEFAULT_MODEL = 'opus[1m]'` | `grep -n 'DEFAULT_MODEL' shared/egg_agent/client.py` | `shared/egg_agent/client.py:62` ✓ | +| `--model` default `opus[1m]` | `grep -n '\"--model\"' shared/egg_agent/__main__.py` | `shared/egg_agent/__main__.py:35` ✓ | +| `['--model','opus[1m]']` in legacy runner | `grep -n 'opus\\[1m\\]' sandbox/llm/runner.py` | `sandbox/llm/runner.py:49` ✓ | +| `allowed_domains.txt` Anthropic excluded | `grep -n 'anthropic' gateway/allowed_domains.txt` | `gateway/allowed_domains.txt:9–17` ✓ (explicitly comments rationale) | +| `k8s/base/gateway-deployment.yaml` `/secrets` mount | inspected file | mount at `:142–155`, Secret `gateway-secrets` ✓ | +| `config/repo_config.py` `get_repo_setting` | `grep -n 'def get_repo_setting' config/repo_config.py` | `config/repo_config.py:248` ✓ | +| `config/repositories.yaml` (live file the helper reads) | `ls config/repositories.yaml*` | **`config/repositories.yaml.example` only — live file is operator-supplied at runtime** (see non-blocker #4 below) | + +All NEW primitives the architect introduces (`UpstreamRegistry`, `LiteLLMCredentialsManager`, `Session.agent_upstream`, `Session.agent_model_litellm`, `k8s/base/litellm-deployment.yaml`, `k8s/base/litellm-service.yaml`, `LITELLM_MASTER_KEY` secret entry, `PipelineConfig.agent_models`, `resolve_model_for_role`, `config/repo_config.get_default_model`, `gateway/tests/test_upstream_router.py`, `orchestrator/tests/test_model_resolver.py`) are unambiguously net-new and the existing slice-1/slice-2 components create them — no false-NACK on missing-grep evidence per the §9 exception rule. + +### Trust-Boundary Audit (§10) — PASS + +All proposed tests live under `gateway/tests/` and `orchestrator/tests/`, both unit-test execution contexts (`make test` / pytest from the trusted-CI-runner). The architect explicitly avoids any test that needs a live LiteLLM endpoint (`httpx.MockTransport`-driven). No `integration_tests/` work is proposed that would hit the in-sandbox-agent vs trusted-CI-runner fixture trap. No `@require_lifecycle_secret` route invocations from in-sandbox-agent contexts. No `ScriptedProvider` references. The cq-4 "no in-pipeline validation" boundary is honored. + +### Slice-Sizing Advisory (#2137 opt-2 advisory only — non-blocking) + +- **slice-1** (gateway router + LiteLLM topology, 7 components + 2 test files): rough estimate ~600–900 LOC. **Well within the 1,000 LOC soft target — no advisory.** +- **slice-2** (per-agent model config + plumbing, 6 components + 3 test files): rough estimate ~400–700 LOC. **Well within the 1,000 LOC soft target — no advisory.** + +### Blocking + +1. **The design records `Session.agent_model_litellm` but does not specify how the gateway consumes it on a LiteLLM-bound request — the request body's `model` field is left unaltered.** This is a load-bearing ambiguity that fundamentally changes the operator-facing contract and the test surface. + + Concrete evidence of the gap, from the architect JSON: + - Slice-1 component **"Upstream-aware credential injection"** (`gateway/gateway.py:9355`) mutates headers only. + - Slice-1 component **"Session-driven upstream resolver"** returns an upstream name only. + - Slice-1 component **"Session-storage extensions"** stores `agent_model_litellm: str|None (e.g. 'qwen-2.5-coder' — the model alias LiteLLM's model_list will map)`. + - Slice-1 AC-2 only verifies "request lands at the litellm client (verify base_url and Authorization: Bearer )" — *not* what the body's `model` field looks like on the wire. + - Slice-2 AC-6 only asserts the *recorded session metadata*, not what the gateway forwards to LiteLLM. + - The compaction-math mitigation (the issue's primary risk per analysis lines 49–59) requires Claude Code to *see* a recognized Claude alias in its `--model` flag — so the body's `model` field is `opus` (or whatever recognized alias `resolve_model_for_role` returns as the first tuple element). + + The architect needs to commit to one of two semantics for the LiteLLM-bound proxy path. Both are buildable; the design implications are very different: + + **Semantics A — "body is dispatch, agent_model_litellm is metadata":** + - Gateway forwards the request body byte-unchanged. LiteLLM dispatches on `body["model"]`. + - Operator MUST configure LiteLLM's `model_list` with the Claude alias as the dispatch key, e.g. `model_list: [{model_name: "opus", litellm_params: {model: "openrouter/qwen/qwen3-coder", ...}}]`. The string `"opus"` in the LiteLLM config does NOT mean Opus; it is the alias the operator's gateway sends. + - `Session.agent_model_litellm` is operational metadata (logging, observability, future cost-tracking #2769-followup) and is not used at request time. + - Acceptance test must verify the request body forwarded to LiteLLM has `model="opus"` byte-for-byte. + - Operator docs must call out the "LiteLLM model_list keys are Claude aliases" constraint loud and early — any operator who configures `model_name: "qwen3-coder"` in `model_list` will see immediate breakage. + + **Semantics B — "gateway rewrites body.model before forwarding":** + - A NEW component (currently missing from slice-1) sits between the credential injection and `client.send(http_req, stream=True)` at `gateway/gateway.py:9827` and, when `upstream='litellm'`, rewrites `body["model"]` from the Claude alias to `session.agent_model_litellm`. + - Operator configures LiteLLM's `model_list` with the natural model names: `model_list: [{model_name: "qwen3-coder", litellm_params: {...}}]`. + - `Session.agent_model_litellm` is functional and required at request time. + - Acceptance test must verify the request body is mutated (`body["model"]` changes from `"opus"` to `"qwen3-coder"`) before reaching LiteLLM. + - Same rewrite is required for `proxy_count_tokens` so the token count returned to Claude Code matches the model that actually runs. + + **Fix:** Re-propose with one semantics chosen explicitly. If A, add a short "LiteLLM model_list operator constraint" sub-section under `key_constraints_carried_from_refine` and re-label `agent_model_litellm` as "operational metadata" in the Session-storage component. If B, add a slice-1 component "Request-body model rewriter for LiteLLM-bound requests" (`gateway/gateway.py`, between credential injection and `client.send`), bump AC-2 to assert body mutation, and add an equivalent assertion for `proxy_count_tokens`. Either path satisfies the refine HITL resolutions; both are buildable; the planner cannot convert components to tasks until this is pinned. + + Why this is blocking now (not a planner-detail): the choice changes (a) what test fixtures slice-1 needs, (b) whether the LiteLLM Deployment manifest needs a sample `model_list` that demonstrates the alias-as-key pattern (Semantics A) vs the natural-key pattern (Semantics B), (c) whether `Session.agent_model_litellm` should even exist as a typed field (under A it could be a free-form `metadata` blob), and (d) the operator-facing documentation surface. Letting the planner pick produces incoherent task boundaries. + +### Non-blocking + +- **R8 (recommended addition to `open_risks_for_risk_analyst`)** — **`/v1/messages/count_tokens` semantics on the LiteLLM path with Semantics A.** Even after #1 is resolved, the count_tokens response from LiteLLM under Semantics A is *for the Claude alias Claude Code passed*, not for the actual backend. Claude Code uses this count to drive compaction. If the LiteLLM model_list maps `"opus" → qwen3-coder`, LiteLLM may compute tokens with the wrong tokenizer (Claude tokenizer vs Qwen tokenizer have different boundaries). Under Semantics B the rewrite ensures LiteLLM uses the right tokenizer. Worth surfacing for the risk_analyst to weigh. + +- **`config/repositories.yaml` is not in-repo today** — only `config/repositories.yaml.example` exists (operator drops the live file at runtime via the EGG_REPO_CONFIG / EGG_SECRETS_PATH mount). The slice-2 `get_default_model(repo)` helper must handle the missing-file case the same way `get_repo_setting` does today (defaults to None silently). Add an AC: "AC-10: When `config/repositories.yaml` is absent or contains no `default_model` for the queried repo, `get_default_model(repo)` returns `None` and the resolver falls back to the built-in `'opus'` default." Without this, dev environments without an opted-in repo config break loudly the moment slice-2 ships. + +- **`LITELLM_BASE_URL` env-var injection into the gateway pod is not in `key_files_changed`.** The architect mentions "base_url from env LITELLM_BASE_URL, default http://litellm.egg-system.svc.cluster.local:4000" but doesn't list `k8s/base/gateway-deployment.yaml` as edited for slice-1. The env var has to be declared on the gateway pod (under `env:` in the container spec) for the default to be overridable. Add `k8s/base/gateway-deployment.yaml` to `production_code_slice_1` with a one-liner noting "add `LITELLM_BASE_URL` env var (optional, defaults to in-cluster service DNS)". If the architect's intent is "no env var, hard-coded default", say so explicitly — but then the seam loses its swap-out flexibility for the refine-Q3 supply-chain mitigation. + +- **`PipelineConfig.agent_models: dict[str, str]` should be typed against the AgentRole enum.** A free-string key invites typos (`'reviewer-refine'` vs `'reviewer_refine'` vs `'refiner_review'`) that silently never resolve. The existing per-phase consensus-timeout pattern at `orchestrator/models.py:452–474` uses *separate fields* per phase, not a dict, precisely because Pydantic validation cannot easily restrict dict keys to an enum without a `field_validator`. Recommendation: either (a) typed as `dict[AgentRole, str]` with a Pydantic v2 `field_validator` that coerces and validates string keys, or (b) split into per-role explicit fields following the overseer-model precedent. Planner can pick, but the architect should call out that key-validation is required (not assumed). + +- **Cost-tracking interaction.** `max_llm_cost_per_hour` envelope at `orchestrator/overseer/self_monitor.py:30–130` assumes Anthropic pricing. Refine feedback Q4 deferred extension to a follow-up, which the architect correctly notes. But: what is the *behavior* when a LiteLLM-bound agent's tokens flow through the existing cost tracker today? Does it crash, no-op silently, or mis-price? Even an `# explicitly out of scope but documented behavior: cost-tracker will mis-price non-Anthropic tokens until follow-up #XXXX` note would prevent a downstream operator surprise. Add a one-line entry under `out_of_scope_explicitly` calling out the *observable behavior* (e.g. "cost tracker continues to apply Anthropic pricing to LiteLLM token counts; the tracked dollar figure will be incorrect for non-Claude agents until #XXXX lands — does NOT block the agent from running"). + +- **NetworkPolicy egress allowlist is left vague.** "Egress to allowlisted backend hosts only" — for the cq-6 hosted-Qwen-first first cut, *which hosts*? The architect should at least name an example (`api.together.xyz`, `api.fireworks.ai`, etc.) and clarify that the NetworkPolicy egress list is *operator-supplied per backend choice* (probably via a kustomize overlay), not hard-coded in `k8s/base/network-policies.yaml`. Otherwise a reader of the slice-1 PR will not know what to put in the egress block. + +- **`config/repo_config.py` helper barrel-decomposition cross-check.** The architect places the new `get_default_model(repo)` near `get_repo_setting` at `config/repo_config.py:248`. Sanity-check that this file isn't itself slated for in-flight decomposition (#2261) — a quick grep of `config/CLAUDE.md` if present, or a check in `orchestrator/CLAUDE.md`. Not blocking; just a sanity check the planner should make before placing the helper. + +- **Slice-1 SSE acceptance test (AC-2 part (e)) is good but should include a "client disconnect mid-stream" path against the LiteLLM upstream.** The existing pre-stream retry (`gateway/gateway.py:9850`) and mid-stream synthetic SSE error frame (`gateway/gateway.py:9920ish`) are exercised against `httpx.MockTransport` for both upstreams; explicitly include the client-disconnect mid-stream case to assert the `_SSEAccumulator` flushes the partial response identically. Marginal cost in the slice-1 test budget; high value because this is exactly the path that's hardest to debug post-deploy. + +### Summary + +Design is sound on the architectural axis the analysis prescribed (Option A with cq-1..11 resolutions respected) and the cited primitives all exist. The one blocking gap (#1) is the missing commitment on how `agent_model_litellm` reaches the wire — that single decision drives slice-1's test plan, the operator's LiteLLM config, the LiteLLM Deployment's sample manifest, and the documentation surface. Address #1 and re-propose; the non-blocking items can roll into the re-propose or be picked up by the planner. + + +````yaml +id: 46b66267-16ab-4f +phase: plan +metadata: + payload: + reason: "\nI read the full architect JSON (`.egg-state/agent-outputs/2769-architect-output.json`),\ + \ spot-grepped every cited primitive (all 25 exist at the cited file:line \u2014\ + \ see audit table below), and cross-checked the design against the HITL resolutions\ + \ and feedback answers. The structural shape is right and the no-op-by-default\ + \ framing is well-defended. **One blocking architectural gap and a small bundle\ + \ of non-blocking items follow.**\n\n### Primitive-Existence Audit (#2594) \u2014\ + \ all PASS\n\n| primitive | grep | result |\n|-----------|------|--------|\n\ + | `_anthropic_client` / `get_anthropic_client()` | `grep -n 'def get_anthropic_client'\ + \ gateway/gateway.py` | `gateway/gateway.py:9320` \u2713 |\n| `proxy_anthropic_messages`\ + \ POST `/v1/messages` | `grep -n 'def proxy_anthropic_messages' gateway/gateway.py`\ + \ | `gateway/gateway.py:9752` \u2713 |\n| `proxy_count_tokens` POST `/v1/messages/count_tokens`\ + \ | `grep -n 'def proxy_count_tokens' gateway/gateway.py` | `gateway/gateway.py:10019`\ + \ \u2713 |\n| `_inject_anthropic_credentials` | `grep -n '_inject_anthropic_credentials'\ + \ gateway/gateway.py` | `gateway/gateway.py:9355` \u2713 |\n| `_filter_blocked_tools`\ + \ | `grep -n 'def _filter_blocked_tools' gateway/gateway.py` | `gateway/gateway.py:9410`\ + \ \u2713 |\n| `_SSEAccumulator` | `grep -n 'class _SSEAccumulator' gateway/gateway.py`\ + \ | `gateway/gateway.py:9552` \u2713 |\n| `get_session_by_ip` | `grep -n 'def\ + \ get_session_by_ip' gateway/session_manager.py` | `gateway/session_manager.py:741`\ + \ \u2713 |\n| `Session` dataclass / `agent_role` field | `grep -n '@dataclass\\\ + \\|agent_role' gateway/session_manager.py` | `gateway/session_manager.py:288,\ + \ agent_role at :314` \u2713 |\n| `AnthropicCredential` (header_name / header_value)\ + \ | `grep -n 'class AnthropicCredential' gateway/anthropic_credentials.py` |\ + \ `gateway/anthropic_credentials.py:36\u201349` \u2713 |\n| `register_session()`\ + \ | `grep -n 'def register_session' gateway/session_manager.py` | `gateway/session_manager.py:548`\ + \ \u2713 |\n| `POST /api/v1/sessions/create` | `grep -n '/api/v1/sessions/create'\ + \ gateway/gateway.py` | `gateway/gateway.py:8507` \u2713 |\n| `build_consensus_wrapped_command(model='opus')`\ + \ | `grep -n 'def build_consensus_wrapped_command' orchestrator/consensus_wrapper.py`\ + \ | `orchestrator/consensus_wrapper.py:620, default model='opus' at :622, '--model'\ + \ arg at :654` \u2713 |\n| Call sites with NO model arg | `grep -n 'build_consensus_wrapped_command'\ + \ orchestrator/concurrent_executor.py orchestrator/routes/pipelines.py` | `concurrent_executor.py:454,\ + \ routes/pipelines.py:2704` \u2713 |\n| `PipelineConfig.overseer_decision_maker_model`\ + \ / `overseer_advisor_model` | `grep -n 'overseer_decision_maker_model\\\\|overseer_advisor_model'\ + \ orchestrator/models.py` | `orchestrator/models.py:546, :620` \u2713 |\n| `_PROTECTED_ENV_KEYS`\ + \ | `grep -n '_PROTECTED_ENV_KEYS' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:138`\ + \ \u2713 |\n| `ANTHROPIC_BASE_URL = GATEWAY_K8S_URL` injection | `grep -n 'ANTHROPIC_BASE_URL'\ + \ orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:807;\ + \ GATEWAY_K8S_URL at :124` \u2713 |\n| `setup_anthropic_api()` | `grep -n 'def\ + \ setup_anthropic_api' sandbox/entrypoint.py` | `sandbox/entrypoint.py:737`\ + \ \u2713 |\n| `DEFAULT_MODEL = 'opus[1m]'` | `grep -n 'DEFAULT_MODEL' shared/egg_agent/client.py`\ + \ | `shared/egg_agent/client.py:62` \u2713 |\n| `--model` default `opus[1m]`\ + \ | `grep -n '\\\"--model\\\"' shared/egg_agent/__main__.py` | `shared/egg_agent/__main__.py:35`\ + \ \u2713 |\n| `['--model','opus[1m]']` in legacy runner | `grep -n 'opus\\\\\ + [1m\\\\]' sandbox/llm/runner.py` | `sandbox/llm/runner.py:49` \u2713 |\n| `allowed_domains.txt`\ + \ Anthropic excluded | `grep -n 'anthropic' gateway/allowed_domains.txt` | `gateway/allowed_domains.txt:9\u2013\ + 17` \u2713 (explicitly comments rationale) |\n| `k8s/base/gateway-deployment.yaml`\ + \ `/secrets` mount | inspected file | mount at `:142\u2013155`, Secret `gateway-secrets`\ + \ \u2713 |\n| `config/repo_config.py` `get_repo_setting` | `grep -n 'def get_repo_setting'\ + \ config/repo_config.py` | `config/repo_config.py:248` \u2713 |\n| `config/repositories.yaml`\ + \ (live file the helper reads) | `ls config/repositories.yaml*` | **`config/repositories.yaml.example`\ + \ only \u2014 live file is operator-supplied at runtime** (see non-blocker #4\ + \ below) |\n\nAll NEW primitives the architect introduces (`UpstreamRegistry`,\ + \ `LiteLLMCredentialsManager`, `Session.agent_upstream`, `Session.agent_model_litellm`,\ + \ `k8s/base/litellm-deployment.yaml`, `k8s/base/litellm-service.yaml`, `LITELLM_MASTER_KEY`\ + \ secret entry, `PipelineConfig.agent_models`, `resolve_model_for_role`, `config/repo_config.get_default_model`,\ + \ `gateway/tests/test_upstream_router.py`, `orchestrator/tests/test_model_resolver.py`)\ + \ are unambiguously net-new and the existing slice-1/slice-2 components create\ + \ them \u2014 no false-NACK on missing-grep evidence per the \xA79 exception\ + \ rule.\n\n### Trust-Boundary Audit (\xA710) \u2014 PASS\n\nAll proposed tests\ + \ live under `gateway/tests/` and `orchestrator/tests/`, both unit-test execution\ + \ contexts (`make test` / pytest from the trusted-CI-runner). The architect\ + \ explicitly avoids any test that needs a live LiteLLM endpoint (`httpx.MockTransport`-driven).\ + \ No `integration_tests/` work is proposed that would hit the in-sandbox-agent\ + \ vs trusted-CI-runner fixture trap. No `@require_lifecycle_secret` route invocations\ + \ from in-sandbox-agent contexts. No `ScriptedProvider` references. The cq-4\ + \ \"no in-pipeline validation\" boundary is honored.\n\n### Slice-Sizing Advisory\ + \ (#2137 opt-2 advisory only \u2014 non-blocking)\n\n- **slice-1** (gateway\ + \ router + LiteLLM topology, 7 components + 2 test files): rough estimate ~600\u2013\ + 900 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n- **slice-2**\ + \ (per-agent model config + plumbing, 6 components + 3 test files): rough estimate\ + \ ~400\u2013700 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n\ + \n### Blocking\n\n1. **The design records `Session.agent_model_litellm` but\ + \ does not specify how the gateway consumes it on a LiteLLM-bound request \u2014\ + \ the request body's `model` field is left unaltered.** This is a load-bearing\ + \ ambiguity that fundamentally changes the operator-facing contract and the\ + \ test surface.\n\n Concrete evidence of the gap, from the architect JSON:\n\ + \ - Slice-1 component **\"Upstream-aware credential injection\"** (`gateway/gateway.py:9355`)\ + \ mutates headers only.\n - Slice-1 component **\"Session-driven upstream\ + \ resolver\"** returns an upstream name only.\n - Slice-1 component **\"Session-storage\ + \ extensions\"** stores `agent_model_litellm: str|None (e.g. 'qwen-2.5-coder'\ + \ \u2014 the model alias LiteLLM's model_list will map)`.\n - Slice-1 AC-2\ + \ only verifies \"request lands at the litellm client (verify base_url and Authorization:\ + \ Bearer )\" \u2014 *not* what the body's `model` field looks like\ + \ on the wire.\n - Slice-2 AC-6 only asserts the *recorded session metadata*,\ + \ not what the gateway forwards to LiteLLM.\n - The compaction-math mitigation\ + \ (the issue's primary risk per analysis lines 49\u201359) requires Claude Code\ + \ to *see* a recognized Claude alias in its `--model` flag \u2014 so the body's\ + \ `model` field is `opus` (or whatever recognized alias `resolve_model_for_role`\ + \ returns as the first tuple element).\n\n The architect needs to commit to\ + \ one of two semantics for the LiteLLM-bound proxy path. Both are buildable;\ + \ the design implications are very different:\n\n **Semantics A \u2014 \"\ + body is dispatch, agent_model_litellm is metadata\":**\n - Gateway forwards\ + \ the request body byte-unchanged. LiteLLM dispatches on `body[\"model\"]`.\n\ + \ - Operator MUST configure LiteLLM's `model_list` with the Claude alias as\ + \ the dispatch key, e.g. `model_list: [{model_name: \"opus\", litellm_params:\ + \ {model: \"openrouter/qwen/qwen3-coder\", ...}}]`. The string `\"opus\"` in\ + \ the LiteLLM config does NOT mean Opus; it is the alias the operator's gateway\ + \ sends.\n - `Session.agent_model_litellm` is operational metadata (logging,\ + \ observability, future cost-tracking #2769-followup) and is not used at request\ + \ time.\n - Acceptance test must verify the request body forwarded to LiteLLM\ + \ has `model=\"opus\"` byte-for-byte.\n - Operator docs must call out the\ + \ \"LiteLLM model_list keys are Claude aliases\" constraint loud and early \u2014\ + \ any operator who configures `model_name: \"qwen3-coder\"` in `model_list`\ + \ will see immediate breakage.\n\n **Semantics B \u2014 \"gateway rewrites\ + \ body.model before forwarding\":**\n - A NEW component (currently missing\ + \ from slice-1) sits between the credential injection and `client.send(http_req,\ + \ stream=True)` at `gateway/gateway.py:9827` and, when `upstream='litellm'`,\ + \ rewrites `body[\"model\"]` from the Claude alias to `session.agent_model_litellm`.\n\ + \ - Operator configures LiteLLM's `model_list` with the natural model names:\ + \ `model_list: [{model_name: \"qwen3-coder\", litellm_params: {...}}]`.\n \ + \ - `Session.agent_model_litellm` is functional and required at request time.\n\ + \ - Acceptance test must verify the request body is mutated (`body[\"model\"\ + ]` changes from `\"opus\"` to `\"qwen3-coder\"`) before reaching LiteLLM.\n\ + \ - Same rewrite is required for `proxy_count_tokens` so the token count returned\ + \ to Claude Code matches the model that actually runs.\n\n **Fix:** Re-propose\ + \ with one semantics chosen explicitly. If A, add a short \"LiteLLM model_list\ + \ operator constraint\" sub-section under `key_constraints_carried_from_refine`\ + \ and re-label `agent_model_litellm` as \"operational metadata\" in the Session-storage\ + \ component. If B, add a slice-1 component \"Request-body model rewriter for\ + \ LiteLLM-bound requests\" (`gateway/gateway.py`, between credential injection\ + \ and `client.send`), bump AC-2 to assert body mutation, and add an equivalent\ + \ assertion for `proxy_count_tokens`. Either path satisfies the refine HITL\ + \ resolutions; both are buildable; the planner cannot convert components to\ + \ tasks until this is pinned.\n\n Why this is blocking now (not a planner-detail):\ + \ the choice changes (a) what test fixtures slice-1 needs, (b) whether the LiteLLM\ + \ Deployment manifest needs a sample `model_list` that demonstrates the alias-as-key\ + \ pattern (Semantics A) vs the natural-key pattern (Semantics B), (c) whether\ + \ `Session.agent_model_litellm` should even exist as a typed field (under A\ + \ it could be a free-form `metadata` blob), and (d) the operator-facing documentation\ + \ surface. Letting the planner pick produces incoherent task boundaries.\n\n\ + ### Non-blocking\n\n- **R8 (recommended addition to `open_risks_for_risk_analyst`)**\ + \ \u2014 **`/v1/messages/count_tokens` semantics on the LiteLLM path with Semantics\ + \ A.** Even after #1 is resolved, the count_tokens response from LiteLLM under\ + \ Semantics A is *for the Claude alias Claude Code passed*, not for the actual\ + \ backend. Claude Code uses this count to drive compaction. If the LiteLLM model_list\ + \ maps `\"opus\" \u2192 qwen3-coder`, LiteLLM may compute tokens with the wrong\ + \ tokenizer (Claude tokenizer vs Qwen tokenizer have different boundaries).\ + \ Under Semantics B the rewrite ensures LiteLLM uses the right tokenizer. Worth\ + \ surfacing for the risk_analyst to weigh.\n\n- **`config/repositories.yaml`\ + \ is not in-repo today** \u2014 only `config/repositories.yaml.example` exists\ + \ (operator drops the live file at runtime via the EGG_REPO_CONFIG / EGG_SECRETS_PATH\ + \ mount). The slice-2 `get_default_model(repo)` helper must handle the missing-file\ + \ case the same way `get_repo_setting` does today (defaults to None silently).\ + \ Add an AC: \"AC-10: When `config/repositories.yaml` is absent or contains\ + \ no `default_model` for the queried repo, `get_default_model(repo)` returns\ + \ `None` and the resolver falls back to the built-in `'opus'` default.\" Without\ + \ this, dev environments without an opted-in repo config break loudly the moment\ + \ slice-2 ships.\n\n- **`LITELLM_BASE_URL` env-var injection into the gateway\ + \ pod is not in `key_files_changed`.** The architect mentions \"base_url from\ + \ env LITELLM_BASE_URL, default http://litellm.egg-system.svc.cluster.local:4000\"\ + \ but doesn't list `k8s/base/gateway-deployment.yaml` as edited for slice-1.\ + \ The env var has to be declared on the gateway pod (under `env:` in the container\ + \ spec) for the default to be overridable. Add `k8s/base/gateway-deployment.yaml`\ + \ to `production_code_slice_1` with a one-liner noting \"add `LITELLM_BASE_URL`\ + \ env var (optional, defaults to in-cluster service DNS)\". If the architect's\ + \ intent is \"no env var, hard-coded default\", say so explicitly \u2014 but\ + \ then the seam loses its swap-out flexibility for the refine-Q3 supply-chain\ + \ mitigation.\n\n- **`PipelineConfig.agent_models: dict[str, str]` should be\ + \ typed against the AgentRole enum.** A free-string key invites typos (`'reviewer-refine'`\ + \ vs `'reviewer_refine'` vs `'refiner_review'`) that silently never resolve.\ + \ The existing per-phase consensus-timeout pattern at `orchestrator/models.py:452\u2013\ + 474` uses *separate fields* per phase, not a dict, precisely because Pydantic\ + \ validation cannot easily restrict dict keys to an enum without a `field_validator`.\ + \ Recommendation: either (a) typed as `dict[AgentRole, str]` with a Pydantic\ + \ v2 `field_validator` that coerces and validates string keys, or (b) split\ + \ into per-role explicit fields following the overseer-model precedent. Planner\ + \ can pick, but the architect should call out that key-validation is required\ + \ (not assumed).\n\n- **Cost-tracking interaction.** `max_llm_cost_per_hour`\ + \ envelope at `orchestrator/overseer/self_monitor.py:30\u2013130` assumes Anthropic\ + \ pricing. Refine feedback Q4 deferred extension to a follow-up, which the architect\ + \ correctly notes. But: what is the *behavior* when a LiteLLM-bound agent's\ + \ tokens flow through the existing cost tracker today? Does it crash, no-op\ + \ silently, or mis-price? Even an `# explicitly out of scope but documented\ + \ behavior: cost-tracker will mis-price non-Anthropic tokens until follow-up\ + \ #XXXX` note would prevent a downstream operator surprise. Add a one-line entry\ + \ under `out_of_scope_explicitly` calling out the *observable behavior* (e.g.\ + \ \"cost tracker continues to apply Anthropic pricing to LiteLLM token counts;\ + \ the tracked dollar figure will be incorrect for non-Claude agents until #XXXX\ + \ lands \u2014 does NOT block the agent from running\").\n\n- **NetworkPolicy\ + \ egress allowlist is left vague.** \"Egress to allowlisted backend hosts only\"\ + \ \u2014 for the cq-6 hosted-Qwen-first first cut, *which hosts*? The architect\ + \ should at least name an example (`api.together.xyz`, `api.fireworks.ai`, etc.)\ + \ and clarify that the NetworkPolicy egress list is *operator-supplied per backend\ + \ choice* (probably via a kustomize overlay), not hard-coded in `k8s/base/network-policies.yaml`.\ + \ Otherwise a reader of the slice-1 PR will not know what to put in the egress\ + \ block.\n\n- **`config/repo_config.py` helper barrel-decomposition cross-check.**\ + \ The architect places the new `get_default_model(repo)` near `get_repo_setting`\ + \ at `config/repo_config.py:248`. Sanity-check that this file isn't itself slated\ + \ for in-flight decomposition (#2261) \u2014 a quick grep of `config/CLAUDE.md`\ + \ if present, or a check in `orchestrator/CLAUDE.md`. Not blocking; just a sanity\ + \ check the planner should make before placing the helper.\n\n- **Slice-1 SSE\ + \ acceptance test (AC-2 part (e)) is good but should include a \"client disconnect\ + \ mid-stream\" path against the LiteLLM upstream.** The existing pre-stream\ + \ retry (`gateway/gateway.py:9850`) and mid-stream synthetic SSE error frame\ + \ (`gateway/gateway.py:9920ish`) are exercised against `httpx.MockTransport`\ + \ for both upstreams; explicitly include the client-disconnect mid-stream case\ + \ to assert the `_SSEAccumulator` flushes the partial response identically.\ + \ Marginal cost in the slice-1 test budget; high value because this is exactly\ + \ the path that's hardest to debug post-deploy.\n\n### Summary\n\nDesign is\ + \ sound on the architectural axis the analysis prescribed (Option A with cq-1..11\ + \ resolutions respected) and the cited primitives all exist. The one blocking\ + \ gap (#1) is the missing commitment on how `agent_model_litellm` reaches the\ + \ wire \u2014 that single decision drives slice-1's test plan, the operator's\ + \ LiteLLM config, the LiteLLM Deployment's sample manifest, and the documentation\ + \ surface. Address #1 and re-propose; the non-blocking items can roll into the\ + \ re-propose or be picked up by the planner.\n" + artifact_references: + - .egg-state/agent-outputs/2769-architect-output.json + - .egg-state/drafts/2769-analysis.md + - gateway/gateway.py + - gateway/session_manager.py + - gateway/anthropic_credentials.py + - gateway/allowed_domains.txt + - orchestrator/consensus_wrapper.py + - orchestrator/concurrent_executor.py + - orchestrator/routes/pipelines.py + - orchestrator/models.py + - orchestrator/kubernetes_spawner.py + - k8s/base/gateway-deployment.yaml + - config/repositories.yaml.example + - shared/egg_restrictions/patterns.py + nack_version: 1 + reason: "\nI read the full architect JSON (`.egg-state/agent-outputs/2769-architect-output.json`),\ + \ spot-grepped every cited primitive (all 25 exist at the cited file:line \u2014\ + \ see audit table below), and cross-checked the design against the HITL resolutions\ + \ and feedback answers. The structural shape is right and the no-op-by-default\ + \ framing is well-defended. **One blocking architectural gap and a small bundle\ + \ of non-blocking items follow.**\n\n### Primitive-Existence Audit (#2594) \u2014\ + \ all PASS\n\n| primitive | grep | result |\n|-----------|------|--------|\n|\ + \ `_anthropic_client` / `get_anthropic_client()` | `grep -n 'def get_anthropic_client'\ + \ gateway/gateway.py` | `gateway/gateway.py:9320` \u2713 |\n| `proxy_anthropic_messages`\ + \ POST `/v1/messages` | `grep -n 'def proxy_anthropic_messages' gateway/gateway.py`\ + \ | `gateway/gateway.py:9752` \u2713 |\n| `proxy_count_tokens` POST `/v1/messages/count_tokens`\ + \ | `grep -n 'def proxy_count_tokens' gateway/gateway.py` | `gateway/gateway.py:10019`\ + \ \u2713 |\n| `_inject_anthropic_credentials` | `grep -n '_inject_anthropic_credentials'\ + \ gateway/gateway.py` | `gateway/gateway.py:9355` \u2713 |\n| `_filter_blocked_tools`\ + \ | `grep -n 'def _filter_blocked_tools' gateway/gateway.py` | `gateway/gateway.py:9410`\ + \ \u2713 |\n| `_SSEAccumulator` | `grep -n 'class _SSEAccumulator' gateway/gateway.py`\ + \ | `gateway/gateway.py:9552` \u2713 |\n| `get_session_by_ip` | `grep -n 'def\ + \ get_session_by_ip' gateway/session_manager.py` | `gateway/session_manager.py:741`\ + \ \u2713 |\n| `Session` dataclass / `agent_role` field | `grep -n '@dataclass\\\ + \\|agent_role' gateway/session_manager.py` | `gateway/session_manager.py:288,\ + \ agent_role at :314` \u2713 |\n| `AnthropicCredential` (header_name / header_value)\ + \ | `grep -n 'class AnthropicCredential' gateway/anthropic_credentials.py` | `gateway/anthropic_credentials.py:36\u2013\ + 49` \u2713 |\n| `register_session()` | `grep -n 'def register_session' gateway/session_manager.py`\ + \ | `gateway/session_manager.py:548` \u2713 |\n| `POST /api/v1/sessions/create`\ + \ | `grep -n '/api/v1/sessions/create' gateway/gateway.py` | `gateway/gateway.py:8507`\ + \ \u2713 |\n| `build_consensus_wrapped_command(model='opus')` | `grep -n 'def\ + \ build_consensus_wrapped_command' orchestrator/consensus_wrapper.py` | `orchestrator/consensus_wrapper.py:620,\ + \ default model='opus' at :622, '--model' arg at :654` \u2713 |\n| Call sites\ + \ with NO model arg | `grep -n 'build_consensus_wrapped_command' orchestrator/concurrent_executor.py\ + \ orchestrator/routes/pipelines.py` | `concurrent_executor.py:454, routes/pipelines.py:2704`\ + \ \u2713 |\n| `PipelineConfig.overseer_decision_maker_model` / `overseer_advisor_model`\ + \ | `grep -n 'overseer_decision_maker_model\\\\|overseer_advisor_model' orchestrator/models.py`\ + \ | `orchestrator/models.py:546, :620` \u2713 |\n| `_PROTECTED_ENV_KEYS` | `grep\ + \ -n '_PROTECTED_ENV_KEYS' orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:138`\ + \ \u2713 |\n| `ANTHROPIC_BASE_URL = GATEWAY_K8S_URL` injection | `grep -n 'ANTHROPIC_BASE_URL'\ + \ orchestrator/kubernetes_spawner.py` | `orchestrator/kubernetes_spawner.py:807;\ + \ GATEWAY_K8S_URL at :124` \u2713 |\n| `setup_anthropic_api()` | `grep -n 'def\ + \ setup_anthropic_api' sandbox/entrypoint.py` | `sandbox/entrypoint.py:737` \u2713\ + \ |\n| `DEFAULT_MODEL = 'opus[1m]'` | `grep -n 'DEFAULT_MODEL' shared/egg_agent/client.py`\ + \ | `shared/egg_agent/client.py:62` \u2713 |\n| `--model` default `opus[1m]` |\ + \ `grep -n '\\\"--model\\\"' shared/egg_agent/__main__.py` | `shared/egg_agent/__main__.py:35`\ + \ \u2713 |\n| `['--model','opus[1m]']` in legacy runner | `grep -n 'opus\\\\[1m\\\ + \\]' sandbox/llm/runner.py` | `sandbox/llm/runner.py:49` \u2713 |\n| `allowed_domains.txt`\ + \ Anthropic excluded | `grep -n 'anthropic' gateway/allowed_domains.txt` | `gateway/allowed_domains.txt:9\u2013\ + 17` \u2713 (explicitly comments rationale) |\n| `k8s/base/gateway-deployment.yaml`\ + \ `/secrets` mount | inspected file | mount at `:142\u2013155`, Secret `gateway-secrets`\ + \ \u2713 |\n| `config/repo_config.py` `get_repo_setting` | `grep -n 'def get_repo_setting'\ + \ config/repo_config.py` | `config/repo_config.py:248` \u2713 |\n| `config/repositories.yaml`\ + \ (live file the helper reads) | `ls config/repositories.yaml*` | **`config/repositories.yaml.example`\ + \ only \u2014 live file is operator-supplied at runtime** (see non-blocker #4\ + \ below) |\n\nAll NEW primitives the architect introduces (`UpstreamRegistry`,\ + \ `LiteLLMCredentialsManager`, `Session.agent_upstream`, `Session.agent_model_litellm`,\ + \ `k8s/base/litellm-deployment.yaml`, `k8s/base/litellm-service.yaml`, `LITELLM_MASTER_KEY`\ + \ secret entry, `PipelineConfig.agent_models`, `resolve_model_for_role`, `config/repo_config.get_default_model`,\ + \ `gateway/tests/test_upstream_router.py`, `orchestrator/tests/test_model_resolver.py`)\ + \ are unambiguously net-new and the existing slice-1/slice-2 components create\ + \ them \u2014 no false-NACK on missing-grep evidence per the \xA79 exception rule.\n\ + \n### Trust-Boundary Audit (\xA710) \u2014 PASS\n\nAll proposed tests live under\ + \ `gateway/tests/` and `orchestrator/tests/`, both unit-test execution contexts\ + \ (`make test` / pytest from the trusted-CI-runner). The architect explicitly\ + \ avoids any test that needs a live LiteLLM endpoint (`httpx.MockTransport`-driven).\ + \ No `integration_tests/` work is proposed that would hit the in-sandbox-agent\ + \ vs trusted-CI-runner fixture trap. No `@require_lifecycle_secret` route invocations\ + \ from in-sandbox-agent contexts. No `ScriptedProvider` references. The cq-4 \"\ + no in-pipeline validation\" boundary is honored.\n\n### Slice-Sizing Advisory\ + \ (#2137 opt-2 advisory only \u2014 non-blocking)\n\n- **slice-1** (gateway router\ + \ + LiteLLM topology, 7 components + 2 test files): rough estimate ~600\u2013\ + 900 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n- **slice-2**\ + \ (per-agent model config + plumbing, 6 components + 3 test files): rough estimate\ + \ ~400\u2013700 LOC. **Well within the 1,000 LOC soft target \u2014 no advisory.**\n\ + \n### Blocking\n\n1. **The design records `Session.agent_model_litellm` but does\ + \ not specify how the gateway consumes it on a LiteLLM-bound request \u2014 the\ + \ request body's `model` field is left unaltered.** This is a load-bearing ambiguity\ + \ that fundamentally changes the operator-facing contract and the test surface.\n\ + \n Concrete evidence of the gap, from the architect JSON:\n - Slice-1 component\ + \ **\"Upstream-aware credential injection\"** (`gateway/gateway.py:9355`) mutates\ + \ headers only.\n - Slice-1 component **\"Session-driven upstream resolver\"\ + ** returns an upstream name only.\n - Slice-1 component **\"Session-storage\ + \ extensions\"** stores `agent_model_litellm: str|None (e.g. 'qwen-2.5-coder'\ + \ \u2014 the model alias LiteLLM's model_list will map)`.\n - Slice-1 AC-2 only\ + \ verifies \"request lands at the litellm client (verify base_url and Authorization:\ + \ Bearer )\" \u2014 *not* what the body's `model` field looks like\ + \ on the wire.\n - Slice-2 AC-6 only asserts the *recorded session metadata*,\ + \ not what the gateway forwards to LiteLLM.\n - The compaction-math mitigation\ + \ (the issue's primary risk per analysis lines 49\u201359) requires Claude Code\ + \ to *see* a recognized Claude alias in its `--model` flag \u2014 so the body's\ + \ `model` field is `opus` (or whatever recognized alias `resolve_model_for_role`\ + \ returns as the first tuple element).\n\n The architect needs to commit to\ + \ one of two semantics for the LiteLLM-bound proxy path. Both are buildable; the\ + \ design implications are very different:\n\n **Semantics A \u2014 \"body is\ + \ dispatch, agent_model_litellm is metadata\":**\n - Gateway forwards the request\ + \ body byte-unchanged. LiteLLM dispatches on `body[\"model\"]`.\n - Operator\ + \ MUST configure LiteLLM's `model_list` with the Claude alias as the dispatch\ + \ key, e.g. `model_list: [{model_name: \"opus\", litellm_params: {model: \"openrouter/qwen/qwen3-coder\"\ + , ...}}]`. The string `\"opus\"` in the LiteLLM config does NOT mean Opus; it\ + \ is the alias the operator's gateway sends.\n - `Session.agent_model_litellm`\ + \ is operational metadata (logging, observability, future cost-tracking #2769-followup)\ + \ and is not used at request time.\n - Acceptance test must verify the request\ + \ body forwarded to LiteLLM has `model=\"opus\"` byte-for-byte.\n - Operator\ + \ docs must call out the \"LiteLLM model_list keys are Claude aliases\" constraint\ + \ loud and early \u2014 any operator who configures `model_name: \"qwen3-coder\"\ + ` in `model_list` will see immediate breakage.\n\n **Semantics B \u2014 \"gateway\ + \ rewrites body.model before forwarding\":**\n - A NEW component (currently\ + \ missing from slice-1) sits between the credential injection and `client.send(http_req,\ + \ stream=True)` at `gateway/gateway.py:9827` and, when `upstream='litellm'`, rewrites\ + \ `body[\"model\"]` from the Claude alias to `session.agent_model_litellm`.\n\ + \ - Operator configures LiteLLM's `model_list` with the natural model names:\ + \ `model_list: [{model_name: \"qwen3-coder\", litellm_params: {...}}]`.\n -\ + \ `Session.agent_model_litellm` is functional and required at request time.\n\ + \ - Acceptance test must verify the request body is mutated (`body[\"model\"\ + ]` changes from `\"opus\"` to `\"qwen3-coder\"`) before reaching LiteLLM.\n \ + \ - Same rewrite is required for `proxy_count_tokens` so the token count returned\ + \ to Claude Code matches the model that actually runs.\n\n **Fix:** Re-propose\ + \ with one semantics chosen explicitly. If A, add a short \"LiteLLM model_list\ + \ operator constraint\" sub-section under `key_constraints_carried_from_refine`\ + \ and re-label `agent_model_litellm` as \"operational metadata\" in the Session-storage\ + \ component. If B, add a slice-1 component \"Request-body model rewriter for LiteLLM-bound\ + \ requests\" (`gateway/gateway.py`, between credential injection and `client.send`),\ + \ bump AC-2 to assert body mutation, and add an equivalent assertion for `proxy_count_tokens`.\ + \ Either path satisfies the refine HITL resolutions; both are buildable; the planner\ + \ cannot convert components to tasks until this is pinned.\n\n Why this is blocking\ + \ now (not a planner-detail): the choice changes (a) what test fixtures slice-1\ + \ needs, (b) whether the LiteLLM Deployment manifest needs a sample `model_list`\ + \ that demonstrates the alias-as-key pattern (Semantics A) vs the natural-key\ + \ pattern (Semantics B), (c) whether `Session.agent_model_litellm` should even\ + \ exist as a typed field (under A it could be a free-form `metadata` blob), and\ + \ (d) the operator-facing documentation surface. Letting the planner pick produces\ + \ incoherent task boundaries.\n\n### Non-blocking\n\n- **R8 (recommended addition\ + \ to `open_risks_for_risk_analyst`)** \u2014 **`/v1/messages/count_tokens` semantics\ + \ on the LiteLLM path with Semantics A.** Even after #1 is resolved, the count_tokens\ + \ response from LiteLLM under Semantics A is *for the Claude alias Claude Code\ + \ passed*, not for the actual backend. Claude Code uses this count to drive compaction.\ + \ If the LiteLLM model_list maps `\"opus\" \u2192 qwen3-coder`, LiteLLM may compute\ + \ tokens with the wrong tokenizer (Claude tokenizer vs Qwen tokenizer have different\ + \ boundaries). Under Semantics B the rewrite ensures LiteLLM uses the right tokenizer.\ + \ Worth surfacing for the risk_analyst to weigh.\n\n- **`config/repositories.yaml`\ + \ is not in-repo today** \u2014 only `config/repositories.yaml.example` exists\ + \ (operator drops the live file at runtime via the EGG_REPO_CONFIG / EGG_SECRETS_PATH\ + \ mount). The slice-2 `get_default_model(repo)` helper must handle the missing-file\ + \ case the same way `get_repo_setting` does today (defaults to None silently).\ + \ Add an AC: \"AC-10: When `config/repositories.yaml` is absent or contains no\ + \ `default_model` for the queried repo, `get_default_model(repo)` returns `None`\ + \ and the resolver falls back to the built-in `'opus'` default.\" Without this,\ + \ dev environments without an opted-in repo config break loudly the moment slice-2\ + \ ships.\n\n- **`LITELLM_BASE_URL` env-var injection into the gateway pod is not\ + \ in `key_files_changed`.** The architect mentions \"base_url from env LITELLM_BASE_URL,\ + \ default http://litellm.egg-system.svc.cluster.local:4000\" but doesn't list\ + \ `k8s/base/gateway-deployment.yaml` as edited for slice-1. The env var has to\ + \ be declared on the gateway pod (under `env:` in the container spec) for the\ + \ default to be overridable. Add `k8s/base/gateway-deployment.yaml` to `production_code_slice_1`\ + \ with a one-liner noting \"add `LITELLM_BASE_URL` env var (optional, defaults\ + \ to in-cluster service DNS)\". If the architect's intent is \"no env var, hard-coded\ + \ default\", say so explicitly \u2014 but then the seam loses its swap-out flexibility\ + \ for the refine-Q3 supply-chain mitigation.\n\n- **`PipelineConfig.agent_models:\ + \ dict[str, str]` should be typed against the AgentRole enum.** A free-string\ + \ key invites typos (`'reviewer-refine'` vs `'reviewer_refine'` vs `'refiner_review'`)\ + \ that silently never resolve. The existing per-phase consensus-timeout pattern\ + \ at `orchestrator/models.py:452\u2013474` uses *separate fields* per phase, not\ + \ a dict, precisely because Pydantic validation cannot easily restrict dict keys\ + \ to an enum without a `field_validator`. Recommendation: either (a) typed as\ + \ `dict[AgentRole, str]` with a Pydantic v2 `field_validator` that coerces and\ + \ validates string keys, or (b) split into per-role explicit fields following\ + \ the overseer-model precedent. Planner can pick, but the architect should call\ + \ out that key-validation is required (not assumed).\n\n- **Cost-tracking interaction.**\ + \ `max_llm_cost_per_hour` envelope at `orchestrator/overseer/self_monitor.py:30\u2013\ + 130` assumes Anthropic pricing. Refine feedback Q4 deferred extension to a follow-up,\ + \ which the architect correctly notes. But: what is the *behavior* when a LiteLLM-bound\ + \ agent's tokens flow through the existing cost tracker today? Does it crash,\ + \ no-op silently, or mis-price? Even an `# explicitly out of scope but documented\ + \ behavior: cost-tracker will mis-price non-Anthropic tokens until follow-up #XXXX`\ + \ note would prevent a downstream operator surprise. Add a one-line entry under\ + \ `out_of_scope_explicitly` calling out the *observable behavior* (e.g. \"cost\ + \ tracker continues to apply Anthropic pricing to LiteLLM token counts; the tracked\ + \ dollar figure will be incorrect for non-Claude agents until #XXXX lands \u2014\ + \ does NOT block the agent from running\").\n\n- **NetworkPolicy egress allowlist\ + \ is left vague.** \"Egress to allowlisted backend hosts only\" \u2014 for the\ + \ cq-6 hosted-Qwen-first first cut, *which hosts*? The architect should at least\ + \ name an example (`api.together.xyz`, `api.fireworks.ai`, etc.) and clarify that\ + \ the NetworkPolicy egress list is *operator-supplied per backend choice* (probably\ + \ via a kustomize overlay), not hard-coded in `k8s/base/network-policies.yaml`.\ + \ Otherwise a reader of the slice-1 PR will not know what to put in the egress\ + \ block.\n\n- **`config/repo_config.py` helper barrel-decomposition cross-check.**\ + \ The architect places the new `get_default_model(repo)` near `get_repo_setting`\ + \ at `config/repo_config.py:248`. Sanity-check that this file isn't itself slated\ + \ for in-flight decomposition (#2261) \u2014 a quick grep of `config/CLAUDE.md`\ + \ if present, or a check in `orchestrator/CLAUDE.md`. Not blocking; just a sanity\ + \ check the planner should make before placing the helper.\n\n- **Slice-1 SSE\ + \ acceptance test (AC-2 part (e)) is good but should include a \"client disconnect\ + \ mid-stream\" path against the LiteLLM upstream.** The existing pre-stream retry\ + \ (`gateway/gateway.py:9850`) and mid-stream synthetic SSE error frame (`gateway/gateway.py:9920ish`)\ + \ are exercised against `httpx.MockTransport` for both upstreams; explicitly include\ + \ the client-disconnect mid-stream case to assert the `_SSEAccumulator` flushes\ + \ the partial response identically. Marginal cost in the slice-1 test budget;\ + \ high value because this is exactly the path that's hardest to debug post-deploy.\n\ + \n### Summary\n\nDesign is sound on the architectural axis the analysis prescribed\ + \ (Option A with cq-1..11 resolutions respected) and the cited primitives all\ + \ exist. The one blocking gap (#1) is the missing commitment on how `agent_model_litellm`\ + \ reaches the wire \u2014 that single decision drives slice-1's test plan, the\ + \ operator's LiteLLM config, the LiteLLM Deployment's sample manifest, and the\ + \ documentation surface. Address #1 and re-propose; the non-blocking items can\ + \ roll into the re-propose or be picked up by the planner.\n" + revision_count: 1 +```` + +### [2026-05-22T03:36:21Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 770e7849-652e-4e +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:30:11.830823+00:00' +```` + +### [2026-05-22T03:36:21Z] architect (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 6cc5495c-d4d8-45 +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:36:37Z] risk_analyst (CONSENSUS_PROPOSE): Proposal from risk_analyst + +Risk assessment for #2769 LiteLLM proxy integration. 15 risks recorded with severity, likelihood, impact, mitigation, residual risk, and human-review flag. Anchored on concrete external research: LiteLLM March 2026 PyPI supply-chain incident (TeamPCP backdoored 1.82.7/1.82.8) and April-May 2026 CVE cluster (CVE-2026-42208 CVSS 9.3 SQLi, CVE-2026-35029 RCE, CVE-2026-42271 subprocess spawn), all fixed in v1.83.7+; LiteLLM /v1/messages streaming SSE tool_use input_json_delta drop bugs against non-Anthropic backends (open issues #25561 vertex_ai/gemini, #25321 v1.82.x regression, #24765 GitHub Copilot path) — the exact configuration egg picked in cq-5 (Claude Code + LiteLLM + non-Anthropic); Claude Code auto-compaction heuristic on alias name + recognised model window; Qwen3 vLLM streaming/reasoning parser bugs (deferred per cq-6 hosted-provider-first). Audits 12 runtime primitives per #2594 (session.upstream/.model_alias missing, register_session() missing fields, PipelineConfig.agent_models missing, build_consensus_wrapped_command callers don't pass model arg, LiteLLM Deployment + LITELLM_MASTER_KEY missing, AnthropicCredential extensibility, SSE accumulator hardcoded Anthropic event names, max_llm_cost_per_hour breaks silently on LiteLLM path, ANTHROPIC_BASE_URL not in _PROTECTED_ENV_KEYS, max_turns hardcoded). Documents 5 trust-boundary surfaces (sandbox->gateway unchanged; gateway->LiteLLM and LiteLLM->hosted-provider are new). Rollback plan: unset PipelineConfig.agent_models[role] -> structural fall-back to Claude singleton (no-op by default per cq-4 deferred validation). Recommendation: PROCEED_WITH_MITIGATIONS. Overall risk HIGH driven by external dependencies (LiteLLM, Claude Code heuristic, hosted provider), not by egg-side code change. Three risks flagged requires_human_review (R1 supply chain controls, R2 LiteLLM streaming tool_use bug, R13 deferred validation residual). + +````yaml +id: 90c8605e-8c84-4f +phase: plan +metadata: + payload: + summary: "Risk assessment for #2769 LiteLLM proxy integration. 15 risks recorded\ + \ with severity, likelihood, impact, mitigation, residual risk, and human-review\ + \ flag. Anchored on concrete external research: LiteLLM March 2026 PyPI supply-chain\ + \ incident (TeamPCP backdoored 1.82.7/1.82.8) and April-May 2026 CVE cluster\ + \ (CVE-2026-42208 CVSS 9.3 SQLi, CVE-2026-35029 RCE, CVE-2026-42271 subprocess\ + \ spawn), all fixed in v1.83.7+; LiteLLM /v1/messages streaming SSE tool_use\ + \ input_json_delta drop bugs against non-Anthropic backends (open issues #25561\ + \ vertex_ai/gemini, #25321 v1.82.x regression, #24765 GitHub Copilot path) \u2014\ + \ the exact configuration egg picked in cq-5 (Claude Code + LiteLLM + non-Anthropic);\ + \ Claude Code auto-compaction heuristic on alias name + recognised model window;\ + \ Qwen3 vLLM streaming/reasoning parser bugs (deferred per cq-6 hosted-provider-first).\ + \ Audits 12 runtime primitives per #2594 (session.upstream/.model_alias missing,\ + \ register_session() missing fields, PipelineConfig.agent_models missing, build_consensus_wrapped_command\ + \ callers don't pass model arg, LiteLLM Deployment + LITELLM_MASTER_KEY missing,\ + \ AnthropicCredential extensibility, SSE accumulator hardcoded Anthropic event\ + \ names, max_llm_cost_per_hour breaks silently on LiteLLM path, ANTHROPIC_BASE_URL\ + \ not in _PROTECTED_ENV_KEYS, max_turns hardcoded). Documents 5 trust-boundary\ + \ surfaces (sandbox->gateway unchanged; gateway->LiteLLM and LiteLLM->hosted-provider\ + \ are new). Rollback plan: unset PipelineConfig.agent_models[role] -> structural\ + \ fall-back to Claude singleton (no-op by default per cq-4 deferred validation).\ + \ Recommendation: PROCEED_WITH_MITIGATIONS. Overall risk HIGH driven by external\ + \ dependencies (LiteLLM, Claude Code heuristic, hosted provider), not by egg-side\ + \ code change. Three risks flagged requires_human_review (R1 supply chain controls,\ + \ R2 LiteLLM streaming tool_use bug, R13 deferred validation residual)." + attestation: {} + artifacts: + - .egg-state/agent-outputs/2769-risk_analyst-output.json + risk_considered: All risks documented in the artifact itself; this proposal is + the risk assessment. See R1 (CRITICAL supply chain), R2 (CRITICAL LiteLLM SSE + bug), R3 (HIGH compaction math), R6-R7 (runtime-primitive gaps), R13 (deferred + validation residual). + commit_sha: dfe5ffb0f04cd0094021fd4bb005592352cf896d + files_changed: + - .egg-state/agent-outputs/2769-risk_analyst-output.json + tests_run: [] + tasks_satisfied: [] + version: 1 + commit_sha: dfe5ffb0f04cd0094021fd4bb005592352cf896d +```` + +### [2026-05-22T03:36:37Z] 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: 4408dba3-db1c-49 +phase: plan +metadata: + ready_to_confirm: true + version: 1 +```` + +### [2026-05-22T03:36:37Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: 6ec1d121-f2d4-4b +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:36:37.446421+00:00' +```` + +### [2026-05-22T03:36:58Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 225a0bf2-38c7-42 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:37:03Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: a65b6d33-2731-4d +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:37:03.057171+00:00' +```` + +### [2026-05-22T03:37:37Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: c93be0bb-3060-4f +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:36:37.446421+00:00' +```` + +### [2026-05-22T03:37:56Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: af788b75-df7d-4d +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:38:03Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 3da702f8-1f0c-44 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:37:03.057171+00:00' +```` + +### [2026-05-22T03:38:37Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: 5f3f143a-68c0-4a +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:36:37.446421+00:00' +```` + +### [2026-05-22T03:38:56Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 3148eed6-4811-40 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:39:03Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 9f186746-0b56-4d +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:37:03.057171+00:00' +```` + +### [2026-05-22T03:39:37Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: 364f79c3-aebc-48 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:36:37.446421+00:00' +```` + +### [2026-05-22T03:39:56Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 9a2d90ac-6417-42 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:40:03Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 65c0dcee-8f3c-44 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:37:03.057171+00:00' +```` + +### [2026-05-22T03:40:37Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: b7c4efc0-24fb-40 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:36:37.446421+00:00' +```` + +### [2026-05-22T03:40:56Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 0653dc14-ca88-4a +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:41:03Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 4f965b0c-91f6-49 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:37:03.057171+00:00' +```` + +### [2026-05-22T03:41:38Z] reviewer_plan (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: 0577b33a-645e-47 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:36:37.446421+00:00' +```` + +### [2026-05-22T03:41:56Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 5d7db74a-9ea9-47 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:41:57Z] orchestrator → risk_analyst (OVERSEER_ALERT): BRC confirmation timeout — call mcp__brc__confirm + +You are PROPOSED and fully ACKed but have not confirmed in 302s. 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: 57861637-0f63-46 +phase: plan +metadata: + alert_type: brc_confirmation_timeout + elapsed_seconds: 302 + source: health_monitor +```` + +### [2026-05-22T03:41:57Z] risk_analyst (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 8f86dabe-e2c1-4a +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:42:01Z] risk_analyst (CONSENSUS_CONFIRMED): Confirmed by risk_analyst + +````yaml +id: 4043b187-f996-44 +phase: plan +metadata: + consensus_reached: false +```` + +### [2026-05-22T03:42:01Z] reviewer_plan (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 12301449-fb9d-4b +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:42:06Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: d50d7e82-7eb1-41 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:42:56Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 5aed2ecd-5926-43 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:43:06Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 5407c11c-9eb8-42 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:43:56Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: fd890fe5-c7a8-44 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:44:06Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: d955d0a0-5ece-45 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:45:01Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 31f4d35f-8bbe-41 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:45:01Z] architect (CONSENSUS_PROPOSE): Proposal from architect + +v2 architecture analysis: addresses reviewer_plan NACK item #1 by pinning Semantics B (gateway rewrites body['model'] from the Claude alias to the natural LiteLLM model name BEFORE forwarding to LiteLLM, on both /v1/messages and /v1/messages/count_tokens), rejecting Semantics A (forward unchanged with LiteLLM model_list keyed on Claude aliases) because Semantics A breaks Claude Code's compaction math via tokenizer mismatch on count_tokens (new R8). Adds the body-rewriter component to slice-1 with exact insertion points (gateway/gateway.py between line 9767 credential injection and line 9778 tool-strip, plus the sibling site in proxy_count_tokens at line 10028). Bumps AC-2 to assert the body mutation on the wire, adds AC-10 (repositories.yaml absence handled by get_default_model returning None silently) and AC-11 (PipelineConfig.agent_models key-validation forbidden to be tolerant of typos). Addresses every non-blocking reviewer item: adds k8s/base/gateway-deployment.yaml LITELLM_BASE_URL env var declaration; flags PipelineConfig.agent_models key-validation as a hard design requirement; documents observable cost-tracker mis-pricing on LiteLLM-bound agents as out-of-scope-but-known; makes NetworkPolicy egress operator-supplied per backend via kustomize overlay; cross-checks config/repo_config.py is not in #2261 decomposition; expands the slice-1 SSE test with a client-disconnect mid-stream case against LiteLLM. The slice DAG is unchanged (slice-1 gateway no-op → slice-2 model config); component count grows by 2 and AC count grows from 9 to 11. + +**Adversarial re-review** + +**Your v2 review has TWO equal-weight mandates:** + +1. **Verify named v1 blockers were addressed** — confirm the producer fixed what you NACK'd. +2. **Audit the v2 delta as a fresh reviewer** — ignore your v1 NACK history. Read the v2 diff as if you'd never seen v1. Apply your lens (security threat-model, concurrency races, contract AC, line-by-line bugs, silent-fallback shapes — whichever your role owns) to the v2 delta itself, not to whether your previous concerns were satisfied. + +Both mandates have equal weight. If (1) passes but (2) finds new issues, you NACK. ACK requires both pass. + +**The named-blockers anchor is a known trap. Every reviewer lens has a mandate-2 in its own territory** — security has v2-introduced threat surfaces, concurrency has v2-introduced races, contract has v2-introduced AC drift, code has v2-introduced line-by-line bugs. The four issues that escaped PR #2724 to the GitHub bot were all of code-lens shape (`${ANSWER}` as bare Python, deprecated `datetime.utcnow()`, non-atomic write, bare `except: pass`) — the persistent reviewer correctly answered mandate 1 ("did v1 issues get fixed? yes") and skipped mandate 2 ("does v2 introduce new issues? actually yes"). The shape generalizes: whatever your lens, the v2 delta can introduce issues your prior NACK didn't name. Watching the producer deliver a targeted fix pulls strongly toward "verify my fix-request landed → ACK." Recognize the pull and do mandate 2 anyway. + +**How to execute mandate 2:** + +- Read each new hunk as an operator who's about to copy-paste / run / integrate it. Would this code execute as written? Would these docs send a copy-paster down a working path? +- Apply every rubric pass to the new hunks. New issues outside the scope of your prior NACK are blocking; your prior NACK does not bound this re-review. +- **Fresh-reviewer simulation.** Before issuing your v2 verdict, ask: would a reviewer who has only seen the v2 diff with no NACK history ACK this? If you can't argue yes from the v2 diff alone, NACK. +- **External-bot anchor.** Imagine `egg-reviewer[bot]` reads only your v2 diff with no NACK context. What would it flag? Anything it'd flag, you should NACK first. + +**Your v2 verdict must enumerate both halves** so mandate 2 doesn't silently disappear from the record: + +- (a) Which v1 blockers you verified-fixed (mandate 1). +- (b) What new issues you audited-and-did-not-find (mandate 2). Name the specific shapes you checked — not "reviewed thoroughly," but "checked for silent fallbacks, doc-snippet executability, API-deprecation, atomicity of file writes." If you can't enumerate (b), you haven't done mandate 2. + +**Re-reviews are cheap by design.** Your amortized context means the work is "read the delta, apply your rubric, decide" — minutes, not hours. NACK without hesitance; the orchestrator absorbs cycles. Two NACKs on the same producer where the second names new findings is the correct trajectory, not goalpost-moving. The downstream GitHub reviewer should find nothing in this delta. Anything it catches that lives in this cycle's diff is a miss attributable to this re-review. + +````yaml +id: f3b84924-fb74-4c +phase: plan +metadata: + payload: + summary: "v2 architecture analysis: addresses reviewer_plan NACK item #1 by pinning\ + \ Semantics B (gateway rewrites body['model'] from the Claude alias to the natural\ + \ LiteLLM model name BEFORE forwarding to LiteLLM, on both /v1/messages and\ + \ /v1/messages/count_tokens), rejecting Semantics A (forward unchanged with\ + \ LiteLLM model_list keyed on Claude aliases) because Semantics A breaks Claude\ + \ Code's compaction math via tokenizer mismatch on count_tokens (new R8). Adds\ + \ the body-rewriter component to slice-1 with exact insertion points (gateway/gateway.py\ + \ between line 9767 credential injection and line 9778 tool-strip, plus the\ + \ sibling site in proxy_count_tokens at line 10028). Bumps AC-2 to assert the\ + \ body mutation on the wire, adds AC-10 (repositories.yaml absence handled by\ + \ get_default_model returning None silently) and AC-11 (PipelineConfig.agent_models\ + \ key-validation forbidden to be tolerant of typos). Addresses every non-blocking\ + \ reviewer item: adds k8s/base/gateway-deployment.yaml LITELLM_BASE_URL env\ + \ var declaration; flags PipelineConfig.agent_models key-validation as a hard\ + \ design requirement; documents observable cost-tracker mis-pricing on LiteLLM-bound\ + \ agents as out-of-scope-but-known; makes NetworkPolicy egress operator-supplied\ + \ per backend via kustomize overlay; cross-checks config/repo_config.py is not\ + \ in #2261 decomposition; expands the slice-1 SSE test with a client-disconnect\ + \ mid-stream case against LiteLLM. The slice DAG is unchanged (slice-1 gateway\ + \ no-op \u2192 slice-2 model config); component count grows by 2 and AC count\ + \ grows from 9 to 11." + attestation: {} + artifacts: + - .egg-state/agent-outputs/2769-architect-output.json + risk_considered: "v2 pins Semantics B explicitly, eliminating the v1 ambiguity\ + \ reviewer_plan flagged as blocking. The chief residual risk (R3) \u2014 drift\ + \ between Claude Code's --model alias and the gateway's session.agent_model_litellm\ + \ \u2014 is mitigated by making resolve_model_for_role the SINGLE source of\ + \ truth both callers consume, with AC-6 asserting all three derived values (consensus_wrapper\ + \ --model, gateway session metadata, and the body-rewritten wire model) flow\ + \ from the same resolver call. The tokenizer-mismatch risk (R8) that motivated\ + \ rejecting Semantics A is eliminated under Semantics B because the rewrite\ + \ extends to /v1/messages/count_tokens, so LiteLLM tokenizes against the actual\ + \ backend model. The body rewriter only touches body['model']; every other field\ + \ (messages, tools, system, max_tokens) is preserved byte-for-byte, and on JSON-parse\ + \ failure the body is forwarded unchanged with a warning (defensive). The Anthropic\ + \ path is unchanged: the rewriter only fires on upstream=='litellm'." + commit_sha: fce400144 + files_changed: + - .egg-state/agent-outputs/2769-architect-output.json + tests_run: [] + tasks_satisfied: [] + version: 2 + commit_sha: fce400144 +```` + +### [2026-05-22T03:45:25Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 99270965-bc54-4e +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:45:25Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 127cc3b8-2429-49 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:45:12.237332+00:00' +```` + +### [2026-05-22T03:46:01Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 3b3a9b66-14c5-46 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:46:21Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: fdbee8bb-d9d9-46 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:46:25Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 604ea25d-f28a-48 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:45:12.237332+00:00' +```` + +### [2026-05-22T03:47:01Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: e4bae38a-fc65-41 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:47:22Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: c9d2521b-ea2c-49 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:47:25Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 89007796-3697-40 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:45:12.237332+00:00' +```` + +### [2026-05-22T03:48:01Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: eaf10202-2f80-46 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:48:22Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 5afedd7c-a59e-40 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:48:25Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 7fcaf4cf-56e4-43 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:45:12.237332+00:00' +```` + +### [2026-05-22T03:49:01Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 69943ab4-206f-47 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:49:22Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 7e6dc3b4-6c3e-4d +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:49:26Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: d8022c57-4953-48 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:45:12.237332+00:00' +```` + +### [2026-05-22T03:49:27Z] reviewer_plan → architect (CONSENSUS_ACK): ACK from reviewer_plan for architect + + +### v1 blocker verification (mandate #1) + +The v1 blocker was "design records `Session.agent_model_litellm` but does not specify how the gateway consumes it on a LiteLLM-bound request." v2 resolves it completely: + +- **`litellm_dispatch_semantics.choice = "Semantics B"`** is pinned explicitly in the recommended_approach (lines 25–37). The architect commits to "gateway rewrites body.model before forwarding to LiteLLM," not the ambiguous "metadata-only" reading I called out. +- **New slice-1 component "Body model rewriter for LiteLLM-bound requests"** (lines 72–77) names exact insertion points: `proxy_anthropic_messages` between `_inject_anthropic_credentials` at `gateway/gateway.py:9767` and `_filter_blocked_tools` at `:9778`; `proxy_count_tokens` between `_inject_anthropic_credentials` at `:10028` and `client.post` at `:10035`. JSON-parse failure mode is specified (log + leave unchanged + proceed — defensive). Ordering invariant (rewriter runs before tool-strip so the tool-strip sees the canonical rewritten body) is articulated explicitly in `key_constraints_carried_from_refine.tool_strip_unchanged_in_private_mode` (line 395). +- **AC-2 (slice-1) bumped** to assert body mutation on the wire for BOTH `/v1/messages` and `/v1/messages/count_tokens` (acceptance_criteria_seeds AC-2 part (b) and (c)). +- **R8 added** to `open_risks_for_risk_analyst` — the tokenizer-mismatch failure mode on count_tokens is the load-bearing technical reason Semantics A was rejected. This is the strongest possible justification for the choice and reads as honest engineering, not post-hoc rationalization. +- **`what_we_explicitly_rejected_semantics_a`** (lines 34–37) names the alternative and the rejection reasons, so future readers don't re-litigate. + +### Non-blocker verification (all addressed) + +- **`config/repositories.yaml` missing-file handling** → AC-10 added (acceptance_criteria_seeds line 457), with explicit "no exception, no error log" + "dev environments without an opted-in repo config continue to work as today" rationale. +- **`LITELLM_BASE_URL` env-var declaration** → new slice-1 component "Gateway pod LITELLM_BASE_URL env var declaration" (lines 103–107), with exact placement (`k8s/base/gateway-deployment.yaml` after the existing port-env block at lines 77–82). `k8s/base/gateway-deployment.yaml` now appears in `production_code_slice_1` (line 176). +- **`PipelineConfig.agent_models` key-validation** → "key-validation is required" flagged as non-negotiable in the component description (line 134); planner gets shape choice (`dict[AgentRole, str]` with Pydantic `field_validator` vs explicit per-role fields) but free-string `dict[str, str]` with no validator is forbidden. AC-11 added (line 458) for typo'd-role-key rejection. +- **Cost-tracker observable behavior** → surfaced in `out_of_scope_explicitly` (line 462) and the runtime_primitives `max_llm_cost_per_hour` plan_action (line 370): documents that the tracker continues to apply Anthropic pricing to LiteLLM tokens and the dollar figure will be wrong on the LiteLLM path — DOES NOT block the agent. +- **NetworkPolicy egress concreteness** → new slice-1 component "NetworkPolicy egress overlay" (lines 109–112) commits to k8s/base shipping ZERO default egress for the LiteLLM pod (defense-in-depth); operator supplies egress via a kustomize overlay; example hosts named (`api.together.xyz`, `api.fireworks.ai`, `openrouter.ai`, `api.deepinfra.com`); example overlay scaffold path noted (`k8s/overlays/example/`). AC-7 (line 454) asserts the zero-default-egress behavior. +- **`config/repo_config.py` decomposition cross-check** → done explicitly: "836 lines, not in any #2261 decomposition row (verified against scripts/file-size-allowlist.yaml and CLAUDE.md tables). Helper placement near get_repo_setting (line 248) is safe." Now baked into the runtime_primitives entry (line 378). +- **Slice-1 SSE test expansion (client-disconnect mid-stream against LiteLLM)** → AC-2 part (g) added (line 449) — explicitly exercises the `gateway/gateway.py:9909` mid-stream synthetic-error-frame branch against the LiteLLM upstream. + +### v2 delta audit (mandate #2 — fresh-reviewer pass) + +I read the v2 delta with no NACK history and looked for new issues. None blocking: + +- **Body-rewriter ordering (BEFORE tool-strip)**: rewriter touches only `body['model']`; `_filter_blocked_tools` touches only `body['tools']`. The orderings are commutative for correctness. The architect's explicit choice (rewriter first → tool-strip sees canonical body) is the cleaner invariant and is documented. OK. +- **JSON-parse failure handling for the rewriter**: leave unchanged + log warning + proceed. Under Semantics B with operator-configured natural-keys, an un-rewritten body with `model='opus'` will fail at LiteLLM with a "model not found" — i.e. fail-closed at the upstream layer rather than the gateway. Acceptable; LiteLLM's error reaches the agent and surfaces the misconfig. +- **`litellm_model_list_operator_pattern_semantics_b`** constraint (line 399) — new top-level invariant documenting the operator-facing config pattern. Clear and unambiguous. This is exactly the "operator UX commitment" the v1 ambiguity prevented. +- **R8 framing**: the tokenizer-mismatch argument depends on LiteLLM's tokenizer registry mapping `model_name → tokenizer`. Under Semantics A, LiteLLM would use the tokenizer associated with whichever model it dispatched to (which IS the real backend), not the alias — so the tokenizer-mismatch claim is slightly overstated *for the dispatch path*. BUT for the count_tokens path, the Anthropic-tokenizer-mapped count is a real concern, since count_tokens may be tokenizer-only without involving the dispatch path. Net: R8 is still a strong argument for Semantics B even if the framing slightly overgeneralizes. Non-blocking observation only. +- **Slice-1 LOC remains under 1000** with the added body-rewriter (~50 LOC) and env-var declaration (~3 LOC). Slice-2 is unchanged. Both well within the #2137 advisory threshold. +- **Primitive-existence audit**: All new primitives (body-rewriter, LITELLM_BASE_URL env var declaration, NetworkPolicy egress overlay) are clearly NEW. All cited existing primitives match the v1 audit I already verified. +- **Trust boundary**: Body-rewriter runs gateway-side in the gateway pod (same trust scope as `_filter_blocked_tools` and the existing credential injection). No sandbox-side credential or trust-boundary crossing introduced. + +### Non-blocking — for the planner / risk_analyst follow-through (not blocking the architect) + +- **Body-rewriter's downstream impact on the SSE accumulator transcript-capture path**: `_SSEAccumulator` captures the response, not the request. So the rewriter (which only touches the request) does not change anything downstream sees in the transcript. Worth a one-line note in the operator docs so future readers don't go looking. Non-blocking. + +- **R8's count_tokens tokenizer mapping nuance**: LiteLLM's `count_tokens` implementation may itself dispatch to the model_list to pick the tokenizer, in which case Semantics A's count would be correct *for the model_list-resolved backend*, NOT incorrect for the alias. The R8 framing is correct only if LiteLLM tokenizes against the alias verbatim. Either way Semantics B is the cleaner choice (no operator footgun), so the rejection of Semantics A stands; just worth being precise in the future docs. + +### Summary + +Architect v2 resolves the v1 blocker comprehensively and addresses every non-blocking item I raised, with new acceptance criteria (AC-10, AC-11), a new risk (R8), a new component (body-rewriter), and updated k8s/component placements. The Semantics-B pin is the right call for the same reasons (decouples Claude Code's compaction math from LiteLLM's dispatch, gives operators natural model_list keys). Reviewed the v2 delta as a fresh reviewer with no NACK history — no new blocking issues. ACK. + + +````yaml +id: 44d32a41-8b0e-44 +phase: plan +metadata: + payload: + artifact_references: + - .egg-state/agent-outputs/2769-architect-output.json + - gateway/gateway.py + - gateway/session_manager.py + - gateway/anthropic_credentials.py + - k8s/base/gateway-deployment.yaml + - config/repo_config.py + - orchestrator/models.py + reason: "\n### v1 blocker verification (mandate #1)\n\nThe v1 blocker was \"design\ + \ records `Session.agent_model_litellm` but does not specify how the gateway\ + \ consumes it on a LiteLLM-bound request.\" v2 resolves it completely:\n\n-\ + \ **`litellm_dispatch_semantics.choice = \"Semantics B\"`** is pinned explicitly\ + \ in the recommended_approach (lines 25\u201337). The architect commits to \"\ + gateway rewrites body.model before forwarding to LiteLLM,\" not the ambiguous\ + \ \"metadata-only\" reading I called out.\n- **New slice-1 component \"Body\ + \ model rewriter for LiteLLM-bound requests\"** (lines 72\u201377) names exact\ + \ insertion points: `proxy_anthropic_messages` between `_inject_anthropic_credentials`\ + \ at `gateway/gateway.py:9767` and `_filter_blocked_tools` at `:9778`; `proxy_count_tokens`\ + \ between `_inject_anthropic_credentials` at `:10028` and `client.post` at `:10035`.\ + \ JSON-parse failure mode is specified (log + leave unchanged + proceed \u2014\ + \ defensive). Ordering invariant (rewriter runs before tool-strip so the tool-strip\ + \ sees the canonical rewritten body) is articulated explicitly in `key_constraints_carried_from_refine.tool_strip_unchanged_in_private_mode`\ + \ (line 395).\n- **AC-2 (slice-1) bumped** to assert body mutation on the wire\ + \ for BOTH `/v1/messages` and `/v1/messages/count_tokens` (acceptance_criteria_seeds\ + \ AC-2 part (b) and (c)).\n- **R8 added** to `open_risks_for_risk_analyst` \u2014\ + \ the tokenizer-mismatch failure mode on count_tokens is the load-bearing technical\ + \ reason Semantics A was rejected. This is the strongest possible justification\ + \ for the choice and reads as honest engineering, not post-hoc rationalization.\n\ + - **`what_we_explicitly_rejected_semantics_a`** (lines 34\u201337) names the\ + \ alternative and the rejection reasons, so future readers don't re-litigate.\n\ + \n### Non-blocker verification (all addressed)\n\n- **`config/repositories.yaml`\ + \ missing-file handling** \u2192 AC-10 added (acceptance_criteria_seeds line\ + \ 457), with explicit \"no exception, no error log\" + \"dev environments without\ + \ an opted-in repo config continue to work as today\" rationale.\n- **`LITELLM_BASE_URL`\ + \ env-var declaration** \u2192 new slice-1 component \"Gateway pod LITELLM_BASE_URL\ + \ env var declaration\" (lines 103\u2013107), with exact placement (`k8s/base/gateway-deployment.yaml`\ + \ after the existing port-env block at lines 77\u201382). `k8s/base/gateway-deployment.yaml`\ + \ now appears in `production_code_slice_1` (line 176).\n- **`PipelineConfig.agent_models`\ + \ key-validation** \u2192 \"key-validation is required\" flagged as non-negotiable\ + \ in the component description (line 134); planner gets shape choice (`dict[AgentRole,\ + \ str]` with Pydantic `field_validator` vs explicit per-role fields) but free-string\ + \ `dict[str, str]` with no validator is forbidden. AC-11 added (line 458) for\ + \ typo'd-role-key rejection.\n- **Cost-tracker observable behavior** \u2192\ + \ surfaced in `out_of_scope_explicitly` (line 462) and the runtime_primitives\ + \ `max_llm_cost_per_hour` plan_action (line 370): documents that the tracker\ + \ continues to apply Anthropic pricing to LiteLLM tokens and the dollar figure\ + \ will be wrong on the LiteLLM path \u2014 DOES NOT block the agent.\n- **NetworkPolicy\ + \ egress concreteness** \u2192 new slice-1 component \"NetworkPolicy egress\ + \ overlay\" (lines 109\u2013112) commits to k8s/base shipping ZERO default egress\ + \ for the LiteLLM pod (defense-in-depth); operator supplies egress via a kustomize\ + \ overlay; example hosts named (`api.together.xyz`, `api.fireworks.ai`, `openrouter.ai`,\ + \ `api.deepinfra.com`); example overlay scaffold path noted (`k8s/overlays/example/`).\ + \ AC-7 (line 454) asserts the zero-default-egress behavior.\n- **`config/repo_config.py`\ + \ decomposition cross-check** \u2192 done explicitly: \"836 lines, not in any\ + \ #2261 decomposition row (verified against scripts/file-size-allowlist.yaml\ + \ and CLAUDE.md tables). Helper placement near get_repo_setting (line 248) is\ + \ safe.\" Now baked into the runtime_primitives entry (line 378).\n- **Slice-1\ + \ SSE test expansion (client-disconnect mid-stream against LiteLLM)** \u2192\ + \ AC-2 part (g) added (line 449) \u2014 explicitly exercises the `gateway/gateway.py:9909`\ + \ mid-stream synthetic-error-frame branch against the LiteLLM upstream.\n\n\ + ### v2 delta audit (mandate #2 \u2014 fresh-reviewer pass)\n\nI read the v2\ + \ delta with no NACK history and looked for new issues. None blocking:\n\n-\ + \ **Body-rewriter ordering (BEFORE tool-strip)**: rewriter touches only `body['model']`;\ + \ `_filter_blocked_tools` touches only `body['tools']`. The orderings are commutative\ + \ for correctness. The architect's explicit choice (rewriter first \u2192 tool-strip\ + \ sees canonical body) is the cleaner invariant and is documented. OK.\n- **JSON-parse\ + \ failure handling for the rewriter**: leave unchanged + log warning + proceed.\ + \ Under Semantics B with operator-configured natural-keys, an un-rewritten body\ + \ with `model='opus'` will fail at LiteLLM with a \"model not found\" \u2014\ + \ i.e. fail-closed at the upstream layer rather than the gateway. Acceptable;\ + \ LiteLLM's error reaches the agent and surfaces the misconfig.\n- **`litellm_model_list_operator_pattern_semantics_b`**\ + \ constraint (line 399) \u2014 new top-level invariant documenting the operator-facing\ + \ config pattern. Clear and unambiguous. This is exactly the \"operator UX commitment\"\ + \ the v1 ambiguity prevented.\n- **R8 framing**: the tokenizer-mismatch argument\ + \ depends on LiteLLM's tokenizer registry mapping `model_name \u2192 tokenizer`.\ + \ Under Semantics A, LiteLLM would use the tokenizer associated with whichever\ + \ model it dispatched to (which IS the real backend), not the alias \u2014 so\ + \ the tokenizer-mismatch claim is slightly overstated *for the dispatch path*.\ + \ BUT for the count_tokens path, the Anthropic-tokenizer-mapped count is a real\ + \ concern, since count_tokens may be tokenizer-only without involving the dispatch\ + \ path. Net: R8 is still a strong argument for Semantics B even if the framing\ + \ slightly overgeneralizes. Non-blocking observation only.\n- **Slice-1 LOC\ + \ remains under 1000** with the added body-rewriter (~50 LOC) and env-var declaration\ + \ (~3 LOC). Slice-2 is unchanged. Both well within the #2137 advisory threshold.\n\ + - **Primitive-existence audit**: All new primitives (body-rewriter, LITELLM_BASE_URL\ + \ env var declaration, NetworkPolicy egress overlay) are clearly NEW. All cited\ + \ existing primitives match the v1 audit I already verified.\n- **Trust boundary**:\ + \ Body-rewriter runs gateway-side in the gateway pod (same trust scope as `_filter_blocked_tools`\ + \ and the existing credential injection). No sandbox-side credential or trust-boundary\ + \ crossing introduced.\n\n### Non-blocking \u2014 for the planner / risk_analyst\ + \ follow-through (not blocking the architect)\n\n- **Body-rewriter's downstream\ + \ impact on the SSE accumulator transcript-capture path**: `_SSEAccumulator`\ + \ captures the response, not the request. So the rewriter (which only touches\ + \ the request) does not change anything downstream sees in the transcript. Worth\ + \ a one-line note in the operator docs so future readers don't go looking. Non-blocking.\n\ + \n- **R8's count_tokens tokenizer mapping nuance**: LiteLLM's `count_tokens`\ + \ implementation may itself dispatch to the model_list to pick the tokenizer,\ + \ in which case Semantics A's count would be correct *for the model_list-resolved\ + \ backend*, NOT incorrect for the alias. The R8 framing is correct only if LiteLLM\ + \ tokenizes against the alias verbatim. Either way Semantics B is the cleaner\ + \ choice (no operator footgun), so the rejection of Semantics A stands; just\ + \ worth being precise in the future docs.\n\n### Summary\n\nArchitect v2 resolves\ + \ the v1 blocker comprehensively and addresses every non-blocking item I raised,\ + \ with new acceptance criteria (AC-10, AC-11), a new risk (R8), a new component\ + \ (body-rewriter), and updated k8s/component placements. The Semantics-B pin\ + \ is the right call for the same reasons (decouples Claude Code's compaction\ + \ math from LiteLLM's dispatch, gives operators natural model_list keys). Reviewed\ + \ the v2 delta as a fresh reviewer with no NACK history \u2014 no new blocking\ + \ issues. ACK.\n" + ack_version: 2 + version: 2 +```` + +### [2026-05-22T03:49:27Z] orchestrator → architect (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: 11211b24-b13e-44 +phase: plan +metadata: + ready_to_confirm: true + version: 2 +```` + +### [2026-05-22T03:49:27Z] architect (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: ba988b3a-253e-45 +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:49:30Z] architect (CONSENSUS_CONFIRMED): Confirmed by architect + +````yaml +id: 181186ba-7063-4a +phase: plan +metadata: + consensus_reached: false +```` + +### [2026-05-22T03:49:33Z] task_planner (HEARTBEAT): heartbeat: WAITING_ON_ROLE + +````yaml +id: 4a66a564-0d6c-4a +phase: plan +metadata: + state: WAITING_ON_ROLE + waiting_on: reviewer_plan +```` + +### [2026-05-22T03:49:38Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 382e08c8-e0c2-4f +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:49:38.773629+00:00' +```` + +### [2026-05-22T03:50:01Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 93b2a4ca-54d8-44 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:34:26.073888+00:00' +```` + +### [2026-05-22T03:50:22Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 82b38ffb-f039-45 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:50:38Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 089e0100-e836-4a +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:49:38.773629+00:00' +```` + +### [2026-05-22T03:50:58Z] reviewer_plan → task_planner (CONSENSUS_ACK): ACK from reviewer_plan for task_planner + + +Reviewed the full `2769-plan.md` (1027 lines), ran the planner's role↔files validator (`OK - no violations`), spot-grepped every cited primitive, and cross-referenced against the architect v2 component breakdown and the risk_analyst's 15-risk artifact. The plan is internally consistent, picks **Semantics B** explicitly (TASK-2-6 adds `_rewrite_upstream_model`), and decomposes cleanly into two dependent slices that match the architect's recommended option-3 shape. + +### Primitive-Existence Audit (#2594) — all PASS + +| primitive | grep | result | +|-----------|------|--------| +| All gateway/orchestrator primitives cited in the Primitives tables (lines 56–116) | re-grepped against current HEAD | every cite resolves to the line number the plan lists ✓ | +| `AgentRole` enum at `shared/egg_contracts/agent_roles.py:46` (cited for TASK-2-1 validator) | `grep -n 'class AgentRole' shared/egg_contracts/agent_roles.py` | exists ✓ | +| `parse_env_file` at `gateway/anthropic_credentials.py:52` (cited for TASK-1-2) | verified | exists ✓ | +| `SECRETS_PATH` env var at `gateway/anthropic_credentials.py:31` (cited for TASK-1-2) | verified | exists ✓ | +| `load_repo_pattern_override` at `shared/egg_restrictions/patterns.py:854` (cited as pattern for TASK-2-2) | verified | exists ✓ | +| `get_repo_setting` at `config/repo_config.py:248` (cited for TASK-2-2) | verified | exists ✓ | +| `_get_forwarded_headers` / `_filter_response_headers` at gateway/gateway.py:9343 / :9348 | verified | exists ✓ | +| Existing test file paths (gateway/tests/test_session_manager.py, tests/gateway/test_anthropic_proxy.py, etc.) | `ls` | exist ✓ | +| `config/repositories.yaml.example` | exists; the live `config/repositories.yaml` is operator-supplied — TASK-2-2's helper inherits the missing-file-returns-None behavior from `get_repo_setting` | OK ✓ | + +All NEW primitives are clearly tagged with `(NEW — task TASK-X-Y)` per the §9 exception (e.g. line 77 `UpstreamRegistry`, line 97 `PipelineConfig.agent_models`), and the downstream task consuming each new primitive has an explicit dependency on the creating task. + +### Trust-Boundary Audit (§10) — PASS + +The plan's "Trust-boundary scope checks" sub-section (lines 118–141) is the cleanest such audit I've seen in a plan. Every gateway-side new primitive runs in the gateway pod; every orchestrator-side new primitive runs in the orchestrator pod (trusted-CI-runner); no in-sandbox-agent test fixture is added that would hit the parent-vs-sibling-conftest trap. All test files live under `tests/gateway/`, `gateway/tests/`, or `orchestrator/tests/` — all reachable from `make test` in the trusted-CI-runner. The plan explicitly defers live-LiteLLM end-to-end validation to the operator-driven smoke test (cq-4), so the in-sandbox-agent vs trusted-CI-runner mismatch never arises. + +### Role↔Files Alignment (§8) — PASS + +`python3 -c "from egg_contracts.plan_parser import parse_plan_file, validate_task_role_alignment as v; …"` returned `OK - no violations` against the current plan file. Every coder task targets files coder can write (gateway/, orchestrator/, config/, k8s/base/), every tester task targets files tester can write (gateway/tests/, tests/gateway/, orchestrator/tests/), every documenter task targets docs/ or `.md` files documenter can write. + +### Slice-Sizing Advisory (#2137 opt-2 advisory only — non-blocking) + +The plan's own LOC estimate (lines 343–355) is **slice-1 ~700 LOC, slice-2 ~600 LOC** — both well within the 1,000-LOC soft target. **No advisory needed.** Spot-check: 12 tasks in slice-1 across 9 files + 3 test files + 1 doc, with the LiteLLM-deployment YAML and the body-routing-credential extensions being the largest. 9 tasks in slice-2 across 6 files + 2 test files + 1 doc. + +### Slice-DAG Forest-Constraint Check (#2137) — PASS + +The `yaml-tasks` block (lines 524–1027) declares `slice-2.dependencies = [slice-1]` and `slice-1` has no `dependencies` field. Single-parent, no cycles, forest-valid. The plan's "Slice DAG" prose (lines 325–339) confirms the constraint. No `forest_violation` discriminator on the contract. + +### Coherence with Architect v2 + +Cross-checked the plan against the architect v2 proposal (which I just ACKed). They converge on Semantics B and on slice boundaries, with two minor differences I record as non-blockers below (body-rewriter slice placement; field naming) — both planner-level judgment calls and either choice ships a working integration. + +### Non-blocking + +- **TASK-2-3 hardcodes `claude_code_alias = "opus"` for every non-Claude model**, with no per-pipeline override of the alias itself. The risk_analyst's R3 (Claude Code compaction math) is explicit: "the alias presented to Claude Code MUST have a context window ≤ the real backend's window. real Qwen3 128K → present 'sonnet' alias (200K) is WRONG; present a 100K-window alias (or set context_token_threshold explicitly) is RIGHT." `opus` resolves to a 200K window — which exceeds Qwen3's 128K context. An operator who flips a role to a sub-200K backend hits the compaction wedge at validation time. Three reasonable fixes — any one is enough; the planner can pick at implementation time, but the plan should note that the operator's escape hatch exists: + - (a) Make the alias configurable: change `PipelineConfig.agent_models` values from a string to a `(claude_code_alias, litellm_model)` tuple/object so the operator can pick `("haiku", "qwen3-coder-30b")`. + - (b) Add a small per-window lookup: when the LiteLLM model's window is known to be <200K, the resolver picks a smaller alias automatically. + - (c) Set Claude Code's `context_token_threshold` SDK option explicitly for LiteLLM-bound agents (separate lever from the model name). + - **Why non-blocking**: cq-4 puts empirical validation on the operator post-merge. The operator can patch the resolver as a stopgap if validation surfaces the issue. Plus the issue is already enumerated in the risk_analyst's R3, so the operator has been warned. But the plan as written has no operator-controllable escape hatch — it requires a source-code patch. Worth a doc note in TASK-2-9 ("known limitation: if your real backend has a context window < ~190K, you must patch the resolver to use a smaller Claude alias or set `context_token_threshold` — see follow-up issue #XXXX"). + +- **TASK-2-6 places the body-rewriter in slice-2**, while the architect v2 places it in slice-1. Both work; planner's split keeps slice-1 purely additive at the routing level (the registry can resolve to LiteLLM but no body is rewritten until slice-2 ships the `_rewrite_upstream_model` helper). This means a hypothetical slice-1-only deployment that an operator somehow points at LiteLLM (by setting `Session.upstream='litellm'` out-of-band) would forward the body byte-unchanged with `model='opus'` to LiteLLM, which would fail at LiteLLM with "model not found" — fail-closed, just at the upstream layer instead of in the gateway. Acceptable. Implementer should be aware of this when reviewing the slice-1 PR. + +- **TASK-2-6 ordering: rewriter AFTER `_filter_blocked_tools`** (vs architect v2's "BEFORE"). Functionally commutative — rewriter touches `body['model']`, tool-strip touches `body['tools']`, no shared keys. Either order produces the same bytes on the wire. The architect's "BEFORE" preserves the "canonical body downstream" invariant slightly better (everything downstream sees one body shape). The planner's "AFTER" is slightly more efficient (skips one re-serialization if no rewrite). Implementer can pick; either is correct. Worth one line in the implementation note explaining the choice and rationale. + +- **Field naming: `Session.upstream` / `Session.upstream_model` (planner) vs `Session.agent_upstream` / `Session.agent_model_litellm` (architect v2)**. Both are valid; planner's names are shorter. Implementer needs to pick one and be consistent across `Session`, `register_session`, `/api/v1/sessions/create` payload, `GatewayClient.register_session`, and the test assertions. The plan and the architect v2 docs both refer to these fields by their own naming, so the implementer needs to reconcile — a one-line note in either re-propose would prevent confusion downstream. + +- **TASK-1-1 acceptance: "the existing Anthropic credential resolver (preserves the `# noqa: EGG200` annotation pattern at `gateway/gateway.py:9325`)"** — good attention to detail (lifting the noqa with the singleton). Worth verifying in code review that the noqa migrates intact to wherever `UpstreamRegistry` lives. + +- **TASK-1-12 doc** mentions cq-1/cq-2/cq-5/cq-7/cq-8 but not cq-9 (tool-strip uniformity) or cq-11 (`opus[1m]` left alone). These are also load-bearing decisions; worth one bullet each. Non-blocking — implementer can include during writing. + +- **TASK-1-8 / TASK-1-9 don't include adding `LITELLM_BASE_URL` to `gateway-deployment.yaml`'s `env:` block**. The architect v2 explicitly recommends this (so an operator can repoint via kustomize overlay without setting an env var the base manifest hasn't declared). Functionally the system works without it (the registry's hard-coded default `http://litellm.egg-system.svc.cluster.local:4000` matches the in-cluster Service DNS, and a kustomize strategic-merge overlay can still inject the env var). But declaring it in the base manifest is the "discoverable optional config knob" pattern. Worth folding into TASK-1-1 or TASK-1-8 as a small addition (3 lines of YAML). Non-blocking — the default works and overlay overrides function regardless. + +- **TASK-1-2 acceptance: "With `LITELLM_MASTER_KEY` unset, the resolver returns `None` and does not warn at startup."** — combined with TASK-1-3 ("Missing credentials for either upstream return a 401 with the same JSON body shape as today"), the failure mode for "session declares LiteLLM but no key" is a 401 from `_inject_upstream_credentials`. But cq-8 / AC-8 (per architect v2) call for a 502 (upstream-unreachable / misconfig), not a 401 (auth). 401 implies "your credential is wrong"; 502 implies "the upstream is broken." For a missing master key, 502 (or 500) is the more truthful status because the *operator* misconfigured the gateway, not the agent. Worth aligning. Non-blocking — the error reaches the agent either way, the agent's failure mode is the same. + +- **R5 in the planner-view risks ("Empirical Claude Code compaction-math compatibility")** says "Slice 2 keeps Claude Code's `--model` flag set to a recognised Claude alias (`opus`) for all LiteLLM-bound agents, per cq-5, so Claude Code's compaction math stays sane." This claim is only true when the backend's window ≥ `opus`'s window (200K). For backends <200K (Qwen3 128K), compaction math is NOT sane. The risk text overstates the mitigation. Tied to the first non-blocker above; would resolve when that follow-up lands. Non-blocking. + +- **Test-path inconsistency**: TASK-1-10 places new tests under `tests/gateway/` (matching the layout of `test_anthropic_proxy.py`); TASK-1-11 places extensions under `gateway/tests/` (matching `test_session_manager.py`). Both directories exist in the repo today (sister layouts — the canonical-test-layout cleanup is a separate concern). The planner correctly mirrors the existing file homes rather than relocating, so this is the right call. Worth one line in TASK-1-10 or 1-11 acknowledging the dual-layout convention so future planners don't try to consolidate one of them without warning. + +- **TASK-2-2's `default_agent_model` schema field** — the plan documents it in `config/repositories.yaml.example` but doesn't specify whether the field lives at the per-repo level (i.e. inside a `repos: :` block) or at a top-level config (applies to all repos). The plan's resolver text says "applies to every role not explicitly overridden" — per-repo only? Or per-repo with a top-level fallback? Worth pinning during implementation. Non-blocking. + +- **Risk_analyst R11 (`ANTHROPIC_BASE_URL` not in `_PROTECTED_ENV_KEYS`) defense-in-depth gap** is not addressed in the plan. The risk_analyst rated this LOW severity / LOW likelihood with NEGLIGIBLE residual after mitigation. Worth a one-line addition to TASK-1-7 or a new task ("Add `ANTHROPIC_BASE_URL` to `_PROTECTED_ENV_KEYS` so per-agent `extra_env` cannot redirect agent LLM traffic away from the gateway"). Non-blocking — current behavior is no caller sets `ANTHROPIC_BASE_URL` via extra_env, so the surface is theoretical. + +- **Risk_analyst R12 (max_turns hardcoded to 1000)** is also not addressed. Risk_analyst rated LOW severity. Worth a one-line follow-up tracker. Non-blocking. + +### Summary + +Plan is shippable. Task decomposition is clean, acceptance criteria are specific and testable, role↔files alignment is validated, primitive-existence audit passes, trust-boundary audit passes, no slice-sizing advisory, slice-DAG forest constraint holds. The single architectural choice that diverges from architect v2 (body-rewriter slice placement) is a defensible planner judgment call. The risk_analyst's R3 (compaction-window mismatch) is the most operationally significant residual — the plan would be strengthened by adding an operator escape hatch (configurable alias OR per-window lookup OR explicit `context_token_threshold`) and a doc note in TASK-2-9, but cq-4's "operator validates post-merge" framing makes this a follow-up concern rather than a merge blocker. ACK. + + +````yaml +id: 68a2986a-6e91-4d +phase: plan +metadata: + payload: + artifact_references: + - .egg-state/drafts/2769-plan.md + - .egg-state/agent-outputs/2769-risk_analyst-output.json + - .egg-state/agent-outputs/2769-architect-output.json + - gateway/gateway.py + - gateway/session_manager.py + - gateway/anthropic_credentials.py + - orchestrator/models.py + - orchestrator/consensus_wrapper.py + - orchestrator/concurrent_executor.py + - orchestrator/routes/pipelines.py + - orchestrator/kubernetes_spawner.py + - orchestrator/gateway_client.py + - config/repo_config.py + - shared/egg_contracts/agent_roles.py + - shared/egg_restrictions/patterns.py + - k8s/base/gateway-deployment.yaml + reason: "\nReviewed the full `2769-plan.md` (1027 lines), ran the planner's role\u2194\ + files validator (`OK - no violations`), spot-grepped every cited primitive,\ + \ and cross-referenced against the architect v2 component breakdown and the\ + \ risk_analyst's 15-risk artifact. The plan is internally consistent, picks\ + \ **Semantics B** explicitly (TASK-2-6 adds `_rewrite_upstream_model`), and\ + \ decomposes cleanly into two dependent slices that match the architect's recommended\ + \ option-3 shape.\n\n### Primitive-Existence Audit (#2594) \u2014 all PASS\n\ + \n| primitive | grep | result |\n|-----------|------|--------|\n| All gateway/orchestrator\ + \ primitives cited in the Primitives tables (lines 56\u2013116) | re-grepped\ + \ against current HEAD | every cite resolves to the line number the plan lists\ + \ \u2713 |\n| `AgentRole` enum at `shared/egg_contracts/agent_roles.py:46` (cited\ + \ for TASK-2-1 validator) | `grep -n 'class AgentRole' shared/egg_contracts/agent_roles.py`\ + \ | exists \u2713 |\n| `parse_env_file` at `gateway/anthropic_credentials.py:52`\ + \ (cited for TASK-1-2) | verified | exists \u2713 |\n| `SECRETS_PATH` env var\ + \ at `gateway/anthropic_credentials.py:31` (cited for TASK-1-2) | verified |\ + \ exists \u2713 |\n| `load_repo_pattern_override` at `shared/egg_restrictions/patterns.py:854`\ + \ (cited as pattern for TASK-2-2) | verified | exists \u2713 |\n| `get_repo_setting`\ + \ at `config/repo_config.py:248` (cited for TASK-2-2) | verified | exists \u2713\ + \ |\n| `_get_forwarded_headers` / `_filter_response_headers` at gateway/gateway.py:9343\ + \ / :9348 | verified | exists \u2713 |\n| Existing test file paths (gateway/tests/test_session_manager.py,\ + \ tests/gateway/test_anthropic_proxy.py, etc.) | `ls` | exist \u2713 |\n| `config/repositories.yaml.example`\ + \ | exists; the live `config/repositories.yaml` is operator-supplied \u2014\ + \ TASK-2-2's helper inherits the missing-file-returns-None behavior from `get_repo_setting`\ + \ | OK \u2713 |\n\nAll NEW primitives are clearly tagged with `(NEW \u2014 task\ + \ TASK-X-Y)` per the \xA79 exception (e.g. line 77 `UpstreamRegistry`, line\ + \ 97 `PipelineConfig.agent_models`), and the downstream task consuming each\ + \ new primitive has an explicit dependency on the creating task.\n\n### Trust-Boundary\ + \ Audit (\xA710) \u2014 PASS\n\nThe plan's \"Trust-boundary scope checks\" sub-section\ + \ (lines 118\u2013141) is the cleanest such audit I've seen in a plan. Every\ + \ gateway-side new primitive runs in the gateway pod; every orchestrator-side\ + \ new primitive runs in the orchestrator pod (trusted-CI-runner); no in-sandbox-agent\ + \ test fixture is added that would hit the parent-vs-sibling-conftest trap.\ + \ All test files live under `tests/gateway/`, `gateway/tests/`, or `orchestrator/tests/`\ + \ \u2014 all reachable from `make test` in the trusted-CI-runner. The plan explicitly\ + \ defers live-LiteLLM end-to-end validation to the operator-driven smoke test\ + \ (cq-4), so the in-sandbox-agent vs trusted-CI-runner mismatch never arises.\n\ + \n### Role\u2194Files Alignment (\xA78) \u2014 PASS\n\n`python3 -c \"from egg_contracts.plan_parser\ + \ import parse_plan_file, validate_task_role_alignment as v; \u2026\"` returned\ + \ `OK - no violations` against the current plan file. Every coder task targets\ + \ files coder can write (gateway/, orchestrator/, config/, k8s/base/), every\ + \ tester task targets files tester can write (gateway/tests/, tests/gateway/,\ + \ orchestrator/tests/), every documenter task targets docs/ or `.md` files documenter\ + \ can write.\n\n### Slice-Sizing Advisory (#2137 opt-2 advisory only \u2014\ + \ non-blocking)\n\nThe plan's own LOC estimate (lines 343\u2013355) is **slice-1\ + \ ~700 LOC, slice-2 ~600 LOC** \u2014 both well within the 1,000-LOC soft target.\ + \ **No advisory needed.** Spot-check: 12 tasks in slice-1 across 9 files + 3\ + \ test files + 1 doc, with the LiteLLM-deployment YAML and the body-routing-credential\ + \ extensions being the largest. 9 tasks in slice-2 across 6 files + 2 test files\ + \ + 1 doc.\n\n### Slice-DAG Forest-Constraint Check (#2137) \u2014 PASS\n\n\ + The `yaml-tasks` block (lines 524\u20131027) declares `slice-2.dependencies\ + \ = [slice-1]` and `slice-1` has no `dependencies` field. Single-parent, no\ + \ cycles, forest-valid. The plan's \"Slice DAG\" prose (lines 325\u2013339)\ + \ confirms the constraint. No `forest_violation` discriminator on the contract.\n\ + \n### Coherence with Architect v2\n\nCross-checked the plan against the architect\ + \ v2 proposal (which I just ACKed). They converge on Semantics B and on slice\ + \ boundaries, with two minor differences I record as non-blockers below (body-rewriter\ + \ slice placement; field naming) \u2014 both planner-level judgment calls and\ + \ either choice ships a working integration.\n\n### Non-blocking\n\n- **TASK-2-3\ + \ hardcodes `claude_code_alias = \"opus\"` for every non-Claude model**, with\ + \ no per-pipeline override of the alias itself. The risk_analyst's R3 (Claude\ + \ Code compaction math) is explicit: \"the alias presented to Claude Code MUST\ + \ have a context window \u2264 the real backend's window. real Qwen3 128K \u2192\ + \ present 'sonnet' alias (200K) is WRONG; present a 100K-window alias (or set\ + \ context_token_threshold explicitly) is RIGHT.\" `opus` resolves to a 200K\ + \ window \u2014 which exceeds Qwen3's 128K context. An operator who flips a\ + \ role to a sub-200K backend hits the compaction wedge at validation time. Three\ + \ reasonable fixes \u2014 any one is enough; the planner can pick at implementation\ + \ time, but the plan should note that the operator's escape hatch exists:\n\ + \ - (a) Make the alias configurable: change `PipelineConfig.agent_models` values\ + \ from a string to a `(claude_code_alias, litellm_model)` tuple/object so the\ + \ operator can pick `(\"haiku\", \"qwen3-coder-30b\")`.\n - (b) Add a small\ + \ per-window lookup: when the LiteLLM model's window is known to be <200K, the\ + \ resolver picks a smaller alias automatically.\n - (c) Set Claude Code's `context_token_threshold`\ + \ SDK option explicitly for LiteLLM-bound agents (separate lever from the model\ + \ name).\n - **Why non-blocking**: cq-4 puts empirical validation on the operator\ + \ post-merge. The operator can patch the resolver as a stopgap if validation\ + \ surfaces the issue. Plus the issue is already enumerated in the risk_analyst's\ + \ R3, so the operator has been warned. But the plan as written has no operator-controllable\ + \ escape hatch \u2014 it requires a source-code patch. Worth a doc note in TASK-2-9\ + \ (\"known limitation: if your real backend has a context window < ~190K, you\ + \ must patch the resolver to use a smaller Claude alias or set `context_token_threshold`\ + \ \u2014 see follow-up issue #XXXX\").\n\n- **TASK-2-6 places the body-rewriter\ + \ in slice-2**, while the architect v2 places it in slice-1. Both work; planner's\ + \ split keeps slice-1 purely additive at the routing level (the registry can\ + \ resolve to LiteLLM but no body is rewritten until slice-2 ships the `_rewrite_upstream_model`\ + \ helper). This means a hypothetical slice-1-only deployment that an operator\ + \ somehow points at LiteLLM (by setting `Session.upstream='litellm'` out-of-band)\ + \ would forward the body byte-unchanged with `model='opus'` to LiteLLM, which\ + \ would fail at LiteLLM with \"model not found\" \u2014 fail-closed, just at\ + \ the upstream layer instead of in the gateway. Acceptable. Implementer should\ + \ be aware of this when reviewing the slice-1 PR.\n\n- **TASK-2-6 ordering:\ + \ rewriter AFTER `_filter_blocked_tools`** (vs architect v2's \"BEFORE\"). Functionally\ + \ commutative \u2014 rewriter touches `body['model']`, tool-strip touches `body['tools']`,\ + \ no shared keys. Either order produces the same bytes on the wire. The architect's\ + \ \"BEFORE\" preserves the \"canonical body downstream\" invariant slightly\ + \ better (everything downstream sees one body shape). The planner's \"AFTER\"\ + \ is slightly more efficient (skips one re-serialization if no rewrite). Implementer\ + \ can pick; either is correct. Worth one line in the implementation note explaining\ + \ the choice and rationale.\n\n- **Field naming: `Session.upstream` / `Session.upstream_model`\ + \ (planner) vs `Session.agent_upstream` / `Session.agent_model_litellm` (architect\ + \ v2)**. Both are valid; planner's names are shorter. Implementer needs to pick\ + \ one and be consistent across `Session`, `register_session`, `/api/v1/sessions/create`\ + \ payload, `GatewayClient.register_session`, and the test assertions. The plan\ + \ and the architect v2 docs both refer to these fields by their own naming,\ + \ so the implementer needs to reconcile \u2014 a one-line note in either re-propose\ + \ would prevent confusion downstream.\n\n- **TASK-1-1 acceptance: \"the existing\ + \ Anthropic credential resolver (preserves the `# noqa: EGG200` annotation pattern\ + \ at `gateway/gateway.py:9325`)\"** \u2014 good attention to detail (lifting\ + \ the noqa with the singleton). Worth verifying in code review that the noqa\ + \ migrates intact to wherever `UpstreamRegistry` lives.\n\n- **TASK-1-12 doc**\ + \ mentions cq-1/cq-2/cq-5/cq-7/cq-8 but not cq-9 (tool-strip uniformity) or\ + \ cq-11 (`opus[1m]` left alone). These are also load-bearing decisions; worth\ + \ one bullet each. Non-blocking \u2014 implementer can include during writing.\n\ + \n- **TASK-1-8 / TASK-1-9 don't include adding `LITELLM_BASE_URL` to `gateway-deployment.yaml`'s\ + \ `env:` block**. The architect v2 explicitly recommends this (so an operator\ + \ can repoint via kustomize overlay without setting an env var the base manifest\ + \ hasn't declared). Functionally the system works without it (the registry's\ + \ hard-coded default `http://litellm.egg-system.svc.cluster.local:4000` matches\ + \ the in-cluster Service DNS, and a kustomize strategic-merge overlay can still\ + \ inject the env var). But declaring it in the base manifest is the \"discoverable\ + \ optional config knob\" pattern. Worth folding into TASK-1-1 or TASK-1-8 as\ + \ a small addition (3 lines of YAML). Non-blocking \u2014 the default works\ + \ and overlay overrides function regardless.\n\n- **TASK-1-2 acceptance: \"\ + With `LITELLM_MASTER_KEY` unset, the resolver returns `None` and does not warn\ + \ at startup.\"** \u2014 combined with TASK-1-3 (\"Missing credentials for either\ + \ upstream return a 401 with the same JSON body shape as today\"), the failure\ + \ mode for \"session declares LiteLLM but no key\" is a 401 from `_inject_upstream_credentials`.\ + \ But cq-8 / AC-8 (per architect v2) call for a 502 (upstream-unreachable /\ + \ misconfig), not a 401 (auth). 401 implies \"your credential is wrong\"; 502\ + \ implies \"the upstream is broken.\" For a missing master key, 502 (or 500)\ + \ is the more truthful status because the *operator* misconfigured the gateway,\ + \ not the agent. Worth aligning. Non-blocking \u2014 the error reaches the agent\ + \ either way, the agent's failure mode is the same.\n\n- **R5 in the planner-view\ + \ risks (\"Empirical Claude Code compaction-math compatibility\")** says \"\ + Slice 2 keeps Claude Code's `--model` flag set to a recognised Claude alias\ + \ (`opus`) for all LiteLLM-bound agents, per cq-5, so Claude Code's compaction\ + \ math stays sane.\" This claim is only true when the backend's window \u2265\ + \ `opus`'s window (200K). For backends <200K (Qwen3 128K), compaction math is\ + \ NOT sane. The risk text overstates the mitigation. Tied to the first non-blocker\ + \ above; would resolve when that follow-up lands. Non-blocking.\n\n- **Test-path\ + \ inconsistency**: TASK-1-10 places new tests under `tests/gateway/` (matching\ + \ the layout of `test_anthropic_proxy.py`); TASK-1-11 places extensions under\ + \ `gateway/tests/` (matching `test_session_manager.py`). Both directories exist\ + \ in the repo today (sister layouts \u2014 the canonical-test-layout cleanup\ + \ is a separate concern). The planner correctly mirrors the existing file homes\ + \ rather than relocating, so this is the right call. Worth one line in TASK-1-10\ + \ or 1-11 acknowledging the dual-layout convention so future planners don't\ + \ try to consolidate one of them without warning.\n\n- **TASK-2-2's `default_agent_model`\ + \ schema field** \u2014 the plan documents it in `config/repositories.yaml.example`\ + \ but doesn't specify whether the field lives at the per-repo level (i.e. inside\ + \ a `repos: :` block) or at a top-level config (applies to all repos).\ + \ The plan's resolver text says \"applies to every role not explicitly overridden\"\ + \ \u2014 per-repo only? Or per-repo with a top-level fallback? Worth pinning\ + \ during implementation. Non-blocking.\n\n- **Risk_analyst R11 (`ANTHROPIC_BASE_URL`\ + \ not in `_PROTECTED_ENV_KEYS`) defense-in-depth gap** is not addressed in the\ + \ plan. The risk_analyst rated this LOW severity / LOW likelihood with NEGLIGIBLE\ + \ residual after mitigation. Worth a one-line addition to TASK-1-7 or a new\ + \ task (\"Add `ANTHROPIC_BASE_URL` to `_PROTECTED_ENV_KEYS` so per-agent `extra_env`\ + \ cannot redirect agent LLM traffic away from the gateway\"). Non-blocking \u2014\ + \ current behavior is no caller sets `ANTHROPIC_BASE_URL` via extra_env, so\ + \ the surface is theoretical.\n\n- **Risk_analyst R12 (max_turns hardcoded to\ + \ 1000)** is also not addressed. Risk_analyst rated LOW severity. Worth a one-line\ + \ follow-up tracker. Non-blocking.\n\n### Summary\n\nPlan is shippable. Task\ + \ decomposition is clean, acceptance criteria are specific and testable, role\u2194\ + files alignment is validated, primitive-existence audit passes, trust-boundary\ + \ audit passes, no slice-sizing advisory, slice-DAG forest constraint holds.\ + \ The single architectural choice that diverges from architect v2 (body-rewriter\ + \ slice placement) is a defensible planner judgment call. The risk_analyst's\ + \ R3 (compaction-window mismatch) is the most operationally significant residual\ + \ \u2014 the plan would be strengthened by adding an operator escape hatch (configurable\ + \ alias OR per-window lookup OR explicit `context_token_threshold`) and a doc\ + \ note in TASK-2-9, but cq-4's \"operator validates post-merge\" framing makes\ + \ this a follow-up concern rather than a merge blocker. ACK.\n" + ack_version: 1 + version: 1 +```` + +### [2026-05-22T03:50:58Z] orchestrator → task_planner (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: a2441044-c6dd-4d +phase: plan +metadata: + ready_to_confirm: true + version: 1 +```` + +### [2026-05-22T03:50:58Z] task_planner (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 21769c19-eb66-4c +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:51:02Z] task_planner (CONSENSUS_CONFIRMED): Confirmed by task_planner + +````yaml +id: 1e3717fa-6888-41 +phase: plan +metadata: + consensus_reached: false +```` + +### [2026-05-22T03:51:13Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: e4ddcf79-9187-45 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:51:13.663337+00:00' +```` + +### [2026-05-22T03:51:22Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: c490a35f-aa86-42 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:51:39Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: bc91191f-d77e-46 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:49:38.773629+00:00' +```` + +### [2026-05-22T03:52:13Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: 17cffbc9-5d4a-4d +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:51:13.663337+00:00' +```` + +### [2026-05-22T03:52:22Z] risk_analyst (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: cdcf8aed-2bc0-47 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:42:06.692615+00:00' +```` + +### [2026-05-22T03:52:36Z] reviewer_plan → risk_analyst (CONSENSUS_ACK): ACK from reviewer_plan for risk_analyst + + +Reviewed the full risk_analyst JSON (`.egg-state/agent-outputs/2769-risk_analyst-output.json`, 459 lines). The artifact is comprehensive and operationally honest: HIGH overall rating with PROCEED_WITH_MITIGATIONS, 15 risks (R1–R15) each with severity, likelihood, impact, description, affected_files, mitigation strategy + effort + residual, and `requires_human_review` flag. The Primitive-Existence and Trust-Boundary sub-sections are explicit and convergent with what I separately verified. + +### Risk-coverage audit + +Cross-checked the risk list against the architect's design, the planner's task breakdown, and my own preparation walk through the codebase. Every meaningful failure surface is covered: + +- **R1 LiteLLM supply-chain (CRITICAL/MEDIUM)** — six layered controls (pinned digest, cosign, NetworkPolicy, ServiceAccount RBAC, no JWT auth, kill-switch via `agent_models = {}`). The March 2026 PyPI incident citation and the subsequent CVE-2026-42208 / -35029 / -42271 list are accurately recent and bound the recommended pin floor (>= 1.83.7). The recommended controls map cleanly onto the architect v2's NetworkPolicy-egress-overlay component and the planner's TASK-1-8 manifest acceptance criteria (image-pinning specifically; planner's AC says "pinned LiteLLM image" but doesn't yet specify "digest, not floating tag" — worth a one-line tightening at implement time; non-blocking). + +- **R2 LiteLLM streaming tool_use drop (CRITICAL/HIGH)** — accurately cites the GitHub issue chain (#25561, #25321, #24765) and identifies the egg-specific worst case (cq-5 + Claude Code + non-Anthropic backend is exactly the configuration these bugs were filed against). The mitigation correctly defers correctness to the operator's acceptance-test smoke run (cq-4) and recommends an optional defensive sentinel in the gateway. The empty-input-detection sentinel idea is a clean future addition; correctly noted as out-of-scope-for-merge. + +- **R3 Claude Code compaction math drift (HIGH/MEDIUM)** — the most operationally consequential risk for the architecture chosen. Correctly identifies that the cq-5 recognized-alias mitigation only works if the alias's window ≤ the real backend's window, and explicitly calls out the Qwen3 128K vs Claude `opus` 200K case as the failure mode. My ACK to the task_planner v1 cited R3 as the basis for a non-blocking observation — the plan's `claude_code_alias = "opus"` hardcoding doesn't give operators an in-config escape hatch. The risk_analyst's recommended mitigations ("(1) explicit invariant, (2) per-(alias, real-model) lookup table, (3) explicit context_token_threshold") are all viable and align with my non-blocker. + +- **R4 vLLM / Qwen3 self-hosted bugs (MEDIUM/HIGH)** — correctly scoped out of this issue (cq-6 deferred self-hosted vLLM); documented for the eventual self-hosted cut. Cites vLLM #21565, #17655, #23992, #20611, #39056 — all real. + +- **R5 Hosted provider as new credential + supply-chain surface (MEDIUM/MEDIUM)** — the operational implications of cq-6 (hosted Qwen first) are surfaced honestly. The mitigation correctly defers to provider-published policies and to the existing cq-7 LITELLM_MASTER_KEY-as-front pattern; cq-9 tool-strip stays uniform. + +- **R6 Session schema gap (MEDIUM/CERTAIN)** — exactly the gap the architect's slice-1 components 4–5 ("Session-storage extensions" and "Orchestrator → gateway session-create payload extension") address. The risk_analyst's mitigation prose maps line-by-line onto the planner's TASK-1-4 / TASK-1-5 / TASK-1-7. Convergent across all three producers. + +- **R7 `build_consensus_wrapped_command` model arg drift (MEDIUM/CERTAIN)** — exactly the gap the planner's TASK-2-3 / TASK-2-4 / TASK-2-5 address. The risk_analyst's "defensive assert in the consensus wrapper that errors if model and the session's model_alias disagree" is a nice belt-and-braces addition the planner did not include — worth folding in as a follow-up enhancement (non-blocking; the single-source-of-truth resolver pattern is itself the primary defense). + +- **R8 Claude Code harness assumption durability (MEDIUM/MEDIUM)** — orthogonal to the body-rewrite Semantics A vs B debate I had with the architect (architect's R8 is the tokenizer-mismatch argument; risk_analyst's R8 is the Claude Code closed-source heuristic durability). Both are real; named-collisions on the "R8" label across the two artifacts is mildly confusing but not substantive. Risk_analyst's mitigation (record + bound the Claude Code version + scheduled CI smoke test) is the right shape; correctly noted as out-of-scope-for-merge per cq-4. + +- **R9 max_llm_cost_per_hour silent break (MEDIUM/CERTAIN)** — exactly the observable-behavior gap I raised against architect v1 (architect v2 then surfaced it explicitly in `out_of_scope_explicitly`). Risk_analyst's mitigation ("create a follow-up issue + document the regression in the deployment YAML / config README") is the right framing. Convergent. + +- **R10 SSE accumulator hardcoded to Anthropic event names (MEDIUM/LOW)** — cheap defensive log on unknown event_type is a good, low-cost mitigation. Worth folding into the implementer's slice-1 work (one logger.warning line in `_SSEAccumulator`); the planner did not break this out as a task. Non-blocking — the bug surface is well-bounded. + +- **R11 `ANTHROPIC_BASE_URL` not in `_PROTECTED_ENV_KEYS` (LOW/LOW)** — accurate defense-in-depth gap. One-line fix. I flagged this as a non-blocker in my task_planner ACK; risk_analyst flagged it more formally with negligible-residual-after-mitigation framing. Convergent. + +- **R12 max_turns=1000 hardcoded (LOW/MEDIUM)** — correctly scoped as a tuning concern for follow-up; per-model max_turns is reasonable as a future PipelineConfig.agent_models extension or a separate `max_turns_per_role` field. + +- **R13 Empirical validation deferred (MEDIUM/CERTAIN)** — the structural-vs-nominal "no-op by default" guarantee is exactly what `requires_human_review: true` should flag. The risk_analyst's mitigation ("Reviewer should verify this by inspection: searching for 'litellm' in the diff should yield only conditional / opt-in code paths") is the right reviewer test at implement-PR time. I noted this as my reviewer mandate for the implement-phase PR. + +- **R14 UpstreamRegistry abstraction needed per feedback Q3 (LOW/CERTAIN)** — exactly the architect's slice-1 component 1. Convergent across all three producers. + +- **R15 Fail-closed visibility requires monitoring (LOW/MEDIUM)** — operational trade-off correctly recorded as known-and-accepted per cq-8, not as discoverable behavior. Operator-side acknowledgement is the right framing. + +### Runtime-Primitive Audit (#2594) — convergent + +The risk_analyst's `runtime_primitive_audit_per_2594` table (lines 308–372) identifies the same MISSING / EXISTING / EXTENDABLE classifications I derived independently. Notable matches: +- `Session.upstream / .model_alias`: MISSING ✓ +- `register_session(upstream=..., model_alias=...)`: MISSING ✓ +- `PipelineConfig.agent_models[role]`: MISSING ✓ +- `build_consensus_wrapped_command(model=...)`: EXISTS but UNUSED ✓ +- `LiteLLM Deployment + LITELLM_MASTER_KEY`: MISSING ✓ +- `Internal cluster DNS allowlisting`: NOT NEEDED (correctly notes that LiteLLM is reached via the gateway's httpx client, not via Squid — the analyst correctly avoids the trap of trying to allowlist `*.svc.cluster.local` in `gateway/allowed_domains.txt`) ✓ +- `AnthropicCredential extension for LiteLLM master key`: EXTENDABLE (matches the architect's "sibling LiteLLMCredentialsManager" recommendation) ✓ +- `Claude Code recognised-alias compaction math`: DOCUMENTED EXTERNALLY (correctly noted as a heuristic dependency) ✓ +- `_SSEAccumulator (Anthropic event names)`: EXISTS, EVENT-NAME-HARDCODED (matches my exploration) ✓ +- `max_llm_cost_per_hour`: EXISTS but UNINSTRUMENTED FOR LITELLM ✓ +- `_PROTECTED_ENV_KEYS for ANTHROPIC_BASE_URL`: MISSING ✓ +- `max_turns per model`: HARDCODED ✓ + +### Trust-boundary audit — convergent + +The risk_analyst's `trust_boundary_audit` (lines 374–398) names all five boundaries (Sandbox→Gateway, Gateway→Anthropic, Gateway→LiteLLM, LiteLLM→Hosted Qwen, Orchestrator→Gateway register_session). The "Gateway → Anthropic UNCHANGED" call-out and the "LiteLLM → Hosted Qwen NEW: sees full request bodies" are the right shape for an operator reviewing this design. + +### Rollback plan + +The `rollback_plan` (lines 400–423) names four scenarios with corresponding actions: +- Per-agent flip exposes bug → unset `agent_models[role]` (config-only, structural) +- Future LiteLLM CVE → scale Deployment to 0, unset cluster-wide, rotate key (operational) +- LiteLLM pinned image regresses → re-pin to last-known-good sha256 (operational, git history dependent) +- Hosted provider outage → unset agent_models, revoke provider key + +All four rollback paths are real and align with cq-8's fail-closed framing. + +### Non-blocking + +- **R8 label collision with architect v2**: the architect's R8 (tokenizer-mismatch on count_tokens, used to reject Semantics A) and the risk_analyst's R8 (Claude Code heuristic durability) share the label but address different concerns. Not blocking — implementer's audit notes should disambiguate. Worth a one-line renumber if either artifact gets a re-propose. + +- **R1 mitigation "(2) Enable cosign signature verification at the imagePullPolicy layer or via a Kyverno/Cosign admission policy"**: cosign / Kyverno admission policies are operator-cluster-dependent and may not be present in every egg deployment. Worth softening to "where a cluster admission controller is available, enable cosign verification; otherwise the digest pin is the minimum bar." Non-blocking; the digest pin alone covers the primary attack surface. + +- **The `requires_human_review` flags on R1, R2, R13** are all accurate — these are the three risks where operator-side awareness materially changes acceptance. The plan reviewer's mandate (which I already addressed in the task_planner ACK) is convergent: the plan-reviewer must confirm the no-op-by-default structural guarantee in the implement-phase PR; the operator must read R1 and R2 before flipping a role to LiteLLM at validation time. + +- **R3 alias-window mitigation** — I raised this as the single most operationally consequential non-blocker in my task_planner ACK; the risk_analyst flagged it independently with the same severity rating (HIGH). Convergent across both reviews. The fix is small (operator-controllable claude_code_alias OR per-window lookup OR explicit context_token_threshold). Worth a follow-up issue if not addressed in the implement phase. + +### Summary + +Risk assessment is thorough and operationally honest. Coverage is convergent with the architect's design and the planner's tasks (where the same gap is named by all three artifacts: R6 ↔ architect Session-storage extensions ↔ planner TASK-1-4/5; R7 ↔ architect resolver ↔ planner TASK-2-3/4/5). The HIGH overall rating is driven by EXTERNAL risks (LiteLLM supply-chain, LiteLLM SSE bugs, Claude Code compaction math) — not by the egg-side code change, which is correctly characterized as low-risk because of the no-op-by-default framing. PROCEED_WITH_MITIGATIONS is the right call. No blocking concerns. ACK. + + +````yaml +id: a6dd2396-e1a5-44 +phase: plan +metadata: + payload: + artifact_references: + - .egg-state/agent-outputs/2769-risk_analyst-output.json + - .egg-state/drafts/2769-analysis.md + - .egg-state/drafts/2769-plan.md + - .egg-state/agent-outputs/2769-architect-output.json + - gateway/gateway.py + - gateway/session_manager.py + - gateway/anthropic_credentials.py + - orchestrator/kubernetes_spawner.py + - orchestrator/consensus_wrapper.py + - orchestrator/models.py + - orchestrator/concurrent_executor.py + - orchestrator/routes/pipelines.py + - orchestrator/overseer/self_monitor.py + - shared/egg_agent/client.py + - k8s/base/gateway-deployment.yaml + reason: "\nReviewed the full risk_analyst JSON (`.egg-state/agent-outputs/2769-risk_analyst-output.json`,\ + \ 459 lines). The artifact is comprehensive and operationally honest: HIGH overall\ + \ rating with PROCEED_WITH_MITIGATIONS, 15 risks (R1\u2013R15) each with severity,\ + \ likelihood, impact, description, affected_files, mitigation strategy + effort\ + \ + residual, and `requires_human_review` flag. The Primitive-Existence and\ + \ Trust-Boundary sub-sections are explicit and convergent with what I separately\ + \ verified.\n\n### Risk-coverage audit\n\nCross-checked the risk list against\ + \ the architect's design, the planner's task breakdown, and my own preparation\ + \ walk through the codebase. Every meaningful failure surface is covered:\n\n\ + - **R1 LiteLLM supply-chain (CRITICAL/MEDIUM)** \u2014 six layered controls\ + \ (pinned digest, cosign, NetworkPolicy, ServiceAccount RBAC, no JWT auth, kill-switch\ + \ via `agent_models = {}`). The March 2026 PyPI incident citation and the subsequent\ + \ CVE-2026-42208 / -35029 / -42271 list are accurately recent and bound the\ + \ recommended pin floor (>= 1.83.7). The recommended controls map cleanly onto\ + \ the architect v2's NetworkPolicy-egress-overlay component and the planner's\ + \ TASK-1-8 manifest acceptance criteria (image-pinning specifically; planner's\ + \ AC says \"pinned LiteLLM image\" but doesn't yet specify \"digest, not floating\ + \ tag\" \u2014 worth a one-line tightening at implement time; non-blocking).\n\ + \n- **R2 LiteLLM streaming tool_use drop (CRITICAL/HIGH)** \u2014 accurately\ + \ cites the GitHub issue chain (#25561, #25321, #24765) and identifies the egg-specific\ + \ worst case (cq-5 + Claude Code + non-Anthropic backend is exactly the configuration\ + \ these bugs were filed against). The mitigation correctly defers correctness\ + \ to the operator's acceptance-test smoke run (cq-4) and recommends an optional\ + \ defensive sentinel in the gateway. The empty-input-detection sentinel idea\ + \ is a clean future addition; correctly noted as out-of-scope-for-merge.\n\n\ + - **R3 Claude Code compaction math drift (HIGH/MEDIUM)** \u2014 the most operationally\ + \ consequential risk for the architecture chosen. Correctly identifies that\ + \ the cq-5 recognized-alias mitigation only works if the alias's window \u2264\ + \ the real backend's window, and explicitly calls out the Qwen3 128K vs Claude\ + \ `opus` 200K case as the failure mode. My ACK to the task_planner v1 cited\ + \ R3 as the basis for a non-blocking observation \u2014 the plan's `claude_code_alias\ + \ = \"opus\"` hardcoding doesn't give operators an in-config escape hatch. The\ + \ risk_analyst's recommended mitigations (\"(1) explicit invariant, (2) per-(alias,\ + \ real-model) lookup table, (3) explicit context_token_threshold\") are all\ + \ viable and align with my non-blocker.\n\n- **R4 vLLM / Qwen3 self-hosted bugs\ + \ (MEDIUM/HIGH)** \u2014 correctly scoped out of this issue (cq-6 deferred self-hosted\ + \ vLLM); documented for the eventual self-hosted cut. Cites vLLM #21565, #17655,\ + \ #23992, #20611, #39056 \u2014 all real.\n\n- **R5 Hosted provider as new credential\ + \ + supply-chain surface (MEDIUM/MEDIUM)** \u2014 the operational implications\ + \ of cq-6 (hosted Qwen first) are surfaced honestly. The mitigation correctly\ + \ defers to provider-published policies and to the existing cq-7 LITELLM_MASTER_KEY-as-front\ + \ pattern; cq-9 tool-strip stays uniform.\n\n- **R6 Session schema gap (MEDIUM/CERTAIN)**\ + \ \u2014 exactly the gap the architect's slice-1 components 4\u20135 (\"Session-storage\ + \ extensions\" and \"Orchestrator \u2192 gateway session-create payload extension\"\ + ) address. The risk_analyst's mitigation prose maps line-by-line onto the planner's\ + \ TASK-1-4 / TASK-1-5 / TASK-1-7. Convergent across all three producers.\n\n\ + - **R7 `build_consensus_wrapped_command` model arg drift (MEDIUM/CERTAIN)**\ + \ \u2014 exactly the gap the planner's TASK-2-3 / TASK-2-4 / TASK-2-5 address.\ + \ The risk_analyst's \"defensive assert in the consensus wrapper that errors\ + \ if model and the session's model_alias disagree\" is a nice belt-and-braces\ + \ addition the planner did not include \u2014 worth folding in as a follow-up\ + \ enhancement (non-blocking; the single-source-of-truth resolver pattern is\ + \ itself the primary defense).\n\n- **R8 Claude Code harness assumption durability\ + \ (MEDIUM/MEDIUM)** \u2014 orthogonal to the body-rewrite Semantics A vs B debate\ + \ I had with the architect (architect's R8 is the tokenizer-mismatch argument;\ + \ risk_analyst's R8 is the Claude Code closed-source heuristic durability).\ + \ Both are real; named-collisions on the \"R8\" label across the two artifacts\ + \ is mildly confusing but not substantive. Risk_analyst's mitigation (record\ + \ + bound the Claude Code version + scheduled CI smoke test) is the right shape;\ + \ correctly noted as out-of-scope-for-merge per cq-4.\n\n- **R9 max_llm_cost_per_hour\ + \ silent break (MEDIUM/CERTAIN)** \u2014 exactly the observable-behavior gap\ + \ I raised against architect v1 (architect v2 then surfaced it explicitly in\ + \ `out_of_scope_explicitly`). Risk_analyst's mitigation (\"create a follow-up\ + \ issue + document the regression in the deployment YAML / config README\")\ + \ is the right framing. Convergent.\n\n- **R10 SSE accumulator hardcoded to\ + \ Anthropic event names (MEDIUM/LOW)** \u2014 cheap defensive log on unknown\ + \ event_type is a good, low-cost mitigation. Worth folding into the implementer's\ + \ slice-1 work (one logger.warning line in `_SSEAccumulator`); the planner did\ + \ not break this out as a task. Non-blocking \u2014 the bug surface is well-bounded.\n\ + \n- **R11 `ANTHROPIC_BASE_URL` not in `_PROTECTED_ENV_KEYS` (LOW/LOW)** \u2014\ + \ accurate defense-in-depth gap. One-line fix. I flagged this as a non-blocker\ + \ in my task_planner ACK; risk_analyst flagged it more formally with negligible-residual-after-mitigation\ + \ framing. Convergent.\n\n- **R12 max_turns=1000 hardcoded (LOW/MEDIUM)** \u2014\ + \ correctly scoped as a tuning concern for follow-up; per-model max_turns is\ + \ reasonable as a future PipelineConfig.agent_models extension or a separate\ + \ `max_turns_per_role` field.\n\n- **R13 Empirical validation deferred (MEDIUM/CERTAIN)**\ + \ \u2014 the structural-vs-nominal \"no-op by default\" guarantee is exactly\ + \ what `requires_human_review: true` should flag. The risk_analyst's mitigation\ + \ (\"Reviewer should verify this by inspection: searching for 'litellm' in the\ + \ diff should yield only conditional / opt-in code paths\") is the right reviewer\ + \ test at implement-PR time. I noted this as my reviewer mandate for the implement-phase\ + \ PR.\n\n- **R14 UpstreamRegistry abstraction needed per feedback Q3 (LOW/CERTAIN)**\ + \ \u2014 exactly the architect's slice-1 component 1. Convergent across all\ + \ three producers.\n\n- **R15 Fail-closed visibility requires monitoring (LOW/MEDIUM)**\ + \ \u2014 operational trade-off correctly recorded as known-and-accepted per\ + \ cq-8, not as discoverable behavior. Operator-side acknowledgement is the right\ + \ framing.\n\n### Runtime-Primitive Audit (#2594) \u2014 convergent\n\nThe risk_analyst's\ + \ `runtime_primitive_audit_per_2594` table (lines 308\u2013372) identifies the\ + \ same MISSING / EXISTING / EXTENDABLE classifications I derived independently.\ + \ Notable matches:\n- `Session.upstream / .model_alias`: MISSING \u2713\n- `register_session(upstream=...,\ + \ model_alias=...)`: MISSING \u2713\n- `PipelineConfig.agent_models[role]`:\ + \ MISSING \u2713\n- `build_consensus_wrapped_command(model=...)`: EXISTS but\ + \ UNUSED \u2713\n- `LiteLLM Deployment + LITELLM_MASTER_KEY`: MISSING \u2713\ + \n- `Internal cluster DNS allowlisting`: NOT NEEDED (correctly notes that LiteLLM\ + \ is reached via the gateway's httpx client, not via Squid \u2014 the analyst\ + \ correctly avoids the trap of trying to allowlist `*.svc.cluster.local` in\ + \ `gateway/allowed_domains.txt`) \u2713\n- `AnthropicCredential extension for\ + \ LiteLLM master key`: EXTENDABLE (matches the architect's \"sibling LiteLLMCredentialsManager\"\ + \ recommendation) \u2713\n- `Claude Code recognised-alias compaction math`:\ + \ DOCUMENTED EXTERNALLY (correctly noted as a heuristic dependency) \u2713\n\ + - `_SSEAccumulator (Anthropic event names)`: EXISTS, EVENT-NAME-HARDCODED (matches\ + \ my exploration) \u2713\n- `max_llm_cost_per_hour`: EXISTS but UNINSTRUMENTED\ + \ FOR LITELLM \u2713\n- `_PROTECTED_ENV_KEYS for ANTHROPIC_BASE_URL`: MISSING\ + \ \u2713\n- `max_turns per model`: HARDCODED \u2713\n\n### Trust-boundary audit\ + \ \u2014 convergent\n\nThe risk_analyst's `trust_boundary_audit` (lines 374\u2013\ + 398) names all five boundaries (Sandbox\u2192Gateway, Gateway\u2192Anthropic,\ + \ Gateway\u2192LiteLLM, LiteLLM\u2192Hosted Qwen, Orchestrator\u2192Gateway\ + \ register_session). The \"Gateway \u2192 Anthropic UNCHANGED\" call-out and\ + \ the \"LiteLLM \u2192 Hosted Qwen NEW: sees full request bodies\" are the right\ + \ shape for an operator reviewing this design.\n\n### Rollback plan\n\nThe `rollback_plan`\ + \ (lines 400\u2013423) names four scenarios with corresponding actions:\n- Per-agent\ + \ flip exposes bug \u2192 unset `agent_models[role]` (config-only, structural)\n\ + - Future LiteLLM CVE \u2192 scale Deployment to 0, unset cluster-wide, rotate\ + \ key (operational)\n- LiteLLM pinned image regresses \u2192 re-pin to last-known-good\ + \ sha256 (operational, git history dependent)\n- Hosted provider outage \u2192\ + \ unset agent_models, revoke provider key\n\nAll four rollback paths are real\ + \ and align with cq-8's fail-closed framing.\n\n### Non-blocking\n\n- **R8 label\ + \ collision with architect v2**: the architect's R8 (tokenizer-mismatch on count_tokens,\ + \ used to reject Semantics A) and the risk_analyst's R8 (Claude Code heuristic\ + \ durability) share the label but address different concerns. Not blocking \u2014\ + \ implementer's audit notes should disambiguate. Worth a one-line renumber if\ + \ either artifact gets a re-propose.\n\n- **R1 mitigation \"(2) Enable cosign\ + \ signature verification at the imagePullPolicy layer or via a Kyverno/Cosign\ + \ admission policy\"**: cosign / Kyverno admission policies are operator-cluster-dependent\ + \ and may not be present in every egg deployment. Worth softening to \"where\ + \ a cluster admission controller is available, enable cosign verification; otherwise\ + \ the digest pin is the minimum bar.\" Non-blocking; the digest pin alone covers\ + \ the primary attack surface.\n\n- **The `requires_human_review` flags on R1,\ + \ R2, R13** are all accurate \u2014 these are the three risks where operator-side\ + \ awareness materially changes acceptance. The plan reviewer's mandate (which\ + \ I already addressed in the task_planner ACK) is convergent: the plan-reviewer\ + \ must confirm the no-op-by-default structural guarantee in the implement-phase\ + \ PR; the operator must read R1 and R2 before flipping a role to LiteLLM at\ + \ validation time.\n\n- **R3 alias-window mitigation** \u2014 I raised this\ + \ as the single most operationally consequential non-blocker in my task_planner\ + \ ACK; the risk_analyst flagged it independently with the same severity rating\ + \ (HIGH). Convergent across both reviews. The fix is small (operator-controllable\ + \ claude_code_alias OR per-window lookup OR explicit context_token_threshold).\ + \ Worth a follow-up issue if not addressed in the implement phase.\n\n### Summary\n\ + \nRisk assessment is thorough and operationally honest. Coverage is convergent\ + \ with the architect's design and the planner's tasks (where the same gap is\ + \ named by all three artifacts: R6 \u2194 architect Session-storage extensions\ + \ \u2194 planner TASK-1-4/5; R7 \u2194 architect resolver \u2194 planner TASK-2-3/4/5).\ + \ The HIGH overall rating is driven by EXTERNAL risks (LiteLLM supply-chain,\ + \ LiteLLM SSE bugs, Claude Code compaction math) \u2014 not by the egg-side\ + \ code change, which is correctly characterized as low-risk because of the no-op-by-default\ + \ framing. PROCEED_WITH_MITIGATIONS is the right call. No blocking concerns.\ + \ ACK.\n" + ack_version: 1 + version: 1 +```` + +### [2026-05-22T03:52:39Z] architect (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: e782e87a-f4ac-47 +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:49:38.773629+00:00' +```` + +### [2026-05-22T03:52:39Z] reviewer_plan (CONSENSUS_CONFIRMED): Confirmed by reviewer_plan + +````yaml +id: 87cf1d9e-86ea-40 +phase: plan +metadata: + consensus_reached: true +```` + +### [2026-05-22T03:52:40Z] task_planner (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 42f1a681-0130-48 +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:52:40Z] risk_analyst (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 2eb393b0-1905-44 +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:52:40Z] architect (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 1e6dbb0a-4d4c-4d +phase: plan +metadata: + state: WORKING +```` + +### [2026-05-22T03:52:44Z] task_planner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: a510f3ca-18a5-4b +phase: plan +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T03:52:44.501632+00:00' +```` diff --git a/.egg-state/brc-history/2769-refine.json b/.egg-state/brc-history/2769-refine.json new file mode 100644 index 0000000000..7261f8dbbb --- /dev/null +++ b/.egg-state/brc-history/2769-refine.json @@ -0,0 +1,629 @@ +[ + { + "id": "d62abdd5-907c-40", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:21:36.053148+00:00", + "phase": "refine" + }, + { + "id": "645d0ba8-dd58-4a", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:22:51.122489+00:00", + "phase": "refine" + }, + { + "id": "a6f2fafc-69bb-43", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:23:03.162602+00:00", + "phase": "refine" + }, + { + "id": "171c4e9a-d281-4f", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "OVERSEER_ALERT", + "subject": "agent-heartbeat-stall [high]", + "body": "Refine-phase agents never launched for pipeline issue-2769 \u2014 zero executions, zero proposals, zero heartbeats after pipeline start\n\nDetail:\nPipeline issue-2769 is in the refine phase (cycle 0). The contract shows agent_executions=[], BRC history is empty (0 items), and no checkpoints exist. BRC state lists refiner/reviewer_agent_design/reviewer_refine all as WORKING with no proposals. The orchestrator is healthy (uptime ~4600s, state stores OK). The pipeline_unreachable alert from the monitor was a long-poll timeout on /api/v1/pipelines/issue-2769/status \u2014 not an orchestrator outage. The real issue is that zero agents have been dispatched. The pipeline may have been registered without triggering agent launch.\n\nRecommended action:\nOperator should verify agent dispatch for issue-2769 refine phase. Options: (1) re-trigger the pipeline via egg-orch or the webhook endpoint, (2) manually launch the refiner agent, or (3) inspect the orchestrator logs for dispatch failures around the pipeline registration time (~2026-05-22T00:05Z).", + "metadata": {}, + "timestamp": "2026-05-22T01:23:26.306589+00:00", + "phase": "refine" + }, + { + "id": "d279a9e0-ab4b-47", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "Monitoring cycle 1 complete. Agent-heartbeat-stall alert issued. Awaiting operator action or agent launch. Running cycle 2.", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T01:23:31.963156+00:00", + "phase": "refine" + }, + { + "id": "60cb9cdf-f9df-4d", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:23:51.142823+00:00", + "phase": "refine" + }, + { + "id": "94bf73b5-822f-4b", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:24:03.223581+00:00", + "phase": "refine" + }, + { + "id": "eb9703a3-b65a-4c", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:24:51.217839+00:00", + "phase": "refine" + }, + { + "id": "5c0e083e-642d-4f", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:25:03.283354+00:00", + "phase": "refine" + }, + { + "id": "ebacc1f1-b02b-40", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:25:51.283765+00:00", + "phase": "refine" + }, + { + "id": "d91065f3-489e-47", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:26:03.358454+00:00", + "phase": "refine" + }, + { + "id": "795a976a-2c79-48", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:26:51.375103+00:00", + "phase": "refine" + }, + { + "id": "1e28afe8-40b3-4d", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WAITING_FOR_EVENT", + "body": "Overseer monitoring active. Waiting for refiner CONSENSUS_PROPOSE. Cycle 13: refiner 321s elapsed, reviewers healthy. Two self-alerts cleared: progress event emitted, heartbeat refreshed.", + "metadata": { + "state": "WAITING_FOR_EVENT", + "since": "2026-05-22T01:20:46.000000+00:00" + }, + "timestamp": "2026-05-22T01:26:53.705770+00:00", + "phase": "refine" + }, + { + "id": "efb8b822-f7bc-4f", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:27:03.426860+00:00", + "phase": "refine" + }, + { + "id": "b9d65e35-d77e-46", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:27:51.490323+00:00", + "phase": "refine" + }, + { + "id": "9687073e-8cc1-4a", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:28:03.519310+00:00", + "phase": "refine" + }, + { + "id": "29eaa948-53c4-4a", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:28:51.554993+00:00", + "phase": "refine" + }, + { + "id": "875016a4-19d4-4e", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:29:03.622460+00:00", + "phase": "refine" + }, + { + "id": "c9a01a04-6c04-47", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WAITING_FOR_EVENT", + "body": "Overseer cycle 24: 523s elapsed, all agents running. Refiner WORKING (no propose yet). Reviewers heartbeating normally. Refreshing overseer heartbeat to stay under 120s threshold.", + "metadata": { + "state": "WAITING_FOR_EVENT", + "since": "2026-05-22T01:20:46.000000+00:00" + }, + "timestamp": "2026-05-22T01:29:48.456895+00:00", + "phase": "refine" + }, + { + "id": "8c5fd16d-68ff-46", + "pipeline_id": "issue-2769", + "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-22T01:21:28.083399+00:00" + }, + "timestamp": "2026-05-22T01:29:51.634526+00:00", + "phase": "refine" + }, + { + "id": "d9a33a4b-4697-40", + "pipeline_id": "issue-2769", + "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-22T01:23:02.990597+00:00" + }, + "timestamp": "2026-05-22T01:30:03.701184+00:00", + "phase": "refine" + }, + { + "id": "4bbb1f0c-c54a-45", + "pipeline_id": "issue-2769", + "from_role": "refiner", + "to_role": "all", + "message_type": "CONSENSUS_PROPOSE", + "subject": "Proposal from refiner", + "body": "Analysis draft for #2769 (non-Claude models per agent via LiteLLM proxy). Recommends Option A: gateway-side upstream router keyed by IP-based session metadata, with LiteLLM as a separate Deployment in egg-system, Claude path structurally unchanged, and inert-by-default behavior. Surfaces the Claude Code auto-compaction risk as the primary validation gate and proposes the recognized-alias mitigation. Inventories the runtime primitives the plan phase will depend on (file:line evidence for get_anthropic_client, proxy_anthropic_messages, _inject_anthropic_credentials, _SSEAccumulator, _filter_blocked_tools, session_manager.get_session_by_ip, build_consensus_wrapped_command's hardcoded `model=\"opus\"`, the per-overseer model fields on PipelineConfig, _PROTECTED_ENV_KEYS, the ANTHROPIC_BASE_URL spawn-time injection, and the three sites that bake `opus[1m]`). Registers 11 HITL decisions (cq-1..cq-11) covering LiteLLM topology, routing signal, per-agent model config shape, acceptance-test role, harness choice, target backend, credential handling, failure policy, private-mode tool-strip policy, slice decomposition, and the `[1m]` syntax; plus 5 open-ended feedback questions on Qwen hardware, target role list, swap-out interface, cost tracking, and compliance/data-residency. Complexity assessed as high. No `## Additional Context` was present in the issue body, so nothing is pre-resolved.", + "metadata": { + "payload": { + "summary": "Analysis draft for #2769 (non-Claude models per agent via LiteLLM proxy). Recommends Option A: gateway-side upstream router keyed by IP-based session metadata, with LiteLLM as a separate Deployment in egg-system, Claude path structurally unchanged, and inert-by-default behavior. Surfaces the Claude Code auto-compaction risk as the primary validation gate and proposes the recognized-alias mitigation. Inventories the runtime primitives the plan phase will depend on (file:line evidence for get_anthropic_client, proxy_anthropic_messages, _inject_anthropic_credentials, _SSEAccumulator, _filter_blocked_tools, session_manager.get_session_by_ip, build_consensus_wrapped_command's hardcoded `model=\"opus\"`, the per-overseer model fields on PipelineConfig, _PROTECTED_ENV_KEYS, the ANTHROPIC_BASE_URL spawn-time injection, and the three sites that bake `opus[1m]`). Registers 11 HITL decisions (cq-1..cq-11) covering LiteLLM topology, routing signal, per-agent model config shape, acceptance-test role, harness choice, target backend, credential handling, failure policy, private-mode tool-strip policy, slice decomposition, and the `[1m]` syntax; plus 5 open-ended feedback questions on Qwen hardware, target role list, swap-out interface, cost tracking, and compliance/data-residency. Complexity assessed as high. No `## Additional Context` was present in the issue body, so nothing is pre-resolved.", + "attestation": {}, + "artifacts": [ + ".egg-state/drafts/2769-analysis.md" + ], + "risk_considered": "Primary risk surface is Claude Code's model-name-derived auto-compaction (raised in the issue and verified via web research on Claude Code's compaction docs). Mitigation proposed: present Claude Code a recognized alias and route on a separate per-agent signal (session metadata). Secondary risks: LiteLLM supply-chain (March 2026 PyPI incident \u2014 addressed via cosign verification and a swap-out interface question in feedback), credential/topology choice (deferred to HITL), and per-agent config drift between orchestrator-declared upstream and agent-passed model (mitigation: spawner derives both from one config field).", + "commit_sha": "9dc90338c", + "files_changed": [ + ".egg-state/drafts/2769-analysis.md" + ], + "tests_run": [], + "tasks_satisfied": [] + }, + "version": 1, + "commit_sha": "9dc90338c" + }, + "timestamp": "2026-05-22T01:30:42.211691+00:00", + "phase": "refine" + }, + { + "id": "5331ff20-5d25-49", + "pipeline_id": "issue-2769", + "from_role": "reviewer_agent_design", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T01:30:42.299767+00:00", + "phase": "refine" + }, + { + "id": "a142d5ea-659a-48", + "pipeline_id": "issue-2769", + "from_role": "reviewer_refine", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T01:30:42.306337+00:00", + "phase": "refine" + }, + { + "id": "16e9ec0b-72ad-44", + "pipeline_id": "issue-2769", + "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-22T01:30:47.149619+00:00" + }, + "timestamp": "2026-05-22T01:31:03.905476+00:00", + "phase": "refine" + }, + { + "id": "dc719d78-a30b-49", + "pipeline_id": "issue-2769", + "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-22T01:30:47.149619+00:00" + }, + "timestamp": "2026-05-22T01:32:02.261852+00:00", + "phase": "refine" + }, + { + "id": "7bcb2661-22ce-4d", + "pipeline_id": "issue-2769", + "from_role": "overseer", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WAITING_FOR_EVENT", + "body": "Overseer cycle 29: BRC review round active. refiner PROPOSED (v1), reviewer_agent_design and reviewer_refine WORKING on proposal. Awaiting ACK/NACK.", + "metadata": { + "state": "WAITING_FOR_EVENT", + "since": "2026-05-22T01:20:46.000000+00:00" + }, + "timestamp": "2026-05-22T01:32:05.219712+00:00", + "phase": "refine" + }, + { + "id": "3b63d219-a510-4b", + "pipeline_id": "issue-2769", + "from_role": "reviewer_agent_design", + "to_role": "refiner", + "message_type": "CONSENSUS_ACK", + "subject": "ACK from reviewer_agent_design for refiner", + "body": "Reviewed `.egg-state/drafts/2769-analysis.md` (344 lines) against the agent-mode design rubric in `docs/guides/agent-mode-design.md`. Cross-checked the cited integration points (`gateway/gateway.py:9316-9329`, `:9355`, `:9410`, `:9552`, `:9752`, `:9774`, `:10019`; `orchestrator/consensus_wrapper.py:620-662`; `orchestrator/kubernetes_spawner.py:807`; `shared/egg_agent/client.py:62`; `shared/egg_agent/__main__.py:35`; `sandbox/llm/runner.py:49`) \u2014 primitive references and line numbers are accurate.\n\n**No blocking agent-design issues found.** The recommended Option A is structurally consistent with every agent-mode invariant we care about:\n\n1. **EGG200 (no direct LLM calls outside sandbox)** \u2014 preserved. The design extends the *existing* gateway proxy (already `noqa: EGG200` as legitimate proxy infrastructure) rather than introducing new `httpx`/Anthropic SDK call sites in orchestrator/shared. LiteLLM is reached *from the gateway*, not from agents or orchestrator code. The constraint \"LiteLLM must not be directly reachable from sandbox pods\" (line 66) and the Squid-allowlist-exclusion note (line 67) explicitly reinforce this.\n\n2. **EGG201 (model aliases, not pinned IDs)** \u2014 explicitly honored: \"No pinned model snapshot versions. Use model aliases (e.g. `opus`, `sonnet`, `qwen3-coder-30b`)\" (line 69). cq-11 surfaces the `opus[1m]` Claude-only suffix cleanly as a decision rather than silently leaving it baked in.\n\n3. **Agent SDK path preserved** \u2014 Option D (bypass Claude Code via `egg_agent.client.run_agent()`) is correctly identified as the SDK path already taking `--model` (line 124), and the recommended Option A retains the existing Claude Code harness for the no-op default. No new harness-bypass code is introduced.\n\n4. **Gateway as the per-request policy point** \u2014 Option A routes via per-agent session metadata (lookup-by-IP, same mechanism as `session_mode` today, line 77); the SSE accumulator, tool-strip, and credential injection sit *above* the upstream selection and stay upstream-agnostic. This is exactly the right factoring per the design guide's \"minimal intermediation\" principle \u2014 the gateway adds one indirection (upstream registry) without inserting itself into request semantics.\n\n5. **No pre-fetched content baked into prompts, no JSON-for-humans, no post-processing pipeline, no rigid procedures, no prompt-level security used as a substitute for sandbox enforcement** \u2014 none of these anti-patterns appear in the design.\n\n6. **The \"present Claude Code a recognized alias while routing on session metadata\" mitigation** (lines 79, 84, 138, cq-2 option 1) is a Claude-Code-compatibility workaround, not an agent-design concern: the *agent itself* sees and uses the real backend; only Claude Code's internal compaction-bookkeeping sees the alias. The agent isn't constrained from doing anything it could otherwise do.\n\n7. **Option B rejection (LiteLLM-fronts-everything)** correctly identifies that adding an LLM-translation hop to the Claude path would (a) violate the no-regression constraint and (b) make compaction-math worse, not better. Option C rejection correctly identifies the body-routing conflict with the recognized-alias mitigation. The option analysis is honest.\n\n### Non-blocking\n- **cq-11 framing** \u2014 option (b) (\"hoist into a single config helper that strips `[1m]` when the resolved upstream is non-Claude\") is the most agent-design-aligned answer: it keeps model strings backend-agnostic at the API boundary and isolates the Claude-only suffix to a single resolution point. Worth flagging this in the operator's decision context if the producer revises.\n- **cq-7 option (c)** (\"sandbox sets its own per-agent API key via `extra_env`\") is correctly flagged as a \"probably non-starter\" because it inverts the zero-credential sandbox invariant. Good \u2014 this is the right call from an agent-mode-security perspective and should stay weighted against in any plan-phase refinement.\n- **The runtime-primitive table (lines 145-172)** is the right shape for handing the plan phase exact anchors; nothing for the design reviewer to flag, just noting it's well-suited to keep the plan agent grounded without baking diffs into its prompt.", + "metadata": { + "payload": { + "artifact_references": [ + ".egg-state/drafts/2769-analysis.md" + ], + "reason": "Reviewed `.egg-state/drafts/2769-analysis.md` (344 lines) against the agent-mode design rubric in `docs/guides/agent-mode-design.md`. Cross-checked the cited integration points (`gateway/gateway.py:9316-9329`, `:9355`, `:9410`, `:9552`, `:9752`, `:9774`, `:10019`; `orchestrator/consensus_wrapper.py:620-662`; `orchestrator/kubernetes_spawner.py:807`; `shared/egg_agent/client.py:62`; `shared/egg_agent/__main__.py:35`; `sandbox/llm/runner.py:49`) \u2014 primitive references and line numbers are accurate.\n\n**No blocking agent-design issues found.** The recommended Option A is structurally consistent with every agent-mode invariant we care about:\n\n1. **EGG200 (no direct LLM calls outside sandbox)** \u2014 preserved. The design extends the *existing* gateway proxy (already `noqa: EGG200` as legitimate proxy infrastructure) rather than introducing new `httpx`/Anthropic SDK call sites in orchestrator/shared. LiteLLM is reached *from the gateway*, not from agents or orchestrator code. The constraint \"LiteLLM must not be directly reachable from sandbox pods\" (line 66) and the Squid-allowlist-exclusion note (line 67) explicitly reinforce this.\n\n2. **EGG201 (model aliases, not pinned IDs)** \u2014 explicitly honored: \"No pinned model snapshot versions. Use model aliases (e.g. `opus`, `sonnet`, `qwen3-coder-30b`)\" (line 69). cq-11 surfaces the `opus[1m]` Claude-only suffix cleanly as a decision rather than silently leaving it baked in.\n\n3. **Agent SDK path preserved** \u2014 Option D (bypass Claude Code via `egg_agent.client.run_agent()`) is correctly identified as the SDK path already taking `--model` (line 124), and the recommended Option A retains the existing Claude Code harness for the no-op default. No new harness-bypass code is introduced.\n\n4. **Gateway as the per-request policy point** \u2014 Option A routes via per-agent session metadata (lookup-by-IP, same mechanism as `session_mode` today, line 77); the SSE accumulator, tool-strip, and credential injection sit *above* the upstream selection and stay upstream-agnostic. This is exactly the right factoring per the design guide's \"minimal intermediation\" principle \u2014 the gateway adds one indirection (upstream registry) without inserting itself into request semantics.\n\n5. **No pre-fetched content baked into prompts, no JSON-for-humans, no post-processing pipeline, no rigid procedures, no prompt-level security used as a substitute for sandbox enforcement** \u2014 none of these anti-patterns appear in the design.\n\n6. **The \"present Claude Code a recognized alias while routing on session metadata\" mitigation** (lines 79, 84, 138, cq-2 option 1) is a Claude-Code-compatibility workaround, not an agent-design concern: the *agent itself* sees and uses the real backend; only Claude Code's internal compaction-bookkeeping sees the alias. The agent isn't constrained from doing anything it could otherwise do.\n\n7. **Option B rejection (LiteLLM-fronts-everything)** correctly identifies that adding an LLM-translation hop to the Claude path would (a) violate the no-regression constraint and (b) make compaction-math worse, not better. Option C rejection correctly identifies the body-routing conflict with the recognized-alias mitigation. The option analysis is honest.\n\n### Non-blocking\n- **cq-11 framing** \u2014 option (b) (\"hoist into a single config helper that strips `[1m]` when the resolved upstream is non-Claude\") is the most agent-design-aligned answer: it keeps model strings backend-agnostic at the API boundary and isolates the Claude-only suffix to a single resolution point. Worth flagging this in the operator's decision context if the producer revises.\n- **cq-7 option (c)** (\"sandbox sets its own per-agent API key via `extra_env`\") is correctly flagged as a \"probably non-starter\" because it inverts the zero-credential sandbox invariant. Good \u2014 this is the right call from an agent-mode-security perspective and should stay weighted against in any plan-phase refinement.\n- **The runtime-primitive table (lines 145-172)** is the right shape for handing the plan phase exact anchors; nothing for the design reviewer to flag, just noting it's well-suited to keep the plan agent grounded without baking diffs into its prompt.", + "ack_version": 1 + }, + "version": 1 + }, + "timestamp": "2026-05-22T01:32:31.322082+00:00", + "phase": "refine" + }, + { + "id": "3f2d1950-7a72-4c", + "pipeline_id": "issue-2769", + "from_role": "refiner", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T01:32:31.371159+00:00", + "phase": "refine" + }, + { + "id": "3c2f19cf-59a4-46", + "pipeline_id": "issue-2769", + "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-22T01:32:34.264676+00:00", + "phase": "refine" + }, + { + "id": "2dad4f29-8b4e-43", + "pipeline_id": "issue-2769", + "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-22T01:32:39.055879+00:00" + }, + "timestamp": "2026-05-22T01:32:39.089009+00:00", + "phase": "refine" + }, + { + "id": "1536be5b-2cc0-46", + "pipeline_id": "issue-2769", + "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-22T01:32:47.754773+00:00" + }, + "timestamp": "2026-05-22T01:32:47.807777+00:00", + "phase": "refine" + }, + { + "id": "07e2ef20-518d-4c", + "pipeline_id": "issue-2769", + "from_role": "reviewer_refine", + "to_role": "refiner", + "message_type": "CONSENSUS_ACK", + "subject": "ACK from reviewer_refine for refiner", + "body": "\nReviewed the refine analysis at `.egg-state/drafts/2769-analysis.md` (commit 9dc90338c) against all seven review-criteria sections, plus cross-checked every cited file/line against the working tree, plus verified the contract scaffolding produced the expected 11 decisions + 5 feedback questions.\n\n## Section-by-Section Evaluation\n\n### 1. Problem Understanding \u2014 strong\nThe Problem Statement (lines 5-13) correctly extracts all three hard requirements from the issue body (parallel Claude + non-Claude, per-agent independent selectability, no-regression on the Claude path) and names the architectural intent (gateway-as-router, LiteLLM as translation layer). The side-benefit (harness decoupling) and the rejection rationale for `claude-code-router` are preserved. The supply-chain footnote on the March 2026 LiteLLM PyPI incident (line 13) is a useful unprompted add \u2014 it surfaces a real risk without overweighting it.\n\n### 2. Research Quality \u2014 exemplary\nSpot-checked the 22 entries in the Runtime-primitives table against the working tree:\n- `get_anthropic_client` cited at gateway.py:9316-9329 \u2014 actual definition spans 9317 (singleton decl) to 9329 (return), match.\n- `_inject_anthropic_credentials` at :9355 \u2014 verified.\n- `_filter_blocked_tools` at :9410 \u2014 verified.\n- `_SSEAccumulator` at :9552 \u2014 verified.\n- `get_session_by_ip` lookup in `proxy_anthropic_messages` \u2014 verified at gateway.py:9775 (draft says 9774, off-by-1).\n- `proxy_anthropic_messages` \u2014 actual `def` is at gateway.py:9753 (draft says 9752, off-by-1 \u2014 the `@app.route` decorator is on the prior line).\n- `proxy_count_tokens` \u2014 actual at gateway.py:10020 (draft says 10019, same off-by-1).\n- `overseer_decision_maker_model` / `overseer_advisor_model` Field decls at orchestrator/models.py:546 and :620 \u2014 verified.\n- `build_consensus_wrapped_command(model=\"opus\", ...)` at orchestrator/consensus_wrapper.py:620-622 with `--model` at :658 \u2014 verified, falls inside the cited 653-662 range.\n- Call sites at concurrent_executor.py:454 and routes/pipelines.py:2704 with no model arg \u2014 verified, confirming the \"every non-overseer agent is opus-only by hardcoding\" claim.\n- `DEFAULT_MODEL = \"opus[1m]\"` at shared/egg_agent/client.py:62 \u2014 verified.\n- `parser.add_argument(\"--model\", default=\"opus[1m]\", ...)` at shared/egg_agent/__main__.py:35 \u2014 verified.\n- `cmd.extend([\"--model\", \"opus[1m]\"])` at sandbox/llm/runner.py:49 \u2014 verified.\n- `ANTHROPIC_BASE_URL=GATEWAY_K8S_URL` at orchestrator/kubernetes_spawner.py:807 \u2014 verified.\n- `GATEWAY_K8S_URL` declaration at kubernetes_spawner.py:124 \u2014 verified.\n- `_PROTECTED_ENV_KEYS` at kubernetes_spawner.py:138 \u2014 verified.\n- `setup_anthropic_api` in sandbox/entrypoint.py:712 sets `ANTHROPIC_BASE_URL` at :738 \u2014 verified (draft says 737-738, close).\n- `allowed_domains.txt` Anthropic-excluded comment block \u2014 verified at lines 9-15, says explicitly \"api.anthropic.com is intentionally NOT in this allowlist\".\n\nThe depth of citation (function + line + role in the request lifecycle) is well above the bar for a refine artifact and will give the planner a sturdy anchor to write tasks against.\n\n### 3. Options Analysis \u2014 well-decomposed\nFour options (A: gateway router + session metadata; B: LiteLLM-fronts-everything; C: route on request-body model name; D: egg_agent SDK bypass). They are meaningfully different along the right axes (where the routing decision lives, what changes on the Claude path, how the compaction-math mitigation gets supported, what new harness surface gets introduced). The pro/con bullets for B and C are explicit about which constraint each fails \u2014 B fails the \"no regression on the Claude path\" gate; C fails the compaction-mitigation requirement that Claude Code be shown a recognized alias even when the backend is Qwen. The reasoning is auditable.\n\n### 4. Constraints and Dependencies \u2014 comprehensive\nConstraints section (lines 61-71) enumerates: no Claude-path regression, routing-point placement below SSE accumulator + tool-filter + stream resilience, zero-credential sandbox invariant, gateway-mediated visibility, Squid network policy, per-agent independence, model-alias-only (no snapshot pins), file-size discipline against the 1500-line / 100KB cap, and the build-now / validate-later split. The runtime-primitives table doubles as a dependency graph for the planner. Primary risk (Claude Code's compaction math driving auto-compact on unrecognized models, lines 49-59) is well-explained with two external references and a concrete mitigation pointer.\n\n### 5. Open Questions \u2014 actionable + properly scaffolded\n11 decisions (cq-1 through cq-11) and 5 feedback questions (Q1-Q5) cover, with no obvious gaps I can identify:\n- topology (cq-1: deployment vs sidecar vs separate ns)\n- routing signal (cq-2: session metadata vs header vs body)\n- config shape (cq-3: PipelineConfig field vs repo YAML vs CLI vs stacked precedence)\n- validation target (cq-4: which role flips first)\n- harness choice (cq-5: Claude Code vs egg_agent SDK)\n- backend (cq-6: self-hosted Qwen vs hosted Qwen vs OpenAI smoke test)\n- credentials (cq-7: gateway-held vs gateway-passthrough vs sandbox-held)\n- failure policy (cq-8: fail-closed vs Claude-fallback vs HITL-on-failure)\n- private-mode tool-strip (cq-9: keep vs upstream-aware vs document-and-defer)\n- slice decomposition (cq-10: single PR vs parallel vs dependent)\n- `[1m]` syntax handling (cq-11: leave vs refactor vs deprecate)\n\nPlus 5 feedback questions covering Qwen hardware/budget, target role list, swap-out interface for LiteLLM, cost tracking extension, and compliance/data-residency. Verified contract state via mcp__sdlc__show_contract: all 11 decisions present with their full option lists, feedback Q1-Q5 present and unresolved. The \"Resolved in Pre-Refine\" section is correctly empty (the issue has no `## Additional Context` block, which the draft accurately notes at line 177). No silent assumptions detected.\n\n### 6. Recommendation Quality \u2014 clear and justified\nOption A is recommended with four specific justifications (lines 135-141) that map back to the constraints. The recommendation is conditional on the operator answering cq-1 through cq-11, which is the correct posture for refine: the architectural shape is recommended, the topology / config-shape / first-target details are surfaced for human resolution rather than presumed.\n\n### 7. HITL Decision Registration \u2014 properly scaffolded\nCross-checked the `` markers in the draft (lines 181, 190, 199, 209, 219, 228, 237, 246, 255, 264, 274) against the contract JSON: all 11 decisions exist with `phase: \"refine\"`, `type: \"hitl\"`, `resolved: false`, and the option labels in the contract match the bullet labels in the draft. The `` marker (line 285) produced the feedback bundle with Q1-Q5 \u2014 verified all five questions are present in `contract.feedback.questions` with `answer: null` and `submitted: false`. The draft is not proceeding on any unvalidated silent assumption.\n\n## Non-blocking notes\n\n- **gateway/gateway.py line numbers off-by-1** \u2014 `proxy_anthropic_messages` is cited as `gateway/gateway.py:9752` (lines 32, 152) but actually starts at 9753 (the `@app.route` decorator is on 9752); `proxy_count_tokens` cited at :10019 is actually at :10020; `get_session_by_ip` call cited at :9774 is at :9775. The planner will not be misled (the surrounding context and symbol names are correct), but worth a one-line fix-up if there's a re-propose for another reason. Not blocking on its own.\n- **kubernetes_spawner.py:1582 in the Current-Behavior table (line 38)** \u2014 cited as a usage site for `overseer_decision_maker_model` (Tier-2), but line 1582 is actually inside the docstring (`decision_model: LLM model for overseer decisions.`); the real usage sites are :1596 (env var) and :1623 (constructor kwarg). The Runtime-primitives table (line 162) cites :1596 correctly, so this is just a minor inconsistency between the two tables.\n- **Supply-chain mitigation seam (Q3-adjacent)** \u2014 The draft's supply-chain caveat at line 13 and feedback Q3 frame this well, but consider whether the recommended Option A description (line 77) should explicitly call out that `UpstreamRegistry` is the natural seam where a swap-out interface lives. The plan can derive this from Q3, but a one-liner in the recommendation would harden the link.\n- **`[1m]` triple-bake risk (cq-11)** \u2014 Worth noting non-blockingly that even on option-A \"leave it\", any code path that builds an agent invocation and forgets to strip `[1m]` for non-Claude routes is a latent foot-gun. cq-11 is the right place to surface this; just flagging that the answer may have implications for how the plan structures the consensus-wrapper signature change.\n- **Refine cycle counter** \u2014 `contract.refine_review_cycles == 0`, consistent with this being cycle 1.\n\nOverall verdict: this is a high-quality refine artifact. Citations are accurate to the line, constraints are exhaustive, options are meaningfully different and clearly trade-off-justified, the recommended approach maps back to the hard constraints, and all open questions are properly scaffolded into the contract for the operator. The few off-by-1s and the docstring-vs-usage cite for kubernetes_spawner.py:1582 are cosmetic. Ready to advance.\n", + "metadata": { + "payload": { + "artifact_references": [ + ".egg-state/drafts/2769-analysis.md", + ".egg-state/contracts/issue-2769.json", + "gateway/gateway.py", + "orchestrator/models.py", + "orchestrator/consensus_wrapper.py", + "orchestrator/concurrent_executor.py", + "orchestrator/routes/pipelines.py", + "orchestrator/kubernetes_spawner.py", + "shared/egg_agent/client.py", + "shared/egg_agent/__main__.py", + "sandbox/llm/runner.py", + "sandbox/entrypoint.py", + "gateway/allowed_domains.txt", + "k8s/base/gateway-deployment.yaml" + ], + "reason": "\nReviewed the refine analysis at `.egg-state/drafts/2769-analysis.md` (commit 9dc90338c) against all seven review-criteria sections, plus cross-checked every cited file/line against the working tree, plus verified the contract scaffolding produced the expected 11 decisions + 5 feedback questions.\n\n## Section-by-Section Evaluation\n\n### 1. Problem Understanding \u2014 strong\nThe Problem Statement (lines 5-13) correctly extracts all three hard requirements from the issue body (parallel Claude + non-Claude, per-agent independent selectability, no-regression on the Claude path) and names the architectural intent (gateway-as-router, LiteLLM as translation layer). The side-benefit (harness decoupling) and the rejection rationale for `claude-code-router` are preserved. The supply-chain footnote on the March 2026 LiteLLM PyPI incident (line 13) is a useful unprompted add \u2014 it surfaces a real risk without overweighting it.\n\n### 2. Research Quality \u2014 exemplary\nSpot-checked the 22 entries in the Runtime-primitives table against the working tree:\n- `get_anthropic_client` cited at gateway.py:9316-9329 \u2014 actual definition spans 9317 (singleton decl) to 9329 (return), match.\n- `_inject_anthropic_credentials` at :9355 \u2014 verified.\n- `_filter_blocked_tools` at :9410 \u2014 verified.\n- `_SSEAccumulator` at :9552 \u2014 verified.\n- `get_session_by_ip` lookup in `proxy_anthropic_messages` \u2014 verified at gateway.py:9775 (draft says 9774, off-by-1).\n- `proxy_anthropic_messages` \u2014 actual `def` is at gateway.py:9753 (draft says 9752, off-by-1 \u2014 the `@app.route` decorator is on the prior line).\n- `proxy_count_tokens` \u2014 actual at gateway.py:10020 (draft says 10019, same off-by-1).\n- `overseer_decision_maker_model` / `overseer_advisor_model` Field decls at orchestrator/models.py:546 and :620 \u2014 verified.\n- `build_consensus_wrapped_command(model=\"opus\", ...)` at orchestrator/consensus_wrapper.py:620-622 with `--model` at :658 \u2014 verified, falls inside the cited 653-662 range.\n- Call sites at concurrent_executor.py:454 and routes/pipelines.py:2704 with no model arg \u2014 verified, confirming the \"every non-overseer agent is opus-only by hardcoding\" claim.\n- `DEFAULT_MODEL = \"opus[1m]\"` at shared/egg_agent/client.py:62 \u2014 verified.\n- `parser.add_argument(\"--model\", default=\"opus[1m]\", ...)` at shared/egg_agent/__main__.py:35 \u2014 verified.\n- `cmd.extend([\"--model\", \"opus[1m]\"])` at sandbox/llm/runner.py:49 \u2014 verified.\n- `ANTHROPIC_BASE_URL=GATEWAY_K8S_URL` at orchestrator/kubernetes_spawner.py:807 \u2014 verified.\n- `GATEWAY_K8S_URL` declaration at kubernetes_spawner.py:124 \u2014 verified.\n- `_PROTECTED_ENV_KEYS` at kubernetes_spawner.py:138 \u2014 verified.\n- `setup_anthropic_api` in sandbox/entrypoint.py:712 sets `ANTHROPIC_BASE_URL` at :738 \u2014 verified (draft says 737-738, close).\n- `allowed_domains.txt` Anthropic-excluded comment block \u2014 verified at lines 9-15, says explicitly \"api.anthropic.com is intentionally NOT in this allowlist\".\n\nThe depth of citation (function + line + role in the request lifecycle) is well above the bar for a refine artifact and will give the planner a sturdy anchor to write tasks against.\n\n### 3. Options Analysis \u2014 well-decomposed\nFour options (A: gateway router + session metadata; B: LiteLLM-fronts-everything; C: route on request-body model name; D: egg_agent SDK bypass). They are meaningfully different along the right axes (where the routing decision lives, what changes on the Claude path, how the compaction-math mitigation gets supported, what new harness surface gets introduced). The pro/con bullets for B and C are explicit about which constraint each fails \u2014 B fails the \"no regression on the Claude path\" gate; C fails the compaction-mitigation requirement that Claude Code be shown a recognized alias even when the backend is Qwen. The reasoning is auditable.\n\n### 4. Constraints and Dependencies \u2014 comprehensive\nConstraints section (lines 61-71) enumerates: no Claude-path regression, routing-point placement below SSE accumulator + tool-filter + stream resilience, zero-credential sandbox invariant, gateway-mediated visibility, Squid network policy, per-agent independence, model-alias-only (no snapshot pins), file-size discipline against the 1500-line / 100KB cap, and the build-now / validate-later split. The runtime-primitives table doubles as a dependency graph for the planner. Primary risk (Claude Code's compaction math driving auto-compact on unrecognized models, lines 49-59) is well-explained with two external references and a concrete mitigation pointer.\n\n### 5. Open Questions \u2014 actionable + properly scaffolded\n11 decisions (cq-1 through cq-11) and 5 feedback questions (Q1-Q5) cover, with no obvious gaps I can identify:\n- topology (cq-1: deployment vs sidecar vs separate ns)\n- routing signal (cq-2: session metadata vs header vs body)\n- config shape (cq-3: PipelineConfig field vs repo YAML vs CLI vs stacked precedence)\n- validation target (cq-4: which role flips first)\n- harness choice (cq-5: Claude Code vs egg_agent SDK)\n- backend (cq-6: self-hosted Qwen vs hosted Qwen vs OpenAI smoke test)\n- credentials (cq-7: gateway-held vs gateway-passthrough vs sandbox-held)\n- failure policy (cq-8: fail-closed vs Claude-fallback vs HITL-on-failure)\n- private-mode tool-strip (cq-9: keep vs upstream-aware vs document-and-defer)\n- slice decomposition (cq-10: single PR vs parallel vs dependent)\n- `[1m]` syntax handling (cq-11: leave vs refactor vs deprecate)\n\nPlus 5 feedback questions covering Qwen hardware/budget, target role list, swap-out interface for LiteLLM, cost tracking extension, and compliance/data-residency. Verified contract state via mcp__sdlc__show_contract: all 11 decisions present with their full option lists, feedback Q1-Q5 present and unresolved. The \"Resolved in Pre-Refine\" section is correctly empty (the issue has no `## Additional Context` block, which the draft accurately notes at line 177). No silent assumptions detected.\n\n### 6. Recommendation Quality \u2014 clear and justified\nOption A is recommended with four specific justifications (lines 135-141) that map back to the constraints. The recommendation is conditional on the operator answering cq-1 through cq-11, which is the correct posture for refine: the architectural shape is recommended, the topology / config-shape / first-target details are surfaced for human resolution rather than presumed.\n\n### 7. HITL Decision Registration \u2014 properly scaffolded\nCross-checked the `` markers in the draft (lines 181, 190, 199, 209, 219, 228, 237, 246, 255, 264, 274) against the contract JSON: all 11 decisions exist with `phase: \"refine\"`, `type: \"hitl\"`, `resolved: false`, and the option labels in the contract match the bullet labels in the draft. The `` marker (line 285) produced the feedback bundle with Q1-Q5 \u2014 verified all five questions are present in `contract.feedback.questions` with `answer: null` and `submitted: false`. The draft is not proceeding on any unvalidated silent assumption.\n\n## Non-blocking notes\n\n- **gateway/gateway.py line numbers off-by-1** \u2014 `proxy_anthropic_messages` is cited as `gateway/gateway.py:9752` (lines 32, 152) but actually starts at 9753 (the `@app.route` decorator is on 9752); `proxy_count_tokens` cited at :10019 is actually at :10020; `get_session_by_ip` call cited at :9774 is at :9775. The planner will not be misled (the surrounding context and symbol names are correct), but worth a one-line fix-up if there's a re-propose for another reason. Not blocking on its own.\n- **kubernetes_spawner.py:1582 in the Current-Behavior table (line 38)** \u2014 cited as a usage site for `overseer_decision_maker_model` (Tier-2), but line 1582 is actually inside the docstring (`decision_model: LLM model for overseer decisions.`); the real usage sites are :1596 (env var) and :1623 (constructor kwarg). The Runtime-primitives table (line 162) cites :1596 correctly, so this is just a minor inconsistency between the two tables.\n- **Supply-chain mitigation seam (Q3-adjacent)** \u2014 The draft's supply-chain caveat at line 13 and feedback Q3 frame this well, but consider whether the recommended Option A description (line 77) should explicitly call out that `UpstreamRegistry` is the natural seam where a swap-out interface lives. The plan can derive this from Q3, but a one-liner in the recommendation would harden the link.\n- **`[1m]` triple-bake risk (cq-11)** \u2014 Worth noting non-blockingly that even on option-A \"leave it\", any code path that builds an agent invocation and forgets to strip `[1m]` for non-Claude routes is a latent foot-gun. cq-11 is the right place to surface this; just flagging that the answer may have implications for how the plan structures the consensus-wrapper signature change.\n- **Refine cycle counter** \u2014 `contract.refine_review_cycles == 0`, consistent with this being cycle 1.\n\nOverall verdict: this is a high-quality refine artifact. Citations are accurate to the line, constraints are exhaustive, options are meaningfully different and clearly trade-off-justified, the recommended approach maps back to the hard constraints, and all open questions are properly scaffolded into the contract for the operator. The few off-by-1s and the docstring-vs-usage cite for kubernetes_spawner.py:1582 are cosmetic. Ready to advance.\n", + "ack_version": 1 + }, + "version": 1 + }, + "timestamp": "2026-05-22T01:33:32.515448+00:00", + "phase": "refine" + }, + { + "id": "dee84602-f761-45", + "pipeline_id": "issue-2769", + "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-22T01:33:32.515525+00:00", + "phase": "refine" + }, + { + "id": "10b1fc6c-c5cb-42", + "pipeline_id": "issue-2769", + "from_role": "refiner", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T01:33:32.589510+00:00", + "phase": "refine" + }, + { + "id": "af619638-534d-45", + "pipeline_id": "issue-2769", + "from_role": "refiner", + "to_role": "all", + "message_type": "CONSENSUS_CONFIRMED", + "subject": "Confirmed by refiner", + "body": "", + "metadata": { + "consensus_reached": false + }, + "timestamp": "2026-05-22T01:33:35.926960+00:00", + "phase": "refine" + }, + { + "id": "c06b8ad9-bf0a-47", + "pipeline_id": "issue-2769", + "from_role": "reviewer_agent_design", + "to_role": "all", + "message_type": "HEARTBEAT", + "subject": "heartbeat: WORKING", + "body": "wait_loop exited", + "metadata": { + "state": "WORKING" + }, + "timestamp": "2026-05-22T01:33:35.997285+00:00", + "phase": "refine" + }, + { + "id": "05db1873-72ea-4f", + "pipeline_id": "issue-2769", + "from_role": "reviewer_refine", + "to_role": "all", + "message_type": "CONSENSUS_CONFIRMED", + "subject": "Confirmed by reviewer_refine", + "body": "", + "metadata": { + "consensus_reached": true + }, + "timestamp": "2026-05-22T01:33:38.592832+00:00", + "phase": "refine" + }, + { + "id": "d7d1a265-7da8-4a", + "pipeline_id": "issue-2769", + "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-22T01:33:40.083389+00:00" + }, + "timestamp": "2026-05-22T01:33:40.124279+00:00", + "phase": "refine" + }, + { + "id": "a9e6805b-0c48-4b", + "pipeline_id": "issue-2769", + "from_role": "refiner", + "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-22T01:33:41.691260+00:00" + }, + "timestamp": "2026-05-22T01:33:41.738308+00:00", + "phase": "refine" + } +] \ No newline at end of file diff --git a/.egg-state/brc-history/2769-refine.md b/.egg-state/brc-history/2769-refine.md new file mode 100644 index 0000000000..461a6c46f2 --- /dev/null +++ b/.egg-state/brc-history/2769-refine.md @@ -0,0 +1,790 @@ +# BRC Consensus History — refine phase + +Generated: 2026-05-22T01:33:41Z +Pipeline: issue-2769 + +### [2026-05-22T01:21:36Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: d62abdd5-907c-40 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:22:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 645d0ba8-dd58-4a +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:23:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: a6f2fafc-69bb-43 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:23:26Z] overseer (OVERSEER_ALERT): agent-heartbeat-stall [high] + +Refine-phase agents never launched for pipeline issue-2769 — zero executions, zero proposals, zero heartbeats after pipeline start + +Detail: +Pipeline issue-2769 is in the refine phase (cycle 0). The contract shows agent_executions=[], BRC history is empty (0 items), and no checkpoints exist. BRC state lists refiner/reviewer_agent_design/reviewer_refine all as WORKING with no proposals. The orchestrator is healthy (uptime ~4600s, state stores OK). The pipeline_unreachable alert from the monitor was a long-poll timeout on /api/v1/pipelines/issue-2769/status — not an orchestrator outage. The real issue is that zero agents have been dispatched. The pipeline may have been registered without triggering agent launch. + +Recommended action: +Operator should verify agent dispatch for issue-2769 refine phase. Options: (1) re-trigger the pipeline via egg-orch or the webhook endpoint, (2) manually launch the refiner agent, or (3) inspect the orchestrator logs for dispatch failures around the pipeline registration time (~2026-05-22T00:05Z). + +````yaml +id: 171c4e9a-d281-4f +phase: refine +```` + +### [2026-05-22T01:23:31Z] overseer (HEARTBEAT): heartbeat: WORKING + +Monitoring cycle 1 complete. Agent-heartbeat-stall alert issued. Awaiting operator action or agent launch. Running cycle 2. + +````yaml +id: d279a9e0-ab4b-47 +phase: refine +metadata: + state: WORKING +```` + +### [2026-05-22T01:23:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 60cb9cdf-f9df-4d +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:24:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 94bf73b5-822f-4b +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:24:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: eb9703a3-b65a-4c +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:25:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 5c0e083e-642d-4f +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:25:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: ebacc1f1-b02b-40 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:26:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: d91065f3-489e-47 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:26:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 795a976a-2c79-48 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:26:53Z] overseer (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +Overseer monitoring active. Waiting for refiner CONSENSUS_PROPOSE. Cycle 13: refiner 321s elapsed, reviewers healthy. Two self-alerts cleared: progress event emitted, heartbeat refreshed. + +````yaml +id: 1e28afe8-40b3-4d +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:20:46.000000+00:00' +```` + +### [2026-05-22T01:27:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: efb8b822-f7bc-4f +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:27:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: b9d65e35-d77e-46 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:28:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 9687073e-8cc1-4a +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:28:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 29eaa948-53c4-4a +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:29:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 875016a4-19d4-4e +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:29:48Z] overseer (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +Overseer cycle 24: 523s elapsed, all agents running. Refiner WORKING (no propose yet). Reviewers heartbeating normally. Refreshing overseer heartbeat to stay under 120s threshold. + +````yaml +id: c9a01a04-6c04-47 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:20:46.000000+00:00' +```` + +### [2026-05-22T01:29:51Z] reviewer_refine (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: 8c5fd16d-68ff-46 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:21:28.083399+00:00' +```` + +### [2026-05-22T01:30:03Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE + +````yaml +id: d9a33a4b-4697-40 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:23:02.990597+00:00' +```` + +### [2026-05-22T01:30:42Z] refiner (CONSENSUS_PROPOSE): Proposal from refiner + +Analysis draft for #2769 (non-Claude models per agent via LiteLLM proxy). Recommends Option A: gateway-side upstream router keyed by IP-based session metadata, with LiteLLM as a separate Deployment in egg-system, Claude path structurally unchanged, and inert-by-default behavior. Surfaces the Claude Code auto-compaction risk as the primary validation gate and proposes the recognized-alias mitigation. Inventories the runtime primitives the plan phase will depend on (file:line evidence for get_anthropic_client, proxy_anthropic_messages, _inject_anthropic_credentials, _SSEAccumulator, _filter_blocked_tools, session_manager.get_session_by_ip, build_consensus_wrapped_command's hardcoded `model="opus"`, the per-overseer model fields on PipelineConfig, _PROTECTED_ENV_KEYS, the ANTHROPIC_BASE_URL spawn-time injection, and the three sites that bake `opus[1m]`). Registers 11 HITL decisions (cq-1..cq-11) covering LiteLLM topology, routing signal, per-agent model config shape, acceptance-test role, harness choice, target backend, credential handling, failure policy, private-mode tool-strip policy, slice decomposition, and the `[1m]` syntax; plus 5 open-ended feedback questions on Qwen hardware, target role list, swap-out interface, cost tracking, and compliance/data-residency. Complexity assessed as high. No `## Additional Context` was present in the issue body, so nothing is pre-resolved. + +````yaml +id: 4bbb1f0c-c54a-45 +phase: refine +metadata: + payload: + summary: 'Analysis draft for #2769 (non-Claude models per agent via LiteLLM proxy). + Recommends Option A: gateway-side upstream router keyed by IP-based session + metadata, with LiteLLM as a separate Deployment in egg-system, Claude path structurally + unchanged, and inert-by-default behavior. Surfaces the Claude Code auto-compaction + risk as the primary validation gate and proposes the recognized-alias mitigation. + Inventories the runtime primitives the plan phase will depend on (file:line + evidence for get_anthropic_client, proxy_anthropic_messages, _inject_anthropic_credentials, + _SSEAccumulator, _filter_blocked_tools, session_manager.get_session_by_ip, build_consensus_wrapped_command''s + hardcoded `model="opus"`, the per-overseer model fields on PipelineConfig, _PROTECTED_ENV_KEYS, + the ANTHROPIC_BASE_URL spawn-time injection, and the three sites that bake `opus[1m]`). + Registers 11 HITL decisions (cq-1..cq-11) covering LiteLLM topology, routing + signal, per-agent model config shape, acceptance-test role, harness choice, + target backend, credential handling, failure policy, private-mode tool-strip + policy, slice decomposition, and the `[1m]` syntax; plus 5 open-ended feedback + questions on Qwen hardware, target role list, swap-out interface, cost tracking, + and compliance/data-residency. Complexity assessed as high. No `## Additional + Context` was present in the issue body, so nothing is pre-resolved.' + attestation: {} + artifacts: + - .egg-state/drafts/2769-analysis.md + risk_considered: "Primary risk surface is Claude Code's model-name-derived auto-compaction\ + \ (raised in the issue and verified via web research on Claude Code's compaction\ + \ docs). Mitigation proposed: present Claude Code a recognized alias and route\ + \ on a separate per-agent signal (session metadata). Secondary risks: LiteLLM\ + \ supply-chain (March 2026 PyPI incident \u2014 addressed via cosign verification\ + \ and a swap-out interface question in feedback), credential/topology choice\ + \ (deferred to HITL), and per-agent config drift between orchestrator-declared\ + \ upstream and agent-passed model (mitigation: spawner derives both from one\ + \ config field)." + commit_sha: 9dc90338c + files_changed: + - .egg-state/drafts/2769-analysis.md + tests_run: [] + tasks_satisfied: [] + version: 1 + commit_sha: 9dc90338c +```` + +### [2026-05-22T01:30:42Z] reviewer_agent_design (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 5331ff20-5d25-49 +phase: refine +metadata: + state: WORKING +```` + +### [2026-05-22T01:30:42Z] reviewer_refine (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: a142d5ea-659a-48 +phase: refine +metadata: + state: WORKING +```` + +### [2026-05-22T01:31:03Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 16e9ec0b-72ad-44 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:30:47.149619+00:00' +```` + +### [2026-05-22T01:32:02Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: dc719d78-a30b-49 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:30:47.149619+00:00' +```` + +### [2026-05-22T01:32:05Z] overseer (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +Overseer cycle 29: BRC review round active. refiner PROPOSED (v1), reviewer_agent_design and reviewer_refine WORKING on proposal. Awaiting ACK/NACK. + +````yaml +id: 7bcb2661-22ce-4d +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:20:46.000000+00:00' +```` + +### [2026-05-22T01:32:31Z] reviewer_agent_design → refiner (CONSENSUS_ACK): ACK from reviewer_agent_design for refiner + +Reviewed `.egg-state/drafts/2769-analysis.md` (344 lines) against the agent-mode design rubric in `docs/guides/agent-mode-design.md`. Cross-checked the cited integration points (`gateway/gateway.py:9316-9329`, `:9355`, `:9410`, `:9552`, `:9752`, `:9774`, `:10019`; `orchestrator/consensus_wrapper.py:620-662`; `orchestrator/kubernetes_spawner.py:807`; `shared/egg_agent/client.py:62`; `shared/egg_agent/__main__.py:35`; `sandbox/llm/runner.py:49`) — primitive references and line numbers are accurate. + +**No blocking agent-design issues found.** The recommended Option A is structurally consistent with every agent-mode invariant we care about: + +1. **EGG200 (no direct LLM calls outside sandbox)** — preserved. The design extends the *existing* gateway proxy (already `noqa: EGG200` as legitimate proxy infrastructure) rather than introducing new `httpx`/Anthropic SDK call sites in orchestrator/shared. LiteLLM is reached *from the gateway*, not from agents or orchestrator code. The constraint "LiteLLM must not be directly reachable from sandbox pods" (line 66) and the Squid-allowlist-exclusion note (line 67) explicitly reinforce this. + +2. **EGG201 (model aliases, not pinned IDs)** — explicitly honored: "No pinned model snapshot versions. Use model aliases (e.g. `opus`, `sonnet`, `qwen3-coder-30b`)" (line 69). cq-11 surfaces the `opus[1m]` Claude-only suffix cleanly as a decision rather than silently leaving it baked in. + +3. **Agent SDK path preserved** — Option D (bypass Claude Code via `egg_agent.client.run_agent()`) is correctly identified as the SDK path already taking `--model` (line 124), and the recommended Option A retains the existing Claude Code harness for the no-op default. No new harness-bypass code is introduced. + +4. **Gateway as the per-request policy point** — Option A routes via per-agent session metadata (lookup-by-IP, same mechanism as `session_mode` today, line 77); the SSE accumulator, tool-strip, and credential injection sit *above* the upstream selection and stay upstream-agnostic. This is exactly the right factoring per the design guide's "minimal intermediation" principle — the gateway adds one indirection (upstream registry) without inserting itself into request semantics. + +5. **No pre-fetched content baked into prompts, no JSON-for-humans, no post-processing pipeline, no rigid procedures, no prompt-level security used as a substitute for sandbox enforcement** — none of these anti-patterns appear in the design. + +6. **The "present Claude Code a recognized alias while routing on session metadata" mitigation** (lines 79, 84, 138, cq-2 option 1) is a Claude-Code-compatibility workaround, not an agent-design concern: the *agent itself* sees and uses the real backend; only Claude Code's internal compaction-bookkeeping sees the alias. The agent isn't constrained from doing anything it could otherwise do. + +7. **Option B rejection (LiteLLM-fronts-everything)** correctly identifies that adding an LLM-translation hop to the Claude path would (a) violate the no-regression constraint and (b) make compaction-math worse, not better. Option C rejection correctly identifies the body-routing conflict with the recognized-alias mitigation. The option analysis is honest. + +### Non-blocking +- **cq-11 framing** — option (b) ("hoist into a single config helper that strips `[1m]` when the resolved upstream is non-Claude") is the most agent-design-aligned answer: it keeps model strings backend-agnostic at the API boundary and isolates the Claude-only suffix to a single resolution point. Worth flagging this in the operator's decision context if the producer revises. +- **cq-7 option (c)** ("sandbox sets its own per-agent API key via `extra_env`") is correctly flagged as a "probably non-starter" because it inverts the zero-credential sandbox invariant. Good — this is the right call from an agent-mode-security perspective and should stay weighted against in any plan-phase refinement. +- **The runtime-primitive table (lines 145-172)** is the right shape for handing the plan phase exact anchors; nothing for the design reviewer to flag, just noting it's well-suited to keep the plan agent grounded without baking diffs into its prompt. + +````yaml +id: 3b63d219-a510-4b +phase: refine +metadata: + payload: + artifact_references: + - .egg-state/drafts/2769-analysis.md + reason: "Reviewed `.egg-state/drafts/2769-analysis.md` (344 lines) against the\ + \ agent-mode design rubric in `docs/guides/agent-mode-design.md`. Cross-checked\ + \ the cited integration points (`gateway/gateway.py:9316-9329`, `:9355`, `:9410`,\ + \ `:9552`, `:9752`, `:9774`, `:10019`; `orchestrator/consensus_wrapper.py:620-662`;\ + \ `orchestrator/kubernetes_spawner.py:807`; `shared/egg_agent/client.py:62`;\ + \ `shared/egg_agent/__main__.py:35`; `sandbox/llm/runner.py:49`) \u2014 primitive\ + \ references and line numbers are accurate.\n\n**No blocking agent-design issues\ + \ found.** The recommended Option A is structurally consistent with every agent-mode\ + \ invariant we care about:\n\n1. **EGG200 (no direct LLM calls outside sandbox)**\ + \ \u2014 preserved. The design extends the *existing* gateway proxy (already\ + \ `noqa: EGG200` as legitimate proxy infrastructure) rather than introducing\ + \ new `httpx`/Anthropic SDK call sites in orchestrator/shared. LiteLLM is reached\ + \ *from the gateway*, not from agents or orchestrator code. The constraint \"\ + LiteLLM must not be directly reachable from sandbox pods\" (line 66) and the\ + \ Squid-allowlist-exclusion note (line 67) explicitly reinforce this.\n\n2.\ + \ **EGG201 (model aliases, not pinned IDs)** \u2014 explicitly honored: \"No\ + \ pinned model snapshot versions. Use model aliases (e.g. `opus`, `sonnet`,\ + \ `qwen3-coder-30b`)\" (line 69). cq-11 surfaces the `opus[1m]` Claude-only\ + \ suffix cleanly as a decision rather than silently leaving it baked in.\n\n\ + 3. **Agent SDK path preserved** \u2014 Option D (bypass Claude Code via `egg_agent.client.run_agent()`)\ + \ is correctly identified as the SDK path already taking `--model` (line 124),\ + \ and the recommended Option A retains the existing Claude Code harness for\ + \ the no-op default. No new harness-bypass code is introduced.\n\n4. **Gateway\ + \ as the per-request policy point** \u2014 Option A routes via per-agent session\ + \ metadata (lookup-by-IP, same mechanism as `session_mode` today, line 77);\ + \ the SSE accumulator, tool-strip, and credential injection sit *above* the\ + \ upstream selection and stay upstream-agnostic. This is exactly the right factoring\ + \ per the design guide's \"minimal intermediation\" principle \u2014 the gateway\ + \ adds one indirection (upstream registry) without inserting itself into request\ + \ semantics.\n\n5. **No pre-fetched content baked into prompts, no JSON-for-humans,\ + \ no post-processing pipeline, no rigid procedures, no prompt-level security\ + \ used as a substitute for sandbox enforcement** \u2014 none of these anti-patterns\ + \ appear in the design.\n\n6. **The \"present Claude Code a recognized alias\ + \ while routing on session metadata\" mitigation** (lines 79, 84, 138, cq-2\ + \ option 1) is a Claude-Code-compatibility workaround, not an agent-design concern:\ + \ the *agent itself* sees and uses the real backend; only Claude Code's internal\ + \ compaction-bookkeeping sees the alias. The agent isn't constrained from doing\ + \ anything it could otherwise do.\n\n7. **Option B rejection (LiteLLM-fronts-everything)**\ + \ correctly identifies that adding an LLM-translation hop to the Claude path\ + \ would (a) violate the no-regression constraint and (b) make compaction-math\ + \ worse, not better. Option C rejection correctly identifies the body-routing\ + \ conflict with the recognized-alias mitigation. The option analysis is honest.\n\ + \n### Non-blocking\n- **cq-11 framing** \u2014 option (b) (\"hoist into a single\ + \ config helper that strips `[1m]` when the resolved upstream is non-Claude\"\ + ) is the most agent-design-aligned answer: it keeps model strings backend-agnostic\ + \ at the API boundary and isolates the Claude-only suffix to a single resolution\ + \ point. Worth flagging this in the operator's decision context if the producer\ + \ revises.\n- **cq-7 option (c)** (\"sandbox sets its own per-agent API key\ + \ via `extra_env`\") is correctly flagged as a \"probably non-starter\" because\ + \ it inverts the zero-credential sandbox invariant. Good \u2014 this is the\ + \ right call from an agent-mode-security perspective and should stay weighted\ + \ against in any plan-phase refinement.\n- **The runtime-primitive table (lines\ + \ 145-172)** is the right shape for handing the plan phase exact anchors; nothing\ + \ for the design reviewer to flag, just noting it's well-suited to keep the\ + \ plan agent grounded without baking diffs into its prompt." + ack_version: 1 + version: 1 +```` + +### [2026-05-22T01:32:31Z] refiner (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 3f2d1950-7a72-4c +phase: refine +metadata: + state: WORKING +```` + +### [2026-05-22T01:32:34Z] reviewer_agent_design (CONSENSUS_CONFIRMED): Confirmed by reviewer_agent_design + +````yaml +id: 3c2f19cf-59a4-46 +phase: refine +metadata: + consensus_reached: false +```` + +### [2026-05-22T01:32:39Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: 2dad4f29-8b4e-43 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:32:39.055879+00:00' +```` + +### [2026-05-22T01:32:47Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_ACK,CONSENSUS_NACK,CONSENSUS_RE_REVIEW,STATUS,OVERSEER_ALERT + +````yaml +id: 1536be5b-2cc0-46 +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:32:47.754773+00:00' +```` + +### [2026-05-22T01:33:32Z] reviewer_refine → refiner (CONSENSUS_ACK): ACK from reviewer_refine for refiner + + +Reviewed the refine analysis at `.egg-state/drafts/2769-analysis.md` (commit 9dc90338c) against all seven review-criteria sections, plus cross-checked every cited file/line against the working tree, plus verified the contract scaffolding produced the expected 11 decisions + 5 feedback questions. + +## Section-by-Section Evaluation + +### 1. Problem Understanding — strong +The Problem Statement (lines 5-13) correctly extracts all three hard requirements from the issue body (parallel Claude + non-Claude, per-agent independent selectability, no-regression on the Claude path) and names the architectural intent (gateway-as-router, LiteLLM as translation layer). The side-benefit (harness decoupling) and the rejection rationale for `claude-code-router` are preserved. The supply-chain footnote on the March 2026 LiteLLM PyPI incident (line 13) is a useful unprompted add — it surfaces a real risk without overweighting it. + +### 2. Research Quality — exemplary +Spot-checked the 22 entries in the Runtime-primitives table against the working tree: +- `get_anthropic_client` cited at gateway.py:9316-9329 — actual definition spans 9317 (singleton decl) to 9329 (return), match. +- `_inject_anthropic_credentials` at :9355 — verified. +- `_filter_blocked_tools` at :9410 — verified. +- `_SSEAccumulator` at :9552 — verified. +- `get_session_by_ip` lookup in `proxy_anthropic_messages` — verified at gateway.py:9775 (draft says 9774, off-by-1). +- `proxy_anthropic_messages` — actual `def` is at gateway.py:9753 (draft says 9752, off-by-1 — the `@app.route` decorator is on the prior line). +- `proxy_count_tokens` — actual at gateway.py:10020 (draft says 10019, same off-by-1). +- `overseer_decision_maker_model` / `overseer_advisor_model` Field decls at orchestrator/models.py:546 and :620 — verified. +- `build_consensus_wrapped_command(model="opus", ...)` at orchestrator/consensus_wrapper.py:620-622 with `--model` at :658 — verified, falls inside the cited 653-662 range. +- Call sites at concurrent_executor.py:454 and routes/pipelines.py:2704 with no model arg — verified, confirming the "every non-overseer agent is opus-only by hardcoding" claim. +- `DEFAULT_MODEL = "opus[1m]"` at shared/egg_agent/client.py:62 — verified. +- `parser.add_argument("--model", default="opus[1m]", ...)` at shared/egg_agent/__main__.py:35 — verified. +- `cmd.extend(["--model", "opus[1m]"])` at sandbox/llm/runner.py:49 — verified. +- `ANTHROPIC_BASE_URL=GATEWAY_K8S_URL` at orchestrator/kubernetes_spawner.py:807 — verified. +- `GATEWAY_K8S_URL` declaration at kubernetes_spawner.py:124 — verified. +- `_PROTECTED_ENV_KEYS` at kubernetes_spawner.py:138 — verified. +- `setup_anthropic_api` in sandbox/entrypoint.py:712 sets `ANTHROPIC_BASE_URL` at :738 — verified (draft says 737-738, close). +- `allowed_domains.txt` Anthropic-excluded comment block — verified at lines 9-15, says explicitly "api.anthropic.com is intentionally NOT in this allowlist". + +The depth of citation (function + line + role in the request lifecycle) is well above the bar for a refine artifact and will give the planner a sturdy anchor to write tasks against. + +### 3. Options Analysis — well-decomposed +Four options (A: gateway router + session metadata; B: LiteLLM-fronts-everything; C: route on request-body model name; D: egg_agent SDK bypass). They are meaningfully different along the right axes (where the routing decision lives, what changes on the Claude path, how the compaction-math mitigation gets supported, what new harness surface gets introduced). The pro/con bullets for B and C are explicit about which constraint each fails — B fails the "no regression on the Claude path" gate; C fails the compaction-mitigation requirement that Claude Code be shown a recognized alias even when the backend is Qwen. The reasoning is auditable. + +### 4. Constraints and Dependencies — comprehensive +Constraints section (lines 61-71) enumerates: no Claude-path regression, routing-point placement below SSE accumulator + tool-filter + stream resilience, zero-credential sandbox invariant, gateway-mediated visibility, Squid network policy, per-agent independence, model-alias-only (no snapshot pins), file-size discipline against the 1500-line / 100KB cap, and the build-now / validate-later split. The runtime-primitives table doubles as a dependency graph for the planner. Primary risk (Claude Code's compaction math driving auto-compact on unrecognized models, lines 49-59) is well-explained with two external references and a concrete mitigation pointer. + +### 5. Open Questions — actionable + properly scaffolded +11 decisions (cq-1 through cq-11) and 5 feedback questions (Q1-Q5) cover, with no obvious gaps I can identify: +- topology (cq-1: deployment vs sidecar vs separate ns) +- routing signal (cq-2: session metadata vs header vs body) +- config shape (cq-3: PipelineConfig field vs repo YAML vs CLI vs stacked precedence) +- validation target (cq-4: which role flips first) +- harness choice (cq-5: Claude Code vs egg_agent SDK) +- backend (cq-6: self-hosted Qwen vs hosted Qwen vs OpenAI smoke test) +- credentials (cq-7: gateway-held vs gateway-passthrough vs sandbox-held) +- failure policy (cq-8: fail-closed vs Claude-fallback vs HITL-on-failure) +- private-mode tool-strip (cq-9: keep vs upstream-aware vs document-and-defer) +- slice decomposition (cq-10: single PR vs parallel vs dependent) +- `[1m]` syntax handling (cq-11: leave vs refactor vs deprecate) + +Plus 5 feedback questions covering Qwen hardware/budget, target role list, swap-out interface for LiteLLM, cost tracking extension, and compliance/data-residency. Verified contract state via mcp__sdlc__show_contract: all 11 decisions present with their full option lists, feedback Q1-Q5 present and unresolved. The "Resolved in Pre-Refine" section is correctly empty (the issue has no `## Additional Context` block, which the draft accurately notes at line 177). No silent assumptions detected. + +### 6. Recommendation Quality — clear and justified +Option A is recommended with four specific justifications (lines 135-141) that map back to the constraints. The recommendation is conditional on the operator answering cq-1 through cq-11, which is the correct posture for refine: the architectural shape is recommended, the topology / config-shape / first-target details are surfaced for human resolution rather than presumed. + +### 7. HITL Decision Registration — properly scaffolded +Cross-checked the `` markers in the draft (lines 181, 190, 199, 209, 219, 228, 237, 246, 255, 264, 274) against the contract JSON: all 11 decisions exist with `phase: "refine"`, `type: "hitl"`, `resolved: false`, and the option labels in the contract match the bullet labels in the draft. The `` marker (line 285) produced the feedback bundle with Q1-Q5 — verified all five questions are present in `contract.feedback.questions` with `answer: null` and `submitted: false`. The draft is not proceeding on any unvalidated silent assumption. + +## Non-blocking notes + +- **gateway/gateway.py line numbers off-by-1** — `proxy_anthropic_messages` is cited as `gateway/gateway.py:9752` (lines 32, 152) but actually starts at 9753 (the `@app.route` decorator is on 9752); `proxy_count_tokens` cited at :10019 is actually at :10020; `get_session_by_ip` call cited at :9774 is at :9775. The planner will not be misled (the surrounding context and symbol names are correct), but worth a one-line fix-up if there's a re-propose for another reason. Not blocking on its own. +- **kubernetes_spawner.py:1582 in the Current-Behavior table (line 38)** — cited as a usage site for `overseer_decision_maker_model` (Tier-2), but line 1582 is actually inside the docstring (`decision_model: LLM model for overseer decisions.`); the real usage sites are :1596 (env var) and :1623 (constructor kwarg). The Runtime-primitives table (line 162) cites :1596 correctly, so this is just a minor inconsistency between the two tables. +- **Supply-chain mitigation seam (Q3-adjacent)** — The draft's supply-chain caveat at line 13 and feedback Q3 frame this well, but consider whether the recommended Option A description (line 77) should explicitly call out that `UpstreamRegistry` is the natural seam where a swap-out interface lives. The plan can derive this from Q3, but a one-liner in the recommendation would harden the link. +- **`[1m]` triple-bake risk (cq-11)** — Worth noting non-blockingly that even on option-A "leave it", any code path that builds an agent invocation and forgets to strip `[1m]` for non-Claude routes is a latent foot-gun. cq-11 is the right place to surface this; just flagging that the answer may have implications for how the plan structures the consensus-wrapper signature change. +- **Refine cycle counter** — `contract.refine_review_cycles == 0`, consistent with this being cycle 1. + +Overall verdict: this is a high-quality refine artifact. Citations are accurate to the line, constraints are exhaustive, options are meaningfully different and clearly trade-off-justified, the recommended approach maps back to the hard constraints, and all open questions are properly scaffolded into the contract for the operator. The few off-by-1s and the docstring-vs-usage cite for kubernetes_spawner.py:1582 are cosmetic. Ready to advance. + + +````yaml +id: 07e2ef20-518d-4c +phase: refine +metadata: + payload: + artifact_references: + - .egg-state/drafts/2769-analysis.md + - .egg-state/contracts/issue-2769.json + - gateway/gateway.py + - orchestrator/models.py + - orchestrator/consensus_wrapper.py + - orchestrator/concurrent_executor.py + - orchestrator/routes/pipelines.py + - orchestrator/kubernetes_spawner.py + - shared/egg_agent/client.py + - shared/egg_agent/__main__.py + - sandbox/llm/runner.py + - sandbox/entrypoint.py + - gateway/allowed_domains.txt + - k8s/base/gateway-deployment.yaml + reason: "\nReviewed the refine analysis at `.egg-state/drafts/2769-analysis.md`\ + \ (commit 9dc90338c) against all seven review-criteria sections, plus cross-checked\ + \ every cited file/line against the working tree, plus verified the contract\ + \ scaffolding produced the expected 11 decisions + 5 feedback questions.\n\n\ + ## Section-by-Section Evaluation\n\n### 1. Problem Understanding \u2014 strong\n\ + The Problem Statement (lines 5-13) correctly extracts all three hard requirements\ + \ from the issue body (parallel Claude + non-Claude, per-agent independent selectability,\ + \ no-regression on the Claude path) and names the architectural intent (gateway-as-router,\ + \ LiteLLM as translation layer). The side-benefit (harness decoupling) and the\ + \ rejection rationale for `claude-code-router` are preserved. The supply-chain\ + \ footnote on the March 2026 LiteLLM PyPI incident (line 13) is a useful unprompted\ + \ add \u2014 it surfaces a real risk without overweighting it.\n\n### 2. Research\ + \ Quality \u2014 exemplary\nSpot-checked the 22 entries in the Runtime-primitives\ + \ table against the working tree:\n- `get_anthropic_client` cited at gateway.py:9316-9329\ + \ \u2014 actual definition spans 9317 (singleton decl) to 9329 (return), match.\n\ + - `_inject_anthropic_credentials` at :9355 \u2014 verified.\n- `_filter_blocked_tools`\ + \ at :9410 \u2014 verified.\n- `_SSEAccumulator` at :9552 \u2014 verified.\n\ + - `get_session_by_ip` lookup in `proxy_anthropic_messages` \u2014 verified at\ + \ gateway.py:9775 (draft says 9774, off-by-1).\n- `proxy_anthropic_messages`\ + \ \u2014 actual `def` is at gateway.py:9753 (draft says 9752, off-by-1 \u2014\ + \ the `@app.route` decorator is on the prior line).\n- `proxy_count_tokens`\ + \ \u2014 actual at gateway.py:10020 (draft says 10019, same off-by-1).\n- `overseer_decision_maker_model`\ + \ / `overseer_advisor_model` Field decls at orchestrator/models.py:546 and :620\ + \ \u2014 verified.\n- `build_consensus_wrapped_command(model=\"opus\", ...)`\ + \ at orchestrator/consensus_wrapper.py:620-622 with `--model` at :658 \u2014\ + \ verified, falls inside the cited 653-662 range.\n- Call sites at concurrent_executor.py:454\ + \ and routes/pipelines.py:2704 with no model arg \u2014 verified, confirming\ + \ the \"every non-overseer agent is opus-only by hardcoding\" claim.\n- `DEFAULT_MODEL\ + \ = \"opus[1m]\"` at shared/egg_agent/client.py:62 \u2014 verified.\n- `parser.add_argument(\"\ + --model\", default=\"opus[1m]\", ...)` at shared/egg_agent/__main__.py:35 \u2014\ + \ verified.\n- `cmd.extend([\"--model\", \"opus[1m]\"])` at sandbox/llm/runner.py:49\ + \ \u2014 verified.\n- `ANTHROPIC_BASE_URL=GATEWAY_K8S_URL` at orchestrator/kubernetes_spawner.py:807\ + \ \u2014 verified.\n- `GATEWAY_K8S_URL` declaration at kubernetes_spawner.py:124\ + \ \u2014 verified.\n- `_PROTECTED_ENV_KEYS` at kubernetes_spawner.py:138 \u2014\ + \ verified.\n- `setup_anthropic_api` in sandbox/entrypoint.py:712 sets `ANTHROPIC_BASE_URL`\ + \ at :738 \u2014 verified (draft says 737-738, close).\n- `allowed_domains.txt`\ + \ Anthropic-excluded comment block \u2014 verified at lines 9-15, says explicitly\ + \ \"api.anthropic.com is intentionally NOT in this allowlist\".\n\nThe depth\ + \ of citation (function + line + role in the request lifecycle) is well above\ + \ the bar for a refine artifact and will give the planner a sturdy anchor to\ + \ write tasks against.\n\n### 3. Options Analysis \u2014 well-decomposed\nFour\ + \ options (A: gateway router + session metadata; B: LiteLLM-fronts-everything;\ + \ C: route on request-body model name; D: egg_agent SDK bypass). They are meaningfully\ + \ different along the right axes (where the routing decision lives, what changes\ + \ on the Claude path, how the compaction-math mitigation gets supported, what\ + \ new harness surface gets introduced). The pro/con bullets for B and C are\ + \ explicit about which constraint each fails \u2014 B fails the \"no regression\ + \ on the Claude path\" gate; C fails the compaction-mitigation requirement that\ + \ Claude Code be shown a recognized alias even when the backend is Qwen. The\ + \ reasoning is auditable.\n\n### 4. Constraints and Dependencies \u2014 comprehensive\n\ + Constraints section (lines 61-71) enumerates: no Claude-path regression, routing-point\ + \ placement below SSE accumulator + tool-filter + stream resilience, zero-credential\ + \ sandbox invariant, gateway-mediated visibility, Squid network policy, per-agent\ + \ independence, model-alias-only (no snapshot pins), file-size discipline against\ + \ the 1500-line / 100KB cap, and the build-now / validate-later split. The runtime-primitives\ + \ table doubles as a dependency graph for the planner. Primary risk (Claude\ + \ Code's compaction math driving auto-compact on unrecognized models, lines\ + \ 49-59) is well-explained with two external references and a concrete mitigation\ + \ pointer.\n\n### 5. Open Questions \u2014 actionable + properly scaffolded\n\ + 11 decisions (cq-1 through cq-11) and 5 feedback questions (Q1-Q5) cover, with\ + \ no obvious gaps I can identify:\n- topology (cq-1: deployment vs sidecar vs\ + \ separate ns)\n- routing signal (cq-2: session metadata vs header vs body)\n\ + - config shape (cq-3: PipelineConfig field vs repo YAML vs CLI vs stacked precedence)\n\ + - validation target (cq-4: which role flips first)\n- harness choice (cq-5:\ + \ Claude Code vs egg_agent SDK)\n- backend (cq-6: self-hosted Qwen vs hosted\ + \ Qwen vs OpenAI smoke test)\n- credentials (cq-7: gateway-held vs gateway-passthrough\ + \ vs sandbox-held)\n- failure policy (cq-8: fail-closed vs Claude-fallback vs\ + \ HITL-on-failure)\n- private-mode tool-strip (cq-9: keep vs upstream-aware\ + \ vs document-and-defer)\n- slice decomposition (cq-10: single PR vs parallel\ + \ vs dependent)\n- `[1m]` syntax handling (cq-11: leave vs refactor vs deprecate)\n\ + \nPlus 5 feedback questions covering Qwen hardware/budget, target role list,\ + \ swap-out interface for LiteLLM, cost tracking extension, and compliance/data-residency.\ + \ Verified contract state via mcp__sdlc__show_contract: all 11 decisions present\ + \ with their full option lists, feedback Q1-Q5 present and unresolved. The \"\ + Resolved in Pre-Refine\" section is correctly empty (the issue has no `## Additional\ + \ Context` block, which the draft accurately notes at line 177). No silent assumptions\ + \ detected.\n\n### 6. Recommendation Quality \u2014 clear and justified\nOption\ + \ A is recommended with four specific justifications (lines 135-141) that map\ + \ back to the constraints. The recommendation is conditional on the operator\ + \ answering cq-1 through cq-11, which is the correct posture for refine: the\ + \ architectural shape is recommended, the topology / config-shape / first-target\ + \ details are surfaced for human resolution rather than presumed.\n\n### 7.\ + \ HITL Decision Registration \u2014 properly scaffolded\nCross-checked the `` markers in the draft (lines 181, 190, 199, 209,\ + \ 219, 228, 237, 246, 255, 264, 274) against the contract JSON: all 11 decisions\ + \ exist with `phase: \"refine\"`, `type: \"hitl\"`, `resolved: false`, and the\ + \ option labels in the contract match the bullet labels in the draft. The `` marker (line 285) produced the feedback bundle\ + \ with Q1-Q5 \u2014 verified all five questions are present in `contract.feedback.questions`\ + \ with `answer: null` and `submitted: false`. The draft is not proceeding on\ + \ any unvalidated silent assumption.\n\n## Non-blocking notes\n\n- **gateway/gateway.py\ + \ line numbers off-by-1** \u2014 `proxy_anthropic_messages` is cited as `gateway/gateway.py:9752`\ + \ (lines 32, 152) but actually starts at 9753 (the `@app.route` decorator is\ + \ on 9752); `proxy_count_tokens` cited at :10019 is actually at :10020; `get_session_by_ip`\ + \ call cited at :9774 is at :9775. The planner will not be misled (the surrounding\ + \ context and symbol names are correct), but worth a one-line fix-up if there's\ + \ a re-propose for another reason. Not blocking on its own.\n- **kubernetes_spawner.py:1582\ + \ in the Current-Behavior table (line 38)** \u2014 cited as a usage site for\ + \ `overseer_decision_maker_model` (Tier-2), but line 1582 is actually inside\ + \ the docstring (`decision_model: LLM model for overseer decisions.`); the real\ + \ usage sites are :1596 (env var) and :1623 (constructor kwarg). The Runtime-primitives\ + \ table (line 162) cites :1596 correctly, so this is just a minor inconsistency\ + \ between the two tables.\n- **Supply-chain mitigation seam (Q3-adjacent)**\ + \ \u2014 The draft's supply-chain caveat at line 13 and feedback Q3 frame this\ + \ well, but consider whether the recommended Option A description (line 77)\ + \ should explicitly call out that `UpstreamRegistry` is the natural seam where\ + \ a swap-out interface lives. The plan can derive this from Q3, but a one-liner\ + \ in the recommendation would harden the link.\n- **`[1m]` triple-bake risk\ + \ (cq-11)** \u2014 Worth noting non-blockingly that even on option-A \"leave\ + \ it\", any code path that builds an agent invocation and forgets to strip `[1m]`\ + \ for non-Claude routes is a latent foot-gun. cq-11 is the right place to surface\ + \ this; just flagging that the answer may have implications for how the plan\ + \ structures the consensus-wrapper signature change.\n- **Refine cycle counter**\ + \ \u2014 `contract.refine_review_cycles == 0`, consistent with this being cycle\ + \ 1.\n\nOverall verdict: this is a high-quality refine artifact. Citations are\ + \ accurate to the line, constraints are exhaustive, options are meaningfully\ + \ different and clearly trade-off-justified, the recommended approach maps back\ + \ to the hard constraints, and all open questions are properly scaffolded into\ + \ the contract for the operator. The few off-by-1s and the docstring-vs-usage\ + \ cite for kubernetes_spawner.py:1582 are cosmetic. Ready to advance.\n" + ack_version: 1 + version: 1 +```` + +### [2026-05-22T01:33:32Z] 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: dee84602-f761-45 +phase: refine +metadata: + ready_to_confirm: true + version: 1 +```` + +### [2026-05-22T01:33:32Z] refiner (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: 10b1fc6c-c5cb-42 +phase: refine +metadata: + state: WORKING +```` + +### [2026-05-22T01:33:35Z] refiner (CONSENSUS_CONFIRMED): Confirmed by refiner + +````yaml +id: af619638-534d-45 +phase: refine +metadata: + consensus_reached: false +```` + +### [2026-05-22T01:33:35Z] reviewer_agent_design (HEARTBEAT): heartbeat: WORKING + +wait_loop exited + +````yaml +id: c06b8ad9-bf0a-47 +phase: refine +metadata: + state: WORKING +```` + +### [2026-05-22T01:33:38Z] reviewer_refine (CONSENSUS_CONFIRMED): Confirmed by reviewer_refine + +````yaml +id: 05db1873-72ea-4f +phase: refine +metadata: + consensus_reached: true +```` + +### [2026-05-22T01:33:40Z] reviewer_agent_design (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_PROPOSE,CONSENSUS_RE_REVIEW,CONSENSUS_CONFIRMED,OVERSEER_ALERT + +````yaml +id: d7d1a265-7da8-4a +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:33:40.083389+00:00' +```` + +### [2026-05-22T01:33:41Z] refiner (HEARTBEAT): heartbeat: WAITING_FOR_EVENT + +wait_loop blocked on CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT + +````yaml +id: a9e6805b-0c48-4b +phase: refine +metadata: + state: WAITING_FOR_EVENT + since: '2026-05-22T01:33:41.691260+00:00' +```` diff --git a/.egg-state/contracts/issue-2769.json b/.egg-state/contracts/issue-2769.json new file mode 100644 index 0000000000..3bc45b0531 --- /dev/null +++ b/.egg-state/contracts/issue-2769.json @@ -0,0 +1,1758 @@ +{ + "schemaVersion": "1.1", + "issue": { + "number": 2769, + "title": "Issue #2769", + "url": "https://github.com/jwbron/egg/issues/2769" + }, + "pipeline_id": "issue-2769", + "current_phase": "refine", + "acceptance_criteria": [], + "slices": [ + { + "id": "slice-1", + "name": "Gateway upstream router + LiteLLM Deployment (no-op by default)", + "status": "complete", + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "tasks": [ + { + "id": "task-1-1", + "description": "Introduce `gateway/upstream_registry.py` (NEW) containing\nan `UpstreamRegistry` class keyed by upstream name\n(`\"anthropic\"`, `\"litellm\"`). Each registry entry pairs a\nsingleton `httpx.Client` (`base_url`, timeout, connection\nlimits) with a credential resolver returning an\n`UpstreamCredential` (the union of today's\n`AnthropicCredential` shape and the new LiteLLM\n`x-api-key` shape). Provide `get(upstream: str)` returning\n`(client, credential_resolver)`, raising a typed\n`UnknownUpstreamError` on miss. Wire it into a\n`get_upstream_registry()` accessor that mirrors today's\n`get_anthropic_client()` lifetime semantics.", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `UpstreamRegistry.get(\"anthropic\")` returns a client\n with `base_url == \"https://api.anthropic.com\"` and the\n existing Anthropic credential resolver (preserves the\n `# noqa: EGG200` annotation pattern at\n `gateway/gateway.py:9325`).\n- `UpstreamRegistry.get(\"litellm\")` returns a client whose\n `base_url` is sourced from a new\n `LITELLM_BASE_URL` env var (default\n `http://litellm.egg-system.svc.cluster.local:4000`) and\n the LiteLLM credential resolver.\n- `UpstreamRegistry.get(\"unknown\")` raises\n `UnknownUpstreamError`.\n- Both clients share the same timeout / pooling\n characteristics as today's `_anthropic_client`.", + "files_affected": [ + "gateway/upstream_registry.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": "Add a LiteLLM credential resolver to\n`gateway/anthropic_credentials.py` (or a sibling module if\nfile-size discipline requires it). The resolver reads\n`LITELLM_MASTER_KEY` from `secrets.env` using the existing\n`parse_env_file` helper at\n`gateway/anthropic_credentials.py:52`, caches with the\nsame mtime-invalidated pattern as\n`AnthropicCredentialsManager`, and returns a credential\nshaped `header_name=\"x-api-key\"`,\n`header_value=\"\"`. Returns `None` when the key is\nabsent (no-op default \u2014 matches today's behavior when\nAnthropic credentials are absent).", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- With `LITELLM_MASTER_KEY` unset, the resolver returns\n `None` and does not warn at startup.\n- With `LITELLM_MASTER_KEY=foo`, the resolver returns a\n credential with `header_name == \"x-api-key\"` and\n `header_value == \"foo\"`.\n- `secrets.env` mtime change invalidates the cache the\n same way `AnthropicCredentialsManager` does.", + "files_affected": [ + "gateway/anthropic_credentials.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": "Make `_inject_anthropic_credentials` upstream-aware\n(rename to `_inject_upstream_credentials(headers,\nupstream)` and keep the old symbol as a back-compat alias\ncalling through with `upstream=\"anthropic\"`). Dispatch to\nthe LiteLLM credential resolver when\n`upstream == \"litellm\"`. Preserve the 401 / \"no credential\"\nerror path verbatim for both.", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `_inject_upstream_credentials(headers, \"anthropic\")`\n behaves byte-identically to today's\n `_inject_anthropic_credentials(headers)`.\n- `_inject_upstream_credentials(headers, \"litellm\")` adds\n `x-api-key: ` when the key is set.\n- Missing credentials for either upstream return a 401\n with the same JSON body shape as today.", + "files_affected": [ + "gateway/gateway.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": "Add `upstream: str = \"anthropic\"` and\n`upstream_model: str | None = None` to the `Session`\ndataclass at `gateway/session_manager.py:288`. Plumb them\nthrough `Session.to_dict_for_persistence` /\n`Session.from_persistence` so existing persisted sessions\nwithout the fields still load (defaults apply). Extend\n`SessionManager.register_session` (`gateway/session_manager.py:548`)\nto accept the two new optional parameters.", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- A `Session` created without the new fields keeps\n `upstream == \"anthropic\"` and `upstream_model is None`.\n- `Session.to_dict_for_persistence` /\n `Session.from_persistence` round-trip both fields\n losslessly and tolerate persisted dicts where the fields\n are absent.\n- `SessionManager.register_session(upstream=\"litellm\",\n upstream_model=\"qwen3-coder-30b\")` stores both on the\n returned `Session`.", + "files_affected": [ + "gateway/session_manager.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-5", + "description": "Wire `upstream` and `upstream_model` through the\n`/api/v1/sessions/create` route handler at\n`gateway/gateway.py:8507` (parse from request body with\ntheir defaults, validate that `upstream` is one of the\nregistered names from `UpstreamRegistry`, pass through to\n`SessionManager.register_session`). Log them in the\nexisting `audit_log(\"session_created\", ...)` call so the\nper-session upstream is auditable.", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- POSTing to `/api/v1/sessions/create` without the new\n fields creates a session with\n `upstream=\"anthropic\"` and `upstream_model is None`.\n- POSTing with `upstream=\"litellm\",\n upstream_model=\"qwen3-coder-30b\"` creates a session\n with those values.\n- POSTing with `upstream=\"bogus\"` returns a 400 with a\n descriptive error.\n- `session_created` audit log includes the upstream and\n upstream_model.", + "files_affected": [ + "gateway/gateway.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": "Refactor `proxy_anthropic_messages` (gateway/gateway.py:9753)\nand `proxy_count_tokens` (gateway/gateway.py:10020) to\nresolve the upstream per request: replace\n`client = get_anthropic_client()` with the registry lookup\nusing `session.upstream` (defaulting to `\"anthropic\"` when\nthere is no session \u2014 preserves today's behavior).\nReplace `_inject_anthropic_credentials(headers)` calls\nwith `_inject_upstream_credentials(headers,\nsession.upstream)`. Keep the SSE accumulator, tool-filter,\nand stream-resilience retry loop unchanged.", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- A request whose session has `upstream == \"anthropic\"`\n (or no session) hits the Anthropic httpx client and\n injects the Anthropic credential \u2014 byte-identical to\n today.\n- A request whose session has `upstream == \"litellm\"`\n hits the LiteLLM client and injects the LiteLLM\n credential.\n- `_filter_blocked_tools`, the `_SSEAccumulator` parse,\n and the connection-reset retry loop are unchanged in\n behavior and code shape (no new branches inside any of\n them).\n- `proxy_count_tokens` mirrors the same routing change.", + "files_affected": [ + "gateway/gateway.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": "Extend `GatewayClient.register_session` at\n`orchestrator/gateway_client.py:602` with optional\n`upstream: str | None = None` and `upstream_model: str |\nNone = None` parameters. Include them in `request_data`\nonly when set (matches the existing optional-field\npattern at `gateway_client.py:653-690`). No caller in\nslice 1 passes them; this is purely the wire-shape.", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `GatewayClient.register_session(...)` without the new\n args produces the same request body as today.\n- `GatewayClient.register_session(upstream=\"litellm\",\n upstream_model=\"qwen3-coder-30b\")` includes both keys\n in the POSTed JSON.", + "files_affected": [ + "orchestrator/gateway_client.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-8", + "description": "Add k8s manifests for the LiteLLM proxy:\n`k8s/base/litellm-deployment.yaml`,\n`k8s/base/litellm-service.yaml`, and\n`k8s/base/litellm-configmap.yaml`. Deployment runs a\npinned LiteLLM image in `egg-system`, mounts the ConfigMap\nat `/app/config.yaml`, exposes port `4000` (LiteLLM's\ndefault). Service is `ClusterIP` named `litellm`. ConfigMap\nships with an EMPTY `model_list` so the deployment comes\nup healthy but serves nothing until operators populate it.\nAdd all three to `k8s/base/kustomization.yaml`.", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `kubectl apply --dry-run=client -k k8s/base/` succeeds\n with the new resources included.\n- The LiteLLM Service resolves to\n `litellm.egg-system.svc.cluster.local:4000`, which\n matches the default `LITELLM_BASE_URL` baked into\n `UpstreamRegistry`.\n- No NetworkPolicy change to `egg-agents` egress \u2014\n agents do not talk to LiteLLM directly.", + "files_affected": [ + "k8s/base/litellm-deployment.yaml", + "k8s/base/litellm-service.yaml", + "k8s/base/litellm-configmap.yaml", + "k8s/base/kustomization.yaml" + ], + "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-9", + "description": "Document `LITELLM_MASTER_KEY` in\n`config/secrets.template.env` (one block below the\n`ANTHROPIC_API_KEY` block, with an explicit \"leave empty\nto disable LiteLLM routing \u2014 no agent will be routed to\nLiteLLM with this unset\" comment).", + "status": "complete", + "commit": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `config/secrets.template.env` contains a documented\n `LITELLM_MASTER_KEY=\"\"` entry with the disable-when-empty\n note.", + "files_affected": [ + "config/secrets.template.env" + ], + "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-10", + "description": "Write unit tests covering the slice 1 gateway-side changes:\n`tests/gateway/test_upstream_registry.py` (new, covering\nthe three registry cases \u2014 anthropic, litellm, unknown),\nextensions to `tests/gateway/test_anthropic_credentials.py`\n(LiteLLM resolver path), and extensions to\n`tests/gateway/test_anthropic_proxy.py` (the two routing\nbranches for both proxy routes).", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `make test` reaches and passes the new + extended\n tests.\n- Coverage includes the unknown-upstream error path, the\n \"no credential\" 401 path for both upstreams, and a\n byte-identity check for the Anthropic-routed request\n shape vs today.", + "files_affected": [ + "tests/gateway/test_upstream_registry.py", + "tests/gateway/test_anthropic_proxy.py", + "tests/gateway/test_anthropic_credentials.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-11", + "description": "Write unit tests covering the slice 1 session-manager and\norchestrator-client changes: extensions to\n`gateway/tests/test_session_manager.py` (round-trip the\ntwo new fields, register_session with defaults, register\nwith explicit LiteLLM values) and to\n`orchestrator/tests/test_gateway_client.py` (omitted args\n\u2192 no new keys in body, explicit args \u2192 keys present).", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- All tests pass under `make test`.\n- The session-persistence test verifies a dict missing\n the new keys still rehydrates cleanly (back-compat\n guard).", + "files_affected": [ + "gateway/tests/test_session_manager.py", + "orchestrator/tests/test_gateway_client.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-12", + "description": "Author a new architecture doc\n`docs/architecture/upstream-routing.md` describing the\n`UpstreamRegistry` seam, the LiteLLM topology, the\nper-session routing decision, the credential layout, and\nthe cq-1 / cq-2 / cq-5 / cq-7 / cq-8 resolutions that\nshape it. Cross-link from `gateway/CLAUDE.md` and\n`docs/architecture/orchestrator.md`.", + "status": "complete", + "commit": "8c68062f024594fb44fe6f808ed6cc90749969f1", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- The doc names every primitive added in slice 1 with a\n `file:line` cite, explains the no-op-by-default\n invariant, and walks through the request lifecycle for\n both upstreams.\n- `gateway/CLAUDE.md` and\n `docs/architecture/orchestrator.md` link to it.", + "files_affected": [ + "docs/architecture/upstream-routing.md", + "gateway/CLAUDE.md", + "docs/architecture/orchestrator.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": "egg/issue-2769/work", + "commit": null, + "review_feedback": [] + }, + { + "id": "slice-2", + "name": "Per-agent model config + spawn-side plumbing + body rewrite", + "status": "complete", + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "tasks": [ + { + "id": "task-2-1", + "description": "Add `agent_models: dict[str, str] = Field(default_factory=dict, ...)`\nto `PipelineConfig` (orchestrator/models.py:405). Validate\nkeys against the `AgentRole` enum at\n`shared/egg_contracts/agent_roles.py:46` via a Pydantic\nvalidator: unknown roles raise a typed config error at\nconstruction time. Values are free-form strings (validated\ndownstream by the resolver in TASK-2-3).", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `PipelineConfig(agent_models={\"refiner\": \"qwen3-coder-30b\"})`\n constructs successfully.\n- `PipelineConfig(agent_models={\"bogus_role\": \"x\"})` raises\n a Pydantic validation error citing the unknown role.\n- Default-constructed `PipelineConfig.agent_models` is an\n empty dict (no behavioral change for existing pipelines).", + "files_affected": [ + "orchestrator/models.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": "Add a `default_agent_model: str | None` field to the\n`repositories.yaml` schema (documented in\n`config/repositories.yaml.example`) and expose it via a new\n`get_default_agent_model(repo)` helper in\n`config/repo_config.py` (mirroring the\n`get_repo_setting(repo, key, default)` pattern at\n`config/repo_config.py:248`).", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `get_default_agent_model(\"owner/repo\")` returns the\n configured value when set in `repositories.yaml`, or\n `None` when absent.\n- `config/repositories.yaml.example` shows the new field\n in context with an inline comment naming the precedence\n rule (per-pipeline `agent_models` > this default >\n built-in `\"opus\"`).", + "files_affected": [ + "config/repo_config.py", + "config/repositories.yaml.example" + ], + "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": "New module `orchestrator/agent_model_resolution.py`\nexporting `resolve_agent_model(role: AgentRole,\npipeline_config: PipelineConfig, repo: str | None) ->\nAgentModelDecision`, where `AgentModelDecision` is a small\ndataclass with fields `(claude_code_alias: str, upstream:\nstr, upstream_model: str | None)`. Precedence:\n`pipeline_config.agent_models.get(role.value)` \u2192\n`get_default_agent_model(repo)` \u2192 built-in `\"opus\"`.\nClassifier: model strings matching `opus`, `opus[1m]`,\n`sonnet`, `haiku`, or `claude-*` map to\n`upstream=\"anthropic\"`, `claude_code_alias=`,\n`upstream_model=None`. Every other string maps to\n`upstream=\"litellm\"`, `claude_code_alias=\"opus\"` (cq-5\nmitigation), `upstream_model=`.", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- `resolve_agent_model(AgentRole.CODER, default_config, None)`\n returns\n `(claude_code_alias=\"opus\", upstream=\"anthropic\",\n upstream_model=None)`.\n- `resolve_agent_model(AgentRole.REFINER,\n PipelineConfig(agent_models={\"refiner\": \"qwen3-coder-30b\"}),\n None)` returns\n `(claude_code_alias=\"opus\", upstream=\"litellm\",\n upstream_model=\"qwen3-coder-30b\")`.\n- `resolve_agent_model(...)` with only\n `default_agent_model=\"sonnet\"` set on the repo returns\n `(claude_code_alias=\"sonnet\",\n upstream=\"anthropic\", upstream_model=None)`.\n- Per-pipeline `agent_models` entry overrides repo-level\n `default_agent_model`.", + "files_affected": [ + "orchestrator/agent_model_resolution.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-4", + "description": "Thread the resolved decision through the initial spawn\npath: `orchestrator/concurrent_executor.py:454` calls\n`resolve_agent_model(role, ...)` and passes\n`model=decision.claude_code_alias` to\n`build_consensus_wrapped_command`. The same site passes\n`upstream=decision.upstream` and\n`upstream_model=decision.upstream_model` to the\ndownstream spawn helper that ultimately reaches\n`GatewayClient.register_session` (the existing call at\n`orchestrator/kubernetes_spawner.py:735`). When the\ndecision is the default-Anthropic case, the new\nregister_session kwargs are omitted (no wire change vs\ntoday).", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- With `PipelineConfig.agent_models == {}`, the spawn\n path produces the same `build_consensus_wrapped_command`\n args and the same `register_session` payload as before\n this slice (regression guard).\n- With `agent_models={\"refiner\": \"qwen3-coder-30b\"}`,\n the refiner spawn passes `--model opus` to the wrapper\n and `upstream=\"litellm\",\n upstream_model=\"qwen3-coder-30b\"` to the gateway.", + "files_affected": [ + "orchestrator/concurrent_executor.py", + "orchestrator/kubernetes_spawner.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-5", + "description": "Thread the resolved decision through the restart path at\n`orchestrator/routes/pipelines.py:2704`. Same shape as\nTASK-2-4 \u2014 resolve, pass `model=` to\n`build_consensus_wrapped_command`, ensure the surrounding\nrestart code reuses the existing session (already\nregistered with the right upstream).", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- Restarting an agent whose pipeline has a non-default\n `agent_models` entry uses the resolved Claude alias for\n the `--model` flag.\n- Restarting an agent on the default Claude path is\n byte-identical to today.", + "files_affected": [ + "orchestrator/routes/pipelines.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 `_rewrite_upstream_model(request_body, upstream_model)`\nnext to `_filter_blocked_tools` in `gateway/gateway.py`.\nOn LiteLLM-routed requests with `session.upstream_model`\nset, the helper parses the JSON body, replaces the\ntop-level `\"model\"` field with `session.upstream_model`,\nand returns the re-serialized body. On parse error or\nwhen `upstream_model is None`, the body is returned\nunchanged. Call it in `proxy_anthropic_messages` and\n`proxy_count_tokens` AFTER `_filter_blocked_tools` and\nBEFORE building the upstream request.", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- With `upstream == \"litellm\"` and\n `upstream_model == \"qwen3-coder-30b\"`, the body\n forwarded upstream has `\"model\":\n \"qwen3-coder-30b\"` regardless of the incoming\n `\"model\"` value.\n- With `upstream == \"anthropic\"`, the body is\n byte-identical to the incoming body (regression\n guard).\n- Invalid JSON returns the original body unchanged (does\n not crash the proxy).", + "files_affected": [ + "gateway/gateway.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-7", + "description": "Unit tests for the resolver and the spawn-side wiring:\n`orchestrator/tests/test_agent_model_resolution.py` (new)\ncovering precedence + classifier; extensions to the\nexisting concurrent-executor and restart-path test\nmodules (`orchestrator/tests/test_concurrent_executor.py`\nor its current equivalent, plus a new or extended\npipeline-restart test) that mock the spawner and assert\nthe resolved `--model` and the `register_session` kwargs.", + "status": "complete", + "commit": "a7658bf1e8934b91bd96e354ad11db0dd6e92dc2", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- All new and extended tests pass under `make test`.\n- Tests assert the cq-5 mitigation explicitly: the\n Claude-Code-facing alias for a LiteLLM-routed agent is\n always `\"opus\"`, never the upstream model name.\n- Default-`agent_models` path is exercised as the\n regression guard (no register_session kwargs added; no\n `--model` change).", + "files_affected": [ + "orchestrator/tests/test_agent_model_resolution.py", + "orchestrator/tests/test_concurrent_executor.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-8", + "description": "Extend `tests/gateway/test_anthropic_proxy.py` with the\nbody-rewrite branch: with a LiteLLM session whose\n`upstream_model` is set, the request body forwarded\nupstream has the rewritten `model` field; with an\nAnthropic session, the body is byte-identical. Also test\nthe invalid-JSON path through `_rewrite_upstream_model`.", + "status": "complete", + "commit": "a7658bf1e8934b91bd96e354ad11db0dd6e92dc2", + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- Tests pass under `make test`.\n- The byte-identical-Claude-path assertion uses a\n non-default incoming model value (e.g.\n `\"opus\"`) and confirms it survives unchanged when\n `upstream == \"anthropic\"`.", + "files_affected": [ + "tests/gateway/test_anthropic_proxy.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-9", + "description": "Write a how-to doc `docs/guides/per-agent-models.md`\ncovering: setting `agent_models` per pipeline; setting\n`default_agent_model` per repository in\n`repositories.yaml`; the precedence rule; the cq-5\nrecognised-alias presented-to-Claude-Code mitigation;\nthe operator smoke test (live LiteLLM endpoint, the\ncq-4-deferred validation). Cross-link from\n`docs/index.md` and the new\n`docs/architecture/upstream-routing.md`.", + "status": "pending", + "commit": null, + "checkpoint_id": null, + "notes": "", + "acceptance_criteria": "- The guide names every primitive added in slice 2 with\n a `file:line` cite (resolver, config field, repo\n helper, body-rewrite helper).\n- It walks an operator through enabling Qwen for the\n refiner role end-to-end without modifying source code.\n- `docs/index.md` and\n `docs/architecture/upstream-routing.md` link to it.", + "files_affected": [ + "docs/guides/per-agent-models.md", + "docs/index.md", + "docs/architecture/upstream-routing.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": "egg/issue-2769/slice-1", + "commit": null, + "review_feedback": [] + } + ], + "decisions": [ + { + "id": "cq-1", + "question": "Where should the LiteLLM proxy run, topologically?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Separate Deployment+Service in egg-system namespace (1 LiteLLM pod, gateway calls it over the cluster Service DNS)", + "description": null + }, + { + "id": "opt-2", + "label": "Sidecar container in the gateway pod (same pod, localhost call, shares lifecycle)", + "description": null + }, + { + "id": "opt-3", + "label": "Separate namespace `egg-llm` with its own NetworkPolicy (stronger isolation, more ops surface)", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Separate Deployment+Service in egg-system namespace (1 LiteLLM pod, gateway calls it over the cluster Service DNS)\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:07:58.264414Z", + "debounce_until": null + }, + { + "id": "cq-2", + "question": "How should the gateway decide which upstream (`api.anthropic.com` vs LiteLLM) to use for a given `/v1/messages` request?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Per-agent session metadata: orchestrator declares the model+upstream when it spawns the agent (session lookup by IP, same path used today for `session_mode`) \u2014 model name in the body is informational only", + "description": null + }, + { + "id": "opt-2", + "label": "Custom HTTP header from the sandbox (e.g. `X-Egg-Upstream: litellm`) injected at agent startup \u2014 gateway reads the header and routes accordingly", + "description": null + }, + { + "id": "opt-3", + "label": "Model name in the request body (any non-Claude model name \u2192 LiteLLM) \u2014 simplest but conflicts with the issue's compaction-mitigation note about presenting a recognized alias to Claude Code", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Per-agent session metadata: orchestrator declares the model+upstream when it spawns the agent (session lookup by IP, same path used today for `session_mode`) \u2014 model name in the body is informational only\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:07:58.283828Z", + "debounce_until": null + }, + { + "id": "cq-3", + "question": "How should per-agent model selection be configured (i.e. where does an operator say 'run the refiner on Qwen, leave the coder on Claude')?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "New per-role field on `PipelineConfig` (alongside `overseer_decision_maker_model` / `overseer_advisor_model`) \u2014 e.g. `agent_models: {refiner: 'qwen3-coder', coder: 'opus'}`", + "description": null + }, + { + "id": "opt-2", + "label": "Repository-level YAML config (`config/repositories.yaml` or similar) \u2014 operator edits once, applies to every pipeline on that repo", + "description": null + }, + { + "id": "opt-3", + "label": "Per-pipeline override only (CLI flag / API payload on submit_task) \u2014 no persistent per-role default, the operator names the override at submission time", + "description": null + }, + { + "id": "opt-4", + "label": "All of the above stacked, with precedence: CLI > pipeline config > repo config > built-in default 'opus'", + "description": null + }, + { + "id": "opt-5", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Per-role PipelineConfig field, plus a 'default model' definition settable in repositories.yaml. The default model is currently opus[1m] in effect \u2014 the per-role field overrides it for individual agents, the repositories.yaml default applies to any role without an explicit override.\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:07:58.291768Z", + "debounce_until": null + }, + { + "id": "cq-4", + "question": "Which agent role should be the first to be flipped to a non-Claude model for the empirical compatibility validation that the issue calls out as the acceptance test?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "A reviewer role (e.g. `reviewer_refine`) \u2014 reviewers do less tool-heavy work and have shorter sessions, so this is the lowest-risk first cut", + "description": null + }, + { + "id": "opt-2", + "label": "The `refiner` (this role) \u2014 produces analysis docs, modest tool use, easy to compare output against the Claude baseline", + "description": null + }, + { + "id": "opt-3", + "label": "The `coder` \u2014 most tool-heavy and longest-running role, so it stresses the auto-compaction edge case the issue flags as the primary risk", + "description": null + }, + { + "id": "opt-4", + "label": "An overseer tier (decision-maker or advisor) \u2014 already model-configurable today, so the plumbing is smaller, but it's less representative of the main SDLC loop", + "description": null + }, + { + "id": "opt-5", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Empirical validation cannot occur inside this pipeline \u2014 there is no configured access to non-Claude models in the pipeline environment. The operator will validate separately via a full pipeline run with all agents switched to non-Claude models. This pipeline therefore delivers only the buildable, no-op-by-default integration; no single-agent acceptance-test flip is in scope.\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:08:44.866338Z", + "debounce_until": null + }, + { + "id": "cq-5", + "question": "For the first non-Claude target, should agents continue to run inside the Claude Code harness (relying on the LiteLLM Anthropic-translation seam) or switch to the egg_agent SDK path?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Keep the Claude Code harness for non-Claude models too \u2014 use the recognized-alias mitigation so compaction math stays sane; minimum disruption to the spawning + entrypoint code", + "description": null + }, + { + "id": "opt-2", + "label": "Route non-Claude agents through the `egg_agent` SDK path (which already supports `--model`) and bypass Claude Code entirely \u2014 sidesteps the auto-compaction risk, but the SDK path has fewer integrations (statusline, settings.json rules) than the CLI", + "description": null + }, + { + "id": "opt-3", + "label": "Both: leave the harness choice as a per-role/per-model config knob \u2014 maximally flexible, but doubles the test surface for the first cut", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Keep the Claude Code harness for non-Claude models too \u2014 use the recognized-alias mitigation so compaction math stays sane; minimum disruption to the spawning + entrypoint code\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:10:34.935140Z", + "debounce_until": null + }, + { + "id": "cq-6", + "question": "What should the first acceptance-test backend be for the non-Claude path?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Self-hosted Qwen on vLLM/SGLang (matches the long-term cost goal and is the primary stated target) \u2014 requires standing up a vLLM Deployment + model weights as part of validation", + "description": null + }, + { + "id": "opt-2", + "label": "A hosted Qwen-compatible provider (e.g. Together, Fireworks, DeepInfra, OpenRouter) \u2014 fastest path to a live endpoint; defers the self-hosting work but adds a third-party dependency and a new credential to hold", + "description": null + }, + { + "id": "opt-3", + "label": "An OpenAI/other already-trusted backend behind LiteLLM as the literal first smoke test, with Qwen as the second cut \u2014 lowest validation risk; lets us decouple 'gateway routing works' from 'Qwen tool-calling works'", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"A hosted Qwen-compatible provider (e.g. Together, Fireworks, DeepInfra, OpenRouter) \u2014 fastest path to a live endpoint; defers the self-hosting work but adds a third-party dependency and a new credential to hold\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:10:34.949959Z", + "debounce_until": null + }, + { + "id": "cq-7", + "question": "How should the gateway handle credentials for the LiteLLM upstream?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Gateway holds a LiteLLM master key in `secrets.env` and injects it on every LiteLLM-bound request (mirrors today's `ANTHROPIC_API_KEY` injection pattern) \u2014 LiteLLM holds the real per-backend keys", + "description": null + }, + { + "id": "opt-2", + "label": "LiteLLM runs with no auth, network-isolated to the gateway (NetworkPolicy or shared pod) and the gateway passes raw upstream credentials per-request \u2014 fewer secrets to hold, but pushes per-backend key management into the gateway", + "description": null + }, + { + "id": "opt-3", + "label": "Gateway holds nothing for LiteLLM; the sandbox sets its own per-agent API key via `extra_env` (e.g. operator-supplied) \u2014 inverts today's zero-credential sandbox invariant, so probably a non-starter", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Gateway holds a LiteLLM master key in `secrets.env` and injects it on every LiteLLM-bound request (mirrors today's `ANTHROPIC_API_KEY` injection pattern) \u2014 LiteLLM holds the real per-backend keys\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:10:34.969361Z", + "debounce_until": null + }, + { + "id": "cq-8", + "question": "When the LiteLLM proxy is unreachable / errors for a non-Claude agent, what is the failure policy?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Fail closed (502 to the agent, no fallback) \u2014 same policy as today's Claude upstream errors; surfaces the misconfig immediately", + "description": null + }, + { + "id": "opt-2", + "label": "Fall back to Claude on transient LiteLLM failures only \u2014 keeps the pipeline progressing but produces a quietly-mixed transcript and erodes the cost goal", + "description": null + }, + { + "id": "opt-3", + "label": "Fail closed but auto-escalate to a HITL decision when a non-Claude agent fails to spawn or stalls \u2014 best operator UX, most code to write", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Fail closed (502 to the agent, no fallback) \u2014 same policy as today's Claude upstream errors; surfaces the misconfig immediately\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:11:21.520734Z", + "debounce_until": null + }, + { + "id": "cq-9", + "question": "In private mode (PR #686 / #702), the gateway strips `WebSearch` / `WebFetch` tools from outbound requests because those route through Anthropic's infrastructure and bypass container network controls. What should the equivalent policy be when the upstream is LiteLLM \u2192 self-hosted Qwen on vLLM (no Anthropic-side tool processing)?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Keep the same tool-strip in private mode regardless of upstream \u2014 conservative; the agent simply cannot call these tools whether or not they would exfiltrate", + "description": null + }, + { + "id": "opt-2", + "label": "Strip only when upstream is Anthropic; allow these tools when upstream is fully self-hosted (no external request hop) \u2014 unblocks the tools but adds upstream-aware logic to the filter", + "description": null + }, + { + "id": "opt-3", + "label": "Keep the strip but document that the rationale only applies to Anthropic upstreams \u2014 defer the per-upstream rule to a future issue", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Keep the same tool-strip in private mode regardless of upstream \u2014 conservative; the agent simply cannot call these tools whether or not they would exfiltrate\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:16:24.174976Z", + "debounce_until": null + }, + { + "id": "cq-10", + "question": "How should this work be decomposed into slices?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Single slice: gateway router + LiteLLM topology + per-agent model config + acceptance-test agent flip, all together (1 PR)", + "description": null + }, + { + "id": "opt-2", + "label": "Two slices in parallel: [gateway upstream router + LiteLLM topology, no-op by default] || [per-agent model config + consensus_wrapper plumbing] (2 PRs) \u2014 acceptance-test agent flip becomes a follow-up", + "description": null + }, + { + "id": "opt-3", + "label": "Two slices with dependency: [gateway router + LiteLLM topology, no-op] \u2192 [per-agent model config + acceptance-test agent flip on top] (2 PRs)", + "description": null + }, + { + "id": "opt-4", + "label": "Three slices with dependency: [gateway router + LiteLLM topology, no-op] \u2192 [per-agent model config plumbing] \u2192 [acceptance-test agent flip + validation] (3 PRs)", + "description": null + }, + { + "id": "opt-5", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Defer slice decomposition to the plan phase \u2014 the planner should determine the actual slice DAG. The likely shape is two dependent slices: [gateway upstream router + LiteLLM topology, no-op by default] -> [per-agent model config + consensus_wrapper plumbing]. The acceptance-test agent flip is out of scope per cq-4. The planner owns the final decomposition.\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:16:24.209196Z", + "debounce_until": null + }, + { + "id": "cq-11", + "question": "Should the `[1m]` Claude-only context-window syntax baked into the existing defaults (`shared/egg_agent/client.py:62`, `shared/egg_agent/__main__.py:35`, `sandbox/llm/runner.py:49`) be addressed in this change?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Leave it: Claude defaults keep `opus[1m]`; only non-Claude paths use a different model string. The `[1m]` is harmless on the Claude path \u2014 zero risk to existing behavior", + "description": null + }, + { + "id": "opt-2", + "label": "Refactor: hoist the model string into a single config helper that strips `[1m]` when the resolved upstream is non-Claude \u2014 cleaner, but more churn outside the issue's scope", + "description": null + }, + { + "id": "opt-3", + "label": "Deprecate `opus[1m]` and adopt plain `opus` everywhere \u2014 lose the 1M context window for current Claude agents to keep model strings backend-agnostic", + "description": null + }, + { + "id": "opt-4", + "label": "Other (explain in reply)", + "description": null + } + ], + "resolved": true, + "resolution": "{\"action\": \"select\", \"selected\": \"Leave it: Claude defaults keep `opus[1m]`; only non-Claude paths use a different model string. The `[1m]` is harmless on the Claude path \u2014 zero risk to existing behavior\"}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:16:24.226176Z", + "debounce_until": null + }, + { + "id": "decision-12", + "question": "Open feedback request feedback-1", + "type": "hitl", + "phase": null, + "options": [], + "resolved": true, + "resolution": "{\"action\": \"submit_feedback\", \"answers\": {\"Q1\": \"Hosted Qwen provider first; no settled self-hosted hardware/budget yet \u2014 the plan should anchor on a hosted provider, not a specific GPU/vLLM/SGLang setup. Self-hosted vLLM is a later target.\", \"Q2\": \"Open-ended \u2014 no fixed target list of roles. The integration just needs to make any agent role independently selectable; which roles move to non-Claude is decided later. The first flip proves the seam.\", \"Q3\": \"Keep a clean swap-out point \u2014 design a thin UpstreamRouter/Registry abstraction so LiteLLM can be replaced if it hits a maintenance/supply-chain problem (the March 2026 PyPI incident is recent).\", \"Q4\": \"Defer to a follow-up \u2014 do not extend max_llm_cost_per_hour cost tracking to LiteLLM/Qwen token pricing in this issue.\", \"Q5\": \"No blocking compliance/data-residency constraint \u2014 code and transcripts may transit a hosted Qwen provider. The cq-6 hosted-provider choice stands.\"}}", + "resolved_by": "human", + "resolved_at": "2026-05-22T03:17:51.422450Z", + "debounce_until": null + } + ], + "workflow_owner": null, + "audit_log": [ + { + "timestamp": "2026-05-22T01:26:01.312976Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.0", + "old_value": null, + "new_value": { + "id": "cq-1", + "question": "Where should the LiteLLM proxy run, topologically?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Separate Deployment+Service in egg-system namespace (1 LiteLLM pod, gateway calls it over the cluster Service DNS)", + "description": null + }, + { + "id": "opt-2", + "label": "Sidecar container in the gateway pod (same pod, localhost call, shares lifecycle)", + "description": null + }, + { + "id": "opt-3", + "label": "Separate namespace `egg-llm` with its own NetworkPolicy (stronger isolation, more ops surface)", + "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: Where should the LiteLLM proxy run, topologically?", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:01.356914Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.1", + "old_value": null, + "new_value": { + "id": "cq-2", + "question": "How should the gateway decide which upstream (`api.anthropic.com` vs LiteLLM) to use for a given `/v1/messages` request?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Per-agent session metadata: orchestrator declares the model+upstream when it spawns the agent (session lookup by IP, same path used today for `session_mode`) \u2014 model name in the body is informational only", + "description": null + }, + { + "id": "opt-2", + "label": "Custom HTTP header from the sandbox (e.g. `X-Egg-Upstream: litellm`) injected at agent startup \u2014 gateway reads the header and routes accordingly", + "description": null + }, + { + "id": "opt-3", + "label": "Model name in the request body (any non-Claude model name \u2192 LiteLLM) \u2014 simplest but conflicts with the issue's compaction-mitigation note about presenting a recognized alias to Claude Code", + "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 gateway decide which upstream (`api...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:01.392718Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.2", + "old_value": null, + "new_value": { + "id": "cq-3", + "question": "How should per-agent model selection be configured (i.e. where does an operator say 'run the refiner on Qwen, leave the coder on Claude')?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "New per-role field on `PipelineConfig` (alongside `overseer_decision_maker_model` / `overseer_advisor_model`) \u2014 e.g. `agent_models: {refiner: 'qwen3-coder', coder: 'opus'}`", + "description": null + }, + { + "id": "opt-2", + "label": "Repository-level YAML config (`config/repositories.yaml` or similar) \u2014 operator edits once, applies to every pipeline on that repo", + "description": null + }, + { + "id": "opt-3", + "label": "Per-pipeline override only (CLI flag / API payload on submit_task) \u2014 no persistent per-role default, the operator names the override at submission time", + "description": null + }, + { + "id": "opt-4", + "label": "All of the above stacked, with precedence: CLI > pipeline config > repo config > built-in default 'opus'", + "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 per-agent model selection be configured...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:01.422663Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.3", + "old_value": null, + "new_value": { + "id": "cq-4", + "question": "Which agent role should be the first to be flipped to a non-Claude model for the empirical compatibility validation that the issue calls out as the acceptance test?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "A reviewer role (e.g. `reviewer_refine`) \u2014 reviewers do less tool-heavy work and have shorter sessions, so this is the lowest-risk first cut", + "description": null + }, + { + "id": "opt-2", + "label": "The `refiner` (this role) \u2014 produces analysis docs, modest tool use, easy to compare output against the Claude baseline", + "description": null + }, + { + "id": "opt-3", + "label": "The `coder` \u2014 most tool-heavy and longest-running role, so it stresses the auto-compaction edge case the issue flags as the primary risk", + "description": null + }, + { + "id": "opt-4", + "label": "An overseer tier (decision-maker or advisor) \u2014 already model-configurable today, so the plumbing is smaller, but it's less representative of the main SDLC loop", + "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: Which agent role should be the first to be flipped...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:28.809710Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.4", + "old_value": null, + "new_value": { + "id": "cq-5", + "question": "For the first non-Claude target, should agents continue to run inside the Claude Code harness (relying on the LiteLLM Anthropic-translation seam) or switch to the egg_agent SDK path?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Keep the Claude Code harness for non-Claude models too \u2014 use the recognized-alias mitigation so compaction math stays sane; minimum disruption to the spawning + entrypoint code", + "description": null + }, + { + "id": "opt-2", + "label": "Route non-Claude agents through the `egg_agent` SDK path (which already supports `--model`) and bypass Claude Code entirely \u2014 sidesteps the auto-compaction risk, but the SDK path has fewer integrations (statusline, settings.json rules) than the CLI", + "description": null + }, + { + "id": "opt-3", + "label": "Both: leave the harness choice as a per-role/per-model config knob \u2014 maximally flexible, but doubles the test surface for the first cut", + "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: For the first non-Claude target, should agents con...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:28.854437Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.5", + "old_value": null, + "new_value": { + "id": "cq-6", + "question": "What should the first acceptance-test backend be for the non-Claude path?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Self-hosted Qwen on vLLM/SGLang (matches the long-term cost goal and is the primary stated target) \u2014 requires standing up a vLLM Deployment + model weights as part of validation", + "description": null + }, + { + "id": "opt-2", + "label": "A hosted Qwen-compatible provider (e.g. Together, Fireworks, DeepInfra, OpenRouter) \u2014 fastest path to a live endpoint; defers the self-hosting work but adds a third-party dependency and a new credential to hold", + "description": null + }, + { + "id": "opt-3", + "label": "An OpenAI/other already-trusted backend behind LiteLLM as the literal first smoke test, with Qwen as the second cut \u2014 lowest validation risk; lets us decouple 'gateway routing works' from 'Qwen tool-calling works'", + "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: What should the first acceptance-test backend be f...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:28.889158Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.6", + "old_value": null, + "new_value": { + "id": "cq-7", + "question": "How should the gateway handle credentials for the LiteLLM upstream?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Gateway holds a LiteLLM master key in `secrets.env` and injects it on every LiteLLM-bound request (mirrors today's `ANTHROPIC_API_KEY` injection pattern) \u2014 LiteLLM holds the real per-backend keys", + "description": null + }, + { + "id": "opt-2", + "label": "LiteLLM runs with no auth, network-isolated to the gateway (NetworkPolicy or shared pod) and the gateway passes raw upstream credentials per-request \u2014 fewer secrets to hold, but pushes per-backend key management into the gateway", + "description": null + }, + { + "id": "opt-3", + "label": "Gateway holds nothing for LiteLLM; the sandbox sets its own per-agent API key via `extra_env` (e.g. operator-supplied) \u2014 inverts today's zero-credential sandbox invariant, so probably a non-starter", + "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 gateway handle credentials for the ...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:28.929847Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.7", + "old_value": null, + "new_value": { + "id": "cq-8", + "question": "When the LiteLLM proxy is unreachable / errors for a non-Claude agent, what is the failure policy?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Fail closed (502 to the agent, no fallback) \u2014 same policy as today's Claude upstream errors; surfaces the misconfig immediately", + "description": null + }, + { + "id": "opt-2", + "label": "Fall back to Claude on transient LiteLLM failures only \u2014 keeps the pipeline progressing but produces a quietly-mixed transcript and erodes the cost goal", + "description": null + }, + { + "id": "opt-3", + "label": "Fail closed but auto-escalate to a HITL decision when a non-Claude agent fails to spawn or stalls \u2014 best operator UX, most code to write", + "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: When the LiteLLM proxy is unreachable / errors for...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:54.840125Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.8", + "old_value": null, + "new_value": { + "id": "cq-9", + "question": "In private mode (PR #686 / #702), the gateway strips `WebSearch` / `WebFetch` tools from outbound requests because those route through Anthropic's infrastructure and bypass container network controls. What should the equivalent policy be when the upstream is LiteLLM \u2192 self-hosted Qwen on vLLM (no Anthropic-side tool processing)?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Keep the same tool-strip in private mode regardless of upstream \u2014 conservative; the agent simply cannot call these tools whether or not they would exfiltrate", + "description": null + }, + { + "id": "opt-2", + "label": "Strip only when upstream is Anthropic; allow these tools when upstream is fully self-hosted (no external request hop) \u2014 unblocks the tools but adds upstream-aware logic to the filter", + "description": null + }, + { + "id": "opt-3", + "label": "Keep the strip but document that the rationale only applies to Anthropic upstreams \u2014 defer the per-upstream rule to a future issue", + "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: In private mode (PR #686 / #702), the gateway stri...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:54.879471Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.9", + "old_value": null, + "new_value": { + "id": "cq-10", + "question": "How should this work be decomposed into slices?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Single slice: gateway router + LiteLLM topology + per-agent model config + acceptance-test agent flip, all together (1 PR)", + "description": null + }, + { + "id": "opt-2", + "label": "Two slices in parallel: [gateway upstream router + LiteLLM topology, no-op by default] || [per-agent model config + consensus_wrapper plumbing] (2 PRs) \u2014 acceptance-test agent flip becomes a follow-up", + "description": null + }, + { + "id": "opt-3", + "label": "Two slices with dependency: [gateway router + LiteLLM topology, no-op] \u2192 [per-agent model config + acceptance-test agent flip on top] (2 PRs)", + "description": null + }, + { + "id": "opt-4", + "label": "Three slices with dependency: [gateway router + LiteLLM topology, no-op] \u2192 [per-agent model config plumbing] \u2192 [acceptance-test agent flip + validation] (3 PRs)", + "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 work be decomposed into slices?", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:54.914295Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "decisions.10", + "old_value": null, + "new_value": { + "id": "cq-11", + "question": "Should the `[1m]` Claude-only context-window syntax baked into the existing defaults (`shared/egg_agent/client.py:62`, `shared/egg_agent/__main__.py:35`, `sandbox/llm/runner.py:49`) be addressed in this change?", + "type": "hitl", + "phase": "refine", + "options": [ + { + "id": "opt-1", + "label": "Leave it: Claude defaults keep `opus[1m]`; only non-Claude paths use a different model string. The `[1m]` is harmless on the Claude path \u2014 zero risk to existing behavior", + "description": null + }, + { + "id": "opt-2", + "label": "Refactor: hoist the model string into a single config helper that strips `[1m]` when the resolved upstream is non-Claude \u2014 cleaner, but more churn outside the issue's scope", + "description": null + }, + { + "id": "opt-3", + "label": "Deprecate `opus[1m]` and adopt plain `opus` everywhere \u2014 lose the 1M context window for current Claude agents to keep model strings backend-agnostic", + "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: Should the `[1m]` Claude-only context-window synta...", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T01:26:54.947689Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "feedback", + "old_value": null, + "new_value": { + "id": "feedback-1", + "phase": "refine", + "questions": [ + { + "id": "Q1", + "question": "For self-hosted Qwen specifically, are there hardware/budget constraints already settled (which GPU, how many, vLLM vs SGLang) that the plan should anchor on, or is the validation expected to use a hosted Qwen provider first regardless of the long-term self-hosted target?", + "answer": null + }, + { + "id": "Q2", + "question": "Is there a target list of agent roles you eventually want on non-Claude models (e.g. all reviewers, only refiner+tester, everything except coder), or is this open-ended and the first flip just proves the seam?", + "answer": null + }, + { + "id": "Q3", + "question": "The issue notes LiteLLM was chosen over claude-code-router, but does the design need to keep a clean swap-out point (e.g. an `UpstreamRouter` interface) in case LiteLLM hits a similar maintenance/supply-chain problem (the March 2026 PyPI incident is recent), or is hard-wiring LiteLLM acceptable for the first cut?", + "answer": null + }, + { + "id": "Q4", + "question": "The existing `max_llm_cost_per_hour` envelope assumes Anthropic-priced tokens. Should the implementation budget include extending cost tracking to LiteLLM/Qwen tokens in this issue, or defer to a follow-up?", + "answer": null + }, + { + "id": "Q5", + "question": "Are there any compliance / data-residency constraints (e.g. egg's source code or transcripts cannot transit a third-party Qwen hosting provider) that should rule out option B on the backend question (hosted Qwen) up front?", + "answer": null + } + ], + "submitted": false, + "submitted_by": null, + "submitted_at": null, + "comment_id": null, + "debounce_until": null + }, + "reason": "Created feedback request with 5 question(s)", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:39:44.938140Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.11.commit", + "old_value": null, + "new_value": "4eaed8b53a539538294cb0b7e2921eec37a51f8f", + "reason": "Linked commit 4eaed8b to task-1-12", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:39:44.949361Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.11.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-12 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:40:43.578335Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.11.commit", + "old_value": "4eaed8b53a539538294cb0b7e2921eec37a51f8f", + "new_value": "8c68062f024594fb44fe6f808ed6cc90749969f1", + "reason": "Linked commit 8c68062 to task-1-12", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:30.944296Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.0.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-1", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:30.962188Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.0.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-1 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:35.247700Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.1.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-2", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:35.262756Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.1.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-2 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:39.436597Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.2.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-3", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:39.452743Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.2.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-3 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:54.199939Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.3.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-4", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:54.230950Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.3.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-4 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:57.706723Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.4.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-5", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:58:57.725217Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.4.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-5 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:01.391972Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.5.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-6", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:01.417768Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.5.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-6 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:04.815008Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.6.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-7", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:04.839304Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.6.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-7 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:08.998691Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.7.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-8", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:09.027041Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.7.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-8 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:12.649503Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.8.commit", + "old_value": null, + "new_value": "dcf0caa5da7c37db06830c588ffb781d0173c418", + "reason": "Linked commit dcf0caa to task-1-9", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T05:59:12.675657Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.0.tasks.8.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-1-9 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T07:19:46.014088Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.1.tasks.6.commit", + "old_value": null, + "new_value": "a7658bf1e8934b91bd96e354ad11db0dd6e92dc2", + "reason": "Linked commit a7658bf to task-2-7", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T07:19:46.026451Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.1.tasks.6.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-2-7 as complete", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T07:19:49.229290Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.1.tasks.7.commit", + "old_value": null, + "new_value": "a7658bf1e8934b91bd96e354ad11db0dd6e92dc2", + "reason": "Linked commit a7658bf to task-2-8", + "checkpoint_id": null + }, + { + "timestamp": "2026-05-22T07:19:49.246613Z", + "actor": "egg", + "role": "implementer", + "action": "update", + "field_path": "phases.1.tasks.7.status", + "old_value": "pending", + "new_value": "complete", + "reason": "Marked task-2-8 as complete", + "checkpoint_id": null + } + ], + "refine_review_cycles": 0, + "refine_review_feedback": "", + "plan_review_cycles": 0, + "plan_review_feedback": "", + "pr": { + "title": "Add per-agent non-Claude model support via LiteLLM proxy", + "description": "Today every egg SDLC agent runs on Claude through the Claude Code\nharness, with the gateway hard-wiring `api.anthropic.com` as the\nonly `/v1/messages` upstream. The orchestrator's consensus wrapper\nhardcodes `--model opus` for every non-overseer role, so per-agent\nmodel selection does not exist. We need to let any agent run on a\nnon-Claude backend (Qwen is the first target, primarily for cost)\nwhile every Claude-bound agent stays byte-identically on the\nexisting path \u2014 and we need the integration to be safe to ship\nbefore a live non-Claude endpoint is available.\n\nThis change lands the buildable, no-op-by-default seam in two\nstacked PRs:\n\n1. **Gateway upstream router + LiteLLM Deployment (slice 1).**\n Introduces a small `UpstreamRegistry` abstraction in the\n gateway that keys per-request `httpx.Client` + credential by\n upstream name, with `proxy_anthropic_messages` /\n `proxy_count_tokens` resolving the upstream per request via\n the existing IP-keyed session lookup that already drives\n `session_mode`. Adds `Session.upstream` (default `\"anthropic\"`)\n and `Session.upstream_model` (default `None`), wired through\n `/api/v1/sessions/create`, `SessionManager.register_session`,\n and `GatewayClient.register_session`. The LiteLLM proxy itself\n ships as a separate Deployment + Service + ConfigMap in\n `egg-system`, reachable only by the gateway. The Claude path\n is structurally untouched.\n2. **Per-agent model config + spawn-side plumbing (slice 2).**\n Adds `PipelineConfig.agent_models: dict[str, str]` and a\n `default_agent_model` repository-level setting, plus a\n resolution function that the orchestrator's spawner calls to\n (a) thread the right `--model` to `build_consensus_wrapped_command`\n and (b) tell the gateway the per-agent `upstream` and\n `upstream_model` at session-create time. The gateway, on a\n LiteLLM-routed request, rewrites the body's `model` field\n from the Claude alias presented to Claude Code to the\n upstream-side model name \u2014 keeping Claude Code's compaction\n math sane (cq-5).\n\nWith `agent_models` empty (the default everywhere), no LiteLLM\nrequest fires. Every existing pipeline keeps running on Claude\nwith byte-identical gateway behavior. The empirical\nClaude-Code-compaction smoke test (cq-4) is an operator-driven\nfollow-up once a live non-Claude endpoint is configured; it is\nexplicitly out of scope here.", + "test_plan": "Automated:\n- `make test` from the repo root catches both slices' reachable\n suites given the changeset.\n- Slice 1: `tests/gateway/test_upstream_registry.py` (new),\n extensions to `tests/gateway/test_anthropic_proxy.py`,\n `tests/gateway/test_anthropic_credentials.py`,\n `gateway/tests/test_session_manager.py`,\n `orchestrator/tests/test_gateway_client.py`.\n- Slice 2: `orchestrator/tests/test_agent_model_resolution.py`\n (new), extensions to the concurrent-executor and\n pipeline-spawn tests, extensions to the gateway proxy tests\n covering the body-rewrite branch.\n\nManual (reviewer):\n- Confirm `make test` and `make lint` are green.\n- `kubectl apply --dry-run=client -k k8s/base/` succeeds with\n the new LiteLLM manifests included.\n- Spot-check that with `agent_models={}` and no\n `LITELLM_MASTER_KEY` in secrets, gateway request flow is\n byte-identical to today's Claude path (no new headers, no\n upstream change, same SSE behavior).\n\nManual (operator, post-merge \u2014 not gating merge):\n- Populate `LITELLM_MASTER_KEY` in `secrets.env`, configure\n LiteLLM `model_list` with a hosted Qwen provider (cq-6), set\n `agent_models={\"refiner\": \"qwen3-coder-30b\"}` on a pipeline,\n and exercise a tool-heavy multi-turn loop plus a long session\n crossing the auto-compaction boundary (the cq-4-deferred\n empirical compatibility check).", + "manual_steps": "Pre-merge: none.\n\nPost-merge (only required when operator wants to actually run an\nagent on a non-Claude backend):\n1. Add `LITELLM_MASTER_KEY=` to `~/.config/egg/secrets.env`.\n2. Populate the LiteLLM ConfigMap `model_list` with at least one\n backend (hosted Qwen provider first, per cq-6). The\n provider-side API key goes in LiteLLM's standard env-var slot,\n not in `secrets.env`.\n3. Either set `default_agent_model` in\n `~/.config/egg/repositories.yaml` for the target repo, or\n pass `agent_models={\"\": \"\"}` on pipeline submit\n to override per pipeline.", + "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": "For self-hosted Qwen specifically, are there hardware/budget constraints already settled (which GPU, how many, vLLM vs SGLang) that the plan should anchor on, or is the validation expected to use a hosted Qwen provider first regardless of the long-term self-hosted target?", + "answer": "Hosted Qwen provider first; no settled self-hosted hardware/budget yet \u2014 the plan should anchor on a hosted provider, not a specific GPU/vLLM/SGLang setup. Self-hosted vLLM is a later target." + }, + { + "id": "Q2", + "question": "Is there a target list of agent roles you eventually want on non-Claude models (e.g. all reviewers, only refiner+tester, everything except coder), or is this open-ended and the first flip just proves the seam?", + "answer": "Open-ended \u2014 no fixed target list of roles. The integration just needs to make any agent role independently selectable; which roles move to non-Claude is decided later. The first flip proves the seam." + }, + { + "id": "Q3", + "question": "The issue notes LiteLLM was chosen over claude-code-router, but does the design need to keep a clean swap-out point (e.g. an `UpstreamRouter` interface) in case LiteLLM hits a similar maintenance/supply-chain problem (the March 2026 PyPI incident is recent), or is hard-wiring LiteLLM acceptable for the first cut?", + "answer": "Keep a clean swap-out point \u2014 design a thin UpstreamRouter/Registry abstraction so LiteLLM can be replaced if it hits a maintenance/supply-chain problem (the March 2026 PyPI incident is recent)." + }, + { + "id": "Q4", + "question": "The existing `max_llm_cost_per_hour` envelope assumes Anthropic-priced tokens. Should the implementation budget include extending cost tracking to LiteLLM/Qwen tokens in this issue, or defer to a follow-up?", + "answer": "Defer to a follow-up \u2014 do not extend max_llm_cost_per_hour cost tracking to LiteLLM/Qwen token pricing in this issue." + }, + { + "id": "Q5", + "question": "Are there any compliance / data-residency constraints (e.g. egg's source code or transcripts cannot transit a third-party Qwen hosting provider) that should rule out option B on the backend question (hosted Qwen) up front?", + "answer": "No blocking compliance/data-residency constraint \u2014 code and transcripts may transit a hosted Qwen provider. The cq-6 hosted-provider choice stands." + } + ], + "submitted": true, + "submitted_by": "human", + "submitted_at": "2026-05-22T03:17:54.498829Z", + "comment_id": null, + "debounce_until": null + }, + "phase_configs": null, + "agent_executions": [] +} diff --git a/.egg-state/drafts/2769-analysis.md b/.egg-state/drafts/2769-analysis.md new file mode 100644 index 0000000000..94da3f8edd --- /dev/null +++ b/.egg-state/drafts/2769-analysis.md @@ -0,0 +1,344 @@ +# Analysis: Support non-Claude models per agent via a LiteLLM proxy + +> Issue: #2769 | Phase: refine + +## Problem Statement + +egg today runs every SDLC agent on Claude via the Claude Agent SDK and Claude Code harness, with the gateway sidecar injecting Anthropic credentials. We want to **run agents on non-Claude models — primarily self-hosted Qwen as the first cost-cutting target — in parallel with Claude**, with **each agent independently selectable**, and **with zero regression for agents that stay on Claude**. + +The chosen vehicle is a **LiteLLM proxy** that exposes an Anthropic-compatible `/v1/messages` endpoint and translates to OpenAI-compatible backends (vLLM-hosted Qwen, hosted providers, etc.). The **gateway becomes a per-agent / per-model upstream router**: Anthropic-bound requests continue straight to `api.anthropic.com`; non-Claude-bound requests divert to LiteLLM. + +A side benefit is reduced coupling to Claude Code as the harness: routing through a translation layer creates the seam needed to swap harnesses later if we want. + +`claude-code-router` was evaluated and rejected for being effectively unmaintained and corrupting streaming tool-call arguments for Qwen thinking-mode models. LiteLLM is actively maintained and exposes a real Anthropic `/v1/messages` translator. (Note: the broader supply-chain risk of any third-party translator is real — see [LiteLLM's March 2026 PyPI incident](https://docs.litellm.ai/docs/) — but does not invalidate the choice.) + +## Current Behavior + +**Gateway upstream is hard-wired to Anthropic.** In `gateway/gateway.py`: + +```python +def get_anthropic_client() -> httpx.Client: + """Get or create the singleton Anthropic API client.""" + global _anthropic_client + if _anthropic_client is None: + _anthropic_client = httpx.Client( + base_url="https://api.anthropic.com", # noqa: EGG200 - gateway proxy client, not direct LLM call + timeout=httpx.Timeout(120.0, connect=10.0), + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + return _anthropic_client +``` + +Both proxy entry points — `proxy_anthropic_messages()` (`gateway/gateway.py:9752`, the `POST /v1/messages` handler) and `proxy_count_tokens()` (`gateway/gateway.py:10019`) — fetch this singleton client and forward the request body unchanged after credential injection. The streaming-resilience logic, the per-request `_SSEAccumulator` (`gateway/gateway.py:9552`), the private-mode `_filter_blocked_tools()` tool-strip (`gateway/gateway.py:9410`), and the credential injection (`_inject_anthropic_credentials()`, `gateway/gateway.py:9355`) all sit on top of this single upstream. + +**Per-agent model config is minimal today.** Three knobs exist: + +| Field | File | Default | Notes | +|-------|------|---------|-------| +| `overseer_decision_maker_model` | `orchestrator/models.py:546` | `sonnet` | Overseer Tier-2 model, used in `kubernetes_spawner.py:1582` / `overseer/monitor.py:259` / etc. | +| `overseer_advisor_model` | `orchestrator/models.py:620` | `opus` | Overseer Tier-1 advisor model | +| `model: str = "opus"` arg | `orchestrator/consensus_wrapper.py:622` | `opus` | Hardcoded in `build_consensus_wrapped_command()`; **no caller overrides it today** | + +The SDLC roles that actually run the BRC consensus loop (refiner, reviewer_*, planner, coder, tester, etc.) all reach the agent through `build_consensus_wrapped_command()`: + +- `orchestrator/concurrent_executor.py:454` calls `build_consensus_wrapped_command(prompt_text)` (no model arg). +- `orchestrator/routes/pipelines.py:2704` does the same on restart. + +The wrapper builds the agent command as `python3 -m egg_agent --model opus --max-turns 1000 ...` (`consensus_wrapper.py:653-662`). So **every non-overseer agent today is opus-only, by hardcoding, not by configuration**. + +**`[1m]` is Claude-only context-window syntax** and is baked into the in-process defaults at three sites: + +- `shared/egg_agent/client.py:62` — `DEFAULT_MODEL = "opus[1m]"` +- `shared/egg_agent/__main__.py:35` — `parser.add_argument("--model", default="opus[1m]", ...)` +- `sandbox/llm/runner.py:49` — `cmd.extend(["--model", "opus[1m]"])` (legacy interactive CLI path) + +A non-Claude model name with `[1m]` appended will be rejected upstream. + +**The sandbox is zero-credential and already routes through the gateway.** `orchestrator/kubernetes_spawner.py:807` sets `ANTHROPIC_BASE_URL=GATEWAY_K8S_URL` on every agent pod, plus a placeholder OAuth token that the gateway strips and replaces with a real credential server-side. `gateway/allowed_domains.txt:9-15` confirms that `api.anthropic.com` is **intentionally not in the Squid allowlist** — every Anthropic-bound byte is forced through the gateway proxy endpoint. **This is what makes the gateway the right routing point**: the sandbox cannot bypass it even if it wanted to. + +**Primary risk surface: Claude Code's model-name assumptions.** Claude Code derives the context window — and therefore auto-compaction timing — from the model name (`/context` shows the components; auto-compact triggers around 95–98% of the model's *known* limit). It also gates features like extended thinking on recognized model names. Compaction in particular is documented as supported only for [recognized Claude model families](https://platform.claude.com/docs/en/build-with-claude/compaction); an unrecognized name falls back to a default window that **will not match a Qwen backend's real limit** and can produce over-length requests that hard-fail and wedge the agent on long sessions. The issue body and community write-ups ([dev.to "Running Claude Code with Local LLMs via vLLM and LiteLLM"](https://dev.to/dcruver/running-claude-code-with-local-llms-via-vllm-and-litellm-599b), [okhlopkov.com on compaction](https://okhlopkov.com/claude-code-compaction-explained/)) converge on one mitigation: **present Claude Code a recognized, conservative model alias** in the request body, and **route on a separate per-agent signal**. + +## Constraints + +- **No regression on the Claude path.** Existing agents keep running on `api.anthropic.com` with no behavior change. The router must be inert by default — same wire format, same credential injection, same SSE accumulator, same tool-strip, same stream-resilience logic. Routing is additive. +- **Routing point sits below the SSE accumulator, tool-filter, and stream-resilience logic.** All three rely on the Anthropic SSE wire format, which is exactly what LiteLLM emits. The router must choose upstream **after** request validation and credential resolution but **before** `client.send(http_req, stream=True)`. +- **Zero-credential sandbox invariant must hold.** The sandbox today has no Anthropic credentials; same must be true for LiteLLM. Whoever holds the LiteLLM master key lives outside the sandbox (gateway or LiteLLM pod). +- **Gateway-mediated visibility must hold.** All non-Claude traffic must still flow through the gateway so today's audit logging, transcript capture, and tool-strip apply uniformly. LiteLLM must not be directly reachable from sandbox pods. +- **Network policy.** Squid's `allowed_domains.txt` deliberately excludes `api.anthropic.com`; the same hands-off treatment should apply to any new LiteLLM upstream — agents reach it only through the gateway, never directly. +- **Independent selection per agent.** The plumbing must let one agent be on Qwen while another in the same pipeline is on Claude. This is non-trivial: today the model choice is decided where the consensus wrapper is built, not where the request leaves the gateway. +- **No pinned model snapshot versions.** Use model aliases (e.g. `opus`, `sonnet`, `qwen3-coder-30b`) — matches the existing overseer-model defaults. +- **File-size discipline (#2261).** `gateway/gateway.py` is ~10K lines and `orchestrator/routes/pipelines.py` is ~16K. The plan should land new symbols in dedicated sub-modules (per the in-flight decomposition tables in `gateway/CLAUDE.md` and `orchestrator/CLAUDE.md`) rather than accreting more into the barrels. +- **Build-now / validate-later split.** End-to-end validation requires a live non-Claude endpoint that is not yet set up. The integration ships **no-op by default** (Claude path untouched, LiteLLM routing inert until configured), so the no-op slice is buildable, testable, and reviewable independent of having a Qwen endpoint live. + +## Options Considered + +### Option A: Gateway-side upstream router + per-agent session metadata (recommended) + +**Approach**: Decompose `get_anthropic_client()` into a tiny `UpstreamRegistry` keyed by upstream name (`anthropic`, `litellm`). `proxy_anthropic_messages()` and `proxy_count_tokens()` resolve the upstream **per request**, using the same IP-based session lookup that already drives `session_mode` (`gateway/gateway.py:9774`). The orchestrator records the per-agent upstream + model when it creates the gateway session at spawn time (the same place it already records `mode`). `_inject_anthropic_credentials()` becomes upstream-aware so LiteLLM-bound requests get the LiteLLM master key instead of the Anthropic OAuth / API key. The SSE accumulator, tool-strip, and stream-resilience logic are upstream-agnostic and unchanged. + +LiteLLM runs as a **separate Deployment in `egg-system`** with a Service the gateway reaches over cluster DNS. The model name *in the request body* stays a recognized Claude alias (per the issue's mitigation), so Claude Code's compaction math stays correct; the **router decides on per-agent session metadata, not on the body**. + +**Pros**: +- Claude path is structurally unchanged — same singleton client (now keyed by `"anthropic"`), same credential path, same SSE plumbing. +- Inert by default: with no agent configured for a non-Claude model, no LiteLLM request ever fires. +- Decouples model name from upstream — works around the Claude Code compaction-math limitation the issue flags as the primary risk without lying to the upstream API (LiteLLM still receives the real model name from a header or per-session override; only Claude Code sees the alias). +- Per-agent granularity falls out naturally from the IP-keyed session metadata that already exists. +- LiteLLM blast radius (PyPI supply-chain, version pinning, restart cycles) is contained to one pod separate from the gateway. + +**Cons**: +- Two source-of-truth coordination points: orchestrator declares the per-agent upstream when it creates the gateway session; agent's `--model` string must agree with what the gateway will route. A drift here produces a wrong-backend request. (Mitigation: spawner derives both from the same per-role config field.) +- Adds a per-role config field to `PipelineConfig` (plus precedence rules if we layer repo / CLI overrides). +- Adds an LiteLLM deployment to operate (image, version pin / cosign verification, model config YAML, restart policy). + +### Option B: LiteLLM-fronts-everything + +**Approach**: All `/v1/messages` traffic — Claude and non-Claude — flows through LiteLLM. LiteLLM's model-list does the upstream selection based on model name. Gateway forwards verbatim to LiteLLM and forgets about Anthropic entirely. + +**Pros**: +- Single upstream from the gateway's perspective — `get_anthropic_client()` keeps its singleton shape, just with a different `base_url`. +- LiteLLM's `model_list` is purpose-built for the "claude-* → anthropic, qwen-* → vllm" mapping pattern. + +**Cons**: +- Adds a hop (and an extra failure mode, supply-chain blast radius, restart-cycle dependency) to **every Claude request**, including the existing production path. +- Issue explicitly says "the existing Claude path must keep working unchanged — no regression for agents that stay on Claude". This option fails that gate. +- Auto-compaction risk gets worse, not better: now even Claude requests transit LiteLLM, so any LiteLLM-side transformation of the model name reaches Claude Code's compaction math. + +### Option C: Decide on the model name in the request body + +**Approach**: `proxy_anthropic_messages()` parses the request body's `model` field and routes to Anthropic if it starts with `claude-`, otherwise to LiteLLM. No new orchestrator plumbing, no per-agent session metadata. + +**Pros**: +- Smallest gateway change (one regex / prefix check inside the existing handler). +- No orchestrator coordination required — the model string in the agent's `--model` flag is the single source of truth. + +**Cons**: +- **Directly conflicts with the issue's primary-risk mitigation.** The mitigation requires presenting Claude Code a *recognized Claude alias* even when the actual backend is Qwen, so that Claude Code's compaction math stays sane. Routing on the body means we cannot do that — we have to lie to *both* Claude Code and the gateway. +- Couples upstream selection to model-name conventions: if Anthropic ever ships a non-`claude-*`-prefixed model (or LiteLLM exposes a `claude-*`-aliased Qwen), the router breaks silently. + +### Option D: Per-agent `egg_agent` SDK path, bypass Claude Code entirely + +**Approach**: For non-Claude agents, swap the consensus-wrapper's `python3 -m egg_agent` invocation away from Claude Code (which the issue identifies as the primary risk surface) and use the egg_agent SDK directly with `--model qwen-...`. The gateway still routes per-agent, but the Claude-Code-harness compatibility surface drops out of the problem. + +**Pros**: +- Sidesteps the Claude Code auto-compaction and feature-gating risks entirely (no Claude Code in the loop for non-Claude agents). +- The `egg_agent` SDK path (`shared/egg_agent/client.py`) already takes `--model` as a string and passes it through to the Claude Agent SDK, so the model wiring is small. + +**Cons**: +- The SDK path has fewer integrations than the CLI (statusline, `settings.json` rules, the in-process MCP servers registered by `egg_agent.client` are wired but the slash-command / claude-rules surface is not). +- Doubles the test surface for the first cut: now there are two harnesses to validate, not one. +- Adds a "which harness?" config knob to the orchestrator plumbing. + +## Recommended Approach + +**Option A** is recommended. + +It is the only option that satisfies the issue's hard constraints: +1. Claude path remains unchanged in behavior, structure, and risk profile. +2. Per-agent model selection is real (not a global swap). +3. The compaction-math mitigation is supported (route on session metadata, not on the body — so Claude Code can see `opus` while the gateway routes to Qwen via LiteLLM). +4. The router sits below the SSE accumulator, tool-strip, and stream-resilience logic, so they all keep working untouched. + +It also matches the existing architecture's grain: the gateway is already the per-request policy point, IP-based session lookup is already the way per-agent state reaches request handlers, and an additive Deployment in `egg-system` is the standard pattern (see `gateway`, `orchestrator`). LiteLLM blast radius is contained to one pod. + +Open questions on topology, routing signal, configuration shape, validation target, harness choice, credential handling, failure policy, and slice decomposition are surfaced below and gated on the operator. + +## Runtime-primitive assumptions for the downstream plan (#2594) + +The plan phase will rely on these primitives existing in their named files/shapes: + +| Primitive | Where it lives today | Used by | Execution context | +|-----------|----------------------|---------|--------------------| +| `_anthropic_client` singleton + `get_anthropic_client()` | `gateway/gateway.py:9316-9329` | `proxy_anthropic_messages` (`gateway/gateway.py:9789`), `proxy_count_tokens` (`gateway/gateway.py:10032`) | Gateway pod (`egg-system`) | +| `proxy_anthropic_messages()` route `POST /v1/messages` | `gateway/gateway.py:9752` | Sandbox `ANTHROPIC_BASE_URL` traffic | Gateway pod | +| `proxy_count_tokens()` route `POST /v1/messages/count_tokens` | `gateway/gateway.py:10019` | Same | Gateway pod | +| `_inject_anthropic_credentials()` | `gateway/gateway.py:9355` | Both proxy routes | Gateway pod | +| `get_credentials_manager().get_credential()` (returns `Credential` with `header_name`, `header_value`) | `gateway/anthropic_credentials.py` (imported at `gateway/gateway.py:87` / `:217`) | `_inject_anthropic_credentials` | Gateway pod | +| `_filter_blocked_tools(request_body, session_mode)` | `gateway/gateway.py:9410` | `proxy_anthropic_messages` | Gateway pod | +| `_SSEAccumulator` (incremental SSE parser) | `gateway/gateway.py:9552` | `proxy_anthropic_messages` streaming branch | Gateway pod | +| `get_session_manager().get_session_by_ip(addr)` (returns session with `.mode`, `.container_id`) | imported `gateway/gateway.py:200` from `gateway/session_manager.py` | `proxy_anthropic_messages` (`gateway/gateway.py:9775`) | Gateway pod — this is the routing-signal carrier in the recommended option | +| `build_consensus_wrapped_command(prompt_text, model="opus", ...)` | `orchestrator/consensus_wrapper.py:620` | Spawn (`orchestrator/concurrent_executor.py:454`) + restart (`orchestrator/routes/pipelines.py:2704`) | Trusted CI runner (orchestrator pod) | +| `PipelineConfig.overseer_decision_maker_model` (`str`, default `"sonnet"`) | `orchestrator/models.py:546` | `pipelines.py:536`, `pipelines.py:20509`, `kubernetes_spawner.py:1596`, `overseer/monitor.py:259` | Trusted CI runner | +| `PipelineConfig.overseer_advisor_model` (`str`, default `"opus"`) | `orchestrator/models.py:620` | `pipelines.py:3633` and overseer advisor invocation | Trusted CI runner | +| `KubernetesSpawner._PROTECTED_ENV_KEYS` | `orchestrator/kubernetes_spawner.py:138` | `_resolve_wait_producer_allowlist` and pod env composition (`kubernetes_spawner.py:789-892`) | Trusted CI runner | +| `environment["ANTHROPIC_BASE_URL"] = GATEWAY_K8S_URL` | `orchestrator/kubernetes_spawner.py:807` | Every spawned agent pod | Trusted CI runner (sets in-sandbox env) | +| `environment["EGG_AGENT_ROLE"]`, `environment["EGG_AGENT_*"]` plumbing pattern | `orchestrator/kubernetes_spawner.py:792` | Sandbox env | Trusted CI runner | +| Sandbox `ANTHROPIC_BASE_URL` setup | `sandbox/entrypoint.py:737-738` (`setup_anthropic_api()`) | In-sandbox-agent | In-sandbox agent | +| `DEFAULT_MODEL = "opus[1m]"` | `shared/egg_agent/client.py:62` | `egg_agent.client.run_agent_async` | In-sandbox agent | +| `--model` default `"opus[1m]"` CLI flag | `shared/egg_agent/__main__.py:35` | `python3 -m egg_agent` | In-sandbox agent | +| `["--model", "opus[1m]"]` Claude CLI invocation | `sandbox/llm/runner.py:49` | Legacy interactive runner | In-sandbox agent | +| `allowed_domains.txt` — Anthropic-excluded Squid allowlist (`gateway/allowed_domains.txt:9-15`) | Squid config in gateway pod | Outbound sandbox HTTPS | Gateway pod | +| K8s Service DNS `gateway.egg-system.svc.cluster.local:9848` (`GATEWAY_K8S_URL`) | `orchestrator/kubernetes_spawner.py:124` | All sandbox pods | Cluster-wide DNS | +| `k8s/base/gateway-deployment.yaml` Anthropic/Atlassian secret layout (`secrets.env` mount at `/secrets`) | `k8s/base/gateway-deployment.yaml:71-118` | Where any new LiteLLM master key would land | Cluster | + +## Open Questions + +### Resolved in Pre-Refine + +_(None — the issue body has no `## Additional Context` section. Every question below is genuinely open.)_ + +### Decisions + + + +**Where should the LiteLLM proxy run, topologically?** + +- [ ] Separate Deployment+Service in egg-system namespace (1 LiteLLM pod, gateway calls it over the cluster Service DNS) +- [ ] Sidecar container in the gateway pod (same pod, localhost call, shares lifecycle) +- [ ] Separate namespace `egg-llm` with its own NetworkPolicy (stronger isolation, more ops surface) +- [ ] Other (explain in reply) + + + +**How should the gateway decide which upstream (`api.anthropic.com` vs LiteLLM) to use for a given `/v1/messages` request?** + +- [ ] Per-agent session metadata: orchestrator declares the model+upstream when it spawns the agent (session lookup by IP, same path used today for `session_mode`) — model name in the body is informational only +- [ ] Custom HTTP header from the sandbox (e.g. `X-Egg-Upstream: litellm`) injected at agent startup — gateway reads the header and routes accordingly +- [ ] Model name in the request body (any non-Claude model name → LiteLLM) — simplest but conflicts with the issue's compaction-mitigation note about presenting a recognized alias to Claude Code +- [ ] Other (explain in reply) + + + +**How should per-agent model selection be configured (i.e. where does an operator say 'run the refiner on Qwen, leave the coder on Claude')?** + +- [ ] New per-role field on `PipelineConfig` (alongside `overseer_decision_maker_model` / `overseer_advisor_model`) — e.g. `agent_models: {refiner: 'qwen3-coder', coder: 'opus'}` +- [ ] Repository-level YAML config (`config/repositories.yaml` or similar) — operator edits once, applies to every pipeline on that repo +- [ ] Per-pipeline override only (CLI flag / API payload on submit_task) — no persistent per-role default, the operator names the override at submission time +- [ ] All of the above stacked, with precedence: CLI > pipeline config > repo config > built-in default 'opus' +- [ ] Other (explain in reply) + + + +**Which agent role should be the first to be flipped to a non-Claude model for the empirical compatibility validation that the issue calls out as the acceptance test?** + +- [ ] A reviewer role (e.g. `reviewer_refine`) — reviewers do less tool-heavy work and have shorter sessions, so this is the lowest-risk first cut +- [ ] The `refiner` (this role) — produces analysis docs, modest tool use, easy to compare output against the Claude baseline +- [ ] The `coder` — most tool-heavy and longest-running role, so it stresses the auto-compaction edge case the issue flags as the primary risk +- [ ] An overseer tier (decision-maker or advisor) — already model-configurable today, so the plumbing is smaller, but it's less representative of the main SDLC loop +- [ ] Other (explain in reply) + + + +**For the first non-Claude target, should agents continue to run inside the Claude Code harness (relying on the LiteLLM Anthropic-translation seam) or switch to the egg_agent SDK path?** + +- [ ] Keep the Claude Code harness for non-Claude models too — use the recognized-alias mitigation so compaction math stays sane; minimum disruption to the spawning + entrypoint code +- [ ] Route non-Claude agents through the `egg_agent` SDK path (which already supports `--model`) and bypass Claude Code entirely — sidesteps the auto-compaction risk, but the SDK path has fewer integrations (statusline, settings.json rules) than the CLI +- [ ] Both: leave the harness choice as a per-role/per-model config knob — maximally flexible, but doubles the test surface for the first cut +- [ ] Other (explain in reply) + + + +**What should the first acceptance-test backend be for the non-Claude path?** + +- [ ] Self-hosted Qwen on vLLM/SGLang (matches the long-term cost goal and is the primary stated target) — requires standing up a vLLM Deployment + model weights as part of validation +- [ ] A hosted Qwen-compatible provider (e.g. Together, Fireworks, DeepInfra, OpenRouter) — fastest path to a live endpoint; defers the self-hosting work but adds a third-party dependency and a new credential to hold +- [ ] An OpenAI/other already-trusted backend behind LiteLLM as the literal first smoke test, with Qwen as the second cut — lowest validation risk; lets us decouple 'gateway routing works' from 'Qwen tool-calling works' +- [ ] Other (explain in reply) + + + +**How should the gateway handle credentials for the LiteLLM upstream?** + +- [ ] Gateway holds a LiteLLM master key in `secrets.env` and injects it on every LiteLLM-bound request (mirrors today's `ANTHROPIC_API_KEY` injection pattern) — LiteLLM holds the real per-backend keys +- [ ] LiteLLM runs with no auth, network-isolated to the gateway (NetworkPolicy or shared pod) and the gateway passes raw upstream credentials per-request — fewer secrets to hold, but pushes per-backend key management into the gateway +- [ ] Gateway holds nothing for LiteLLM; the sandbox sets its own per-agent API key via `extra_env` (e.g. operator-supplied) — inverts today's zero-credential sandbox invariant, so probably a non-starter +- [ ] Other (explain in reply) + + + +**When the LiteLLM proxy is unreachable / errors for a non-Claude agent, what is the failure policy?** + +- [ ] Fail closed (502 to the agent, no fallback) — same policy as today's Claude upstream errors; surfaces the misconfig immediately +- [ ] Fall back to Claude on transient LiteLLM failures only — keeps the pipeline progressing but produces a quietly-mixed transcript and erodes the cost goal +- [ ] Fail closed but auto-escalate to a HITL decision when a non-Claude agent fails to spawn or stalls — best operator UX, most code to write +- [ ] Other (explain in reply) + + + +**In private mode (PR #686 / #702), the gateway strips `WebSearch` / `WebFetch` tools from outbound requests because those route through Anthropic's infrastructure and bypass container network controls. What should the equivalent policy be when the upstream is LiteLLM → self-hosted Qwen on vLLM (no Anthropic-side tool processing)?** + +- [ ] Keep the same tool-strip in private mode regardless of upstream — conservative; the agent simply cannot call these tools whether or not they would exfiltrate +- [ ] Strip only when upstream is Anthropic; allow these tools when upstream is fully self-hosted (no external request hop) — unblocks the tools but adds upstream-aware logic to the filter +- [ ] Keep the strip but document that the rationale only applies to Anthropic upstreams — defer the per-upstream rule to a future issue +- [ ] Other (explain in reply) + + + +**How should this work be decomposed into slices?** + +- [ ] Single slice: gateway router + LiteLLM topology + per-agent model config + acceptance-test agent flip, all together (1 PR) +- [ ] Two slices in parallel: [gateway upstream router + LiteLLM topology, no-op by default] || [per-agent model config + consensus_wrapper plumbing] (2 PRs) — acceptance-test agent flip becomes a follow-up +- [ ] Two slices with dependency: [gateway router + LiteLLM topology, no-op] → [per-agent model config + acceptance-test agent flip on top] (2 PRs) +- [ ] Three slices with dependency: [gateway router + LiteLLM topology, no-op] → [per-agent model config plumbing] → [acceptance-test agent flip + validation] (3 PRs) +- [ ] Other (explain in reply) + + + +**Should the `[1m]` Claude-only context-window syntax baked into the existing defaults (`shared/egg_agent/client.py:62`, `shared/egg_agent/__main__.py:35`, `sandbox/llm/runner.py:49`) be addressed in this change?** + +- [ ] Leave it: Claude defaults keep `opus[1m]`; only non-Claude paths use a different model string. The `[1m]` is harmless on the Claude path — zero risk to existing behavior +- [ ] Refactor: hoist the model string into a single config helper that strips `[1m]` when the resolved upstream is non-Claude — cleaner, but more churn outside the issue's scope +- [ ] Deprecate `opus[1m]` and adopt plain `opus` everywhere — lose the 1M context window for current Claude agents to keep model strings backend-agnostic +- [ ] Other (explain in reply) + +### Feedback + + + +## Questions & Feedback + +Please **edit this comment** to answer questions or provide feedback. +When you're done, check the box below to submit. + +--- + +### Open Questions + +**Q1: For self-hosted Qwen specifically, are there hardware/budget constraints already settled (which GPU, how many, vLLM vs SGLang) that the plan should anchor on, or is the validation expected to use a hosted Qwen provider first regardless of the long-term self-hosted target?** + +> _Your answer here_ + +**Q2: Is there a target list of agent roles you eventually want on non-Claude models (e.g. all reviewers, only refiner+tester, everything except coder), or is this open-ended and the first flip just proves the seam?** + +> _Your answer here_ + +**Q3: The issue notes LiteLLM was chosen over claude-code-router, but does the design need to keep a clean swap-out point (e.g. an `UpstreamRouter` interface) in case LiteLLM hits a similar maintenance/supply-chain problem (the March 2026 PyPI incident is recent), or is hard-wiring LiteLLM acceptable for the first cut?** + +> _Your answer here_ + +**Q4: The existing `max_llm_cost_per_hour` envelope assumes Anthropic-priced tokens. Should the implementation budget include extending cost tracking to LiteLLM/Qwen tokens in this issue, or defer to a follow-up?** + +> _Your answer here_ + +**Q5: Are there any compliance / data-residency constraints (e.g. egg's source code or transcripts cannot transit a third-party Qwen hosting provider) that should rule out option B on the backend question (hosted Qwen) up front?** + +> _Your answer here_ + +--- + +### Additional Feedback (optional) + +> _Add any other feedback or context here_ + +--- + +- [ ] Submit feedback (I'm done editing) + +--- + +## Complexity Assessment + +**high** — this is a cross-cutting architectural change. It touches: + +- a new long-running cluster component (LiteLLM Deployment + Service + secrets) +- the gateway's per-request upstream selection (currently a singleton) +- the gateway's credential injection (currently single-provider) +- new per-agent / per-role configuration in `PipelineConfig` (and possibly repo config and CLI) +- the orchestrator's spawn path (the consensus wrapper's hardcoded model arg) and the spawner's pod-env composition +- an empirical compatibility-validation surface (Claude Code's compaction math against a real non-Claude backend) +- arguably a new harness path if option D wins + +It is decomposable into at least two independent slices (gateway router + LiteLLM topology as a no-op shipment, then per-agent model config + acceptance-test flip on top), which is reflected in cq-10. + +--- + +*Authored-by: egg* diff --git a/.egg-state/drafts/2769-plan.md b/.egg-state/drafts/2769-plan.md new file mode 100644 index 0000000000..3bfb04afee --- /dev/null +++ b/.egg-state/drafts/2769-plan.md @@ -0,0 +1,1027 @@ +# Plan: Support non-Claude models per agent via a LiteLLM proxy + +> Issue: #2769 | Phase: plan | Refine artifact: `.egg-state/drafts/2769-analysis.md` + +## Goal + +Land a no-op-by-default LiteLLM seam that lets the gateway route any +individual agent's `/v1/messages` traffic to a non-Claude backend +through a translation proxy, while every existing Claude-bound agent +continues to talk to `api.anthropic.com` unchanged. + +The work decomposes into **two dependent slices** (per the +operator-resolved cq-10 on issue #2769): + +1. **Slice 1 — Gateway upstream router + LiteLLM Deployment, inert by + default.** Per-request upstream selection seam plus the LiteLLM + pod, with no agent yet configured to use it. +2. **Slice 2 — Per-agent model config + spawn-side plumbing.** + `PipelineConfig.agent_models` + `repositories.yaml`-level default, + resolution in the spawner, body rewrite on the gateway, end-to-end + wiring from a per-pipeline knob through the consensus wrapper to + the gateway router. + +The empirical acceptance-test agent flip is **out of scope** for this +pipeline (cq-4 resolution: the operator validates separately once a +non-Claude endpoint is live; this work ships the buildable, no-op +integration only). + +## Architecture recap (from refine cq resolutions) + +| HITL | Resolved choice | Implication on the plan | +|------|-----------------|--------------------------| +| cq-1 | LiteLLM as **separate Deployment+Service in egg-system** | New `k8s/base/litellm-{deployment,service,configmap}.yaml` + `kustomization.yaml` entry | +| cq-2 | Route on **per-agent session metadata** (IP-keyed session lookup, same path as today's `session_mode`) | Add `upstream` (+ `upstream_model`) to `Session`; orchestrator declares both at session-create time | +| cq-3 | **Per-role field on `PipelineConfig`** + `repositories.yaml` default | New `agent_models: dict[str, str]` field + `default_agent_model: str` repo-level setting | +| cq-4 | **No acceptance-test agent flip** in this pipeline | Slice 2 stops at "plumbing is correct"; no `agent_models` entry is shipped pointing to LiteLLM | +| cq-5 | **Keep Claude Code harness** for non-Claude; rely on recognized-alias mitigation | Agent's `--model` flag stays a Claude alias (e.g. `opus`); gateway rewrites body's `model` field to the LiteLLM-side name before forwarding | +| cq-6 | First validation backend is a **hosted Qwen provider** | LiteLLM config supports hosted providers (Together / Fireworks / DeepInfra / OpenRouter) via standard LiteLLM `model_list` entries. Self-hosted vLLM is deferred. | +| cq-7 | Gateway holds a **LiteLLM master key in `secrets.env`**, injects it on every LiteLLM-bound request | New credential reader keyed on `LITELLM_MASTER_KEY`; `_inject_anthropic_credentials` becomes upstream-aware | +| cq-8 | **Fail closed** on LiteLLM unreachable | Mirror today's Anthropic 502 path — no fallback to Claude | +| cq-9 | Keep tool-strip in private mode regardless of upstream | `_filter_blocked_tools()` is upstream-agnostic; no change | +| cq-10 | **Two dependent slices** | This plan's slice DAG | +| cq-11 | Leave `[1m]` for Claude | Non-Claude paths simply use a different model alias on the wire; no `[1m]` refactor in this issue | +| feedback Q3 | Keep a **swap-out interface** in case LiteLLM falls over | Introduce `UpstreamRegistry` (gateway-side abstraction). LiteLLM is the only initial non-`anthropic` registration, but the registration shape can accept future translators. | +| feedback Q4 | Defer cost tracking | Out of scope. | + +## Primitives + +The tasks below depend on these existing primitives. Every cite is a +fresh `file:line` from the current `main` checkout — the plan reviewer +will verify these against the §9 Primitive-Existence Audit. Where a +task creates a new primitive, the row is marked `(NEW — task TASK-X-Y)`. + +### Gateway (executes in `egg-system` pod) + +| Primitive | Where | Used by | +|-----------|-------|---------| +| `_anthropic_client` singleton + `get_anthropic_client()` | `gateway/gateway.py:9320` | `proxy_anthropic_messages` (`gateway/gateway.py:9789`), `proxy_count_tokens` (`gateway/gateway.py:10020`) | +| `proxy_anthropic_messages()` route `POST /v1/messages` | `gateway/gateway.py:9753` | Sandbox `ANTHROPIC_BASE_URL` traffic | +| `proxy_count_tokens()` route `POST /v1/messages/count_tokens` | `gateway/gateway.py:10020` | Same | +| `_inject_anthropic_credentials()` | `gateway/gateway.py:9355` | Both proxy routes | +| `_filter_blocked_tools(request_body, session_mode)` | `gateway/gateway.py:9410` | `proxy_anthropic_messages` | +| `_SSEAccumulator` incremental SSE parser | `gateway/gateway.py:9552` | Streaming branch of `proxy_anthropic_messages` | +| `_get_forwarded_headers` / `_filter_response_headers` | `gateway/gateway.py:9343` / `:9348` | Both proxy routes | +| `get_credentials_manager()` returns `AnthropicCredentialsManager` | `gateway/anthropic_credentials.py:218` | `_inject_anthropic_credentials` | +| `AnthropicCredential` dataclass (`header_name`, `header_value`) | `gateway/anthropic_credentials.py:37` | Credential injection | +| `parse_env_file(path)` (reads `secrets.env` key=value) | `gateway/anthropic_credentials.py:52` | New LiteLLM credential reader (slice 1) | +| `SECRETS_PATH = os.environ.get("EGG_SECRETS_PATH", ...)` | `gateway/anthropic_credentials.py:31` | New LiteLLM credential reader (slice 1) | +| `get_session_manager()` returns `SessionManager` | `gateway/session_manager.py:1242` | `proxy_anthropic_messages` (`gateway/gateway.py:9774`) | +| `SessionManager.get_session_by_ip(ip)` | `gateway/session_manager.py:741` | Same routing path | +| `SessionManager.register_session(...)` (the parameter list `Session` receives) | `gateway/session_manager.py:548` | `/api/v1/sessions/create` handler | +| `Session` dataclass (the field list the route persists) | `gateway/session_manager.py:288` | All proxy-routing decisions | +| `Session.mode` field | `gateway/session_manager.py:310` | Today's tool-strip; pattern slice 1 mirrors | +| `Session.to_dict_for_persistence` / `Session.from_persistence` | `gateway/session_manager.py:338` / `:380` | Slice 1 must add `upstream`/`upstream_model` to both | +| `/api/v1/sessions/create` route | `gateway/gateway.py:8507` | Orchestrator → gateway session registration | +| `gateway/allowed_domains.txt` (Squid allowlist — `api.anthropic.com` intentionally excluded) | `gateway/allowed_domains.txt` | Network-policy invariant for proxy-only LLM access | +| `UpstreamRegistry` (NEW — task TASK-1-1) | `gateway/upstream_registry.py` (new) | Replaces direct `get_anthropic_client()` calls in both proxy routes | + +### Orchestrator (executes in `egg-system` pod, trusted) + +| Primitive | Where | Used by | +|-----------|-------|---------| +| `GatewayClient.register_session()` (HTTP wrapper around `/api/v1/sessions/create`) | `orchestrator/gateway_client.py:602` | `kubernetes_spawner.py:735` | +| `KubernetesSpawner.spawn_agent` session-creation call site | `orchestrator/kubernetes_spawner.py:735` | Every agent spawn | +| `_PROTECTED_ENV_KEYS` frozenset | `orchestrator/kubernetes_spawner.py:138` | Env-var allowlist when restarting agents | +| `GATEWAY_K8S_URL` constant | `orchestrator/kubernetes_spawner.py:124` | `ANTHROPIC_BASE_URL` env var on every agent pod | +| Agent pod `ANTHROPIC_BASE_URL` env assignment | `orchestrator/kubernetes_spawner.py:807` | All sandbox-side Claude/LiteLLM traffic | +| `build_consensus_wrapped_command(prompt_text, model="opus", ...)` | `orchestrator/consensus_wrapper.py:620` | Concurrent spawn + restart | +| `build_consensus_wrapped_command` call site (initial spawn) | `orchestrator/concurrent_executor.py:454` | All consensus agents | +| `build_consensus_wrapped_command` call site (restart) | `orchestrator/routes/pipelines.py:2704` | Agent restart path | +| `PipelineConfig` dataclass | `orchestrator/models.py:405` | All per-pipeline config | +| `PipelineConfig.overseer_decision_maker_model` (existing per-role-ish model field) | `orchestrator/models.py:546` | Pattern slice 2 mirrors | +| `PipelineConfig.overseer_advisor_model` | `orchestrator/models.py:620` | Same pattern | +| `AgentRole` enum (canonical role names) | `shared/egg_contracts/agent_roles.py:46` | Validation of `agent_models` keys | +| `load_repo_pattern_override(repo)` (precedent for repo-level overrides) | `shared/egg_restrictions/patterns.py:854` | Pattern slice 2 mirrors for `default_agent_model` | +| `config/repo_config.py` setters/getters (`get_repo_setting(repo, key, default)`) | `config/repo_config.py:248` | Reading `default_agent_model` (slice 2) | +| `PipelineConfig.agent_models: dict[str, str]` (NEW — task TASK-2-1) | `orchestrator/models.py` | Reviewed-against-`AgentRole` per-role model overrides | + +### Sandbox & shared (executes inside agent pod, zero-credential) + +| Primitive | Where | Used by | +|-----------|-------|---------| +| `DEFAULT_MODEL = "opus[1m]"` | `shared/egg_agent/client.py:62` | Default for `egg_agent` SDK path | +| `--model` default `"opus[1m]"` | `shared/egg_agent/__main__.py:35` | `python3 -m egg_agent` CLI | +| `["--model", "opus[1m]"]` (legacy interactive CLI) | `sandbox/llm/runner.py:49` | Compose-only interactive path — **not touched by this issue** | +| `setup_anthropic_api()` (sets `ANTHROPIC_BASE_URL` in-sandbox) | `sandbox/entrypoint.py:712` | Sandbox entrypoint — **not touched by this issue** (gateway URL is upstream-agnostic) | + +### k8s / infra + +| Primitive | Where | Notes | +|-----------|-------|-------| +| `k8s/base/kustomization.yaml` resources list | `k8s/base/kustomization.yaml` | Slice 1 adds `litellm-{deployment,service,configmap}.yaml` | +| Gateway pod `/secrets` volume mount + `gateway-secrets` Secret | `k8s/base/gateway-deployment.yaml:142-145` / `:153-155` | LiteLLM master key lands in the same `secrets.env` (no new mount needed) | +| `egg-system` namespace + `egg-agents` namespace egress NetworkPolicy | `k8s/base/network-policies.yaml` | New LiteLLM Service stays inside `egg-system`; agents have NO egress to it — gateway is the only client | +| `config/repositories.yaml.example` (template) | `config/repositories.yaml.example` | Slice 2 documents the new `default_agent_model` field here | +| `config/secrets.template.env` | `config/secrets.template.env` | Slice 1 documents the new `LITELLM_MASTER_KEY` env var here | + +### Trust-boundary scope checks (#2594 §10) + +- All new gateway-side primitives (`UpstreamRegistry`, upstream-aware + credential injection, `Session.upstream`/`Session.upstream_model`) + run in the **gateway pod**, which is the trusted policy point — same + scope as the existing `_anthropic_client` singleton, `_filter_blocked_tools`, + `_SSEAccumulator`, and `_inject_anthropic_credentials`. Nothing in + this issue places upstream-routing logic in the sandbox or asks the + sandbox to hold a non-zero credential. +- All new orchestrator-side primitives (`PipelineConfig.agent_models`, + the spawn-time resolution function, the `register_session` extra + fields on `GatewayClient`) run in the **orchestrator pod**, which is + trusted CI-runner scope — same as `_PROTECTED_ENV_KEYS` and the + existing `overseer_*_model` fields. The pre-resolved per-role model + reaches the agent pod only as a CLI `--model` argument (already on + the existing Claude path) and reaches the gateway only as a session + field (the gateway is the trust boundary, same as today). +- No new pytest fixture is added in `integration_tests/` outside of + what already exists in `integration_tests/local_pipeline/`. End-to-end + validation against a live LiteLLM endpoint is **deferred** to a + separate operator-driven smoke test (cq-4 explicitly puts the agent + flip out of scope); the integration tests added here cover the + no-op buildability of slice 1 and the config-resolution paths of + slice 2. + +## Slice 1 — Gateway upstream router + LiteLLM Deployment (no-op by default) + +### Goal + +The gateway can route a `/v1/messages` request to either +`api.anthropic.com` (today's path, unchanged) or a new LiteLLM Service +in `egg-system`, chosen per request via session metadata. With **no +agent** configured to opt into LiteLLM, no LiteLLM-bound request ever +fires; the Claude path stays byte-identical. + +### Approach + +Replace the bare `get_anthropic_client()` singleton with a tiny +`UpstreamRegistry` keyed by upstream name (`"anthropic"`, `"litellm"`). +The registry holds per-upstream `httpx.Client` instances and a +per-upstream credential resolver. `proxy_anthropic_messages()` and +`proxy_count_tokens()` look up the upstream from the IP-resolved +session (`session.upstream`, defaulting to `"anthropic"`), then fetch +the right client and credential. `_inject_anthropic_credentials` is +renamed to `_inject_upstream_credentials(headers, upstream)` and +dispatches on the upstream name. The SSE accumulator, tool filter, and +stream-resilience retry logic are upstream-agnostic and unchanged. + +The `Session` dataclass grows two optional fields — `upstream: str` +(default `"anthropic"`) and `upstream_model: str | None` (default +`None`, only consumed in slice 2). Both flow through +`/api/v1/sessions/create` and `SessionManager.register_session`, with +matching keys on `GatewayClient.register_session` and the orchestrator +spawner. **In slice 1 the orchestrator never sends a non-default +value**, so the new fields are exercised by tests only — the live +Claude path remains untouched. + +A LiteLLM Deployment + Service + ConfigMap lands in `k8s/base/`. The +Service has a ClusterIP in `egg-system` and is reachable only by the +gateway (no NetworkPolicy change to `egg-agents` egress; agents do not +talk to it directly). The ConfigMap holds the LiteLLM `config.yaml` +with an empty `model_list` (operators populate it later). The +LiteLLM master key lands in the existing `gateway-secrets` Secret as +`LITELLM_MASTER_KEY`, read out of `secrets.env` the same way +`ANTHROPIC_API_KEY` is today. + +### Tests + +- **Unit (gateway)** — new + `tests/gateway/test_upstream_registry.py`: + - `UpstreamRegistry.get("anthropic")` returns a `httpx.Client` with + `base_url == "https://api.anthropic.com"`. + - `UpstreamRegistry.get("litellm")` returns a `httpx.Client` pointed + at the cluster Service DNS env var. + - `UpstreamRegistry.get("unknown")` raises a typed error. +- **Unit (gateway)** — extend + `tests/gateway/test_anthropic_credentials.py`: + - Add a LiteLLM credential branch — when `LITELLM_MASTER_KEY` is set + in the mocked `secrets.env`, the LiteLLM credential resolver + returns the `x-api-key`-shaped credential; the existing Anthropic + resolver is unaffected. +- **Unit (gateway)** — extend + `tests/gateway/test_anthropic_proxy.py`: + - `proxy_anthropic_messages` with `session.upstream == "anthropic"` + (or no session) routes to the Anthropic httpx client (today's + behavior; regression guard). + - `proxy_anthropic_messages` with a session whose `upstream == + "litellm"` routes to the LiteLLM client and injects the LiteLLM + credential. The SSE accumulator path runs uniformly for both. + - `proxy_count_tokens` mirrors the same routing. +- **Unit (gateway)** — extend + `gateway/tests/test_session_manager.py`: + - `Session` round-trips `upstream` and `upstream_model` through + `to_dict_for_persistence` / `from_persistence` with their defaults + when omitted. + - `SessionManager.register_session(upstream="litellm", + upstream_model="qwen3-coder-30b")` stores both on the Session. +- **Unit (orchestrator)** — extend + `orchestrator/tests/test_gateway_client.py`: + - `GatewayClient.register_session(upstream="litellm", ...)` POSTs + the new fields in the request body; omitted call passes through + without them (back-compat). +- **Manual / integration** — none in slice 1. The LiteLLM Deployment + comes up empty (no `model_list` entries), and no agent points at it, + so there is nothing to drive end-to-end. A separate operator smoke + test outside this pipeline (cq-4) covers the real-traffic path. + +### Manual steps for slice 1 + +- **Pre-merge** (none for the no-op case): operators do **not** need + to add `LITELLM_MASTER_KEY` to `secrets.env` before merge — without + it, the LiteLLM credential resolver simply returns `None`, the + registry still serves the Anthropic upstream, and no request ever + routes to LiteLLM. +- **Post-merge** (manual): if an operator wants the LiteLLM + Deployment up, they add `LITELLM_MASTER_KEY` to their `secrets.env` + and populate the LiteLLM `model_list` ConfigMap with at least one + backend. Documented in the new architecture doc. + +## Slice 2 — Per-agent model config + spawn-side plumbing + +### Goal + +Per-pipeline / per-role configuration knob that, when set, makes the +orchestrator (a) pass the right `--model` flag to the consensus +wrapper for that role, and (b) tell the gateway (at session-create +time) the upstream and upstream-side model name to route to. The +gateway, on a LiteLLM-routed request, rewrites the request body's +`model` field from the Claude-alias presented to Claude Code to the +upstream model name LiteLLM expects. + +### Approach + +- Add `PipelineConfig.agent_models: dict[str, str]` (default + `{}`), keyed by `AgentRole` value (validated against + `shared/egg_contracts/agent_roles.py:46`). Value is the + **upstream-side model name** (e.g. `"qwen3-coder-30b"` for LiteLLM, + or `"opus"`/`"sonnet"`/etc. for Anthropic). +- Add `default_agent_model: str | None` to `repositories.yaml` + (template at `config/repositories.yaml.example`), with a getter in + `config/repo_config.py` (mirroring `get_auth_mode` / `should_disable_auto_fix`). +- New resolution function `resolve_agent_model(role, pipeline_config, + repo)` lands in a small new module + `orchestrator/agent_model_resolution.py`. Precedence: + `PipelineConfig.agent_models[role]` → `repositories.yaml` + `default_agent_model` → built-in `"opus"` default. Returns a + `(claude_code_alias, upstream, upstream_model)` triple. The + classifier maps known Claude aliases (`opus`, `opus[1m]`, `sonnet`, + `haiku`, `claude-*`) to `upstream="anthropic"`; everything else + routes to `upstream="litellm"` with the recognised Claude alias + `"opus"` presented to Claude Code (per cq-5 mitigation). +- Thread the resolved triple through: + - `concurrent_executor.py:454` and `pipelines.py:2704` — pass + `model=claude_code_alias` to `build_consensus_wrapped_command`. + - `kubernetes_spawner.py:735` — pass `upstream=` and `upstream_model=` + to `GatewayClient.register_session`. +- Gateway side: in `proxy_anthropic_messages` and `proxy_count_tokens`, + when `session.upstream == "litellm"` and `session.upstream_model` + is set, rewrite the request body's `model` field to + `session.upstream_model` **after** `_filter_blocked_tools` and + **before** building the upstream request. Wrap this in a new helper + `_rewrite_upstream_model(request_body, upstream_model)` colocated + with `_filter_blocked_tools` so the SSE accumulator and + stream-resilience logic see no shape change. + +### Tests + +- **Unit (orchestrator)** — new + `orchestrator/tests/test_agent_model_resolution.py`: + - Precedence: pipeline > repo > built-in. + - Claude alias classification: `opus`, `opus[1m]`, `sonnet`, + `haiku`, `claude-3-5-sonnet-20241022` all map to + `upstream="anthropic"`. + - LiteLLM classification: `qwen3-coder-30b`, `qwen-...`, anything + unrecognised → `upstream="litellm"` with `claude_code_alias = + "opus"`. + - Validation: an unknown `AgentRole` key in `agent_models` raises a + typed config error at `PipelineConfig` validation time. +- **Unit (orchestrator)** — extend + `orchestrator/tests/test_concurrent_executor.py` (or its current + equivalent) and `orchestrator/tests/test_pipeline_*.py`: + - The resolved `--model` flag reaches + `build_consensus_wrapped_command`, end-to-end from a + `PipelineConfig(agent_models={"refiner": "qwen3-coder-30b"})`. + - `register_session` is called with `upstream="litellm"`, + `upstream_model="qwen3-coder-30b"` for that role. +- **Unit (gateway)** — extend + `tests/gateway/test_anthropic_proxy.py`: + - With a LiteLLM session and `upstream_model="qwen3-coder-30b"`, + the body forwarded upstream has `"model": "qwen3-coder-30b"` + even when the incoming body has `"model": "opus"`. + - With an Anthropic session, the body is forwarded byte-for-byte + unchanged (regression guard for the Claude path). +- **Manual** — none required to merge. End-to-end with a live LiteLLM + backend remains the operator's separate smoke test (cq-4). + +### Manual steps for slice 2 + +- **Pre-merge**: none. The integration is still no-op by default — + every existing pipeline has an empty `agent_models` dict. +- **Post-merge** (when the operator wants to actually run an agent on + Qwen): operator populates the LiteLLM ConfigMap with a `model_list` + entry naming the upstream-side model, adds the hosted-provider API + key under LiteLLM's standard env-var convention (LiteLLM reads it, + not the gateway), sets the per-pipeline `agent_models` override, + and triggers a pipeline. + +## Slice DAG + +``` +slice-1 (root — gateway upstream router + LiteLLM topology, no-op) + │ + ▼ +slice-2 (per-agent model config + spawn plumbing + body rewrite) +``` + +Both slices have at most one parent (forest constraint satisfied). +Slice 1 is the root (no `dependencies`); slice 2 depends on slice 1 +because the per-agent config resolution and the body-rewrite helper +both rely on the `Session.upstream`/`Session.upstream_model` fields +that slice 1 introduces, and on the `UpstreamRegistry` slice 1 stands +up. + +### LOC estimate (advisory) + +- Slice 1: ~700 LOC across `gateway/upstream_registry.py` (new), + edits to `gateway/gateway.py`, `gateway/session_manager.py`, + `gateway/anthropic_credentials.py`, `orchestrator/gateway_client.py`, + three new `k8s/base/litellm-*.yaml`, a kustomization edit, tests, + and an architecture doc. +- Slice 2: ~600 LOC across `orchestrator/models.py`, + `orchestrator/agent_model_resolution.py` (new), + `orchestrator/concurrent_executor.py`, + `orchestrator/routes/pipelines.py`, `orchestrator/kubernetes_spawner.py`, + edits to `config/repo_config.py`, `config/repositories.yaml.example`, + tests, and a how-to doc. + +Both within the 1,000-LOC soft target. + +## Risks and mitigations (planner view; the risk_analyst owns the +canonical list) + +- **R1: Gateway file-size discipline (#2261).** `gateway/gateway.py` + is already ~10K lines; slice 1 lands the new upstream-registry + symbols in a dedicated `gateway/upstream_registry.py` module (per + the in-flight decomposition guidance in `gateway/CLAUDE.md`), and + the body-rewrite helper in slice 2 lives next to + `_filter_blocked_tools`. Net delta to `gateway.py` is small. +- **R2: Session-field back-compat.** Existing on-disk session + persistence must keep loading. `Session.from_persistence` already + defaults absent fields, and the new `upstream` field defaults to + `"anthropic"` — sessions created before slice 1 stay valid. +- **R3: Credential drift.** Two credentials now exist (Anthropic + + LiteLLM). The upstream-aware injector dispatches by name; the wrong + credential cannot reach the wrong upstream because the registry + keys the client and the resolver together. +- **R4: LiteLLM supply-chain blast radius.** Confined by topology + (separate Deployment, not a sidecar in the gateway pod — cq-1) and + by the swap-out `UpstreamRegistry` interface (feedback Q3). A future + issue can register a different translator without touching the + per-request routing logic. +- **R5: Empirical Claude Code compaction-math compatibility.** + Slice 2 keeps Claude Code's `--model` flag set to a recognised + Claude alias (`opus`) for all LiteLLM-bound agents, per cq-5, so + Claude Code's compaction math stays sane. Confirming this in + practice is the operator's separate smoke test (cq-4); the plan + ships only the buildable seam. + +## Test plan summary + +- **Automated coverage**: + - Slice 1 — `tests/gateway/test_upstream_registry.py` (new), + extensions to `tests/gateway/test_anthropic_proxy.py`, + `tests/gateway/test_anthropic_credentials.py`, + `gateway/tests/test_session_manager.py`, + `orchestrator/tests/test_gateway_client.py`. + - Slice 2 — `orchestrator/tests/test_agent_model_resolution.py` + (new), extensions to the concurrent-executor and pipeline tests, + extensions to `tests/gateway/test_anthropic_proxy.py` for the + body-rewrite branch. + - `make test` from the repo root catches both reachable suites + given the changeset. +- **Manual verification**: + - Reviewer-side: confirm `make test` and `make lint` pass; confirm + the new k8s manifests render under `kubectl apply --dry-run` for + slice 1; confirm no `egg-agents` pod needs egress to LiteLLM + (gateway-only); spot-check that with `agent_models` empty (the + default), gateway request flow is byte-identical to today. + - Operator-side (not gating merge): once a LiteLLM endpoint is + live, flip one agent role to a non-Claude model and exercise a + tool-heavy multi-turn loop plus a session long enough to cross + the auto-compaction boundary (the cq-4-deferred smoke test). + +## Manual steps + +- **Pre-merge** (none). +- **Post-merge** (operator, to actually use the new path): + 1. Add `LITELLM_MASTER_KEY=` to `secrets.env` on the host. + 2. Populate the LiteLLM ConfigMap (`k8s/base/litellm-configmap.yaml`) + `model_list` entries naming the upstream backends (hosted Qwen + provider first, per cq-6). Provider-side API keys go in + LiteLLM's standard env-var slots, **not** in `secrets.env`. + 3. Either set a `default_agent_model` in + `~/.config/egg/repositories.yaml` for the target repo, **or** + pass `agent_models={"": ""}` on the pipeline + submission to override per pipeline. + 4. Run a pipeline and observe gateway logs — LiteLLM-bound requests + log `upstream=litellm` in the routing audit line; Claude-bound + requests log `upstream=anthropic` (default). + +--- + +```yaml +# yaml-tasks +pr: + title: |- + Add per-agent non-Claude model support via LiteLLM proxy + description: |- + Today every egg SDLC agent runs on Claude through the Claude Code + harness, with the gateway hard-wiring `api.anthropic.com` as the + only `/v1/messages` upstream. The orchestrator's consensus wrapper + hardcodes `--model opus` for every non-overseer role, so per-agent + model selection does not exist. We need to let any agent run on a + non-Claude backend (Qwen is the first target, primarily for cost) + while every Claude-bound agent stays byte-identically on the + existing path — and we need the integration to be safe to ship + before a live non-Claude endpoint is available. + + This change lands the buildable, no-op-by-default seam in two + stacked PRs: + + 1. **Gateway upstream router + LiteLLM Deployment (slice 1).** + Introduces a small `UpstreamRegistry` abstraction in the + gateway that keys per-request `httpx.Client` + credential by + upstream name, with `proxy_anthropic_messages` / + `proxy_count_tokens` resolving the upstream per request via + the existing IP-keyed session lookup that already drives + `session_mode`. Adds `Session.upstream` (default `"anthropic"`) + and `Session.upstream_model` (default `None`), wired through + `/api/v1/sessions/create`, `SessionManager.register_session`, + and `GatewayClient.register_session`. The LiteLLM proxy itself + ships as a separate Deployment + Service + ConfigMap in + `egg-system`, reachable only by the gateway. The Claude path + is structurally untouched. + 2. **Per-agent model config + spawn-side plumbing (slice 2).** + Adds `PipelineConfig.agent_models: dict[str, str]` and a + `default_agent_model` repository-level setting, plus a + resolution function that the orchestrator's spawner calls to + (a) thread the right `--model` to `build_consensus_wrapped_command` + and (b) tell the gateway the per-agent `upstream` and + `upstream_model` at session-create time. The gateway, on a + LiteLLM-routed request, rewrites the body's `model` field + from the Claude alias presented to Claude Code to the + upstream-side model name — keeping Claude Code's compaction + math sane (cq-5). + + With `agent_models` empty (the default everywhere), no LiteLLM + request fires. Every existing pipeline keeps running on Claude + with byte-identical gateway behavior. The empirical + Claude-Code-compaction smoke test (cq-4) is an operator-driven + follow-up once a live non-Claude endpoint is configured; it is + explicitly out of scope here. + test_plan: |- + Automated: + - `make test` from the repo root catches both slices' reachable + suites given the changeset. + - Slice 1: `tests/gateway/test_upstream_registry.py` (new), + extensions to `tests/gateway/test_anthropic_proxy.py`, + `tests/gateway/test_anthropic_credentials.py`, + `gateway/tests/test_session_manager.py`, + `orchestrator/tests/test_gateway_client.py`. + - Slice 2: `orchestrator/tests/test_agent_model_resolution.py` + (new), extensions to the concurrent-executor and + pipeline-spawn tests, extensions to the gateway proxy tests + covering the body-rewrite branch. + + Manual (reviewer): + - Confirm `make test` and `make lint` are green. + - `kubectl apply --dry-run=client -k k8s/base/` succeeds with + the new LiteLLM manifests included. + - Spot-check that with `agent_models={}` and no + `LITELLM_MASTER_KEY` in secrets, gateway request flow is + byte-identical to today's Claude path (no new headers, no + upstream change, same SSE behavior). + + Manual (operator, post-merge — not gating merge): + - Populate `LITELLM_MASTER_KEY` in `secrets.env`, configure + LiteLLM `model_list` with a hosted Qwen provider (cq-6), set + `agent_models={"refiner": "qwen3-coder-30b"}` on a pipeline, + and exercise a tool-heavy multi-turn loop plus a long session + crossing the auto-compaction boundary (the cq-4-deferred + empirical compatibility check). + manual_steps: |- + Pre-merge: none. + + Post-merge (only required when operator wants to actually run an + agent on a non-Claude backend): + 1. Add `LITELLM_MASTER_KEY=` to `~/.config/egg/secrets.env`. + 2. Populate the LiteLLM ConfigMap `model_list` with at least one + backend (hosted Qwen provider first, per cq-6). The + provider-side API key goes in LiteLLM's standard env-var slot, + not in `secrets.env`. + 3. Either set `default_agent_model` in + `~/.config/egg/repositories.yaml` for the target repo, or + pass `agent_models={"": ""}` on pipeline submit + to override per pipeline. +slices: + - id: 1 + name: |- + Gateway upstream router + LiteLLM Deployment (no-op by default) + goal: |- + Per-request upstream selection seam in the gateway plus a + LiteLLM Deployment + Service + ConfigMap in `egg-system`, with + no agent yet configured to route to LiteLLM. Claude path is + structurally unchanged. + tasks: + - id: TASK-1-1 + description: |- + Introduce `gateway/upstream_registry.py` (NEW) containing + an `UpstreamRegistry` class keyed by upstream name + (`"anthropic"`, `"litellm"`). Each registry entry pairs a + singleton `httpx.Client` (`base_url`, timeout, connection + limits) with a credential resolver returning an + `UpstreamCredential` (the union of today's + `AnthropicCredential` shape and the new LiteLLM + `x-api-key` shape). Provide `get(upstream: str)` returning + `(client, credential_resolver)`, raising a typed + `UnknownUpstreamError` on miss. Wire it into a + `get_upstream_registry()` accessor that mirrors today's + `get_anthropic_client()` lifetime semantics. + acceptance: |- + - `UpstreamRegistry.get("anthropic")` returns a client + with `base_url == "https://api.anthropic.com"` and the + existing Anthropic credential resolver (preserves the + `# noqa: EGG200` annotation pattern at + `gateway/gateway.py:9325`). + - `UpstreamRegistry.get("litellm")` returns a client whose + `base_url` is sourced from a new + `LITELLM_BASE_URL` env var (default + `http://litellm.egg-system.svc.cluster.local:4000`) and + the LiteLLM credential resolver. + - `UpstreamRegistry.get("unknown")` raises + `UnknownUpstreamError`. + - Both clients share the same timeout / pooling + characteristics as today's `_anthropic_client`. + role: coder + files: + - gateway/upstream_registry.py + - id: TASK-1-2 + description: |- + Add a LiteLLM credential resolver to + `gateway/anthropic_credentials.py` (or a sibling module if + file-size discipline requires it). The resolver reads + `LITELLM_MASTER_KEY` from `secrets.env` using the existing + `parse_env_file` helper at + `gateway/anthropic_credentials.py:52`, caches with the + same mtime-invalidated pattern as + `AnthropicCredentialsManager`, and returns a credential + shaped `header_name="x-api-key"`, + `header_value=""`. Returns `None` when the key is + absent (no-op default — matches today's behavior when + Anthropic credentials are absent). + acceptance: |- + - With `LITELLM_MASTER_KEY` unset, the resolver returns + `None` and does not warn at startup. + - With `LITELLM_MASTER_KEY=foo`, the resolver returns a + credential with `header_name == "x-api-key"` and + `header_value == "foo"`. + - `secrets.env` mtime change invalidates the cache the + same way `AnthropicCredentialsManager` does. + role: coder + files: + - gateway/anthropic_credentials.py + - id: TASK-1-3 + description: |- + Make `_inject_anthropic_credentials` upstream-aware + (rename to `_inject_upstream_credentials(headers, + upstream)` and keep the old symbol as a back-compat alias + calling through with `upstream="anthropic"`). Dispatch to + the LiteLLM credential resolver when + `upstream == "litellm"`. Preserve the 401 / "no credential" + error path verbatim for both. + acceptance: |- + - `_inject_upstream_credentials(headers, "anthropic")` + behaves byte-identically to today's + `_inject_anthropic_credentials(headers)`. + - `_inject_upstream_credentials(headers, "litellm")` adds + `x-api-key: ` when the key is set. + - Missing credentials for either upstream return a 401 + with the same JSON body shape as today. + role: coder + files: + - gateway/gateway.py + - id: TASK-1-4 + description: |- + Add `upstream: str = "anthropic"` and + `upstream_model: str | None = None` to the `Session` + dataclass at `gateway/session_manager.py:288`. Plumb them + through `Session.to_dict_for_persistence` / + `Session.from_persistence` so existing persisted sessions + without the fields still load (defaults apply). Extend + `SessionManager.register_session` (`gateway/session_manager.py:548`) + to accept the two new optional parameters. + acceptance: |- + - A `Session` created without the new fields keeps + `upstream == "anthropic"` and `upstream_model is None`. + - `Session.to_dict_for_persistence` / + `Session.from_persistence` round-trip both fields + losslessly and tolerate persisted dicts where the fields + are absent. + - `SessionManager.register_session(upstream="litellm", + upstream_model="qwen3-coder-30b")` stores both on the + returned `Session`. + role: coder + files: + - gateway/session_manager.py + - id: TASK-1-5 + description: |- + Wire `upstream` and `upstream_model` through the + `/api/v1/sessions/create` route handler at + `gateway/gateway.py:8507` (parse from request body with + their defaults, validate that `upstream` is one of the + registered names from `UpstreamRegistry`, pass through to + `SessionManager.register_session`). Log them in the + existing `audit_log("session_created", ...)` call so the + per-session upstream is auditable. + acceptance: |- + - POSTing to `/api/v1/sessions/create` without the new + fields creates a session with + `upstream="anthropic"` and `upstream_model is None`. + - POSTing with `upstream="litellm", + upstream_model="qwen3-coder-30b"` creates a session + with those values. + - POSTing with `upstream="bogus"` returns a 400 with a + descriptive error. + - `session_created` audit log includes the upstream and + upstream_model. + role: coder + files: + - gateway/gateway.py + - id: TASK-1-6 + description: |- + Refactor `proxy_anthropic_messages` (gateway/gateway.py:9753) + and `proxy_count_tokens` (gateway/gateway.py:10020) to + resolve the upstream per request: replace + `client = get_anthropic_client()` with the registry lookup + using `session.upstream` (defaulting to `"anthropic"` when + there is no session — preserves today's behavior). + Replace `_inject_anthropic_credentials(headers)` calls + with `_inject_upstream_credentials(headers, + session.upstream)`. Keep the SSE accumulator, tool-filter, + and stream-resilience retry loop unchanged. + acceptance: |- + - A request whose session has `upstream == "anthropic"` + (or no session) hits the Anthropic httpx client and + injects the Anthropic credential — byte-identical to + today. + - A request whose session has `upstream == "litellm"` + hits the LiteLLM client and injects the LiteLLM + credential. + - `_filter_blocked_tools`, the `_SSEAccumulator` parse, + and the connection-reset retry loop are unchanged in + behavior and code shape (no new branches inside any of + them). + - `proxy_count_tokens` mirrors the same routing change. + role: coder + files: + - gateway/gateway.py + - id: TASK-1-7 + description: |- + Extend `GatewayClient.register_session` at + `orchestrator/gateway_client.py:602` with optional + `upstream: str | None = None` and `upstream_model: str | + None = None` parameters. Include them in `request_data` + only when set (matches the existing optional-field + pattern at `gateway_client.py:653-690`). No caller in + slice 1 passes them; this is purely the wire-shape. + acceptance: |- + - `GatewayClient.register_session(...)` without the new + args produces the same request body as today. + - `GatewayClient.register_session(upstream="litellm", + upstream_model="qwen3-coder-30b")` includes both keys + in the POSTed JSON. + role: coder + files: + - orchestrator/gateway_client.py + - id: TASK-1-8 + description: |- + Add k8s manifests for the LiteLLM proxy: + `k8s/base/litellm-deployment.yaml`, + `k8s/base/litellm-service.yaml`, and + `k8s/base/litellm-configmap.yaml`. Deployment runs a + pinned LiteLLM image in `egg-system`, mounts the ConfigMap + at `/app/config.yaml`, exposes port `4000` (LiteLLM's + default). Service is `ClusterIP` named `litellm`. ConfigMap + ships with an EMPTY `model_list` so the deployment comes + up healthy but serves nothing until operators populate it. + Add all three to `k8s/base/kustomization.yaml`. + acceptance: |- + - `kubectl apply --dry-run=client -k k8s/base/` succeeds + with the new resources included. + - The LiteLLM Service resolves to + `litellm.egg-system.svc.cluster.local:4000`, which + matches the default `LITELLM_BASE_URL` baked into + `UpstreamRegistry`. + - No NetworkPolicy change to `egg-agents` egress — + agents do not talk to LiteLLM directly. + role: coder + files: + - k8s/base/litellm-deployment.yaml + - k8s/base/litellm-service.yaml + - k8s/base/litellm-configmap.yaml + - k8s/base/kustomization.yaml + - id: TASK-1-9 + description: |- + Document `LITELLM_MASTER_KEY` in + `config/secrets.template.env` (one block below the + `ANTHROPIC_API_KEY` block, with an explicit "leave empty + to disable LiteLLM routing — no agent will be routed to + LiteLLM with this unset" comment). + acceptance: |- + - `config/secrets.template.env` contains a documented + `LITELLM_MASTER_KEY=""` entry with the disable-when-empty + note. + role: coder + files: + - config/secrets.template.env + - id: TASK-1-10 + description: |- + Write unit tests covering the slice 1 gateway-side changes: + `tests/gateway/test_upstream_registry.py` (new, covering + the three registry cases — anthropic, litellm, unknown), + extensions to `tests/gateway/test_anthropic_credentials.py` + (LiteLLM resolver path), and extensions to + `tests/gateway/test_anthropic_proxy.py` (the two routing + branches for both proxy routes). + acceptance: |- + - `make test` reaches and passes the new + extended + tests. + - Coverage includes the unknown-upstream error path, the + "no credential" 401 path for both upstreams, and a + byte-identity check for the Anthropic-routed request + shape vs today. + role: tester + files: + - tests/gateway/test_upstream_registry.py + - tests/gateway/test_anthropic_proxy.py + - tests/gateway/test_anthropic_credentials.py + - id: TASK-1-11 + description: |- + Write unit tests covering the slice 1 session-manager and + orchestrator-client changes: extensions to + `gateway/tests/test_session_manager.py` (round-trip the + two new fields, register_session with defaults, register + with explicit LiteLLM values) and to + `orchestrator/tests/test_gateway_client.py` (omitted args + → no new keys in body, explicit args → keys present). + acceptance: |- + - All tests pass under `make test`. + - The session-persistence test verifies a dict missing + the new keys still rehydrates cleanly (back-compat + guard). + role: tester + files: + - gateway/tests/test_session_manager.py + - orchestrator/tests/test_gateway_client.py + - id: TASK-1-12 + description: |- + Author a new architecture doc + `docs/architecture/upstream-routing.md` describing the + `UpstreamRegistry` seam, the LiteLLM topology, the + per-session routing decision, the credential layout, and + the cq-1 / cq-2 / cq-5 / cq-7 / cq-8 resolutions that + shape it. Cross-link from `gateway/CLAUDE.md` and + `docs/architecture/orchestrator.md`. + acceptance: |- + - The doc names every primitive added in slice 1 with a + `file:line` cite, explains the no-op-by-default + invariant, and walks through the request lifecycle for + both upstreams. + - `gateway/CLAUDE.md` and + `docs/architecture/orchestrator.md` link to it. + role: documenter + files: + - docs/architecture/upstream-routing.md + - gateway/CLAUDE.md + - docs/architecture/orchestrator.md + - id: 2 + name: |- + Per-agent model config + spawn-side plumbing + body rewrite + goal: |- + A `PipelineConfig.agent_models` knob (and a `repositories.yaml` + default) drives both the agent's `--model` flag and the + gateway's per-session `upstream` / `upstream_model`. The gateway + rewrites the request body's `model` field to the upstream-side + name on LiteLLM-routed requests so Claude Code keeps seeing a + recognized Claude alias. + dependencies: + - slice-1 + tasks: + - id: TASK-2-1 + description: |- + Add `agent_models: dict[str, str] = Field(default_factory=dict, ...)` + to `PipelineConfig` (orchestrator/models.py:405). Validate + keys against the `AgentRole` enum at + `shared/egg_contracts/agent_roles.py:46` via a Pydantic + validator: unknown roles raise a typed config error at + construction time. Values are free-form strings (validated + downstream by the resolver in TASK-2-3). + acceptance: |- + - `PipelineConfig(agent_models={"refiner": "qwen3-coder-30b"})` + constructs successfully. + - `PipelineConfig(agent_models={"bogus_role": "x"})` raises + a Pydantic validation error citing the unknown role. + - Default-constructed `PipelineConfig.agent_models` is an + empty dict (no behavioral change for existing pipelines). + role: coder + files: + - orchestrator/models.py + - id: TASK-2-2 + description: |- + Add a `default_agent_model: str | None` field to the + `repositories.yaml` schema (documented in + `config/repositories.yaml.example`) and expose it via a new + `get_default_agent_model(repo)` helper in + `config/repo_config.py` (mirroring the + `get_repo_setting(repo, key, default)` pattern at + `config/repo_config.py:248`). + acceptance: |- + - `get_default_agent_model("owner/repo")` returns the + configured value when set in `repositories.yaml`, or + `None` when absent. + - `config/repositories.yaml.example` shows the new field + in context with an inline comment naming the precedence + rule (per-pipeline `agent_models` > this default > + built-in `"opus"`). + role: coder + files: + - config/repo_config.py + - config/repositories.yaml.example + - id: TASK-2-3 + description: |- + New module `orchestrator/agent_model_resolution.py` + exporting `resolve_agent_model(role: AgentRole, + pipeline_config: PipelineConfig, repo: str | None) -> + AgentModelDecision`, where `AgentModelDecision` is a small + dataclass with fields `(claude_code_alias: str, upstream: + str, upstream_model: str | None)`. Precedence: + `pipeline_config.agent_models.get(role.value)` → + `get_default_agent_model(repo)` → built-in `"opus"`. + Classifier: model strings matching `opus`, `opus[1m]`, + `sonnet`, `haiku`, or `claude-*` map to + `upstream="anthropic"`, `claude_code_alias=`, + `upstream_model=None`. Every other string maps to + `upstream="litellm"`, `claude_code_alias="opus"` (cq-5 + mitigation), `upstream_model=`. + acceptance: |- + - `resolve_agent_model(AgentRole.CODER, default_config, None)` + returns + `(claude_code_alias="opus", upstream="anthropic", + upstream_model=None)`. + - `resolve_agent_model(AgentRole.REFINER, + PipelineConfig(agent_models={"refiner": "qwen3-coder-30b"}), + None)` returns + `(claude_code_alias="opus", upstream="litellm", + upstream_model="qwen3-coder-30b")`. + - `resolve_agent_model(...)` with only + `default_agent_model="sonnet"` set on the repo returns + `(claude_code_alias="sonnet", + upstream="anthropic", upstream_model=None)`. + - Per-pipeline `agent_models` entry overrides repo-level + `default_agent_model`. + role: coder + files: + - orchestrator/agent_model_resolution.py + - id: TASK-2-4 + description: |- + Thread the resolved decision through the initial spawn + path: `orchestrator/concurrent_executor.py:454` calls + `resolve_agent_model(role, ...)` and passes + `model=decision.claude_code_alias` to + `build_consensus_wrapped_command`. The same site passes + `upstream=decision.upstream` and + `upstream_model=decision.upstream_model` to the + downstream spawn helper that ultimately reaches + `GatewayClient.register_session` (the existing call at + `orchestrator/kubernetes_spawner.py:735`). When the + decision is the default-Anthropic case, the new + register_session kwargs are omitted (no wire change vs + today). + acceptance: |- + - With `PipelineConfig.agent_models == {}`, the spawn + path produces the same `build_consensus_wrapped_command` + args and the same `register_session` payload as before + this slice (regression guard). + - With `agent_models={"refiner": "qwen3-coder-30b"}`, + the refiner spawn passes `--model opus` to the wrapper + and `upstream="litellm", + upstream_model="qwen3-coder-30b"` to the gateway. + role: coder + files: + - orchestrator/concurrent_executor.py + - orchestrator/kubernetes_spawner.py + - id: TASK-2-5 + description: |- + Thread the resolved decision through the restart path at + `orchestrator/routes/pipelines.py:2704`. Same shape as + TASK-2-4 — resolve, pass `model=` to + `build_consensus_wrapped_command`, ensure the surrounding + restart code reuses the existing session (already + registered with the right upstream). + acceptance: |- + - Restarting an agent whose pipeline has a non-default + `agent_models` entry uses the resolved Claude alias for + the `--model` flag. + - Restarting an agent on the default Claude path is + byte-identical to today. + role: coder + files: + - orchestrator/routes/pipelines.py + - id: TASK-2-6 + description: |- + Add `_rewrite_upstream_model(request_body, upstream_model)` + next to `_filter_blocked_tools` in `gateway/gateway.py`. + On LiteLLM-routed requests with `session.upstream_model` + set, the helper parses the JSON body, replaces the + top-level `"model"` field with `session.upstream_model`, + and returns the re-serialized body. On parse error or + when `upstream_model is None`, the body is returned + unchanged. Call it in `proxy_anthropic_messages` and + `proxy_count_tokens` AFTER `_filter_blocked_tools` and + BEFORE building the upstream request. + acceptance: |- + - With `upstream == "litellm"` and + `upstream_model == "qwen3-coder-30b"`, the body + forwarded upstream has `"model": + "qwen3-coder-30b"` regardless of the incoming + `"model"` value. + - With `upstream == "anthropic"`, the body is + byte-identical to the incoming body (regression + guard). + - Invalid JSON returns the original body unchanged (does + not crash the proxy). + role: coder + files: + - gateway/gateway.py + - id: TASK-2-7 + description: |- + Unit tests for the resolver and the spawn-side wiring: + `orchestrator/tests/test_agent_model_resolution.py` (new) + covering precedence + classifier; extensions to the + existing concurrent-executor and restart-path test + modules (`orchestrator/tests/test_concurrent_executor.py` + or its current equivalent, plus a new or extended + pipeline-restart test) that mock the spawner and assert + the resolved `--model` and the `register_session` kwargs. + acceptance: |- + - All new and extended tests pass under `make test`. + - Tests assert the cq-5 mitigation explicitly: the + Claude-Code-facing alias for a LiteLLM-routed agent is + always `"opus"`, never the upstream model name. + - Default-`agent_models` path is exercised as the + regression guard (no register_session kwargs added; no + `--model` change). + role: tester + files: + - orchestrator/tests/test_agent_model_resolution.py + - orchestrator/tests/test_concurrent_executor.py + - id: TASK-2-8 + description: |- + Extend `tests/gateway/test_anthropic_proxy.py` with the + body-rewrite branch: with a LiteLLM session whose + `upstream_model` is set, the request body forwarded + upstream has the rewritten `model` field; with an + Anthropic session, the body is byte-identical. Also test + the invalid-JSON path through `_rewrite_upstream_model`. + acceptance: |- + - Tests pass under `make test`. + - The byte-identical-Claude-path assertion uses a + non-default incoming model value (e.g. + `"opus"`) and confirms it survives unchanged when + `upstream == "anthropic"`. + role: tester + files: + - tests/gateway/test_anthropic_proxy.py + - id: TASK-2-9 + description: |- + Write a how-to doc `docs/guides/per-agent-models.md` + covering: setting `agent_models` per pipeline; setting + `default_agent_model` per repository in + `repositories.yaml`; the precedence rule; the cq-5 + recognised-alias presented-to-Claude-Code mitigation; + the operator smoke test (live LiteLLM endpoint, the + cq-4-deferred validation). Cross-link from + `docs/index.md` and the new + `docs/architecture/upstream-routing.md`. + acceptance: |- + - The guide names every primitive added in slice 2 with + a `file:line` cite (resolver, config field, repo + helper, body-rewrite helper). + - It walks an operator through enabling Qwen for the + refiner role end-to-end without modifying source code. + - `docs/index.md` and + `docs/architecture/upstream-routing.md` link to it. + role: documenter + files: + - docs/guides/per-agent-models.md + - docs/index.md + - docs/architecture/upstream-routing.md +```