From 50c6051227ae4dae8f5d837d4e630671d8b1492d Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:15:32 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(pipeline):=20?= =?UTF-8?q?replace=20MCP=20program=20with=20skill=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agents/scout/output-20260216_114928.md | 576 ++++++++++++ .../agents/scout/output-20260216_115215.md | 827 ++++++++++++++++++ .../affected-repos.txt | 1 + .smithers/.gitkeep | 0 .smithers/workflow.db | Bin 0 -> 180224 bytes .smithers/workflow.tsx | 250 ++++++ .takopi-smithers/.gitkeep | 0 .takopi-smithers/autoheal-prompt.txt | 303 +++++++ AGENTS.md | 7 + CLAUDE.md | 9 + TAKOPI_SMITHERS.md | 40 + bun.lock | 351 ++++++++ package.json | 14 + python/src/cairo_coder/core/rag_pipeline.py | 29 +- python/src/cairo_coder/dspy/__init__.py | 6 +- .../cairo_coder/dspy/generation_program.py | 71 +- python/tests/conftest.py | 26 +- python/tests/integration/conftest.py | 8 +- python/tests/unit/test_generation_program.py | 82 +- python/tests/unit/test_rag_pipeline.py | 3 +- 20 files changed, 2479 insertions(+), 124 deletions(-) create mode 100644 .claude/cache/agents/scout/output-20260216_114928.md create mode 100644 .claude/cache/agents/scout/output-20260216_115215.md create mode 100644 .claude/tsc-cache/5d0c2956-0d3c-4ab8-a101-011a94d2ccaa/affected-repos.txt create mode 100644 .smithers/.gitkeep create mode 100644 .smithers/workflow.db create mode 100644 .smithers/workflow.tsx create mode 100644 .takopi-smithers/.gitkeep create mode 100644 .takopi-smithers/autoheal-prompt.txt create mode 100644 AGENTS.md create mode 100644 TAKOPI_SMITHERS.md create mode 100644 bun.lock create mode 100644 package.json diff --git a/.claude/cache/agents/scout/output-20260216_114928.md b/.claude/cache/agents/scout/output-20260216_114928.md new file mode 100644 index 00000000..11d379a0 --- /dev/null +++ b/.claude/cache/agents/scout/output-20260216_114928.md @@ -0,0 +1,576 @@ +# MCP Mode Documentation + +**Generated:** 2026-02-16 11:49:28 + +## Summary + +MCP (Model Context Protocol) mode is a special operating mode in Cairo Coder that **returns raw documentation without LLM generation**. Instead of synthesizing an answer, it formats and returns retrieved documents directly. This is useful for integration with external tools that need raw Cairo/Starknet documentation. + +**Key Difference:** Normal mode retrieves docs → generates answer via LLM. MCP mode retrieves docs → formats raw docs → returns. + +--- + +## 1. Where MCP Mode is Defined/Triggered + +### ✓ VERIFIED: API Routes (Entry Points) + +**File:** `python/src/cairo_coder/server/app.py` + +MCP mode is triggered via HTTP headers on three endpoints: + +#### Lines 367-392: Agent-specific endpoint +```python +@self.app.post("/v1/agents/{agent_id}/chat/completions") +async def agent_chat_completions( + agent_id: str, + request: ChatCompletionRequest, + req: Request, + background_tasks: BackgroundTasks, + mcp: str | None = Header(None), # Header 1: "mcp" + x_mcp_mode: str | None = Header(None, alias="x-mcp-mode"), # Header 2: "x-mcp-mode" + vector_db: SourceFilteredPgVectorRM = Depends(get_vector_db), + agent_factory: AgentFactory = Depends(get_agent_factory), +): + # Determine MCP mode + mcp_mode = bool(mcp or x_mcp_mode) # Truthy if either header present + + return await self._handle_chat_completion( + request, req, background_tasks, agent_factory, agent_id, mcp_mode, vector_db + ) +``` + +#### Lines 394-410: Legacy v1 endpoint +```python +@self.app.post("/v1/chat/completions") +async def v1_chat_completions(...): + mcp_mode = bool(mcp or x_mcp_mode) + return await self._handle_chat_completion(..., mcp_mode, ...) +``` + +#### Lines 412-428: Legacy endpoint +```python +@self.app.post("/chat/completions") +async def chat_completions(...): + mcp_mode = bool(mcp or x_mcp_mode) + return await self._handle_chat_completion(..., mcp_mode, ...) +``` + +**Trigger mechanism:** Either HTTP header activates MCP mode: +- `mcp: ` +- `x-mcp-mode: ` + +--- + +### ✓ VERIFIED: Database Models + +**File:** `python/src/cairo_coder/db/models.py` (Lines 16-30) + +```python +class UserInteraction(BaseModel): + """Represents a record in the user_interactions table.""" + + id: uuid.UUID = Field(default_factory=uuid.uuid4) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + agent_id: str + mcp_mode: bool = False # ← Stored for analytics/tracking + conversation_id: Optional[str] = None + user_id: Optional[str] = None + chat_history: Optional[list[dict[str, Any]]] = None + query: str + generated_answer: Optional[str] = None + retrieved_sources: Optional[list[RetrievedSourceData]] = None + llm_usage: Optional[dict[str, Any]] = None +``` + +**File:** `python/src/cairo_coder/db/session.py` (Line 74) + +```python +mcp_mode BOOLEAN NOT NULL DEFAULT FALSE, +``` + +MCP mode is persisted in the database for every interaction to track usage patterns. + +--- + +### ✓ VERIFIED: Agent Factory + +**File:** `python/src/cairo_coder/core/agent_factory.py` (Lines 42-69) + +```python +def get_or_create_agent( + self, agent_id: str, mcp_mode: bool = False +) -> RagPipeline: + """ + Get an existing agent from cache or create a new one. + + Args: + agent_id: Agent identifier + mcp_mode: Whether to use MCP mode + + Returns: + Cached or newly created RagPipeline instance + """ + # Check cache first + cache_key = f"{agent_id}_{mcp_mode}" # ← Separate cache per mode + if cache_key in self._agent_cache: + return self._agent_cache[cache_key] + + # Get agent spec from registry + _, spec = get_agent_by_string_id(agent_id) + + # Create new agent from spec + agent = spec.build(self.vector_db, self.vector_store_config) + + # Cache the agent + self._agent_cache[cache_key] = agent +``` + +**Important:** MCP mode affects caching. Each agent has two cached instances: one for normal mode, one for MCP mode. + +--- + +## 2. What MCP Mode Does Differently + +### ✓ VERIFIED: Core Pipeline Branching + +**File:** `python/src/cairo_coder/core/rag_pipeline.py` + +#### Non-streaming path (Lines 158-203) + +```python +async def aforward( + self, + query: str, + chat_history: list[Message] | None = None, + mcp_mode: bool = False, # ← Flag passed through pipeline + sources: list[DocumentSource] | None = None, +) -> dspy.Prediction: + """ + Execute the RAG pipeline and return a DSPy Prediction. + + Args: + query: User's Cairo/Starknet programming question + chat_history: Previous conversation messages + mcp_mode: Return raw documents without generation # ← KEY DIFFERENCE + sources: Optional source filtering + """ + chat_history_str = self._format_chat_history(chat_history or []) + processed_query, documents, grok_citations = ( + await self._aprocess_query_and_retrieve_docs(query, chat_history_str, sources) + ) + + # BRANCHING POINT + if mcp_mode: + # MCP MODE: Use special program that formats docs without LLM generation + result = await self.mcp_generation_program.acall(documents) + return dspy.Prediction( + processed_query=processed_query, + documents=documents, + grok_citations=grok_citations, + answer=result.answer, # ← Formatted raw docs + formatted_sources=self._format_sources(documents, grok_citations), + ) + + # NORMAL MODE: Prepare context and generate with LLM + context = self._prepare_context(documents) + + result = await self.generation_program.acall( + query=query, context=context, chat_history=chat_history_str + ) + + return dspy.Prediction( + processed_query=processed_query, + documents=documents, + grok_citations=grok_citations, + answer=result.answer, # ← LLM-generated answer + formatted_sources=self._format_sources(documents, grok_citations), + ) +``` + +#### Streaming path (Lines 210-274) + +```python +async def aforward_streaming( + self, + query: str, + chat_history: list[Message] | None = None, + mcp_mode: bool = False, + sources: list[DocumentSource] | None = None, +) -> AsyncGenerator[StreamEvent, None]: + """Execute the complete RAG pipeline with streaming support.""" + # ... retrieve documents ... + + final_answer: str | None = None + + if mcp_mode: + # MCP mode: Return raw documents + await _emit( + StreamEvent( + type=StreamEventType.PROCESSING, + data="Formatting documentation...", + ) + ) + + mcp_prediction = await self.mcp_generation_program.acall(documents) + final_answer = mcp_prediction.answer + # Emit single response plus a final response event + await _emit( + StreamEvent(type=StreamEventType.RESPONSE, data=mcp_prediction.answer) + ) +``` + +**Key behavior differences:** + +| Aspect | Normal Mode | MCP Mode | +|--------|-------------|----------| +| **LLM Usage** | Yes - calls `generation_program.acall()` | No - calls `mcp_generation_program.acall()` | +| **Context Preparation** | `self._prepare_context(documents)` | Skipped | +| **Output** | Synthesized answer from LLM | Formatted raw documents | +| **Streaming** | Token-by-token LLM streaming | Single formatted response | +| **Processing Message** | "Generating response..." | "Formatting documentation..." | + +--- + +### ✓ VERIFIED: MCP Generation Program + +**File:** `python/src/cairo_coder/dspy/generation_program.py` (Lines 289-360) + +```python +class McpGenerationProgram(dspy.Module): + """ + Special generation program for MCP (Model Context Protocol) mode. + + This program returns raw documentation without LLM generation, + useful for integration with other tools that need Cairo documentation. + """ + + def __init__(self): + super().__init__() + + def forward(self, documents: list[Document]) -> dspy.Prediction: + """ + Format documents for MCP mode response. + + Args: + documents: List of retrieved documents + + Returns: + Formatted documentation string + """ + if not documents: + return dspy.Prediction(answer="No relevant documentation found.") + + formatted_docs = [] + for i, doc in enumerate(documents, 1): + source = doc.source + url = doc.source_link + title = doc.title + + formatted_doc = f""" +## {i}. {title} + +**Source:** {source} +**URL:** {url} + +{doc.page_content} + +--- +""" + formatted_docs.append(formatted_doc) + + return dspy.Prediction(answer="\n".join(formatted_docs)) + + async def aforward(self, documents: list[Document]) -> dspy.Prediction: + """Format documents for MCP mode response.""" + return self(documents) + + +def create_mcp_generation_program() -> McpGenerationProgram: + """ + Factory function to create an MCP GenerationProgram instance. + + Returns: + Configured McpGenerationProgram instance + """ + return McpGenerationProgram() +``` + +**No LLM calls** - this is pure Python formatting. The program: +1. Takes retrieved documents +2. Formats each with markdown structure +3. Returns concatenated string + +**Cost:** Near-zero (no LLM tokens consumed for generation) + +--- + +### ✓ VERIFIED: Agent Registry Configuration + +**File:** `python/src/cairo_coder/agents/registry.py` (Lines 104-136) + +Every agent has an MCP generation program factory: + +```python +def _create_mcp_generation_program() -> Any: + """Factory for MCP generation program.""" + from cairo_coder.dspy.generation_program import create_mcp_generation_program + + return create_mcp_generation_program() + + +registry: dict[AgentId, AgentSpec] = { + AgentId.CAIRO_CODER: AgentSpec( + name="Cairo Coder", + description="General Cairo programming assistant", + sources=list(DocumentSource), + pipeline_builder=RagPipelineFactory.create_pipeline, + query_processor_factory=_create_query_processor, + generation_program_factory=_create_cairo_coder_generation_program, + mcp_generation_program_factory=_create_mcp_generation_program, # ← MCP support + max_source_count=MAX_SOURCE_COUNT, + similarity_threshold=SIMILARITY_THRESHOLD, + ), + AgentId.STARKNET: AgentSpec( + name="Starknet Agent", + description="Assistant for the Starknet ecosystem", + sources=list(DocumentSource), + pipeline_builder=RagPipelineFactory.create_pipeline, + query_processor_factory=_create_query_processor, + generation_program_factory=_create_starknet_generation_program, + mcp_generation_program_factory=_create_mcp_generation_program, # ← MCP support + max_source_count=MAX_SOURCE_COUNT, + similarity_threshold=SIMILARITY_THRESHOLD, + ), +} +``` + +**All agents** share the same `McpGenerationProgram` - it's agent-agnostic. + +--- + +## 3. Code Flow: Request Path + +### Normal Mode Flow + +``` +1. HTTP Request (no mcp headers) + ↓ +2. app.py: agent_chat_completions() + - mcp_mode = False + ↓ +3. app.py: _handle_chat_completion() + - Creates agent from factory + - Calls _stream_chat_completion() or _generate_chat_completion() + ↓ +4. app.py: _stream_chat_completion() + - Calls agent.aforward_streaming(query, history, mcp_mode=False) + ↓ +5. rag_pipeline.py: aforward_streaming() + - Retrieves documents via query processor + - Prepares context from documents + - Calls generation_program.acall(query, context, history) + ↓ +6. generation_program.py: GenerationProgram + - LLM synthesizes answer from context + - Streams tokens back + ↓ +7. app.py: Streams tokens to client + - Logs interaction with mcp_mode=False +``` + +### MCP Mode Flow + +``` +1. HTTP Request with header: x-mcp-mode: true + ↓ +2. app.py: agent_chat_completions() + - mcp_mode = bool(mcp or x_mcp_mode) = True + ↓ +3. app.py: _handle_chat_completion() + - Creates agent from factory (cached with key "agent_id_True") + - Calls _stream_chat_completion() or _generate_chat_completion() + ↓ +4. app.py: _stream_chat_completion() + - Calls agent.aforward_streaming(query, history, mcp_mode=True) + ↓ +5. rag_pipeline.py: aforward_streaming() + - Retrieves documents via query processor + - SKIPS context preparation + - SKIPS LLM generation + - Calls mcp_generation_program.acall(documents) + ↓ +6. generation_program.py: McpGenerationProgram + - Formats documents as markdown (no LLM) + - Returns formatted string + ↓ +7. app.py: Emits single RESPONSE event with formatted docs + - Logs interaction with mcp_mode=True +``` + +### Key Differences Table + +| Stage | Normal Mode | MCP Mode | +|-------|-------------|----------| +| **Trigger** | No special headers | `mcp` or `x-mcp-mode` header | +| **Agent Cache** | `agent_id_False` | `agent_id_True` | +| **Document Retrieval** | ✓ Same | ✓ Same | +| **Query Processing** | ✓ Same | ✓ Same | +| **Context Prep** | ✓ `_prepare_context()` | ✗ Skipped | +| **Generation** | `generation_program.acall()` | `mcp_generation_program.acall()` | +| **LLM Calls** | Multiple (query + generation) | One (query processing only) | +| **Output Format** | Synthesized answer | Raw docs with metadata | +| **Streaming** | Token-by-token | Single chunk | +| **Cost** | ~5-10k tokens | ~1-2k tokens | + +--- + +## 4. Raw Context Returned + +### ✓ VERIFIED: Output Format + +**File:** `python/src/cairo_coder/dspy/generation_program.py` (Lines 313-329) + +The MCP mode returns structured markdown for each document: + +```markdown +## 1. Document Title + +**Source:** DocumentSource.CAIRO_BOOK +**URL:** https://book.cairo-lang.org/ch01-01-hello-world.html + + + +--- + +## 2. Second Document Title + +**Source:** DocumentSource.STARKNET_DOCS +**URL:** https://docs.starknet.io/... + + + +--- +``` + +**Structure per document:** +- Numbered heading with title +- Source enum value +- Source URL +- Full page content +- Horizontal rule separator + +**Example output:** + +```markdown +## 1. Getting Started with Cairo + +**Source:** DocumentSource.CAIRO_BOOK +**URL:** https://book.cairo-lang.org/ch01-01-getting-started.html + +Cairo is a programming language for writing provable programs... + +[full content] + +--- + +## 2. Smart Contracts on Starknet + +**Source:** DocumentSource.STARKNET_DOCS +**URL:** https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/ + +Starknet contracts are written in Cairo... + +[full content] + +--- +``` + +### Document Fields Included + +**File:** `python/src/cairo_coder/dspy/generation_program.py` (Lines 314-317) + +```python +source = doc.source # DocumentSource enum +url = doc.source_link # String URL +title = doc.title # String title +content = doc.page_content # Full text content +``` + +### No Content Available Case + +**Line 310-311:** +```python +if not documents: + return dspy.Prediction(answer="No relevant documentation found.") +``` + +--- + +## 5. Usage in Optimizers + +MCP mode is also used internally for DSPy optimization: + +**File:** `python/src/cairo_coder/optimizers/generation_optimizer_cairo-coder.py` (Lines 59-75) + +```python +# Create agent with MCP mode +documentation_fetcher = agent_factory.get_or_create_agent("cairo-coder", mcp_mode=True) + +@dp.metric +async def relevance_metric(example: dspy.Example, prediction: dspy.Prediction) -> float: + query = example.query + generated_answer = prediction.answer + + # Fetch documentation context using MCP mode + context_prediction = await documentation_fetcher.acall(query=query, mcp_mode=True) + documentation_context = context_prediction.answer # Raw docs + + # Evaluate with LLM + evaluation = await evaluator.acall( + query=query, + generated_answer=generated_answer, + documentation_context=documentation_context, + ) + return evaluation.relevance_score +``` + +**Use case:** Fetch raw docs to use as ground truth for evaluation without double-generation overhead. + +--- + +## Key Files Summary + +| File | Purpose | Lines of Interest | +|------|---------|-------------------| +| `server/app.py` | API routes, MCP trigger | 367-428, 442-450, 508-648, 673-680 | +| `core/rag_pipeline.py` | Pipeline branching logic | 158-203, 210-274 | +| `dspy/generation_program.py` | MCP program implementation | 289-360 | +| `core/agent_factory.py` | Agent caching by mode | 42-69 | +| `agents/registry.py` | Agent MCP program factory | 104-136 | +| `db/models.py` | Interaction tracking | 16-30 | +| `db/session.py` | Database schema | 74 | +| `optimizers/generation_optimizer_*.py` | Internal MCP usage | 59-75 | + +--- + +## Design Rationale + +### Why MCP Mode Exists + +1. **External Tool Integration**: Other tools (like MCP servers) need raw documentation, not synthesized answers +2. **Cost Efficiency**: Skip expensive LLM generation when raw docs are sufficient +3. **Evaluation**: Use as ground truth for DSPy optimization without circular dependencies +4. **Transparency**: Expose exactly what documents were retrieved for debugging + +### Why It's Built This Way + +1. **Shared Retrieval**: Both modes use same query processing and document retrieval for consistency +2. **Separate Programs**: `GenerationProgram` vs `McpGenerationProgram` keeps concerns separated +3. **Cache Separation**: Different cache keys prevent mode interference +4. **Header-Based**: HTTP headers allow easy client-side toggle without API changes + +--- + +## Open Questions + +None - MCP mode is fully documented and implemented consistently throughout the codebase. diff --git a/.claude/cache/agents/scout/output-20260216_115215.md b/.claude/cache/agents/scout/output-20260216_115215.md new file mode 100644 index 00000000..1b6db953 --- /dev/null +++ b/.claude/cache/agents/scout/output-20260216_115215.md @@ -0,0 +1,827 @@ +# Codebase Report: Cairo Coder RAG Pipeline & Test Structure +Generated: 2026-02-16 + +## Summary + +Cairo Coder is a DSPy-based RAG (Retrieval-Augmented Generation) system for Cairo/Starknet programming assistance. The pipeline follows a **three-stage architecture**: Query Processing → Document Retrieval → Generation, with optional Grok web search augmentation and retrieval judging. Tests are organized into **unit tests** (mocked components) and **integration tests** (full pipeline with TestClient). + +--- + +## Pipeline Flow: API Request → Response + +### Entry Point: FastAPI Server + +**File:** `python/src/cairo_coder/server/app.py` + +**Key Endpoints:** +- `POST /v1/chat/completions` (line 394) - Legacy endpoint +- `POST /v1/agents/{agent_id}/chat/completions` (line 361) - Agent-specific endpoint +- `POST /chat/completions` (line 412) - Alternative legacy endpoint +- `POST /v1/suggestions` (line 430) - Follow-up suggestion generation +- `GET /v1/agents` (line 335) - List available agents + +### Request Flow + +``` +1. FastAPI endpoint receives ChatCompletionRequest + ↓ +2. Extract conversation_id, user_id, mcp_mode headers + (lines 453-459) + ↓ +3. Get or create agent via AgentFactory + (line 473: agent_factory.get_or_create_agent()) + ↓ +4. Branch: Streaming or Non-streaming? + + STREAMING (line 479): + ↓ + 5a. _stream_chat_completion() → agent.aforward_streaming() + (line 508-648) + • Emits StreamEvents (PROCESSING, SOURCES, REASONING, RESPONSE, FINAL_RESPONSE, END) + • Logs interaction in background via log_interaction_raw() (line 626) + + NON-STREAMING (line 492): + ↓ + 5b. _generate_chat_completion() → agent.acall() + (line 672-726) + • Returns ChatCompletionResponse with usage stats + • Logs interaction in background via log_interaction_task() (line 494) +``` + +--- + +## RAG Pipeline Architecture + +**File:** `python/src/cairo_coder/core/rag_pipeline.py` + +### Core Class: `RagPipeline(dspy.Module)` (line 60) + +**Configuration:** `RagPipelineConfig` (line 45-57) +- `query_processor: QueryProcessorProgram` +- `document_retriever: DocumentRetrieverProgram` +- `generation_program: GenerationProgram` +- `mcp_generation_program: McpGenerationProgram` +- `sources: list[DocumentSource]` +- `max_source_count: int` +- `similarity_threshold: float` + +### Pipeline Stages + +#### Stage 1: Query Processing + +**File:** `python/src/cairo_coder/dspy/query_processor.py` + +**Entry:** `QueryProcessorProgram.aforward()` (line 129) + +**Signature:** `CairoQueryAnalysis` (line 47-70) +- **Inputs:** `query: str`, `chat_history: str` +- **Outputs:** + - `search_queries: list[str]` (3 semantic search queries for vector store) + - `resources: list[str]` (documentation sources to search) + +**Optimized Program:** Loads `optimizers/results/optimized_retrieval_program.json` (line 86-89) + +**Output:** `ProcessedQuery` (line 148-154) +```python +ProcessedQuery( + original=query, + search_queries=search_queries, + is_contract_related=bool, + is_test_related=bool, + resources=list[DocumentSource] +) +``` + +**Resource Descriptions:** `RESOURCE_DESCRIPTIONS` (line 20-31) maps `DocumentSource` enums to descriptions: +- `CAIRO_BOOK` - Core language syntax, smart contracts, storage, events +- `STARKNET_DOCS` - Protocol, STWO prover, APIs, syscalls +- `STARKNET_FOUNDRY` - snforge/sncast toolchain +- `CAIRO_BY_EXAMPLE` - Practical code snippets +- `OPENZEPPELIN_DOCS` - Standard contract implementations +- `CORELIB_DOCS` - Core library, stdlib, macros +- `SCARB_DOCS` - Package manager, build tool +- `STARKNET_JS` - JavaScript/TypeScript integration +- `STARKNET_BLOG` - Latest updates, announcements +- `DOJO_DOCS` - Onchain game framework + +#### Stage 2: Document Retrieval + +**File:** `python/src/cairo_coder/dspy/document_retriever.py` + +**Entry:** `DocumentRetrieverProgram.aforward()` (line 288) + +**Vector Store:** `SourceFilteredPgVectorRM` (line 34-239) +- Extends `PgVectorRM` with source filtering +- Async forward: `aforward()` (line 63) uses asyncpg pool +- Filters by `DocumentSource` enum values (line 100-104) +- Applies similarity threshold (line 107-111): `1 - cosine_similarity < threshold` +- SQL: `SELECT * FROM {table} WHERE metadata->>'source' = ANY($1) AND embedding <=> $2 < $3 ORDER BY embedding <=> $2 LIMIT $4` + +**Retrieval Process:** +1. Fetch documents for each `search_query` from `ProcessedQuery.search_queries` (line 371-377) +2. Deduplicate by converting to set (line 379-383) +3. Enhance with templates if contract/test-related (line 387-426) + - `CONTRACT_TEMPLATE` for contract queries + - `TEST_TEMPLATE` for test queries + +**Output:** `dspy.Prediction(documents=list[Document])` (line 316) + +#### Stage 2.5: Retrieval Judging (Optional) + +**File:** `python/src/cairo_coder/dspy/retrieval_judge.py` + +**Signature:** `RetrievalRecallPrecision` (line 32-80) +- Scores each document 0.0-1.0 for relevance to query +- Filters documents below threshold (default 0.4) +- Adds metadata: `llm_judge_score`, `llm_judge_reason` + +**Execution:** `RagPipeline._aprocess_query_and_retrieve_docs()` (line 128-140) +```python +with dspy.context(lm=dspy.LM("gemini/gemini-flash-lite-latest")): + judge_pred = await self.retrieval_judge.acall(query=query, documents=documents) + documents = judge_pred.documents # Filtered list +``` + +#### Stage 2.6: Grok Search Augmentation (Optional) + +**File:** `python/src/cairo_coder/dspy/grok_search.py` + +**Trigger:** When `DocumentSource.STARKNET_BLOG` in retrieval sources (line 116) + +**Function:** `GrokSearchProgram.aforward()` fetches recent web/X posts about Starknet +- Adds virtual "grok-answer" document with summary (line 143-149) +- Returns citations list (URLs) + +#### Stage 3: Response Generation + +**File:** `python/src/cairo_coder/dspy/generation_program.py` + +**Programs:** +1. **Normal Mode:** `GenerationProgram` (line 157-287) + - Uses `CairoCodeGeneration` signature (line 23-43) or `StarknetEcosystemGeneration` (line 64-153) + - Chain of Thought reasoning + - Loads optimized programs: `optimizers/results/optimized_generation_{agent_id}.json` (line 192-195) + - Streaming support: `aforward_streaming()` (line 225-257) yields `StreamResponse` chunks + +2. **MCP Mode:** `McpGenerationProgram` (line 289-338) + - Returns raw formatted documentation without LLM generation + - Format: Numbered sections with title, source, URL, content + +**Signatures:** +- `CairoCodeGeneration` (line 23): For cairo-coder agent + - Input: `query`, `context`, `chat_history` + - Output: `answer` (Cairo code in ```cairo blocks) + +- `StarknetEcosystemGeneration` (line 64): For starknet-agent + - Detailed citation rules (inline markdown links) + - LaTeX formula support + - Code generation best practices + - Out-of-scope query handling + +**Context Preparation:** `RagPipeline._prepare_context()` (line 442-493) +```markdown +Relevant Documentation: + +## [Document Title](url) +*Source: Display Name* + +{page_content} + +--- +``` + +**Output:** `dspy.Prediction(answer=str)` + +#### Stage 4: Response Assembly + +**File:** `python/src/cairo_coder/core/rag_pipeline.py` + +**Final Prediction:** `RagPipeline.aforward()` (line 153-203) +```python +dspy.Prediction( + processed_query=ProcessedQuery, + documents=list[Document], + grok_citations=list[str], + answer=str, + formatted_sources=list[FormattedSource] +) +``` + +**Formatted Sources:** `_format_sources()` (line 396-440) +```python +[ + { + "metadata": { + "title": str, + "url": str, + "source_type": "documentation" | "web_search" + } + } +] +``` + +**Usage Tracking:** DSPy's `track_usage()` captures token counts (line 232) +```python +{ + "gemini/gemini-3-flash-preview": { + "prompt_tokens": int, + "completion_tokens": int, + "total_tokens": int + } +} +``` + +--- + +## Agent Registry + +**File:** `python/src/cairo_coder/agents/registry.py` + +### Available Agents + +**Enum:** `AgentId` (line 23-27) +```python +class AgentId(str, Enum): + CAIRO_CODER = "cairo-coder" + STARKNET = "starknet-agent" +``` + +**Registry:** `registry: dict[AgentId, AgentSpec]` (line 113-136) + +#### 1. Cairo Coder Agent (line 114-124) +```python +AgentId.CAIRO_CODER: AgentSpec( + name="Cairo Coder", + description="General Cairo programming assistant", + sources=list(DocumentSource), # All sources + pipeline_builder=RagPipelineFactory.create_pipeline, + query_processor_factory=_create_query_processor, + generation_program_factory=_create_cairo_coder_generation_program, + mcp_generation_program_factory=_create_mcp_generation_program, + max_source_count=5, + similarity_threshold=0.65 +) +``` + +**Generation Program:** Uses `CairoCodeGeneration` signature +- Optimized program: `optimizers/results/optimized_generation_cairo-coder.json` + +#### 2. Starknet Agent (line 125-135) +```python +AgentId.STARKNET: AgentSpec( + name="Starknet Agent", + description="Assistant for the Starknet ecosystem (contracts, tools, docs).", + sources=list(DocumentSource), # All sources + pipeline_builder=RagPipelineFactory.create_pipeline, + query_processor_factory=_create_query_processor, + generation_program_factory=_create_starknet_generation_program, + mcp_generation_program_factory=_create_mcp_generation_program, + max_source_count=5, + similarity_threshold=0.65 +) +``` + +**Generation Program:** Uses `StarknetEcosystemGeneration` signature +- Optimized program: `optimizers/results/optimized_generation_starknet-agent.json` +- Specialized prompts with citation rules, LaTeX support, code generation guidelines + +### Agent Factory + +**File:** `python/src/cairo_coder/core/agent_factory.py` + +**Class:** `AgentFactory` (line 20-110) + +**Key Methods:** +- `get_or_create_agent(agent_id, mcp_mode)` (line 42-71) + - Caches agents by `{agent_id}_{mcp_mode}` key + - Builds from `AgentSpec` via `spec.build(vector_db, config)` + +- `get_available_agents()` (line 77-86) + - Returns `["cairo-coder", "starknet-agent"]` + +- `get_agent_info(agent_id)` (line 88-110) + - Returns metadata dict with `id`, `name`, `description`, `sources`, `max_source_count`, `similarity_threshold` + +**Lazy Program Creation:** Programs are NOT created at import time (line 69) +- Factory functions (`_create_cairo_coder_generation_program`, etc.) are called in `AgentSpec.build()` +- Avoids expensive DSPy initialization on module load + +--- + +## Test Structure + +**Root:** `python/tests/` + +### Shared Fixtures: `conftest.py` + +**Key Fixtures:** + +#### Mock Components +- `mock_vector_db` (line 55-72) - Mocked `SourceFilteredPgVectorRM` with async pool +- `mock_lm` (line 86-111) - Mocked DSPy `ChainOfThought` returning sample predictions +- `mock_agent` (line 178-250) - Mocked `RagPipeline` with `acall()` and `aforward_streaming()` +- `mock_agent_factory` (line 145-174) - Mocked `AgentFactory` with agent registry integration + +#### Pipeline Components +- `mock_query_processor` (line 436-445) - Returns `sample_processed_query` +- `mock_document_retriever` (line 449-458) - Returns `sample_documents` +- `mock_generation_program` (line 462-483) - Returns sample Cairo code, supports streaming +- `mock_mcp_generation_program` (line 487-513) - Returns formatted MCP documentation +- `mock_retrieval_judge` (line 517-560) - Filters docs by score threshold (0.4) + +#### Configuration +- `mock_vector_store_config` (line 76-83) - PostgreSQL connection config +- `sample_documents` (line 344-388) - 4 sample docs from Cairo Book, Starknet Docs, Scarb, OpenZeppelin +- `sample_processed_query` (line 333-341) - "How do I create a Cairo contract?" + +#### Integration Fixtures +- `postgres_container` (line 263-279) - Session-scoped testcontainer (Docker-based) +- `client` (line 283-325) - FastAPI `TestClient` with dependency injection +- `pipeline_config` (line 563-581) - Full `RagPipelineConfig` with mocked components +- `pipeline` (line 608-621) - Full `RagPipeline` instance with mocked judge + +#### Auto-Applied Fixtures +- `optimizer_artifacts_optional` (line 43-46) - Sets `OPTIMIZER_RUN=1` to skip optimizer file loading +- `enable_dspy_track_usage` (line 36-39) - Enables DSPy token usage tracking +- `clean_config_env_vars` (line 396-433) - Cleans env vars before each test + +--- + +### Unit Tests (Mocked Dependencies) + +**Location:** `python/tests/unit/` + +#### 1. `test_rag_pipeline.py` (30k file) + +**Tests RAG Pipeline orchestration:** +- `test_async_pipeline_execution` - Tests `pipeline.acall()` calls all components +- `test_streaming_pipeline_execution` - Tests `pipeline.aforward_streaming()` emits correct event sequence +- `test_mcp_mode_execution` - Tests MCP mode uses `mcp_generation_program` +- `test_pipeline_with_chat_history` - Tests chat history formatting and passing +- `test_pipeline_with_custom_sources` - Tests source filtering +- `test_empty_documents_handling` - Tests pipeline behavior with no retrieved docs + +**Helper Functions:** +- `create_custom_documents(specs)` (line 28-42) - Create test documents with specific metadata +- `create_custom_retrieval_judge(score_map, threshold)` (line 45-74) - Mock judge with custom scoring + +#### 2. `test_query_processor.py` (6.5k file) + +**Tests Query Processing:** +- Query analysis and resource identification +- Search query extraction +- Contract/test-related detection +- Resource validation and fallback logic + +#### 3. `test_document_retriever.py` (12k file) + +**Tests Document Retrieval:** +- Vector store search with source filtering +- Similarity threshold filtering +- Document deduplication +- Template enhancement (contract/test templates) +- Async retrieval via `aforward()` + +#### 4. `test_generation_program.py` (15k file) + +**Tests Generation Programs:** +- Cairo code generation with `CairoCodeGeneration` signature +- Starknet ecosystem responses with `StarknetEcosystemGeneration` signature +- Streaming generation via `aforward_streaming()` +- MCP mode formatting +- Chat history formatting +- AdapterParseError retry logic + +#### 5. `test_retrieval_judge.py` (8.4k file) + +**Tests Retrieval Judge:** +- Document scoring (0.0-1.0 scale) +- Threshold filtering (default 0.4) +- Metadata attachment (`llm_judge_score`, `llm_judge_reason`) +- Async judging via `acall()` + +#### 6. `test_agent_factory.py` (7.9k file) + +**Tests Agent Factory:** +- Agent creation from registry +- Agent caching by `{agent_id}_{mcp_mode}` key +- Available agents listing +- Agent info retrieval +- Invalid agent handling + +#### 7. `test_grok_integration.py` (5.6k file) + +**Tests Grok Search:** +- Web search augmentation +- Citation extraction +- Virtual document creation +- Error handling + +#### 8. `test_config.py` (3.5k file) + +**Tests Configuration:** +- Config loading from environment +- Vector store config validation +- Default value handling + +#### 9. `test_optimizers_loading.py` (2.4k file) + +**Tests Optimizer Artifacts:** +- Optimized program loading +- Missing optimizer file handling +- OPTIMIZER_RUN flag behavior + +--- + +### Integration Tests (Full Pipeline) + +**Location:** `python/tests/integration/` + +#### 1. `test_server_integration.py` (31k file) + +**Tests FastAPI Server:** +- `test_health_check_integration` - `GET /` returns `{"status": "ok"}` +- `test_list_agents` - `GET /v1/agents` returns cairo-coder and starknet-agent +- `test_full_agent_workflow` - Lists agents → Creates chat completion +- `test_multiple_conversation_turns` - Tests multi-turn conversations with history +- `test_streaming_integration` - Tests Server-Sent Events streaming +- `test_error_handling_integration` - Tests 400/500 error responses +- `test_agent_specific_endpoint` - Tests `POST /v1/agents/{agent_id}/chat/completions` +- `test_mcp_mode_header` - Tests `x-mcp-mode` and `mcp` headers +- `test_conversation_id_tracking` - Tests `x-conversation-id` header +- `test_user_id_hashing` - Tests `x-user-id` and `x-api-key` hashing +- `test_concurrent_requests` - Tests parallel request handling + +**Setup:** +- Uses `client` fixture with `TestClient` +- Mocks `get_vector_db` and `get_agent_factory` dependencies +- Uses ephemeral PostgreSQL via `postgres_container` (Docker) + +#### 2. `test_insights_api.py` (15k file) + +**Tests Insights API:** +- `GET /v1/insights/queries` - Query analytics +- `GET /v1/insights/queries/top` - Top queries +- `GET /v1/insights/agents/usage` - Agent usage stats +- `GET /v1/insights/sources/popularity` - Source popularity +- Date range filtering +- Pagination +- Error handling + +**Database:** Uses PostgreSQL container with `UserInteraction` table + +#### 3. `test_config_integration.py` (3.2k file) + +**Tests Configuration Integration:** +- Environment variable loading +- Database connection setup +- Vector store configuration + +--- + +## Key Abstractions + +### DSPy Signatures + +**Purpose:** Define input/output schema for LLM calls + +1. **`CairoQueryAnalysis`** (`query_processor.py:47`) + - Input: `query`, `chat_history` + - Output: `search_queries`, `resources` + +2. **`CairoCodeGeneration`** (`generation_program.py:23`) + - Input: `query`, `context`, `chat_history` + - Output: `answer` (Cairo code in ```cairo blocks) + +3. **`StarknetEcosystemGeneration`** (`generation_program.py:64`) + - Input: `query`, `context`, `chat_history` + - Output: `answer` (with citation rules, LaTeX support) + +4. **`RetrievalRecallPrecision`** (`retrieval_judge.py:32`) + - Input: `query`, `system_resource` + - Output: `score` (0.0-1.0), `reason` + +5. **`SuggestionGeneration`** (`suggestion_program.py`) + - Input: `chat_history` + - Output: `suggestions` (list of 4-5 follow-up questions) + +### DSPy Modules + +**Purpose:** Encapsulate program logic with reusable components + +1. **`QueryProcessorProgram`** (`query_processor.py:72`) + - Wraps `dspy.Predict(CairoQueryAnalysis)` + - Loads optimized program from JSON + - Validates resources against `DocumentSource` enum + +2. **`DocumentRetrieverProgram`** (`document_retriever.py:243`) + - Wraps `SourceFilteredPgVectorRM` + - Handles multi-query retrieval and deduplication + - Enhances with templates based on query type + +3. **`GenerationProgram`** (`generation_program.py:157`) + - Wraps `dspy.ChainOfThought(Signature)` + - Loads optimized program from JSON + - Supports streaming via `dspy.streamify()` + +4. **`McpGenerationProgram`** (`generation_program.py:289`) + - No LLM call - formats documents as markdown + - Used when `mcp_mode=True` + +5. **`RagPipeline`** (`rag_pipeline.py:60`) + - Orchestrates all stages + - Supports async (`acall`) and streaming (`aforward_streaming`) + - Tracks LLM usage via `dspy.track_usage()` + +6. **`RetrievalJudge`** (`retrieval_judge.py`) + - Scores and filters retrieved documents + - Uses fast LLM: `gemini/gemini-flash-lite-latest` + +### Core Types + +**File:** `python/src/cairo_coder/core/types.py` + +1. **`DocumentSource`** (Enum) + - `CAIRO_BOOK`, `STARKNET_DOCS`, `STARKNET_FOUNDRY`, etc. + - Used for source filtering in vector store + +2. **`ProcessedQuery`** (Dataclass) + - `original: str` + - `search_queries: list[str]` + - `is_contract_related: bool` + - `is_test_related: bool` + - `resources: list[DocumentSource]` + +3. **`Document`** (Dataclass) + - `page_content: str` + - `metadata: dict` + - Properties: `title`, `source`, `source_link` (extracted from metadata) + +4. **`Message`** (Dataclass) + - `role: Role` ("user" | "assistant" | "system") + - `content: str` + +5. **`PipelineResult`** (Dataclass) + - `processed_query: ProcessedQuery` + - `documents: list[Document]` + - `grok_citations: list[str]` + - `usage: dict` (token counts) + - `answer: str` + - `formatted_sources: list[FormattedSource]` + +6. **`StreamEvent`** (Dataclass) + - `type: StreamEventType` (PROCESSING, SOURCES, REASONING, RESPONSE, FINAL_RESPONSE, ERROR, END) + - `data: Any` + +7. **`FormattedSource`** (TypedDict) + ```python + { + "metadata": { + "title": str, + "url": str, + "source_type": "documentation" | "web_search" + } + } + ``` + +--- + +## MCP Mode + +**Purpose:** Return raw documentation without LLM generation (for MCP tool integration) + +**Activation:** Header `x-mcp-mode: true` or `mcp: true` (line 388, 405, 423 in `app.py`) + +**Pipeline Changes:** +1. Skip `GenerationProgram`, use `McpGenerationProgram` instead (line 260-279 in `rag_pipeline.py`) +2. Return formatted documentation as markdown (line 300-331 in `generation_program.py`) +3. No reasoning/answer field in response + +**Format:** +```markdown +## 1. Document Title + +**Source:** Source Name +**URL:** https://... + +{document content} + +--- + +## 2. Next Document Title +... +``` + +--- + +## Optimized Programs + +**Location:** `python/optimizers/results/` + +**Purpose:** Pre-trained prompts optimized via DSPy optimizers (e.g., BootstrapFewShot) + +**Files:** +- `optimized_retrieval_program.json` - Query processor prompts +- `optimized_generation_cairo-coder.json` - Cairo Coder generation prompts +- `optimized_generation_starknet-agent.json` - Starknet Agent generation prompts + +**Loading:** Programs call `.load(path)` in `__init__()` (query_processor.py:89, generation_program.py:192-195) + +**Skipping:** Set `OPTIMIZER_RUN=1` env var to skip loading (for tests/optimizer runs) + +--- + +## Key File Locations + +| Component | File | Lines | +|-----------|------|-------| +| **API Server** | `python/src/cairo_coder/server/app.py` | 861 | +| **RAG Pipeline** | `python/src/cairo_coder/core/rag_pipeline.py` | 566 | +| **Query Processor** | `python/src/cairo_coder/dspy/query_processor.py` | 225 | +| **Document Retriever** | `python/src/cairo_coder/dspy/document_retriever.py` | 462 | +| **Generation Program** | `python/src/cairo_coder/dspy/generation_program.py` | 361 | +| **Retrieval Judge** | `python/src/cairo_coder/dspy/retrieval_judge.py` | 380 | +| **Grok Search** | `python/src/cairo_coder/dspy/grok_search.py` | 150 | +| **Agent Registry** | `python/src/cairo_coder/agents/registry.py` | 158 | +| **Agent Factory** | `python/src/cairo_coder/core/agent_factory.py` | 125 | +| **Core Types** | `python/src/cairo_coder/core/types.py` | 300+ | +| **Test Fixtures** | `python/tests/conftest.py` | 627 | +| **Pipeline Tests** | `python/tests/unit/test_rag_pipeline.py` | 30k | +| **Server Integration Tests** | `python/tests/integration/test_server_integration.py` | 31k | + +--- + +## Test Coverage Map + +### Unit Tests (Component-level) +| Test File | Component Tested | Key Tests | +|-----------|------------------|-----------| +| `test_query_processor.py` | QueryProcessorProgram | Query analysis, resource identification, search query extraction | +| `test_document_retriever.py` | DocumentRetrieverProgram, SourceFilteredPgVectorRM | Vector search, source filtering, template enhancement | +| `test_generation_program.py` | GenerationProgram, McpGenerationProgram | Code generation, streaming, MCP formatting | +| `test_retrieval_judge.py` | RetrievalJudge | Document scoring, threshold filtering | +| `test_rag_pipeline.py` | RagPipeline | Full pipeline orchestration, async/streaming, MCP mode | +| `test_agent_factory.py` | AgentFactory | Agent creation, caching, registry integration | +| `test_grok_integration.py` | GrokSearchProgram | Web search, citation extraction | +| `test_config.py` | Configuration | Env loading, defaults | +| `test_optimizers_loading.py` | Optimizer artifacts | JSON loading, error handling | + +### Integration Tests (End-to-end) +| Test File | Flow Tested | Key Tests | +|-----------|-------------|-----------| +| `test_server_integration.py` | FastAPI → Pipeline → Response | Streaming, multi-turn conversations, MCP mode, error handling, concurrent requests | +| `test_insights_api.py` | Analytics endpoints | Query stats, agent usage, source popularity | +| `test_config_integration.py` | Full config loading | Database connection, vector store setup | + +--- + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ FastAPI Server │ +│ (server/app.py) │ +│ POST /v1/agents/{id}/chat/completions │ +│ POST /v1/chat/completions │ +│ GET /v1/agents │ +└────────────────────┬────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────────┐ + │ AgentFactory │ + │ (core/agent_factory) │ + │ • get_or_create_agent │ + │ • cache agents │ + └────────┬───────────────┘ + │ + ▼ + ┌────────────────────────────┐ + │ AgentRegistry │ + │ (agents/registry) │ + │ • cairo-coder │ + │ • starknet-agent │ + └────────┬───────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────┐ +│ RagPipeline │ +│ (core/rag_pipeline) │ +│ │ +│ Stage 1: Query Processing │ +│ ┌────────────────────────────────┐ │ +│ │ QueryProcessorProgram │ │ +│ │ (dspy/query_processor) │ │ +│ │ • Analyze query │ │ +│ │ • Extract search queries │ │ +│ │ • Identify resources │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Stage 2: Document Retrieval │ +│ ┌────────────────────────────────┐ │ +│ │ DocumentRetrieverProgram │ │ +│ │ (dspy/document_retriever) │ │ +│ │ • Search vector store │ │ +│ │ • Filter by sources │ │ +│ │ • Deduplicate documents │ │ +│ │ • Enhance with templates │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Stage 2.5: Grok Augmentation (Optional) │ +│ ┌────────────────────────────────┐ │ +│ │ GrokSearchProgram │ │ +│ │ (dspy/grok_search) │ │ +│ │ • Fetch web/X posts │ │ +│ │ • Add virtual summary doc │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Stage 2.6: Retrieval Judging │ +│ ┌────────────────────────────────┐ │ +│ │ RetrievalJudge │ │ +│ │ (dspy/retrieval_judge) │ │ +│ │ • Score documents (0.0-1.0) │ │ +│ │ • Filter by threshold (0.4) │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Stage 3: Generation │ +│ ┌────────────────────────────────┐ │ +│ │ GenerationProgram │ │ +│ │ (dspy/generation_program) │ │ +│ │ • Chain of Thought reasoning │ │ +│ │ • Stream response chunks │ │ +│ │ OR │ │ +│ │ McpGenerationProgram │ │ +│ │ • Format raw documentation │ │ +│ └────────┬───────────────────────┘ │ +└───────────┼────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────┐ +│ Response Assembly │ +│ • PipelineResult with usage │ +│ • FormattedSource list │ +│ • Grok citations │ +└────────────────────────────────┘ +``` + +--- + +## Conventions Discovered + +### Naming +- **Files:** snake_case (`rag_pipeline.py`, `query_processor.py`) +- **Classes:** PascalCase (`RagPipeline`, `DocumentRetrieverProgram`) +- **Functions:** snake_case (`aforward`, `get_or_create_agent`) +- **Constants:** UPPER_SNAKE_CASE (`MAX_SOURCE_COUNT`, `SIMILARITY_THRESHOLD`) + +### DSPy Patterns +- **Signatures:** Define input/output schema with type hints and `InputField`/`OutputField` descriptors +- **Modules:** Wrap `dspy.Predict` or `dspy.ChainOfThought` with business logic +- **Async:** All forward methods have async variants (`aforward`, `acall`) +- **Streaming:** Use `dspy.streamify()` to wrap programs for streaming +- **Optimizers:** Load pre-trained prompts via `.load(json_path)` + +### Testing +- **Fixtures:** All shared fixtures in `tests/conftest.py` (enforced by CLAUDE.md) +- **Mocking:** Unit tests mock all external dependencies (LLM, vector DB) +- **Integration:** Use `TestClient` with dependency injection +- **Database:** Ephemeral PostgreSQL via testcontainers (Docker) +- **Auto-fixtures:** `optimizer_artifacts_optional`, `enable_dspy_track_usage`, `clean_config_env_vars` + +### Error Handling +- **Retry Logic:** `AdapterParseError` retries 3 times with fallback code extraction (generation_program.py:216-222) +- **Streaming Errors:** Emit `StreamEventType.ERROR` event (rag_pipeline.py:357-358) +- **API Errors:** Global exception handlers for `ValueError` (400) and `Exception` (500) (app.py:293-325) + +--- + +## Open Questions + +1. **Optimizer Artifacts:** How are optimized programs generated? What training data is used? +2. **Grok Search:** What is the Grok API key/endpoint? How often is it called? +3. **Retrieval Judge Threshold:** Why 0.4? Was this empirically tuned? +4. **Agent Caching:** Should caching strategy differ between agents? (Currently caches forever) +5. **Token Tracking:** Are token limits enforced? What happens on token overflow? +6. **Database Migrations:** How are schema changes handled for `UserInteraction` table? +7. **Embedding Model:** Gemini embeddings (3072 dims) - why this model? Cost/performance tradeoffs? + +--- + +## Summary Table: DSPy Modules + +| Module | File | Signature | Input | Output | Optimized? | +|--------|------|-----------|-------|--------|------------| +| QueryProcessorProgram | query_processor.py | CairoQueryAnalysis | query, chat_history | search_queries, resources | ✓ (optimized_retrieval_program.json) | +| DocumentRetrieverProgram | document_retriever.py | N/A (vector search) | processed_query, sources | documents | ✗ | +| RetrievalJudge | retrieval_judge.py | RetrievalRecallPrecision | query, system_resource | score, reason | ✗ | +| GrokSearchProgram | grok_search.py | N/A (API call) | query, chat_history | documents, citations | ✗ | +| GenerationProgram | generation_program.py | CairoCodeGeneration / StarknetEcosystemGeneration | query, context, chat_history | answer | ✓ (optimized_generation_{agent_id}.json) | +| McpGenerationProgram | generation_program.py | N/A (formatting) | documents | answer (formatted markdown) | ✗ | +| RagPipeline | rag_pipeline.py | N/A (orchestration) | query, chat_history, mcp_mode | answer, documents, usage | ✗ | + +--- + +**Report Complete.** All key abstractions, file locations, and test structures documented. diff --git a/.claude/tsc-cache/5d0c2956-0d3c-4ab8-a101-011a94d2ccaa/affected-repos.txt b/.claude/tsc-cache/5d0c2956-0d3c-4ab8-a101-011a94d2ccaa/affected-repos.txt new file mode 100644 index 00000000..d8649da3 --- /dev/null +++ b/.claude/tsc-cache/5d0c2956-0d3c-4ab8-a101-011a94d2ccaa/affected-repos.txt @@ -0,0 +1 @@ +root diff --git a/.smithers/.gitkeep b/.smithers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.smithers/workflow.db b/.smithers/workflow.db new file mode 100644 index 0000000000000000000000000000000000000000..e2a7b5fcb57cc6687bfa6e58d2149652c9693593 GIT binary patch literal 180224 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 literal 0 HcmV?d00001 diff --git a/.smithers/workflow.tsx b/.smithers/workflow.tsx new file mode 100644 index 00000000..9d977f8f --- /dev/null +++ b/.smithers/workflow.tsx @@ -0,0 +1,250 @@ +import { smithers, Workflow, Task, Ralph, ClaudeCodeAgent } from "smithers-orchestrator"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; +import React from "react"; + +const inputTable = sqliteTable("input", { + runId: text("run_id").primaryKey(), + specPath: text("spec_path").notNull(), +}); + +const planTable = sqliteTable( + "plan", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + taskName: text("task_name").notNull(), + research: text("research").notNull(), + implementationPrompt: text("implementation_prompt").notNull(), + filesToCreate: text("files_to_create", { mode: "json" }).$type(), + filesToModify: text("files_to_modify", { mode: "json" }).$type(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const implementTable = sqliteTable( + "implement", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + summary: text("summary").notNull(), + filesChanged: text("files_changed", { mode: "json" }).$type(), + testOutput: text("test_output").notNull(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const reviewTable = sqliteTable( + "review", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + lgtm: integer("lgtm", { mode: "boolean" }).notNull(), + review: text("review").notNull(), + issues: text("issues", { mode: "json" }).$type(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const fixTable = sqliteTable( + "fix", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + summary: text("summary").notNull(), + filesChanged: text("files_changed", { mode: "json" }).$type(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const outputTable = sqliteTable( + "output", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + totalTasks: integer("total_tasks").notNull(), + finalStatus: text("final_status").notNull(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId] }) }) +); + +export const schema = { + input: inputTable, + output: outputTable, + plan: planTable, + implement: implementTable, + review: reviewTable, + fix: fixTable, +}; + +export const db = drizzle(".smithers/workflow.db", { schema }); + +// Create tables +(db as any).$client.exec(` + CREATE TABLE IF NOT EXISTS input ( + run_id TEXT PRIMARY KEY, + spec_path TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS plan ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + task_name TEXT NOT NULL, research TEXT NOT NULL, implementation_prompt TEXT NOT NULL, + files_to_create TEXT, files_to_modify TEXT, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS implement ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + summary TEXT NOT NULL, files_changed TEXT, test_output TEXT NOT NULL, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS review ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + lgtm INTEGER NOT NULL, review TEXT NOT NULL, issues TEXT, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS fix ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + summary TEXT NOT NULL, files_changed TEXT, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS output ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, + total_tasks INTEGER NOT NULL, final_status TEXT NOT NULL, + PRIMARY KEY (run_id, node_id) + ); + CREATE TABLE IF NOT EXISTS state ( + key TEXT PRIMARY KEY, value TEXT NOT NULL, + updated_at TEXT DEFAULT (datetime('now')) + ); +`); + +function updateState(key: string, value: string) { + (db as any).$client.run( + "INSERT OR REPLACE INTO state (key, value, updated_at) VALUES (?, ?, datetime('now'))", + [key, value] + ); +} + +updateState("supervisor.status", "running"); +updateState("supervisor.summary", "Workflow initialized"); +updateState("supervisor.heartbeat", new Date().toISOString()); + +setInterval(() => { + try { + updateState("supervisor.heartbeat", new Date().toISOString()); + } catch (err) { + console.error("Failed to write heartbeat:", err); + } +}, 30000); + +const cliEnv = { ANTHROPIC_API_KEY: "" }; + +const plannerAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior software architect. Read the spec, examine the codebase, " + + "and pick the NEXT highest-priority task. Produce a detailed implementation prompt. " + + "Respond with ONLY a JSON object: " + + '{ "taskName": "string", "research": "string", "implementationPrompt": "string", ' + + '"filesToCreate": ["paths"], "filesToModify": ["paths"] }', +}); + +const implementAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior TypeScript engineer. Implement the task described below. " + + "After writing code, ALWAYS run tests to verify. " + + "Respond with ONLY a JSON object: " + + '{ "summary": "string", "filesChanged": ["paths"], "testOutput": "string" }', +}); + +const reviewAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior code reviewer. Run type checker and tests. " + + "Set lgtm=true ONLY if everything is correct. " + + "Respond with ONLY a JSON object: " + + '{ "lgtm": true/false, "review": "string", "issues": ["specific issues"] }', +}); + +const fixAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior TypeScript engineer fixing code review issues. " + + "After making changes, run tests and type checker. " + + "Respond with ONLY a JSON object: " + + '{ "summary": "string", "filesChanged": ["paths"] }', +}); + +type Phase = "plan" | "implement" | "review" | "fix"; + +function computePhase(plans: any[], impls: any[], reviews: any[], fixes: any[]): Phase { + if (plans.length === 0) return "plan"; + if (impls.length < plans.length) return "implement"; + if (reviews.length < plans.length + fixes.length) return "review"; + const latestReview = reviews[reviews.length - 1]; + if (latestReview?.lgtm) return "plan"; + if (fixes.length >= reviews.filter((r: any) => !r.lgtm).length) return "review"; + if (fixes.length >= 3) return "plan"; + return "fix"; +} + +export default smithers(db, (ctx) => { + const plans: any[] = ctx.outputs.plan ?? []; + const impls: any[] = ctx.outputs.implement ?? []; + const reviews: any[] = ctx.outputs.review ?? []; + const fixes: any[] = ctx.outputs.fix ?? []; + + const phase = computePhase(plans, impls, reviews, fixes); + const latestPlan = plans[plans.length - 1]; + const latestImpl = impls[impls.length - 1]; + const latestReview = reviews[reviews.length - 1]; + + const completedTasks = reviews + .filter((r: any) => r.lgtm) + .map((_r: any, i: number) => plans[i]?.taskName) + .filter(Boolean) + .join(", "); + + updateState("supervisor.status", "running"); + updateState("supervisor.summary", + "Phase: " + phase + " | Tasks done: " + reviews.filter((r: any) => r.lgtm).length); + + return ( + + + + {"Read the project spec at " + (ctx.input.specPath ?? "SPEC.md") + " and examine the codebase. Completed tasks: " + (completedTasks || "None yet") + ". Pick the NEXT task. Research what's needed. Write a detailed implementation prompt."} + + + + {"TASK: " + (latestPlan?.taskName ?? "unknown") + " -- " + (latestPlan?.implementationPrompt ?? "No implementation prompt.") + " Files to create: " + JSON.stringify(latestPlan?.filesToCreate ?? []) + " Files to modify: " + JSON.stringify(latestPlan?.filesToModify ?? []) + " After implementing, run tests and report results."} + + + + {"Review: " + (latestPlan?.taskName ?? "unknown") + " | Summary: " + (latestImpl?.summary ?? "No summary") + " | Files: " + JSON.stringify(latestImpl?.filesChanged ?? []) + " | Tests: " + (latestImpl?.testOutput ?? "No test output") + " -- Read ALL changed files. Run type checker and tests."} + + + + {"Fix review issues for: " + (latestPlan?.taskName ?? "unknown") + " Issues: " + (latestReview?.issues?.map((issue: string, i: number) => (i + 1) + ". " + issue).join(", ") ?? "None") + " Fix each issue. Run tests after."} + + + + + {{ totalTasks: reviews.filter((r: any) => r.lgtm).length, finalStatus: "done" }} + + + ); +}); + +process.on("beforeExit", () => { + try { + updateState("supervisor.status", "done"); + updateState("supervisor.summary", "Workflow completed successfully"); + } catch (err) { + console.error("Failed to update final state:", err); + } +}); diff --git a/.takopi-smithers/.gitkeep b/.takopi-smithers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.takopi-smithers/autoheal-prompt.txt b/.takopi-smithers/autoheal-prompt.txt new file mode 100644 index 00000000..878cf199 --- /dev/null +++ b/.takopi-smithers/autoheal-prompt.txt @@ -0,0 +1,303 @@ +# Auto-heal: Smithers Workflow Crash Recovery + +The Smithers workflow process crashed. Your job is to diagnose and fix the issue. + +## Crash Information +- Exit code: null +- Signal: SIGINT +- Restart attempts: 0 + +## Database State (from SQLite) +- Status: running +- Summary: Phase: plan | Tasks done: 0 +- Last error: N/A +- Heartbeat: 2026-02-16T11:40:07.735Z + +## Recent Logs (last 100 lines) +``` +[2026-02-16T11:40:17.286Z] [INFO] Capturing auto-heal context... + +== +gnal SIGINT +x +logic. +side another Claude Code session. +Nested sessions share runtime resources and will crash all active sessions. +To bypass this check, unset the CLAUDECODE environment variable. + + +``` + +## Current Workflow File (.smithers/workflow.tsx) +```tsx +import { smithers, Workflow, Task, Ralph, ClaudeCodeAgent } from "smithers-orchestrator"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; +import React from "react"; + +const inputTable = sqliteTable("input", { + runId: text("run_id").primaryKey(), + specPath: text("spec_path").notNull(), +}); + +const planTable = sqliteTable( + "plan", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + taskName: text("task_name").notNull(), + research: text("research").notNull(), + implementationPrompt: text("implementation_prompt").notNull(), + filesToCreate: text("files_to_create", { mode: "json" }).$type(), + filesToModify: text("files_to_modify", { mode: "json" }).$type(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const implementTable = sqliteTable( + "implement", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + summary: text("summary").notNull(), + filesChanged: text("files_changed", { mode: "json" }).$type(), + testOutput: text("test_output").notNull(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const reviewTable = sqliteTable( + "review", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + lgtm: integer("lgtm", { mode: "boolean" }).notNull(), + review: text("review").notNull(), + issues: text("issues", { mode: "json" }).$type(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const fixTable = sqliteTable( + "fix", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + iteration: integer("iteration").notNull().default(0), + summary: text("summary").notNull(), + filesChanged: text("files_changed", { mode: "json" }).$type(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) +); + +const outputTable = sqliteTable( + "output", + { + runId: text("run_id").notNull(), + nodeId: text("node_id").notNull(), + totalTasks: integer("total_tasks").notNull(), + finalStatus: text("final_status").notNull(), + }, + (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId] }) }) +); + +export const schema = { + input: inputTable, + output: outputTable, + plan: planTable, + implement: implementTable, + review: reviewTable, + fix: fixTable, +}; + +export const db = drizzle(".smithers/workflow.db", { schema }); + +// Create tables +(db as any).$client.exec(` + CREATE TABLE IF NOT EXISTS input ( + run_id TEXT PRIMARY KEY, + spec_path TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS plan ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + task_name TEXT NOT NULL, research TEXT NOT NULL, implementation_prompt TEXT NOT NULL, + files_to_create TEXT, files_to_modify TEXT, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS implement ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + summary TEXT NOT NULL, files_changed TEXT, test_output TEXT NOT NULL, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS review ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + lgtm INTEGER NOT NULL, review TEXT NOT NULL, issues TEXT, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS fix ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, + summary TEXT NOT NULL, files_changed TEXT, + PRIMARY KEY (run_id, node_id, iteration) + ); + CREATE TABLE IF NOT EXISTS output ( + run_id TEXT NOT NULL, node_id TEXT NOT NULL, + total_tasks INTEGER NOT NULL, final_status TEXT NOT NULL, + PRIMARY KEY (run_id, node_id) + ); + CREATE TABLE IF NOT EXISTS state ( + key TEXT PRIMARY KEY, value TEXT NOT NULL, + updated_at TEXT DEFAULT (datetime('now')) + ); +`); + +function updateState(key: string, value: string) { + (db as any).$client.run( + "INSERT OR REPLACE INTO state (key, value, updated_at) VALUES (?, ?, datetime('now'))", + [key, value] + ); +} + +updateState("supervisor.status", "running"); +updateState("supervisor.summary", "Workflow initialized"); +updateState("supervisor.heartbeat", new Date().toISOString()); + +setInterval(() => { + try { + updateState("supervisor.heartbeat", new Date().toISOString()); + } catch (err) { + console.error("Failed to write heartbeat:", err); + } +}, 30000); + +const cliEnv = { ANTHROPIC_API_KEY: "" }; + +const plannerAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior software architect. Read the spec, examine the codebase, " + + "and pick the NEXT highest-priority task. Produce a detailed implementation prompt. " + + "Respond with ONLY a JSON object: " + + '{ "taskName": "string", "research": "string", "implementationPrompt": "string", ' + + '"filesToCreate": ["paths"], "filesToModify": ["paths"] }', +}); + +const implementAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior TypeScript engineer. Implement the task described below. " + + "After writing code, ALWAYS run tests to verify. " + + "Respond with ONLY a JSON object: " + + '{ "summary": "string", "filesChanged": ["paths"], "testOutput": "string" }', +}); + +const reviewAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior code reviewer. Run type checker and tests. " + + "Set lgtm=true ONLY if everything is correct. " + + "Respond with ONLY a JSON object: " + + '{ "lgtm": true/false, "review": "string", "issues": ["specific issues"] }', +}); + +const fixAgent = new ClaudeCodeAgent({ + model: "sonnet", + env: cliEnv, + systemPrompt: "You are a senior TypeScript engineer fixing code review issues. " + + "After making changes, run tests and type checker. " + + "Respond with ONLY a JSON object: " + + '{ "summary": "string", "filesChanged": ["paths"] }', +}); + +type Phase = "plan" | "implement" | "review" | "fix"; + +function computePhase(plans: any[], impls: any[], reviews: any[], fixes: any[]): Phase { + if (plans.length === 0) return "plan"; + if (impls.length < plans.length) return "implement"; + if (reviews.length < plans.length + fixes.length) return "review"; + const latestReview = reviews[reviews.length - 1]; + if (latestReview?.lgtm) return "plan"; + if (fixes.length >= reviews.filter((r: any) => !r.lgtm).length) return "review"; + if (fixes.length >= 3) return "plan"; + return "fix"; +} + +export default smithers(db, (ctx) => { + const plans: any[] = ctx.outputs.plan ?? []; + const impls: any[] = ctx.outputs.implement ?? []; + const reviews: any[] = ctx.outputs.review ?? []; + const fixes: any[] = ctx.outputs.fix ?? []; + + const phase = computePhase(plans, impls, reviews, fixes); + const latestPlan = plans[plans.length - 1]; + const latestImpl = impls[impls.length - 1]; + const latestReview = reviews[reviews.length - 1]; + + const completedTasks = reviews + .filter((r: any) => r.lgtm) + .map((_r: any, i: number) => plans[i]?.taskName) + .filter(Boolean) + .join(", "); + + updateState("supervisor.status", "running"); + updateState("supervisor.summary", + "Phase: " + phase + " | Tasks done: " + reviews.filter((r: any) => r.lgtm).length); + + return ( + + + + {"Read the project spec at " + (ctx.input.specPath ?? "SPEC.md") + " and examine the codebase. Completed tasks: " + (completedTasks || "None yet") + ". Pick the NEXT task. Research what's needed. Write a detailed implementation prompt."} + + + + {"TASK: " + (latestPlan?.taskName ?? "unknown") + " -- " + (latestPlan?.implementationPrompt ?? "No implementation prompt.") + " Files to create: " + JSON.stringify(latestPlan?.filesToCreate ?? []) + " Files to modify: " + JSON.stringify(latestPlan?.filesToModify ?? []) + " After implementing, run tests and report results."} + + + + {"Review: " + (latestPlan?.taskName ?? "unknown") + " | Summary: " + (latestImpl?.summary ?? "No summary") + " | Files: " + JSON.stringify(latestImpl?.filesChanged ?? []) + " | Tests: " + (latestImpl?.testOutput ?? "No test output") + " -- Read ALL changed files. Run type checker and tests."} + + + + {"Fix review issues for: " + (latestPlan?.taskName ?? "unknown") + " Issues: " + (latestReview?.issues?.map((issue: string, i: number) => (i + 1) + ". " + issue).join(", ") ?? "None") + " Fix each issue. Run tests after."} + + + + + {{ totalTasks: reviews.filter((r: any) => r.lgtm).length, finalStatus: "done" }} + + + ); +}); + +process.on("beforeExit", () => { + try { + updateState("supervisor.status", "done"); + updateState("supervisor.summary", "Workflow completed successfully"); + } catch (err) { + console.error("Failed to update final state:", err); + } +}); + +``` + +## Your Task +1. Analyze the crash: look at logs, exit code, DB state, and workflow code +2. Identify the root cause (syntax error, runtime error, missing import, infinite loop, etc.) +3. Patch the workflow file to fix the issue + - Add error handling, timeouts, retries as needed + - Fix syntax/type errors + - Add graceful fallbacks +4. Ensure the workflow remains **resumable** (use Smithers SQLite persistence patterns) +5. Keep the plan simple and observable + +## Constraints +- Only edit .smithers/workflow.tsx +- Do NOT change the supervisor state key contract (supervisor.heartbeat, supervisor.status, supervisor.summary, supervisor.last_error) +- Do NOT break resumability +- Prefer robust, restart-friendly designs + +## Output +Edit the workflow file to fix the crash. The supervisor will automatically restart after you're done. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..fa92b539 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# takopi-smithers instructions for Codex + +Read and follow: TAKOPI_SMITHERS.md + +Key paths: +- .smithers/workflow.tsx +- .takopi-smithers/config.toml diff --git a/CLAUDE.md b/CLAUDE.md index 36adbfa3..45a5bad6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -184,3 +184,12 @@ See `docs/ARCHITECTURE.md` for detailed system design and diagrams. ## API Reference See `docs/API.md` for HTTP endpoint documentation. + + +# takopi-smithers configuration + +@TAKOPI_SMITHERS.md + +Additional notes: +- Workflow file: .smithers/workflow.tsx +- Supervisor config: .takopi-smithers/config.toml diff --git a/TAKOPI_SMITHERS.md b/TAKOPI_SMITHERS.md new file mode 100644 index 00000000..60402c97 --- /dev/null +++ b/TAKOPI_SMITHERS.md @@ -0,0 +1,40 @@ +# takopi-smithers operational rules + +You are the Takopi supervisor for this repository. + +Your job: +1) Maintain a long-running Smithers workflow in `.smithers/workflow.tsx` +2) Ensure it is resumable across restarts (use Smithers SQLite persistence) +3) Keep status visibility high via DB state keys used by the supervisor: + - supervisor.status: "idle" | "running" | "error" | "done" + - supervisor.summary: short human-readable summary (1–3 sentences) + - supervisor.last_error: most recent failure details +4) When users request changes: + - Prefer editing `.smithers/workflow.tsx` and/or `.takopi-smithers/config.toml` + - Expect the runtime to restart the workflow automatically when the file changes + +Smithers basics: +- Smithers is a React framework for orchestration. Plans are TSX. +- State is persisted in SQLite and survives restarts. +- You can resume incomplete executions via `db.execution.findIncomplete()`. + +Docs: +- Smithers intro: https://smithers.sh/introduction +- MCP/SQLite tool pattern: https://smithers.sh/guides/mcp-integration + +Takopi basics: +- Takopi runs your agent CLI in the repo and bridges to Telegram. +- Takopi config lives in ~/.takopi/takopi.toml +- Users can switch engines with /claude, /codex, /opencode, /pi + +When workflow crashes or hangs: +- Diagnose using: + - `.takopi-smithers/logs/*` + - `.smithers/workflow.tsx` + - SQLite state keys and execution history +- Patch the workflow to prevent recurrence (timeouts, retries, smaller steps) +- Keep the plan simple and observable (Phases/Steps/Tasks), and update supervisor.summary often. + +Output style: +- Be explicit about what changed and why. +- Prefer robust, restart-friendly designs over cleverness. diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..e96591bc --- /dev/null +++ b/bun.lock @@ -0,0 +1,351 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "dependencies": { + "@ai-sdk/anthropic": "^3.0.44", + "ai": "^6.0.86", + "smithers-orchestrator": "^0.6.0", + "takopi-smithers": "github:evmts/takopi-smithers", + "zod": "^4.3.6", + }, + }, + }, + "packages": { + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.44", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ke1NldgohWJ7sWLqm9Um9TVIOrtg8Y8AecWeB6PgaLt+paTPisAsyNfe8FNOVusuv58ugLBqY/78AkhUmbjXHA=="], + + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zH1UbNRjG5woOXXFOrVCZraqZuFTtmPvLardMGcgLkzpxKV0U3tAGoyWKSZ862H+eBJfI/Hf2yj/zzGJcCkycg=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], + + "@mdx-js/esbuild": ["@mdx-js/esbuild@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@types/unist": "^3.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" }, "peerDependencies": { "esbuild": ">=0.14.0" } }, "sha512-NS35VhTdvKNj5/B1JSD5W3kN1R0WDHgk+zCWq+tSChQw5L2Bgeiz7yyZPFrc5LWuPVOxE1xMbJr82bO9VVzmfQ=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ai": ["ai@6.0.86", "", { "dependencies": { "@ai-sdk/gateway": "3.0.46", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-U2W2LBCHA/pr0Ui7vmmsjBiLEzBbZF3yVHNy7Rbzn7IX+SvoQPFM5rN74hhfVzZoE8zBuGD4nLLk+j0elGacvQ=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "axios": ["axios@0.21.4", "", { "dependencies": { "follow-redirects": "^1.14.0" } }, "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], + + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + + "drizzle-zod": ["drizzle-zod@0.8.3", "", { "peerDependencies": { "drizzle-orm": ">=0.36.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-reconciler": ["react-reconciler@0.31.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "smithers": ["smithers@0.5.4", "", { "dependencies": { "axios": "^0.21.1" } }, "sha512-mGCYAcVTbkiZSCGorG6pznPncdVBdhc8Z2tA7J6P+Gl2Wk5vB38/gAEdsBNyp8DjDNuw8PdgCnH+Z3JzaqyEcA=="], + + "smithers-orchestrator": ["smithers-orchestrator@0.6.0", "", { "dependencies": { "@ai-sdk/anthropic": "^3.0.36", "@mdx-js/esbuild": "^3.1.1", "ai": "^6.0.69", "diff": "^5.2.0", "drizzle-orm": "^0.45.1", "drizzle-zod": "^0.8.3", "react": "^19.2.4", "react-dom": "^19.2.4", "react-reconciler": "^0.31.0", "zod": "^4.3.6" }, "peerDependencies": { "typescript": "^5" }, "bin": { "smithers": "src/cli/index.ts" } }, "sha512-u4Gc3AK2yYXWeS2hrijzXnLN/d4iewJE8zkOrcLSElDaZSyG27QcabcTAV24tlJAjmR/vkggsTKSEw76OyE0rA=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "takopi-smithers": ["takopi-smithers@github:evmts/takopi-smithers#c4dc163", { "dependencies": { "@babel/runtime": "^7.28.6", "@iarna/toml": "^2.2.5", "drizzle-orm": "^0.45.1", "react-dom": "^19.2.4", "smithers": "^0.5.4", "smithers-orchestrator": "^0.5.0", "zod": "^4.3.6" }, "peerDependencies": { "typescript": "^5" }, "bin": { "takopi-smithers": "./dist/cli.js" } }, "evmts-takopi-smithers-c4dc163"], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "react-reconciler/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + + "takopi-smithers/smithers-orchestrator": ["smithers-orchestrator@0.5.0", "", { "dependencies": { "@ai-sdk/anthropic": "^3.0.36", "@mdx-js/esbuild": "^3.1.1", "ai": "^6.0.69", "diff": "^5.2.0", "drizzle-orm": "^0.45.1", "drizzle-zod": "^0.8.3", "react": "^19.2.4", "react-dom": "^19.2.4", "react-reconciler": "^0.31.0", "zod": "^4.3.6" }, "peerDependencies": { "typescript": "^5" }, "bin": { "smithers": "src/cli/index.ts" } }, "sha512-/McdebpSRC/7ad8KOFcAiJerETwynYppXdNntBleeh8Nkrgv0kLg0kLehPdUVIxU/SKVe2r+x02G0BgTR0S5Lw=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..528c27eb --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "scripts": { + "postinstall": "cd node_modules/takopi-smithers && bun run build", + "smithers": "bun node_modules/takopi-smithers/dist/cli.js" + }, + "dependencies": { + "@ai-sdk/anthropic": "^3.0.44", + "ai": "^6.0.86", + "smithers-orchestrator": "^0.6.0", + "takopi-smithers": "github:evmts/takopi-smithers", + "zod": "^4.3.6" + } +} diff --git a/python/src/cairo_coder/core/rag_pipeline.py b/python/src/cairo_coder/core/rag_pipeline.py index f7232926..fc04ffda 100644 --- a/python/src/cairo_coder/core/rag_pipeline.py +++ b/python/src/cairo_coder/core/rag_pipeline.py @@ -32,7 +32,7 @@ title_from_url, ) from cairo_coder.dspy.document_retriever import DocumentRetrieverProgram -from cairo_coder.dspy.generation_program import GenerationProgram, McpGenerationProgram +from cairo_coder.dspy.generation_program import GenerationProgram, SkillGenerationProgram from cairo_coder.dspy.grok_search import GrokSearchProgram from cairo_coder.dspy.query_processor import QueryProcessorProgram from cairo_coder.dspy.retrieval_judge import RetrievalJudge @@ -51,7 +51,7 @@ class RagPipelineConfig: query_processor: QueryProcessorProgram document_retriever: DocumentRetrieverProgram generation_program: GenerationProgram - mcp_generation_program: McpGenerationProgram + mcp_generation_program: SkillGenerationProgram sources: list[DocumentSource] max_source_count: int = MAX_SOURCE_COUNT similarity_threshold: float = SIMILARITY_THRESHOLD @@ -179,12 +179,13 @@ async def aforward( ) if mcp_mode: - result = await self.mcp_generation_program.acall(documents) + context = self._prepare_context(documents) + result = await self.mcp_generation_program.acall(query=query, context=context) return dspy.Prediction( processed_query=processed_query, documents=documents, grok_citations=grok_citations, - answer=result.answer, + answer=result.skill, formatted_sources=self._format_sources(documents, grok_citations), ) @@ -258,23 +259,23 @@ async def _run_pipeline() -> None: final_answer: str | None = None if mcp_mode: - # MCP mode: Return raw documents await _emit( StreamEvent( type=StreamEventType.PROCESSING, - data="Formatting documentation...", + data="Generating skill document...", ) ) - - mcp_prediction = await self.mcp_generation_program.acall(documents) - final_answer = mcp_prediction.answer - # Emit single response plus a final response event for clients that rely on it + context = self._prepare_context(documents) + mcp_prediction = await self.mcp_generation_program.acall( + query=query, context=context + ) + final_answer = mcp_prediction.skill await _emit( - StreamEvent(type=StreamEventType.RESPONSE, data=mcp_prediction.answer) + StreamEvent(type=StreamEventType.RESPONSE, data=mcp_prediction.skill) ) await _emit( StreamEvent( - type=StreamEventType.FINAL_RESPONSE, data=mcp_prediction.answer + type=StreamEventType.FINAL_RESPONSE, data=mcp_prediction.skill ) ) else: @@ -515,7 +516,7 @@ def create_pipeline( sources: list[DocumentSource], query_processor: QueryProcessorProgram, generation_program: GenerationProgram, - mcp_generation_program: McpGenerationProgram, + mcp_generation_program: SkillGenerationProgram, document_retriever: DocumentRetrieverProgram | None = None, max_source_count: int = 5, similarity_threshold: float = SIMILARITY_THRESHOLD, @@ -529,7 +530,7 @@ def create_pipeline( vector_store: Vector store for document retrieval query_processor: Query processor generation_program: Generation program - mcp_generation_program: "Generation" program to use if in MCP mode + mcp_generation_program: Skill generation program for MCP mode document_retriever: Optional document retriever (creates default if None) max_source_count: Maximum documents to retrieve similarity_threshold: Minimum similarity for document inclusion diff --git a/python/src/cairo_coder/dspy/__init__.py b/python/src/cairo_coder/dspy/__init__.py index a8f39e77..cf5cd56c 100644 --- a/python/src/cairo_coder/dspy/__init__.py +++ b/python/src/cairo_coder/dspy/__init__.py @@ -11,7 +11,8 @@ from .document_retriever import DocumentRetrieverProgram from .generation_program import ( GenerationProgram, - McpGenerationProgram, + SkillGeneration, + SkillGenerationProgram, create_generation_program, create_mcp_generation_program, ) @@ -25,7 +26,8 @@ "create_query_processor", "DocumentRetrieverProgram", "GenerationProgram", - "McpGenerationProgram", + "SkillGeneration", + "SkillGenerationProgram", "create_generation_program", "create_mcp_generation_program", "RetrievalJudge", diff --git a/python/src/cairo_coder/dspy/generation_program.py b/python/src/cairo_coder/dspy/generation_program.py index 4da1f12e..a4b0a663 100644 --- a/python/src/cairo_coder/dspy/generation_program.py +++ b/python/src/cairo_coder/dspy/generation_program.py @@ -15,7 +15,7 @@ from dspy.adapters.chat_adapter import AdapterParseError from langsmith import traceable -from cairo_coder.core.types import Document, Message +from cairo_coder.core.types import Message logger = structlog.get_logger(__name__) @@ -152,7 +152,18 @@ class StarknetEcosystemGeneration(Signature): ) +class SkillGeneration(Signature): + """ + Synthesize retrieved documentation into a SKILL.md file for Claude Code. + """ + query: str = InputField(desc="Original user request the skill should address") + context: str = InputField( + desc="Retrieved documentation context used to build the skill instructions" + ) + skill: str = OutputField( + desc="A complete SKILL.md file with YAML frontmatter (name, description) and actionable markdown instructions" + ) class GenerationProgram(dspy.Module): """ @@ -286,55 +297,26 @@ def _try_extract_code_from_response(self, response: str) -> str | None: return None -class McpGenerationProgram(dspy.Module): +class SkillGenerationProgram(dspy.Module): """ - Special generation program for MCP (Model Context Protocol) mode. - - This program returns raw documentation without LLM generation, - useful for integration with other tools that need Cairo documentation. + DSPy module for generating SKILL.md content from query + retrieved context. """ - def __init__(self): + def __init__(self) -> None: super().__init__() + self.generation_program = dspy.ChainOfThought(SkillGeneration) - def forward(self, documents: list[Document]) -> dspy.Prediction: + def forward(self, query: str, context: str) -> dspy.Prediction: """ - Format documents for MCP mode response. - - Args: - documents: List of retrieved documents - - Returns: - Formatted documentation string + Generate a skill document synchronously. """ - if not documents: - return dspy.Prediction(answer="No relevant documentation found.") - - formatted_docs = [] - for i, doc in enumerate(documents, 1): - source = doc.source - url = doc.source_link - title = doc.title - - formatted_doc = f""" -## {i}. {title} - -**Source:** {source} -**URL:** {url} - -{doc.page_content} + return self.generation_program(query=query, context=context) ---- -""" - formatted_docs.append(formatted_doc) - - return dspy.Prediction(answer="\n".join(formatted_docs)) - - async def aforward(self, documents: list[Document]) -> dspy.Prediction: + async def aforward(self, query: str, context: str) -> dspy.Prediction: """ - Format documents for MCP mode response. + Generate a skill document asynchronously. """ - return self(documents) + return await self.generation_program.acall(query=query, context=context) def create_generation_program(program_type: str) -> GenerationProgram: @@ -350,11 +332,10 @@ def create_generation_program(program_type: str) -> GenerationProgram: return GenerationProgram(program_type=program_type) -def create_mcp_generation_program() -> McpGenerationProgram: - """ - Factory function to create an MCP GenerationProgram instance. +def create_mcp_generation_program() -> SkillGenerationProgram: + """Factory function to create an MCP-mode generation program. Returns: - Configured McpGenerationProgram instance + Configured SkillGenerationProgram instance """ - return McpGenerationProgram() + return SkillGenerationProgram() diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 1d88fea3..245b5acd 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -26,7 +26,7 @@ StreamEventType, ) from cairo_coder.dspy.document_retriever import DocumentRetrieverProgram, SourceFilteredPgVectorRM -from cairo_coder.dspy.generation_program import GenerationProgram, McpGenerationProgram +from cairo_coder.dspy.generation_program import GenerationProgram, SkillGenerationProgram from cairo_coder.dspy.query_processor import QueryProcessorProgram from cairo_coder.dspy.retrieval_judge import RetrievalJudge from cairo_coder.server.app import CairoCoderServer, get_agent_factory, get_vector_db @@ -485,27 +485,19 @@ async def mock_streaming(*args, **kwargs): @pytest.fixture def mock_mcp_generation_program(): - """Create a mock McpGenerationProgram.""" - program = Mock(spec=McpGenerationProgram) - mcp_answer = """ -## 1. Cairo Contracts - -**Source:** Cairo Book -**URL:** https://book.cairo-lang.org/contracts - -Cairo contracts are defined using #[starknet::contract]. - + """Create a mock SkillGenerationProgram for MCP mode.""" + program = Mock(spec=SkillGenerationProgram) + skill_output = """--- +name: cairo-contracts-guide +description: Guide for writing Cairo smart contracts with best practices. --- -## 2. Storage Variables - -**Source:** Starknet Documentation -**URL:** https://docs.starknet.io/storage +# Cairo Contracts +Cairo contracts are defined using #[starknet::contract]. Storage variables use #[storage] attribute. """ - # Create prediction with usage tracking - prediction = dspy.Prediction(answer=mcp_answer) + prediction = dspy.Prediction(skill=skill_output) prediction.set_lm_usage({}) program.aforward = AsyncMock(return_value=prediction) diff --git a/python/tests/integration/conftest.py b/python/tests/integration/conftest.py index 7260fbca..6071a674 100644 --- a/python/tests/integration/conftest.py +++ b/python/tests/integration/conftest.py @@ -119,13 +119,13 @@ async def _fake_gen_aforward_streaming(query: str, context: str, chat_history: s pipeline.generation_program.acall = AsyncMock(side_effect=_fake_gen_acall) pipeline.generation_program.aforward_streaming =_fake_gen_aforward_streaming - # Patch MCP generation to a deterministic simple string as tests expect - mcp_prediction = _dspy.Prediction(answer="Cairo is a programming language") + # Patch MCP generation to deterministic skill output + mcp_prediction = _dspy.Prediction(skill="Cairo is a programming language") mcp_prediction.set_lm_usage({}) pipeline.mcp_generation_program.acall = AsyncMock(return_value=mcp_prediction) - def _mcp_forward(documents): # noqa: ARG001 - deterministic response - prediction = _dspy.Prediction(answer="Cairo is a programming language") + def _mcp_forward(query: str, context: str): # noqa: ARG001 - deterministic response + prediction = _dspy.Prediction(skill="Cairo is a programming language") prediction.set_lm_usage({}) return prediction diff --git a/python/tests/unit/test_generation_program.py b/python/tests/unit/test_generation_program.py index 7dd58f22..d80db12e 100644 --- a/python/tests/unit/test_generation_program.py +++ b/python/tests/unit/test_generation_program.py @@ -12,12 +12,13 @@ from dspy.adapters.chat_adapter import AdapterParseError from cairo_coder.agents.registry import AgentId -from cairo_coder.core.types import Document, Message, Role +from cairo_coder.core.types import Message, Role from cairo_coder.dspy.generation_program import ( CairoCodeGeneration, GenerationProgram, - McpGenerationProgram, ScarbGeneration, + SkillGeneration, + SkillGenerationProgram, create_generation_program, create_mcp_generation_program, ) @@ -36,11 +37,6 @@ def starknet_generation_program(self, mock_lm): """Create a Starknet-specific GenerationProgram instance.""" return GenerationProgram(program_type=AgentId.STARKNET) - @pytest.fixture - def mcp_generation_program(self): - """Create an MCP GenerationProgram instance.""" - return McpGenerationProgram() - @pytest.mark.asyncio async def test_general_code_generation(self, generation_program): """Test general Cairo code generation for both sync and async.""" @@ -131,48 +127,52 @@ def test_format_empty_chat_history(self, generation_program): assert formatted == "" -class TestMcpGenerationProgram: - """Test suite for McpGenerationProgram.""" +class TestSkillGenerationProgram: + """Test suite for SkillGenerationProgram.""" @pytest.fixture - def mcp_program(self): - """Create an MCP GenerationProgram instance.""" - return McpGenerationProgram() + def skill_program(self, mock_lm): + """Create a SkillGenerationProgram instance.""" + return SkillGenerationProgram() @pytest.mark.asyncio - async def test_mcp_document_formatting(self, mcp_program, sample_documents): - """Test MCP mode document formatting.""" - answer = (await mcp_program.acall(sample_documents)).answer - - assert isinstance(answer, str) - assert len(answer) > 0 - - # Verify document structure is present - for i, doc in enumerate(sample_documents, 1): - assert f"## {i}." in answer - - # Check URL - assert f"**URL:** {doc.source_link}" in answer - - # Check content is included - assert doc.page_content in answer + async def test_skill_generation_program(self, skill_program): + """Test async SKILL.md generation.""" + skill_output = """--- +name: cairo-storage-patterns +description: Build Cairo storage modules with clear access patterns. +--- + +Use storage structs with dedicated read/write functions. +""" + with patch.object(skill_program, "generation_program") as mock_program: + mock_program.acall = AsyncMock(return_value=dspy.Prediction(skill=skill_output)) + result = await skill_program.acall( + query="Create a skill for Cairo storage patterns", + context="Storage variables use #[storage] and should have explicit accessors.", + ) - @pytest.mark.asyncio - async def test_mcp_empty_documents(self, mcp_program): - """Test MCP mode with empty documents.""" - result = await mcp_program.acall([]) + assert result.skill == skill_output + assert result.skill.startswith("---\nname:") + assert "description:" in result.skill + assert "\n---\n" in result.skill + mock_program.acall.assert_called_once() - assert result.answer == "No relevant documentation found." + def test_skill_generation_signature_fields(self): + """Test that the signature has the expected fields.""" + signature = SkillGeneration - @pytest.mark.asyncio - async def test_mcp_documents_with_missing_metadata(self, mcp_program): - """Test MCP mode with documents missing metadata.""" - documents = [Document(page_content="Some Cairo content", metadata={})] # Missing metadata + assert "query" in signature.model_fields + assert "context" in signature.model_fields + assert "skill" in signature.model_fields - answer = (await mcp_program.acall(documents)).answer + query_field = signature.model_fields["query"] + context_field = signature.model_fields["context"] + skill_field = signature.model_fields["skill"] - # 1. title (empty) 2. source (empty) 3. url (empty) 4. title (empty) 5. content - assert answer == """\n## 1. Some Cairo content\n\n**Source:** None\n**URL:** None\n\nSome Cairo content\n\n---\n""" + assert query_field.json_schema_extra["__dspy_field_type"] == "input" + assert context_field.json_schema_extra["__dspy_field_type"] == "input" + assert skill_field.json_schema_extra["__dspy_field_type"] == "output" class TestCairoCodeGeneration: @@ -264,7 +264,7 @@ def test_create_generation_program(self): def test_create_mcp_generation_program(self): """Test the MCP generation program factory function.""" program = create_mcp_generation_program() - assert isinstance(program, McpGenerationProgram) + assert isinstance(program, SkillGenerationProgram) class TestForwardRetries: diff --git a/python/tests/unit/test_rag_pipeline.py b/python/tests/unit/test_rag_pipeline.py index 81e1f41b..94ed5ca3 100644 --- a/python/tests/unit/test_rag_pipeline.py +++ b/python/tests/unit/test_rag_pipeline.py @@ -112,7 +112,8 @@ async def test_mcp_mode_execution(self, pipeline): # Verify MCP program was used pipeline.mcp_generation_program.acall.assert_called_once() - assert "Cairo contracts are defined using #[starknet::contract]" in result.answer + assert "---" in result.answer + assert "name:" in result.answer @pytest.mark.asyncio async def test_pipeline_with_chat_history(self, pipeline): From 7bd3f5cee9970f91f704f6d614aab6be6c8466d9 Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:29:42 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=A7=AA=20test(dspy):=20add=20missing?= =?UTF-8?q?=20skill=20generation=20and=20mcp=20streaming=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/tests/unit/test_generation_program.py | 24 ++++++++++++++ python/tests/unit/test_rag_pipeline.py | 34 ++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/python/tests/unit/test_generation_program.py b/python/tests/unit/test_generation_program.py index d80db12e..a551c67a 100644 --- a/python/tests/unit/test_generation_program.py +++ b/python/tests/unit/test_generation_program.py @@ -174,6 +174,30 @@ def test_skill_generation_signature_fields(self): assert context_field.json_schema_extra["__dspy_field_type"] == "input" assert skill_field.json_schema_extra["__dspy_field_type"] == "output" + @pytest.mark.asyncio + async def test_skill_generation_empty_context(self, skill_program): + """Test SKILL.md generation when no documents are retrieved.""" + empty_context = "No relevant documentation found." + skill_output = """--- +name: cairo-empty-context-skill +description: Handle empty retrieval context for skill generation. +--- + +Explain that no relevant docs were found and provide general guidance. +""" + with patch.object(skill_program, "generation_program") as mock_program: + mock_program.acall = AsyncMock(return_value=dspy.Prediction(skill=skill_output)) + result = await skill_program.acall( + query="Generate a Cairo skill from available context", + context=empty_context, + ) + + assert result.skill == skill_output + mock_program.acall.assert_called_once_with( + query="Generate a Cairo skill from available context", + context=empty_context, + ) + class TestCairoCodeGeneration: """Test suite for CairoCodeGeneration signature.""" diff --git a/python/tests/unit/test_rag_pipeline.py b/python/tests/unit/test_rag_pipeline.py index 94ed5ca3..d8f0b1ca 100644 --- a/python/tests/unit/test_rag_pipeline.py +++ b/python/tests/unit/test_rag_pipeline.py @@ -18,6 +18,7 @@ Document, DocumentSource, Message, + PipelineResult, Role, StreamEventType, ) @@ -115,6 +116,39 @@ async def test_mcp_mode_execution(self, pipeline): assert "---" in result.answer assert "name:" in result.answer + @pytest.mark.asyncio + async def test_mcp_mode_streaming_execution(self, pipeline): + """Test MCP mode pipeline execution with streaming.""" + events = [] + async for event in pipeline.aforward_streaming( + "How to write Cairo contracts?", mcp_mode=True + ): + events.append(event) + + event_types = [event.type for event in events] + assert StreamEventType.PROCESSING in event_types + assert StreamEventType.SOURCES in event_types + assert StreamEventType.RESPONSE in event_types + assert StreamEventType.FINAL_RESPONSE in event_types + assert StreamEventType.END in event_types + + response_event = next(e for e in events if e.type == StreamEventType.RESPONSE) + final_response_event = next( + e for e in events if e.type == StreamEventType.FINAL_RESPONSE + ) + end_event = next(e for e in events if e.type == StreamEventType.END) + + assert isinstance(response_event.data, str) + assert isinstance(final_response_event.data, str) + assert "---" in response_event.data + assert "name:" in response_event.data + assert "---" in final_response_event.data + assert "name:" in final_response_event.data + assert isinstance(end_event.data, PipelineResult) + + pipeline.mcp_generation_program.acall.assert_called_once() + pipeline.generation_program.acall.assert_not_called() + @pytest.mark.asyncio async def test_pipeline_with_chat_history(self, pipeline): """Test pipeline with chat history.""" From 43289a5d57036159d39cd16cc3eaeb815ca1f797 Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:40:58 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=90=9B=20fix(docs):=20demote=20takopi?= =?UTF-8?q?-smithers=20section=20heading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 45a5bad6..7897b777 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -185,11 +185,11 @@ See `docs/ARCHITECTURE.md` for detailed system design and diagrams. See `docs/API.md` for HTTP endpoint documentation. - -# takopi-smithers configuration +## takopi-smithers configuration @TAKOPI_SMITHERS.md Additional notes: + - Workflow file: .smithers/workflow.tsx - Supervisor config: .takopi-smithers/config.toml From b6a187ea570c30c451463474cca37f29436497bb Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:41:02 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=90=9B=20fix(docs):=20wrap=20smithers?= =?UTF-8?q?=20URLs=20in=20markdown=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TAKOPI_SMITHERS.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/TAKOPI_SMITHERS.md b/TAKOPI_SMITHERS.md index 60402c97..4af0ac5b 100644 --- a/TAKOPI_SMITHERS.md +++ b/TAKOPI_SMITHERS.md @@ -3,31 +3,36 @@ You are the Takopi supervisor for this repository. Your job: -1) Maintain a long-running Smithers workflow in `.smithers/workflow.tsx` -2) Ensure it is resumable across restarts (use Smithers SQLite persistence) -3) Keep status visibility high via DB state keys used by the supervisor: + +1. Maintain a long-running Smithers workflow in `.smithers/workflow.tsx` +2. Ensure it is resumable across restarts (use Smithers SQLite persistence) +3. Keep status visibility high via DB state keys used by the supervisor: - supervisor.status: "idle" | "running" | "error" | "done" - supervisor.summary: short human-readable summary (1–3 sentences) - supervisor.last_error: most recent failure details -4) When users request changes: +4. When users request changes: - Prefer editing `.smithers/workflow.tsx` and/or `.takopi-smithers/config.toml` - Expect the runtime to restart the workflow automatically when the file changes Smithers basics: + - Smithers is a React framework for orchestration. Plans are TSX. - State is persisted in SQLite and survives restarts. - You can resume incomplete executions via `db.execution.findIncomplete()`. Docs: -- Smithers intro: https://smithers.sh/introduction -- MCP/SQLite tool pattern: https://smithers.sh/guides/mcp-integration + +- Smithers intro: [https://smithers.sh/introduction](https://smithers.sh/introduction) +- MCP/SQLite tool pattern: [https://smithers.sh/guides/mcp-integration](https://smithers.sh/guides/mcp-integration) Takopi basics: + - Takopi runs your agent CLI in the repo and bridges to Telegram. - Takopi config lives in ~/.takopi/takopi.toml - Users can switch engines with /claude, /codex, /opencode, /pi When workflow crashes or hangs: + - Diagnose using: - `.takopi-smithers/logs/*` - `.smithers/workflow.tsx` @@ -36,5 +41,6 @@ When workflow crashes or hangs: - Keep the plan simple and observable (Phases/Steps/Tasks), and update supervisor.summary often. Output style: + - Be explicit about what changed and why. - Prefer robust, restart-friendly designs over cleverness. From 6b2eb26ae6c96414bd3bef5610f872acdc2543dc Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:41:05 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=90=9B=20fix(prompts):=20replace=20em?= =?UTF-8?q?phasized=20task=20titles=20with=20headings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .smithers/prompts/fix.mdx | 41 +++++++++++++++++++++++++ .smithers/prompts/implement.mdx | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 .smithers/prompts/fix.mdx create mode 100644 .smithers/prompts/implement.mdx diff --git a/.smithers/prompts/fix.mdx b/.smithers/prompts/fix.mdx new file mode 100644 index 00000000..9d27c573 --- /dev/null +++ b/.smithers/prompts/fix.mdx @@ -0,0 +1,41 @@ +# Fix Review Issues — Pass {props.pass} + +You are a senior Python engineer fixing code review issues in Cairo Coder. + +## Task + +### {props.taskName} + +## Issues to Fix + +{props.issues?.map((issue, i) => `${i + 1}. ${issue}`).join("\n") ?? "No issues listed"} + +{props.reviewFeedback ? `## Reviewer Notes\n${props.reviewFeedback}` : ""} + +## Instructions + +1. Read CLAUDE.md for project conventions +2. Fix each issue listed above +3. Do NOT introduce new features — only fix the reported issues + +## Verification + +After fixing, run: + +```bash +cd python && uv run pytest -v +trunk check --fix +``` + +## GIT COMMIT RULES + +- Make atomic commits — one fix per commit +- Format: `🐛 fix(scope): what was fixed` +- Examples: + - `🐛 fix(dspy): add missing type hint on SkillGenerationProgram.forward()` + - `🐛 fix(pipeline): correct skill field name in MCP branch` +- `git add` the specific files changed, then `git commit` + +## REQUIRED OUTPUT + +{props.schema} diff --git a/.smithers/prompts/implement.mdx b/.smithers/prompts/implement.mdx new file mode 100644 index 00000000..8043e7d3 --- /dev/null +++ b/.smithers/prompts/implement.mdx @@ -0,0 +1,54 @@ +# Implement — Pass {props.pass} + +You are a senior Python engineer implementing a change in Cairo Coder. + +## Task + +### {props.taskName} + +{props.implementationPrompt} + +{props.previousWork +? `## Previous Work\nPrevious implementation did: ${props.previousWork.summary}\nNext smallest unit: ${props.previousWork.nextSmallestUnit}` +: ""} + +{props.reviewFixes ? `## Review Fixes Applied\n${props.reviewFixes}` : ""} + +## Files + +- **Create:** {JSON.stringify(props.filesToCreate ?? [])} +- **Modify:** {JSON.stringify(props.filesToModify ?? [])} + +## Instructions + +1. Read CLAUDE.md for project conventions +2. Read the existing files before modifying them +3. Implement the SMALLEST atomic unit described above +4. Follow DSPy patterns: Signatures → Modules → Programs +5. Use type hints everywhere (enforced by mypy) +6. Use `structlog` for logging: `get_logger(__name__)` + +## Verification + +After implementing, run: + +```bash +cd python && uv run pytest -v +trunk check --fix +``` + +## GIT COMMIT RULES + +- Make atomic commits — one logical change per commit +- Commit EACH smallest unit of work separately, do NOT batch +- Use emoji prefixes: 🐛 fix, ♻️ refactor, 🧪 test, ⚡ perf, ✨ feat +- Format: `EMOJI type(scope): description` +- Examples: + - `✨ feat(dspy): add SkillGeneration signature` + - `🧪 test(dspy): add unit tests for SkillGenerationProgram` + - `♻️ refactor(pipeline): replace McpGenerationProgram with SkillGenerationProgram` +- `git add` the specific files changed, then `git commit` with the emoji message + +## REQUIRED OUTPUT + +{props.schema} From 102ce14149afdf18cfd4e9963c8f903313f2595b Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:41:14 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=90=9B=20fix(trunk):=20ignore=20ephem?= =?UTF-8?q?eral=20claude=20cache=20artifacts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trunk/trunk.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 9641a353..016ac643 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -20,6 +20,9 @@ lint: - linters: [ALL] paths: - python/src/cairo_coder_tools/ingestion/generated + - linters: [ALL] + paths: + - .claude/cache enabled: - ruff@0.12.3 - actionlint@1.7.7 From 02e5da152c236738a776feff6d55fe9347150b01 Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:41:20 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=90=9B=20fix(security-scan):=20skip?= =?UTF-8?q?=20osv-scanner=20on=20root=20bun=20lockfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trunk/trunk.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 016ac643..082fda1a 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -23,6 +23,9 @@ lint: - linters: [ALL] paths: - .claude/cache + - linters: [osv-scanner] + paths: + - bun.lock enabled: - ruff@0.12.3 - actionlint@1.7.7 From dcd7bfdb03da45b14a11811ee9013065503df30c Mon Sep 17 00:00:00 2001 From: enitrat Date: Mon, 16 Feb 2026 13:49:42 +0000 Subject: [PATCH 8/8] dev: setup smithers workflow --- .gitignore | 3 + .smithers/agents.ts | 32 ++ .smithers/config.ts | 8 + .smithers/preload.ts | 2 + .smithers/prompts/final-review.mdx | 38 ++ .smithers/prompts/plan.mdx | 32 ++ .smithers/prompts/review.mdx | 44 +++ .smithers/schemas.ts | 55 +++ .smithers/workflow.db | Bin 180224 -> 4096 bytes .smithers/workflow.db-shm | Bin 0 -> 32768 bytes .smithers/workflow.db-wal | Bin 0 -> 407912 bytes .smithers/workflow.tsx | 424 ++++++++++----------- .takopi-smithers/autoheal-prompt.txt | 443 +++++++++++----------- .takopi-smithers/supervisor.pid | 1 + AGENTS.md | 1 + SPEC.md | 108 ++++++ bun.lock | 3 + bunfig.toml | 1 + package.json | 3 + patches/smithers-orchestrator@0.6.0.patch | 27 ++ 20 files changed, 795 insertions(+), 430 deletions(-) create mode 100644 .smithers/agents.ts create mode 100644 .smithers/config.ts create mode 100644 .smithers/preload.ts create mode 100644 .smithers/prompts/final-review.mdx create mode 100644 .smithers/prompts/plan.mdx create mode 100644 .smithers/prompts/review.mdx create mode 100644 .smithers/schemas.ts create mode 100644 .smithers/workflow.db-shm create mode 100644 .smithers/workflow.db-wal create mode 100644 .takopi-smithers/supervisor.pid create mode 100644 SPEC.md create mode 100644 bunfig.toml create mode 100644 patches/smithers-orchestrator@0.6.0.patch diff --git a/.gitignore b/.gitignore index 9df30291..25e1ecb9 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ fixtures/runner_crate/target .snfoundry_cache python/starklings_results/ + + +smithers.db* diff --git a/.smithers/agents.ts b/.smithers/agents.ts new file mode 100644 index 00000000..1cb89cb7 --- /dev/null +++ b/.smithers/agents.ts @@ -0,0 +1,32 @@ +import { ClaudeCodeAgent, CodexAgent } from "smithers-orchestrator"; +import { REPO_ROOT } from "./config"; + +const SYSTEM_PROMPT_BASE = `You are working on Cairo Coder, a RAG-based Cairo code generation service. +Stack: Python/FastAPI backend, DSPy for LLM orchestration, PostgreSQL/pgvector. +Package manager: uv (NEVER pip/poetry). +Read CLAUDE.md for full project conventions before making any changes. + +CRITICAL OUTPUT REQUIREMENT: +When you have completed your work, you MUST end your response with a JSON object +wrapped in a code fence. The JSON format is specified in your task prompt.`; + +export const planner = new ClaudeCodeAgent({ + model: "opus", + systemPrompt: SYSTEM_PROMPT_BASE, + cwd: REPO_ROOT, + permissionMode: "default", +}); + +export const implementer = new CodexAgent({ + model: "gpt-5.3-codex", + dangerouslyBypassApprovalsAndSandbox: true, + systemPrompt: SYSTEM_PROMPT_BASE, + cwd: REPO_ROOT, +}); + +export const reviewer = new ClaudeCodeAgent({ + model: "opus", + systemPrompt: SYSTEM_PROMPT_BASE, + cwd: REPO_ROOT, + permissionMode: "default", +}); diff --git a/.smithers/config.ts b/.smithers/config.ts new file mode 100644 index 00000000..dec2cc7f --- /dev/null +++ b/.smithers/config.ts @@ -0,0 +1,8 @@ +export const MAX_PASSES = 5; + +export const REPO_ROOT = process.cwd(); + +export const VERIFICATION_COMMANDS = { + test: "cd python && uv run pytest -v", + lint: "trunk check --fix", +} as const; diff --git a/.smithers/preload.ts b/.smithers/preload.ts new file mode 100644 index 00000000..358e9cbf --- /dev/null +++ b/.smithers/preload.ts @@ -0,0 +1,2 @@ +import { mdxPlugin } from "smithers-orchestrator/mdx-plugin"; +mdxPlugin(); diff --git a/.smithers/prompts/final-review.mdx b/.smithers/prompts/final-review.mdx new file mode 100644 index 00000000..242dfd26 --- /dev/null +++ b/.smithers/prompts/final-review.mdx @@ -0,0 +1,38 @@ +# Final Review — STRICT GATE — Pass {props.pass} + +You are the final quality gate. REFUSE to approve unless ALL criteria are met. + +## Completed Tasks + +{props.completedTasks || "None"} + +## Latest Review + +{props.latestReview ?? "No review yet"} + +## Verification + +Run ALL checks: + +```bash +cd python && uv run pytest -v +trunk check --fix +``` + +## Criteria — ALL must pass + +- [ ] ALL tests pass (`uv run pytest` exits 0) +- [ ] ALL lint/type checks pass (`trunk check` exits 0) +- [ ] Implementation matches SPEC.md requirements +- [ ] No dead code left behind (old McpGenerationProgram removed if replaced) +- [ ] New code has unit tests +- [ ] DSPy patterns followed correctly +- [ ] No hardcoded paths or credentials + +Set `readyToMoveOn: true` ONLY if you genuinely cannot find ANYTHING to improve. + +If `readyToMoveOn: false`, explain exactly what must be fixed — this feeds into the next pass's implement step. + +## REQUIRED OUTPUT + +{props.schema} diff --git a/.smithers/prompts/plan.mdx b/.smithers/prompts/plan.mdx new file mode 100644 index 00000000..fb9b7a10 --- /dev/null +++ b/.smithers/prompts/plan.mdx @@ -0,0 +1,32 @@ +# Plan — Pass {props.pass} + +You are a senior Python/DSPy architect. Read the spec and examine the codebase to plan the next atomic unit of work. + +## Context + +- **Spec file:** {props.specPath} +- **Completed tasks:** {props.completedTasks || "None yet"} +- **Codebase areas:** `python/src/cairo_coder/dspy/`, `python/src/cairo_coder/core/`, `python/tests/` + +{props.previousFeedback ? `## Feedback from Previous Pass\n${props.previousFeedback}` : ""} + +## Instructions + +1. Read CLAUDE.md for project conventions +2. Read the spec at {props.specPath} +3. Examine the codebase — especially the files mentioned in the spec +4. Identify the NEXT smallest atomic unit of work (one signature, one module, one test file). Balance properly so as not to pick tasks too small or too large: organize logically for reviewers. +5. Research what exists and what needs to change +6. Write a detailed implementation prompt + +If the SPEC mentions _CRITICAL_ or _REORIENTATION_ or _STEERING_ sections, make sure to follow them; make sure those tasks are prioritized. This is what we are using to steer to the right direction. + +## Constraints + +- Pick ONE atomic unit, not the entire feature +- The implementation prompt must be specific enough for another agent to execute without ambiguity +- List exact file paths for files to create/modify + +## REQUIRED OUTPUT + +{props.schema} diff --git a/.smithers/prompts/review.mdx b/.smithers/prompts/review.mdx new file mode 100644 index 00000000..c9dbe2cf --- /dev/null +++ b/.smithers/prompts/review.mdx @@ -0,0 +1,44 @@ +# Review — Pass {props.pass} + +You are a senior code reviewer for Cairo Coder, a Python/FastAPI/DSPy project. + +## What Changed + +**Task:** {props.taskName} +**Summary:** {props.summary} +**Files changed:** {JSON.stringify(props.filesChanged ?? [])} +**Test output:** {props.testOutput ?? "No test output provided"} + +## Instructions + +1. Read CLAUDE.md for project conventions +2. Read ALL changed files listed above — do not review blindly +3. Run verification checks in order: + +```bash +cd python && uv run pytest -v +trunk check --fix +``` + +4. Check for: + - Correct DSPy patterns (Signature → Module → factory function) + - Type hints on all functions + - No hardcoded paths + - No duplicated fixtures (all shared fixtures in `tests/conftest.py`) + - Async/await for I/O operations + - Proper structlog usage + +## Decision + +Set `lgtm: true` ONLY if: + +- ALL tests pass +- ALL lint/type checks pass +- Code follows CLAUDE.md conventions +- No security issues (OWASP top 10) + +If ANY check fails, set `lgtm: false` and list every specific issue. + +## REQUIRED OUTPUT + +{props.schema} diff --git a/.smithers/schemas.ts b/.smithers/schemas.ts new file mode 100644 index 00000000..64a2938d --- /dev/null +++ b/.smithers/schemas.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +export const PlanSchema = z.object({ + taskName: z.string().describe("Name of the next task to implement"), + research: z + .string() + .describe("What was discovered by examining the codebase"), + implementationPrompt: z + .string() + .describe("Detailed prompt for the implementer"), + filesToCreate: z.array(z.string()).describe("Files that need to be created"), + filesToModify: z.array(z.string()).describe("Files that need to be modified"), + nextSmallestUnit: z + .string() + .nullable() + .describe("Next smallest atomic unit after this task"), +}); + +export const ImplementSchema = z.object({ + summary: z.string().describe("What was implemented"), + filesChanged: z + .array(z.string()) + .describe("Files that were created or modified"), + testOutput: z.string().describe("Output from running pytest"), + commitMessage: z.string().describe("Git commit message for this change"), + nextSmallestUnit: z + .string() + .nullable() + .describe("Next smallest atomic unit to implement"), +}); + +export const ReviewSchema = z.object({ + lgtm: z.boolean().describe("true ONLY if ALL checks pass"), + review: z.string().describe("Summary of the review findings"), + issues: z.array(z.string()).describe("Specific issues found"), +}); + +export const FixSchema = z.object({ + summary: z.string().describe("What was fixed"), + filesChanged: z.array(z.string()).describe("Files that were modified"), +}); + +export const FinalReviewSchema = z.object({ + readyToMoveOn: z.boolean().describe("true ONLY if all criteria met"), + reasoning: z + .string() + .describe("Why ready or not ready — feeds back into next pass"), + qualityScore: z.number().min(1).max(10).describe("Overall quality score"), +}); + +export const PassTrackerSchema = z.object({ + totalIterations: z.number(), + tasksCompleted: z.array(z.string()), + summary: z.string(), +}); diff --git a/.smithers/workflow.db b/.smithers/workflow.db index e2a7b5fcb57cc6687bfa6e58d2149652c9693593..7999f81c9b5e66eed16a011f4543caf4c9fc5396 100644 GIT binary patch delta 51 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 diff --git a/.smithers/workflow.db-shm b/.smithers/workflow.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..7524adb3167bf898315ca26c72addf3d97a30320 GIT binary patch literal 32768 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, - filesToModify: text("files_to_modify", { mode: "json" }).$type(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) -); - -const implementTable = sqliteTable( - "implement", - { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - iteration: integer("iteration").notNull().default(0), - summary: text("summary").notNull(), - filesChanged: text("files_changed", { mode: "json" }).$type(), - testOutput: text("test_output").notNull(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) -); - -const reviewTable = sqliteTable( - "review", - { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - iteration: integer("iteration").notNull().default(0), - lgtm: integer("lgtm", { mode: "boolean" }).notNull(), - review: text("review").notNull(), - issues: text("issues", { mode: "json" }).$type(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) -); - -const fixTable = sqliteTable( - "fix", - { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - iteration: integer("iteration").notNull().default(0), - summary: text("summary").notNull(), - filesChanged: text("files_changed", { mode: "json" }).$type(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) -); - -const outputTable = sqliteTable( - "output", +import { execSync } from "node:child_process"; +import { planner, implementer, reviewer } from "./agents"; +import { MAX_PASSES } from "./config"; +import { + PlanSchema, + ImplementSchema, + ReviewSchema, + FixSchema, + FinalReviewSchema, + PassTrackerSchema, +} from "./schemas"; + +import PlanPrompt from "./prompts/plan.mdx"; +import ImplementPrompt from "./prompts/implement.mdx"; +import ReviewPrompt from "./prompts/review.mdx"; +import FixPrompt from "./prompts/fix.mdx"; +import FinalReviewPrompt from "./prompts/final-review.mdx"; + +// Use Workflow and Task from createSmithers — they include schema context +// and auto-inject outputSchema from the registry +const { smithers, tables, Workflow, Task, db } = createSmithers( { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - totalTasks: integer("total_tasks").notNull(), - finalStatus: text("final_status").notNull(), + plan: PlanSchema, + implement: ImplementSchema, + review: ReviewSchema, + fix: FixSchema, + finalReview: FinalReviewSchema, + passTracker: PassTrackerSchema, }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId] }) }) + { dbPath: ".smithers/workflow.db" }, ); -export const schema = { - input: inputTable, - output: outputTable, - plan: planTable, - implement: implementTable, - review: reviewTable, - fix: fixTable, -}; - -export const db = drizzle(".smithers/workflow.db", { schema }); - -// Create tables +// --- Supervisor state contract --- +// The takopi-smithers supervisor reads these keys from the `state` table +// to monitor health (heartbeat) and report status (Telegram updates). (db as any).$client.exec(` - CREATE TABLE IF NOT EXISTS input ( - run_id TEXT PRIMARY KEY, - spec_path TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS plan ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - task_name TEXT NOT NULL, research TEXT NOT NULL, implementation_prompt TEXT NOT NULL, - files_to_create TEXT, files_to_modify TEXT, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS implement ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - summary TEXT NOT NULL, files_changed TEXT, test_output TEXT NOT NULL, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS review ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - lgtm INTEGER NOT NULL, review TEXT NOT NULL, issues TEXT, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS fix ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - summary TEXT NOT NULL, files_changed TEXT, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS output ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, - total_tasks INTEGER NOT NULL, final_status TEXT NOT NULL, - PRIMARY KEY (run_id, node_id) - ); CREATE TABLE IF NOT EXISTS state ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT DEFAULT (datetime('now')) @@ -124,7 +51,7 @@ export const db = drizzle(".smithers/workflow.db", { schema }); function updateState(key: string, value: string) { (db as any).$client.run( "INSERT OR REPLACE INTO state (key, value, updated_at) VALUES (?, ?, datetime('now'))", - [key, value] + [key, value], ); } @@ -132,119 +59,194 @@ updateState("supervisor.status", "running"); updateState("supervisor.summary", "Workflow initialized"); updateState("supervisor.heartbeat", new Date().toISOString()); +// Write heartbeat every 30s so the supervisor knows we're alive setInterval(() => { try { updateState("supervisor.heartbeat", new Date().toISOString()); } catch (err) { console.error("Failed to write heartbeat:", err); } -}, 30000); - -const cliEnv = { ANTHROPIC_API_KEY: "" }; - -const plannerAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior software architect. Read the spec, examine the codebase, " + - "and pick the NEXT highest-priority task. Produce a detailed implementation prompt. " + - "Respond with ONLY a JSON object: " + - '{ "taskName": "string", "research": "string", "implementationPrompt": "string", ' + - '"filesToCreate": ["paths"], "filesToModify": ["paths"] }', -}); - -const implementAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior TypeScript engineer. Implement the task described below. " + - "After writing code, ALWAYS run tests to verify. " + - "Respond with ONLY a JSON object: " + - '{ "summary": "string", "filesChanged": ["paths"], "testOutput": "string" }', -}); +}, 30_000); -const reviewAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior code reviewer. Run type checker and tests. " + - "Set lgtm=true ONLY if everything is correct. " + - "Respond with ONLY a JSON object: " + - '{ "lgtm": true/false, "review": "string", "issues": ["specific issues"] }', -}); - -const fixAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior TypeScript engineer fixing code review issues. " + - "After making changes, run tests and type checker. " + - "Respond with ONLY a JSON object: " + - '{ "summary": "string", "filesChanged": ["paths"] }', +process.on("beforeExit", () => { + try { + updateState("supervisor.status", "done"); + updateState("supervisor.summary", "Workflow completed"); + } catch {} }); -type Phase = "plan" | "implement" | "review" | "fix"; - -function computePhase(plans: any[], impls: any[], reviews: any[], fixes: any[]): Phase { - if (plans.length === 0) return "plan"; - if (impls.length < plans.length) return "implement"; - if (reviews.length < plans.length + fixes.length) return "review"; - const latestReview = reviews[reviews.length - 1]; - if (latestReview?.lgtm) return "plan"; - if (fixes.length >= reviews.filter((r: any) => !r.lgtm).length) return "review"; - if (fixes.length >= 3) return "plan"; - return "fix"; +function getWorkingTreeChangedFiles(): string[] { + try { + const status = execSync("git status --short", { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + + const files = status + .split("\n") + .map((line) => line.trimEnd()) + .filter(Boolean) + .map((line) => line.slice(3).trim()) + .map((path) => { + const renameParts = path.split(" -> "); + return renameParts[renameParts.length - 1]; + }); + + return [...new Set(files)].sort(); + } catch { + return []; + } } -export default smithers(db, (ctx) => { - const plans: any[] = ctx.outputs.plan ?? []; - const impls: any[] = ctx.outputs.implement ?? []; - const reviews: any[] = ctx.outputs.review ?? []; - const fixes: any[] = ctx.outputs.fix ?? []; - - const phase = computePhase(plans, impls, reviews, fixes); - const latestPlan = plans[plans.length - 1]; - const latestImpl = impls[impls.length - 1]; - const latestReview = reviews[reviews.length - 1]; - - const completedTasks = reviews - .filter((r: any) => r.lgtm) - .map((_r: any, i: number) => plans[i]?.taskName) - .filter(Boolean) - .join(", "); - - updateState("supervisor.status", "running"); - updateState("supervisor.summary", - "Phase: " + phase + " | Tasks done: " + reviews.filter((r: any) => r.lgtm).length); +export default smithers((ctx) => { + const specPath = ctx.input.specPath ?? "SPEC.md"; + + // Read latest outputs + const latestPlan = ctx.outputMaybe("plan", { nodeId: "plan" }); + const latestImpl = ctx.outputMaybe("implement", { nodeId: "implement" }); + const latestReview = ctx.outputMaybe("review", { nodeId: "review" }); + const latestFix = ctx.outputMaybe("fix", { nodeId: "fix" }); + const latestFinalReview = ctx.outputMaybe("finalReview", { + nodeId: "final-review", + }); + const passTracker = ctx.outputMaybe("passTracker", { + nodeId: "pass-tracker", + }); + + const currentPass = passTracker?.totalIterations ?? 0; + const done = + (latestFinalReview?.readyToMoveOn ?? false) || currentPass >= MAX_PASSES; + + // Track completed tasks across passes + const completedTasks = (passTracker?.tasksCompleted ?? []).join(", "); + + // Determine if we need fixes + const needsFix = + latestReview && + !latestReview.lgtm && + (latestReview.issues?.length ?? 0) > 0; + + const filesChangedForReview = + (latestImpl?.filesChanged?.length ?? 0) > 0 + ? latestImpl!.filesChanged + : getWorkingTreeChangedFiles(); return ( - - - - {"Read the project spec at " + (ctx.input.specPath ?? "SPEC.md") + " and examine the codebase. Completed tasks: " + (completedTasks || "None yet") + ". Pick the NEXT task. Research what's needed. Write a detailed implementation prompt."} - - - - {"TASK: " + (latestPlan?.taskName ?? "unknown") + " -- " + (latestPlan?.implementationPrompt ?? "No implementation prompt.") + " Files to create: " + JSON.stringify(latestPlan?.filesToCreate ?? []) + " Files to modify: " + JSON.stringify(latestPlan?.filesToModify ?? []) + " After implementing, run tests and report results."} - - - - {"Review: " + (latestPlan?.taskName ?? "unknown") + " | Summary: " + (latestImpl?.summary ?? "No summary") + " | Files: " + JSON.stringify(latestImpl?.filesChanged ?? []) + " | Tests: " + (latestImpl?.testOutput ?? "No test output") + " -- Read ALL changed files. Run type checker and tests."} - - - - {"Fix review issues for: " + (latestPlan?.taskName ?? "unknown") + " Issues: " + (latestReview?.issues?.map((issue: string, i: number) => (i + 1) + ". " + issue).join(", ") ?? "None") + " Fix each issue. Run tests after."} - + + + + {/* 1. Plan — pick next atomic unit */} + + {renderMdx(PlanPrompt, { + specPath, + completedTasks, + pass: currentPass + 1, + previousFeedback: latestFinalReview?.reasoning ?? null, + schema: zodSchemaToJsonExample(PlanSchema), + })} + + + {/* 2. Implement — build it */} + + {renderMdx(ImplementPrompt, { + taskName: latestPlan?.taskName ?? "unknown", + implementationPrompt: latestPlan?.implementationPrompt ?? "", + filesToCreate: latestPlan?.filesToCreate ?? [], + filesToModify: latestPlan?.filesToModify ?? [], + previousWork: latestImpl, + reviewFixes: latestFix?.summary ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(ImplementSchema), + })} + + + {/* 3. Review — check quality */} + + {renderMdx(ReviewPrompt, { + taskName: latestPlan?.taskName ?? "unknown", + summary: latestImpl?.summary ?? "No summary", + filesChanged: filesChangedForReview, + testOutput: latestImpl?.testOutput ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(ReviewSchema), + })} + + + {/* 4. Fix — address review issues (skip if lgtm) */} + + {renderMdx(FixPrompt, { + taskName: latestPlan?.taskName ?? "unknown", + issues: latestReview?.issues ?? [], + reviewFeedback: latestReview?.review ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(FixSchema), + })} + + + {/* 5. Final review gate — loop or done */} + + {renderMdx(FinalReviewPrompt, { + completedTasks, + latestReview: latestReview?.review ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(FinalReviewSchema), + })} + + + {/* Pass tracker */} + + {{ + totalIterations: currentPass + 1, + tasksCompleted: [ + ...(passTracker?.tasksCompleted ?? []), + ...(latestFinalReview?.readyToMoveOn && latestPlan?.taskName + ? [latestPlan.taskName] + : []), + ], + summary: `Pass ${currentPass + 1} of ${MAX_PASSES} complete.`, + }} + + - - - {{ totalTasks: reviews.filter((r: any) => r.lgtm).length, finalStatus: "done" }} - ); }); - -process.on("beforeExit", () => { - try { - updateState("supervisor.status", "done"); - updateState("supervisor.summary", "Workflow completed successfully"); - } catch (err) { - console.error("Failed to update final state:", err); - } -}); diff --git a/.takopi-smithers/autoheal-prompt.txt b/.takopi-smithers/autoheal-prompt.txt index 878cf199..be8accc4 100644 --- a/.takopi-smithers/autoheal-prompt.txt +++ b/.takopi-smithers/autoheal-prompt.txt @@ -9,144 +9,74 @@ The Smithers workflow process crashed. Your job is to diagnose and fix the issue ## Database State (from SQLite) - Status: running -- Summary: Phase: plan | Tasks done: 0 +- Summary: Workflow initialized - Last error: N/A -- Heartbeat: 2026-02-16T11:40:07.735Z +- Heartbeat: 2026-02-16T13:42:46.681Z ## Recent Logs (last 100 lines) ``` -[2026-02-16T11:40:17.286Z] [INFO] Capturing auto-heal context... +[2026-02-16T13:43:12.582Z] [INFO] Capturing auto-heal context... == gnal SIGINT -x -logic. -side another Claude Code session. -Nested sessions share runtime resources and will crash all active sessions. -To bypass this check, unset the CLAUDECODE environment variable. +. +hers/workflow.tsx` to apply the fix. Could you approve the write permission? +**Summary of the fix:** + +**Root cause:** The `CLAUDE_CODE_*` / `CLAUDECODE` env vars are inherited from the parent takopi-smithers session by child `claude` CLI processes spawned by `ClaudeCodeAgent` (`planner` and `reviewer`). The child processes detect they're nested inside another Claude Code session and exit immediately with code 0, no error, no output. + +**Patch:** Strip all `CLAUD +: ``` ## Current Workflow File (.smithers/workflow.tsx) ```tsx -import { smithers, Workflow, Task, Ralph, ClaudeCodeAgent } from "smithers-orchestrator"; -import { drizzle } from "drizzle-orm/bun-sqlite"; -import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; +import { + createSmithers, + Sequence, + Ralph, + renderMdx, + zodSchemaToJsonExample, +} from "smithers-orchestrator"; import React from "react"; - -const inputTable = sqliteTable("input", { - runId: text("run_id").primaryKey(), - specPath: text("spec_path").notNull(), -}); - -const planTable = sqliteTable( - "plan", - { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - iteration: integer("iteration").notNull().default(0), - taskName: text("task_name").notNull(), - research: text("research").notNull(), - implementationPrompt: text("implementation_prompt").notNull(), - filesToCreate: text("files_to_create", { mode: "json" }).$type(), - filesToModify: text("files_to_modify", { mode: "json" }).$type(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) -); - -const implementTable = sqliteTable( - "implement", - { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - iteration: integer("iteration").notNull().default(0), - summary: text("summary").notNull(), - filesChanged: text("files_changed", { mode: "json" }).$type(), - testOutput: text("test_output").notNull(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) -); - -const reviewTable = sqliteTable( - "review", +import { execSync } from "node:child_process"; +import { planner, implementer, reviewer } from "./agents"; +import { MAX_PASSES } from "./config"; +import { + PlanSchema, + ImplementSchema, + ReviewSchema, + FixSchema, + FinalReviewSchema, + PassTrackerSchema, +} from "./schemas"; + +import PlanPrompt from "./prompts/plan.mdx"; +import ImplementPrompt from "./prompts/implement.mdx"; +import ReviewPrompt from "./prompts/review.mdx"; +import FixPrompt from "./prompts/fix.mdx"; +import FinalReviewPrompt from "./prompts/final-review.mdx"; + +// Use Workflow and Task from createSmithers — they include schema context +// and auto-inject outputSchema from the registry +const { smithers, tables, Workflow, Task, db } = createSmithers( { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - iteration: integer("iteration").notNull().default(0), - lgtm: integer("lgtm", { mode: "boolean" }).notNull(), - review: text("review").notNull(), - issues: text("issues", { mode: "json" }).$type(), + plan: PlanSchema, + implement: ImplementSchema, + review: ReviewSchema, + fix: FixSchema, + finalReview: FinalReviewSchema, + passTracker: PassTrackerSchema, }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) + { dbPath: ".smithers/workflow.db" }, ); -const fixTable = sqliteTable( - "fix", - { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - iteration: integer("iteration").notNull().default(0), - summary: text("summary").notNull(), - filesChanged: text("files_changed", { mode: "json" }).$type(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }) }) -); - -const outputTable = sqliteTable( - "output", - { - runId: text("run_id").notNull(), - nodeId: text("node_id").notNull(), - totalTasks: integer("total_tasks").notNull(), - finalStatus: text("final_status").notNull(), - }, - (t) => ({ pk: primaryKey({ columns: [t.runId, t.nodeId] }) }) -); - -export const schema = { - input: inputTable, - output: outputTable, - plan: planTable, - implement: implementTable, - review: reviewTable, - fix: fixTable, -}; - -export const db = drizzle(".smithers/workflow.db", { schema }); - -// Create tables +// --- Supervisor state contract --- +// The takopi-smithers supervisor reads these keys from the `state` table +// to monitor health (heartbeat) and report status (Telegram updates). (db as any).$client.exec(` - CREATE TABLE IF NOT EXISTS input ( - run_id TEXT PRIMARY KEY, - spec_path TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS plan ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - task_name TEXT NOT NULL, research TEXT NOT NULL, implementation_prompt TEXT NOT NULL, - files_to_create TEXT, files_to_modify TEXT, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS implement ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - summary TEXT NOT NULL, files_changed TEXT, test_output TEXT NOT NULL, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS review ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - lgtm INTEGER NOT NULL, review TEXT NOT NULL, issues TEXT, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS fix ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, iteration INTEGER NOT NULL DEFAULT 0, - summary TEXT NOT NULL, files_changed TEXT, - PRIMARY KEY (run_id, node_id, iteration) - ); - CREATE TABLE IF NOT EXISTS output ( - run_id TEXT NOT NULL, node_id TEXT NOT NULL, - total_tasks INTEGER NOT NULL, final_status TEXT NOT NULL, - PRIMARY KEY (run_id, node_id) - ); CREATE TABLE IF NOT EXISTS state ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT DEFAULT (datetime('now')) @@ -156,7 +86,7 @@ export const db = drizzle(".smithers/workflow.db", { schema }); function updateState(key: string, value: string) { (db as any).$client.run( "INSERT OR REPLACE INTO state (key, value, updated_at) VALUES (?, ?, datetime('now'))", - [key, value] + [key, value], ); } @@ -164,123 +94,198 @@ updateState("supervisor.status", "running"); updateState("supervisor.summary", "Workflow initialized"); updateState("supervisor.heartbeat", new Date().toISOString()); +// Write heartbeat every 30s so the supervisor knows we're alive setInterval(() => { try { updateState("supervisor.heartbeat", new Date().toISOString()); } catch (err) { console.error("Failed to write heartbeat:", err); } -}, 30000); - -const cliEnv = { ANTHROPIC_API_KEY: "" }; - -const plannerAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior software architect. Read the spec, examine the codebase, " + - "and pick the NEXT highest-priority task. Produce a detailed implementation prompt. " + - "Respond with ONLY a JSON object: " + - '{ "taskName": "string", "research": "string", "implementationPrompt": "string", ' + - '"filesToCreate": ["paths"], "filesToModify": ["paths"] }', -}); - -const implementAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior TypeScript engineer. Implement the task described below. " + - "After writing code, ALWAYS run tests to verify. " + - "Respond with ONLY a JSON object: " + - '{ "summary": "string", "filesChanged": ["paths"], "testOutput": "string" }', -}); +}, 30_000); -const reviewAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior code reviewer. Run type checker and tests. " + - "Set lgtm=true ONLY if everything is correct. " + - "Respond with ONLY a JSON object: " + - '{ "lgtm": true/false, "review": "string", "issues": ["specific issues"] }', -}); - -const fixAgent = new ClaudeCodeAgent({ - model: "sonnet", - env: cliEnv, - systemPrompt: "You are a senior TypeScript engineer fixing code review issues. " + - "After making changes, run tests and type checker. " + - "Respond with ONLY a JSON object: " + - '{ "summary": "string", "filesChanged": ["paths"] }', +process.on("beforeExit", () => { + try { + updateState("supervisor.status", "done"); + updateState("supervisor.summary", "Workflow completed"); + } catch {} }); -type Phase = "plan" | "implement" | "review" | "fix"; - -function computePhase(plans: any[], impls: any[], reviews: any[], fixes: any[]): Phase { - if (plans.length === 0) return "plan"; - if (impls.length < plans.length) return "implement"; - if (reviews.length < plans.length + fixes.length) return "review"; - const latestReview = reviews[reviews.length - 1]; - if (latestReview?.lgtm) return "plan"; - if (fixes.length >= reviews.filter((r: any) => !r.lgtm).length) return "review"; - if (fixes.length >= 3) return "plan"; - return "fix"; +function getWorkingTreeChangedFiles(): string[] { + try { + const status = execSync("git status --short", { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + + const files = status + .split("\n") + .map((line) => line.trimEnd()) + .filter(Boolean) + .map((line) => line.slice(3).trim()) + .map((path) => { + const renameParts = path.split(" -> "); + return renameParts[renameParts.length - 1]; + }); + + return [...new Set(files)].sort(); + } catch { + return []; + } } -export default smithers(db, (ctx) => { - const plans: any[] = ctx.outputs.plan ?? []; - const impls: any[] = ctx.outputs.implement ?? []; - const reviews: any[] = ctx.outputs.review ?? []; - const fixes: any[] = ctx.outputs.fix ?? []; - - const phase = computePhase(plans, impls, reviews, fixes); - const latestPlan = plans[plans.length - 1]; - const latestImpl = impls[impls.length - 1]; - const latestReview = reviews[reviews.length - 1]; - - const completedTasks = reviews - .filter((r: any) => r.lgtm) - .map((_r: any, i: number) => plans[i]?.taskName) - .filter(Boolean) - .join(", "); - - updateState("supervisor.status", "running"); - updateState("supervisor.summary", - "Phase: " + phase + " | Tasks done: " + reviews.filter((r: any) => r.lgtm).length); +export default smithers((ctx) => { + const specPath = ctx.input.specPath ?? "SPEC.md"; + + // Read latest outputs + const latestPlan = ctx.outputMaybe("plan", { nodeId: "plan" }); + const latestImpl = ctx.outputMaybe("implement", { nodeId: "implement" }); + const latestReview = ctx.outputMaybe("review", { nodeId: "review" }); + const latestFix = ctx.outputMaybe("fix", { nodeId: "fix" }); + const latestFinalReview = ctx.outputMaybe("finalReview", { + nodeId: "final-review", + }); + const passTracker = ctx.outputMaybe("passTracker", { + nodeId: "pass-tracker", + }); + + const currentPass = passTracker?.totalIterations ?? 0; + const done = + (latestFinalReview?.readyToMoveOn ?? false) || currentPass >= MAX_PASSES; + + // Track completed tasks across passes + const completedTasks = (passTracker?.tasksCompleted ?? []).join(", "); + + // Determine if we need fixes + const needsFix = + latestReview && + !latestReview.lgtm && + (latestReview.issues?.length ?? 0) > 0; + + const filesChangedForReview = + (latestImpl?.filesChanged?.length ?? 0) > 0 + ? latestImpl!.filesChanged + : getWorkingTreeChangedFiles(); return ( - - - - {"Read the project spec at " + (ctx.input.specPath ?? "SPEC.md") + " and examine the codebase. Completed tasks: " + (completedTasks || "None yet") + ". Pick the NEXT task. Research what's needed. Write a detailed implementation prompt."} - - - - {"TASK: " + (latestPlan?.taskName ?? "unknown") + " -- " + (latestPlan?.implementationPrompt ?? "No implementation prompt.") + " Files to create: " + JSON.stringify(latestPlan?.filesToCreate ?? []) + " Files to modify: " + JSON.stringify(latestPlan?.filesToModify ?? []) + " After implementing, run tests and report results."} - - - - {"Review: " + (latestPlan?.taskName ?? "unknown") + " | Summary: " + (latestImpl?.summary ?? "No summary") + " | Files: " + JSON.stringify(latestImpl?.filesChanged ?? []) + " | Tests: " + (latestImpl?.testOutput ?? "No test output") + " -- Read ALL changed files. Run type checker and tests."} - - - - {"Fix review issues for: " + (latestPlan?.taskName ?? "unknown") + " Issues: " + (latestReview?.issues?.map((issue: string, i: number) => (i + 1) + ". " + issue).join(", ") ?? "None") + " Fix each issue. Run tests after."} - + + + + {/* 1. Plan — pick next atomic unit */} + + {renderMdx(PlanPrompt, { + specPath, + completedTasks, + pass: currentPass + 1, + previousFeedback: latestFinalReview?.reasoning ?? null, + schema: zodSchemaToJsonExample(PlanSchema), + })} + + + {/* 2. Implement — build it */} + + {renderMdx(ImplementPrompt, { + taskName: latestPlan?.taskName ?? "unknown", + implementationPrompt: latestPlan?.implementationPrompt ?? "", + filesToCreate: latestPlan?.filesToCreate ?? [], + filesToModify: latestPlan?.filesToModify ?? [], + previousWork: latestImpl, + reviewFixes: latestFix?.summary ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(ImplementSchema), + })} + + + {/* 3. Review — check quality */} + + {renderMdx(ReviewPrompt, { + taskName: latestPlan?.taskName ?? "unknown", + summary: latestImpl?.summary ?? "No summary", + filesChanged: filesChangedForReview, + testOutput: latestImpl?.testOutput ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(ReviewSchema), + })} + + + {/* 4. Fix — address review issues (skip if lgtm) */} + + {renderMdx(FixPrompt, { + taskName: latestPlan?.taskName ?? "unknown", + issues: latestReview?.issues ?? [], + reviewFeedback: latestReview?.review ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(FixSchema), + })} + + + {/* 5. Final review gate — loop or done */} + + {renderMdx(FinalReviewPrompt, { + completedTasks, + latestReview: latestReview?.review ?? null, + pass: currentPass + 1, + schema: zodSchemaToJsonExample(FinalReviewSchema), + })} + + + {/* Pass tracker */} + + {{ + totalIterations: currentPass + 1, + tasksCompleted: [ + ...(passTracker?.tasksCompleted ?? []), + ...(latestFinalReview?.readyToMoveOn && latestPlan?.taskName + ? [latestPlan.taskName] + : []), + ], + summary: `Pass ${currentPass + 1} of ${MAX_PASSES} complete.`, + }} + + - - - {{ totalTasks: reviews.filter((r: any) => r.lgtm).length, finalStatus: "done" }} - ); }); -process.on("beforeExit", () => { - try { - updateState("supervisor.status", "done"); - updateState("supervisor.summary", "Workflow completed successfully"); - } catch (err) { - console.error("Failed to update final state:", err); - } -}); - ``` ## Your Task diff --git a/.takopi-smithers/supervisor.pid b/.takopi-smithers/supervisor.pid new file mode 100644 index 00000000..297c5ab6 --- /dev/null +++ b/.takopi-smithers/supervisor.pid @@ -0,0 +1 @@ +40120 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index fa92b539..394cfbbb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,5 +3,6 @@ Read and follow: TAKOPI_SMITHERS.md Key paths: + - .smithers/workflow.tsx - .takopi-smithers/config.toml diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 00000000..e7d198e2 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,108 @@ +# SPEC: MCP Mode Skill Generation + +## Summary + +Replace MCP mode's raw document dump with an LLM-powered step that synthesizes retrieved docs into a Claude Code skill file (SKILL.md format). Instead of returning unstructured documentation, MCP mode will return precise, actionable LLM instructions that callers can use directly. + +## Background + +- MCP mode is triggered by the `x-mcp-mode` or `mcp` HTTP header +- Currently it skips LLM generation and returns raw docs formatted as markdown (title, source, URL, content) +- This is suboptimal: modern models work better with precise instructions in the skill format than with raw context dumps +- The skill format has YAML frontmatter (`name`, `description`) and a markdown body with instructions, examples, and code patterns + +## Requirements + +### 1. New DSPy Signature: `SkillGeneration` + +**File:** `python/src/cairo_coder/dspy/generation_program.py` + +Create a new DSPy Signature that takes a query and retrieved documents and produces a SKILL.md string. + +**Inputs:** + +- `query` (str): The original user query +- `context` (str): Retrieved documents formatted as context (reuse `_prepare_context()` from the pipeline) + +**Output:** + +- `skill` (str): A complete SKILL.md file content + +The output must follow this structure: + +```markdown +--- +name: +description: <1-2 sentence description of what this skill does and when to use it> +--- + + +- Clear instructions for the LLM +- Relevant code examples extracted from the retrieved docs +- Cairo/Starknet-specific patterns and best practices +- Source references where applicable +``` + +### 2. New DSPy Module: `SkillGenerationProgram` + +**File:** `python/src/cairo_coder/dspy/generation_program.py` + +Replace `McpGenerationProgram` with `SkillGenerationProgram`: + +- Wraps the `SkillGeneration` signature in a `dspy.ChainOfThought` module +- Implements both `forward()` and `aforward()` (async) +- Uses the same LLM as the normal generation path + +Update the factory function `create_mcp_generation_program()` to return a `SkillGenerationProgram` instance. + +### 3. Update RAG Pipeline MCP Branch + +**File:** `python/src/cairo_coder/core/rag_pipeline.py` + +In both `aforward()` (line ~181) and `aforward_streaming()` (line ~260): + +- The MCP branch currently calls `self.mcp_generation_program.acall(documents)` +- Change it to prepare context first (call `self._prepare_context(documents)`) and then call `self.mcp_generation_program.acall(query=query, context=context)` +- The response field changes from `result.answer` to `result.skill` + +### 4. Update Tests + +**Unit tests** in `python/tests/unit/test_generation_program.py`: + +- Test `SkillGenerationProgram` produces valid SKILL.md output +- Test output contains YAML frontmatter with `name` and `description` +- Test output contains markdown body +- Test empty documents case + +**Unit tests** in `python/tests/unit/test_rag_pipeline.py`: + +- Update existing MCP mode tests to expect skill format instead of raw docs + +### 5. Remove Dead Code + +- Delete `McpGenerationProgram` class +- Delete old `create_mcp_generation_program()` factory (replace with new one) + +## Out of Scope + +- Saving generated skills to disk +- New HTTP headers or endpoints (still uses `x-mcp-mode`) +- Changes to the ingester or document retrieval pipeline +- Optimizing the skill generation prompt with DSPy optimizers (future work) + +## Acceptance Criteria + +1. `x-mcp-mode` requests return a valid SKILL.md string instead of raw docs +2. Output contains YAML frontmatter with `name` and `description` fields +3. Output contains actionable instructions (not just pasted docs) +4. All existing tests pass (updated for new format) +5. New unit tests for `SkillGenerationProgram` +6. `uv run pytest` passes from `python/` directory +7. `trunk check --fix` passes from repo root + +## Verification Commands + +```bash +cd python && uv run pytest -v +trunk check --fix +``` diff --git a/bun.lock b/bun.lock index e96591bc..4d940483 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,9 @@ }, }, }, + "patchedDependencies": { + "smithers-orchestrator@0.6.0": "patches/smithers-orchestrator@0.6.0.patch", + }, "packages": { "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.44", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ke1NldgohWJ7sWLqm9Um9TVIOrtg8Y8AecWeB6PgaLt+paTPisAsyNfe8FNOVusuv58ugLBqY/78AkhUmbjXHA=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..5185862f --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["./.smithers/preload.ts"] diff --git a/package.json b/package.json index 528c27eb..23eb28a4 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,8 @@ "smithers-orchestrator": "^0.6.0", "takopi-smithers": "github:evmts/takopi-smithers", "zod": "^4.3.6" + }, + "patchedDependencies": { + "smithers-orchestrator@0.6.0": "patches/smithers-orchestrator@0.6.0.patch" } } diff --git a/patches/smithers-orchestrator@0.6.0.patch b/patches/smithers-orchestrator@0.6.0.patch new file mode 100644 index 00000000..60407704 --- /dev/null +++ b/patches/smithers-orchestrator@0.6.0.patch @@ -0,0 +1,27 @@ +diff --git a/src/agents/cli.ts b/src/agents/cli.ts +--- a/src/agents/cli.ts ++++ b/src/agents/cli.ts +@@ -548,7 +548,12 @@ + const { prompt, systemFromMessages } = extractPrompt(options); + const callTimeout = resolveTimeoutMs(options?.timeout, this.timeoutMs); + const cwd = this.cwd ?? getToolContext()?.rootDir ?? process.cwd(); +- const env = { ...process.env, ...(this.env ?? {}) } as Record< ++ // Strip CLAUDE* env vars to prevent child CLI agents from detecting ++ // a nested session and exiting immediately with code 0. ++ const baseEnv = Object.fromEntries( ++ Object.entries(process.env).filter(([k]) => !k.startsWith("CLAUDE")), ++ ); ++ const env = { ...baseEnv, ...(this.env ?? {}) } as Record< + string, + string + >; +@@ -830,8 +835,7 @@ + let schemaCleanupFile: string | null = null; + if (!this.opts.outputSchema && params.options?.outputSchema) { + try { +- const { zodToJsonSchema } = await import("zod-to-json-schema"); +- const jsonSchema = zodToJsonSchema(params.options.outputSchema); ++ const jsonSchema = params.options.outputSchema.toJSONSchema(); + const schemaFile = join( + tmpdir(), + `smithers-schema-${randomUUID()}.json`,