forked from Rockachopa/Timmy-time-dashboard
Compare commits
1 Commits
claude/iss
...
gemini/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e4206a91e |
@@ -33,12 +33,12 @@ from dashboard.routes.calm import router as calm_router
|
|||||||
from dashboard.routes.chat_api import router as chat_api_router
|
from dashboard.routes.chat_api import router as chat_api_router
|
||||||
from dashboard.routes.chat_api_v1 import router as chat_api_v1_router
|
from dashboard.routes.chat_api_v1 import router as chat_api_v1_router
|
||||||
from dashboard.routes.daily_run import router as daily_run_router
|
from dashboard.routes.daily_run import router as daily_run_router
|
||||||
from dashboard.routes.hermes import router as hermes_router
|
|
||||||
from dashboard.routes.db_explorer import router as db_explorer_router
|
from dashboard.routes.db_explorer import router as db_explorer_router
|
||||||
from dashboard.routes.discord import router as discord_router
|
from dashboard.routes.discord import router as discord_router
|
||||||
from dashboard.routes.experiments import router as experiments_router
|
from dashboard.routes.experiments import router as experiments_router
|
||||||
from dashboard.routes.grok import router as grok_router
|
from dashboard.routes.grok import router as grok_router
|
||||||
from dashboard.routes.health import router as health_router
|
from dashboard.routes.health import router as health_router
|
||||||
|
from dashboard.routes.hermes import router as hermes_router
|
||||||
from dashboard.routes.loop_qa import router as loop_qa_router
|
from dashboard.routes.loop_qa import router as loop_qa_router
|
||||||
from dashboard.routes.memory import router as memory_router
|
from dashboard.routes.memory import router as memory_router
|
||||||
from dashboard.routes.mobile import router as mobile_router
|
from dashboard.routes.mobile import router as mobile_router
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ def _save_voice_settings(data: dict) -> None:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to save voice settings: %s", exc)
|
logger.warning("Failed to save voice settings: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(prefix="/voice", tags=["voice"])
|
router = APIRouter(prefix="/voice", tags=["voice"])
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ Monitors the local machine (Hermes/M3 Max) for memory pressure, disk usage,
|
|||||||
Ollama model health, zombie processes, and network connectivity.
|
Ollama model health, zombie processes, and network connectivity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from infrastructure.hermes.monitor import HermesMonitor, HealthLevel, HealthReport, hermes_monitor
|
from infrastructure.hermes.monitor import HealthLevel, HealthReport, HermesMonitor, hermes_monitor
|
||||||
|
|
||||||
__all__ = ["HermesMonitor", "HealthLevel", "HealthReport", "hermes_monitor"]
|
__all__ = ["HermesMonitor", "HealthLevel", "HealthReport", "hermes_monitor"]
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
@@ -31,7 +32,7 @@ from config import settings
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HealthLevel(str, Enum):
|
class HealthLevel(StrEnum):
|
||||||
"""Severity level for a health check result."""
|
"""Severity level for a health check result."""
|
||||||
|
|
||||||
OK = "ok"
|
OK = "ok"
|
||||||
@@ -194,8 +195,7 @@ class HermesMonitor:
|
|||||||
name="memory",
|
name="memory",
|
||||||
level=HealthLevel.CRITICAL,
|
level=HealthLevel.CRITICAL,
|
||||||
message=(
|
message=(
|
||||||
f"Critical: only {free_gb:.1f}GB free "
|
f"Critical: only {free_gb:.1f}GB free (threshold: {memory_free_min_gb}GB)"
|
||||||
f"(threshold: {memory_free_min_gb}GB)"
|
|
||||||
),
|
),
|
||||||
details=details,
|
details=details,
|
||||||
needs_human=True,
|
needs_human=True,
|
||||||
@@ -302,8 +302,7 @@ class HermesMonitor:
|
|||||||
name="disk",
|
name="disk",
|
||||||
level=HealthLevel.CRITICAL,
|
level=HealthLevel.CRITICAL,
|
||||||
message=(
|
message=(
|
||||||
f"Critical: only {free_gb:.1f}GB free "
|
f"Critical: only {free_gb:.1f}GB free (threshold: {disk_free_min_gb}GB)"
|
||||||
f"(threshold: {disk_free_min_gb}GB)"
|
|
||||||
),
|
),
|
||||||
details=details,
|
details=details,
|
||||||
needs_human=True,
|
needs_human=True,
|
||||||
@@ -335,7 +334,7 @@ class HermesMonitor:
|
|||||||
cutoff = time.time() - 86400 # 24 hours ago
|
cutoff = time.time() - 86400 # 24 hours ago
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tmp = Path("/tmp")
|
tmp = Path(tempfile.gettempdir())
|
||||||
for item in tmp.iterdir():
|
for item in tmp.iterdir():
|
||||||
try:
|
try:
|
||||||
stat = item.stat()
|
stat = item.stat()
|
||||||
@@ -345,11 +344,7 @@ class HermesMonitor:
|
|||||||
freed_bytes += stat.st_size
|
freed_bytes += stat.st_size
|
||||||
item.unlink(missing_ok=True)
|
item.unlink(missing_ok=True)
|
||||||
elif item.is_dir():
|
elif item.is_dir():
|
||||||
dir_size = sum(
|
dir_size = sum(f.stat().st_size for f in item.rglob("*") if f.is_file())
|
||||||
f.stat().st_size
|
|
||||||
for f in item.rglob("*")
|
|
||||||
if f.is_file()
|
|
||||||
)
|
|
||||||
freed_bytes += dir_size
|
freed_bytes += dir_size
|
||||||
shutil.rmtree(str(item), ignore_errors=True)
|
shutil.rmtree(str(item), ignore_errors=True)
|
||||||
except (PermissionError, OSError):
|
except (PermissionError, OSError):
|
||||||
@@ -392,10 +387,7 @@ class HermesMonitor:
|
|||||||
return CheckResult(
|
return CheckResult(
|
||||||
name="ollama",
|
name="ollama",
|
||||||
level=HealthLevel.OK,
|
level=HealthLevel.OK,
|
||||||
message=(
|
message=(f"Ollama OK — {len(models)} model(s) available, {len(loaded)} loaded"),
|
||||||
f"Ollama OK — {len(models)} model(s) available, "
|
|
||||||
f"{len(loaded)} loaded"
|
|
||||||
),
|
|
||||||
details={
|
details={
|
||||||
"reachable": True,
|
"reachable": True,
|
||||||
"model_count": len(models),
|
"model_count": len(models),
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ class BannerlordObserver:
|
|||||||
self._host = host or settings.gabs_host
|
self._host = host or settings.gabs_host
|
||||||
self._port = port or settings.gabs_port
|
self._port = port or settings.gabs_port
|
||||||
self._timeout = timeout if timeout is not None else settings.gabs_timeout
|
self._timeout = timeout if timeout is not None else settings.gabs_timeout
|
||||||
self._poll_interval = poll_interval if poll_interval is not None else settings.gabs_poll_interval
|
self._poll_interval = (
|
||||||
|
poll_interval if poll_interval is not None else settings.gabs_poll_interval
|
||||||
|
)
|
||||||
self._journal_path = Path(journal_path) if journal_path else _get_journal_path()
|
self._journal_path = Path(journal_path) if journal_path else _get_journal_path()
|
||||||
self._entry_count = 0
|
self._entry_count = 0
|
||||||
self._days_observed: set[str] = set()
|
self._days_observed: set[str] = set()
|
||||||
|
|||||||
@@ -196,9 +196,7 @@ class EmotionalStateTracker:
|
|||||||
"intensity_label": _intensity_label(self.state.intensity),
|
"intensity_label": _intensity_label(self.state.intensity),
|
||||||
"previous_emotion": self.state.previous_emotion,
|
"previous_emotion": self.state.previous_emotion,
|
||||||
"trigger_event": self.state.trigger_event,
|
"trigger_event": self.state.trigger_event,
|
||||||
"prompt_modifier": EMOTION_PROMPT_MODIFIERS.get(
|
"prompt_modifier": EMOTION_PROMPT_MODIFIERS.get(self.state.current_emotion, ""),
|
||||||
self.state.current_emotion, ""
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_prompt_modifier(self) -> str:
|
def get_prompt_modifier(self) -> str:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -70,7 +70,9 @@ _LOOP_TAG = "loop-generated"
|
|||||||
|
|
||||||
# Regex patterns for scoring
|
# Regex patterns for scoring
|
||||||
_TAG_RE = re.compile(r"\[([^\]]+)\]")
|
_TAG_RE = re.compile(r"\[([^\]]+)\]")
|
||||||
_FILE_RE = re.compile(r"(?:src/|tests/|scripts/|\.py|\.html|\.js|\.yaml|\.toml|\.sh)", re.IGNORECASE)
|
_FILE_RE = re.compile(
|
||||||
|
r"(?:src/|tests/|scripts/|\.py|\.html|\.js|\.yaml|\.toml|\.sh)", re.IGNORECASE
|
||||||
|
)
|
||||||
_FUNC_RE = re.compile(r"(?:def |class |function |method |`\w+\(\)`)", re.IGNORECASE)
|
_FUNC_RE = re.compile(r"(?:def |class |function |method |`\w+\(\)`)", re.IGNORECASE)
|
||||||
_ACCEPT_RE = re.compile(
|
_ACCEPT_RE = re.compile(
|
||||||
r"(?:should|must|expect|verify|assert|test.?case|acceptance|criteria"
|
r"(?:should|must|expect|verify|assert|test.?case|acceptance|criteria"
|
||||||
@@ -451,9 +453,7 @@ async def add_label(
|
|||||||
|
|
||||||
# Apply to the issue
|
# Apply to the issue
|
||||||
apply_url = _repo_url(f"issues/{issue_number}/labels")
|
apply_url = _repo_url(f"issues/{issue_number}/labels")
|
||||||
apply_resp = await client.post(
|
apply_resp = await client.post(apply_url, headers=headers, json={"labels": [label_id]})
|
||||||
apply_url, headers=headers, json={"labels": [label_id]}
|
|
||||||
)
|
|
||||||
return apply_resp.status_code in (200, 201)
|
return apply_resp.status_code in (200, 201)
|
||||||
|
|
||||||
except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException) as exc:
|
except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException) as exc:
|
||||||
@@ -692,7 +692,9 @@ class BacklogTriageLoop:
|
|||||||
# 1. Fetch
|
# 1. Fetch
|
||||||
raw_issues = await fetch_open_issues(client)
|
raw_issues = await fetch_open_issues(client)
|
||||||
result.total_open = len(raw_issues)
|
result.total_open = len(raw_issues)
|
||||||
logger.info("Triage cycle #%d: fetched %d open issues", self._cycle_count, len(raw_issues))
|
logger.info(
|
||||||
|
"Triage cycle #%d: fetched %d open issues", self._cycle_count, len(raw_issues)
|
||||||
|
)
|
||||||
|
|
||||||
# 2. Score
|
# 2. Score
|
||||||
scored = [score_issue(i) for i in raw_issues]
|
scored = [score_issue(i) for i in raw_issues]
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
@@ -48,7 +48,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# Enumerations
|
# Enumerations
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class AgentType(str, Enum):
|
|
||||||
|
class AgentType(StrEnum):
|
||||||
"""Known agents in the swarm."""
|
"""Known agents in the swarm."""
|
||||||
|
|
||||||
CLAUDE_CODE = "claude_code"
|
CLAUDE_CODE = "claude_code"
|
||||||
@@ -57,7 +58,7 @@ class AgentType(str, Enum):
|
|||||||
TIMMY = "timmy"
|
TIMMY = "timmy"
|
||||||
|
|
||||||
|
|
||||||
class TaskType(str, Enum):
|
class TaskType(StrEnum):
|
||||||
"""Categories of engineering work."""
|
"""Categories of engineering work."""
|
||||||
|
|
||||||
# Claude Code strengths
|
# Claude Code strengths
|
||||||
@@ -83,7 +84,7 @@ class TaskType(str, Enum):
|
|||||||
ORCHESTRATION = "orchestration"
|
ORCHESTRATION = "orchestration"
|
||||||
|
|
||||||
|
|
||||||
class DispatchStatus(str, Enum):
|
class DispatchStatus(StrEnum):
|
||||||
"""Lifecycle state of a dispatched task."""
|
"""Lifecycle state of a dispatched task."""
|
||||||
|
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
@@ -99,6 +100,7 @@ class DispatchStatus(str, Enum):
|
|||||||
# Agent registry
|
# Agent registry
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentSpec:
|
class AgentSpec:
|
||||||
"""Capabilities and limits for a single agent."""
|
"""Capabilities and limits for a single agent."""
|
||||||
@@ -106,9 +108,9 @@ class AgentSpec:
|
|||||||
name: AgentType
|
name: AgentType
|
||||||
display_name: str
|
display_name: str
|
||||||
strengths: frozenset[TaskType]
|
strengths: frozenset[TaskType]
|
||||||
gitea_label: str | None # label to apply when dispatching
|
gitea_label: str | None # label to apply when dispatching
|
||||||
max_concurrent: int = 1
|
max_concurrent: int = 1
|
||||||
interface: str = "gitea" # "gitea" | "api" | "local"
|
interface: str = "gitea" # "gitea" | "api" | "local"
|
||||||
api_endpoint: str | None = None # for interface="api"
|
api_endpoint: str | None = None # for interface="api"
|
||||||
|
|
||||||
|
|
||||||
@@ -197,6 +199,7 @@ _TASK_ROUTING: dict[TaskType, AgentType] = {
|
|||||||
# Dispatch result
|
# Dispatch result
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DispatchResult:
|
class DispatchResult:
|
||||||
"""Outcome of a dispatch call."""
|
"""Outcome of a dispatch call."""
|
||||||
@@ -220,6 +223,7 @@ class DispatchResult:
|
|||||||
# Routing logic
|
# Routing logic
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def select_agent(task_type: TaskType) -> AgentType:
|
def select_agent(task_type: TaskType) -> AgentType:
|
||||||
"""Return the best agent for *task_type* based on the routing table.
|
"""Return the best agent for *task_type* based on the routing table.
|
||||||
|
|
||||||
@@ -248,11 +252,23 @@ def infer_task_type(title: str, description: str = "") -> TaskType:
|
|||||||
text = (title + " " + description).lower()
|
text = (title + " " + description).lower()
|
||||||
|
|
||||||
_SIGNALS: list[tuple[TaskType, frozenset[str]]] = [
|
_SIGNALS: list[tuple[TaskType, frozenset[str]]] = [
|
||||||
(TaskType.ARCHITECTURE, frozenset({"architect", "design", "adr", "system design", "schema"})),
|
(
|
||||||
(TaskType.REFACTORING, frozenset({"refactor", "clean up", "cleanup", "reorganise", "reorganize"})),
|
TaskType.ARCHITECTURE,
|
||||||
|
frozenset({"architect", "design", "adr", "system design", "schema"}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TaskType.REFACTORING,
|
||||||
|
frozenset({"refactor", "clean up", "cleanup", "reorganise", "reorganize"}),
|
||||||
|
),
|
||||||
(TaskType.CODE_REVIEW, frozenset({"review", "pr review", "pull request review", "audit"})),
|
(TaskType.CODE_REVIEW, frozenset({"review", "pr review", "pull request review", "audit"})),
|
||||||
(TaskType.COMPLEX_REASONING, frozenset({"complex", "hard problem", "debug", "investigate", "diagnose"})),
|
(
|
||||||
(TaskType.RESEARCH, frozenset({"research", "survey", "literature", "benchmark", "analyse", "analyze"})),
|
TaskType.COMPLEX_REASONING,
|
||||||
|
frozenset({"complex", "hard problem", "debug", "investigate", "diagnose"}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TaskType.RESEARCH,
|
||||||
|
frozenset({"research", "survey", "literature", "benchmark", "analyse", "analyze"}),
|
||||||
|
),
|
||||||
(TaskType.ANALYSIS, frozenset({"analysis", "profil", "trace", "metric", "performance"})),
|
(TaskType.ANALYSIS, frozenset({"analysis", "profil", "trace", "metric", "performance"})),
|
||||||
(TaskType.TRIAGE, frozenset({"triage", "classify", "prioritise", "prioritize"})),
|
(TaskType.TRIAGE, frozenset({"triage", "classify", "prioritise", "prioritize"})),
|
||||||
(TaskType.PLANNING, frozenset({"plan", "roadmap", "milestone", "epic", "spike"})),
|
(TaskType.PLANNING, frozenset({"plan", "roadmap", "milestone", "epic", "spike"})),
|
||||||
@@ -273,6 +289,7 @@ def infer_task_type(title: str, description: str = "") -> TaskType:
|
|||||||
# Gitea helpers
|
# Gitea helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
async def _post_gitea_comment(
|
async def _post_gitea_comment(
|
||||||
client: Any,
|
client: Any,
|
||||||
base_url: str,
|
base_url: str,
|
||||||
@@ -405,6 +422,7 @@ async def _poll_issue_completion(
|
|||||||
# Core dispatch functions
|
# Core dispatch functions
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
async def _dispatch_via_gitea(
|
async def _dispatch_via_gitea(
|
||||||
agent: AgentType,
|
agent: AgentType,
|
||||||
issue_number: int,
|
issue_number: int,
|
||||||
@@ -479,7 +497,11 @@ async def _dispatch_via_gitea(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 2. Post assignment comment
|
# 2. Post assignment comment
|
||||||
criteria_md = "\n".join(f"- {c}" for c in acceptance_criteria) if acceptance_criteria else "_None specified_"
|
criteria_md = (
|
||||||
|
"\n".join(f"- {c}" for c in acceptance_criteria)
|
||||||
|
if acceptance_criteria
|
||||||
|
else "_None specified_"
|
||||||
|
)
|
||||||
comment_body = (
|
comment_body = (
|
||||||
f"## Assigned to {spec.display_name}\n\n"
|
f"## Assigned to {spec.display_name}\n\n"
|
||||||
f"**Task type:** `{task_type.value}`\n\n"
|
f"**Task type:** `{task_type.value}`\n\n"
|
||||||
@@ -616,9 +638,7 @@ async def _dispatch_local(
|
|||||||
assumed to succeed at dispatch time).
|
assumed to succeed at dispatch time).
|
||||||
"""
|
"""
|
||||||
task_type = infer_task_type(title, description)
|
task_type = infer_task_type(title, description)
|
||||||
logger.info(
|
logger.info("Timmy handling task locally: %r (issue #%s)", title[:60], issue_number)
|
||||||
"Timmy handling task locally: %r (issue #%s)", title[:60], issue_number
|
|
||||||
)
|
|
||||||
return DispatchResult(
|
return DispatchResult(
|
||||||
task_type=task_type,
|
task_type=task_type,
|
||||||
agent=AgentType.TIMMY,
|
agent=AgentType.TIMMY,
|
||||||
@@ -632,6 +652,7 @@ async def _dispatch_local(
|
|||||||
# Public entry point
|
# Public entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_task(
|
async def dispatch_task(
|
||||||
title: str,
|
title: str,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
@@ -769,9 +790,7 @@ async def _log_escalation(
|
|||||||
f"---\n*Timmy agent dispatcher.*"
|
f"---\n*Timmy agent dispatcher.*"
|
||||||
)
|
)
|
||||||
async with httpx.AsyncClient(timeout=10) as client:
|
async with httpx.AsyncClient(timeout=10) as client:
|
||||||
await _post_gitea_comment(
|
await _post_gitea_comment(client, base_url, repo, headers, issue_number, body)
|
||||||
client, base_url, repo, headers, issue_number, body
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to post escalation comment: %s", exc)
|
logger.warning("Failed to post escalation comment: %s", exc)
|
||||||
|
|
||||||
@@ -780,6 +799,7 @@ async def _log_escalation(
|
|||||||
# Monitoring helper
|
# Monitoring helper
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_completion(
|
async def wait_for_completion(
|
||||||
issue_number: int,
|
issue_number: int,
|
||||||
poll_interval: int = 60,
|
poll_interval: int = 60,
|
||||||
|
|||||||
@@ -418,9 +418,7 @@ class MCPBridge:
|
|||||||
return f"Error executing {name}: {exc}"
|
return f"Error executing {name}: {exc}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_initial_messages(
|
def _build_initial_messages(prompt: str, system_prompt: str | None) -> list[dict]:
|
||||||
prompt: str, system_prompt: str | None
|
|
||||||
) -> list[dict]:
|
|
||||||
"""Build the initial message list for a run."""
|
"""Build the initial message list for a run."""
|
||||||
messages: list[dict] = []
|
messages: list[dict] = []
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
@@ -512,9 +510,7 @@ class MCPBridge:
|
|||||||
error_msg = ""
|
error_msg = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content, tool_calls_made, rounds, error_msg = await self._run_tool_loop(
|
content, tool_calls_made, rounds, error_msg = await self._run_tool_loop(messages, tools)
|
||||||
messages, tools
|
|
||||||
)
|
|
||||||
except httpx.ConnectError as exc:
|
except httpx.ConnectError as exc:
|
||||||
logger.warning("Ollama connection failed: %s", exc)
|
logger.warning("Ollama connection failed: %s", exc)
|
||||||
error_msg = f"Ollama connection failed: {exc}"
|
error_msg = f"Ollama connection failed: {exc}"
|
||||||
|
|||||||
@@ -47,13 +47,11 @@ _DEFAULT_IDLE_THRESHOLD = 30
|
|||||||
class AgentStatus:
|
class AgentStatus:
|
||||||
"""Health snapshot for one agent at a point in time."""
|
"""Health snapshot for one agent at a point in time."""
|
||||||
|
|
||||||
agent: str # "claude" | "kimi" | "timmy"
|
agent: str # "claude" | "kimi" | "timmy"
|
||||||
is_idle: bool = True
|
is_idle: bool = True
|
||||||
active_issue_numbers: list[int] = field(default_factory=list)
|
active_issue_numbers: list[int] = field(default_factory=list)
|
||||||
stuck_issue_numbers: list[int] = field(default_factory=list)
|
stuck_issue_numbers: list[int] = field(default_factory=list)
|
||||||
checked_at: str = field(
|
checked_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
||||||
default_factory=lambda: datetime.now(UTC).isoformat()
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_stuck(self) -> bool:
|
def is_stuck(self) -> bool:
|
||||||
@@ -69,9 +67,7 @@ class AgentHealthReport:
|
|||||||
"""Combined health report for all monitored agents."""
|
"""Combined health report for all monitored agents."""
|
||||||
|
|
||||||
agents: list[AgentStatus] = field(default_factory=list)
|
agents: list[AgentStatus] = field(default_factory=list)
|
||||||
generated_at: str = field(
|
generated_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
||||||
default_factory=lambda: datetime.now(UTC).isoformat()
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def any_stuck(self) -> bool:
|
def any_stuck(self) -> bool:
|
||||||
@@ -193,18 +189,14 @@ async def check_agent_health(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=15) as client:
|
async with httpx.AsyncClient(timeout=15) as client:
|
||||||
issues = await _fetch_labeled_issues(
|
issues = await _fetch_labeled_issues(client, base_url, headers, repo, label)
|
||||||
client, base_url, headers, repo, label
|
|
||||||
)
|
|
||||||
|
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
num = issue.get("number", 0)
|
num = issue.get("number", 0)
|
||||||
status.active_issue_numbers.append(num)
|
status.active_issue_numbers.append(num)
|
||||||
|
|
||||||
# Check last activity
|
# Check last activity
|
||||||
last_activity = await _last_comment_time(
|
last_activity = await _last_comment_time(client, base_url, headers, repo, num)
|
||||||
client, base_url, headers, repo, num
|
|
||||||
)
|
|
||||||
if last_activity is None:
|
if last_activity is None:
|
||||||
last_activity = await _issue_created_time(issue)
|
last_activity = await _issue_created_time(issue)
|
||||||
|
|
||||||
|
|||||||
@@ -91,9 +91,9 @@ _PRIORITY_LABEL_SCORES: dict[str, int] = {
|
|||||||
class AgentTarget(StrEnum):
|
class AgentTarget(StrEnum):
|
||||||
"""Which agent should handle this issue."""
|
"""Which agent should handle this issue."""
|
||||||
|
|
||||||
TIMMY = "timmy" # Timmy handles locally (self)
|
TIMMY = "timmy" # Timmy handles locally (self)
|
||||||
CLAUDE = "claude" # Dispatch to Claude Code
|
CLAUDE = "claude" # Dispatch to Claude Code
|
||||||
KIMI = "kimi" # Dispatch to Kimi Code
|
KIMI = "kimi" # Dispatch to Kimi Code
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -172,9 +172,7 @@ def triage_issues(raw_issues: list[dict[str, Any]]) -> list[TriagedIssue]:
|
|||||||
title = issue.get("title", "")
|
title = issue.get("title", "")
|
||||||
body = issue.get("body") or ""
|
body = issue.get("body") or ""
|
||||||
labels = _extract_labels(issue)
|
labels = _extract_labels(issue)
|
||||||
assignees = [
|
assignees = [a.get("login", "") for a in issue.get("assignees") or []]
|
||||||
a.get("login", "") for a in issue.get("assignees") or []
|
|
||||||
]
|
|
||||||
url = issue.get("html_url", "")
|
url = issue.get("html_url", "")
|
||||||
|
|
||||||
priority = _score_priority(labels, assignees)
|
priority = _score_priority(labels, assignees)
|
||||||
@@ -252,9 +250,7 @@ async def fetch_open_issues(
|
|||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
logger.warning(
|
logger.warning("fetch_open_issues: Gitea returned %s", resp.status_code)
|
||||||
"fetch_open_issues: Gitea returned %s", resp.status_code
|
|
||||||
)
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
issues = resp.json()
|
issues = resp.json()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ _LABEL_MAP: dict[AgentTarget, str] = {
|
|||||||
|
|
||||||
_LABEL_COLORS: dict[str, str] = {
|
_LABEL_COLORS: dict[str, str] = {
|
||||||
"claude-ready": "#8b6f47", # warm brown
|
"claude-ready": "#8b6f47", # warm brown
|
||||||
"kimi-ready": "#006b75", # dark teal
|
"kimi-ready": "#006b75", # dark teal
|
||||||
"timmy-ready": "#0075ca", # blue
|
"timmy-ready": "#0075ca", # blue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +52,7 @@ class DispatchRecord:
|
|||||||
issue_title: str
|
issue_title: str
|
||||||
agent: AgentTarget
|
agent: AgentTarget
|
||||||
rationale: str
|
rationale: str
|
||||||
dispatched_at: str = field(
|
dispatched_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
||||||
default_factory=lambda: datetime.now(UTC).isoformat()
|
|
||||||
)
|
|
||||||
label_applied: bool = False
|
label_applied: bool = False
|
||||||
comment_posted: bool = False
|
comment_posted: bool = False
|
||||||
|
|
||||||
@@ -170,9 +168,7 @@ async def dispatch_issue(issue: TriagedIssue) -> DispatchRecord:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=15) as client:
|
async with httpx.AsyncClient(timeout=15) as client:
|
||||||
label_id = await _get_or_create_label(
|
label_id = await _get_or_create_label(client, base_url, headers, repo, label_name)
|
||||||
client, base_url, headers, repo, label_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# Apply label
|
# Apply label
|
||||||
if label_id is not None:
|
if label_id is not None:
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ logger = logging.getLogger(__name__)
|
|||||||
# Thresholds
|
# Thresholds
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_WARN_DISK_PCT = 85.0 # warn when disk is more than 85% full
|
_WARN_DISK_PCT = 85.0 # warn when disk is more than 85% full
|
||||||
_WARN_MEM_PCT = 90.0 # warn when memory is more than 90% used
|
_WARN_MEM_PCT = 90.0 # warn when memory is more than 90% used
|
||||||
_WARN_CPU_PCT = 95.0 # warn when CPU is above 95% sustained
|
_WARN_CPU_PCT = 95.0 # warn when CPU is above 95% sustained
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -63,9 +63,7 @@ class SystemSnapshot:
|
|||||||
memory: MemoryUsage = field(default_factory=MemoryUsage)
|
memory: MemoryUsage = field(default_factory=MemoryUsage)
|
||||||
ollama: OllamaHealth = field(default_factory=OllamaHealth)
|
ollama: OllamaHealth = field(default_factory=OllamaHealth)
|
||||||
warnings: list[str] = field(default_factory=list)
|
warnings: list[str] = field(default_factory=list)
|
||||||
taken_at: str = field(
|
taken_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
||||||
default_factory=lambda: datetime.now(UTC).isoformat()
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def healthy(self) -> bool:
|
def healthy(self) -> bool:
|
||||||
@@ -117,8 +115,8 @@ def _probe_memory() -> MemoryUsage:
|
|||||||
def _probe_ollama_sync(ollama_url: str) -> OllamaHealth:
|
def _probe_ollama_sync(ollama_url: str) -> OllamaHealth:
|
||||||
"""Synchronous Ollama health probe — run in a thread."""
|
"""Synchronous Ollama health probe — run in a thread."""
|
||||||
try:
|
try:
|
||||||
import urllib.request
|
|
||||||
import json
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
url = ollama_url.rstrip("/") + "/api/tags"
|
url = ollama_url.rstrip("/") + "/api/tags"
|
||||||
with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
|
with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
|
||||||
@@ -154,14 +152,12 @@ async def get_system_snapshot() -> SystemSnapshot:
|
|||||||
|
|
||||||
if disk.percent_used >= _WARN_DISK_PCT:
|
if disk.percent_used >= _WARN_DISK_PCT:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"Disk {disk.path}: {disk.percent_used:.0f}% used "
|
f"Disk {disk.path}: {disk.percent_used:.0f}% used ({disk.free_gb:.1f} GB free)"
|
||||||
f"({disk.free_gb:.1f} GB free)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if memory.percent_used >= _WARN_MEM_PCT:
|
if memory.percent_used >= _WARN_MEM_PCT:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"Memory: {memory.percent_used:.0f}% used "
|
f"Memory: {memory.percent_used:.0f}% used ({memory.available_gb:.1f} GB available)"
|
||||||
f"({memory.available_gb:.1f} GB available)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not ollama.reachable:
|
if not ollama.reachable:
|
||||||
@@ -216,7 +212,5 @@ async def cleanup_stale_files(
|
|||||||
errors.append(str(exc))
|
errors.append(str(exc))
|
||||||
|
|
||||||
await asyncio.to_thread(_cleanup)
|
await asyncio.to_thread(_cleanup)
|
||||||
logger.info(
|
logger.info("cleanup_stale_files: deleted %d files, %d errors", deleted, len(errors))
|
||||||
"cleanup_stale_files: deleted %d files, %d errors", deleted, len(errors)
|
|
||||||
)
|
|
||||||
return {"deleted_count": deleted, "errors": errors}
|
return {"deleted_count": deleted, "errors": errors}
|
||||||
|
|||||||
@@ -10,14 +10,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from integrations.bannerlord.gabs_client import GabsClient, GabsError
|
from integrations.bannerlord.gabs_client import GabsClient, GabsError
|
||||||
|
|
||||||
|
|
||||||
# ── GabsClient unit tests ─────────────────────────────────────────────────────
|
# ── GabsClient unit tests ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -236,7 +234,13 @@ class TestBannerlordObserver:
|
|||||||
|
|
||||||
snapshot = {
|
snapshot = {
|
||||||
"game_state": {"day": 7, "season": "winter", "campaign_phase": "early"},
|
"game_state": {"day": 7, "season": "winter", "campaign_phase": "early"},
|
||||||
"player": {"name": "Timmy", "clan": "Thalheimer", "renown": 42, "level": 3, "gold": 1000},
|
"player": {
|
||||||
|
"name": "Timmy",
|
||||||
|
"clan": "Thalheimer",
|
||||||
|
"renown": 42,
|
||||||
|
"level": 3,
|
||||||
|
"gold": 1000,
|
||||||
|
},
|
||||||
"player_party": {"size": 25, "morale": 80, "food_days_left": 5},
|
"player_party": {"size": 25, "morale": 80, "food_days_left": 5},
|
||||||
"kingdoms": [{"name": "Vlandia", "ruler": "Derthert", "military_strength": 5000}],
|
"kingdoms": [{"name": "Vlandia", "ruler": "Derthert", "military_strength": 5000}],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Tests for agent emotional state simulation (src/timmy/agents/emotional_state.py)."""
|
"""Tests for agent emotional state simulation (src/timmy/agents/emotional_state.py)."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from timmy.agents.emotional_state import (
|
from timmy.agents.emotional_state import (
|
||||||
EMOTION_PROMPT_MODIFIERS,
|
EMOTION_PROMPT_MODIFIERS,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from timmy.dispatcher import (
|
from timmy.dispatcher import (
|
||||||
AGENT_REGISTRY,
|
AGENT_REGISTRY,
|
||||||
AgentType,
|
AgentType,
|
||||||
@@ -21,11 +19,11 @@ from timmy.dispatcher import (
|
|||||||
wait_for_completion,
|
wait_for_completion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Agent registry
|
# Agent registry
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestAgentRegistry:
|
class TestAgentRegistry:
|
||||||
def test_all_agents_present(self):
|
def test_all_agents_present(self):
|
||||||
for member in AgentType:
|
for member in AgentType:
|
||||||
@@ -41,7 +39,7 @@ class TestAgentRegistry:
|
|||||||
assert spec.gitea_label, f"{agent} is gitea interface but has no label"
|
assert spec.gitea_label, f"{agent} is gitea interface but has no label"
|
||||||
|
|
||||||
def test_non_gitea_agents_have_no_labels(self):
|
def test_non_gitea_agents_have_no_labels(self):
|
||||||
for agent, spec in AGENT_REGISTRY.items():
|
for _agent, spec in AGENT_REGISTRY.items():
|
||||||
if spec.interface not in ("gitea",):
|
if spec.interface not in ("gitea",):
|
||||||
# api and local agents may have no label
|
# api and local agents may have no label
|
||||||
assert spec.gitea_label is None or spec.interface == "gitea"
|
assert spec.gitea_label is None or spec.interface == "gitea"
|
||||||
@@ -55,6 +53,7 @@ class TestAgentRegistry:
|
|||||||
# select_agent
|
# select_agent
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestSelectAgent:
|
class TestSelectAgent:
|
||||||
def test_architecture_routes_to_claude(self):
|
def test_architecture_routes_to_claude(self):
|
||||||
assert select_agent(TaskType.ARCHITECTURE) == AgentType.CLAUDE_CODE
|
assert select_agent(TaskType.ARCHITECTURE) == AgentType.CLAUDE_CODE
|
||||||
@@ -85,6 +84,7 @@ class TestSelectAgent:
|
|||||||
# infer_task_type
|
# infer_task_type
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestInferTaskType:
|
class TestInferTaskType:
|
||||||
def test_architecture_keyword(self):
|
def test_architecture_keyword(self):
|
||||||
assert infer_task_type("Design the LLM router architecture") == TaskType.ARCHITECTURE
|
assert infer_task_type("Design the LLM router architecture") == TaskType.ARCHITECTURE
|
||||||
@@ -119,6 +119,7 @@ class TestInferTaskType:
|
|||||||
# DispatchResult
|
# DispatchResult
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDispatchResult:
|
class TestDispatchResult:
|
||||||
def test_success_when_assigned(self):
|
def test_success_when_assigned(self):
|
||||||
r = DispatchResult(
|
r = DispatchResult(
|
||||||
@@ -161,6 +162,7 @@ class TestDispatchResult:
|
|||||||
# _dispatch_local
|
# _dispatch_local
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDispatchLocal:
|
class TestDispatchLocal:
|
||||||
async def test_returns_assigned(self):
|
async def test_returns_assigned(self):
|
||||||
result = await _dispatch_local(
|
result = await _dispatch_local(
|
||||||
@@ -190,6 +192,7 @@ class TestDispatchLocal:
|
|||||||
# _dispatch_via_api
|
# _dispatch_via_api
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDispatchViaApi:
|
class TestDispatchViaApi:
|
||||||
async def test_no_endpoint_returns_failed(self):
|
async def test_no_endpoint_returns_failed(self):
|
||||||
result = await _dispatch_via_api(
|
result = await _dispatch_via_api(
|
||||||
@@ -304,7 +307,9 @@ class TestDispatchViaGitea:
|
|||||||
assert result.status == DispatchStatus.ASSIGNED
|
assert result.status == DispatchStatus.ASSIGNED
|
||||||
|
|
||||||
async def test_no_gitea_token_returns_failed(self):
|
async def test_no_gitea_token_returns_failed(self):
|
||||||
bad_settings = MagicMock(gitea_enabled=True, gitea_token="", gitea_url="http://x", gitea_repo="a/b")
|
bad_settings = MagicMock(
|
||||||
|
gitea_enabled=True, gitea_token="", gitea_url="http://x", gitea_repo="a/b"
|
||||||
|
)
|
||||||
with patch("timmy.dispatcher.settings", bad_settings):
|
with patch("timmy.dispatcher.settings", bad_settings):
|
||||||
result = await _dispatch_via_gitea(
|
result = await _dispatch_via_gitea(
|
||||||
agent=AgentType.CLAUDE_CODE,
|
agent=AgentType.CLAUDE_CODE,
|
||||||
@@ -317,7 +322,9 @@ class TestDispatchViaGitea:
|
|||||||
assert "not configured" in (result.error or "").lower()
|
assert "not configured" in (result.error or "").lower()
|
||||||
|
|
||||||
async def test_gitea_disabled_returns_failed(self):
|
async def test_gitea_disabled_returns_failed(self):
|
||||||
bad_settings = MagicMock(gitea_enabled=False, gitea_token="tok", gitea_url="http://x", gitea_repo="a/b")
|
bad_settings = MagicMock(
|
||||||
|
gitea_enabled=False, gitea_token="tok", gitea_url="http://x", gitea_repo="a/b"
|
||||||
|
)
|
||||||
with patch("timmy.dispatcher.settings", bad_settings):
|
with patch("timmy.dispatcher.settings", bad_settings):
|
||||||
result = await _dispatch_via_gitea(
|
result = await _dispatch_via_gitea(
|
||||||
agent=AgentType.CLAUDE_CODE,
|
agent=AgentType.CLAUDE_CODE,
|
||||||
@@ -368,6 +375,7 @@ class TestDispatchViaGitea:
|
|||||||
# dispatch_task (integration-style)
|
# dispatch_task (integration-style)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestDispatchTask:
|
class TestDispatchTask:
|
||||||
async def test_empty_title_returns_failed(self):
|
async def test_empty_title_returns_failed(self):
|
||||||
result = await dispatch_task(title=" ")
|
result = await dispatch_task(title=" ")
|
||||||
@@ -396,7 +404,9 @@ class TestDispatchTask:
|
|||||||
client_mock = AsyncMock()
|
client_mock = AsyncMock()
|
||||||
client_mock.__aenter__ = AsyncMock(return_value=client_mock)
|
client_mock.__aenter__ = AsyncMock(return_value=client_mock)
|
||||||
client_mock.__aexit__ = AsyncMock(return_value=False)
|
client_mock.__aexit__ = AsyncMock(return_value=False)
|
||||||
client_mock.get = AsyncMock(return_value=MagicMock(status_code=200, json=MagicMock(return_value=[])))
|
client_mock.get = AsyncMock(
|
||||||
|
return_value=MagicMock(status_code=200, json=MagicMock(return_value=[]))
|
||||||
|
)
|
||||||
create_resp = MagicMock(status_code=201, json=MagicMock(return_value={"id": 1}))
|
create_resp = MagicMock(status_code=201, json=MagicMock(return_value={"id": 1}))
|
||||||
apply_resp = MagicMock(status_code=201)
|
apply_resp = MagicMock(status_code=201)
|
||||||
comment_resp = MagicMock(status_code=201, json=MagicMock(return_value={"id": 5}))
|
comment_resp = MagicMock(status_code=201, json=MagicMock(return_value={"id": 5}))
|
||||||
@@ -464,6 +474,7 @@ class TestDispatchTask:
|
|||||||
# wait_for_completion
|
# wait_for_completion
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestWaitForCompletion:
|
class TestWaitForCompletion:
|
||||||
async def test_returns_completed_when_issue_closed(self):
|
async def test_returns_completed_when_issue_closed(self):
|
||||||
closed_resp = MagicMock(
|
closed_resp = MagicMock(
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from timmy.backlog_triage import (
|
|||||||
score_issue,
|
score_issue,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Fixtures ─────────────────────────────────────────────────────────────────
|
# ── Fixtures ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Refs: #1073
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from io import BytesIO
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -79,7 +78,9 @@ def test_get_memory_info_handles_subprocess_failure(monitor):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_memory_ok(monitor):
|
async def test_check_memory_ok(monitor):
|
||||||
with patch.object(monitor, "_get_memory_info", return_value={"free_gb": 20.0, "total_gb": 64.0}):
|
with patch.object(
|
||||||
|
monitor, "_get_memory_info", return_value={"free_gb": 20.0, "total_gb": 64.0}
|
||||||
|
):
|
||||||
result = await monitor._check_memory()
|
result = await monitor._check_memory()
|
||||||
|
|
||||||
assert result.name == "memory"
|
assert result.name == "memory"
|
||||||
@@ -126,7 +127,7 @@ async def test_check_memory_exception_returns_unknown(monitor):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_disk_ok(monitor):
|
async def test_check_disk_ok(monitor):
|
||||||
usage = MagicMock()
|
usage = MagicMock()
|
||||||
usage.free = 100 * (1024**3) # 100 GB
|
usage.free = 100 * (1024**3) # 100 GB
|
||||||
usage.total = 500 * (1024**3) # 500 GB
|
usage.total = 500 * (1024**3) # 500 GB
|
||||||
usage.used = 400 * (1024**3)
|
usage.used = 400 * (1024**3)
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ async def test_check_disk_ok(monitor):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_disk_low_triggers_cleanup(monitor):
|
async def test_check_disk_low_triggers_cleanup(monitor):
|
||||||
usage = MagicMock()
|
usage = MagicMock()
|
||||||
usage.free = 5 * (1024**3) # 5 GB — below threshold
|
usage.free = 5 * (1024**3) # 5 GB — below threshold
|
||||||
usage.total = 500 * (1024**3)
|
usage.total = 500 * (1024**3)
|
||||||
usage.used = 495 * (1024**3)
|
usage.used = 495 * (1024**3)
|
||||||
|
|
||||||
@@ -176,12 +177,8 @@ async def test_check_disk_critical_when_cleanup_fails(monitor):
|
|||||||
|
|
||||||
|
|
||||||
def test_get_ollama_status_reachable(monitor):
|
def test_get_ollama_status_reachable(monitor):
|
||||||
tags_body = json.dumps({
|
tags_body = json.dumps({"models": [{"name": "qwen3:30b"}, {"name": "llama3.1:8b"}]}).encode()
|
||||||
"models": [{"name": "qwen3:30b"}, {"name": "llama3.1:8b"}]
|
ps_body = json.dumps({"models": [{"name": "qwen3:30b", "size": 1000}]}).encode()
|
||||||
}).encode()
|
|
||||||
ps_body = json.dumps({
|
|
||||||
"models": [{"name": "qwen3:30b", "size": 1000}]
|
|
||||||
}).encode()
|
|
||||||
|
|
||||||
responses = [
|
responses = [
|
||||||
_FakeHTTPResponse(tags_body),
|
_FakeHTTPResponse(tags_body),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import pytest
|
|||||||
|
|
||||||
from timmy.vassal.agent_health import AgentHealthReport, AgentStatus
|
from timmy.vassal.agent_health import AgentHealthReport, AgentStatus
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# AgentStatus
|
# AgentStatus
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -49,9 +48,7 @@ def test_report_any_stuck():
|
|||||||
|
|
||||||
|
|
||||||
def test_report_all_idle():
|
def test_report_all_idle():
|
||||||
report = AgentHealthReport(
|
report = AgentHealthReport(agents=[AgentStatus(agent="claude"), AgentStatus(agent="kimi")])
|
||||||
agents=[AgentStatus(agent="claude"), AgentStatus(agent="kimi")]
|
|
||||||
)
|
|
||||||
assert report.all_idle is True
|
assert report.all_idle is True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import pytest
|
|||||||
|
|
||||||
from timmy.vassal.backlog import (
|
from timmy.vassal.backlog import (
|
||||||
AgentTarget,
|
AgentTarget,
|
||||||
TriagedIssue,
|
|
||||||
_choose_agent,
|
_choose_agent,
|
||||||
_extract_labels,
|
_extract_labels,
|
||||||
_score_priority,
|
_score_priority,
|
||||||
triage_issues,
|
triage_issues,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _extract_labels
|
# _extract_labels
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from timmy.vassal.house_health import (
|
|||||||
_probe_disk,
|
_probe_disk,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Data model tests
|
# Data model tests
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import pytest
|
|||||||
|
|
||||||
from timmy.vassal.orchestration_loop import VassalCycleRecord, VassalOrchestrator
|
from timmy.vassal.orchestration_loop import VassalCycleRecord, VassalOrchestrator
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# VassalCycleRecord
|
# VassalCycleRecord
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -134,6 +133,6 @@ def test_orchestrator_stop_when_not_running():
|
|||||||
|
|
||||||
|
|
||||||
def test_module_singleton_exists():
|
def test_module_singleton_exists():
|
||||||
from timmy.vassal import vassal_orchestrator, VassalOrchestrator
|
from timmy.vassal import VassalOrchestrator, vassal_orchestrator
|
||||||
|
|
||||||
assert isinstance(vassal_orchestrator, VassalOrchestrator)
|
assert isinstance(vassal_orchestrator, VassalOrchestrator)
|
||||||
|
|||||||
Reference in New Issue
Block a user