From 1b3bca99027d6ef1d8407592c70f742f52c1b60a Mon Sep 17 00:00:00 2001 From: Ezra Date: Thu, 2 Apr 2026 19:47:00 +0000 Subject: [PATCH 1/2] Implement Archon Architecture POC (Epic #370, Dispatch #371) Three-layer architecture implementation: - Layer 1: Thin Hermes profile (profile.yaml, < 50 lines) - Layer 2: Claw Code runtime (harness.py, tool_registry.py) - Layer 3: Gemma via Ollama (ollama_client.py) Features: - Tool registry with pattern matching (/time, /status, /echo, /ollama_list) - Full routing between tool execution and intelligence layer - Ollama client with streaming and chat support - Comprehensive integration test suite (12 tests) - Connection to localhost:11434 using gemma3:4b model --- archon-poc/README.md | 62 +++++ .../__pycache__/ollama_client.cpython-312.pyc | Bin 0 -> 8997 bytes archon-poc/ollama_client.py | 226 ++++++++++++++++++ archon-poc/profile.yaml | 25 ++ .../__pycache__/harness.cpython-312.pyc | Bin 0 -> 6741 bytes .../__pycache__/tool_registry.cpython-312.pyc | Bin 0 -> 7570 bytes archon-poc/runtime/harness.py | 177 ++++++++++++++ archon-poc/runtime/tool_registry.py | 194 +++++++++++++++ archon-poc/test_integration.py | 214 +++++++++++++++++ 9 files changed, 898 insertions(+) create mode 100644 archon-poc/README.md create mode 100644 archon-poc/__pycache__/ollama_client.cpython-312.pyc create mode 100644 archon-poc/ollama_client.py create mode 100644 archon-poc/profile.yaml create mode 100644 archon-poc/runtime/__pycache__/harness.cpython-312.pyc create mode 100644 archon-poc/runtime/__pycache__/tool_registry.cpython-312.pyc create mode 100644 archon-poc/runtime/harness.py create mode 100644 archon-poc/runtime/tool_registry.py create mode 100644 archon-poc/test_integration.py diff --git a/archon-poc/README.md b/archon-poc/README.md new file mode 100644 index 0000000..d3bc4ed --- /dev/null +++ b/archon-poc/README.md @@ -0,0 +1,62 @@ +# Archon Architecture Proof-of-Concept + +A three-layer architecture separating identity, runtime logic, and intelligence. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Layer 1: Hermes Profile (Thin) │ +│ - Identity & routing only │ +│ - No intelligence or reasoning logic │ +│ - Pure configuration (< 50 lines) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Layer 2: Claw Code Runtime │ +│ - All business logic lives here │ +│ - Tool registry and execution │ +│ - Message routing and orchestration │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Layer 3: Gemma 4 via Ollama │ +│ - The actual intelligence │ +│ - Local inference at localhost:11434 │ +│ - Model: gemma3:4b (available) / gemma4:4b (target) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Files + +| File | Purpose | +|------|---------| +| `profile.yaml` | Thin Hermes layer - identity only | +| `runtime/harness.py` | Claw Code runtime stub | +| `runtime/tool_registry.py` | Tool definitions and registry | +| `ollama_client.py` | Gemma 4 interface layer | +| `test_integration.py` | End-to-end integration test | + +## Quick Start + +```bash +# Run integration test +python3 test_integration.py + +# Test Ollama connection +python3 ollama_client.py "Hello, Gemma!" +``` + +## Design Principles + +1. **Profile is THIN** - Only identity, no logic +2. **Runtime owns everything** - All intelligence orchestration +3. **Local inference** - No external API dependencies +4. **Testable** - Full integration test coverage + +## Epic Reference + +Implements Epic #370: Archon Architecture +Dispatch #371: Proof-of-Concept Implementation diff --git a/archon-poc/__pycache__/ollama_client.cpython-312.pyc b/archon-poc/__pycache__/ollama_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50d557a0360959ea069530a60cfa315a7e21a114 GIT binary patch literal 8997 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 bool: + """Check if Ollama is reachable.""" + try: + response = self.session.get( + f"{self.config.base_url}/api/tags", + timeout=5 + ) + return response.status_code == 200 + except requests.RequestException: + return False + + def list_models(self) -> List[str]: + """List available models.""" + try: + response = self.session.get( + f"{self.config.base_url}/api/tags", + timeout=self.config.timeout + ) + response.raise_for_status() + data = response.json() + return [m["name"] for m in data.get("models", [])] + except requests.RequestException as e: + raise OllamaError(f"Failed to list models: {e}") + + def generate( + self, + prompt: str, + model: Optional[str] = None, + system: Optional[str] = None, + context: Optional[list] = None, + options: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Generate a response from the model. + + Args: + prompt: The user prompt + model: Model name (default: gemma3:4b) + system: System prompt + context: Previous context for conversation + options: Additional generation options + + Returns: + Response dict from Ollama + """ + model = model or self.config.default_model + + payload = { + "model": model, + "prompt": prompt, + "stream": False + } + + if system: + payload["system"] = system + if context: + payload["context"] = context + if options: + payload["options"] = options + + try: + response = self.session.post( + f"{self.config.base_url}/api/generate", + json=payload, + timeout=self.config.timeout + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + raise OllamaError(f"Generation failed: {e}") + + def generate_stream( + self, + prompt: str, + model: Optional[str] = None, + system: Optional[str] = None + ) -> Generator[str, None, None]: + """ + Stream generate responses. + + Yields response chunks as they arrive. + """ + model = model or self.config.default_model + + payload = { + "model": model, + "prompt": prompt, + "stream": True + } + + if system: + payload["system"] = system + + try: + response = self.session.post( + f"{self.config.base_url}/api/generate", + json=payload, + stream=True, + timeout=self.config.timeout + ) + response.raise_for_status() + + for line in response.iter_lines(): + if line: + try: + data = json.loads(line) + if "response" in data: + yield data["response"] + except json.JSONDecodeError: + continue + + except requests.RequestException as e: + raise OllamaError(f"Streaming failed: {e}") + + def chat( + self, + messages: list, + model: Optional[str] = None + ) -> Dict[str, Any]: + """ + Chat completion with message history. + + Args: + messages: List of {role, content} dicts + model: Model name + """ + model = model or self.config.default_model + + payload = { + "model": model, + "messages": messages, + "stream": False + } + + try: + response = self.session.post( + f"{self.config.base_url}/api/chat", + json=payload, + timeout=self.config.timeout + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + raise OllamaError(f"Chat failed: {e}") + + +class OllamaError(Exception): + """Ollama API error.""" + pass + + +def main(): + """CLI entry point.""" + client = OllamaClient() + + # Health check + print("Checking Ollama connection...") + if not client.health_check(): + print("ERROR: Ollama not reachable at localhost:11434") + sys.exit(1) + print("✓ Ollama is running\n") + + # List models + print("Available models:") + for model in client.list_models(): + print(f" - {model}") + print() + + # Generate if prompt provided + if len(sys.argv) > 1: + prompt = " ".join(sys.argv[1:]) + print(f"Prompt: {prompt}") + print("-" * 40) + + try: + response = client.generate( + prompt=prompt, + system="You are a helpful assistant. Be concise." + ) + print(response.get("response", "No response")) + print("-" * 40) + print(f"Tokens: {response.get('eval_count', 'N/A')}") + except OllamaError as e: + print(f"Error: {e}") + sys.exit(1) + else: + print("Usage: ollama_client.py ") + print("Example: ollama_client.py 'What is the Archon architecture?'") + + +if __name__ == "__main__": + main() diff --git a/archon-poc/profile.yaml b/archon-poc/profile.yaml new file mode 100644 index 0000000..555b5aa --- /dev/null +++ b/archon-poc/profile.yaml @@ -0,0 +1,25 @@ +name: Archon-POC +display_name: Archon (Proof-of-Concept) +model: gemma3:4b +variant: archon +provider: ollama +ollama_host: http://localhost:11434 + +creator: Ezra +lineage: Archon / Timmy Time Nexus +purpose: Three-layer architecture POC + +# THIN PROFILE - No logic, only identity and routing +# All intelligence lives in Claw runtime + Gemma layer + +routing: + runtime: claw.harness + intelligence: ollama.gemma + +constraints: + max_tokens: 4096 + temperature: 0.7 + +tagging: + required: true + tag: "#archon-poc" diff --git a/archon-poc/runtime/__pycache__/harness.cpython-312.pyc b/archon-poc/runtime/__pycache__/harness.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..940f5469f50f77b175640873c92fe39fb04c6b07 GIT binary patch literal 6741 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 F{|CzE*7E=W literal 0 HcmV?d00001 diff --git a/archon-poc/runtime/harness.py b/archon-poc/runtime/harness.py new file mode 100644 index 0000000..0784ae6 --- /dev/null +++ b/archon-poc/runtime/harness.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +Claw Code Runtime - Layer 2 of Archon Architecture + +This harness contains all business logic, orchestrating between +the thin Hermes profile (Layer 1) and Gemma intelligence (Layer 3). +""" + +import sys +import json +from typing import Dict, Any, Optional, List +from pathlib import Path + +# Add parent to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from runtime.tool_registry import ToolRegistry + + +class ClawHarness: + """ + The Claw Runtime - where all logic lives. + + Responsibilities: + - Message routing and orchestration + - Tool execution management + - Context window management + - Conversation state tracking + """ + + def __init__(self, ollama_host: str = "http://localhost:11434"): + self.ollama_host = ollama_host + self.tools = ToolRegistry() + self.conversation_history: List[Dict[str, Any]] = [] + self.session_id: Optional[str] = None + + def process_message(self, message: str, context: Optional[Dict] = None) -> Dict[str, Any]: + """ + Main entry point for processing messages. + + Flow: + 1. Parse intent + 2. Determine if tools needed + 3. Route to Gemma or execute tools + 4. Return formatted response + """ + context = context or {} + + # Log to conversation history + self.conversation_history.append({ + "role": "user", + "content": message, + "timestamp": self._get_timestamp() + }) + + # Check for tool invocation + tool_call = self.tools.parse_tool_call(message) + if tool_call: + result = self._execute_tool(tool_call) + return self._format_response( + content=result, + tool_used=tool_call["name"], + metadata={"tool_result": True} + ) + + # Route to intelligence layer (Gemma) + return self._route_to_intelligence(message, context) + + def _route_to_intelligence(self, message: str, context: Dict) -> Dict[str, Any]: + """Route message to Gemma via Ollama.""" + from ollama_client import OllamaClient + + client = OllamaClient(base_url=self.ollama_host) + + # Build prompt with context + prompt = self._build_prompt(message, context) + + # Get response from Gemma + response = client.generate( + model="gemma3:4b", + prompt=prompt, + system=self._get_system_prompt() + ) + + # Log response + self.conversation_history.append({ + "role": "assistant", + "content": response.get("response", ""), + "timestamp": self._get_timestamp() + }) + + return self._format_response( + content=response.get("response", ""), + metadata={ + "model": "gemma3:4b", + "tokens_used": response.get("eval_count", 0) + } + ) + + def _execute_tool(self, tool_call: Dict) -> str: + """Execute a tool and return result.""" + return self.tools.execute(tool_call) + + def _build_prompt(self, message: str, context: Dict) -> str: + """Build context-aware prompt for Gemma.""" + history = "\n".join([ + f"{msg['role']}: {msg['content']}" + for msg in self.conversation_history[-5:] # Last 5 messages + ]) + + return f"""Previous conversation: +{history} + +User: {message} + +Assistant:""" + + def _get_system_prompt(self) -> str: + """Get system prompt for Gemma.""" + return """You are the Archon POC, a helpful AI assistant. +Be concise but thorough. Tag your response with #archon-poc.""" + + def _format_response(self, content: str, tool_used: Optional[str] = None, + metadata: Optional[Dict] = None) -> Dict[str, Any]: + """Format response for return to Layer 1.""" + response = { + "content": content, + "status": "success", + "layer": "claw_runtime", + "tag": "#archon-poc" + } + + if tool_used: + response["tool_used"] = tool_used + if metadata: + response["metadata"] = metadata + + return response + + def _get_timestamp(self) -> str: + """Get current timestamp.""" + from datetime import datetime + return datetime.now().isoformat() + + +def main(): + """CLI entry point.""" + harness = ClawHarness() + + if len(sys.argv) > 1: + message = " ".join(sys.argv[1:]) + result = harness.process_message(message) + print(json.dumps(result, indent=2)) + else: + # Interactive mode + print("Archon Harness - Interactive Mode") + print("Type 'exit' to quit\n") + + while True: + try: + message = input("> ") + if message.lower() in ("exit", "quit"): + break + + result = harness.process_message(message) + print(f"\n{result['content']}\n") + + except KeyboardInterrupt: + break + except EOFError: + break + + print("\nGoodbye!") + + +if __name__ == "__main__": + main() diff --git a/archon-poc/runtime/tool_registry.py b/archon-poc/runtime/tool_registry.py new file mode 100644 index 0000000..7e8b931 --- /dev/null +++ b/archon-poc/runtime/tool_registry.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +Tool Registry - Layer 2 Component + +Defines and manages tools available to the Claw runtime. +Tools are executed locally, not sent to the intelligence layer. +""" + +import re +import json +import subprocess +from typing import Dict, Any, List, Callable, Optional +from dataclasses import dataclass +from datetime import datetime + + +@dataclass +class Tool: + name: str + description: str + parameters: Dict[str, Any] + handler: Callable + + +class ToolRegistry: + """ + Registry of available tools for the Claw runtime. + + Tools are pattern-matched from user messages and executed + before routing to the intelligence layer. + """ + + def __init__(self): + self.tools: Dict[str, Tool] = {} + self._register_builtin_tools() + + def _register_builtin_tools(self): + """Register built-in tools.""" + self.register(Tool( + name="time", + description="Get current time", + parameters={}, + handler=self._get_time + )) + + self.register(Tool( + name="status", + description="Get system status", + parameters={}, + handler=self._get_status + )) + + self.register(Tool( + name="echo", + description="Echo a message back", + parameters={"message": "string"}, + handler=self._echo + )) + + self.register(Tool( + name="ollama_list", + description="List available Ollama models", + parameters={}, + handler=self._ollama_list + )) + + def register(self, tool: Tool): + """Register a new tool.""" + self.tools[tool.name] = tool + + def parse_tool_call(self, message: str) -> Optional[Dict[str, Any]]: + """ + Parse a message for tool invocation. + + Patterns: + - /tool_name + - /tool_name param=value + - @tool_name + """ + # Match /tool_name or @tool_name patterns + match = re.match(r'^[/@](\w+)(?:\s+(.+))?$', message.strip()) + if not match: + return None + + tool_name = match.group(1) + args_str = match.group(2) or "" + + if tool_name not in self.tools: + return None + + # Parse parameters + params = {} + if args_str: + # Simple key=value parsing + for pair in args_str.split(): + if '=' in pair: + key, value = pair.split('=', 1) + params[key] = value + else: + # Positional argument as 'message' + params["message"] = args_str + + return { + "name": tool_name, + "parameters": params + } + + def execute(self, tool_call: Dict[str, Any]) -> str: + """Execute a tool call.""" + tool_name = tool_call["name"] + params = tool_call.get("parameters", {}) + + if tool_name not in self.tools: + return f"Error: Unknown tool '{tool_name}'" + + tool = self.tools[tool_name] + + try: + result = tool.handler(**params) + return f"[Tool: {tool_name}]\n{result}" + except Exception as e: + return f"[Tool Error: {tool_name}]\n{str(e)}" + + def list_tools(self) -> List[str]: + """List all available tools.""" + return [ + f"{name}: {tool.description}" + for name, tool in self.tools.items() + ] + + # --- Tool Handlers --- + + def _get_time(self) -> str: + """Get current time.""" + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + def _get_status(self) -> str: + """Get system status.""" + return json.dumps({ + "runtime": "claw", + "version": "1.0.0-poc", + "status": "operational", + "tools_available": len(self.tools) + }, indent=2) + + def _echo(self, message: str = "") -> str: + """Echo a message.""" + return message + + def _ollama_list(self) -> str: + """List Ollama models.""" + try: + result = subprocess.run( + ["curl", "-s", "http://localhost:11434/api/tags"], + capture_output=True, + text=True + ) + data = json.loads(result.stdout) + models = [m["name"] for m in data.get("models", [])] + return f"Available models:\n" + "\n".join(f" - {m}" for m in models) + except Exception as e: + return f"Error listing models: {e}" + + +def main(): + """CLI for testing tool registry.""" + registry = ToolRegistry() + + print("Available tools:") + for tool_info in registry.list_tools(): + print(f" {tool_info}") + + print("\nTest parsing:") + test_messages = [ + "/time", + "/status", + "/echo Hello world", + "/ollama_list", + "Regular message without tool" + ] + + for msg in test_messages: + parsed = registry.parse_tool_call(msg) + if parsed: + result = registry.execute(parsed) + print(f"\n> {msg}") + print(result) + else: + print(f"\n> {msg}") + print("(No tool call detected)") + + +if __name__ == "__main__": + main() diff --git a/archon-poc/test_integration.py b/archon-poc/test_integration.py new file mode 100644 index 0000000..9bb9f46 --- /dev/null +++ b/archon-poc/test_integration.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Integration Test - Archon Architecture + +End-to-end test verifying the full three-layer flow: +1. Thin Hermes Profile (Layer 1) +2. Claw Code Runtime (Layer 2) +3. Gemma via Ollama (Layer 3) +""" + +import sys +import yaml +import json +import unittest +from pathlib import Path +from typing import Dict, Any + +# Add archon-poc to path +sys.path.insert(0, str(Path(__file__).parent)) + +from ollama_client import OllamaClient, OllamaError +from runtime.harness import ClawHarness +from runtime.tool_registry import ToolRegistry + + +class TestArchonArchitecture(unittest.TestCase): + """Integration tests for Archon three-layer architecture.""" + + @classmethod + def setUpClass(cls): + """Set up test fixtures.""" + cls.base_path = Path(__file__).parent + + # === Layer 1: Profile Tests === + + def test_01_profile_exists(self): + """Verify profile.yaml exists and is valid.""" + profile_path = self.base_path / "profile.yaml" + self.assertTrue(profile_path.exists(), "profile.yaml must exist") + + with open(profile_path) as f: + profile = yaml.safe_load(f) + + self.assertIn("name", profile) + self.assertIn("model", profile) + self.assertEqual(profile["model"], "gemma3:4b") + + def test_02_profile_is_thin(self): + """Verify profile is thin (< 50 lines, no logic).""" + profile_path = self.base_path / "profile.yaml" + lines = profile_path.read_text().splitlines() + + self.assertLess(len(lines), 50, "Profile must be < 50 lines") + + # Check for routing section (should exist) + content = "\n".join(lines) + self.assertIn("routing:", content) + + # === Layer 2: Runtime Tests === + + def test_03_harness_exists(self): + """Verify harness.py exists and imports.""" + harness_path = self.base_path / "runtime" / "harness.py" + self.assertTrue(harness_path.exists()) + + # Already imported in setUp, if we got here it works + harness = ClawHarness() + self.assertIsNotNone(harness) + + def test_04_tool_registry(self): + """Verify tool registry works.""" + registry = ToolRegistry() + + # Test tool listing + tools = registry.list_tools() + self.assertGreater(len(tools), 0, "Must have at least one tool") + + # Test tool parsing + parsed = registry.parse_tool_call("/time") + self.assertIsNotNone(parsed) + self.assertEqual(parsed["name"], "time") + + # Test tool execution + result = registry.execute(parsed) + self.assertIn("Tool: time", result) + + def test_05_harness_message_processing(self): + """Test harness can process messages.""" + harness = ClawHarness() + + # Test tool invocation path + result = harness.process_message("/status") + self.assertEqual(result["status"], "success") + self.assertEqual(result["layer"], "claw_runtime") + + # === Layer 3: Ollama Tests === + + def test_06_ollama_connection(self): + """Verify Ollama is reachable.""" + client = OllamaClient() + self.assertTrue(client.health_check(), "Ollama must be running") + + def test_07_ollama_models(self): + """Verify gemma model is available.""" + client = OllamaClient() + models = client.list_models() + + # Check for gemma3:4b (available) or gemma4:4b (target) + gemma_models = [m for m in models if "gemma" in m.lower()] + self.assertGreater(len(gemma_models), 0, "Need at least one Gemma model") + + def test_08_ollama_generation(self): + """Test basic generation through Ollama.""" + client = OllamaClient() + + response = client.generate( + prompt="Say 'Archon test passed' and nothing else.", + system="You are a test assistant. Be brief." + ) + + self.assertIn("response", response) + self.assertIsInstance(response["response"], str) + self.assertGreater(len(response["response"]), 0) + + # === End-to-End Tests === + + def test_09_full_flow_tool_path(self): + """Test full flow: Profile -> Runtime -> Tool -> Response.""" + harness = ClawHarness() + + # Simulate what the thin profile would do - just route to harness + result = harness.process_message("/echo Integration test") + + self.assertEqual(result["status"], "success") + self.assertIn("content", result) + self.assertIn("Integration test", result["content"]) + self.assertEqual(result["layer"], "claw_runtime") + self.assertEqual(result["tag"], "#archon-poc") + + def test_10_full_flow_intelligence_path(self): + """Test full flow: Profile -> Runtime -> Ollama -> Response.""" + harness = ClawHarness() + + # This routes through Ollama + result = harness.process_message("Say 'Archon intelligence layer active'") + + self.assertEqual(result["status"], "success") + self.assertIn("content", result) + self.assertIn("layer", result) + self.assertIn("metadata", result) + + def test_11_conversation_history(self): + """Test conversation history tracking.""" + harness = ClawHarness() + + # Send multiple messages + harness.process_message("Message 1") + harness.process_message("Message 2") + + # Check history + self.assertEqual(len(harness.conversation_history), 4) # 2 user + 2 assistant + + # === Architecture Compliance Tests === + + def test_12_all_files_exist(self): + """Verify all required files exist.""" + required_files = [ + "README.md", + "profile.yaml", + "runtime/harness.py", + "runtime/tool_registry.py", + "ollama_client.py", + "test_integration.py" + ] + + for file_path in required_files: + full_path = self.base_path / file_path + self.assertTrue( + full_path.exists(), + f"Required file missing: {file_path}" + ) + + +def run_tests(): + """Run all integration tests.""" + print("=" * 60) + print("ARCHON ARCHITECTURE - INTEGRATION TEST SUITE") + print("=" * 60) + print() + + # Create test suite + loader = unittest.TestLoader() + suite = loader.loadTestsFromTestCase(TestArchonArchitecture) + + # Run with verbose output + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + print() + print("=" * 60) + if result.wasSuccessful(): + print("✓ ALL TESTS PASSED") + print("Archon Architecture POC is working correctly!") + print("=" * 60) + return 0 + else: + print("✗ SOME TESTS FAILED") + print("See output above for details") + print("=" * 60) + return 1 + + +if __name__ == "__main__": + sys.exit(run_tests()) From 19fae5a6e52ba3bb13c7142d359c6f69f8bcab06 Mon Sep 17 00:00:00 2001 From: Ezra Date: Thu, 2 Apr 2026 19:47:15 +0000 Subject: [PATCH 2/2] Add .gitignore for Python artifacts --- archon-poc/.gitignore | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 archon-poc/.gitignore diff --git a/archon-poc/.gitignore b/archon-poc/.gitignore new file mode 100644 index 0000000..4fa861d --- /dev/null +++ b/archon-poc/.gitignore @@ -0,0 +1,16 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info/ +dist/ +build/ +.pytest_cache/ +.coverage +htmlcov/ +.env +.venv +venv/