From cd3b4b8836cef6b6b39a6ed5eec8419601c5fb20 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 22:02:04 +0200 Subject: [PATCH 1/8] remove Item Pydantic models --- src/app/models/item.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 src/app/models/item.py diff --git a/src/app/models/item.py b/src/app/models/item.py deleted file mode 100644 index efeafb9..0000000 --- a/src/app/models/item.py +++ /dev/null @@ -1,26 +0,0 @@ -from datetime import datetime - -from pydantic import BaseModel, ConfigDict, Field - - -class ItemCreate(BaseModel): - model_config = ConfigDict(extra="forbid") - - name: str = Field(min_length=1, max_length=100) - description: str | None = Field(default=None, max_length=1000) - - -class ItemUpdate(BaseModel): - model_config = ConfigDict(extra="forbid") - - name: str | None = Field(default=None, min_length=1, max_length=100) - description: str | None = Field(default=None, max_length=1000) - - -class Item(BaseModel): - model_config = ConfigDict(frozen=True) - - id: str - name: str - description: str | None - created_at: datetime From eea3a32e1311411c7217d0527e4369bd70494c37 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 22:02:27 +0200 Subject: [PATCH 2/8] remove ItemStore and DI factory --- src/app/store/item_store.py | 55 ------------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/app/store/item_store.py diff --git a/src/app/store/item_store.py b/src/app/store/item_store.py deleted file mode 100644 index e98726b..0000000 --- a/src/app/store/item_store.py +++ /dev/null @@ -1,55 +0,0 @@ -import asyncio -from datetime import datetime, timezone -from uuid import uuid4 - -from app.models.item import Item, ItemCreate, ItemUpdate - - -class ItemStore: - def __init__(self) -> None: - self._store: dict[str, Item] = {} - self._lock = asyncio.Lock() - - async def create(self, data: ItemCreate) -> Item: - async with self._lock: - item = Item( - id=str(uuid4()), - name=data.name, - description=data.description, - created_at=datetime.now(tz=timezone.utc), - ) - self._store[item.id] = item - return item - - async def get(self, item_id: str) -> Item: - async with self._lock: - return self._store[item_id] - - async def list_items(self, limit: int = 20, offset: int = 0) -> list[Item]: - async with self._lock: - return list(self._store.values())[offset : offset + limit] - - async def update(self, item_id: str, data: ItemUpdate) -> Item: - async with self._lock: - existing = self._store[item_id] - updated = Item( - id=existing.id, - name=data.name if data.name is not None else existing.name, - description=data.description - if data.description is not None - else existing.description, - created_at=existing.created_at, - ) - self._store[item_id] = updated - return updated - - async def delete(self, item_id: str) -> None: - async with self._lock: - del self._store[item_id] - - -_item_store = ItemStore() - - -def get_item_store() -> ItemStore: - return _item_store From c469ac97abc68e583f6de4cb4fcdc6f9f282c4d6 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 22:03:03 +0200 Subject: [PATCH 3/8] remove items router and unwire from app --- src/app/main.py | 3 -- src/app/routes/items.py | 66 ----------------------------------------- 2 files changed, 69 deletions(-) delete mode 100644 src/app/routes/items.py diff --git a/src/app/main.py b/src/app/main.py index f4a3571..652ccda 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -8,7 +8,6 @@ from fastapi.templating import Jinja2Templates from app.core.config import settings -from app.routes.items import router as items_router logger = logging.getLogger(__name__) @@ -37,8 +36,6 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app.mount("/static", StaticFiles(directory="src/app/ui"), name="static") templates = Jinja2Templates(directory="src/app/ui") -app.include_router(items_router, prefix="/items", tags=["items"]) - # --Health-- @app.get("/health/live", response_model=None, status_code=200) diff --git a/src/app/routes/items.py b/src/app/routes/items.py deleted file mode 100644 index 61e8c5d..0000000 --- a/src/app/routes/items.py +++ /dev/null @@ -1,66 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, Query, status - -from app.models.item import Item, ItemCreate, ItemUpdate -from app.store.item_store import ItemStore, get_item_store - - -router = APIRouter() - - -@router.post("", response_model=Item, status_code=status.HTTP_201_CREATED) -async def create_item( - payload: ItemCreate, - store: ItemStore = Depends(get_item_store), -) -> Item: - return await store.create(payload) - - -@router.get("", response_model=list[Item], status_code=status.HTTP_200_OK) -async def list_items( - limit: int = Query(default=20, ge=1, le=100), - offset: int = Query(default=0, ge=0), - store: ItemStore = Depends(get_item_store), -) -> list[Item]: - return await store.list_items(limit=limit, offset=offset) - - -@router.get("/{item_id}", response_model=Item, status_code=status.HTTP_200_OK) -async def get_item( - item_id: str, - store: ItemStore = Depends(get_item_store), -) -> Item: - try: - return await store.get(item_id) - except KeyError: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Item not found" - ) - - -@router.put("/{item_id}", response_model=Item, status_code=status.HTTP_200_OK) -async def update_item( - item_id: str, - payload: ItemUpdate, - store: ItemStore = Depends(get_item_store), -) -> Item: - try: - return await store.update(item_id, payload) - except KeyError: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Item not found" - ) - - -@router.delete( - "/{item_id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT -) -async def delete_item( - item_id: str, - store: ItemStore = Depends(get_item_store), -) -> None: - try: - await store.delete(item_id) - except KeyError: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Item not found" - ) From 4b893c983985f8608cb049f5e50896ffb9c1e14f Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 22:04:36 +0200 Subject: [PATCH 4/8] remove items test suite --- tests/test_items.py | 168 -------------------------------------------- 1 file changed, 168 deletions(-) delete mode 100644 tests/test_items.py diff --git a/tests/test_items.py b/tests/test_items.py deleted file mode 100644 index 8c5b1fb..0000000 --- a/tests/test_items.py +++ /dev/null @@ -1,168 +0,0 @@ -import pytest -import pytest_asyncio -from httpx import AsyncClient - - -@pytest_asyncio.fixture(autouse=True) -async def store_reset() -> None: - from app.store.item_store import _item_store - - _item_store._store.clear() - - -# POST /items - - -async def test_create_item_happy(client: AsyncClient) -> None: - r = await client.post("/items", json={"name": "widget", "description": "a widget"}) - assert r.status_code == 201 - body = r.json() - assert body["name"] == "widget" - assert body["description"] == "a widget" - assert "id" in body - assert "created_at" in body - - -@pytest.mark.parametrize( - "payload", - [ - {"name": ""}, - {"name": "x" * 101}, - {}, - ], -) -async def test_create_item_validation( - client: AsyncClient, payload: dict[str, object] -) -> None: - r = await client.post("/items", json=payload) - assert r.status_code == 422 - - -# GET /items - - -async def test_list_items_happy(client: AsyncClient) -> None: - await client.post("/items", json={"name": "alpha"}) - await client.post("/items", json={"name": "beta"}) - r = await client.get("/items") - assert r.status_code == 200 - names = [i["name"] for i in r.json()] - assert "alpha" in names - assert "beta" in names - - -async def test_list_items_empty(client: AsyncClient) -> None: - r = await client.get("/items") - assert r.status_code == 200 - assert isinstance(r.json(), list) - - -async def test_list_items_no_404(client: AsyncClient) -> None: - r = await client.get("/items") - assert r.status_code != 404 - - -@pytest.mark.parametrize( - "limit,offset,expected_count,expected_status", - [ - (20, 0, 3, 200), # default pagination, fewer items than limit - (2, 0, 2, 200), # custom limit clips result - (2, 2, 1, 200), # offset skips into list - (20, 99, 0, 200), # offset beyond length → empty list - ], -) -async def test_list_items_pagination( - client: AsyncClient, - limit: int, - offset: int, - expected_count: int, - expected_status: int, -) -> None: - for i in range(3): - await client.post("/items", json={"name": f"item-{i}"}) - r = await client.get("/items", params={"limit": limit, "offset": offset}) - assert r.status_code == expected_status - assert len(r.json()) == expected_count - - -@pytest.mark.parametrize( - "params,expected_status", - [ - ({"limit": 0}, 422), # below ge=1 - ({"limit": 101}, 422), # above le=100 - ({"offset": -1}, 422), # below ge=0 - ], -) -async def test_list_items_pagination_validation( - client: AsyncClient, - params: dict[str, int], - expected_status: int, -) -> None: - r = await client.get("/items", params=params) - assert r.status_code == expected_status - - -# GET /items/{item_id} - - -async def test_get_item_happy(client: AsyncClient) -> None: - create = await client.post("/items", json={"name": "gadget"}) - item_id = create.json()["id"] - r = await client.get(f"/items/{item_id}") - assert r.status_code == 200 - assert r.json()["id"] == item_id - - -async def test_get_item_validation_missing_name(client: AsyncClient) -> None: - r = await client.post("/items", json={"description": "no name"}) - assert r.status_code == 422 - - -async def test_get_item_not_found(client: AsyncClient) -> None: - r = await client.get("/items/does-not-exist") - assert r.status_code == 404 - - -# PUT /items/{item_id} - - -async def test_update_item_happy(client: AsyncClient) -> None: - create = await client.post("/items", json={"name": "old"}) - item_id = create.json()["id"] - r = await client.put(f"/items/{item_id}", json={"name": "new"}) - assert r.status_code == 200 - assert r.json()["name"] == "new" - - -async def test_update_item_validation(client: AsyncClient) -> None: - create = await client.post("/items", json={"name": "valid"}) - item_id = create.json()["id"] - r = await client.put(f"/items/{item_id}", json={"name": ""}) - assert r.status_code == 422 - - -async def test_update_item_not_found(client: AsyncClient) -> None: - r = await client.put("/items/no-such-id", json={"name": "x"}) - assert r.status_code == 404 - - -# DELETE /items/{item_id} - - -async def test_delete_item_happy(client: AsyncClient) -> None: - create = await client.post("/items", json={"name": "doomed"}) - item_id = create.json()["id"] - r = await client.delete(f"/items/{item_id}") - assert r.status_code == 204 - r2 = await client.get(f"/items/{item_id}") - assert r2.status_code == 404 - - -async def test_delete_item_validation(client: AsyncClient) -> None: - r = await client.post("/items", json={"name": ""}) - assert r.status_code == 422 - - -async def test_delete_item_not_found(client: AsyncClient) -> None: - r = await client.delete("/items/ghost-id") - assert r.status_code == 404 From cd3f86c8e59a27fc22f9fcb242fa2dbea1f34248 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 22:31:53 +0200 Subject: [PATCH 5/8] update readme features and skill --- .gitignore | 3 +- CLAUDE.md | 7 ++++ README.md | 118 ++++++++++++++++++++++++++++------------------------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index d0ebf1e..67c8be4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ eggs/ # Type checkers .ruff_cache/ - .mypy_cache/ +.mypy_cache/ .dmypy.json dmypy.json @@ -115,6 +115,7 @@ venv.bak/ .claude/settings.local.json .claude/state/ docs/plans/ +docs/specs/ # Project-specific TODO.md diff --git a/CLAUDE.md b/CLAUDE.md index 70dfed7..ed600d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,13 @@ async def client() -> AsyncGenerator[AsyncClient, None]: --- +## Git commit style + +- Use `git commit -m ""` with a plain, descriptive one-line message. No HEREDOC. +- **Never** append attribution lines to commit messages. + +--- + ## Forbidden patterns | Pattern | Replacement | diff --git a/README.md b/README.md index 56f5352..0459a98 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,55 @@ # agentic-coding-template -Containered FastAPI service with Claude Code patterns: typed agents, auto-trigger skills, inline push gate, and a Spec→Plan→Implement→Review→Ship loop. +Template for a containered FastAPI service with Claude Code support: typed agents, auto-trigger skills, inline push gate, and a Spec→Plan→Implement→Review→Ship loop. + --- -## Quick Start +## Features -```bash -docker compose up --build # start dev container -cp .env.template .env # configure environment -``` +- **FastAPI 0.136 + Pydantic v2** — fully preconfigured with async route handlers, strict type annotations, dependency injection via `Depends()`, and response models on every endpoint +- **BCE Architecture (Boundary / Control / Entity)** — enforced by parallel reviewer agents (`architecture-reviewer`, `performance-reviewer`, `security-reviewer`) that run on every `/review` call +- **Automated quality gate** — `ruff format`, `ruff check`, `mypy --strict`, and `pytest` must all pass before any `git push`; enforced inline by the `pre_push_quality_gate` hook +- **Docker-based dev environment** — `docker compose up --build` brings up the full dev container; no local Python setup required +- **Guard-rail hook suite** — blocks secrets in `.env`, dangerous Bash patterns (`rm -rf`, force-push), `pip` in favour of `uv`, and auto-formats Python after every edit +- **Spec→Plan→Implement→Review→Ship loop** — structured agentic workflow from fuzzy requirement to merged PR, all driven from the Claude Code chat; no external project-management tool needed +- **`docs/plans/` as the single source of truth** — every feature starts as a plan file that the `planner` agent writes and the `implementer` agent reads; the plan is referenced in the PR body and versioned alongside the code + +--- + +## Agents + +| Agent | Model | Write targets | Purpose | +|---|---|---|---| +| `planner` | Sonnet 4.6 | `docs/plans/` | Reads codebase, writes implementation plan | +| `implementer` | Sonnet 4.6 | `src/`, `tests/` | Turns plan into working code + tests | +| `architecture-reviewer` | Haiku 4.5 | none (read-only) | +| `performance-reviewer` | Haiku 4.5 | none (read-only) | +| `security-reviewer` | Haiku 4.5 | none (read-only) | Secrets, OWASP-lite, input validation | + + +--- + +## Skills + +Skills extend Claude Code with domain-specific context — path-triggered ones load automatically when you open a matching file; manual ones are invoked on demand. + +| Skill | `paths:` trigger | Purpose | +|---|---|---| +| `fastapi-conventions` | `src/app/routes/**`, `src/app/models/**`, `src/app/main.py` | Route/DI/response-model conventions | +| `pytest-patterns` | `tests/**`, `**/conftest.py` | AsyncClient fixtures, parametrize patterns | +| `uv-workflows` | `pyproject.toml`, `uv.lock` | `uv add`, `uv sync`, `uv run` idioms | +| `infrastructure` | `Dockerfile`, `docker-compose*`, CI workflows | Container, compose, and CI pipeline patterns | +| `openapi` | `openapi*.yml`, `openapi*.json` | Spec authoring and codegen | +| `frontend` | `*.html`, `ui/**` | Jinja2, Tailwind, HTMX patterns | +| `spec-feature` | manual | 2-phase interview → `docs/specs//` before planning | +| `review` | manual | Inline BCE + FastAPI + Python checklist for ad-hoc review | +| `doc` | manual | Reads source → writes project docs into `docs/` | +| `blog-post` | manual | Audience interview → structured technical blog post | +| `infografik` | manual | AI image generation via Hugging Face FLUX.1 → `docs/assets/` | + + +--- ### Agentic loop @@ -27,41 +67,20 @@ spec → plan → implement → review → ship Skip the spec step for well-understood changes (bug fixes, small additive work). Use it when requirements are fuzzy or need alignment before any code is written — the skill runs a structured 6-question interview and records what was agreed. ---- - -## Stack - -| Tool | Version | Purpose | -|---|---|---| -| FastAPI | 0.136 | Async API framework | -| Pydantic v2 | 2.x | Typed models, settings | -| uv | latest | Dependency management | -| pytest-asyncio | latest | Async test runner | -| ruff | latest | Formatter + linter | -| mypy | latest | Strict static typing | -| httpx | latest | Async test client | --- -## Agent Map +## Quick Start -| Agent | Model | Write targets | Purpose | -|---|---|---|---| -| `planner` | Sonnet 4.6 | `docs/plans/` | Reads codebase, writes implementation plan | -| `implementer` | Sonnet 4.6 | `src/`, `tests/` | Turns plan into working code + tests | -| `architecture-reviewer` | Haiku 4.5 | none (read-only) | -| `performance-reviewer` | Haiku 4.5 | none (read-only) | -| `security-reviewer` | Haiku 4.5 | none (read-only) | Secrets, OWASP-lite, input validation | +```bash +git clone # clone the repo +docker compose up --build # start dev container +cp .env.template .env # configure environment +``` ---- +This template uses Claude Code subscription per default. For API connection add the ANTHROPIC_API_KEY to the .env`. -## Skill Roster -| Skill | `paths:` trigger | Purpose | -|---|---|---| -| `fastapi-conventions` | `src/app/routes/**`, `src/app/models/**`, `src/app/main.py` | Route/DI/response-model conventions | -| `pytest-patterns` | `tests/**`, `**/conftest.py` | AsyncClient fixtures, parametrize patterns | -| `uv-workflows` | `pyproject.toml`, `uv.lock` | `uv add`, `uv sync`, `uv run` idioms | --- @@ -77,32 +96,21 @@ Skip the spec step for well-understood changes (bug fixes, small additive work). See [.claude/hooks/README.md](.claude/hooks/README.md) for exit codes and how to add a hook. ---- - -## Command Map - -| Command | What it does | -|---|---| -| `/plan ` | Invokes `planner` → writes `docs/plans/.md` | -| `/implement` | Reads latest plan, creates `feat/` branch, builds code | -| `/review` | Invokes `architecture-reviewer`, `architecture-reviewer` and `security-reviewer` in parallel | -| `/ship` | Runs quality gate, then `gh pr create` | --- -## Demo Feature: `/items` CRUD - -Five in-memory endpoints proving the full loop works end-to-end: +## Stack -``` -POST /items → 201 create item -GET /items → 200 list all -GET /items/{id} → 200 get by id -PUT /items/{id} → 200 update -DELETE /items/{id} → 204 delete -``` +| Tool | Version | Purpose | +|---|---|---| +| FastAPI | 0.136 | Async API framework | +| Pydantic v2 | 2.x | Typed models, settings | +| uv | latest | Dependency management | +| pytest-asyncio | latest | Async test runner | +| ruff | latest | Formatter + linter | +| mypy | latest | Strict static typing | +| httpx | latest | Async test client | -Run the 15 tests: `uv run pytest tests/test_items.py -v` --- From d00f47baeb0504105b1c0e83e8ae885009908573 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 22:41:20 +0200 Subject: [PATCH 6/8] update docs/ to recent changes --- docs/AGENTIC_WORKFLOW.md | 4 ++-- docs/PROMPTS.md | 44 ++++++++++++++++++++-------------------- docs/TROUBLESHOOTING.md | 10 ++++----- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/AGENTIC_WORKFLOW.md b/docs/AGENTIC_WORKFLOW.md index 3910c10..2c505c0 100644 --- a/docs/AGENTIC_WORKFLOW.md +++ b/docs/AGENTIC_WORKFLOW.md @@ -89,7 +89,7 @@ Never use `exit(1)` — it is reserved for unexpected Python exceptions and will { "tool_name": "Bash", "tool_input": { - "command": "git push origin feat/items-resource" + "command": "git push origin feat/my-feature" } } ``` @@ -132,7 +132,7 @@ Events: `PreToolUse`, `PostToolUse`. Matchers: `Bash`, `Edit`, `Write`, `MultiEd ## Self-Correcting Review Loop -`/review` invokes both reviewer agents in parallel. If either finds violations: +`/review` invokes all three reviewer agents in parallel. If any finds violations: ``` Iteration 1: diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index 37fbe38..0d7fc9f 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -1,20 +1,20 @@ # Paste-Ready Plan-Mode Prompts -Ten prompts you can paste directly into a `/plan` invocation. Each targets a common scenario in this codebase. +Ten prompts you can paste directly into a `/plan` invocation. Each targets a common scenario for AI engineers building on this template. --- -## 1. New resource +## 1. Claude streaming chat endpoint ``` -/plan orders-resource +/plan streaming-chat-endpoint ``` -Scope: in-memory CRUD for an `Order` resource (5 endpoints, Pydantic v2 models, asyncio.Lock store, 15 tests). Follow the same pattern as `src/app/routes/items.py`. +Scope: add `POST /chat` that accepts `{"messages": [...], "model": "claude-sonnet-4-6"}` and streams the response as Server-Sent Events using the Anthropic SDK (`stream=True`). Use the `ai-service-generator` agent. Return `data: \n\n` chunks; send `data: [DONE]\n\n` on completion. Tests verify: happy path SSE stream, 422 on missing messages field, 400 on unsupported model string. --- -## 2. Middleware — auth, CORS, rate-limit +## 2. API-key auth middleware ``` /plan auth-middleware @@ -24,23 +24,23 @@ Scope: add API-key header auth via FastAPI middleware. Requests missing `X-API-K --- -## 3. Dependency upgrade +## 3. Structured output extraction ``` -/plan dependency-upgrade +/plan structured-output-extraction ``` -Scope: audit `pyproject.toml` for outdated packages (`uv lock --upgrade --dry-run`), upgrade FastAPI and httpx, confirm `uv run pytest -x` passes, confirm `mypy --strict src/` passes. Document any breaking API changes. +Scope: add `POST /extract` that accepts `{"text": str, "schema": dict}` and uses `claude-sonnet-4-6` with tool use to extract structured data matching the caller-supplied JSON Schema. Return a validated Pydantic model. Use the `ai-service-generator` agent. Tests verify: correct extraction on a fixture text, 422 on missing fields, graceful 400 when the model cannot satisfy the schema. --- -## 4. Debug a 500 error +## 4. Debug a streaming truncation issue ``` -/plan debug-500 +/plan debug-stream-truncation ``` -Scope: reproduce a 500 from `POST /items` with an empty name field. Trace through `ItemCreate` validation, the store, and the exception handler. Identify whether the issue is a missing `@app.exception_handler`, a Pydantic field constraint, or an unhandled `KeyError`. Propose a fix with a regression test. +Scope: reproduce a bug where `POST /chat` stops emitting SSE tokens mid-response on long outputs. Trace through the Anthropic SDK stream iterator, the FastAPI `StreamingResponse`, and the ASGI send loop. Identify whether the cause is a missing `content_block_delta` handler, a response buffer flush issue, or a client timeout. Propose a fix with a regression test using a mocked stream. --- @@ -50,7 +50,7 @@ Scope: reproduce a 500 from `POST /items` with an empty name field. Trace throug /plan error-handling-layer ``` -Scope: add a global `@app.exception_handler(Exception)` that logs the traceback and returns `{"detail": "internal server error"}` with status 500. Add a specific handler for `KeyError → 404`. Ensure no bare `except` clauses remain in `src/`. Tests verify both handlers fire correctly. +Scope: add a global `@app.exception_handler(Exception)` that logs the traceback and returns `{"detail": "internal server error"}` with status 500. Add specific handlers for `anthropic.RateLimitError → 429`, `anthropic.APIStatusError → 502`, and `KeyError → 404`. Ensure no bare `except` clauses remain in `src/`. Tests verify all handlers fire correctly. --- @@ -64,33 +64,33 @@ Scope: read-only pass over all files in `src/` and `tests/`. Flag every CLAUDE.m --- -## 7. Performance investigation +## 7. LLM cost and latency profiling ``` -/plan performance-investigation +/plan llm-cost-latency-profiling ``` -Scope: profile `GET /items` under 1 000 concurrent synthetic requests using `httpx` + `asyncio.gather`. Identify bottlenecks in the in-memory store (lock contention, list copy). Propose a lock-free read path. Do not implement — output a findings report and proposed change set only. +Scope: instrument every Anthropic SDK call in `src/` to record input tokens, output tokens, latency (ms), and model name. Expose `GET /metrics` returning aggregated totals per model. Use an in-memory store with `asyncio.Lock`. Do not implement a database. Tests verify: metrics increment on a mocked SDK call, the endpoint returns correct totals, concurrent calls do not race. --- -## 8. Test gap analysis +## 8. Conversation thread resource ``` -/plan test-gap-analysis +/plan conversation-threads ``` -Scope: compare route handlers in `src/app/routes/` against tests in `tests/`. For each route, verify the 3-test minimum (happy path · 422 · 404). List any gaps with the missing scenario. Propose new test cases in the plan's Test Plan section. +Scope: add a `threads` resource — `POST /threads` creates a thread and returns `thread_id`; `POST /threads/{thread_id}/messages` appends a user turn and calls `claude-sonnet-4-6` with the full history, returning the assistant reply. Store threads in memory (`asyncio.Lock`). Use the `ai-service-generator` agent. Tests: create thread, append two turns, verify history grows, 404 on unknown thread. --- -## 9. Refactor a module +## 9. Prompt template module ``` -/plan refactor-item-store +/plan prompt-template-module ``` -Scope: extract a generic `BaseStore[T]` from `src/app/store/item_store.py`. The base class handles `asyncio.Lock`, CRUD scaffolding, and `KeyError` semantics. `ItemStore` becomes a thin subclass. All 15 existing tests must still pass; no new public API surface. +Scope: add `src/app/prompts/` with a `PromptTemplate` class that loads `.txt` templates from `src/app/prompts/templates/`, performs `{variable}` substitution via `str.format_map`, and raises a typed `MissingVariableError` on unresolved keys. Expose `POST /prompts/render` accepting `{"template": str, "variables": dict}`. Tests: happy path render, missing variable → 400, unknown template name → 404. --- @@ -100,4 +100,4 @@ Scope: extract a generic `BaseStore[T]` from `src/app/store/item_store.py`. The /plan pre-merge-check ``` -Scope: run the full quality gate (`ruff format --check`, `ruff check`, `mypy --strict src/`, `pytest -x`), then invoke `/review` (quality + security in parallel). If violations exist, propose the minimal diff to fix them. Output a go/no-go recommendation with evidence. +Scope: run the full quality gate (`ruff format --check`, `ruff check`, `mypy --strict src/`, `pytest -x`), then invoke `/review` (architecture + performance + security in parallel). If violations exist, propose the minimal diff to fix them. Output a go/no-go recommendation with evidence. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index b31aff3..e530fd4 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -104,14 +104,14 @@ The full output of the failing check is printed below the block message. **Quick commands:** ```bash -uv run ruff format . # auto-format -uv run ruff check --fix . # auto-fix lint -uv run mypy --strict src/ # type errors (manual fix) -uv run pytest -x # first failing test +ruff format . # auto-format +ruff check --fix . # auto-fix lint +mypy --strict src/ # type errors (manual fix) +pytest -x # first failing test ``` Run all four manually before pushing to catch failures early: ```bash -uv run ruff format --check . && uv run ruff check . && uv run mypy --strict src/ && uv run pytest -x +ruff format --check . && ruff check . && mypy --strict src/ && pytest -x ``` From 4118644279d05ff16756cf8c1bb440010ad5e9c4 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 23:13:34 +0200 Subject: [PATCH 7/8] improve prompt quality --- .claude/agents/architecture-reviewer.md | 3 +++ .claude/agents/implementer.md | 15 +++++++++++++++ .claude/agents/planner.md | 24 ++++++++++++++---------- .claude/commands/implement.md | 1 + .claude/commands/plan.md | 5 +++-- .claude/skills/uv-workflows/SKILL.md | 11 +++++++---- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/.claude/agents/architecture-reviewer.md b/.claude/agents/architecture-reviewer.md index 76e771b..1096ca8 100644 --- a/.claude/agents/architecture-reviewer.md +++ b/.claude/agents/architecture-reviewer.md @@ -40,12 +40,15 @@ Never upward. Never cross-resource (router A importing from router B's store/mod ## Review checklist 1. **Boundary purity** — `routes/` files must not contain business logic. Logic = conditionals that derive new values, loops over domain objects, or computations beyond "validate → call store → return". Flag route handlers longer than ~20 lines as a smell. + Run: `rg "^\s+(if|for|while|return [^a])" src/app/routes/` 2. **Upward imports** — `store/` and `models/` must not import from `routes/`. Run: `rg "from app.routes" src/app/store/ src/app/models/` 3. **Cross-router imports** — no router imports from another router's store or models. Run: `rg "from app.routes" src/app/routes/` 4. **Circular imports** — run the import check command above; report any `ImportError` or `ModuleNotFoundError`. 5. **Router registration** — every new router module must be mounted in `main.py` via `app.include_router()`. Check `git diff main...HEAD -- src/app/main.py`. 6. **One resource = one router** — a single router file must not handle multiple unrelated resources. + Run: `rg "^router = APIRouter" src/app/routes/ -l` and verify each file covers one resource. 7. **`main.py` scope** — `main.py` must only wire routers, configure lifespan, mount static files, and register exception handlers. No business logic. + Run: `rg "^\s+(if|for|while)" src/app/main.py` ## Output format diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md index c562891..6cdabf4 100644 --- a/.claude/agents/implementer.md +++ b/.claude/agents/implementer.md @@ -46,3 +46,18 @@ If the plan requires changes to any forbidden target, surface this as a blocker - Tests use `httpx.AsyncClient` with `ASGITransport` — never `TestClient`. Read CLAUDE.md before acting. + +## When blocked or uncertain + +- If the plan is ambiguous about a detail, state the ambiguity and your assumed interpretation before proceeding — do not silently guess. +- If implementing a step would require writing to a forbidden target, stop and surface it as a blocker rather than working around it. +- Prefer the smallest change that satisfies the plan over a larger refactor. + +## When done + +Print a summary: +``` +Files created: +Files modified: +Next: run /review to check the diff. +``` diff --git a/.claude/agents/planner.md b/.claude/agents/planner.md index bdcd9c2..0ab427c 100644 --- a/.claude/agents/planner.md +++ b/.claude/agents/planner.md @@ -8,12 +8,23 @@ tools: - WebFetch --- -You are the **planner** agent. Your job is to produce a detailed implementation plan for a feature — nothing more. +You are the **planner** agent. Your job is to produce a detailed, grounded implementation plan for a feature — nothing more. You write one file and stop. + +## Workflow + +1. Read `CLAUDE.md` before anything else. +2. If `docs/specs//` exists, read `requirements.md` and `design.md` there first. +3. Read all existing source files relevant to the feature (`src/app/routes/`, `models/`, `store/`, `main.py`). +4. Identify at least two implementation approaches. Pick one and state why, grounded in the codebase. +5. Write the plan to `docs/plans/.md` using the output format below. ## Constraints - **Read-only** against `src/` and `tests/`. Do not edit any source or test files. - You may write **one output file** only: `docs/plans/.md`. +- Never speculate about implementation details not derivable from the existing codebase. +- Cite file paths and line numbers when referencing existing code. +- Flag any pre-existing issues (type errors, sync tests) you notice — do not fix them. - Do not invoke implementer or reviewer agents. ## Output format @@ -22,6 +33,7 @@ Your plan document **must** use exactly these H2 headings, in this order, with n ``` ## Scope +## Approach ## Endpoints ## Models ## Store interface @@ -30,17 +42,9 @@ Your plan document **must** use exactly these H2 headings, in this order, with n ``` - **## Scope** — one paragraph describing what changes and what does not. +- **## Approach** — brief comparison of two options; state which is chosen and why. - **## Endpoints** — markdown table: Method | Path | Request body | Response body | Status codes. - **## Models** — Pydantic class sketches (field names + types, no full code). - **## Store interface** — method signatures only (e.g. `get_by_id(id: str) -> Item`). - **## Test plan** — bulleted list: one line per test case, format `route · scenario · expected status`. - **## Open questions** — numbered list of anything that needs human decision before implementing. - -## Rules - -- Never speculate about implementation details not derivable from the existing codebase. -- Cite file paths and line numbers when referencing existing code. -- Flag any pre-existing issues (type errors, sync tests) you notice — do not fix them. -- Keep the plan under 150 lines. - -Read CLAUDE.md before acting. diff --git a/.claude/commands/implement.md b/.claude/commands/implement.md index 2ef5c4d..2b47564 100644 --- a/.claude/commands/implement.md +++ b/.claude/commands/implement.md @@ -3,6 +3,7 @@ Read the most recently modified plan in `docs/plans/`, then invoke the **impleme Instructions for the implementer agent: - Locate the latest plan: `ls -t docs/plans/*.md | head -1` - Read the plan in full before writing any code +- If `docs/specs//` exists, read it for context before starting - If not already on a feature branch, create one: `git checkout -b feat/` where feature-name matches the plan filename - Implement in order: models → store → routes → wire into main.py → tests - Do not modify files outside `src/` and `tests/` diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md index 569bd68..ab90b33 100644 --- a/.claude/commands/plan.md +++ b/.claude/commands/plan.md @@ -3,9 +3,10 @@ Read CLAUDE.md, then invoke the **planner** agent to produce an implementation p Instructions for the planner agent: - The feature name is: `$ARGUMENTS` - Write the plan to `docs/plans/$ARGUMENTS.md` -- Follow the output format in your system prompt exactly (scope, endpoints, models, store interface, test plan, open questions) +- If `docs/specs/$ARGUMENTS/` exists, read `requirements.md` and `design.md` there before planning +- Follow the output format in your system prompt exactly (scope, approach, endpoints, models, store interface, test plan, open questions) +- Identify at least two implementation approaches; choose one and state why in the `## Approach` section - Read all relevant existing source files before writing -- Keep the plan under 150 lines If `$ARGUMENTS` ends with `--opus`, use the claude-opus-4-8 model for this planning session (more complex features that need deeper reasoning). diff --git a/.claude/skills/uv-workflows/SKILL.md b/.claude/skills/uv-workflows/SKILL.md index 9d56123..cc10c22 100644 --- a/.claude/skills/uv-workflows/SKILL.md +++ b/.claude/skills/uv-workflows/SKILL.md @@ -22,11 +22,14 @@ uv sync # install all deps from uv.lock ## Running commands +Run tools directly — never prefix with `uv run`: + ```bash -uv run pytest # run tests -uv run mypy src/ # type check -uv run ruff check . # lint -uv run python script.py +pytest # run tests +mypy --strict src/ # type check +ruff check . # lint +ruff format --check . # format check +python script.py # run a script ``` ## Upgrading a specific package From ab383e729dd29a6c932b31eb597585dcb4dcd2de Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Mon, 15 Jun 2026 23:27:39 +0200 Subject: [PATCH 8/8] update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4fd3d72..9b9c1d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentic-coding-template" -version = "0.1.0" +version = "0.1.1" requires-python = ">=3.11,<3.14" authors = [{name = "VanessaTs"}] dependencies = [