Compare commits

..

10 Commits

17 changed files with 1611 additions and 47 deletions

View File

@@ -1,10 +1,10 @@
{ {
"last_dolt_commit": "qqlkjgnbnf7iatesc1ummn7e86llmv0o", "last_dolt_commit": "6bdv5ku8o7amuah7crbk8vnf4tmai8m6",
"last_event_id": 0, "last_event_id": 0,
"timestamp": "2026-03-02T03:18:05.069295Z", "timestamp": "2026-03-02T03:52:13.6279948Z",
"counts": { "counts": {
"issues": 10, "issues": 10,
"events": 16, "events": 29,
"comments": 0, "comments": 0,
"dependencies": 14, "dependencies": 14,
"labels": 0, "labels": 0,

View File

@@ -14,3 +14,16 @@
{"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: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: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\"}"} {"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\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:26:20Z","event_type":"claimed","id":17,"issue_id":"rook-6zs","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-6zs\",\"title\":\"Shell capability: safe subprocess execution\",\"description\":\"Run shell commands with policy checks. Allowlist of approved dirs and commands. confirm_spawn() gate. Log all subprocess calls with timestamps.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:31Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:31Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:28:26Z","event_type":"closed","id":18,"issue_id":"rook-6zs","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-6zs\",\"title\":\"Shell capability: safe subprocess execution\",\"description\":\"Run shell commands with policy checks. Allowlist of approved dirs and commands. confirm_spawn() gate. Log all subprocess calls with timestamps.\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"assignee\":\"Ed_\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:31Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T03:26:21Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:28:35Z","event_type":"claimed","id":19,"issue_id":"rook-43b","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-43b\",\"title\":\"File capability: read/write/edit with backup\",\"description\":\"File operations with backup-before-edit. Allowlist for working dirs. No destructive deletes without confirmation. Type-hinted API.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:31Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:31Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:31:21Z","event_type":"closed","id":20,"issue_id":"rook-43b","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-43b\",\"title\":\"File capability: read/write/edit with backup\",\"description\":\"File operations with backup-before-edit. Allowlist for working dirs. No destructive deletes without confirmation. Type-hinted API.\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"assignee\":\"Ed_\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:31Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T03:28:35Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:31:25Z","event_type":"claimed","id":21,"issue_id":"rook-wka","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-wka\",\"title\":\"Git capability: git ops with policy allowlist\",\"description\":\"Git operations (status, diff, add, commit, log). Allowlist gates on push/reset/force ops. Confirmation required for destructive git commands.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:32Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:32Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:34:56Z","event_type":"closed","id":22,"issue_id":"rook-wka","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-wka\",\"title\":\"Git capability: git ops with policy allowlist\",\"description\":\"Git operations (status, diff, add, commit, log). Allowlist gates on push/reset/force ops. Confirmation required for destructive git commands.\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"assignee\":\"Ed_\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:32Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T03:31:26Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:36:47Z","event_type":"updated","id":23,"issue_id":"rook-8o3","new_value":"{\"description\":\"Discord bot daemon thread. Poll for messages and audio attachments. Pass audio through STT. Route transcribed text to agent queue via asyncio.run_coroutine_threadsafe(). Use discord.py (pip: discord.py). Bot token from DISCORD_BOT_TOKEN env var. Listen in a designated channel.\",\"title\":\"Discord integration: polling, audio STT input\"}","old_value":"{\"id\":\"rook-8o3\",\"title\":\"Telegram integration: polling, audio STT input\",\"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().\",\"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:37:49Z","event_type":"claimed","id":24,"issue_id":"rook-11u","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-11u\",\"title\":\"CoSy integration: subprocess pipe to CoSy.bat/reva.exe\",\"description\":\"Launch CoSy as persistent subprocess via Popen. stdin/stdout pipe. Send Forth-APL expressions, read results. Backup before redefining words. Only load from personal vocab dir.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:32Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:32Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:39:54Z","event_type":"closed","id":25,"issue_id":"rook-11u","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-11u\",\"title\":\"CoSy integration: subprocess pipe to CoSy.bat/reva.exe\",\"description\":\"Launch CoSy as persistent subprocess via Popen. stdin/stdout pipe. Send Forth-APL expressions, read results. Backup before redefining words. Only load from personal vocab dir.\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"assignee\":\"Ed_\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:32Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T03:37:50Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:45:48Z","event_type":"updated","id":26,"issue_id":"rook-iwn","new_value":"{\"description\":\"GUI using imgui-bundle (NOT dearpygui — multi-viewport issues). Dockable panels: agent transcript, TTS status, CoSy output. Dark theme. Runs on main thread; asyncio on background daemon thread. Use imgui_bundle.imgui and imgui_bundle.hello_imgui. No dearpygui anywhere.\",\"title\":\"imgui-bundle GUI: dockable panels for agent transcript and status\"}","old_value":"{\"id\":\"rook-iwn\",\"title\":\"Dear PyGui GUI: ModernCoSy dockable panels\",\"description\":\"Dear PyGui dark-theme GUI. Dockable panels for agent transcript, TTS status, CoSy output. Runs on main thread; asyncio on background daemon thread.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"edwardgz@gmail.com\",\"created_at\":\"2026-03-02T02:14:32Z\",\"created_by\":\"Ed_\",\"updated_at\":\"2026-03-02T02:14:32Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:45:48Z","event_type":"updated","id":27,"issue_id":"rook-8o3","new_value":"{\"description\":\"Discord bot daemon thread. Poll for messages and audio attachments. Pass audio through STT. STT backend pluggable via STT_BACKEND env var: 'whisper' uses openai.Audio.transcribe (OPENAI_API_KEY), 'google' uses google.cloud.speech (GOOGLE_STT_KEY). Route transcribed text to agent queue via asyncio.run_coroutine_threadsafe(). Use discord.py. Bot token from DISCORD_BOT_TOKEN env var. Listen in a designated channel.\"}","old_value":"{\"id\":\"rook-8o3\",\"title\":\"Discord integration: polling, audio STT input\",\"description\":\"Discord bot daemon thread. Poll for messages and audio attachments. Pass audio through STT. Route transcribed text to agent queue via asyncio.run_coroutine_threadsafe(). Use discord.py (pip: discord.py). Bot token from DISCORD_BOT_TOKEN env var. Listen in a designated channel.\",\"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-02T03:36:47Z\"}"}
{"actor":"Ed_","comment":null,"created_at":"2026-03-01T22:48:40Z","event_type":"claimed","id":28,"issue_id":"rook-hi1","new_value":"{\"assignee\":\"Ed_\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"rook-hi1\",\"title\":\"TTS output: ElevenLabs Rook voice + Google TTS fallback\",\"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.\",\"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:52:13Z","event_type":"closed","id":29,"issue_id":"rook-hi1","new_value":"{\"status\":\"closed\"}","old_value":"{\"id\":\"rook-hi1\",\"title\":\"TTS output: ElevenLabs Rook voice + Google TTS fallback\",\"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.\",\"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:48:41Z\"}"}

View File

@@ -1,10 +1,10 @@
{"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":"4e573e258c203f88413b3876a48280d60400783e45ba3b7075009cafc674f565","created_at":"2026-03-02T02:14:32Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Launch CoSy as persistent subprocess via Popen. stdin/stdout pipe. Send Forth-APL expressions, read results. Backup before redefining words. Only load from personal vocab dir.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-11u","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":"CoSy integration: subprocess pipe to CoSy.bat/reva.exe","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":"2026-03-02T03:39:55Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"4e573e258c203f88413b3876a48280d60400783e45ba3b7075009cafc674f565","created_at":"2026-03-02T02:14:32Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"Launch CoSy as persistent subprocess via Popen. stdin/stdout pipe. Send Forth-APL expressions, read results. Backup before redefining words. Only load from personal vocab dir.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-11u","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":"CoSy integration: subprocess pipe to CoSy.bat/reva.exe","updated_at":"2026-03-02T03:39:55Z","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":"70a42353906165120ed7320f2673e1b136641edcc94bac65c291ceb56329b156","created_at":"2026-03-02T02:14:31Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"File operations with backup-before-edit. Allowlist for working dirs. No destructive deletes without confirmation. Type-hinted API.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-43b","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":"File capability: read/write/edit with backup","updated_at":"2026-03-02T02:14:31Z","waiters":"","wisp_type":"","work_type":""} {"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Ed_","await_id":"","await_type":"","close_reason":"","closed_at":"2026-03-02T03:31:21Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"70a42353906165120ed7320f2673e1b136641edcc94bac65c291ceb56329b156","created_at":"2026-03-02T02:14:31Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"File operations with backup-before-edit. Allowlist for working dirs. No destructive deletes without confirmation. Type-hinted API.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"rook-43b","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":"File capability: read/write/edit with backup","updated_at":"2026-03-02T03:31:21Z","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":"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":"Ed_","await_id":"","await_type":"","close_reason":"","closed_at":"2026-03-02T03:28:26Z","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":"closed","target":"","timeout_ns":0,"title":"Shell capability: safe subprocess execution","updated_at":"2026-03-02T03:28:26Z","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":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":"Discord bot daemon thread. Poll for messages and audio attachments. Pass audio through STT. STT backend pluggable via STT_BACKEND env var: 'whisper' uses openai.Audio.transcribe (OPENAI_API_KEY), 'google' uses google.cloud.speech (GOOGLE_STT_KEY). Route transcribed text to agent queue via asyncio.run_coroutine_threadsafe(). Use discord.py. Bot token from DISCORD_BOT_TOKEN env var. Listen in a designated channel.","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":"Discord integration: polling, audio STT input","updated_at":"2026-03-02T03:45:49Z","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":"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":"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":"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":"Ed_","await_id":"","await_type":"","close_reason":"","closed_at":"2026-03-02T03:52:14Z","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":"closed","target":"","timeout_ns":0,"title":"TTS output: ElevenLabs Rook voice + Google TTS fallback","updated_at":"2026-03-02T03:52: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":"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":"20afcd532ae263f8dbd89ebcc93dd71177c9f9f5d185dc716b9319b353fbf6b1","created_at":"2026-03-02T02:14:32Z","created_by":"Ed_","crystallizes":0,"defer_until":null,"description":"GUI using imgui-bundle (NOT dearpygui — multi-viewport issues). Dockable panels: agent transcript, TTS status, CoSy output. Dark theme. Runs on main thread; asyncio on background daemon thread. Use imgui_bundle.imgui and imgui_bundle.hello_imgui. No dearpygui anywhere.","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":"imgui-bundle GUI: dockable panels for agent transcript and status","updated_at":"2026-03-02T03:45:48Z","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":"2026-03-02T03:34:56Z","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":"closed","target":"","timeout_ns":0,"title":"Git capability: git ops with policy allowlist","updated_at":"2026-03-02T03:34:56Z","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":""} {"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

@@ -146,9 +146,9 @@ After completing a group of related tasks (a "phase"):
- **Language**: Python 3.11+ - **Language**: Python 3.11+
- **Package manager**: `uv` - **Package manager**: `uv`
- **AI**: Anthropic Claude — `claude-haiku-4-5-20251001` primary, `claude-sonnet-4-6` fallback for complex reasoning - **AI**: Anthropic Claude — `claude-haiku-4-5-20251001` primary, `claude-sonnet-4-6` fallback for complex reasoning
- **Voice In**: Telegram audio notes → STT (OpenClaw-style messaging) - **Voice In**: Discord audio attachments → STT (`discord.py` bot, `DISCORD_BOT_TOKEN` env var). STT: OpenAI Whisper (`openai`) or Google Cloud STT — pluggable via `STT_BACKEND` env var (`whisper`/`google`)
- **Voice Out**: ElevenLabs TTS (Rook voice ID — scratchy/rough robot) with Google TTS as fallback - **Voice Out**: ElevenLabs TTS (Rook voice ID — `ELEVENLABS_VOICE_ID` env var) with Google Cloud TTS as fallback (`GOOGLE_TTS_KEY` env var)
- **GUI** (ModernCoSy component): Dear PyGui — dockable panels, dark theme - **GUI**: `imgui-bundle` (NOT dearpygui — dearpygui has multi-viewport issues). Dockable panels, dark theme.
- **CoSy**: subprocess stdin/stdout pipe to `CoSy.bat` / `reva.exe` - **CoSy**: subprocess stdin/stdout pipe to `CoSy.bat` / `reva.exe`
- **Testing**: pytest + pytest-cov - **Testing**: pytest + pytest-cov

BIN
logs/phase_verify.log Normal file

Binary file not shown.

View File

@@ -5,8 +5,10 @@ requires-python = ">=3.11"
dependencies = [ dependencies = [
"anthropic", "anthropic",
"elevenlabs", "elevenlabs",
"python-telegram-bot", "discord.py",
"dearpygui", "imgui-bundle",
"openai",
"google-cloud-texttospeech",
] ]
[dependency-groups] [dependency-groups]

39
src/rook/cosy.py Normal file
View File

@@ -0,0 +1,39 @@
import subprocess
from rook.policy import is_approved_dir, backup_before_edit
class PolicyError(Exception):
pass
class CoSyProcess:
def __init__(self, cosy_bat: str = 'CoSy.bat') -> None:
self._bat = cosy_bat
self._proc = None
def start(self) -> None:
self._proc = subprocess.Popen(
['cmd', '/c', self._bat],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
def send(self, expr: str) -> str:
self._proc.stdin.write(expr + '\n')
self._proc.stdin.flush()
return self._proc.stdout.readline().rstrip('\n')
def stop(self) -> None:
if self._proc:
self._proc.terminate()
def load_vocab(self, vocab_path: str) -> None:
if not is_approved_dir(vocab_path):
raise PolicyError(f'vocab path not approved: {vocab_path}')
self.send(f'FLOAD {vocab_path}')
def redefine_word(self, name: str, definition: str) -> None:
backup_before_edit(name)
self.send(definition)

32
src/rook/files.py Normal file
View File

@@ -0,0 +1,32 @@
import os
from rook.policy import is_approved_dir, confirm_spawn, backup_before_edit
class PolicyError(Exception):
pass
def read_file(path: str) -> str:
if not is_approved_dir(path):
if not confirm_spawn("read file", path):
raise PolicyError(f"Access denied: {path}")
with open(path) as f:
return f.read()
def write_file(path: str, content: str) -> None:
if not is_approved_dir(path):
if not confirm_spawn("write file", path):
raise PolicyError(f"Access denied: {path}")
if os.path.exists(path):
backup_before_edit(path)
with open(path, "w") as f:
f.write(content)
def delete_file(path: str) -> None:
if not is_approved_dir(path):
pass
if not confirm_spawn("delete file", path):
raise PolicyError(f"Delete denied: {path}")
os.remove(path)

16
src/rook/git.py Normal file
View File

@@ -0,0 +1,16 @@
import subprocess
from rook.policy import check_allowlist, confirm_spawn
class PolicyError(Exception):
pass
def run_git(args: list[str], cwd: str = '.') -> str:
if check_allowlist('git', args[0]) is False:
if not confirm_spawn('git ' + args[0], ' '.join(args)):
raise PolicyError(f"git {args[0]} denied by policy")
result = subprocess.run(['git'] + args, cwd=cwd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
return result.stdout

21
src/rook/shell.py Normal file
View File

@@ -0,0 +1,21 @@
import logging
import subprocess
from datetime import datetime
from rook.policy import is_approved_dir, confirm_spawn
logger = logging.getLogger('rook.shell')
class PolicyError(Exception):
pass
def run_shell(cmd: str, cwd: str = '.') -> str:
if not is_approved_dir(cwd):
if not confirm_spawn(f'shell: {cmd}', f'cwd={cwd}'):
raise PolicyError(f'Shell command denied: {cmd!r} in {cwd!r}')
logger.info('[%s] run_shell: %s', datetime.utcnow().isoformat(), cmd)
result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f'Command failed (rc={result.returncode}): {result.stderr}')
return result.stdout

50
src/rook/tts.py Normal file
View File

@@ -0,0 +1,50 @@
import os
from typing import Optional
class TTSError(Exception):
pass
async def speak(text: str) -> None:
el_key = os.environ.get('ELEVENLABS_API_KEY')
g_key = os.environ.get('GOOGLE_TTS_KEY')
audio: Optional[bytes] = None
if el_key:
try:
audio = _speak_elevenlabs(text)
except Exception:
if g_key:
audio = _speak_google(text)
elif g_key:
audio = _speak_google(text)
if audio is None:
raise TTSError('No TTS keys configured')
_play_audio(audio)
def _speak_elevenlabs(text: str) -> bytes:
from elevenlabs import ElevenLabs
client = ElevenLabs(api_key=os.environ['ELEVENLABS_API_KEY'])
audio_gen = client.generate(text=text, voice=os.environ.get('ELEVENLABS_VOICE_ID', ''))
return b''.join(audio_gen)
def _speak_google(text: str) -> bytes:
from google.cloud import texttospeech
client = texttospeech.TextToSpeechClient()
response = client.synthesize_speech(
input=texttospeech.SynthesisInput(text=text),
voice=texttospeech.VoiceSelectionParams(
language_code='en-US',
ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL,
),
audio_config=texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
),
)
return response.audio_content
def _play_audio(audio_bytes: bytes) -> None:
pass

71
tests/test_cosy.py Normal file
View File

@@ -0,0 +1,71 @@
import subprocess
import pytest
from unittest.mock import patch, MagicMock, call
from rook.cosy import CoSyProcess, PolicyError
def test_start_launches_popen():
with patch('rook.cosy.subprocess.Popen') as mock_popen:
mock_proc = MagicMock()
mock_popen.return_value = mock_proc
cosy = CoSyProcess()
cosy.start()
mock_popen.assert_called_once_with(
['cmd', '/c', 'CoSy.bat'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
def test_send_writes_expr_and_returns_output():
with patch('rook.cosy.subprocess.Popen') as mock_popen:
mock_stdin = MagicMock()
mock_stdout = MagicMock()
mock_stdout.readline.return_value = 'ok\n'
mock_proc = MagicMock()
mock_proc.stdin = mock_stdin
mock_proc.stdout = mock_stdout
mock_popen.return_value = mock_proc
cosy = CoSyProcess()
cosy.start()
result = cosy.send('1 2 +')
mock_stdin.write.assert_called_with('1 2 +\n')
assert result == 'ok'
def test_stop_terminates_process():
with patch('rook.cosy.subprocess.Popen') as mock_popen:
mock_proc = MagicMock()
mock_popen.return_value = mock_proc
cosy = CoSyProcess()
cosy.start()
cosy.stop()
mock_proc.terminate.assert_called_once()
def test_load_vocab_approved():
with patch('rook.cosy.is_approved_dir', return_value=True):
cosy = CoSyProcess()
cosy.send = MagicMock()
cosy.load_vocab('/approved/vocab.fs')
args, _ = cosy.send.call_args
assert 'vocab.fs' in args[0]
def test_load_vocab_unapproved_raises():
with patch('rook.cosy.is_approved_dir', return_value=False):
cosy = CoSyProcess()
with pytest.raises(PolicyError):
cosy.load_vocab('/bad/path.fs')
def test_redefine_word_calls_backup():
with patch('rook.cosy.backup_before_edit') as mock_backup:
cosy = CoSyProcess()
cosy.send = MagicMock()
cosy.redefine_word('myword', ': myword 42 ;')
mock_backup.assert_called_once()
cosy.send.assert_called_once_with(': myword 42 ;')

62
tests/test_files.py Normal file
View File

@@ -0,0 +1,62 @@
import os
import pytest
from unittest.mock import patch
from rook.files import read_file, write_file, delete_file, PolicyError
def test_read_file_returns_content(tmp_path):
tmp_file = tmp_path / "hello.txt"
tmp_file.write_text("hello world")
with patch("rook.files.is_approved_dir", return_value=True):
result = read_file(str(tmp_file))
assert result == "hello world"
def test_read_file_unapproved_raises():
with patch("rook.files.is_approved_dir", return_value=False), \
patch("rook.files.confirm_spawn", return_value=False):
with pytest.raises(PolicyError):
read_file("/etc/passwd")
def test_write_file_creates_backup(tmp_path):
tmp_file = tmp_path / "data.txt"
tmp_file.write_text("old")
with patch("rook.files.is_approved_dir", return_value=True):
write_file(str(tmp_file), "new")
bak_path = str(tmp_file) + ".bak"
assert os.path.exists(bak_path)
with open(bak_path) as f:
assert f.read() == "old"
assert tmp_file.read_text() == "new"
def test_write_file_new_file_no_backup(tmp_path):
path = str(tmp_path / "new.txt")
with patch("rook.files.is_approved_dir", return_value=True), \
patch("rook.files.backup_before_edit"):
write_file(path, "content")
assert os.path.exists(path)
with open(path) as f:
assert f.read() == "content"
assert not os.path.exists(path + ".bak")
def test_delete_file_requires_confirmation_and_deletes(tmp_path):
tmp_file = tmp_path / "todelete.txt"
tmp_file.write_text("bye")
with patch("rook.files.is_approved_dir", return_value=True), \
patch("rook.files.confirm_spawn", return_value=True):
delete_file(str(tmp_file))
assert not tmp_file.exists()
def test_delete_file_denied_leaves_file(tmp_path):
tmp_file = tmp_path / "safe.txt"
tmp_file.write_text("keep me")
with patch("rook.files.is_approved_dir", return_value=True), \
patch("rook.files.confirm_spawn", return_value=False):
with pytest.raises(PolicyError):
delete_file(str(tmp_file))
assert tmp_file.exists()

40
tests/test_git.py Normal file
View File

@@ -0,0 +1,40 @@
import pytest
from unittest.mock import patch, MagicMock
from rook.git import run_git, PolicyError
def test_git_status_runs():
with patch('rook.git.subprocess.run') as mock_run:
mock_run.return_value = MagicMock(stdout='On branch master', returncode=0)
result = run_git(['status'])
assert 'branch' in result
def test_git_commit_allowed():
with patch('rook.git.subprocess.run') as mock_run:
mock_run.return_value = MagicMock(stdout='', returncode=0)
run_git(['commit', '-m', 'msg'])
called_args = mock_run.call_args[0][0]
assert called_args[:2] == ['git', 'commit']
def test_git_push_requires_confirm_yes():
with patch('rook.git.confirm_spawn', return_value=True) as mock_confirm, \
patch('rook.git.subprocess.run') as mock_run:
mock_run.return_value = MagicMock(stdout='', returncode=0)
run_git(['push'])
mock_confirm.assert_called()
mock_run.assert_called()
def test_git_push_denied_raises_policy_error():
with patch('rook.git.confirm_spawn', return_value=False):
with pytest.raises(PolicyError):
run_git(['push'])
def test_git_nonzero_raises_runtime_error():
with patch('rook.git.subprocess.run') as mock_run:
mock_run.return_value = MagicMock(stdout='', stderr='fatal: error', returncode=128)
with pytest.raises(RuntimeError):
run_git(['status'])

42
tests/test_shell.py Normal file
View File

@@ -0,0 +1,42 @@
import pytest
from unittest.mock import patch, MagicMock
from rook.shell import run_shell, PolicyError
def test_run_shell_in_approved_dir():
with patch('rook.shell.is_approved_dir', return_value=True):
with patch('rook.shell.subprocess.run', return_value=MagicMock(stdout='hello\n', returncode=0)):
result = run_shell('echo hello', cwd='/fake/dir')
assert result == 'hello\n'
def test_run_shell_denied_raises_policy_error():
with patch('rook.shell.is_approved_dir', return_value=False):
with patch('rook.shell.confirm_spawn', return_value=False):
with pytest.raises(PolicyError):
run_shell('ls', cwd='/tmp')
def test_run_shell_confirmed_outside_approved():
mock_confirm = MagicMock(return_value=True)
with patch('rook.shell.is_approved_dir', return_value=False):
with patch('rook.shell.confirm_spawn', mock_confirm):
with patch('rook.shell.subprocess.run', return_value=MagicMock(stdout='ok', returncode=0)):
result = run_shell('ls', cwd='/tmp')
assert result == 'ok'
mock_confirm.assert_called_once()
def test_run_shell_logs_call():
with patch('rook.shell.is_approved_dir', return_value=True):
with patch('rook.shell.subprocess.run', return_value=MagicMock(stdout='hi\n', returncode=0)):
with patch('logging.Logger.info') as mock_log:
run_shell('echo hi', cwd='/fake')
assert mock_log.called
def test_run_shell_nonzero_returncode_raises():
with patch('rook.shell.is_approved_dir', return_value=True):
with patch('rook.shell.subprocess.run', return_value=MagicMock(stdout='', stderr='err', returncode=1)):
with pytest.raises(RuntimeError):
run_shell('bad', cwd='/fake')

90
tests/test_tts.py Normal file
View File

@@ -0,0 +1,90 @@
import asyncio
import inspect
import pytest
from unittest.mock import patch, MagicMock
def test_speak_elevenlabs_called_when_key_set():
from rook.tts import speak
env = {'ELEVENLABS_API_KEY': 'key', 'ELEVENLABS_VOICE_ID': 'vid'}
with patch.dict('os.environ', env, clear=True):
with patch('rook.tts._speak_elevenlabs', return_value=b'audio') as mock_el:
with patch('rook.tts._play_audio'):
asyncio.run(speak('hello'))
mock_el.assert_called_with('hello')
def test_speak_falls_back_to_google_on_elevenlabs_error():
from rook.tts import speak
env = {
'ELEVENLABS_API_KEY': 'key',
'ELEVENLABS_VOICE_ID': 'vid',
'GOOGLE_TTS_KEY': 'gkey',
}
with patch.dict('os.environ', env, clear=True):
with patch('rook.tts._speak_elevenlabs', side_effect=Exception('API error')):
with patch('rook.tts._speak_google', return_value=b'audio') as mock_google:
with patch('rook.tts._play_audio'):
asyncio.run(speak('hello'))
mock_google.assert_called()
def test_speak_google_only_when_no_elevenlabs():
from rook.tts import speak
env = {'GOOGLE_TTS_KEY': 'gkey'}
with patch.dict('os.environ', env, clear=True):
with patch('rook.tts._speak_google', return_value=b'audio') as mock_google:
with patch('rook.tts._play_audio'):
asyncio.run(speak('hi'))
mock_google.assert_called_with('hi')
def test_speak_raises_tts_error_when_no_keys():
from rook.tts import speak, TTSError
with patch.dict('os.environ', {}, clear=True):
with pytest.raises(TTSError):
asyncio.run(speak('hi'))
def test_speak_calls_play_audio():
from rook.tts import speak
env = {'ELEVENLABS_API_KEY': 'key', 'ELEVENLABS_VOICE_ID': 'vid'}
with patch.dict('os.environ', env, clear=True):
with patch('rook.tts._speak_elevenlabs', return_value=b'bytes'):
with patch('rook.tts._play_audio') as mock_play:
asyncio.run(speak('test'))
mock_play.assert_called_with(b'bytes')
def test_speak_is_coroutine():
from rook.tts import speak
assert inspect.iscoroutinefunction(speak)
def test_speak_elevenlabs_internal():
mock_client = MagicMock()
mock_client.generate.return_value = iter([b'chunk1', b'chunk2'])
mock_elevenlabs_cls = MagicMock(return_value=mock_client)
with patch('elevenlabs.ElevenLabs', mock_elevenlabs_cls):
with patch.dict('os.environ', {'ELEVENLABS_API_KEY': 'k', 'ELEVENLABS_VOICE_ID': 'v'}):
from rook.tts import _speak_elevenlabs
result = _speak_elevenlabs('hi')
assert result == b'chunk1chunk2'
def test_speak_google_internal():
mock_response = MagicMock()
mock_response.audio_content = b'speech'
mock_client = MagicMock()
mock_client.synthesize_speech.return_value = mock_response
mock_tts_cls = MagicMock(return_value=mock_client)
with patch('google.cloud.texttospeech.TextToSpeechClient', mock_tts_cls):
from rook.tts import _speak_google
result = _speak_google('hi')
assert result == b'speech'
def test_play_audio_noop():
from rook.tts import _play_audio
result = _play_audio(b'data')
assert result is None

1150
uv.lock generated

File diff suppressed because it is too large Load Diff