Compare commits

...

3 Commits

12 changed files with 215 additions and 10 deletions

View File

@@ -1,10 +1,10 @@
{
"last_dolt_commit": "v0626gcsbms86q235ic8075e3pto4mhe",
"last_dolt_commit": "qqlkjgnbnf7iatesc1ummn7e86llmv0o",
"last_event_id": 0,
"timestamp": "2026-03-02T03:00:11.7685764Z",
"timestamp": "2026-03-02T03:18:05.069295Z",
"counts": {
"issues": 10,
"events": 13,
"events": 16,
"comments": 0,
"dependencies": 14,
"labels": 0,

View File

@@ -11,3 +11,6 @@
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T21:33:41Z","event_type":"claimed","id":11,"issue_id":"rook-bfj","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-bfj\",\"title\":\"Project scaffolding: pyproject.toml, src layout, uv setup\",\"description\":\"Initialize Python project. pyproject.toml with deps (anthropic, elevenlabs, python-telegram-bot, dearpygui). Set up src/rook/ package. Configure uv.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:29Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:29Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T21:38:13Z","event_type":"closed","id":12,"issue_id":"rook-bfj","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-bfj\",\"title\":\"Project scaffolding: pyproject.toml, src layout, uv setup\",\"description\":\"Initialize Python project. pyproject.toml with deps (anthropic, elevenlabs, python-telegram-bot, dearpygui). Set up src/rook/ package. Configure uv.\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"assignee\":\"Ed_\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:29Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:33:42Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:00:11Z","event_type":"claimed","id":13,"issue_id":"rook-z5s","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-z5s\",\"title\":\"Core agent loop: asyncio, Claude API, tool dispatch\",\"description\":\"Main agent turn loop. Claude Haiku 4.5 primary, Sonnet 4.6 fallback. Tool call dispatch. Asyncio event loop with background daemon thread support.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:30Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:30Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:04:02Z","event_type":"closed","id":14,"issue_id":"rook-z5s","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-z5s\",\"title\":\"Core agent loop: asyncio, Claude API, tool dispatch\",\"description\":\"Main agent turn loop. Claude Haiku 4.5 primary, Sonnet 4.6 fallback. Tool call dispatch. Asyncio event loop with background daemon thread support.\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"assignee\":\"Ed_\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:30Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T03:00:12Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:14:30Z","event_type":"claimed","id":15,"issue_id":"rook-dn4","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-dn4\",\"title\":\"Policy engine: allowlists, confirm gates, backup-before-edit\",\"description\":\"Core policy module. Allowlists per capability (file, shell, git). confirm_spawn() gate for risky ops. backup_before_edit() helper. Approved working dirs enforcement.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:29Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:29Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:18:04Z","event_type":"closed","id":16,"issue_id":"rook-dn4","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-dn4\",\"title\":\"Policy engine: allowlists, confirm gates, backup-before-edit\",\"description\":\"Core policy module. Allowlists per capability (file, shell, git). confirm_spawn() gate for risky ops. backup_before_edit() helper. Approved working dirs enforcement.\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"assignee\":\"Ed_\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:29Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T03:14:31Z\"}"}

View File

@@ -3,8 +3,8 @@
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4c5f1fdc2ddb1c860cc8f07897a384cc32f8adbd99dd0914128a747e4b439dc3","created_at":"2026-03-02T02:14:31Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Run shell commands with policy checks. Allowlist of approved dirs and commands. confirm_spawn() gate. Log all subprocess calls with timestamps.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-6zs","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Shell capability: safe subprocess execution","updated_at":"2026-03-02T02:14:31Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"484cebdc8e9e90f14a3dab875ebabff4591a3be96365feaa5a3c565d88de9557","created_at":"2026-03-02T02:14:30Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Telegram bot daemon thread. Poll for messages and audio notes. Pass audio through STT. Route transcribed text to agent queue via asyncio.run_coroutine_threadsafe().","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-8o3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Telegram integration: polling, audio STT input","updated_at":"2026-03-02T02:14:30Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Ed_","await_id":"","await_type":"","close_reason":"","closed_at":"2026-03-02T02:38:14Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"281bb6b78c0987562762da2cafe040ff3a9e6c3b00f429a978eaf10810c336cb","created_at":"2026-03-02T02:14:29Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Initialize Python project. pyproject.toml with deps (anthropic, elevenlabs, python-telegram-bot, dearpygui). Set up src/rook/ package. Configure uv.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-bfj","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Project scaffolding: pyproject.toml, src layout, uv setup","updated_at":"2026-03-02T02:38:14Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"0d909735f985b5028b9f49b5430097b81a618db45fb77ae7998ec418cfd31216","created_at":"2026-03-02T02:14:29Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Core policy module. Allowlists per capability (file, shell, git). confirm_spawn() gate for risky ops. backup_before_edit() helper. Approved working dirs enforcement.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-dn4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Policy engine: allowlists, confirm gates, backup-before-edit","updated_at":"2026-03-02T02:14:29Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Ed_","await_id":"","await_type":"","close_reason":"","closed_at":"2026-03-02T03:18:05Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"0d909735f985b5028b9f49b5430097b81a618db45fb77ae7998ec418cfd31216","created_at":"2026-03-02T02:14:29Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Core policy module. Allowlists per capability (file, shell, git). confirm_spawn() gate for risky ops. backup_before_edit() helper. Approved working dirs enforcement.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-dn4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Policy engine: allowlists, confirm gates, backup-before-edit","updated_at":"2026-03-02T03:18:05Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"5b9bc779e38b599a57edb6a2374a863a62adc559b106e23e6d266e8b7a7c9079","created_at":"2026-03-02T02:14:30Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Speak agent responses. ElevenLabs TTS with Rook voice ID (scratchy/military). Google TTS fallback. Async playback so it doesn't block the agent loop.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-hi1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"TTS output: ElevenLabs Rook voice + Google TTS fallback","updated_at":"2026-03-02T02:14:30Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"20afcd532ae263f8dbd89ebcc93dd71177c9f9f5d185dc716b9319b353fbf6b1","created_at":"2026-03-02T02:14:32Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Dear PyGui dark-theme GUI. Dockable panels for agent transcript, TTS status, CoSy output. Runs on main thread; asyncio on background daemon thread.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-iwn","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Dear PyGui GUI: ModernCoSy dockable panels","updated_at":"2026-03-02T02:14:32Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b1d9f7aa2ce83b5c655f8f56cb29fba8a3e73f09fa1522bd56a1db9cd9206d57","created_at":"2026-03-02T02:14:32Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Git operations (status, diff, add, commit, log). Allowlist gates on push/reset/force ops. Confirmation required for destructive git commands.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-wka","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Git capability: git ops with policy allowlist","updated_at":"2026-03-02T02:14:32Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Ed_","await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"911e58b2f36018c64b9bd4fdfec5dc4fffbca208deea583105ff268f501f5fbc","created_at":"2026-03-02T02:14:30Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Main agent turn loop. Claude Haiku 4.5 primary, Sonnet 4.6 fallback. Tool call dispatch. Asyncio event loop with background daemon thread support.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-z5s","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"in_progress","target":"","timeout_ns":0,"title":"Core agent loop: asyncio, Claude API, tool dispatch","updated_at":"2026-03-02T03:00:12Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Ed_","await_id":"","await_type":"","close_reason":"","closed_at":"2026-03-02T03:04:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"911e58b2f36018c64b9bd4fdfec5dc4fffbca208deea583105ff268f501f5fbc","created_at":"2026-03-02T02:14:30Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Main agent turn loop. Claude Haiku 4.5 primary, Sonnet 4.6 fallback. Tool call dispatch. Asyncio event loop with background daemon thread support.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-z5s","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"edwardgz@gmail.com","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Core agent loop: asyncio, Claude API, tool dispatch","updated_at":"2026-03-02T03:04:02Z","waiters":"","wisp_type":"","work_type":""}

View File

@@ -1 +1 @@
80236
57728

View File

@@ -1 +1 @@
1772417621
1772420411

View File

@@ -84,7 +84,7 @@ git notes add -m "{task_id} — {summary of changes} — {files changed}" $sha
### 8. Mark Done
```powershell
cd C:\projects\rook
bd update <id> --status done
bd update <id> --status closed
```
### 9. Next Task or Phase Verification

8
.gitignore vendored
View File

@@ -2,3 +2,11 @@
# Dolt database files (added by bd init)
.dolt/
*.db
# Python
__pycache__/
*.pyc
.coverage
.venv/
*.egg-info/
dist/

View File

@@ -68,7 +68,7 @@ Beads replaces `plan.md`. Same discipline, different storage:
cd C:\projects\rook
bd ready --json # find unblocked tasks (start here)
bd update <id> --claim # mark in-progress before starting
bd update <id> --status done # mark complete after commit + git note
bd update <id> --status closed # mark complete after commit + git note
bd create --title "..." --description "..." # create a new task
bd dep add <id> <depends-on-id> # link dependency
bd list --json # list all tasks
@@ -117,7 +117,7 @@ All work follows this strict lifecycle. **MMA tiered delegation is mandatory —
git notes add -m "<task id> — <summary of changes and why>" <sha>
```
8. **Mark done**: `bd update <id> --status done`
8. **Mark done**: `bd update <id> --status closed`
### Worker Prompt Rules

54
src/rook/agent.py Normal file
View File

@@ -0,0 +1,54 @@
import asyncio
import threading
from typing import Any, Callable, Optional
import anthropic
PRIMARY_MODEL: str = 'claude-haiku-4-5-20251001'
FALLBACK_MODEL: str = 'claude-sonnet-4-6'
class AgentLoop:
def __init__(self, system: str, model: str = PRIMARY_MODEL) -> None:
self.model = model
self.system = system
self.history: list[dict] = []
self.tools: dict[str, Any] = {}
self._client = anthropic.Anthropic()
def register_tool(self, name: str, fn: Callable[..., Any]) -> None:
self.tools[name] = fn
async def send(self, user_message: str) -> str:
self.history.append({'role': 'user', 'content': user_message})
response = self._client.messages.create(
model=self.model,
system=self.system,
messages=self.history,
max_tokens=4096,
)
while any(block.type == 'tool_use' for block in response.content):
for block in response.content:
if block.type == 'tool_use':
result = self.tools[block.name](**block.input)
self.history.append({'role': 'assistant', 'content': response.content})
self.history.append({
'role': 'user',
'content': [{'type': 'tool_result', 'tool_use_id': block.id, 'content': str(result)}],
})
response = self._client.messages.create(
model=self.model,
system=self.system,
messages=self.history,
max_tokens=4096,
)
break
for block in response.content:
if block.type == 'text':
return block.text
return ''
def run_in_thread(self) -> threading.Thread:
self._loop = asyncio.new_event_loop()
t = threading.Thread(target=self._loop.run_forever, daemon=True)
t.start()
return t

34
src/rook/policy.py Normal file
View File

@@ -0,0 +1,34 @@
import os
import shutil
APPROVED_DIRS: list[str] = [
os.path.abspath(os.path.expanduser('~/dev')),
os.path.abspath(os.path.expanduser('~/logs')),
os.path.abspath(os.path.expanduser('~/ModernCoSy')),
]
CAPABILITY_ALLOWLISTS: dict[str, list[str]] = {
'file': ['read', 'write', 'edit'],
'shell': ['run'],
'git': ['status', 'log', 'diff', 'add', 'commit'],
}
def is_approved_dir(path: str) -> bool:
resolved = os.path.abspath(path)
return any(resolved.startswith(d) for d in APPROVED_DIRS)
def confirm_spawn(action: str, details: str) -> bool:
response = input(f"Allow '{action}' with '{details}'? [y/n]: ")
return response.strip().lower() == 'y'
def backup_before_edit(file_path: str) -> str:
backup_path = file_path + '.bak'
shutil.copy2(file_path, backup_path)
return backup_path
def check_allowlist(capability: str, operation: str) -> bool:
return operation in CAPABILITY_ALLOWLISTS.get(capability, [])

64
tests/test_agent.py Normal file
View File

@@ -0,0 +1,64 @@
import asyncio
import pytest
from unittest.mock import MagicMock, patch
from rook.agent import AgentLoop, PRIMARY_MODEL, FALLBACK_MODEL
def test_agent_loop_init():
loop = AgentLoop(system='You are Rook.')
assert loop.model == PRIMARY_MODEL
assert loop.history == []
assert isinstance(loop.tools, dict)
def test_register_tool():
loop = AgentLoop(system='You are Rook.')
loop.register_tool('ping', lambda: 'pong')
assert 'ping' in loop.tools
def test_send_returns_string():
mock_text_block = MagicMock()
mock_text_block.type = 'text'
mock_text_block.text = 'Hello'
mock_response = MagicMock()
mock_response.content = [mock_text_block]
mock_client = MagicMock()
mock_client.messages.create.return_value = mock_response
with patch('anthropic.Anthropic', return_value=mock_client):
loop = AgentLoop(system='You are Rook.')
result = asyncio.run(loop.send('hi'))
assert result == 'Hello'
def test_tool_dispatch_called():
tool_use_block = MagicMock()
tool_use_block.type = 'tool_use'
tool_use_block.name = 'ping'
tool_use_block.id = 'tu_1'
tool_use_block.input = {}
first_response = MagicMock()
first_response.content = [tool_use_block]
text_block = MagicMock()
text_block.type = 'text'
text_block.text = 'done'
second_response = MagicMock()
second_response.content = [text_block]
mock_client = MagicMock()
mock_client.messages.create.side_effect = [first_response, second_response]
ping_fn = MagicMock(return_value='pong')
with patch('anthropic.Anthropic', return_value=mock_client):
loop = AgentLoop(system='You are Rook.')
loop.register_tool('ping', ping_fn)
result = asyncio.run(loop.send('call ping'))
ping_fn.assert_called_once()
assert result == 'done'
def test_run_in_thread():
loop = AgentLoop(system='You are Rook.')
t = loop.run_in_thread()
assert t.is_alive()
assert t.daemon == True
t.join(timeout=0.1)

42
tests/test_policy.py Normal file
View File

@@ -0,0 +1,42 @@
import os
from unittest.mock import patch
import pytest
from rook.policy import is_approved_dir, confirm_spawn, backup_before_edit, check_allowlist
def test_is_approved_dir_true():
assert is_approved_dir(os.path.expanduser('~/dev/myproject')) == True
def test_is_approved_dir_false():
assert is_approved_dir('/tmp/evil') == False
def test_is_approved_dir_subpath():
assert is_approved_dir(os.path.expanduser('~/logs/app.log')) == True
def test_confirm_spawn_yes():
with patch('builtins.input', return_value='y'):
assert confirm_spawn('git push', 'origin main') == True
def test_confirm_spawn_no():
with patch('builtins.input', return_value='n'):
assert confirm_spawn('rm file', 'myfile.txt') == False
def test_backup_before_edit(tmp_path):
tmp_file = tmp_path / 'sample.txt'
tmp_file.write_text('hello')
backup_path = backup_before_edit(str(tmp_file))
assert os.path.exists(backup_path)
assert open(backup_path).read() == 'hello'
def test_check_allowlist():
assert check_allowlist('file', 'read') == True
assert check_allowlist('file', 'delete') == False
assert check_allowlist('git', 'push') == False
assert check_allowlist('git', 'commit') == True