From f51d3f1de9a51b16b7346281eaa4cb48edd2af1e Mon Sep 17 00:00:00 2001 From: Lintel Date: Tue, 7 Apr 2026 19:16:18 +0000 Subject: [PATCH] Enhance DECOMPOSE_SYSTEM_PROMPT with sizing, traceability, interface-fir --- .gitignore | 5 ++ .../src/lintel/chat_api/decomposer.py | 15 ++--- packages/chat-api/tests/test_decomposer.py | 55 ++++++++++++++++++- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 349079a6..565e2ffa 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ worktrees .superpowers ui/src/generated/ ui/.vite/ + +# Python bytecode (added by lintel) +*.pyc +*.pyo +*.pyd diff --git a/packages/chat-api/src/lintel/chat_api/decomposer.py b/packages/chat-api/src/lintel/chat_api/decomposer.py index 0369d86d..88b38a68 100644 --- a/packages/chat-api/src/lintel/chat_api/decomposer.py +++ b/packages/chat-api/src/lintel/chat_api/decomposer.py @@ -78,10 +78,10 @@ def _try_salvage_partial_json(text: str) -> list[dict[str, Any]] | None: via a feature_to_pr pipeline (research → plan → implement → review → PR) with NO \ human clarification available. -## Sizing constraints +## Sizing constraint -Each work item MUST be implementable in a single pull request: -- Touches at most 3-5 files substantially +Each work item MUST be implementable in a single pull request touching at most \ +3 files: - One cohesive concern — never bundle unrelated changes - If a concept needs both an interface/protocol AND an implementation, split them \ into separate work items (interface first) @@ -92,10 +92,11 @@ def _try_salvage_partial_json(text: str) -> list[dict[str, Any]] | None: MUST map to at least one work item. Do NOT silently drop requirements. If the idea \ lists 9 things, produce at least 9 work items. -## Dependency ordering +## Interface-first ordering +- Protocols and abstractions MUST appear before concrete implementations - Foundation first: project scaffold, shared types/protocols, configuration -- Abstractions before implementations: define interfaces before concrete adapters +- Define interfaces before concrete adapters - Data layer before API layer before UI layer - Cross-cutting concerns (multi-tenancy, auth, event sourcing) early @@ -103,11 +104,11 @@ def _try_salvage_partial_json(text: str) -> list[dict[str, Any]] | None: Each description MUST include ALL of the following so the agent can implement \ without asking questions: -1. What package/directory to create or modify +1. Target package and key files to create or modify 2. Key types, classes, or functions to implement (with field names where relevant) 3. API routes with HTTP methods and paths (if applicable) 4. Events to emit (if the project uses event sourcing) -5. How to test: specific acceptance criteria an agent can verify +5. Acceptance tests: specific criteria an agent can verify 6. What is explicitly OUT OF SCOPE for this work item ## Output format diff --git a/packages/chat-api/tests/test_decomposer.py b/packages/chat-api/tests/test_decomposer.py index ef0351d9..7cf63b1b 100644 --- a/packages/chat-api/tests/test_decomposer.py +++ b/packages/chat-api/tests/test_decomposer.py @@ -6,11 +6,57 @@ from unittest.mock import AsyncMock, MagicMock from lintel.chat_api.decomposer import ( + DECOMPOSE_SYSTEM_PROMPT, DecomposedItem, IdeaDecomposer, ) from lintel.domain.types import WorkItemType +# --------------------------------------------------------------------------- +# DECOMPOSE_SYSTEM_PROMPT instruction section tests +# --------------------------------------------------------------------------- + + +class TestDecomposeSystemPrompt: + """Verify that the prompt contains all four required instruction sections.""" + + def test_sizing_constraint_present(self) -> None: + assert "Sizing constraint" in DECOMPOSE_SYSTEM_PROMPT + assert "at most 3 files" in DECOMPOSE_SYSTEM_PROMPT + + def test_requirement_traceability_present(self) -> None: + assert "Requirement traceability" in DECOMPOSE_SYSTEM_PROMPT + assert "every numbered point" in DECOMPOSE_SYSTEM_PROMPT.lower() + + def test_interface_first_ordering_present(self) -> None: + assert "Interface-first ordering" in DECOMPOSE_SYSTEM_PROMPT + assert "protocols and abstractions" in DECOMPOSE_SYSTEM_PROMPT.lower() + + def test_agent_readiness_present(self) -> None: + assert "Agent-ready descriptions" in DECOMPOSE_SYSTEM_PROMPT + assert "target package" in DECOMPOSE_SYSTEM_PROMPT.lower() or ( + "package/directory" in DECOMPOSE_SYSTEM_PROMPT.lower() + ) + assert "API routes" in DECOMPOSE_SYSTEM_PROMPT or "API route" in DECOMPOSE_SYSTEM_PROMPT + assert "events to emit" in DECOMPOSE_SYSTEM_PROMPT.lower() or ( + "Events to emit" in DECOMPOSE_SYSTEM_PROMPT + ) + assert "acceptance" in DECOMPOSE_SYSTEM_PROMPT.lower() or ( + "test" in DECOMPOSE_SYSTEM_PROMPT.lower() + ) + + def test_all_four_sections_in_single_prompt(self) -> None: + """All four instruction keywords must appear in the prompt string.""" + required_phrases = [ + "Sizing constraint", + "Requirement traceability", + "Interface-first ordering", + "Agent-ready descriptions", + ] + for phrase in required_phrases: + assert phrase in DECOMPOSE_SYSTEM_PROMPT, f"Missing instruction section: {phrase!r}" + + # --------------------------------------------------------------------------- # _parse_response tests # --------------------------------------------------------------------------- @@ -202,7 +248,9 @@ class TestDecompose: async def test_calls_llm_and_returns_items(self) -> None: mock_router = MagicMock() - mock_router.select_model = AsyncMock(return_value=MagicMock()) + mock_policy = MagicMock() + mock_policy.max_tokens = 16384 + mock_router.select_model = AsyncMock(return_value=mock_policy) mock_router.call_model = AsyncMock( return_value={ "content": json.dumps( @@ -228,6 +276,7 @@ async def test_calls_llm_and_returns_items(self) -> None: async def test_uses_provided_model_policy(self) -> None: mock_router = MagicMock() mock_policy = MagicMock() + mock_policy.max_tokens = 16384 mock_router.call_model = AsyncMock(return_value={"content": "[]"}) decomposer = IdeaDecomposer(model_router=mock_router) @@ -240,7 +289,9 @@ async def test_uses_provided_model_policy(self) -> None: async def test_includes_project_context_in_prompt(self) -> None: mock_router = MagicMock() - mock_router.select_model = AsyncMock(return_value=MagicMock()) + mock_policy = MagicMock() + mock_policy.max_tokens = 16384 + mock_router.select_model = AsyncMock(return_value=mock_policy) mock_router.call_model = AsyncMock(return_value={"content": "[]"}) decomposer = IdeaDecomposer(model_router=mock_router)