diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..064c06e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: pip install -e ".[dev]" + + - name: Lint (ruff) + run: ruff check . + + - name: Test (pytest) + run: pytest tests/ diff --git a/.gitignore b/.gitignore index 27a8a8f..5f94031 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,12 @@ apps/*/output/ # Legacy directories (use canonical paths in apps/) whatsapp_notion_bot/ -interfaces/web/ deploy/ monarch_phase1/ +# Local CI verification venv (not committed) +.venv-ci/ + # Binary/docs *.docx *.pdf diff --git a/agents/architecture.py b/agents/architecture.py index 466f6be..b101227 100644 --- a/agents/architecture.py +++ b/agents/architecture.py @@ -1,4 +1,4 @@ -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import OPUS_MODEL, BaseAgent from core.task import Task _SYSTEM = """You are the Architecture Agent for Monarch AI. @@ -26,7 +26,7 @@ class ArchitectureAgent(BaseAgent): name = "architecture" display_name = "Carla - Arquitetura" - model = SONNET_MODEL + model = OPUS_MODEL system_prompt = _SYSTEM async def build_user_message(self, task: Task) -> str: @@ -43,7 +43,7 @@ async def run(self, task: Task): result = await super().run(task) task.architecture = result.output task.add_history( - agent=self.label, + agent=self.name, action="architecture_complete", detail=f"new_files={len(result.output.get('new_files', []))} modified={len(result.output.get('modified_files', []))}", ) diff --git a/agents/base.py b/agents/base.py index 771df5f..4c8843a 100644 --- a/agents/base.py +++ b/agents/base.py @@ -3,7 +3,6 @@ import logging import os import re -import sys from abc import ABC, abstractmethod from dataclasses import dataclass, field from pathlib import Path @@ -251,7 +250,7 @@ async def _handle_ask_user(self, question: str) -> str: print(f"{'='*60}\n") if self._current_task: self._current_task.add_history( - agent=self.label, + agent=self.name, action="asked_user", detail=f"Q: {question[:80]} | A: {answer[:80]}", ) @@ -346,7 +345,7 @@ async def run(self, task: Task) -> AgentResult: interactive = os.environ.get("MONARCH_INTERACTIVE", "0") == "1" or self._ask_user_fn is not None extra = [_ASK_USER_TOOL] if interactive else [] all_tools = (self.tools or []) + extra - message = await self._call_claude_api(messages, all_tools or None) + message = await self._call_claude(messages, all_tools or None) while message.stop_reason == "tool_use": message = await self._handle_tool_use(message, messages) diff --git a/agents/deploy.py b/agents/deploy.py index 12960d0..e3ef600 100644 --- a/agents/deploy.py +++ b/agents/deploy.py @@ -1,8 +1,7 @@ import json import logging -from typing import Any -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import SONNET_MODEL, BaseAgent from core.task import Task from tools.github_tools import GitHubTools @@ -124,7 +123,7 @@ async def run(self, task: Task): task.deploy_report = result.output task.add_history( - agent=self.label, + agent=self.name, action="deploy_config_complete", detail=( f"type={result.output.get('project_type')} " diff --git a/agents/devils_advocate.py b/agents/devils_advocate.py index dd28fc5..2cfa567 100644 --- a/agents/devils_advocate.py +++ b/agents/devils_advocate.py @@ -1,4 +1,4 @@ -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import OPUS_MODEL, BaseAgent from core.task import Task _SYSTEM = """You are the Devil's Advocate Agent for Monarch AI. @@ -29,7 +29,7 @@ class DevilsAdvocateAgent(BaseAgent): name = "devils_advocate" display_name = "Ewerton - Advogado do Diabo" - model = SONNET_MODEL + model = OPUS_MODEL system_prompt = _SYSTEM async def build_user_message(self, task: Task) -> str: @@ -51,7 +51,7 @@ async def run(self, task: Task): approved = result.output.get("approved", False) issues_count = len(result.output.get("issues", [])) task.add_history( - agent=self.label, + agent=self.name, action="review_complete", detail=f"approved={approved} issues={issues_count}", ) diff --git a/agents/discovery.py b/agents/discovery.py index 48607e3..59a3a5d 100644 --- a/agents/discovery.py +++ b/agents/discovery.py @@ -1,4 +1,4 @@ -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import OPUS_MODEL, BaseAgent from core.task import Task _SYSTEM = """You are the Discovery Agent for Monarch AI — a multi-agent system that @@ -24,7 +24,7 @@ class DiscoveryAgent(BaseAgent): name = "discovery" display_name = "Alice - Descoberta" - model = SONNET_MODEL + model = OPUS_MODEL system_prompt = _SYSTEM async def build_user_message(self, task: Task) -> str: @@ -38,7 +38,7 @@ async def run(self, task: Task): # Persist the structured requirements back onto the task task.requirements = result.output task.add_history( - agent=self.label, + agent=self.name, action="discovery_complete", detail=f"complexity={result.output.get('complexity')} confidence={result.confidence:.2f}", ) diff --git a/agents/documentation.py b/agents/documentation.py index 5f42b7e..5d98146 100644 --- a/agents/documentation.py +++ b/agents/documentation.py @@ -1,8 +1,7 @@ import json import logging -from typing import Any -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import HAIKU_MODEL, BaseAgent from core.task import Task from tools.github_tools import GitHubTools @@ -33,7 +32,7 @@ class DocumentationAgent(BaseAgent): name = "documentation" display_name = "Karla - Documentação" - model = SONNET_MODEL + model = HAIKU_MODEL system_prompt = _SYSTEM def __init__(self) -> None: @@ -54,7 +53,7 @@ async def build_user_message(self, task: Task) -> str: async def run(self, task: Task): result = await super().run(task) task.add_history( - agent=self.label, + agent=self.name, action="documentation_complete", detail=f"changelog_entry={'yes' if result.output.get('changelog_entry') else 'no'}", ) diff --git a/agents/implementer.py b/agents/implementer.py index 8d7e8e0..0dc7265 100644 --- a/agents/implementer.py +++ b/agents/implementer.py @@ -2,11 +2,11 @@ import logging from typing import Any -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import OPUS_MODEL, BaseAgent from config import config from core.task import Task -from tools.github_tools import GitHubTools from tools.fs_tools import FsTools +from tools.github_tools import GitHubTools logger = logging.getLogger(__name__) @@ -53,7 +53,7 @@ class ImplementerAgent(BaseAgent): name = "implementer" display_name = "Fernanda - Implementação" - model = config.implementer_model or SONNET_MODEL + model = config.implementer_model or OPUS_MODEL max_tokens = config.implementer_max_tokens system_prompt = _SYSTEM @@ -63,7 +63,7 @@ def __init__(self) -> None: self._fs = FsTools() @property - def system_prompt(self) -> str: # type: ignore[override] + def system_prompt(self) -> str: # type: ignore[override] # noqa: F811 (property overrides the class-level default for local mode) return _SYSTEM_LOCAL if config.local_mode else _SYSTEM @property @@ -236,7 +236,7 @@ async def run(self, task: Task): files = result.output.get("files_written", []) task.add_history( - agent=self.label, + agent=self.name, action="implementation_complete", detail=f"files_written={len(files)}", ) diff --git a/agents/observability.py b/agents/observability.py index 21cbc62..a9a35b1 100644 --- a/agents/observability.py +++ b/agents/observability.py @@ -1,8 +1,7 @@ import json import logging -from typing import Any -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import HAIKU_MODEL, BaseAgent from core.task import Task logger = logging.getLogger(__name__) @@ -31,7 +30,7 @@ class ObservabilityAgent(BaseAgent): name = "observability" display_name = "Lucas - Observabilidade" - model = SONNET_MODEL + model = HAIKU_MODEL system_prompt = _SYSTEM async def build_user_message(self, task: Task) -> str: @@ -48,7 +47,7 @@ async def run(self, task: Task): result = await super().run(task) metrics_count = len(result.output.get("metrics_to_add", [])) task.add_history( - agent=self.label, + agent=self.name, action="observability_complete", detail=f"metrics={metrics_count} alerts={len(result.output.get('alerts_to_configure', []))}", ) diff --git a/agents/planning.py b/agents/planning.py index 200929d..c293886 100644 --- a/agents/planning.py +++ b/agents/planning.py @@ -1,4 +1,4 @@ -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import OPUS_MODEL, BaseAgent from core.task import Task _SYSTEM = """You are the Planning Agent for Monarch AI. @@ -31,7 +31,7 @@ class PlanningAgent(BaseAgent): name = "planning" display_name = "Diego - Planejamento" - model = SONNET_MODEL + model = OPUS_MODEL system_prompt = _SYSTEM async def build_user_message(self, task: Task) -> str: @@ -51,7 +51,7 @@ async def run(self, task: Task): if result.output.get("branch_name"): task.branch_name = result.output["branch_name"] task.add_history( - agent=self.label, + agent=self.name, action="planning_complete", detail=f"steps={len(task.plan)} branch={task.branch_name}", ) diff --git a/agents/prioritization.py b/agents/prioritization.py index 19e6b6e..57e1466 100644 --- a/agents/prioritization.py +++ b/agents/prioritization.py @@ -1,4 +1,4 @@ -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import SONNET_MODEL, BaseAgent from core.task import Task _SYSTEM = """You are the Prioritization Agent for Monarch AI. @@ -38,7 +38,7 @@ async def run(self, task: Task): result = await super().run(task) task.priority = result.output.get("priority", "medium") task.add_history( - agent=self.label, + agent=self.name, action="prioritization_complete", detail=f"priority={task.priority} score={result.output.get('priority_score')}", ) diff --git a/agents/reviewer.py b/agents/reviewer.py index 01dd07e..8353b91 100644 --- a/agents/reviewer.py +++ b/agents/reviewer.py @@ -1,8 +1,6 @@ -import json import logging -from typing import Any -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import OPUS_MODEL, BaseAgent from core.task import Task from tools.github_tools import GitHubTools @@ -36,7 +34,7 @@ class ReviewerAgent(BaseAgent): name = "reviewer" display_name = "Helena - Revisão" - model = SONNET_MODEL + model = OPUS_MODEL system_prompt = _SYSTEM def __init__(self) -> None: @@ -65,7 +63,7 @@ async def run(self, task: Task): task.review_report = result.output approved = result.output.get("approved", False) task.add_history( - agent=self.label, + agent=self.name, action="review_complete", detail=f"approved={approved} quality={result.output.get('overall_quality')}", ) diff --git a/agents/security.py b/agents/security.py index 7df3a68..b8ac5ce 100644 --- a/agents/security.py +++ b/agents/security.py @@ -1,8 +1,7 @@ import json import logging -from typing import Any -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import OPUS_MODEL, BaseAgent from core.task import Task logger = logging.getLogger(__name__) @@ -36,7 +35,7 @@ class SecurityAgent(BaseAgent): name = "security" display_name = "Igor - Segurança" - model = SONNET_MODEL + model = OPUS_MODEL system_prompt = _SYSTEM async def build_user_message(self, task: Task) -> str: @@ -56,7 +55,7 @@ async def run(self, task: Task): approved = result.output.get("approved", False) risk = result.output.get("risk_level", "unknown") task.add_history( - agent=self.label, + agent=self.name, action="security_review_complete", detail=f"approved={approved} risk={risk}", ) diff --git a/agents/testing.py b/agents/testing.py index f9b7967..36fc80d 100644 --- a/agents/testing.py +++ b/agents/testing.py @@ -1,8 +1,7 @@ import json import logging -from typing import Any -from agents.base import BaseAgent, SONNET_MODEL +from agents.base import SONNET_MODEL, BaseAgent from core.task import Task from tools.code_tools import CodeTools @@ -50,7 +49,7 @@ async def run(self, task: Task): task.test_results = result.output all_passed = result.output.get("all_passed", False) task.add_history( - agent=self.label, + agent=self.name, action="testing_complete", detail=f"all_passed={all_passed}", ) diff --git a/apps/achadinhos/achadinhos/cli.py b/apps/achadinhos/achadinhos/cli.py index 5ceedf4..eb67947 100644 --- a/apps/achadinhos/achadinhos/cli.py +++ b/apps/achadinhos/achadinhos/cli.py @@ -1,7 +1,6 @@ """CLI para Achadinhos.""" import csv import json -import sys from pathlib import Path import rich.console @@ -9,7 +8,7 @@ import typer from achadinhos.config import settings -from achadinhos.models import ProductCandidate, ProductShortlist +from achadinhos.models import ProductCandidate from achadinhos.pipeline import build_shortlist, render_shortlist_markdown, score_candidate app = typer.Typer(help="Achadinhos — descubra, catogue e priorize produtos.") diff --git a/apps/achadinhos/tests/test_models.py b/apps/achadinhos/tests/test_models.py index 3a9f8d5..3ad5b86 100644 --- a/apps/achadinhos/tests/test_models.py +++ b/apps/achadinhos/tests/test_models.py @@ -1,5 +1,5 @@ """Testes para modelos do Achadinhos.""" -from achadinhos.models import ProductCandidate, ScoredCandidate +from achadinhos.models import ProductCandidate from achadinhos.pipeline import score_candidate diff --git a/apps/canal-dark/canal_dark/__init__.py b/apps/canal-dark/canal_dark/__init__.py index 206f398..6a4a051 100644 --- a/apps/canal-dark/canal_dark/__init__.py +++ b/apps/canal-dark/canal_dark/__init__.py @@ -1,5 +1,10 @@ """Canal Dark — produza pautas e roteiros para canais dark.""" from canal_dark.models import ChannelBrief, ScriptDraft, TopicIdea -from canal_dark.pipeline import build_script_draft, build_topic_backlog, choose_priority_topic, render_script +from canal_dark.pipeline import ( + build_script_draft, + build_topic_backlog, + choose_priority_topic, + render_script, +) __all__ = ["ChannelBrief", "TopicIdea", "ScriptDraft", "build_topic_backlog", "choose_priority_topic", "build_script_draft", "render_script"] diff --git a/apps/canal-dark/canal_dark/__main__.py b/apps/canal-dark/canal_dark/__main__.py index 9e13e15..24daf1c 100644 --- a/apps/canal-dark/canal_dark/__main__.py +++ b/apps/canal-dark/canal_dark/__main__.py @@ -1,4 +1,5 @@ """Permite executar: python -m canal_dark.""" from canal_dark.cli import app + if __name__ == "__main__": app() diff --git a/apps/canal-dark/canal_dark/cli.py b/apps/canal-dark/canal_dark/cli.py index bc4a61a..2250398 100644 --- a/apps/canal-dark/canal_dark/cli.py +++ b/apps/canal-dark/canal_dark/cli.py @@ -1,13 +1,17 @@ """CLI para Canal Dark.""" -import json from pathlib import Path import rich.console import rich.table import typer -from canal_dark.models import ChannelBrief, TopicIdea -from canal_dark.pipeline import build_script_draft, build_topic_backlog, choose_priority_topic, render_script +from canal_dark.models import ChannelBrief +from canal_dark.pipeline import ( + build_script_draft, + build_topic_backlog, + choose_priority_topic, + render_script, +) app = typer.Typer(help="Canal Dark — produza pautas e roteiros para canais dark.") console = rich.console.Console() diff --git a/apps/canal-dark/tests/test_models.py b/apps/canal-dark/tests/test_models.py index 908c481..82ff02c 100644 --- a/apps/canal-dark/tests/test_models.py +++ b/apps/canal-dark/tests/test_models.py @@ -1,6 +1,6 @@ """Testes para modelos do Canal Dark.""" from canal_dark.models import ChannelBrief, ScriptDraft, TopicIdea -from canal_dark.pipeline import build_topic_backlog, choose_priority_topic, build_script_draft +from canal_dark.pipeline import build_script_draft, build_topic_backlog, choose_priority_topic def test_channel_brief_defaults(): diff --git a/apps/canal-dark/tests/test_pipeline.py b/apps/canal-dark/tests/test_pipeline.py index 2f7dd3d..2ef92dd 100644 --- a/apps/canal-dark/tests/test_pipeline.py +++ b/apps/canal-dark/tests/test_pipeline.py @@ -1,7 +1,11 @@ import pytest - from canal_dark.models import ChannelBrief -from canal_dark.pipeline import build_script_draft, build_topic_backlog, choose_priority_topic, render_script +from canal_dark.pipeline import ( + build_script_draft, + build_topic_backlog, + choose_priority_topic, + render_script, +) def test_build_topic_backlog_creates_one_topic_per_theme(): diff --git a/apps/instagram-automation/instagram_automation/pipeline.py b/apps/instagram-automation/instagram_automation/pipeline.py index 5becdec..258ab22 100644 --- a/apps/instagram-automation/instagram_automation/pipeline.py +++ b/apps/instagram-automation/instagram_automation/pipeline.py @@ -4,7 +4,6 @@ from instagram_automation.models import ContentAngle, PublicationChecklist, QueueItem, ResearchInput - DEFAULT_FORMATS = ["reel curto", "carrossel", "post de autoridade"] DEFAULT_REVIEW_NOTES = [ "Validar se o conteudo respeita as politicas da plataforma.", diff --git a/apps/instagram-automation/tests/test_models.py b/apps/instagram-automation/tests/test_models.py index 90fc533..bce48fb 100644 --- a/apps/instagram-automation/tests/test_models.py +++ b/apps/instagram-automation/tests/test_models.py @@ -1,5 +1,5 @@ """Testes para modelos do Instagram Automation.""" -from instagram_automation.models import ContentAngle, QueueItem, ResearchInput +from instagram_automation.models import QueueItem, ResearchInput from instagram_automation.pipeline import build_content_angles, choose_queue_item diff --git a/apps/monarch-core/app/api/routes/dashboard.py b/apps/monarch-core/app/api/routes/dashboard.py index 1b8793a..5bea0f4 100644 --- a/apps/monarch-core/app/api/routes/dashboard.py +++ b/apps/monarch-core/app/api/routes/dashboard.py @@ -5,7 +5,11 @@ from app.db.session import get_db_session from app.schemas.dashboard import ActivityItem, DashboardOverview, PerformanceItem -from app.services.dashboard_service import get_dashboard_activity, get_dashboard_overview, get_dashboard_performance +from app.services.dashboard_service import ( + get_dashboard_activity, + get_dashboard_overview, + get_dashboard_performance, +) router = APIRouter() diff --git a/apps/monarch-core/app/main.py b/apps/monarch-core/app/main.py index 44cef60..23379db 100644 --- a/apps/monarch-core/app/main.py +++ b/apps/monarch-core/app/main.py @@ -1,6 +1,6 @@ -from contextlib import asynccontextmanager import time from collections import defaultdict, deque +from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware diff --git a/apps/monarch-core/app/models/__init__.py b/apps/monarch-core/app/models/__init__.py index cf57384..ed8773d 100644 --- a/apps/monarch-core/app/models/__init__.py +++ b/apps/monarch-core/app/models/__init__.py @@ -1,4 +1,5 @@ from app.models.agent_profile import AgentProfile +from app.models.approval import Approval from app.models.business_unit import BusinessUnit from app.models.execution import Execution from app.models.idea import Idea @@ -6,7 +7,6 @@ from app.models.project_metric import ProjectMetric from app.models.roadmap_item import RoadmapItem from app.models.task import Task -from app.models.approval import Approval __all__ = [ "AgentProfile", diff --git a/apps/monarch-core/app/models/agent_profile.py b/apps/monarch-core/app/models/agent_profile.py index 0ce3886..ac4a01d 100644 --- a/apps/monarch-core/app/models/agent_profile.py +++ b/apps/monarch-core/app/models/agent_profile.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, String, Text from sqlalchemy.orm import Mapped, mapped_column @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class AgentProfile(Base): diff --git a/apps/monarch-core/app/models/approval.py b/apps/monarch-core/app/models/approval.py index ab752df..7a6ad37 100644 --- a/apps/monarch-core/app/models/approval.py +++ b/apps/monarch-core/app/models/approval.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, ForeignKey, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class Approval(Base): diff --git a/apps/monarch-core/app/models/business_unit.py b/apps/monarch-core/app/models/business_unit.py index 76656f1..a6d5dbf 100644 --- a/apps/monarch-core/app/models/business_unit.py +++ b/apps/monarch-core/app/models/business_unit.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class BusinessUnit(Base): diff --git a/apps/monarch-core/app/models/execution.py b/apps/monarch-core/app/models/execution.py index 0b31df0..a89fa2c 100644 --- a/apps/monarch-core/app/models/execution.py +++ b/apps/monarch-core/app/models/execution.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, ForeignKey, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class Execution(Base): diff --git a/apps/monarch-core/app/models/idea.py b/apps/monarch-core/app/models/idea.py index ca1d6f9..b7b1970 100644 --- a/apps/monarch-core/app/models/idea.py +++ b/apps/monarch-core/app/models/idea.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, ForeignKey, Numeric, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class Idea(Base): diff --git a/apps/monarch-core/app/models/project.py b/apps/monarch-core/app/models/project.py index 67cc694..2635039 100644 --- a/apps/monarch-core/app/models/project.py +++ b/apps/monarch-core/app/models/project.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, ForeignKey, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class Project(Base): diff --git a/apps/monarch-core/app/models/project_metric.py b/apps/monarch-core/app/models/project_metric.py index f3de816..5065626 100644 --- a/apps/monarch-core/app/models/project_metric.py +++ b/apps/monarch-core/app/models/project_metric.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, Float, ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class ProjectMetric(Base): diff --git a/apps/monarch-core/app/models/roadmap_item.py b/apps/monarch-core/app/models/roadmap_item.py index 6029a8a..895e80b 100644 --- a/apps/monarch-core/app/models/roadmap_item.py +++ b/apps/monarch-core/app/models/roadmap_item.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import DateTime, ForeignKey, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class RoadmapItem(Base): diff --git a/apps/monarch-core/app/models/task.py b/apps/monarch-core/app/models/task.py index fa37d51..21a0241 100644 --- a/apps/monarch-core/app/models/task.py +++ b/apps/monarch-core/app/models/task.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) class Task(Base): diff --git a/apps/monarch-core/app/seed/initial_data.py b/apps/monarch-core/app/seed/initial_data.py index 15c807e..bfe2d70 100644 --- a/apps/monarch-core/app/seed/initial_data.py +++ b/apps/monarch-core/app/seed/initial_data.py @@ -1,7 +1,7 @@ from sqlalchemy import select -from app.models.approval import Approval from app.db.session import AsyncSessionLocal +from app.models.approval import Approval from app.models.business_unit import BusinessUnit from app.models.project import Project from app.models.project_metric import ProjectMetric diff --git a/apps/monarch-core/app/services/approval_service.py b/apps/monarch-core/app/services/approval_service.py index f7b7b9c..7876a28 100644 --- a/apps/monarch-core/app/services/approval_service.py +++ b/apps/monarch-core/app/services/approval_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import UUID from sqlalchemy import Select, select @@ -65,7 +65,7 @@ async def _decide_approval( approval.status = decision approval.decision = decision approval.decided_by = decided_by - approval.decided_at = datetime.now(timezone.utc) + approval.decided_at = datetime.now(UTC) await db.commit() await db.refresh(approval) return approval diff --git a/apps/monarch-core/app/services/project_service.py b/apps/monarch-core/app/services/project_service.py index 560bdd5..8faeb0d 100644 --- a/apps/monarch-core/app/services/project_service.py +++ b/apps/monarch-core/app/services/project_service.py @@ -11,7 +11,6 @@ from app.models.task import Task from app.schemas.project import ProjectCreate, ProjectUpdate - REPO_ROOT = Path(__file__).resolve().parents[4] IMPLEMENTATION_BLUEPRINTS: dict[str, dict[str, object]] = { @@ -19,7 +18,7 @@ "canonical_path": ".", "deliverable": "Hub, orchestracao multiagente e governanca do portfolio.", "module_labels": ["hub", "monarch-core", "monarch-web", "orchestrator"], - "test_targets": ["tests", "apps/monarch-core", "apps/monarch-web"], + "test_targets": ["tests", "apps/monarch-core", "interfaces/web"], }, "whatsapp-notion-bot": { "canonical_path": "apps/whatsapp-notion-bot", diff --git a/apps/monarch-core/migrations/env.py b/apps/monarch-core/migrations/env.py index 38c3a7e..ae00fe7 100644 --- a/apps/monarch-core/migrations/env.py +++ b/apps/monarch-core/migrations/env.py @@ -3,11 +3,10 @@ from logging.config import fileConfig from alembic import context -from sqlalchemy import engine_from_config, pool - from app.core.config import get_settings from app.db.base import Base from app.models import business_unit, idea, project # noqa: F401 +from sqlalchemy import engine_from_config, pool config = context.config settings = get_settings() diff --git a/apps/monarch-core/migrations/versions/0001_initial_schema.py b/apps/monarch-core/migrations/versions/0001_initial_schema.py index b4903a0..0da62d9 100644 --- a/apps/monarch-core/migrations/versions/0001_initial_schema.py +++ b/apps/monarch-core/migrations/versions/0001_initial_schema.py @@ -1,8 +1,7 @@ """Initial schema for Monarch Phase 1 core API.""" -from alembic import op import sqlalchemy as sa - +from alembic import op revision = "0001_initial_schema" down_revision = None diff --git a/apps/monarch-core/migrations/versions/0002_tasks_and_approvals.py b/apps/monarch-core/migrations/versions/0002_tasks_and_approvals.py index b9daf63..94dd5f8 100644 --- a/apps/monarch-core/migrations/versions/0002_tasks_and_approvals.py +++ b/apps/monarch-core/migrations/versions/0002_tasks_and_approvals.py @@ -1,8 +1,7 @@ """Add tasks and approvals tables.""" -from alembic import op import sqlalchemy as sa - +from alembic import op revision = "0002_tasks_and_approvals" down_revision = "0001_initial_schema" diff --git a/apps/monarch-core/migrations/versions/0003_executions.py b/apps/monarch-core/migrations/versions/0003_executions.py index 4f07a40..c6882ed 100644 --- a/apps/monarch-core/migrations/versions/0003_executions.py +++ b/apps/monarch-core/migrations/versions/0003_executions.py @@ -1,8 +1,7 @@ """Add executions table.""" -from alembic import op import sqlalchemy as sa - +from alembic import op revision = "0003_executions" down_revision = "0002_tasks_and_approvals" diff --git a/apps/monarch-core/migrations/versions/0004_project_metrics.py b/apps/monarch-core/migrations/versions/0004_project_metrics.py index ff6a3c2..1a05f54 100644 --- a/apps/monarch-core/migrations/versions/0004_project_metrics.py +++ b/apps/monarch-core/migrations/versions/0004_project_metrics.py @@ -1,8 +1,7 @@ """Add project metrics table.""" -from alembic import op import sqlalchemy as sa - +from alembic import op revision = "0004_project_metrics" down_revision = "0003_executions" diff --git a/apps/monarch-core/migrations/versions/0005_agents_and_roadmap.py b/apps/monarch-core/migrations/versions/0005_agents_and_roadmap.py index bc4c849..5e51fad 100644 --- a/apps/monarch-core/migrations/versions/0005_agents_and_roadmap.py +++ b/apps/monarch-core/migrations/versions/0005_agents_and_roadmap.py @@ -1,8 +1,7 @@ """Add agent profiles and roadmap items tables.""" -from alembic import op import sqlalchemy as sa - +from alembic import op revision = "0005_agents_and_roadmap" down_revision = "0004_project_metrics" diff --git a/apps/monarch-runtime/cli.py b/apps/monarch-runtime/cli.py index 7f083ce..fb4381e 100644 --- a/apps/monarch-runtime/cli.py +++ b/apps/monarch-runtime/cli.py @@ -2,6 +2,5 @@ from interfaces.cli import main - if __name__ == "__main__": main() diff --git a/apps/monarch-runtime/main.py b/apps/monarch-runtime/main.py index e9d32cb..0b350b5 100644 --- a/apps/monarch-runtime/main.py +++ b/apps/monarch-runtime/main.py @@ -4,6 +4,5 @@ from main import main as monarch_main - if __name__ == "__main__": asyncio.run(monarch_main()) diff --git a/apps/pdf-factory/pdf_factory/__init__.py b/apps/pdf-factory/pdf_factory/__init__.py index 0a4b826..9950ae4 100644 --- a/apps/pdf-factory/pdf_factory/__init__.py +++ b/apps/pdf-factory/pdf_factory/__init__.py @@ -1,6 +1,11 @@ """PDF Factory — transforme briefs em ativos digitais profissionais.""" from pdf_factory.models import BriefInput, DocumentPlan, DocumentSection -from pdf_factory.pipeline import build_document_plan, build_output_filename, render_markdown, slugify +from pdf_factory.pipeline import ( + build_document_plan, + build_output_filename, + render_markdown, + slugify, +) __all__ = [ "BriefInput", diff --git a/apps/pdf-factory/pdf_factory/cli.py b/apps/pdf-factory/pdf_factory/cli.py index 3442d6c..3d886fa 100644 --- a/apps/pdf-factory/pdf_factory/cli.py +++ b/apps/pdf-factory/pdf_factory/cli.py @@ -1,6 +1,5 @@ """CLI para PDF Factory.""" import json -import sys from pathlib import Path import rich.console diff --git a/apps/pdf-factory/pdf_factory/pipeline.py b/apps/pdf-factory/pdf_factory/pipeline.py index be48ea7..e8dd7f5 100644 --- a/apps/pdf-factory/pdf_factory/pipeline.py +++ b/apps/pdf-factory/pdf_factory/pipeline.py @@ -4,7 +4,6 @@ from pdf_factory.models import BriefInput, DocumentPlan, DocumentSection - DEFAULT_SECTION_HEADINGS = [ "Panorama inicial", "Fundamentos", diff --git a/apps/pdf-factory/pdf_factory/render.py b/apps/pdf-factory/pdf_factory/render.py index 52cea94..f823ad2 100644 --- a/apps/pdf-factory/pdf_factory/render.py +++ b/apps/pdf-factory/pdf_factory/render.py @@ -54,8 +54,6 @@ def render_html(plan: DocumentPlan, template: str = "minimal") -> str: def render_pdf(html_content: str, output_path: Path) -> bool: """Gera PDF a partir de HTML. Retorna True se bem-sucedido.""" try: - import weasyprint - from weasyprint import HTML # type: ignore[import] HTML(string=html_content).write_pdf(str(output_path)) diff --git a/apps/pdf-factory/tests/test_models.py b/apps/pdf-factory/tests/test_models.py index c025f49..3c60c05 100644 --- a/apps/pdf-factory/tests/test_models.py +++ b/apps/pdf-factory/tests/test_models.py @@ -1,9 +1,8 @@ """Testes para modelos.""" import pytest -from pydantic import ValidationError - -from pdf_factory.models import BriefInput, DocumentSection, DocumentPlan +from pdf_factory.models import BriefInput, DocumentPlan, DocumentSection from pdf_factory.pipeline import build_document_plan +from pydantic import ValidationError def test_brief_input_valid(): diff --git a/apps/pdf-factory/tests/test_pipeline.py b/apps/pdf-factory/tests/test_pipeline.py index 3a74ee9..67bb7c4 100644 --- a/apps/pdf-factory/tests/test_pipeline.py +++ b/apps/pdf-factory/tests/test_pipeline.py @@ -1,5 +1,10 @@ from pdf_factory.models import BriefInput -from pdf_factory.pipeline import build_document_plan, build_output_filename, render_markdown, slugify +from pdf_factory.pipeline import ( + build_document_plan, + build_output_filename, + render_markdown, + slugify, +) def test_slugify_normalizes_title(): diff --git a/apps/pdf-factory/tests/test_render.py b/apps/pdf-factory/tests/test_render.py index 1f54bdd..a1e9335 100644 --- a/apps/pdf-factory/tests/test_render.py +++ b/apps/pdf-factory/tests/test_render.py @@ -2,7 +2,7 @@ import tempfile from pathlib import Path -from pdf_factory.models import BriefInput, DocumentPlan, DocumentSection +from pdf_factory.models import BriefInput, DocumentPlan from pdf_factory.pipeline import build_document_plan from pdf_factory.render import render_html, render_pdf diff --git a/apps/solo-leveling-lab/solo_leveling_lab/__init__.py b/apps/solo-leveling-lab/solo_leveling_lab/__init__.py index ac66887..2b83446 100644 --- a/apps/solo-leveling-lab/solo_leveling_lab/__init__.py +++ b/apps/solo-leveling-lab/solo_leveling_lab/__init__.py @@ -1,5 +1,9 @@ """Solo Leveling Lab — experimente, produza e registre aprendizados.""" from solo_leveling_lab.models import ExperimentCycle, LabBrief, LabExperiment -from solo_leveling_lab.pipeline import build_experiment, build_experiment_cycle, render_experiment_plan +from solo_leveling_lab.pipeline import ( + build_experiment, + build_experiment_cycle, + render_experiment_plan, +) __all__ = ["LabBrief", "LabExperiment", "ExperimentCycle", "build_experiment", "build_experiment_cycle", "render_experiment_plan"] diff --git a/apps/solo-leveling-lab/solo_leveling_lab/__main__.py b/apps/solo-leveling-lab/solo_leveling_lab/__main__.py index e46bef3..6b80924 100644 --- a/apps/solo-leveling-lab/solo_leveling_lab/__main__.py +++ b/apps/solo-leveling-lab/solo_leveling_lab/__main__.py @@ -1,4 +1,5 @@ """Permite executar: python -m solo_leveling_lab.""" from solo_leveling_lab.cli import app + if __name__ == "__main__": app() diff --git a/apps/solo-leveling-lab/solo_leveling_lab/cli.py b/apps/solo-leveling-lab/solo_leveling_lab/cli.py index a9e57b0..622276a 100644 --- a/apps/solo-leveling-lab/solo_leveling_lab/cli.py +++ b/apps/solo-leveling-lab/solo_leveling_lab/cli.py @@ -6,7 +6,11 @@ import typer from solo_leveling_lab.models import LabBrief -from solo_leveling_lab.pipeline import build_experiment, build_experiment_cycle, render_experiment_plan +from solo_leveling_lab.pipeline import ( + build_experiment, + build_experiment_cycle, + render_experiment_plan, +) app = typer.Typer(help="Solo Leveling Lab — experimente, produza e registre aprendizados.") console = rich.console.Console() @@ -43,7 +47,7 @@ def experiment( for item in experiment.artifact_outline: console.print(f" - {item}") - console.print(f"\n[bold]Proximo ciclo:[/bold]") + console.print("\n[bold]Proximo ciclo:[/bold]") for i, step in enumerate(cycle.next_steps, 1): console.print(f" {i}. {step}") diff --git a/apps/solo-leveling-lab/tests/test_pipeline.py b/apps/solo-leveling-lab/tests/test_pipeline.py index 53186cf..6952fea 100644 --- a/apps/solo-leveling-lab/tests/test_pipeline.py +++ b/apps/solo-leveling-lab/tests/test_pipeline.py @@ -1,5 +1,9 @@ from solo_leveling_lab.models import LabBrief -from solo_leveling_lab.pipeline import build_experiment, build_experiment_cycle, render_experiment_plan +from solo_leveling_lab.pipeline import ( + build_experiment, + build_experiment_cycle, + render_experiment_plan, +) def test_build_experiment_uses_thesis_and_format(): diff --git a/apps/tiktok-shop/tests/test_models.py b/apps/tiktok-shop/tests/test_models.py index 42ad335..54f84cd 100644 --- a/apps/tiktok-shop/tests/test_models.py +++ b/apps/tiktok-shop/tests/test_models.py @@ -1,7 +1,7 @@ """Testes para modelos do TikTok Shop.""" from dataclasses import dataclass -from tiktok_shop.models import OfferAngle, ValidationPlan, VideoScript +from tiktok_shop.models import VideoScript from tiktok_shop.pipeline import build_offer_angle, build_validation_plan, build_video_script diff --git a/apps/tiktok-shop/tests/test_pipeline.py b/apps/tiktok-shop/tests/test_pipeline.py index 2cbecce..431d181 100644 --- a/apps/tiktok-shop/tests/test_pipeline.py +++ b/apps/tiktok-shop/tests/test_pipeline.py @@ -1,7 +1,12 @@ """Testes para pipeline do TikTok Shop.""" from dataclasses import dataclass -from tiktok_shop.pipeline import build_offer_angle, build_validation_plan, build_video_script, render_validation_plan +from tiktok_shop.pipeline import ( + build_offer_angle, + build_validation_plan, + build_video_script, + render_validation_plan, +) @dataclass(slots=True) diff --git a/apps/tiktok-shop/tiktok_shop/__init__.py b/apps/tiktok-shop/tiktok_shop/__init__.py index e62fae7..a0b5f92 100644 --- a/apps/tiktok-shop/tiktok_shop/__init__.py +++ b/apps/tiktok-shop/tiktok_shop/__init__.py @@ -1,5 +1,10 @@ """TikTok Shop — transforme candidatos em planos de validacao.""" from tiktok_shop.models import OfferAngle, ValidationPlan, VideoScript -from tiktok_shop.pipeline import build_offer_angle, build_validation_plan, build_video_script, render_validation_plan +from tiktok_shop.pipeline import ( + build_offer_angle, + build_validation_plan, + build_video_script, + render_validation_plan, +) __all__ = ["OfferAngle", "VideoScript", "ValidationPlan", "build_offer_angle", "build_validation_plan", "build_video_script", "render_validation_plan"] diff --git a/apps/tiktok-shop/tiktok_shop/__main__.py b/apps/tiktok-shop/tiktok_shop/__main__.py index 04cde2f..3a0be02 100644 --- a/apps/tiktok-shop/tiktok_shop/__main__.py +++ b/apps/tiktok-shop/tiktok_shop/__main__.py @@ -1,4 +1,5 @@ """Permite executar: python -m tiktok_shop.""" from tiktok_shop.cli import app + if __name__ == "__main__": app() diff --git a/apps/tiktok-shop/tiktok_shop/cli.py b/apps/tiktok-shop/tiktok_shop/cli.py index b1385ab..d78989d 100644 --- a/apps/tiktok-shop/tiktok_shop/cli.py +++ b/apps/tiktok-shop/tiktok_shop/cli.py @@ -5,7 +5,6 @@ import rich.table import typer -from tiktok_shop.models import ValidationPlan from tiktok_shop.pipeline import build_validation_plan, render_validation_plan app = typer.Typer(help="TikTok Shop — transforme candidatos em planos de validacao comercial.") diff --git a/config.py b/config.py index 7512021..0294662 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,3 @@ -import os from pydantic import Field, field_validator from pydantic_settings import BaseSettings @@ -62,7 +61,7 @@ def github_enabled(self) -> bool: default=True, validation_alias="HUB_READ_ONLY" ) implementer_model: str = Field( - default="claude-sonnet-4-6", validation_alias="IMPLEMENTER_MODEL" + default="claude-opus-4-6", validation_alias="IMPLEMENTER_MODEL" ) implementer_max_tokens: int = Field( default=3200, validation_alias="IMPLEMENTER_MAX_TOKENS" diff --git a/core/circuit_breaker.py b/core/circuit_breaker.py index 4172937..8c2c055 100644 --- a/core/circuit_breaker.py +++ b/core/circuit_breaker.py @@ -1,8 +1,8 @@ -import asyncio import logging import time +from collections.abc import Callable, Coroutine from enum import Enum -from typing import Any, Callable, Coroutine +from typing import Any logger = logging.getLogger(__name__) diff --git a/core/monarch_core_client.py b/core/monarch_core_client.py index 4b90057..5a05ab6 100644 --- a/core/monarch_core_client.py +++ b/core/monarch_core_client.py @@ -4,7 +4,6 @@ import httpx - logger = logging.getLogger(__name__) diff --git a/core/orchestrator.py b/core/orchestrator.py index db68753..17fc83f 100644 --- a/core/orchestrator.py +++ b/core/orchestrator.py @@ -1,25 +1,26 @@ import asyncio import logging +from collections.abc import Awaitable, Callable from datetime import UTC, datetime -from typing import TYPE_CHECKING, Any, Callable, Awaitable +from typing import TYPE_CHECKING, Any -from config import config -from core.monarch_core_client import MonarchCoreClient -from core.circuit_breaker import CircuitBreaker, CircuitBreakerError -from core.task import Task, TaskMode, TaskStatus -from storage.database import Database -from agents.discovery import DiscoveryAgent -from agents.prioritization import PrioritizationAgent from agents.architecture import ArchitectureAgent -from agents.planning import PlanningAgent +from agents.deploy import DeployAgent from agents.devils_advocate import DevilsAdvocateAgent +from agents.discovery import DiscoveryAgent +from agents.documentation import DocumentationAgent from agents.implementer import ImplementerAgent -from agents.testing import TestingAgent +from agents.observability import ObservabilityAgent +from agents.planning import PlanningAgent +from agents.prioritization import PrioritizationAgent from agents.reviewer import ReviewerAgent from agents.security import SecurityAgent -from agents.documentation import DocumentationAgent -from agents.observability import ObservabilityAgent -from agents.deploy import DeployAgent +from agents.testing import TestingAgent +from config import config +from core.circuit_breaker import CircuitBreaker, CircuitBreakerError +from core.monarch_core_client import MonarchCoreClient +from core.task import Task, TaskMode, TaskStatus +from storage.database import Database from tools.github_tools import GitHubTools if TYPE_CHECKING: @@ -37,7 +38,7 @@ def __init__( ) -> None: self.db = db self._breakers: dict[str, CircuitBreaker] = {} - self._telegram: "TelegramBot | None" = None + self._telegram: TelegramBot | None = None self._core_client = MonarchCoreClient( base_url=config.monarch_core_api_url, project_slug=config.monarch_core_project_slug, @@ -500,13 +501,13 @@ async def _sync_approval_decision(self, task: Task, decision: str) -> None: def _build_pr_body(self, task: Task) -> str: reqs = task.requirements or {} lines = [ - f"## Summary", + "## Summary", f"{reqs.get('summary', task.raw_input)}", "", - f"## Complexity", + "## Complexity", f"`{reqs.get('complexity', 'unknown')}`", "", - f"## Affected Areas", + "## Affected Areas", ] for area in reqs.get("affected_areas", []): lines.append(f"- {area}") diff --git a/core/task.py b/core/task.py index cc9aaa9..3f4fa7f 100644 --- a/core/task.py +++ b/core/task.py @@ -1,6 +1,6 @@ import uuid from dataclasses import dataclass, field -from datetime import datetime, UTC +from datetime import UTC, datetime from enum import Enum from typing import Any diff --git a/interfaces/cli.py b/interfaces/cli.py index f449215..436563c 100644 --- a/interfaces/cli.py +++ b/interfaces/cli.py @@ -480,11 +480,11 @@ def _print_task_summary(task) -> None: async def _run_plan_only(raw_input: str, mode: str = "ideation") -> None: - from config import config from agents.architecture import ArchitectureAgent from agents.discovery import DiscoveryAgent from agents.planning import PlanningAgent from agents.prioritization import PrioritizationAgent + from config import config from core.task import Task, TaskMode from storage.database import Database @@ -518,8 +518,8 @@ async def _run_plan_only(raw_input: str, mode: str = "ideation") -> None: async def _run_cli(raw_input: str, verbose: bool = True, mode: str = "execution") -> None: from config import config - from core.task import Task, TaskMode, TaskStatus from core.orchestrator import Orchestrator + from core.task import Task, TaskMode, TaskStatus from storage.database import Database db = Database(config.database_url) @@ -774,7 +774,7 @@ def pop_flag(flag: str) -> str | None: def _run_local_command(cmd: list[str], cwd: Path) -> int: - print(f"\n[monarch] Routing to local command:") + print("\n[monarch] Routing to local command:") print(f" {' '.join(cmd)}") print() result = subprocess.run(cmd, cwd=str(cwd)) diff --git a/interfaces/telegram_bot.py b/interfaces/telegram_bot.py index ac2b7c1..7e86ac2 100644 --- a/interfaces/telegram_bot.py +++ b/interfaces/telegram_bot.py @@ -2,8 +2,15 @@ import logging from typing import Any -from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ( + Application, + CallbackQueryHandler, + CommandHandler, + ContextTypes, + MessageHandler, + filters, +) from config import config from core.orchestrator import Orchestrator diff --git a/apps/monarch-web/Dockerfile b/interfaces/web/Dockerfile similarity index 100% rename from apps/monarch-web/Dockerfile rename to interfaces/web/Dockerfile diff --git a/apps/monarch-web/README.md b/interfaces/web/README.md similarity index 100% rename from apps/monarch-web/README.md rename to interfaces/web/README.md diff --git a/apps/monarch-web/__init__.py b/interfaces/web/__init__.py similarity index 100% rename from apps/monarch-web/__init__.py rename to interfaces/web/__init__.py diff --git a/apps/monarch-web/app.py b/interfaces/web/app.py similarity index 99% rename from apps/monarch-web/app.py rename to interfaces/web/app.py index 0fdbef1..5566ae6 100644 --- a/apps/monarch-web/app.py +++ b/interfaces/web/app.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import os import re import time from collections import defaultdict, deque @@ -10,16 +11,14 @@ import httpx from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect -from fastapi.responses import JSONResponse -from fastapi.responses import HTMLResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates from fastapi.requests import Request +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.templating import Jinja2Templates from pydantic import BaseModel from config import config from core.orchestrator import Orchestrator -from core.task import Task, TaskStatus +from core.task import Task from storage.database import Database logger = logging.getLogger(__name__) @@ -89,7 +88,6 @@ async def lifespan(app: FastAPI): app = FastAPI(title="Monarch AI", lifespan=lifespan) -import os _templates_dir = os.path.join(os.path.dirname(__file__), "templates") templates = Jinja2Templates(directory=_templates_dir) diff --git a/apps/monarch-web/requirements.txt b/interfaces/web/requirements.txt similarity index 100% rename from apps/monarch-web/requirements.txt rename to interfaces/web/requirements.txt diff --git a/apps/monarch-web/templates/hub.html b/interfaces/web/templates/hub.html similarity index 100% rename from apps/monarch-web/templates/hub.html rename to interfaces/web/templates/hub.html diff --git a/apps/monarch-web/templates/index.html b/interfaces/web/templates/index.html similarity index 100% rename from apps/monarch-web/templates/index.html rename to interfaces/web/templates/index.html diff --git a/main.py b/main.py index 836910b..34018c6 100644 --- a/main.py +++ b/main.py @@ -5,14 +5,13 @@ import asyncio import logging import signal -import sys import uvicorn from config import config from core.orchestrator import Orchestrator -from storage.database import Database from interfaces.telegram_bot import TelegramBot +from storage.database import Database logging.basicConfig( level=logging.INFO, diff --git a/pyproject.toml b/pyproject.toml index 4c7a94f..ada998a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,17 @@ target-version = "py312" [tool.ruff.lint] select = ["E", "F", "I", "UP"] -ignore = ["E501"] +# E501: line length is advisory; UP042: keep (str, Enum) — StrEnum migration is behavior-sensitive. +ignore = ["E501", "UP042"] + +[tool.ruff.lint.per-file-ignores] +# Unused locals (e.g. `result = await agent.run(...)`) are acceptable for test readability. +"tests/**" = ["F841"] +# conftest sets required env vars before importing modules that read them. +"**/conftest.py" = ["E402"] +# CLI groups deferred stdlib imports by section; some display-only locals are unused. +"interfaces/cli.py" = ["E402", "F841"] +"apps/pdf-factory/**" = ["F841"] [tool.mypy] python_version = "3.12" diff --git a/storage/database.py b/storage/database.py index 9b7834e..2257433 100644 --- a/storage/database.py +++ b/storage/database.py @@ -3,7 +3,7 @@ from datetime import datetime from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from core.task import HistoryEntry, Task, TaskMode, TaskStatus from storage.models import Base, TaskRecord diff --git a/storage/models.py b/storage/models.py index 379f260..e7d5f39 100644 --- a/storage/models.py +++ b/storage/models.py @@ -1,5 +1,6 @@ -from datetime import datetime, UTC -from sqlalchemy import Column, String, Text, DateTime +from datetime import UTC, datetime + +from sqlalchemy import Column, DateTime, String, Text from sqlalchemy.orm import DeclarativeBase diff --git a/tests/conftest.py b/tests/conftest.py index 7ef43bb..e991ad1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,3 +9,5 @@ os.environ.setdefault("TELEGRAM_CHAT_ID", "123456789") os.environ.setdefault("MONARCH_CORE_API_URL", "") os.environ.setdefault("MONARCH_CORE_PROJECT_SLUG", "monarch-ai") +# Hub write endpoints are exercised by the web tests; production stays read-only by default. +os.environ.setdefault("HUB_READ_ONLY", "false") diff --git a/tests/integration/test_pipeline_integration.py b/tests/integration/test_pipeline_integration.py index ae7a9c6..6835b12 100644 --- a/tests/integration/test_pipeline_integration.py +++ b/tests/integration/test_pipeline_integration.py @@ -4,15 +4,14 @@ task state is persisted, and the final status is DONE. """ import json -import pytest from unittest.mock import AsyncMock, MagicMock, patch import anthropic +import pytest -from core.task import Task, TaskStatus from core.orchestrator import Orchestrator +from core.task import Task, TaskStatus from storage.database import Database -from agents.base import AgentResult def _claude_msg(payload: dict) -> anthropic.types.Message: @@ -67,7 +66,7 @@ def next_msg(*args, **kwargs): return _claude_msg(next(response_iter)) with ( - patch("agents.base._client") as mock_client, + patch("agents.base._api_client") as mock_client, patch("core.orchestrator.GitHubTools") as MockGHOrch, patch("agents.implementer.GitHubTools") as MockGH, patch("agents.reviewer.GitHubTools"), @@ -109,7 +108,7 @@ def next_msg(*args, **kwargs): return _claude_msg(next(response_iter)) with ( - patch("agents.base._client") as mock_client, + patch("agents.base._api_client") as mock_client, patch("core.orchestrator.GitHubTools") as MockGHOrch, patch("agents.implementer.GitHubTools") as MockGH, patch("agents.reviewer.GitHubTools"), diff --git a/tests/unit/test_circuit_breaker.py b/tests/unit/test_circuit_breaker.py index 139dc4a..434de86 100644 --- a/tests/unit/test_circuit_breaker.py +++ b/tests/unit/test_circuit_breaker.py @@ -1,5 +1,7 @@ -import pytest import asyncio + +import pytest + from core.circuit_breaker import CircuitBreaker, CircuitBreakerError, CircuitState diff --git a/tests/unit/test_code_tools.py b/tests/unit/test_code_tools.py index fe399e7..e15a381 100644 --- a/tests/unit/test_code_tools.py +++ b/tests/unit/test_code_tools.py @@ -1,5 +1,7 @@ +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import patch, MagicMock + from tools.code_tools import CodeTools diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index db66966..50f8ae4 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -14,6 +14,7 @@ def test_config_loads_required_vars(): } with patch.dict(os.environ, env): import importlib + import config as cfg_module importlib.reload(cfg_module) c = cfg_module.Config() diff --git a/tests/unit/test_definition_agents.py b/tests/unit/test_definition_agents.py index 0cad4ec..d46a887 100644 --- a/tests/unit/test_definition_agents.py +++ b/tests/unit/test_definition_agents.py @@ -1,11 +1,13 @@ -import pytest from unittest.mock import MagicMock, patch -from core.task import Task -from agents.prioritization import PrioritizationAgent + +import pytest + from agents.architecture import ArchitectureAgent -from agents.planning import PlanningAgent -from agents.devils_advocate import DevilsAdvocateAgent from agents.base import OPUS_MODEL, SONNET_MODEL +from agents.devils_advocate import DevilsAdvocateAgent +from agents.planning import PlanningAgent +from agents.prioritization import PrioritizationAgent +from core.task import Task def _mock_message(json_text: str) -> MagicMock: diff --git a/tests/unit/test_deploy_agent.py b/tests/unit/test_deploy_agent.py index 0925662..d619f53 100644 --- a/tests/unit/test_deploy_agent.py +++ b/tests/unit/test_deploy_agent.py @@ -1,8 +1,10 @@ import json +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import MagicMock, patch, AsyncMock -from core.task import Task + from agents.base import SONNET_MODEL +from core.task import Task def _msg(payload: dict) -> MagicMock: diff --git a/tests/unit/test_discovery_agent.py b/tests/unit/test_discovery_agent.py index 2f61d7f..cb30731 100644 --- a/tests/unit/test_discovery_agent.py +++ b/tests/unit/test_discovery_agent.py @@ -1,7 +1,9 @@ +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import AsyncMock, MagicMock, patch -from agents.discovery import DiscoveryAgent + from agents.base import AgentResult +from agents.discovery import DiscoveryAgent from core.task import Task @@ -28,7 +30,7 @@ async def test_discovery_returns_result(agent): mock_message = MagicMock() mock_message.stop_reason = "end_turn" mock_message.content = [ - MagicMock(type="text", text=f'{{"confidence": 0.92, "summary": "Build JWT login endpoint", "feature_type": "backend_api", "complexity": "medium", "affected_areas": ["auth", "users"], "acceptance_criteria": ["returns 200 on valid creds", "returns 401 on invalid"], "out_of_scope": [], "concerns": []}}') + MagicMock(type="text", text='{"confidence": 0.92, "summary": "Build JWT login endpoint", "feature_type": "backend_api", "complexity": "medium", "affected_areas": ["auth", "users"], "acceptance_criteria": ["returns 200 on valid creds", "returns 401 on invalid"], "out_of_scope": [], "concerns": []}') ] with patch.object(agent, "_call_claude", return_value=mock_message): diff --git a/tests/unit/test_execution_agents.py b/tests/unit/test_execution_agents.py index db68fd0..1a8c204 100644 --- a/tests/unit/test_execution_agents.py +++ b/tests/unit/test_execution_agents.py @@ -1,12 +1,14 @@ import json +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import AsyncMock, MagicMock, patch -from core.task import Task + +from agents.base import OPUS_MODEL, SONNET_MODEL from agents.implementer import ImplementerAgent -from agents.testing import TestingAgent from agents.reviewer import ReviewerAgent from agents.security import SecurityAgent -from agents.base import OPUS_MODEL, SONNET_MODEL +from agents.testing import TestingAgent +from core.task import Task def _msg(payload: dict) -> MagicMock: diff --git a/tests/unit/test_fs_tools.py b/tests/unit/test_fs_tools.py index b7e5167..6b7b389 100644 --- a/tests/unit/test_fs_tools.py +++ b/tests/unit/test_fs_tools.py @@ -1,5 +1,6 @@ + import pytest -import os + from tools.fs_tools import FsTools diff --git a/tests/unit/test_github_tools.py b/tests/unit/test_github_tools.py index a3584a5..057fbf5 100644 --- a/tests/unit/test_github_tools.py +++ b/tests/unit/test_github_tools.py @@ -1,5 +1,7 @@ +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import MagicMock, patch, PropertyMock + from tools.github_tools import GitHubTools diff --git a/tests/unit/test_orchestrator.py b/tests/unit/test_orchestrator.py index a92d812..e4e4f5c 100644 --- a/tests/unit/test_orchestrator.py +++ b/tests/unit/test_orchestrator.py @@ -1,8 +1,10 @@ -import pytest from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from agents.base import AgentResult from core.orchestrator import Orchestrator from core.task import Task, TaskMode, TaskStatus -from agents.base import AgentResult def _result(output: dict = None, confidence: float = 0.9) -> AgentResult: diff --git a/tests/unit/test_orchestrator_core_sync.py b/tests/unit/test_orchestrator_core_sync.py index 6f1547d..f78820c 100644 --- a/tests/unit/test_orchestrator_core_sync.py +++ b/tests/unit/test_orchestrator_core_sync.py @@ -1,6 +1,7 @@ -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from agents.base import AgentResult from core.orchestrator import Orchestrator from core.task import Task, TaskStatus diff --git a/tests/unit/test_storage.py b/tests/unit/test_storage.py index 824cce6..3140c7d 100644 --- a/tests/unit/test_storage.py +++ b/tests/unit/test_storage.py @@ -1,7 +1,8 @@ import pytest import pytest_asyncio -from storage.database import Database + from core.task import Task, TaskStatus +from storage.database import Database @pytest_asyncio.fixture diff --git a/tests/unit/test_support_agents.py b/tests/unit/test_support_agents.py index e8f0de5..4158db2 100644 --- a/tests/unit/test_support_agents.py +++ b/tests/unit/test_support_agents.py @@ -1,10 +1,12 @@ import json -import pytest from unittest.mock import MagicMock, patch -from core.task import Task + +import pytest + +from agents.base import HAIKU_MODEL from agents.documentation import DocumentationAgent from agents.observability import ObservabilityAgent -from agents.base import HAIKU_MODEL +from core.task import Task def _msg(payload: dict) -> MagicMock: diff --git a/tests/unit/test_task.py b/tests/unit/test_task.py index c0a6ecb..557bc92 100644 --- a/tests/unit/test_task.py +++ b/tests/unit/test_task.py @@ -1,4 +1,4 @@ -from core.task import HistoryEntry, Task, TaskStatus +from core.task import Task, TaskStatus def test_task_creation(): diff --git a/tests/unit/test_telegram_bot.py b/tests/unit/test_telegram_bot.py index f8c2df9..508f7d0 100644 --- a/tests/unit/test_telegram_bot.py +++ b/tests/unit/test_telegram_bot.py @@ -1,5 +1,7 @@ -import pytest from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + from core.task import Task, TaskStatus diff --git a/tests/unit/test_web_app.py b/tests/unit/test_web_app.py index 63bde60..7cc574f 100644 --- a/tests/unit/test_web_app.py +++ b/tests/unit/test_web_app.py @@ -1,12 +1,13 @@ -import pytest from unittest.mock import AsyncMock, MagicMock, patch -from httpx import AsyncClient, ASGITransport + +import pytest +from httpx import ASGITransport, AsyncClient @pytest.fixture async def client(): - from interfaces.web.app import app, _db, _orchestrator import interfaces.web.app as web_module + from interfaces.web.app import app mock_db = AsyncMock() mock_orch = AsyncMock() @@ -295,8 +296,12 @@ async def test_hub_project_detail_returns_404_when_project_missing(client): @pytest.mark.asyncio async def test_hub_project_detail_returns_aggregated_payload(client): ac, _, _ = client + # Endpoint aggregates, in order: project, execution_summary, implementation_summary, + # ideas, tasks, approvals, executions, metrics, roadmap_items. fetch_mock = AsyncMock(side_effect=[ {"id": "project-1", "name": "Canal Dark"}, + {"total": 3, "last_status": "done"}, + {"implemented": True, "files": 12}, [{"id": "idea-1", "title": "Ideia"}], [{"id": "task-1", "title": "Tarefa"}], [{"id": "approval-1", "title": "Aprovacao"}], @@ -309,6 +314,8 @@ async def test_hub_project_detail_returns_aggregated_payload(client): assert resp.status_code == 200 data = resp.json() assert data["project"]["id"] == "project-1" + assert data["execution_summary"]["total"] == 3 + assert data["implementation_summary"]["implemented"] is True assert data["ideas"][0]["id"] == "idea-1" assert data["tasks"][0]["id"] == "task-1" assert data["approvals"][0]["id"] == "approval-1" diff --git a/tools/code_tools.py b/tools/code_tools.py index 918d487..0f1e5d3 100644 --- a/tools/code_tools.py +++ b/tools/code_tools.py @@ -1,5 +1,5 @@ -import subprocess import logging +import subprocess from typing import Any logger = logging.getLogger(__name__) diff --git a/tools/fs_tools.py b/tools/fs_tools.py index 49fcbea..58031c0 100644 --- a/tools/fs_tools.py +++ b/tools/fs_tools.py @@ -1,7 +1,7 @@ from __future__ import annotations -import os import logging +import os from pathlib import Path from typing import Any