Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ worktrees
.superpowers
ui/src/generated/
ui/.vite/

# Python bytecode (added by lintel)
*.pyc
*.pyo
*.pyd
15 changes: 8 additions & 7 deletions packages/chat-api/src/lintel/chat_api/decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -92,22 +92,23 @@ 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

## Agent-ready descriptions

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
Expand Down
55 changes: 53 additions & 2 deletions packages/chat-api/tests/test_decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading