Skip to content

Commit 82d5f9a

Browse files
authored
Fix CI quality gates and cross-platform Bash selection (#1)
Resolve Ruff and MyPy failures, pin development tooling, update Actions, and make Bash selection work on Windows.
1 parent 5be190b commit 82d5f9a

27 files changed

Lines changed: 484 additions & 268 deletions

.github/workflows/ci.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,40 @@ name: CI
22

33
on:
44
push:
5-
branches: ["main", "master"]
5+
branches: ["main"]
66
pull_request:
7-
branches: ["main", "master"]
7+
branches: ["main"]
8+
9+
permissions:
10+
contents: read
811

912
jobs:
1013
test-lint-typecheck:
1114
runs-on: ubuntu-latest
1215

1316
steps:
1417
- name: Checkout code
15-
uses: actions/checkout@v4
18+
uses: actions/checkout@v7
1619

1720
- name: Set up Python
18-
uses: actions/setup-python@v5
21+
uses: actions/setup-python@v6
1922
with:
2023
python-version: "3.11"
2124
cache: "pip"
2225

2326
- name: Install dependencies
2427
run: |
2528
python -m pip install --upgrade pip
26-
pip install -r requirements.txt
27-
pip install ruff black mypy
29+
pip install -r requirements-dev.txt
2830
29-
- name: Ruff (lint + auto-fix)
30-
run: |
31-
ruff check . --fix
32-
ruff check .
31+
- name: Ruff
32+
run: ruff check .
3333

3434
- name: Black (format check)
3535
run: black --check .
3636

37-
- name: MyPy (type check, non-blocking for now)
38-
run: mypy . || true
37+
- name: MyPy
38+
run: mypy .
3939

4040
- name: Pytest
4141
run: pytest -v --tb=short

agent.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,31 @@
1616

1717
import random
1818
import time
19-
from typing import Annotated, TypedDict, Iterator
19+
from typing import Annotated, TypedDict
2020

2121
from dotenv import load_dotenv
2222
from langchain_anthropic import ChatAnthropic
23-
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage
24-
from langgraph.graph import StateGraph, END
23+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
24+
from langgraph.graph import END, StateGraph
2525
from langgraph.graph.message import add_messages
2626
from langgraph.prebuilt import ToolNode
2727

2828
from config import config
2929
from context import build_system_prompt
30-
from tools import ALL_TOOLS
31-
from cost_tracker import track_call, init_tracker, register_exit_hook
30+
from cost_tracker import init_tracker, register_exit_hook, track_call
3231
from exceptions import ConfigError, RetryExhaustedError
3332
from logger import get_logger
3433
from metrics import record_request
3534
from rate_limiter import acquire_or_raise
3635
from task import (
37-
TaskType, TaskStatus,
38-
create_task, complete_task, fail_task, get_duration,
36+
TaskStatus,
37+
TaskType,
38+
complete_task,
39+
create_task,
40+
fail_task,
41+
get_duration,
3942
)
43+
from tools import ALL_TOOLS
4044

4145
load_dotenv()
4246
log = get_logger("agent")
@@ -221,7 +225,7 @@ def chat_stream(self, user_input, history=None):
221225
log.debug("Tool call detected in stream, switching to graph")
222226
response = self.chat(user_input, history)
223227
if collected_text and response.startswith(collected_text):
224-
yield response[len(collected_text):]
228+
yield response[len(collected_text) :]
225229
else:
226230
yield response
227231
success = True
@@ -264,7 +268,8 @@ def _extract_text(messages):
264268
if isinstance(msg, AIMessage) and msg.content:
265269
if isinstance(msg.content, list):
266270
parts = [
267-
p["text"] for p in msg.content
271+
p["text"]
272+
for p in msg.content
268273
if isinstance(p, dict) and p.get("type") == "text"
269274
]
270275
if parts:
@@ -289,7 +294,9 @@ def get_task_summary(self):
289294
if not self.task_history:
290295
return "No tasks have run yet."
291296

292-
completed = sum(1 for t in self.task_history if t.status == TaskStatus.COMPLETED)
297+
completed = sum(
298+
1 for t in self.task_history if t.status == TaskStatus.COMPLETED
299+
)
293300
failed = sum(1 for t in self.task_history if t.status == TaskStatus.FAILED)
294301
total = len(self.task_history)
295302

config.py

Lines changed: 119 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import os
77
from dataclasses import dataclass, field
8+
89
from dotenv import load_dotenv
910

1011
load_dotenv()
@@ -34,75 +35,163 @@ def _env_float(name: str, default: float) -> float:
3435
@dataclass(frozen=True)
3536
class ModelConfig:
3637
"""LLM model settings."""
37-
name: str = field(default_factory=lambda: os.getenv("DEVMIND_MODEL", "claude-sonnet-4-6"))
38-
max_tokens: int = field(default_factory=lambda: _env_int("DEVMIND_MAX_TOKENS", 4096))
38+
39+
name: str = field(
40+
default_factory=lambda: os.getenv("DEVMIND_MODEL", "claude-sonnet-4-6")
41+
)
42+
max_tokens: int = field(
43+
default_factory=lambda: _env_int("DEVMIND_MAX_TOKENS", 4096)
44+
)
3945
streaming: bool = True
4046

4147

4248
@dataclass(frozen=True)
4349
class RetryConfig:
4450
"""Retry logic settings with exponential backoff + jitter."""
51+
4552
max_retries: int = field(default_factory=lambda: _env_int("DEVMIND_MAX_RETRIES", 4))
46-
base_delay: float = field(default_factory=lambda: _env_float("DEVMIND_RETRY_BASE_DELAY", 1.0))
47-
max_delay: float = field(default_factory=lambda: _env_float("DEVMIND_RETRY_MAX_DELAY", 30.0))
53+
base_delay: float = field(
54+
default_factory=lambda: _env_float("DEVMIND_RETRY_BASE_DELAY", 1.0)
55+
)
56+
max_delay: float = field(
57+
default_factory=lambda: _env_float("DEVMIND_RETRY_MAX_DELAY", 30.0)
58+
)
4859
backoff_factor: float = 2.0
4960
jitter: float = 0.25
5061
retryable_codes: tuple = (
51-
"529", "overloaded", "rate_limit",
52-
"timeout", "connection", "502", "503", "504",
53-
"ECONNRESET", "Temporary failure",
62+
"529",
63+
"overloaded",
64+
"rate_limit",
65+
"timeout",
66+
"connection",
67+
"502",
68+
"503",
69+
"504",
70+
"ECONNRESET",
71+
"Temporary failure",
5472
)
5573

5674

5775
@dataclass(frozen=True)
5876
class RateLimitConfig:
5977
"""Client-side rate limiting (token bucket)."""
78+
6079
enabled: bool = field(default_factory=lambda: _env_bool("DEVMIND_RATE_LIMIT", True))
61-
requests_per_minute: int = field(default_factory=lambda: _env_int("DEVMIND_RPM", 50))
80+
requests_per_minute: int = field(
81+
default_factory=lambda: _env_int("DEVMIND_RPM", 50)
82+
)
6283
burst: int = field(default_factory=lambda: _env_int("DEVMIND_BURST", 10))
6384

6485

6586
@dataclass(frozen=True)
6687
class ToolConfig:
6788
"""Tool execution settings."""
68-
bash_timeout: int = field(default_factory=lambda: _env_int("DEVMIND_BASH_TIMEOUT", 30))
89+
90+
bash_timeout: int = field(
91+
default_factory=lambda: _env_int("DEVMIND_BASH_TIMEOUT", 30)
92+
)
6993
file_read_max_lines: int = 200
7094
file_write_max_bytes: int = 5 * 1024 * 1024
7195
file_read_max_bytes: int = 2 * 1024 * 1024
7296
grep_max_results: int = 20
7397
grep_max_files: int = 5000
7498
blocked_commands: tuple = (
75-
"rm -rf /", "rm -rf /*", "rm -rf ~", "rm -rf ~/",
76-
"mkfs", "dd if=", "dd of=/dev/",
77-
":(){", "fork bomb", "chmod -R 777 /",
78-
"wget ", "curl ",
79-
"shutdown", "reboot", "halt", "poweroff",
80-
"> /dev/sda", "mv / ",
99+
"rm -rf /",
100+
"rm -rf /*",
101+
"rm -rf ~",
102+
"rm -rf ~/",
103+
"mkfs",
104+
"dd if=",
105+
"dd of=/dev/",
106+
":(){",
107+
"fork bomb",
108+
"chmod -R 777 /",
109+
"wget ",
110+
"curl ",
111+
"shutdown",
112+
"reboot",
113+
"halt",
114+
"poweroff",
115+
"> /dev/sda",
116+
"mv / ",
117+
)
118+
text_extensions: frozenset = frozenset(
119+
{
120+
".py",
121+
".js",
122+
".ts",
123+
".jsx",
124+
".tsx",
125+
".java",
126+
".c",
127+
".cpp",
128+
".h",
129+
".cs",
130+
".go",
131+
".rs",
132+
".rb",
133+
".php",
134+
".swift",
135+
".kt",
136+
".scala",
137+
".html",
138+
".css",
139+
".scss",
140+
".less",
141+
".xml",
142+
".json",
143+
".yaml",
144+
".yml",
145+
".toml",
146+
".ini",
147+
".cfg",
148+
".conf",
149+
".env",
150+
".sh",
151+
".bash",
152+
".zsh",
153+
".md",
154+
".txt",
155+
".rst",
156+
".csv",
157+
".sql",
158+
".r",
159+
".R",
160+
".lua",
161+
".dockerfile",
162+
".makefile",
163+
".gitignore",
164+
".editorconfig",
165+
}
166+
)
167+
skip_dirs: frozenset = frozenset(
168+
{
169+
".git",
170+
"__pycache__",
171+
"node_modules",
172+
".venv",
173+
"venv",
174+
".mypy_cache",
175+
".pytest_cache",
176+
"dist",
177+
"build",
178+
".tox",
179+
}
81180
)
82-
text_extensions: frozenset = frozenset({
83-
".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".c", ".cpp", ".h",
84-
".cs", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala",
85-
".html", ".css", ".scss", ".less", ".xml", ".json", ".yaml", ".yml",
86-
".toml", ".ini", ".cfg", ".conf", ".env", ".sh", ".bash", ".zsh",
87-
".md", ".txt", ".rst", ".csv", ".sql", ".r", ".R", ".lua",
88-
".dockerfile", ".makefile", ".gitignore", ".editorconfig",
89-
})
90-
skip_dirs: frozenset = frozenset({
91-
".git", "__pycache__", "node_modules", ".venv", "venv",
92-
".mypy_cache", ".pytest_cache", "dist", "build", ".tox",
93-
})
94181

95182

96183
@dataclass(frozen=True)
97184
class HistoryConfig:
98185
"""Conversation history settings."""
186+
99187
max_messages: int = 20
100188
summary_threshold: int = 16
101189

102190

103191
@dataclass(frozen=True)
104192
class PersistenceConfig:
105193
"""Conversation save/load settings."""
194+
106195
enabled: bool = field(default_factory=lambda: _env_bool("DEVMIND_PERSIST", True))
107196
sessions_dir: str = field(
108197
default_factory=lambda: os.getenv(
@@ -116,13 +205,15 @@ class PersistenceConfig:
116205
@dataclass(frozen=True)
117206
class MetricsConfig:
118207
"""Performance metrics tracking."""
208+
119209
enabled: bool = field(default_factory=lambda: _env_bool("DEVMIND_METRICS", True))
120210
persist_on_exit: bool = True
121211

122212

123213
@dataclass(frozen=True)
124214
class PluginConfig:
125215
"""Plugin/extension system settings."""
216+
126217
enabled: bool = field(default_factory=lambda: _env_bool("DEVMIND_PLUGINS", True))
127218
plugins_dir: str = field(
128219
default_factory=lambda: os.getenv(
@@ -135,6 +226,7 @@ class PluginConfig:
135226
@dataclass(frozen=True)
136227
class AppConfig:
137228
"""Master application configuration."""
229+
138230
model: ModelConfig = field(default_factory=ModelConfig)
139231
retry: RetryConfig = field(default_factory=RetryConfig)
140232
rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)

context.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import subprocess
1414
from datetime import datetime
1515
from pathlib import Path
16-
from typing import Optional
1716

1817
from logger import log
1918

@@ -37,7 +36,7 @@ def _run_git(args: list[str]) -> str:
3736
return ""
3837

3938

40-
def get_git_status() -> Optional[str]:
39+
def get_git_status() -> str | None:
4140
"""
4241
Return a summary of the current git repository state.
4342
(Inspired by context.ts: getGitStatus)
@@ -74,7 +73,7 @@ def get_git_status() -> Optional[str]:
7473
# CLAUDE.md — Project-specific instructions
7574
# (Inspired by context.ts: getClaudeMds pattern)
7675
# ============================================================
77-
def get_claude_md() -> Optional[str]:
76+
def get_claude_md() -> str | None:
7877
"""
7978
Read CLAUDE.md if it exists.
8079
This file contains project-specific instructions for Claude.
@@ -138,7 +137,10 @@ def build_system_prompt() -> str:
138137
"- bash_tool: Run terminal commands (git, python, etc.)",
139138
"- file_read_tool: Read file contents",
140139
"- file_write_tool: Create new files or fully overwrite existing ones",
141-
"- file_edit_tool: Find and replace specific text in an existing file (bug fixes, code updates)",
140+
(
141+
"- file_edit_tool: Find and replace specific text in an existing file "
142+
"(bug fixes, code updates)"
143+
),
142144
"- grep_tool: Search for patterns across the codebase",
143145
"- list_files_tool: List files in a directory",
144146
"",

0 commit comments

Comments
 (0)