From 9bf100042a7ceb07e684fcdb7ef440cfda6ef12d Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Sun, 12 Jul 2026 22:57:22 +0400 Subject: [PATCH] feat(plugins): apply augment_prompt on Direct-API/Codex backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin prompt augmentation (augment_prompt) reached the Claude SDK backend only: create_client() appends plugin contributions to the system prompt, but the Direct-API/Codex path builds its session with an empty system prompt and carries the agent's instructions in the message. So enabled plugins' prompt contributions (rules-steering-compiler, codebase-intelligence, skill-pack-runtime, ...) were silently absent from every non-claude build. Append the same augmentations to the agent message on the Direct-API path, right after the provider is resolved. Guarded on provider name so the claude provider is a no-op (it already augments via create_client) — no double application. Failures degrade to the un-augmented prompt. Covers the primary autonomous planner+coder flow (coder.py). The follow-up planner (planner.run_followup_planner) uses a separate session builder and is left for a focused follow-up. Co-Authored-By: Claude Opus 4.8 --- apps/backend/agents/coder.py | 47 +++++++++++++++++++ tests/test_coder_prompt_augmentation.py | 60 +++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/test_coder_prompt_augmentation.py diff --git a/apps/backend/agents/coder.py b/apps/backend/agents/coder.py index e88b5fd0b..88d814f17 100644 --- a/apps/backend/agents/coder.py +++ b/apps/backend/agents/coder.py @@ -130,6 +130,39 @@ logger = logging.getLogger(__name__) +def _augment_direct_api_prompt( + prompt: str, + provider_name: str, + project_dir: Path, + spec_dir: Path, + agent_type: str, + metadata: dict[str, object] | None, +) -> str: + """Append enabled plugins' prompt augmentations to a Direct-API agent message. + + The Claude SDK path augments the system prompt inside ``create_client()``, + so this is a no-op for the ``claude`` provider to avoid double-applying. + Direct providers (OpenAI, Codex, ...) carry their instructions in the + message, so the same plugin contributions are appended there. Any failure + degrades to the original prompt rather than breaking the build. + """ + if provider_name == "claude": + return prompt + try: + from plugins.runtime import apply_plugin_prompt_augmentations + + return apply_plugin_prompt_augmentations( + prompt, + project_dir, + spec_dir, + agent_type, + metadata=metadata, + ) + except Exception as exc: + logger.warning("Failed to apply plugin prompt augmentations: %s", exc) + return prompt + + # ============================================================================= # FILE VALIDATION UTILITIES # ============================================================================= @@ -1288,6 +1321,20 @@ def _validate_and_fix_implementation_plan() -> tuple[bool, list[str]]: analysis_only_terminal_message: str | None = None provider = create_engine_provider(provider_config) + + # Plugin prompt augmentation for Direct-API / Codex backends. The Claude + # SDK path augments the system prompt inside create_client(); direct + # providers carry their instructions in the message, so append the same + # plugin contributions there (no-op for claude to avoid double-applying). + prompt = _augment_direct_api_prompt( + prompt, + provider.name, + project_dir, + spec_dir, + agent_type_for_session, + runtime_metadata, + ) + runtime_phase = ( "planning" if current_log_phase == LogPhase.PLANNING else "coding" ) diff --git a/tests/test_coder_prompt_augmentation.py b/tests/test_coder_prompt_augmentation.py new file mode 100644 index 000000000..d0921807f --- /dev/null +++ b/tests/test_coder_prompt_augmentation.py @@ -0,0 +1,60 @@ +"""Tests for cross-backend plugin prompt augmentation on the Direct-API path. + +The Claude SDK backend augments the system prompt inside ``create_client()``. +Direct providers carry their instructions in the agent message, so coder.py +appends the same plugin contributions to that message — but only for non-claude +providers, so the claude path is never augmented twice. +""" + +from __future__ import annotations + +import sys +from pathlib import Path +from unittest.mock import patch + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "apps" / "backend")) + +from agents.coder import _augment_direct_api_prompt + +_TARGET = "plugins.runtime.apply_plugin_prompt_augmentations" + + +def test_claude_provider_is_not_augmented(tmp_path): + # Claude already augments via create_client(); the message must be left + # untouched so contributions are not applied twice. + with patch(_TARGET) as apply_mock: + out = _augment_direct_api_prompt( + "BASE", "claude", tmp_path, tmp_path, "coder", None + ) + assert out == "BASE" + apply_mock.assert_not_called() + + +def test_direct_provider_is_augmented(tmp_path): + augmented = "BASE\n\n# Plugin Runtime Instructions\n## demo\nhello" + with patch(_TARGET, return_value=augmented) as apply_mock: + out = _augment_direct_api_prompt( + "BASE", "openai", tmp_path, tmp_path, "coder", {"subtask_id": "s1"} + ) + apply_mock.assert_called_once_with( + "BASE", tmp_path, tmp_path, "coder", metadata={"subtask_id": "s1"} + ) + assert out == augmented + + +def test_codex_provider_is_augmented(tmp_path): + with patch(_TARGET, return_value="BASE+X") as apply_mock: + out = _augment_direct_api_prompt( + "BASE", "codex", tmp_path, tmp_path, "planner", None + ) + apply_mock.assert_called_once() + assert out == "BASE+X" + + +def test_augmentation_failure_degrades_to_base_prompt(tmp_path): + with patch(_TARGET, side_effect=RuntimeError("boom")): + out = _augment_direct_api_prompt( + "BASE", "openai", tmp_path, tmp_path, "coder", None + ) + assert out == "BASE"