From 35ec1e5c072e045bdd8094d0d24e16699a7b872e Mon Sep 17 00:00:00 2001 From: chauncygu Date: Tue, 16 Jun 2026 10:12:03 -0700 Subject: [PATCH] refactor: drop cc_ prefix from module names Rename the four cc_-prefixed modules to plain names for readability: cc_config.py -> config.py cc_daemon/ -> daemon/ cc_kernel/ -> kernel/ cc_mcp/ -> mcp_client/ The MCP client is named mcp_client/ rather than bare mcp/ to avoid shadowing Python's namespace and the modelcontextprotocol package. Test files test_cc_daemon_*.py are renamed to test_daemon_*.py to match. References updated across .py, docs/, RFCs, README, CONTRIBUTING, pyproject.toml (py-modules + packages.find), and ci.yml using whole-word matching so unrelated tokens (cc_bin, cc_script, cv_acc_mean, cc_tool_call_debug, etc.) are left untouched. Fix two name-collision regressions surfaced by the rename, where the now-generic module name clashed with a same-scope local variable: - tests/test_setup_wizard.py: `import config` shadowed the `config` dict param of the wizard helper; alias the module to `_config_mod`. - examples/kernel_e2e_smoke.py: `with kernel.Kernel.open() as kernel` self-shadowed the module; and `_run_demo`'s `kernel` instance param collided with module-level `kernel.ScheduleSpec`/`SandboxPolicy`. Bind the context var as `kern` and import the classes at module level. Full suite: 2449 passed, 3 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 4 +- CONTRIBUTING.md | 4 +- README.md | 2 +- agent_runner.py | 12 +- bridges/qq.py | 2 +- bridges/slack.py | 2 +- bridges/telegram.py | 2 +- bridges/wechat.py | 8 +- cheetahclaws.py | 18 +-- commands/advanced.py | 6 +- commands/config_cmd.py | 14 +- commands/core.py | 10 +- commands/daemon_cmd.py | 18 +-- commands/monitor_cmd.py | 10 +- commands/session.py | 18 +-- commands/theme_cmd.py | 2 +- cc_config.py => config.py | 2 +- {cc_daemon => daemon}/__init__.py | 0 {cc_daemon => daemon}/agent_methods.py | 2 +- {cc_daemon => daemon}/auth.py | 0 {cc_daemon => daemon}/bridge_methods.py | 4 +- {cc_daemon => daemon}/bridge_supervisor.py | 4 +- {cc_daemon => daemon}/cli.py | 24 +-- {cc_daemon => daemon}/discovery.py | 2 +- {cc_daemon => daemon}/events.py | 2 +- {cc_daemon => daemon}/methods.py | 0 {cc_daemon => daemon}/monitor_methods.py | 0 {cc_daemon => daemon}/originator.py | 0 {cc_daemon => daemon}/permission.py | 0 {cc_daemon => daemon}/proactive_methods.py | 4 +- {cc_daemon => daemon}/proactive_scheduler.py | 0 {cc_daemon => daemon}/proactive_state.py | 0 {cc_daemon => daemon}/rpc.py | 0 {cc_daemon => daemon}/runner_ipc.py | 4 +- {cc_daemon => daemon}/runner_supervisor.py | 4 +- {cc_daemon => daemon}/schema.py | 2 +- {cc_daemon => daemon}/server.py | 0 {cc_daemon => daemon}/session_methods.py | 0 {cc_daemon => daemon}/spike_client.py | 0 {cc_daemon => daemon}/system_methods.py | 2 +- demo.py | 2 +- docs/README.md | 2 +- docs/RFC/0001-spike-notes.md | 34 ++--- docs/RFC/0002-daemon-foundation-roadmap.md | 142 +++++++++--------- docs/RFC/0003-agent-process-and-event-log.md | 12 +- docs/RFC/0005-capability-model.md | 2 +- docs/RFC/0006-resource-ledger.md | 2 +- docs/RFC/0007-agent-scheduler.md | 2 +- docs/RFC/0008-agent-sandbox.md | 10 +- docs/RFC/0009-agent-mailbox.md | 4 +- docs/RFC/0010-agent-registry.md | 2 +- docs/RFC/0011-agent-fs.md | 2 +- docs/RFC/0012-observability.md | 6 +- docs/RFC/0013-api-stability.md | 8 +- docs/RFC/0016-subprocess-agent-runner.md | 10 +- docs/RFC/0017-worker-loop.md | 6 +- docs/RFC/0018-bridge-mirror.md | 6 +- docs/RFC/0019-llm-runner.md | 20 +-- docs/RFC/0020-dialogue-orchestrator.md | 8 +- docs/RFC/0021-tool-dispatch.md | 2 +- docs/RFC/0022-llm-tool-calling.md | 2 +- docs/RFC/0023-shell-exec-tool.md | 4 +- docs/RFC/0024-glob-list-tools.md | 2 +- docs/RFC/0025-fetch-tool.md | 4 +- docs/RFC/0026-ipc-streaming.md | 2 +- docs/RFC/0027-llm-streaming.md | 4 +- docs/RFC/0028-exec-streaming.md | 2 +- docs/RFC/0029-fetch-streaming.md | 2 +- docs/RFC/0030-diff-tool.md | 2 +- docs/RFC/0031-ast-tool.md | 2 +- docs/RFC/0032-git-tool.md | 2 +- docs/agent-os.md | 12 +- docs/architecture.md | 126 ++++++++-------- docs/contributor_guide.md | 24 +-- docs/news.md | 44 +++--- docs/roadmap/readme.md | 2 +- examples/kernel_e2e_smoke.py | 19 ++- jobs.py | 14 +- {cc_kernel => kernel}/__init__.py | 4 +- {cc_kernel => kernel}/agentfs.py | 2 +- {cc_kernel => kernel}/api.py | 6 +- {cc_kernel => kernel}/bridge_mirror.py | 0 {cc_kernel => kernel}/capability.py | 2 +- {cc_kernel => kernel}/chaos.py | 0 {cc_kernel => kernel}/cli.py | 12 +- {cc_kernel => kernel}/contract.py | 4 +- {cc_kernel => kernel}/errors.py | 2 +- {cc_kernel => kernel}/event_log.py | 0 {cc_kernel => kernel}/integration.py | 14 +- {cc_kernel => kernel}/ledger.py | 2 +- {cc_kernel => kernel}/mailbox.py | 2 +- {cc_kernel => kernel}/methods.py | 2 +- {cc_kernel => kernel}/observability.py | 2 +- .../orchestrator/__init__.py | 4 +- .../orchestrator/dialogue.py | 2 +- {cc_kernel => kernel}/process.py | 0 {cc_kernel => kernel}/registry.py | 2 +- {cc_kernel => kernel}/runner/__init__.py | 2 +- {cc_kernel => kernel}/runner/ipc.py | 0 {cc_kernel => kernel}/runner/llm/__init__.py | 4 +- {cc_kernel => kernel}/runner/llm/__main__.py | 0 .../runner/llm/anthropic_provider.py | 2 +- .../runner/llm/litellm_provider.py | 0 {cc_kernel => kernel}/runner/llm/provider.py | 2 +- {cc_kernel => kernel}/runner/runner_main.py | 2 +- {cc_kernel => kernel}/runner/supervisor.py | 0 {cc_kernel => kernel}/sandbox.py | 0 {cc_kernel => kernel}/scheduler.py | 2 +- {cc_kernel => kernel}/schema.py | 0 {cc_kernel => kernel}/store.py | 2 +- {cc_kernel => kernel}/tools/__init__.py | 2 +- {cc_kernel => kernel}/tools/ast_tool.py | 0 {cc_kernel => kernel}/tools/builtin.py | 0 {cc_kernel => kernel}/tools/diff_tool.py | 0 {cc_kernel => kernel}/tools/exec_tool.py | 0 {cc_kernel => kernel}/tools/fetch_tool.py | 0 {cc_kernel => kernel}/tools/git_tool.py | 0 {cc_kernel => kernel}/tools/registry.py | 0 {cc_kernel => kernel}/worker.py | 0 {cc_mcp => mcp_client}/__init__.py | 2 +- {cc_mcp => mcp_client}/client.py | 2 +- {cc_mcp => mcp_client}/config.py | 0 {cc_mcp => mcp_client}/oauth.py | 2 +- {cc_mcp => mcp_client}/tools.py | 0 {cc_mcp => mcp_client}/types.py | 0 monitor/scheduler.py | 4 +- monitor/store.py | 2 +- providers.py | 2 +- pyproject.toml | 8 +- quota.py | 2 +- research/lab/orchestrator.py | 2 +- session_store.py | 2 +- tests/e2e_daemon_skeleton.py | 10 +- tests/e2e_f4_runner.py | 22 +-- tests/e2e_litellm_provider.py | 8 +- tests/test_budget.py | 4 +- ...ethods.py => test_daemon_agent_methods.py} | 14 +- ...thods.py => test_daemon_bridge_methods.py} | 18 +-- ...phase2.py => test_daemon_bridge_phase2.py} | 26 ++-- ...or.py => test_daemon_bridge_supervisor.py} | 62 ++++---- ...st_cc_daemon_cli.py => test_daemon_cli.py} | 4 +- ..._discovery.py => test_daemon_discovery.py} | 6 +- ...sqlite.py => test_daemon_events_sqlite.py} | 4 +- ...9_budgets.py => test_daemon_f9_budgets.py} | 18 +-- ...hods.py => test_daemon_monitor_methods.py} | 8 +- ..._proactive.py => test_daemon_proactive.py} | 58 +++---- ...ta_pause.py => test_daemon_quota_pause.py} | 16 +- ...y => test_daemon_runner_notify_routing.py} | 28 ++-- ... test_daemon_runner_permission_routing.py} | 44 +++--- ...y => test_daemon_runner_restart_policy.py} | 56 +++---- ...or.py => test_daemon_runner_supervisor.py} | 56 +++---- ...daemon_schema.py => test_daemon_schema.py} | 4 +- ...hods.py => test_daemon_session_methods.py} | 22 +-- tests/test_daemon_spike.py | 12 +- ...thods.py => test_daemon_system_methods.py} | 10 +- tests/test_deepseek_thinking.py | 4 +- tests/test_jobs_sqlite.py | 2 +- tests/test_kernel_agentfs.py | 4 +- tests/test_kernel_agentfs_daemon.py | 12 +- tests/test_kernel_api_contract.py | 10 +- tests/test_kernel_ast_tool.py | 8 +- tests/test_kernel_bridge_mirror.py | 6 +- tests/test_kernel_capability.py | 6 +- tests/test_kernel_chaos_smoke.py | 4 +- tests/test_kernel_cli.py | 16 +- tests/test_kernel_daemon.py | 26 ++-- tests/test_kernel_dialogue.py | 10 +- tests/test_kernel_diff_tool.py | 8 +- tests/test_kernel_exec_streaming.py | 10 +- tests/test_kernel_exec_tool.py | 8 +- tests/test_kernel_facade.py | 14 +- tests/test_kernel_fetch_streaming.py | 6 +- tests/test_kernel_fetch_tool.py | 20 +-- tests/test_kernel_git_tool.py | 6 +- tests/test_kernel_glob_list_tools.py | 4 +- tests/test_kernel_ledger.py | 4 +- tests/test_kernel_llm_runner.py | 16 +- tests/test_kernel_llm_streaming.py | 18 +-- tests/test_kernel_llm_tools.py | 16 +- tests/test_kernel_mailbox.py | 4 +- tests/test_kernel_observability.py | 8 +- tests/test_kernel_phase2_daemon.py | 12 +- tests/test_kernel_phase3_complete_daemon.py | 12 +- tests/test_kernel_recovery.py | 8 +- tests/test_kernel_registry.py | 4 +- tests/test_kernel_runner.py | 6 +- tests/test_kernel_sandbox.py | 6 +- tests/test_kernel_scheduler.py | 4 +- tests/test_kernel_scheduler_daemon.py | 10 +- tests/test_kernel_state_machine.py | 4 +- tests/test_kernel_store.py | 10 +- tests/test_kernel_streaming.py | 4 +- tests/test_kernel_tools.py | 16 +- tests/test_kernel_worker.py | 6 +- tests/test_litellm_provider.py | 8 +- tests/test_mcp.py | 10 +- tests/test_monitor_scheduler_events.py | 2 +- tests/test_monitor_store_sqlite.py | 2 +- tests/test_qq_bridge.py | 6 +- tests/test_setup_wizard.py | 7 +- tests/test_wechat_smart_reply.py | 4 +- tools/__init__.py | 2 +- web/server.py | 12 +- 203 files changed, 886 insertions(+), 876 deletions(-) rename cc_config.py => config.py (99%) rename {cc_daemon => daemon}/__init__.py (100%) rename {cc_daemon => daemon}/agent_methods.py (99%) rename {cc_daemon => daemon}/auth.py (100%) rename {cc_daemon => daemon}/bridge_methods.py (97%) rename {cc_daemon => daemon}/bridge_supervisor.py (99%) rename {cc_daemon => daemon}/cli.py (95%) rename {cc_daemon => daemon}/discovery.py (99%) rename {cc_daemon => daemon}/events.py (99%) rename {cc_daemon => daemon}/methods.py (100%) rename {cc_daemon => daemon}/monitor_methods.py (100%) rename {cc_daemon => daemon}/originator.py (100%) rename {cc_daemon => daemon}/permission.py (100%) rename {cc_daemon => daemon}/proactive_methods.py (94%) rename {cc_daemon => daemon}/proactive_scheduler.py (100%) rename {cc_daemon => daemon}/proactive_state.py (100%) rename {cc_daemon => daemon}/rpc.py (100%) rename {cc_daemon => daemon}/runner_ipc.py (91%) rename {cc_daemon => daemon}/runner_supervisor.py (99%) rename {cc_daemon => daemon}/schema.py (99%) rename {cc_daemon => daemon}/server.py (100%) rename {cc_daemon => daemon}/session_methods.py (100%) rename {cc_daemon => daemon}/spike_client.py (100%) rename {cc_daemon => daemon}/system_methods.py (97%) rename {cc_kernel => kernel}/__init__.py (98%) rename {cc_kernel => kernel}/agentfs.py (99%) rename {cc_kernel => kernel}/api.py (98%) rename {cc_kernel => kernel}/bridge_mirror.py (100%) rename {cc_kernel => kernel}/capability.py (99%) rename {cc_kernel => kernel}/chaos.py (100%) rename {cc_kernel => kernel}/cli.py (98%) rename {cc_kernel => kernel}/contract.py (98%) rename {cc_kernel => kernel}/errors.py (99%) rename {cc_kernel => kernel}/event_log.py (100%) rename {cc_kernel => kernel}/integration.py (93%) rename {cc_kernel => kernel}/ledger.py (99%) rename {cc_kernel => kernel}/mailbox.py (99%) rename {cc_kernel => kernel}/methods.py (99%) rename {cc_kernel => kernel}/observability.py (99%) rename {cc_kernel => kernel}/orchestrator/__init__.py (81%) rename {cc_kernel => kernel}/orchestrator/dialogue.py (99%) rename {cc_kernel => kernel}/process.py (100%) rename {cc_kernel => kernel}/registry.py (99%) rename {cc_kernel => kernel}/runner/__init__.py (95%) rename {cc_kernel => kernel}/runner/ipc.py (100%) rename {cc_kernel => kernel}/runner/llm/__init__.py (92%) rename {cc_kernel => kernel}/runner/llm/__main__.py (100%) rename {cc_kernel => kernel}/runner/llm/anthropic_provider.py (99%) rename {cc_kernel => kernel}/runner/llm/litellm_provider.py (100%) rename {cc_kernel => kernel}/runner/llm/provider.py (99%) rename {cc_kernel => kernel}/runner/runner_main.py (99%) rename {cc_kernel => kernel}/runner/supervisor.py (100%) rename {cc_kernel => kernel}/sandbox.py (100%) rename {cc_kernel => kernel}/scheduler.py (99%) rename {cc_kernel => kernel}/schema.py (100%) rename {cc_kernel => kernel}/store.py (99%) rename {cc_kernel => kernel}/tools/__init__.py (96%) rename {cc_kernel => kernel}/tools/ast_tool.py (100%) rename {cc_kernel => kernel}/tools/builtin.py (100%) rename {cc_kernel => kernel}/tools/diff_tool.py (100%) rename {cc_kernel => kernel}/tools/exec_tool.py (100%) rename {cc_kernel => kernel}/tools/fetch_tool.py (100%) rename {cc_kernel => kernel}/tools/git_tool.py (100%) rename {cc_kernel => kernel}/tools/registry.py (100%) rename {cc_kernel => kernel}/worker.py (100%) rename {cc_mcp => mcp_client}/__init__.py (94%) rename {cc_mcp => mcp_client}/client.py (99%) rename {cc_mcp => mcp_client}/config.py (100%) rename {cc_mcp => mcp_client}/oauth.py (99%) rename {cc_mcp => mcp_client}/tools.py (100%) rename {cc_mcp => mcp_client}/types.py (100%) rename tests/{test_cc_daemon_agent_methods.py => test_daemon_agent_methods.py} (95%) rename tests/{test_cc_daemon_bridge_methods.py => test_daemon_bridge_methods.py} (94%) rename tests/{test_cc_daemon_bridge_phase2.py => test_daemon_bridge_phase2.py} (93%) rename tests/{test_cc_daemon_bridge_supervisor.py => test_daemon_bridge_supervisor.py} (92%) rename tests/{test_cc_daemon_cli.py => test_daemon_cli.py} (93%) rename tests/{test_cc_daemon_discovery.py => test_daemon_discovery.py} (97%) rename tests/{test_cc_daemon_events_sqlite.py => test_daemon_events_sqlite.py} (98%) rename tests/{test_cc_daemon_f9_budgets.py => test_daemon_f9_budgets.py} (95%) rename tests/{test_cc_daemon_monitor_methods.py => test_daemon_monitor_methods.py} (96%) rename tests/{test_cc_daemon_proactive.py => test_daemon_proactive.py} (90%) rename tests/{test_cc_daemon_quota_pause.py => test_daemon_quota_pause.py} (94%) rename tests/{test_cc_daemon_runner_notify_routing.py => test_daemon_runner_notify_routing.py} (90%) rename tests/{test_cc_daemon_runner_permission_routing.py => test_daemon_runner_permission_routing.py} (94%) rename tests/{test_cc_daemon_runner_restart_policy.py => test_daemon_runner_restart_policy.py} (92%) rename tests/{test_cc_daemon_runner_supervisor.py => test_daemon_runner_supervisor.py} (93%) rename tests/{test_cc_daemon_schema.py => test_daemon_schema.py} (98%) rename tests/{test_cc_daemon_session_methods.py => test_daemon_session_methods.py} (93%) rename tests/{test_cc_daemon_system_methods.py => test_daemon_system_methods.py} (92%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42d95a86..e91660ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: # Top-level modules from pyproject.toml py-modules modules = [ 'cheetahclaws', 'agent', 'agent_runner', 'bootstrap', - 'circuit_breaker', 'cloudsave', 'compaction', 'cc_config', + 'circuit_breaker', 'cloudsave', 'compaction', 'config', 'context', 'health', 'jobs', 'logging_utils', 'memory', 'providers', 'quota', 'runtime', 'skills', 'subagent', 'tmux_tools', 'tool_registry', @@ -66,7 +66,7 @@ jobs: 'tools', 'tools.security', 'tools.fs', 'tools.shell', 'tools.web', 'tools.notebook', 'tools.diagnostics', 'tools.interaction', - 'cc_mcp', 'monitor', 'multi_agent', 'plugin', 'skill', 'task', + 'mcp_client', 'monitor', 'multi_agent', 'plugin', 'skill', 'task', 'voice', 'video', 'checkpoint', 'ui', 'bridges', 'commands', 'modular', ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 805b9573..2e7777b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,8 +40,8 @@ cheetahclaws/ ├── bridges/ # Telegram, WeChat, Slack integrations ├── plugin/ # Plugin system (install, load, manifest parsing) ├── skill/ # Skill system (Markdown prompt templates) -├── cc_daemon/ # Daemon foundation (F-1..F-9 all landed) — `cheetahclaws serve` + RPC surface (agent/monitor/bridge/session/proactive/system); see docs/RFC/0002 -├── cc_mcp/ # MCP (Model Context Protocol) client & tools +├── daemon/ # Daemon foundation (F-1..F-9 all landed) — `cheetahclaws serve` + RPC surface (agent/monitor/bridge/session/proactive/system); see docs/RFC/0002 +├── mcp_client/ # MCP (Model Context Protocol) client & tools ├── research/lab/ # Autonomous multi-agent research engine (/lab — 9-stage state machine + sandboxed experiments + citation verifier) ├── memory/ # Persistent memory system ├── multi_agent/ # Sub-agent spawning & worktree isolation diff --git a/README.md b/README.md index d6b0d0a1..d67b6f8b 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,7 @@ Detailed guides live in [`docs/guides/`](docs/guides/) to keep this README focus | [**FAQ**](docs/guides/faq.md) | The full FAQ (MCP, models/providers, CLI/scripting, voice) | | [**Plugin Authoring**](docs/guides/plugin-authoring.md) · [Example](examples/example-plugin/) | Build a plugin: tools, commands, skills, MCP; starter template | | [**Research Lab**](docs/guides/research-lab.md) | `/lab start ` — autonomous multi-agent paper writing with sandboxed experiments | -| [**Agent OS**](docs/agent-os.md) · [RFC index](docs/RFC/) | The `cc_kernel/` layer + all design notes (RFC 0001-0032) | +| [**Agent OS**](docs/agent-os.md) · [RFC index](docs/RFC/) | The `kernel/` layer + all design notes (RFC 0001-0032) | | [**Contributing**](CONTRIBUTING.md) | Project structure, architecture guide, PR checklist | --- diff --git a/agent_runner.py b/agent_runner.py index 85e5e17b..c985e24d 100644 --- a/agent_runner.py +++ b/agent_runner.py @@ -122,7 +122,7 @@ def start_runner( # send_fn is ignored in subprocess mode for the skeleton — # ``notify`` IPC messages are dropped on the supervisor side # until F-6/7/8 wires bridge delivery in. - from cc_daemon import runner_supervisor + from daemon import runner_supervisor return runner_supervisor.start( name=name, template_name=template_name, @@ -161,7 +161,7 @@ def stop_runner(name: str) -> bool: return True # Subprocess mode (F-4): the handle lives in the daemon supervisor. try: - from cc_daemon import runner_supervisor + from daemon import runner_supervisor except Exception: return False return runner_supervisor.stop(name) @@ -175,7 +175,7 @@ def stop_all() -> int: r.stop() count = len(runners) try: - from cc_daemon import runner_supervisor + from daemon import runner_supervisor count += runner_supervisor.stop_all() except Exception: pass @@ -624,9 +624,9 @@ def _persist_record(self, rec: _IterationRecord) -> None: # When invoked as ``python -m agent_runner --pipe``, this module turns # itself into a runner driven by a JSON-line IPC channel on stdin/stdout # instead of the in-process thread + send_fn API. The supervisor side lives -# in ``cc_daemon/runner_supervisor.py``. +# in ``daemon/runner_supervisor.py``. # -# Protocol (see cc_daemon/runner_ipc.py docstring for the full message +# Protocol (see daemon/runner_ipc.py docstring for the full message # catalogue): # 1. Supervisor writes {"op": "init", "payload": {...}} on stdin. # 2. We reply {"op": "ready"} on stdout, then run the existing @@ -642,7 +642,7 @@ def _pipe_main(name_arg: Optional[str] = None) -> int: """Subprocess entry point. Returns the process exit code.""" import argparse import sys as _sys - from cc_daemon.runner_ipc import IpcReadTimeout, JsonLineChannel + from daemon.runner_ipc import IpcReadTimeout, JsonLineChannel parser = argparse.ArgumentParser(prog="agent_runner") parser.add_argument("--pipe", action="store_true", diff --git a/bridges/qq.py b/bridges/qq.py index 76d8b1de..4200f1c2 100644 --- a/bridges/qq.py +++ b/bridges/qq.py @@ -1115,7 +1115,7 @@ def cmd_qq(args: str, _state, config) -> bool: """ global _qq_thread, _qq_stop import os as _os - from cc_config import save_config + from config import save_config from bridges import resolve_bridge_token, scrub_token_from_history parts = args.strip().split() diff --git a/bridges/slack.py b/bridges/slack.py index 382c0b15..1955dac0 100644 --- a/bridges/slack.py +++ b/bridges/slack.py @@ -615,7 +615,7 @@ def cmd_slack(args: str, _state, config) -> bool: """ global _slack_thread, _slack_stop import os as _os - from cc_config import save_config + from config import save_config from bridges import resolve_bridge_token, scrub_token_from_history parts = args.strip().split() diff --git a/bridges/telegram.py b/bridges/telegram.py index 5657eed0..abd551f2 100644 --- a/bridges/telegram.py +++ b/bridges/telegram.py @@ -929,7 +929,7 @@ def cmd_telegram(args: str, _state, config) -> bool: /telegram status — show current status """ global _telegram_thread, _telegram_stop - from cc_config import save_config + from config import save_config from bridges import resolve_bridge_token, scrub_token_from_history parts = args.strip().split() diff --git a/bridges/wechat.py b/bridges/wechat.py index b4521bab..b242a1c3 100644 --- a/bridges/wechat.py +++ b/bridges/wechat.py @@ -267,7 +267,7 @@ def _wx_typing_loop(user_id: str, stop_event: threading.Event, config: dict) -> def _wx_qr_login(config: dict, bot_type: str = _ILINK_DEFAULT_BOT_TYPE, timeout_seconds: int = 480) -> bool: - from cc_config import save_config + from config import save_config import time as _time info("Fetching WeChat QR code from iLink...") @@ -394,7 +394,7 @@ def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: print(clr("\n ⚠ WeChat: session expired — re-authenticate with /wechat login", "yellow")) config.pop("wechat_token", None) config.pop("wechat_base_url", None) - from cc_config import save_config + from config import save_config save_config(config) _log.warn("bridge_auth_error", bridge="wechat", ret=ret, errcode=errcode) session_ctx.wx_send = None @@ -425,7 +425,7 @@ def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: and not from_uid.endswith("@chatroom")): config["wechat_self_uid"] = from_uid try: - from cc_config import save_config + from config import save_config save_config(config) except Exception: pass @@ -1022,7 +1022,7 @@ def cmd_wechat(args: str, _state, config) -> bool: /wechat logout — clear saved credentials """ global _wechat_thread, _wechat_stop - from cc_config import save_config + from config import save_config sub = args.strip().split()[0].lower() if args.strip() else "" diff --git a/cheetahclaws.py b/cheetahclaws.py index 4c1587dc..80bdee00 100755 --- a/cheetahclaws.py +++ b/cheetahclaws.py @@ -390,13 +390,13 @@ def ask_permission_interactive(desc: str, config: dict) -> bool: def _proactive_foreign_daemon_running() -> bool: """True iff a daemon other than this process owns the discovery file. - F-5: when one is running, the daemon's :mod:`cc_daemon.proactive_scheduler` + F-5: when one is running, the daemon's :mod:`daemon.proactive_scheduler` fires the tick events; the REPL's own watcher must step aside so we don't double-fire or run on stale local state. """ try: import os - from cc_daemon import discovery + from daemon import discovery info_d = discovery.locate() if info_d is None: return False @@ -896,7 +896,7 @@ def _headless_run_query(prompt: str, is_background: bool = False) -> None: # ── Main REPL ────────────────────────────────────────────────────────────── def repl(config: dict, initial_prompt: str = None): - from cc_config import HISTORY_FILE + from config import HISTORY_FILE from context import build_system_prompt from agent import AgentState, run, TextChunk, ThinkingChunk, ToolStart, ToolEnd, TurnDone, PermissionRequest, QuotaPause @@ -1918,7 +1918,7 @@ def main(): # rotate-token). See docs/RFC/0001-daemon-design-note.md and # docs/RFC/0002-daemon-foundation-roadmap.md. if len(sys.argv) >= 2 and sys.argv[1] == "serve": - from cc_daemon.cli import serve_main as _serve_main + from daemon.cli import serve_main as _serve_main sys.exit(_serve_main(sys.argv[2:])) if len(sys.argv) >= 2 and sys.argv[1] == "daemon": from commands.daemon_cmd import dispatch as _daemon_dispatch @@ -1928,14 +1928,14 @@ def main(): # existing daemon RPC channel; gracefully reports "not running" # when the daemon is absent. if len(sys.argv) >= 2 and sys.argv[1] == "kernel": - from cc_kernel.cli import dispatch as _kernel_dispatch + from kernel.cli import dispatch as _kernel_dispatch sys.exit(_kernel_dispatch(sys.argv[2:])) # Backward-compat alias for the spike's `cheetahclaws spike-daemon ...` # surface (referenced in docs/RFC/0001-spike-notes.md). Routes through # the same paths as `serve` / `daemon ` so spike-notes commands # keep working unchanged. if len(sys.argv) >= 2 and sys.argv[1] == "spike-daemon": - from cc_daemon.cli import main as _legacy_main + from daemon.cli import main as _legacy_main sys.exit(_legacy_main(sys.argv[2:])) parser = argparse.ArgumentParser( @@ -1985,7 +1985,7 @@ def main(): sys.exit(0) if args.web: - from cc_config import load_config as _load_cfg, save_config as _save_cfg + from config import load_config as _load_cfg, save_config as _save_cfg _cfg = _load_cfg() # --model needs to persist: web request handlers reload config from # disk per request, so an in-memory override would be ignored. @@ -2008,7 +2008,7 @@ def main(): start_web_server(port=args.port, host=args.host, no_auth=args.no_auth) sys.exit(0) - from cc_config import load_config, save_config, has_api_key + from config import load_config, save_config, has_api_key from providers import detect_provider, PROVIDERS config = load_config() @@ -2054,7 +2054,7 @@ def main(): warn(f"--budget: {_e} (e.g. --budget $5 or --budget 200k); ignoring.") # ── Setup wizard: --setup flag or first-run auto-trigger ───────────── - from cc_config import CONFIG_FILE + from config import CONFIG_FILE is_first_run = not CONFIG_FILE.exists() or os.path.getsize(CONFIG_FILE) < 5 if args.setup or (is_first_run and sys.stdin.isatty() and not args.print_mode): run_setup_wizard(config) diff --git a/commands/advanced.py b/commands/advanced.py index 4dbedf77..ba508790 100644 --- a/commands/advanced.py +++ b/commands/advanced.py @@ -2240,10 +2240,10 @@ def cmd_skills(_args: str, _state, config) -> bool: def cmd_mcp(args: str, _state, config) -> bool: """Show MCP server status, or manage servers.""" - from cc_mcp.client import get_mcp_manager - from cc_mcp.config import (load_mcp_configs, add_server_to_user_config, + from mcp_client.client import get_mcp_manager + from mcp_client.config import (load_mcp_configs, add_server_to_user_config, remove_server_from_user_config, list_config_files) - from cc_mcp.tools import initialize_mcp, reload_mcp, refresh_server + from mcp_client.tools import initialize_mcp, reload_mcp, refresh_server parts = args.split() if args.strip() else [] subcmd = parts[0].lower() if parts else "" diff --git a/commands/config_cmd.py b/commands/config_cmd.py index 352236cf..94ab0632 100644 --- a/commands/config_cmd.py +++ b/commands/config_cmd.py @@ -55,7 +55,7 @@ def cmd_model(args: str, _state, config) -> bool: config["model"] = m pname = detect_provider(m) ok(f"Model set to {m} (provider: {pname})") - from cc_config import save_config + from config import save_config save_config(config) return True @@ -89,7 +89,7 @@ def _interactive_ollama_picker(config: dict) -> bool: if 0 <= idx < len(models): new_model = f"ollama/{models[idx]}" config["model"] = new_model - from cc_config import save_config + from config import save_config save_config(config) ok(f"Model updated to {new_model}") return True @@ -101,7 +101,7 @@ def _interactive_ollama_picker(config: dict) -> bool: def cmd_config(args: str, _state, config) -> bool: - from cc_config import save_config + from config import save_config if not args: _SECRETS = {"api_key", "anthropic_api_key", "telegram_token", "wechat_token"} display = {k: v for k, v in config.items() @@ -148,7 +148,7 @@ def cmd_config(args: str, _state, config) -> bool: def cmd_verbose(_args: str, _state, config) -> bool: - from cc_config import save_config + from config import save_config config["verbose"] = not config.get("verbose", False) state_str = "ON" if config["verbose"] else "OFF" ok(f"Verbose mode: {state_str}") @@ -157,7 +157,7 @@ def cmd_verbose(_args: str, _state, config) -> bool: def cmd_quiet(_args: str, _state, config) -> bool: - from cc_config import save_config + from config import save_config config["quiet"] = not config.get("quiet", True) state_str = "ON" if config["quiet"] else "OFF" ok(f"Quiet mode: {state_str} " @@ -168,7 +168,7 @@ def cmd_quiet(_args: str, _state, config) -> bool: def cmd_thinking(_args: str, _state, config) -> bool: - from cc_config import save_config + from config import save_config config["thinking"] = not config.get("thinking", False) state_str = "ON" if config["thinking"] else "OFF" ok(f"Extended thinking: {state_str}") @@ -177,7 +177,7 @@ def cmd_thinking(_args: str, _state, config) -> bool: def cmd_permissions(args: str, _state, config) -> bool: - from cc_config import save_config + from config import save_config from tools import ask_input_interactive modes = ["auto", "accept-all", "manual"] mode_desc = { diff --git a/commands/core.py b/commands/core.py index 413bf0c7..25b2665f 100644 --- a/commands/core.py +++ b/commands/core.py @@ -211,7 +211,7 @@ def _est(text: str) -> int: def cmd_cost(_args: str, state, config) -> bool: - from cc_config import calc_cost + from config import calc_cost cost = calc_cost(config["model"], state.total_input_tokens, state.total_output_tokens) @@ -237,7 +237,7 @@ def cmd_budget(args: str, state, config) -> bool: /budget clear remove all caps (unlimited) """ import quota as _quota - from cc_config import save_config + from config import save_config arg = args.strip() sid = config.get("_session_id", "default") @@ -668,7 +668,7 @@ def _fail(msg): def run_setup_wizard(config: dict) -> None: """Interactive first-run setup: pick provider, set API key, verify.""" - from cc_config import save_config + from config import save_config from providers import PROVIDERS, detect_provider, get_api_key print() @@ -840,7 +840,7 @@ def _proactive_daemon_running() -> bool: """ try: import os - from cc_daemon import discovery + from daemon import discovery info_d = discovery.locate() if info_d is None: return False @@ -858,7 +858,7 @@ def _proactive_rpc(method: str, params: dict | None = None) -> dict | None: import http.client import json import os - from cc_daemon import API_VERSION, API_VERSION_HEADER, discovery + from daemon import API_VERSION, API_VERSION_HEADER, discovery info_d = discovery.locate() if info_d is None: diff --git a/commands/daemon_cmd.py b/commands/daemon_cmd.py index 3c72389b..9d1a2ad5 100644 --- a/commands/daemon_cmd.py +++ b/commands/daemon_cmd.py @@ -4,8 +4,8 @@ is ``daemon``. All actions read the discovery file written by ``cheetahclaws serve``; absence of that file means "no daemon running". -Auth + token storage live in :mod:`cc_daemon.auth`; discovery in -:mod:`cc_daemon.discovery`. +Auth + token storage live in :mod:`daemon.auth`; discovery in +:mod:`daemon.discovery`. """ from __future__ import annotations @@ -20,8 +20,8 @@ from pathlib import Path from typing import Any, Optional, Tuple -from cc_daemon import auth as _auth -from cc_daemon import discovery as _discovery +from daemon import auth as _auth +from daemon import discovery as _discovery LOG_DIR_NAME = "logs" @@ -30,12 +30,12 @@ RPC_TIMEOUT_S = 2.0 STOP_WAIT_S = 5.0 -# Default token path matches cc_daemon.cli.DEFAULT_TOKEN_PATH; resolved +# Default token path matches daemon.cli.DEFAULT_TOKEN_PATH; resolved # lazily via _default_token_path() so unit tests can monkeypatch it. def _default_token_path() -> Path: - from cc_daemon.cli import DEFAULT_TOKEN_PATH + from daemon.cli import DEFAULT_TOKEN_PATH return DEFAULT_TOKEN_PATH @@ -211,9 +211,9 @@ def _call_rpc(method: str, params: Any = None) -> Tuple[bool, Any]: transport = info.get("transport") address = info.get("address", "") - # cc_daemon's server enforces the API-version header; sending it lets + # daemon's server enforces the API-version header; sending it lets # the request through the version gate. - from cc_daemon import API_VERSION, API_VERSION_HEADER + from daemon import API_VERSION, API_VERSION_HEADER headers = {"Content-Type": "application/json", "Content-Length": str(len(body)), "Host": "localhost", @@ -302,7 +302,7 @@ def _post_unix(sock_path: str, path: str, body: bytes, # ── Helpers ──────────────────────────────────────────────────────────────── def _log_path() -> Path: - from cc_config import CONFIG_DIR + from config import CONFIG_DIR return CONFIG_DIR / LOG_DIR_NAME / LOG_FILENAME diff --git a/commands/monitor_cmd.py b/commands/monitor_cmd.py index 1b0dbd93..b7e12f6e 100644 --- a/commands/monitor_cmd.py +++ b/commands/monitor_cmd.py @@ -212,7 +212,7 @@ def cmd_monitor(args: str, state, config) -> bool: # in REPL are still picked up by the daemon scheduler on its # next 60 s poll because both processes read the same SQLite. try: - from cc_daemon import discovery as _disc + from daemon import discovery as _disc _live = _disc.locate() except Exception: _live = None @@ -229,7 +229,7 @@ def cmd_monitor(args: str, state, config) -> bool: elif sub == "stop": try: - from cc_daemon import discovery as _disc + from daemon import discovery as _disc _live = _disc.locate() except Exception: _live = None @@ -294,7 +294,7 @@ def _cmd_monitor_wizard(config: dict) -> None: """Full interactive setup wizard — zero prior knowledge required.""" from monitor.store import list_subscriptions, add_subscription, remove_subscription from monitor import scheduler as _sched - from cc_config import save_config + from config import save_config _BORDER = clr("─" * 52, "dim") @@ -504,7 +504,7 @@ def _wizard_remove_subscription(config: dict, subs: list) -> None: def _wizard_configure_notifications(config: dict) -> None: """Walk through Telegram / Slack setup.""" - from cc_config import save_config + from config import save_config print() print(clr(" Push notification setup:", "bold")) @@ -655,7 +655,7 @@ def _cmd_monitor_status(config: dict) -> None: def _cmd_monitor_set(args: str, config: dict) -> None: - from cc_config import save_config + from config import save_config parts = args.split() if not parts: diff --git a/commands/session.py b/commands/session.py index 78b17c52..9ae2e8be 100644 --- a/commands/session.py +++ b/commands/session.py @@ -61,7 +61,7 @@ def _build_session_data(state, session_id: str | None = None) -> dict: # ── /save ────────────────────────────────────────────────────────────────── def cmd_save(args: str, state, config) -> bool: - from cc_config import SESSIONS_DIR + from config import SESSIONS_DIR import uuid sid = uuid.uuid4().hex[:8] ts = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -77,7 +77,7 @@ def cmd_save(args: str, state, config) -> bool: def save_latest(args: str, state, config=None) -> bool: """Save session on exit: session_latest.json + daily/ copy + append to history.json.""" - from cc_config import MR_SESSION_DIR, DAILY_DIR, SESSION_HIST_FILE + from config import MR_SESSION_DIR, DAILY_DIR, SESSION_HIST_FILE if not state.messages: return True @@ -162,7 +162,7 @@ def _atomic_write(path: Path, content: str): # ── /load ────────────────────────────────────────────────────────────────── def cmd_load(args: str, state, config) -> bool: - from cc_config import SESSIONS_DIR, MR_SESSION_DIR, DAILY_DIR + from config import SESSIONS_DIR, MR_SESSION_DIR, DAILY_DIR from tools import ask_input_interactive path = None @@ -203,7 +203,7 @@ def cmd_load(args: str, state, config) -> bool: print(clr(f" [{i+1:2d}] ", "yellow") + label) menu_buf += "\n" + clr(f" [{i+1:2d}] ", "yellow") + label - from cc_config import SESSION_HIST_FILE + from config import SESSION_HIST_FILE has_history = SESSION_HIST_FILE.exists() if has_history: try: @@ -323,7 +323,7 @@ def cmd_load(args: str, state, config) -> bool: # ── /resume ──────────────────────────────────────────────────────────────── def cmd_resume(args: str, state, config) -> bool: - from cc_config import MR_SESSION_DIR + from config import MR_SESSION_DIR if not args.strip(): path = MR_SESSION_DIR / "session_latest.json" @@ -345,7 +345,7 @@ def cmd_resume(args: str, state, config) -> bool: err(f"Session file is corrupted: {path}") warn(f" JSON error: {e}") # Try falling back to daily backups - from cc_config import DAILY_DIR + from config import DAILY_DIR daily_files = sorted(DAILY_DIR.rglob("session_*.json"), key=lambda f: f.stat().st_mtime, reverse=True) if daily_files: warn(f" Try loading a recent backup: /load {daily_files[0]}") @@ -376,7 +376,7 @@ def cmd_search(args: str, state, config) -> bool: # Auto-import legacy JSON sessions on first search count = session_count() if count == 0: - from cc_config import SESSION_HIST_FILE + from config import SESSION_HIST_FILE imported = import_json_sessions(SESSION_HIST_FILE) if imported: info(f"Imported {imported} sessions from history.json into search index.") @@ -453,7 +453,7 @@ def cmd_cloudsave(args: str, state, config) -> bool: /cloudsave load — download and load a session from Gist """ from cloudsave import validate_token, upload_session, list_sessions, download_session - from cc_config import save_config + from config import save_config parts = args.strip().split(None, 1) sub = parts[0].lower() if parts else "" @@ -562,7 +562,7 @@ def cmd_exit(_args: str, _state, config) -> bool: if config.get("cloudsave_auto") and config.get("gist_token") and _state.messages: info("Auto cloud-sync: uploading session to Gist…") from cloudsave import upload_session - from cc_config import save_config + from config import save_config session_data = _build_session_data(_state) gist_id, err_msg = upload_session( session_data, config["gist_token"], diff --git a/commands/theme_cmd.py b/commands/theme_cmd.py index 984eed9d..716649e4 100644 --- a/commands/theme_cmd.py +++ b/commands/theme_cmd.py @@ -53,7 +53,7 @@ def cmd_theme(args: str, _state, config) -> bool: config["theme"] = name try: - from cc_config import save_config + from config import save_config save_config(config) except Exception as e: warn(f"Theme applied but could not be saved: {e}") diff --git a/cc_config.py b/config.py similarity index 99% rename from cc_config.py rename to config.py index 6e6b9640..7876e390 100644 --- a/cc_config.py +++ b/config.py @@ -84,7 +84,7 @@ # multi-day work where consecutive iterations may produce similar status # updates. "auto_agent_dup_summary_limit": 3, - # RFC 0002 F-4: run agent_runner as a subprocess under cc_daemon + # RFC 0002 F-4: run agent_runner as a subprocess under daemon # supervision instead of an in-process thread. Off by default so # REPL behaviour is unchanged; daemon code paths can opt in # explicitly. The CHEETAHCLAWS_ENABLE_F4 env var also enables it. diff --git a/cc_daemon/__init__.py b/daemon/__init__.py similarity index 100% rename from cc_daemon/__init__.py rename to daemon/__init__.py diff --git a/cc_daemon/agent_methods.py b/daemon/agent_methods.py similarity index 99% rename from cc_daemon/agent_methods.py rename to daemon/agent_methods.py index 3baef0b1..ba3856a0 100644 --- a/cc_daemon/agent_methods.py +++ b/daemon/agent_methods.py @@ -1,6 +1,6 @@ """agent_methods.py — `agent.*` JSON-RPC methods (RFC 0002 F-4). -Thin wrappers over :mod:`cc_daemon.runner_supervisor` so external clients +Thin wrappers over :mod:`daemon.runner_supervisor` so external clients (REPL `/agent` command, future Web UI, third-party tools) can manage agent runners through the daemon's RPC channel instead of importing the supervisor directly. diff --git a/cc_daemon/auth.py b/daemon/auth.py similarity index 100% rename from cc_daemon/auth.py rename to daemon/auth.py diff --git a/cc_daemon/bridge_methods.py b/daemon/bridge_methods.py similarity index 97% rename from cc_daemon/bridge_methods.py rename to daemon/bridge_methods.py index 98134682..2d604374 100644 --- a/cc_daemon/bridge_methods.py +++ b/daemon/bridge_methods.py @@ -1,6 +1,6 @@ """bridge_methods.py — `bridge.*` JSON-RPC methods (RFC 0002 F-6/7/8). -Thin wrappers over :mod:`cc_daemon.bridge_supervisor` so external clients +Thin wrappers over :mod:`daemon.bridge_supervisor` so external clients (REPL `/telegram` command, future Web UI, third-party tools) can manage daemon-owned bridges through the same RPC channel they use for ``agent.*`` and ``monitor.*``. @@ -72,7 +72,7 @@ def bridge_start(params: dict, _ctx) -> dict: if not isinstance(config, dict): raise TypeError("bridge.start: 'config' must be an object") # Merge with the daemon-level config so callers don't have to - # repeat tokens that are already stored under cc_config. + # repeat tokens that are already stored under config. merged = dict(daemon_state.config or {}) merged.update(config) # RFC 0002 F-6 Phase 2 — when ``daemon_phase2`` is True, the diff --git a/cc_daemon/bridge_supervisor.py b/daemon/bridge_supervisor.py similarity index 99% rename from cc_daemon/bridge_supervisor.py rename to daemon/bridge_supervisor.py index a2e0b9ff..140989ad 100644 --- a/cc_daemon/bridge_supervisor.py +++ b/daemon/bridge_supervisor.py @@ -62,7 +62,7 @@ def enabled(kind: str) -> bool: """Return True iff the named bridge is allowed to run in-daemon. - Mirrors :func:`cc_daemon.runner_supervisor.enabled`'s shape: truthy + Mirrors :func:`daemon.runner_supervisor.enabled`'s shape: truthy env vars (``1``/``true``/``yes``/``on``) flip the bridge on. The flag is per-kind because users may want to migrate one bridge at a time. """ @@ -310,7 +310,7 @@ def _safe_cfg(cfg: dict) -> dict: The bridge config is merged with the daemon's full config at ``bridge.start`` time (so callers don't have to repeat tokens - already stored under ``cc_config``), which means *provider* + already stored under ``config``), which means *provider* secrets — ``anthropic_api_key`` / ``openai_api_key`` / etc. — can bleed through if we don't redact them too. Match any key whose lowercase form contains a known secret fragment; values are kept diff --git a/cc_daemon/cli.py b/daemon/cli.py similarity index 95% rename from cc_daemon/cli.py rename to daemon/cli.py index 73e09aa7..7a7f6c86 100644 --- a/cc_daemon/cli.py +++ b/daemon/cli.py @@ -43,14 +43,14 @@ # ── F-9: serve-mode cost-guardrail defaults ─────────────────────────────── # -# REPL ``--in-process`` mode keeps the all-None defaults from cc_config so +# REPL ``--in-process`` mode keeps the all-None defaults from config so # existing users see no surprise. Headless ``cheetahclaws serve`` mode # applies *conservative* defaults instead: a daemon often runs unattended # for hours/days, an unbounded agent that's quietly compounding costs # while no one is watching is the failure mode F-9 (#68) addresses. # # Operators who want a different ceiling override via: -# * cc_config keys (`session_token_budget` etc.), or +# * config keys (`session_token_budget` etc.), or # * the agent.resume RPC's `budget_overrides` argument. F9_SERVE_BUDGET_DEFAULTS = { "session_token_budget": 200_000, @@ -121,13 +121,13 @@ def _build_serve_parser() -> argparse.ArgumentParser: p.add_argument("--unauthenticated-metrics", action="store_true", help="Serve /healthz, /readyz, /metrics without auth " "(off by default; opt-in for Prometheus scrapers).") - # ── cc_kernel (RFC 0003) — opt-in only, default off. ────────────────── - # When absent, cc_kernel is never imported and the daemon behaviour is + # ── kernel (RFC 0003) — opt-in only, default off. ────────────────── + # When absent, kernel is never imported and the daemon behaviour is # byte-for-byte identical to the pre-RFC build (existing users see no # change). When present, kernel.db is opened, startup recovery runs, # and the kernel.* RPC methods join the registry. p.add_argument("--enable-kernel", action="store_true", - help="Activate cc_kernel (RFC 0003: AgentProcess + EventLog). " + help="Activate kernel (RFC 0003: AgentProcess + EventLog). " "Off by default; existing users see no change.") p.add_argument("--kernel-db", default=None, help="Path to kernel.db (default: /kernel.db). " @@ -175,7 +175,7 @@ def cmd_serve(args: argparse.Namespace) -> int: return 1 # ── Load config + bootstrap (logging, tool registry) ────────────────── - from cc_config import load_config + from config import load_config from bootstrap import bootstrap as _bootstrap config = load_config() if not config.get("log_file"): @@ -234,14 +234,14 @@ def cmd_serve(args: argparse.Namespace) -> int: if args.print_token: print(f"token: {token}", flush=True) - # ── cc_kernel activation (RFC 0003) — strictly opt-in. ─────────────── - # Importing cc_kernel is gated on the flag so the no-flag default + # ── kernel activation (RFC 0003) — strictly opt-in. ─────────────── + # Importing kernel is gated on the flag so the no-flag default # path imports nothing new and pays no startup cost. if getattr(args, "enable_kernel", False): kernel_db = Path(args.kernel_db).expanduser() if args.kernel_db \ else (data_dir / "kernel.db") try: - from cc_kernel import register_with_daemon as _register_kernel + from kernel import register_with_daemon as _register_kernel _register_kernel( server.daemon_state, kernel_db, recovery=args.kernel_recovery, @@ -392,14 +392,14 @@ def _lookup_version() -> str: return "unknown" -# ── Backward-compat entry: `python -m cc_daemon.cli` ────────────────────── +# ── Backward-compat entry: `python -m daemon.cli` ────────────────────── # # The Cheetahclaws spike branch (RFC 0001-spike-notes.md §"How to run it") # documented a subparser CLI with verbs ``serve``, ``status``, ``stop``, # and ``rotate-token``. Foundation moves the canonical surface to # ``cheetahclaws serve`` / ``cheetahclaws daemon ``, but anyone # following the spike notes should still be able to invoke -# ``python -m cc_daemon.cli ...``. +# ``python -m daemon.cli ...``. # # We handle that here by dispatching: # * ``serve`` → the same :func:`serve_main` used by ``cheetahclaws serve`` @@ -412,7 +412,7 @@ def _lookup_version() -> str: # silently accepted as a courtesy and ignored. _USAGE = ( - "usage: python -m cc_daemon.cli {serve|status|stop|logs|rotate-token} [options]\n" + "usage: python -m daemon.cli {serve|status|stop|logs|rotate-token} [options]\n" "\n" "Subcommands:\n" " serve Run the headless daemon. See `serve --help` for flags.\n" diff --git a/cc_daemon/discovery.py b/daemon/discovery.py similarity index 99% rename from cc_daemon/discovery.py rename to daemon/discovery.py index 69d1c8b1..e5714501 100644 --- a/cc_daemon/discovery.py +++ b/daemon/discovery.py @@ -35,7 +35,7 @@ def get_default_path() -> Path: """Default discovery-file location: ``~/.cheetahclaws/daemon.json``.""" - from cc_config import CONFIG_DIR + from config import CONFIG_DIR return CONFIG_DIR / DEFAULT_FILENAME diff --git a/cc_daemon/events.py b/daemon/events.py similarity index 99% rename from cc_daemon/events.py rename to daemon/events.py index 9fa2b4ae..ea92e619 100644 --- a/cc_daemon/events.py +++ b/daemon/events.py @@ -24,7 +24,7 @@ evicted what the client wanted), so SSE clients can resync. Concurrency: each thread that hits the bus gets its own SQLite connection -through :func:`cc_daemon.schema.get_conn` (mirrors ``session_store``'s +through :func:`daemon.schema.get_conn` (mirrors ``session_store``'s pattern); WAL + 5 s busy timeout is set there. """ from __future__ import annotations diff --git a/cc_daemon/methods.py b/daemon/methods.py similarity index 100% rename from cc_daemon/methods.py rename to daemon/methods.py diff --git a/cc_daemon/monitor_methods.py b/daemon/monitor_methods.py similarity index 100% rename from cc_daemon/monitor_methods.py rename to daemon/monitor_methods.py diff --git a/cc_daemon/originator.py b/daemon/originator.py similarity index 100% rename from cc_daemon/originator.py rename to daemon/originator.py diff --git a/cc_daemon/permission.py b/daemon/permission.py similarity index 100% rename from cc_daemon/permission.py rename to daemon/permission.py diff --git a/cc_daemon/proactive_methods.py b/daemon/proactive_methods.py similarity index 94% rename from cc_daemon/proactive_methods.py rename to daemon/proactive_methods.py index 5cc30b8e..7d3612a9 100644 --- a/cc_daemon/proactive_methods.py +++ b/daemon/proactive_methods.py @@ -1,6 +1,6 @@ """proactive_methods.py — ``proactive.*`` JSON-RPC methods (RFC 0002 F-5). -Thin wrappers over :mod:`cc_daemon.proactive_state` so external clients +Thin wrappers over :mod:`daemon.proactive_state` so external clients (REPL ``/proactive``, future Web UI, bridge integrations) can manage the daemon-owned proactive watcher through the same SQLite-backed settings the scheduler reads from. @@ -18,7 +18,7 @@ every time the user interacts so the watcher doesn't fire during active conversations. -Same threat model as :mod:`cc_daemon.monitor_methods` (single-user) — +Same threat model as :mod:`daemon.monitor_methods` (single-user) — any authenticated caller may invoke these. Per-method authorisation arrives with the originator-routed RPCs in a later phase. """ diff --git a/cc_daemon/proactive_scheduler.py b/daemon/proactive_scheduler.py similarity index 100% rename from cc_daemon/proactive_scheduler.py rename to daemon/proactive_scheduler.py diff --git a/cc_daemon/proactive_state.py b/daemon/proactive_state.py similarity index 100% rename from cc_daemon/proactive_state.py rename to daemon/proactive_state.py diff --git a/cc_daemon/rpc.py b/daemon/rpc.py similarity index 100% rename from cc_daemon/rpc.py rename to daemon/rpc.py diff --git a/cc_daemon/runner_ipc.py b/daemon/runner_ipc.py similarity index 91% rename from cc_daemon/runner_ipc.py rename to daemon/runner_ipc.py index fa0eecfd..d1bc47aa 100644 --- a/cc_daemon/runner_ipc.py +++ b/daemon/runner_ipc.py @@ -1,6 +1,6 @@ """runner_ipc.py — IPC channel re-exports for the agent-runner supervisor (RFC 0002 F-4). -The supervisor (cc_daemon/runner_supervisor.py) and the runner entry point +The supervisor (daemon/runner_supervisor.py) and the runner entry point (`python -m agent_runner --pipe`) speak newline-delimited JSON over a pair of pipes. The wire format and stream wrapper already exist for the kernel LLM runner — we re-export them here so the daemon side doesn't grow a @@ -28,6 +28,6 @@ """ from __future__ import annotations -from cc_kernel.runner.ipc import IpcReadTimeout, JsonLineChannel +from kernel.runner.ipc import IpcReadTimeout, JsonLineChannel __all__ = ["IpcReadTimeout", "JsonLineChannel"] diff --git a/cc_daemon/runner_supervisor.py b/daemon/runner_supervisor.py similarity index 99% rename from cc_daemon/runner_supervisor.py rename to daemon/runner_supervisor.py index 1d03550a..09d3b8b0 100644 --- a/cc_daemon/runner_supervisor.py +++ b/daemon/runner_supervisor.py @@ -64,7 +64,7 @@ def _get_event_bus(): def _iso_now() -> str: """ISO 8601 UTC timestamp with microsecond precision and Z suffix. - Same shape as cc_daemon.events._epoch_to_iso so the two columns sort + Same shape as daemon.events._epoch_to_iso so the two columns sort consistently when joined on time.""" return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") @@ -951,7 +951,7 @@ def resume(name: str) -> bool: # Every DB write is best-effort: a failed insert/update is logged via # returning False but never raises, so the supervisor can keep going even # if the daemon DB is missing or read-only. The schema lives in -# cc_daemon/schema.py (tables created by F-2's init_schema). +# daemon/schema.py (tables created by F-2's init_schema). def _db_insert_agent_run(handle: "RunnerHandle") -> bool: diff --git a/cc_daemon/schema.py b/daemon/schema.py similarity index 99% rename from cc_daemon/schema.py rename to daemon/schema.py index 9d8793fa..97ed4b2d 100644 --- a/cc_daemon/schema.py +++ b/daemon/schema.py @@ -142,7 +142,7 @@ def get_default_db_path() -> Path: """Return ``~/.cheetahclaws/sessions.db`` (shared with session_store).""" - from cc_config import CONFIG_DIR + from config import CONFIG_DIR return CONFIG_DIR / "sessions.db" diff --git a/cc_daemon/server.py b/daemon/server.py similarity index 100% rename from cc_daemon/server.py rename to daemon/server.py diff --git a/cc_daemon/session_methods.py b/daemon/session_methods.py similarity index 100% rename from cc_daemon/session_methods.py rename to daemon/session_methods.py diff --git a/cc_daemon/spike_client.py b/daemon/spike_client.py similarity index 100% rename from cc_daemon/spike_client.py rename to daemon/spike_client.py diff --git a/cc_daemon/system_methods.py b/daemon/system_methods.py similarity index 97% rename from cc_daemon/system_methods.py rename to daemon/system_methods.py index 9b8c0165..6047365f 100644 --- a/cc_daemon/system_methods.py +++ b/daemon/system_methods.py @@ -1,6 +1,6 @@ """system_methods.py — Always-available daemon-control RPC methods. -These ride on top of the spike's :mod:`cc_daemon.methods` (which carries +These ride on top of the spike's :mod:`daemon.methods` (which carries the demo and permission methods) so the contract surface is the same on day one as it will be once the real ``agent.run`` integration lands. diff --git a/demo.py b/demo.py index dda5ca9d..b350feaa 100644 --- a/demo.py +++ b/demo.py @@ -12,7 +12,7 @@ # Add parent path for imports sys.path.insert(0, os.path.dirname(__file__)) -from cc_config import load_config +from config import load_config from context import build_system_prompt from agent import AgentState, run, TextChunk, ThinkingChunk, ToolStart, ToolEnd, TurnDone, PermissionRequest diff --git a/docs/README.md b/docs/README.md index 804ee7a4..d9dc4069 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ start with the [top-level README](../README.md). | Path | What | |------|------| | [`news.md`](news.md) | Full release notes (the top-level README has one-line summaries linking here) | -| [`agent-os.md`](agent-os.md) | Agent-OS layer (`cc_kernel/`) reference | +| [`agent-os.md`](agent-os.md) | Agent-OS layer (`kernel/`) reference | | [`architecture.md`](architecture.md) | System architecture deep dive | | [`contributor_guide.md`](contributor_guide.md) | How to set up dev environment and submit PRs | | [`guides/`](guides/) | User-facing how-to guides (Web UI, bridges, trading, research lab, plugin authoring, voice/video, recipes, reference, Docker, advanced) | diff --git a/docs/RFC/0001-spike-notes.md b/docs/RFC/0001-spike-notes.md index 1f532b78..00fda22e 100644 --- a/docs/RFC/0001-spike-notes.md +++ b/docs/RFC/0001-spike-notes.md @@ -15,15 +15,15 @@ modules); none of the spike code is load-bearing for production. | File | LoC | Role | |---|---|---| -| `cc_daemon/server.py` | 250 | `ThreadedTCPServer`, `ThreadedUnixServer`, request handler, dispatch, SSE loop | -| `cc_daemon/rpc.py` | 90 | JSON-RPC 2.0 dispatcher + method registry | -| `cc_daemon/events.py` | 110 | In-memory ring buffer + pub/sub + SSE frame format | -| `cc_daemon/auth.py` | 180 | `SO_PEERCRED` (Linux) + bearer token, audit log, brute-force throttle | -| `cc_daemon/originator.py` | 70 | client_id mint / persist / resume | -| `cc_daemon/permission.py` | 130 | Pending-request store, originator-only answer, timeout janitor | -| `cc_daemon/methods.py` | 75 | `echo.ping` / `permission.demo` / `permission.answer` / `permission.refresh_timeout` / `permission.list` | -| `cc_daemon/cli.py` | 165 | `cheetahclaws spike-daemon {serve, status, stop, rotate-token}` | -| `cc_daemon/spike_client.py` | 175 | Stdlib-only smoke client (`ping`, `watch`, `request`, `answer`, `list`) | +| `daemon/server.py` | 250 | `ThreadedTCPServer`, `ThreadedUnixServer`, request handler, dispatch, SSE loop | +| `daemon/rpc.py` | 90 | JSON-RPC 2.0 dispatcher + method registry | +| `daemon/events.py` | 110 | In-memory ring buffer + pub/sub + SSE frame format | +| `daemon/auth.py` | 180 | `SO_PEERCRED` (Linux) + bearer token, audit log, brute-force throttle | +| `daemon/originator.py` | 70 | client_id mint / persist / resume | +| `daemon/permission.py` | 130 | Pending-request store, originator-only answer, timeout janitor | +| `daemon/methods.py` | 75 | `echo.ping` / `permission.demo` / `permission.answer` / `permission.refresh_timeout` / `permission.list` | +| `daemon/cli.py` | 165 | `cheetahclaws spike-daemon {serve, status, stop, rotate-token}` | +| `daemon/spike_client.py` | 175 | Stdlib-only smoke client (`ping`, `watch`, `request`, `answer`, `list`) | | `tests/test_daemon_spike.py` | 290 | 13 cases (8 covering RFC must-fix matrix + 5 unit) | `cheetahclaws.py` gets a single 4-line shim that intercepts `spike-daemon` before the main argparse runs. Nothing else in the main code is touched. @@ -90,7 +90,7 @@ cheetahclaws spike-daemon rotate-token --print-token ### Talk to it -The smoke client lives at `cc_daemon/spike_client.py`. It reads a token from +The smoke client lives at `daemon/spike_client.py`. It reads a token from `$CHEETAHCLAWS_TOKEN` so you don't have to pass `--token` on every call — which also sidesteps argparse's "value starts with `-`" trap on URL-safe-base64 tokens. @@ -99,11 +99,11 @@ URL-safe-base64 tokens. export CHEETAHCLAWS_TOKEN="" # Sync RPC: returns immediately, also fires a ping_received event. -python -m cc_daemon.spike_client --target tcp://127.0.0.1:8765 \ +python -m daemon.spike_client --target tcp://127.0.0.1:8765 \ --kind play ping --message hi # Tail the event stream (heartbeats every 15s). -python -m cc_daemon.spike_client --target tcp://127.0.0.1:8765 \ +python -m daemon.spike_client --target tcp://127.0.0.1:8765 \ --kind watcher watch ``` @@ -120,23 +120,23 @@ structurally impossible. ```bash # Two distinct clients (alice / bob) get distinct client_ids on first touch. rm -f ~/.cheetahclaws/clients/alice.id ~/.cheetahclaws/clients/bob.id -python -m cc_daemon.spike_client --target tcp://127.0.0.1:8765 --kind alice ping -python -m cc_daemon.spike_client --target tcp://127.0.0.1:8765 --kind bob ping +python -m daemon.spike_client --target tcp://127.0.0.1:8765 --kind alice ping +python -m daemon.spike_client --target tcp://127.0.0.1:8765 --kind bob ping # Alice creates a PermissionRequest (originator = alice's client_id). -python -m cc_daemon.spike_client --target tcp://127.0.0.1:8765 --kind alice \ +python -m daemon.spike_client --target tcp://127.0.0.1:8765 --kind alice \ request --tool Bash --input '{"cmd":"rm -rf /tmp/x"}' # → result.request_id = pr_ export RID="" # Bob tries to answer Alice's request: -python -m cc_daemon.spike_client --target tcp://127.0.0.1:8765 --kind bob \ +python -m daemon.spike_client --target tcp://127.0.0.1:8765 --kind bob \ answer --request-id "$RID" --approve # → status 403, error.code -32001, "not the originator" # Alice answers her own: -python -m cc_daemon.spike_client --target tcp://127.0.0.1:8765 --kind alice \ +python -m daemon.spike_client --target tcp://127.0.0.1:8765 --kind alice \ answer --request-id "$RID" # → status 200, result.answer = {"approve": false} ``` diff --git a/docs/RFC/0002-daemon-foundation-roadmap.md b/docs/RFC/0002-daemon-foundation-roadmap.md index 649a74ad..e62f20ba 100644 --- a/docs/RFC/0002-daemon-foundation-roadmap.md +++ b/docs/RFC/0002-daemon-foundation-roadmap.md @@ -22,26 +22,26 @@ The "foundation PR" described at the end of [RFC 0001](./0001-daemon-design-note ## F-1 — daemon skeleton -**Scope.** Adopt the `cc_daemon/` reference scaffolding from +**Scope.** Adopt the `daemon/` reference scaffolding from [`feature/daemon-spike`](https://github.com/SafeRL-Lab/cheetahclaws/tree/feature/daemon-spike) (`server`, `auth`, `originator`, `rpc`, `events`, `permission`, `methods`) **as-is** — those modules encode the contract the maintainer reviewed in PR #74. Layer the foundation glue on top: -- `cc_daemon/discovery.py` — atomic `~/.cheetahclaws/daemon.json` so +- `daemon/discovery.py` — atomic `~/.cheetahclaws/daemon.json` so REPL / Web / bridge clients can locate the running daemon (transport, address, version). Spike's pid file stays for "is anything running?" liveness; discovery answers "where is it?". -- `cc_daemon/system_methods.py` — registers `system.ping` (returns +- `daemon/system_methods.py` — registers `system.ping` (returns `"pong"`) and `system.shutdown` (sets `DaemonState.shutdown_event`, giving us cross-platform graceful exit since Windows can't deliver SIGTERM cleanly to another Python process). -- `cc_daemon/cli.py` — rewritten `serve_main(argv)` that calls +- `daemon/cli.py` — rewritten `serve_main(argv)` that calls `bootstrap()`, pins `log_file` to `/logs/daemon.log`, threads the loaded `config` and the `--unauthenticated-metrics` flag through `DaemonState`, writes the discovery file on bind, watches the shutdown event, and clears discovery on exit. -- `cc_daemon/server.py` — minimal patch: route `/healthz` `/readyz` +- `daemon/server.py` — minimal patch: route `/healthz` `/readyz` `/metrics` through `health.payload_for(path, config)` instead of the spike's stub `{"status": "ok"}`. Auth-gated by default; opt out via `--unauthenticated-metrics`. Adds Windows guard around @@ -55,11 +55,11 @@ PR #74. Layer the foundation glue on top: - `health.py` — refactor: extract module-level `healthz_payload(config)` / `readyz_payload(config)` / `metrics_payload(config)` / `payload_for(path, config)` so both the existing standalone health - HTTP server and `cc_daemon/server.py` reuse the same + HTTP server and `daemon/server.py` reuse the same circuit-breaker / quota / runtime-registry probes. No behaviour change for existing `health_check_port` users. - `cheetahclaws.py` — main() short-circuit: `cheetahclaws serve` - dispatches to `cc_daemon.cli.serve_main`; `cheetahclaws daemon + dispatches to `daemon.cli.serve_main`; `cheetahclaws daemon ` dispatches to `commands.daemon_cmd.dispatch`. Replaces the spike's `spike-daemon` shim. @@ -86,21 +86,21 @@ PR #74. Layer the foundation glue on top: **Scope.** Seven additive tables in `~/.cheetahclaws/sessions.db`; swap the F-1 in-memory event ring for a SQLite-backed channel; migrate `jobs.py` JSON storage to SQLite. **Originator-tracked permission flow -is already provided by spike's `cc_daemon/originator.py` + -`cc_daemon/permission.py`** (see PR #80) — this PR doesn't re-do it. +is already provided by spike's `daemon/originator.py` + +`daemon/permission.py`** (see PR #80) — this PR doesn't re-do it. **Tables (additive — `sessions` from `session_store.py` untouched).** `schema_meta`, `daemon_events`, `agent_runs`, `agent_iterations`, `jobs`, `monitor_subscriptions`, `monitor_reports`, `bridges`. **Deliverables.** -- `cc_daemon/schema.py` — DDL + `init_schema(db_path)` (idempotent, +- `daemon/schema.py` — DDL + `init_schema(db_path)` (idempotent, internally locked) + `get_conn()` (thread-local, mirrors `session_store` pattern) + `get_schema_version()` accessor; future migrations land in `_apply_migrations()`. -- `cc_daemon/cli.py:cmd_serve` calls `init_schema()` right after +- `daemon/cli.py:cmd_serve` calls `init_schema()` right after `bootstrap()` so tables exist before the first publish. -- `cc_daemon/events.py` — rewritten: `EventBus.publish` does an INSERT +- `daemon/events.py` — rewritten: `EventBus.publish` does an INSERT into `daemon_events` (id from `AUTOINCREMENT`, monotonic across restarts and prunes), still fans out to in-process subscribers for live tail; `replay_since(N)` reads from SQLite and emits a synthetic @@ -116,7 +116,7 @@ is already provided by spike's `cc_daemon/originator.py` + Public API unchanged. **Follow-ups (#fix-f2).** -- `cc_daemon/schema.py` sets `PRAGMA synchronous=NORMAL` on init and +- `daemon/schema.py` sets `PRAGMA synchronous=NORMAL` on init and on every thread-local connection. Safe under WAL — only the most recent transactions can be lost on hard kernel crash, which for an event log already retention-pruned in 24 h windows is an acceptable @@ -131,12 +131,12 @@ is already provided by spike's `cc_daemon/originator.py` + **Acceptance.** - `init_schema()` is idempotent across daemon restarts and concurrent - callers (verified by 12 unit tests in `tests/test_cc_daemon_schema.py`). + callers (verified by 12 unit tests in `tests/test_daemon_schema.py`). - Spike's 13 contract tests in `tests/test_daemon_spike.py` keep passing on the SQLite-backed bus (only the two ring-buffer tests needed an in-place rewrite to test retention-based eviction instead of the deleted in-memory cap). -- New `tests/test_cc_daemon_events_sqlite.py` (15 tests) covers +- New `tests/test_daemon_events_sqlite.py` (15 tests) covers persistence, retention by row count + age, gap-on-old-since, cross-instance replay (simulated daemon restart), and the `reset_bus_for_tests()` truncate path. @@ -166,25 +166,25 @@ daemon is detected. API of the legacy store unchanged. - `monitor/scheduler.py` — `run_one()` persists the full report body via `save_report` and publishes a `monitor_report` event on - `cc_daemon.events.get_bus()` with `{topic, report_id, body, sent_to, + `daemon.events.get_bus()` with `{topic, report_id, body, sent_to, errors}`. Loop's idle wait switched from `time.sleep(30)` ×60 to a single `Event.wait(60)` so daemon shutdown isn't stalled by the scheduler thread napping. -- `cc_daemon/monitor_methods.py` — registers `monitor.subscribe`, +- `daemon/monitor_methods.py` — registers `monitor.subscribe`, `monitor.unsubscribe`, `monitor.list`, `monitor.run` for external clients (Web UI / third-party tools). `DaemonState.__init__` calls `monitor_methods.register` next to `system_methods`. -- `cc_daemon/cli.py:cmd_serve` — starts the scheduler with +- `daemon/cli.py:cmd_serve` — starts the scheduler with `monitor.scheduler.start(config)` after schema init; the existing shutdown watcher calls `monitor.scheduler.stop()` before triggering HTTP-server shutdown. - `commands/monitor_cmd.py` — `/monitor start` and `/monitor stop` - detect a live daemon via `cc_daemon.discovery.locate()` and no-op + detect a live daemon via `daemon.discovery.locate()` and no-op with a friendly message. `/monitor subscribe` / `unsubscribe` / `list` continue to work in REPL because they hit SQLite directly. **Follow-ups (#fix-f2).** -- `cc_daemon/cli.py:cmd_serve` now starts `monitor.scheduler.start(...)` +- `daemon/cli.py:cmd_serve` now starts `monitor.scheduler.start(...)` **after** the listener has bound and the discovery file is on disk (PR #101 had it before the bind). Order matters — if a due subscription fires before the daemon is reachable, an LLM/network @@ -219,7 +219,7 @@ daemon is detected. **Tests.** `tests/test_monitor_store_sqlite.py` (18), `tests/test_monitor_scheduler_events.py` (7), -`tests/test_cc_daemon_monitor_methods.py` (12), plus 1 new e2e in +`tests/test_daemon_monitor_methods.py` (12), plus 1 new e2e in `tests/e2e_daemon_skeleton.py` for the survive-restart case. ## F-4 — agent_runner subprocess @@ -227,10 +227,10 @@ daemon is detected. **Scope.** Each `AgentRunner` is its own subprocess. From #68: *"subprocess-per-agent rather than threads — one leaking/crashing runner shouldn't take down the scheduler and bridges."* **Deliverables.** -- `cc_daemon/runner_supervisor.py` — spawn / monitor / restart agent-runner subprocesses. -- `cc_daemon/runner_ipc.py` — line-delimited JSON over stdin/stdout between supervisor and runner. +- `daemon/runner_supervisor.py` — spawn / monitor / restart agent-runner subprocesses. +- `daemon/runner_ipc.py` — line-delimited JSON over stdin/stdout between supervisor and runner. - `agent_runner.py` — main entry point usable as `python -m agent_runner --pipe …`; iteration-log writes flow back to the daemon and land in `agent_iterations`. -- Permission requests from runners routed through supervisor → `cc_daemon/permission.py`. +- Permission requests from runners routed through supervisor → `daemon/permission.py`. **Acceptance.** - Runner crash (`kill -9 `) does not kill the daemon; supervisor logs the crash and emits `agent_runner_crash` event. @@ -246,12 +246,12 @@ unchanged). Files: | File | LoC | Role | |------|-----|------| -| `cc_daemon/runner_supervisor.py` | ~610 | Lifecycle (`start` / `stop` / `stop_all` / `get` / `list_all`), three-phase stop (IPC `stop` → SIGTERM → SIGKILL, ≤5 s), reader loop, crash classification, SQLite persistence helpers | -| `cc_daemon/runner_ipc.py` | 33 | Thin re-export of `cc_kernel.runner.ipc.JsonLineChannel` | -| `cc_daemon/agent_methods.py` | ~100 | `agent.start` / `agent.stop` / `agent.list` / `agent.status` RPCs, registered from `cc_daemon/server.py:DaemonState.__init__` | +| `daemon/runner_supervisor.py` | ~610 | Lifecycle (`start` / `stop` / `stop_all` / `get` / `list_all`), three-phase stop (IPC `stop` → SIGTERM → SIGKILL, ≤5 s), reader loop, crash classification, SQLite persistence helpers | +| `daemon/runner_ipc.py` | 33 | Thin re-export of `kernel.runner.ipc.JsonLineChannel` | +| `daemon/agent_methods.py` | ~100 | `agent.start` / `agent.stop` / `agent.list` / `agent.status` RPCs, registered from `daemon/server.py:DaemonState.__init__` | | `agent_runner.py` | +231 | `python -m agent_runner --pipe` subprocess entry, `_PipeAgentRunner` shim that bridges `send_fn` and `iteration_done` to IPC, dispatch in `start_runner` / `stop_runner` | -| `tests/test_cc_daemon_runner_supervisor.py` | ~430 | 17 unit tests: handshake, graceful stop, SIGKILL escalation on hung runner, crash via external SIGKILL, IPC shim identity, 9 SQLite persistence cases | -| `tests/test_cc_daemon_agent_methods.py` | ~210 | 10 RPC tests: registration, param validation, list/status when empty, end-to-end list→stop with inline runner | +| `tests/test_daemon_runner_supervisor.py` | ~430 | 17 unit tests: handshake, graceful stop, SIGKILL escalation on hung runner, crash via external SIGKILL, IPC shim identity, 9 SQLite persistence cases | +| `tests/test_daemon_agent_methods.py` | ~210 | 10 RPC tests: registration, param validation, list/status when empty, end-to-end list→stop with inline runner | Acceptance status: @@ -273,7 +273,7 @@ Acceptance status: 1. **Permission routing.** ✅ *Landed (see §F-4.1 below).* The supervisor now routes `permission_request` IPC through - `cc_daemon/permission.py:PermissionStore` when the runner was started + `daemon/permission.py:PermissionStore` when the runner was started with `auto_approve=False`. The originator (the client_id that called `agent.start`) answers via `permission.answer` and the supervisor forwards the response back to the runner as `permission_response`. @@ -324,9 +324,9 @@ Files touched: | File | What changed | |------|--------------| -| `cc_daemon/permission.py` | `PermissionRequest` gains an optional `on_answer(req)` callback; `PermissionStore.create()` accepts it, `answer()` fires it after the store has been mutated (outside the lock), the janitor synthesises `{"approve": False, "timeout": True}` and fires it on expiry. | -| `cc_daemon/runner_supervisor.py` | `RunnerHandle` gains `originator: str` + `permission_store: Optional`. `start()` takes both as kwargs. `_reader_loop`'s `permission_request` branch now: (a) keeps the auto-approve fast path when `auto_approve=True` *or* no store is wired in (back-compat), (b) otherwise calls `store.create(originator=…, on_answer=…)` and the callback ships `permission_response` back to the runner. | -| `cc_daemon/agent_methods.py` | `agent.start` reads `ctx.client_id` for the originator and passes `daemon_state.permissions` as the store. `agent.list` / `agent.status` results now include `originator`. | +| `daemon/permission.py` | `PermissionRequest` gains an optional `on_answer(req)` callback; `PermissionStore.create()` accepts it, `answer()` fires it after the store has been mutated (outside the lock), the janitor synthesises `{"approve": False, "timeout": True}` and fires it on expiry. | +| `daemon/runner_supervisor.py` | `RunnerHandle` gains `originator: str` + `permission_store: Optional`. `start()` takes both as kwargs. `_reader_loop`'s `permission_request` branch now: (a) keeps the auto-approve fast path when `auto_approve=True` *or* no store is wired in (back-compat), (b) otherwise calls `store.create(originator=…, on_answer=…)` and the callback ships `permission_response` back to the runner. | +| `daemon/agent_methods.py` | `agent.start` reads `ctx.client_id` for the originator and passes `daemon_state.permissions` as the store. `agent.list` / `agent.status` results now include `originator`. | | `agent_runner.py` | Extracted today's inline PermissionRequest handling into `AgentRunner._handle_permission_request(event) -> rec_status`. `_PipeAgentRunner` overrides it: emit `permission_request` with a fresh correlation id, wait on a `threading.Event` populated by the control-loop's `permission_response` handler (already in place), then set `event.granted` and either continue or stop. | Semantics: @@ -336,7 +336,7 @@ Semantics: - **`auto_approve=False` and store absent** — the supervisor still grants (treated as the back-compat safety path so a misconfigured caller doesn't lock the runner up). Only RPC-driven flows pass a store. - **Timeout** — the store's janitor fires the same callback path with `{"approve": False, "timeout": True}` so the runner unblocks rather than waiting `_PERMISSION_WAIT_S` (30 min) for an IPC frame that's never coming. -Tests live in `tests/test_cc_daemon_runner_permission_routing.py` (10 +Tests live in `tests/test_daemon_runner_permission_routing.py` (10 new cases — store callback unit, supervisor approve/deny/timeout round-trips, non-originator guard, missing-store fallback, and the `agent.start` RPC wiring). The existing 17 supervisor tests + 10 @@ -380,7 +380,7 @@ Defaults & semantics: the bus; retry policy (if any) belongs to the originator, not the supervisor. -Tests live in `tests/test_cc_daemon_runner_notify_routing.py` (3 +Tests live in `tests/test_daemon_runner_notify_routing.py` (3 cases — single-bridge dispatch, broadcast default, empty-text drop). Verifies via an inline `python -c` runner that speaks the IPC protocol and a `patch.object(bs, "notify", ...)` so we don't need a @@ -445,10 +445,10 @@ Files touched: | File | What changed | |-----------------------------------------------------|--------------| -| `cc_daemon/runner_supervisor.py` | Adds `RestartPolicy` dataclass + `RunnerHandle.restart_policy / restart_count / _start_kwargs / _restart_timer / _restart_decided`. `start()` gains kwargs (`restart_policy`, `_restart_count_carry`) and stashes `_start_kwargs` for successor calls. Reader's `finally` invokes `_maybe_schedule_restart()` on crash. `stop()` cancels the pending Timer before the kill ladder. New `_RESTART_SPAWNER` module hook for tests. | -| `cc_daemon/agent_methods.py` | `agent.start` parses `RestartPolicy.from_params(params)` and threads it through. `_handle_to_dict` now reports `restart_count` + flattened `restart_policy` on `agent.list` / `agent.status`. | -| `tests/test_cc_daemon_runner_restart_policy.py` | New, 16 cases: 10 pure-function (`next_delay` matrix, `from_params` validation), 3 reader-loop integration (`disabled → no timer`, `on-crash → spawner called with carry+1`, exhaustion publishes the event), 1 `stop()` cancellation, 2 handle serialisation / sanity. | -| `tests/test_cc_daemon_runner_permission_routing.py` | `_FakeHandle` stub gains `restart_policy` + `restart_count` so `_handle_to_dict` doesn't `AttributeError`. | +| `daemon/runner_supervisor.py` | Adds `RestartPolicy` dataclass + `RunnerHandle.restart_policy / restart_count / _start_kwargs / _restart_timer / _restart_decided`. `start()` gains kwargs (`restart_policy`, `_restart_count_carry`) and stashes `_start_kwargs` for successor calls. Reader's `finally` invokes `_maybe_schedule_restart()` on crash. `stop()` cancels the pending Timer before the kill ladder. New `_RESTART_SPAWNER` module hook for tests. | +| `daemon/agent_methods.py` | `agent.start` parses `RestartPolicy.from_params(params)` and threads it through. `_handle_to_dict` now reports `restart_count` + flattened `restart_policy` on `agent.list` / `agent.status`. | +| `tests/test_daemon_runner_restart_policy.py` | New, 16 cases: 10 pure-function (`next_delay` matrix, `from_params` validation), 3 reader-loop integration (`disabled → no timer`, `on-crash → spawner called with carry+1`, exhaustion publishes the event), 1 `stop()` cancellation, 2 handle serialisation / sanity. | +| `tests/test_daemon_runner_permission_routing.py` | `_FakeHandle` stub gains `restart_policy` + `restart_count` so `_handle_to_dict` doesn't `AttributeError`. | Events: @@ -466,10 +466,10 @@ Race-safety notes: Tests: -`pytest tests/test_cc_daemon_runner_restart_policy.py` — 16/16 green in +`pytest tests/test_daemon_runner_restart_policy.py` — 16/16 green in ~3 s. The wider F-4 regression -(`test_cc_daemon_runner_supervisor.py` + `test_cc_daemon_agent_methods.py` + -`test_cc_daemon_runner_permission_routing.py`) is 55/55 green in ~13 s, +(`test_daemon_runner_supervisor.py` + `test_daemon_agent_methods.py` + +`test_daemon_runner_permission_routing.py`) is 55/55 green in ~13 s, plus the F-4.4 e2e (4/4 in ~2 s) was rerun unchanged. ### §F-4.4 — End-to-end test with the real subprocess (landed) @@ -527,11 +527,11 @@ e2e). | File | Role | |------|------| -| `cc_daemon/proactive_state.py` | `schema_meta`-backed KV for ``proactive.enabled`` / ``proactive.interval_s`` / ``proactive.last_tick_at``. Public surface: `get_state()`, `set_state()`, `disable()`, `tickle()`, `record_tick()`. Survives daemon restarts because it's on the same `sessions.db` the F-2 schema owns. | -| `cc_daemon/proactive_scheduler.py` | Single background thread (`proactive-scheduler`). Ticks at `TICK_INTERVAL_S = 1.0`, reads `proactive_state`, publishes `proactive_tick` on the SSE bus when the idle threshold is crossed, and resets `last_tick_at` using one `now` reading so the event and the row share a clock. Mirrors F-3's `monitor.scheduler` (`owned_by_daemon`, `_foreign_daemon_running()`, interruptible `Event.wait` so shutdown doesn't stall). | -| `cc_daemon/proactive_methods.py` | `proactive.set` / `proactive.get` / `proactive.tickle` RPCs. Same param-validation conventions as `monitor.*`. Registered next to the other method modules in `DaemonState.__init__`. | -| `cc_daemon/cli.py:cmd_serve` | Starts the proactive scheduler after bind + discovery (so external clients can subscribe to `proactive_tick` *before* the first tick lands), with `owned_by_daemon=True`. Shutdown watcher stops it alongside `monitor.scheduler`. | -| `cc_daemon/server.py` | `DaemonState.__init__` registers `proactive_methods` alongside `system_methods`, `monitor_methods`, and `agent_methods`. | +| `daemon/proactive_state.py` | `schema_meta`-backed KV for ``proactive.enabled`` / ``proactive.interval_s`` / ``proactive.last_tick_at``. Public surface: `get_state()`, `set_state()`, `disable()`, `tickle()`, `record_tick()`. Survives daemon restarts because it's on the same `sessions.db` the F-2 schema owns. | +| `daemon/proactive_scheduler.py` | Single background thread (`proactive-scheduler`). Ticks at `TICK_INTERVAL_S = 1.0`, reads `proactive_state`, publishes `proactive_tick` on the SSE bus when the idle threshold is crossed, and resets `last_tick_at` using one `now` reading so the event and the row share a clock. Mirrors F-3's `monitor.scheduler` (`owned_by_daemon`, `_foreign_daemon_running()`, interruptible `Event.wait` so shutdown doesn't stall). | +| `daemon/proactive_methods.py` | `proactive.set` / `proactive.get` / `proactive.tickle` RPCs. Same param-validation conventions as `monitor.*`. Registered next to the other method modules in `DaemonState.__init__`. | +| `daemon/cli.py:cmd_serve` | Starts the proactive scheduler after bind + discovery (so external clients can subscribe to `proactive_tick` *before* the first tick lands), with `owned_by_daemon=True`. Shutdown watcher stops it alongside `monitor.scheduler`. | +| `daemon/server.py` | `DaemonState.__init__` registers `proactive_methods` alongside `system_methods`, `monitor_methods`, and `agent_methods`. | | `commands/core.py:cmd_proactive` | When a foreign daemon is registered, the slash command routes through the `proactive.set` / `proactive.get` RPCs instead of mutating `RuntimeContext`. On RPC failure, falls back to today's in-process path so a misbehaving daemon doesn't break the REPL UX. | | `cheetahclaws.py:_proactive_watcher_loop` | Polls `_proactive_foreign_daemon_running()` and step-asides when a daemon owns the watcher — prevents double-fire across REPL + daemon. | @@ -549,7 +549,7 @@ Consumers (REPL, bridges, future agents) decide what to do with it — typically ### Tests -- `tests/test_cc_daemon_proactive.py` — 20 cases across: +- `tests/test_daemon_proactive.py` — 20 cases across: - `proactive_state`: defaults, round-trip, validation (rejects 0/negative), `disable()` keeps interval, `tickle()` bumps timestamp, corrupt-row tolerance. - `proactive_scheduler`: disabled state silent, idle threshold publishes one event, `owned_by_daemon=True` disables foreign-check, `stop()` joins within 5 s, double-start returns False. - `proactive_methods`: round-trip, missing `enabled` rejected, non-int interval rejected, zero rejected, `tickle` bumps `last_tick_at`, `get` reports scheduler-running flag. @@ -587,12 +587,12 @@ Files: | File | LoC | Role | |------|-----|------| -| `cc_daemon/bridge_supervisor.py` | ~430 | Lifecycle (`start` / `stop` / `stop_all` / `get` / `list_all`), per-kind feature-flag gate (`CHEETAHCLAWS_ENABLE_F6/7/8`), outbound `notify()` mailbox consumed by F-4 #2 + `bridge.send` RPC, `bridges` table upsert/finalize, redacted config snapshots in event payloads. | -| `cc_daemon/bridge_methods.py` | ~135 | `bridge.start` / `bridge.stop` / `bridge.list` / `bridge.send` / `bridge.status` RPCs. Registered from `cc_daemon/server.py:DaemonState.__init__` next to `agent_methods`. | -| `cc_daemon/server.py` | +6 | `DaemonState.__init__` adds `bridge_methods.register`. The methods are exposed unconditionally so `bridge.list` always answers, but `bridge.start` itself enforces the per-kind flag. | -| `cc_daemon/cli.py` | +6 | `_watch_shutdown` calls `bridge_supervisor.stop_all` before triggering the HTTP listener shutdown, so a SIGTERM cleanly tears down bridge worker threads. | -| `tests/test_cc_daemon_bridge_supervisor.py` | ~290 | 17 cases across feature flag, lifecycle (start/stop/double-start/dependency-on-F6), outbound `notify` (single + broadcast + empty drop), SQLite persistence (`list_persisted`, DB-failure tolerance), config redaction. | -| `tests/test_cc_daemon_bridge_methods.py` | ~210 | 10 RPC cases: registration, param validation across all five methods, start-list-stop round trip with redacted config in response, `bridge.send` outbound dispatch. | +| `daemon/bridge_supervisor.py` | ~430 | Lifecycle (`start` / `stop` / `stop_all` / `get` / `list_all`), per-kind feature-flag gate (`CHEETAHCLAWS_ENABLE_F6/7/8`), outbound `notify()` mailbox consumed by F-4 #2 + `bridge.send` RPC, `bridges` table upsert/finalize, redacted config snapshots in event payloads. | +| `daemon/bridge_methods.py` | ~135 | `bridge.start` / `bridge.stop` / `bridge.list` / `bridge.send` / `bridge.status` RPCs. Registered from `daemon/server.py:DaemonState.__init__` next to `agent_methods`. | +| `daemon/server.py` | +6 | `DaemonState.__init__` adds `bridge_methods.register`. The methods are exposed unconditionally so `bridge.list` always answers, but `bridge.start` itself enforces the per-kind flag. | +| `daemon/cli.py` | +6 | `_watch_shutdown` calls `bridge_supervisor.stop_all` before triggering the HTTP listener shutdown, so a SIGTERM cleanly tears down bridge worker threads. | +| `tests/test_daemon_bridge_supervisor.py` | ~290 | 17 cases across feature flag, lifecycle (start/stop/double-start/dependency-on-F6), outbound `notify` (single + broadcast + empty drop), SQLite persistence (`list_persisted`, DB-failure tolerance), config redaction. | +| `tests/test_daemon_bridge_methods.py` | ~210 | 10 RPC cases: registration, param validation across all five methods, start-list-stop round trip with redacted config in response, `bridge.send` outbound dispatch. | Per-bridge flag matrix (per the "Bridge flag" decision): @@ -650,10 +650,10 @@ New files / sections: | File | Role | |------|------| -| `cc_daemon/session_methods.py` | `session.send(session_id, text, origin?, message_id?)` publishes `session_inbound`. `session.reply(session_id, text, target_bridges?, message_id?)` publishes `session_outbound`. `session.list_recent(limit=20)` reads the in-memory LRU. Permission-routing originator defaults to the RPC caller's `client_id` when no explicit `origin` is supplied. | -| `cc_daemon/bridge_supervisor.py` | New `BridgeHandle.daemon_phase2` flag + `session_id()` helper (`tg:`, `sl:`, `wc:`). When `daemon_phase2=True`, the worker bypasses the legacy supervisor and runs `_phase2_worker`, which: (a) subscribes to the bus, filters `session_outbound` by session_id + target_bridges, forwards to `handle.sender`; (b) runs a per-kind inbound poller (`_phase2_telegram_inbound`, `_phase2_slack_inbound`, `_phase2_wechat_inbound`) that re-uses the existing HTTP helpers in `bridges/.py` but publishes `session_inbound` on every new message instead of invoking `session_ctx.run_query`. | -| `cc_daemon/bridge_methods.py` | `bridge.start` now accepts `daemon_phase2: bool` (default False). The bridge handle response surfaces `daemon_phase2` + `session_id` so the caller can confirm what mode the worker is in. | -| `cc_daemon/server.py` | Registers `session_methods` on `DaemonState.__init__`. No feature flag — the methods are pure message-passing primitives and are safe on any daemon. | +| `daemon/session_methods.py` | `session.send(session_id, text, origin?, message_id?)` publishes `session_inbound`. `session.reply(session_id, text, target_bridges?, message_id?)` publishes `session_outbound`. `session.list_recent(limit=20)` reads the in-memory LRU. Permission-routing originator defaults to the RPC caller's `client_id` when no explicit `origin` is supplied. | +| `daemon/bridge_supervisor.py` | New `BridgeHandle.daemon_phase2` flag + `session_id()` helper (`tg:`, `sl:`, `wc:`). When `daemon_phase2=True`, the worker bypasses the legacy supervisor and runs `_phase2_worker`, which: (a) subscribes to the bus, filters `session_outbound` by session_id + target_bridges, forwards to `handle.sender`; (b) runs a per-kind inbound poller (`_phase2_telegram_inbound`, `_phase2_slack_inbound`, `_phase2_wechat_inbound`) that re-uses the existing HTTP helpers in `bridges/.py` but publishes `session_inbound` on every new message instead of invoking `session_ctx.run_query`. | +| `daemon/bridge_methods.py` | `bridge.start` now accepts `daemon_phase2: bool` (default False). The bridge handle response surfaces `daemon_phase2` + `session_id` so the caller can confirm what mode the worker is in. | +| `daemon/server.py` | Registers `session_methods` on `DaemonState.__init__`. No feature flag — the methods are pure message-passing primitives and are safe on any daemon. | Acceptance criteria revisited: @@ -661,7 +661,7 @@ Acceptance criteria revisited: |----------------------------------------------------------|:------:| | Phone message → daemon `session.send` → REPL/Web/another bridge can subscribe to the same session and see events | ✅ via `session_inbound` events on the SSE feed | | Bridge survives REPL exit; user can keep texting | ✅ (already from Phase 1; the daemon owns the worker thread) | -| Permission requests originating from a bridge-driven turn route only to that bridge for answer | ✅ via originator stamping — `session.send` writes `origin=:` (or the explicit `origin` param) onto the event. The agent driver (REPL/Web) uses that string as the `originator` when minting a PermissionRequest; the existing `cc_daemon/permission.py` `PermissionStore` already enforces "only this originator can answer." | +| Permission requests originating from a bridge-driven turn route only to that bridge for answer | ✅ via originator stamping — `session.send` writes `origin=:` (or the explicit `origin` param) onto the event. The agent driver (REPL/Web) uses that string as the `originator` when minting a PermissionRequest; the existing `daemon/permission.py` `PermissionStore` already enforces "only this originator can answer." | Bus events: @@ -670,8 +670,8 @@ Bus events: Tests: -- `tests/test_cc_daemon_session_methods.py` — 13 cases (publish, LRU, param validation across `session.send` / `session.reply` / `session.list_recent`). -- `tests/test_cc_daemon_bridge_phase2.py` — 7 cases: `session_id()` formatting (3, all three kinds), outbound delivery via `session_outbound` event matching session_id + target_bridges (2), inbound poller publishes `session_inbound` for a new Telegram message (1), `bridge.start` RPC passes `daemon_phase2` through and surfaces it on the response (1). +- `tests/test_daemon_session_methods.py` — 13 cases (publish, LRU, param validation across `session.send` / `session.reply` / `session.list_recent`). +- `tests/test_daemon_bridge_phase2.py` — 7 cases: `session_id()` formatting (3, all three kinds), outbound delivery via `session_outbound` event matching session_id + target_bridges (2), inbound poller publishes `session_inbound` for a new Telegram message (1), `bridge.start` RPC passes `daemon_phase2` through and surfaces it on the response (1). Phase 1 still works unchanged — `daemon_phase2=False` (the default) keeps the legacy `bridges/.py` supervisor as the worker, preserving the REPL-shaped behaviour for callers that haven't migrated. @@ -698,7 +698,7 @@ in alongside Telegram's. What's new for F-7: HTTP code the REPL uses. - **`bridges` SQLite row.** Same schema as Telegram's; the `bridge.list` RPC merges Slack rows in. -- **Tests** in `tests/test_cc_daemon_bridge_supervisor.py::TestSlackWorker` +- **Tests** in `tests/test_daemon_bridge_supervisor.py::TestSlackWorker` cover: F-6 dependency error, supervisor invocation with the expected `(token, channel, config)` shape, outbound sender wiring. @@ -732,7 +732,7 @@ Files / tests: - **Feature flag `CHEETAHCLAWS_ENABLE_F8`** (default off; depends on F-6 enabled too). -- **Tests** in `tests/test_cc_daemon_bridge_supervisor.py::TestWechatWorker`: +- **Tests** in `tests/test_daemon_bridge_supervisor.py::TestWechatWorker`: F-6 dependency error, supervisor invocation with `(token, base_url, config)`, missing-config clean-exit path, outbound sender wiring. @@ -773,11 +773,11 @@ What landed: | File | Role | |------|------| -| `cc_daemon/cli.py` | New module-level `F9_SERVE_BUDGET_DEFAULTS` dict (200k tokens / $2 / 2M tokens / $20) plus `_apply_serve_defaults(config)` — pure function that flips any `None` budget key to its conservative default. Called from `cmd_serve` after `load_config()` and before `_bootstrap`, so the quota module sees the final values on first init. | -| `cc_daemon/system_methods.py` | New `system.status` RPC returning `{budgets: {…four keys…}, runners: int, bridges: int}`. The four keys are surfaced verbatim from `daemon_state.config` so `agent.resume`'s mutations are visible the next time someone polls. | -| `cc_daemon/agent_methods.py` | New `agent.resume` RPC accepting `budget_overrides: {key: value | null}`. Values are coerced (`int` for token budgets, `float` for cost). `null` resets to unlimited. Unknown keys → `-32602`. | +| `daemon/cli.py` | New module-level `F9_SERVE_BUDGET_DEFAULTS` dict (200k tokens / $2 / 2M tokens / $20) plus `_apply_serve_defaults(config)` — pure function that flips any `None` budget key to its conservative default. Called from `cmd_serve` after `load_config()` and before `_bootstrap`, so the quota module sees the final values on first init. | +| `daemon/system_methods.py` | New `system.status` RPC returning `{budgets: {…four keys…}, runners: int, bridges: int}`. The four keys are surfaced verbatim from `daemon_state.config` so `agent.resume`'s mutations are visible the next time someone polls. | +| `daemon/agent_methods.py` | New `agent.resume` RPC accepting `budget_overrides: {key: value | null}`. Values are coerced (`int` for token budgets, `float` for cost). `null` resets to unlimited. Unknown keys → `-32602`. | | `commands/daemon_cmd.py` | `_status` now calls `system.status` after `system.ping` and prints a `budgets:` block plus live `runners` / `bridges` counts. Backward-compatible: an older daemon that doesn't speak `system.status` falls through silently (the `system.ping` line still appears). | -| `tests/test_cc_daemon_f9_budgets.py` | 12 cases: `_apply_serve_defaults` (3, pure-function), `system.status` (3, returns budgets + counts, handles unlimited), `agent.resume` (6, merge, null=unlimited, unknown key, non-numeric, non-dict, noop empty). | +| `tests/test_daemon_f9_budgets.py` | 12 cases: `_apply_serve_defaults` (3, pure-function), `system.status` (3, returns budgets + counts, handles unlimited), `agent.resume` (6, merge, null=unlimited, unknown key, non-numeric, non-dict, noop empty). | **Per-runner quota-pause hook (landed in second pass):** @@ -798,7 +798,7 @@ Events on the bus: The pre-iter check is **read-only** — it doesn't write to the quota file or consume tokens. The actual budget enforcement still happens inside `agent.run` on every API call (`record_usage` after each turn, `check_quota` before the next). The runner-side hook just adds a fast-fail check at iteration boundaries so a paused runner can sit cheaply on a `wait_event` instead of repeatedly bouncing off the quota inside `agent.run`. -Tests for the quota-pause hook in `tests/test_cc_daemon_quota_pause.py` (2 cases): full IPC roundtrip (`paused_budget` → supervisor `quota_warn` → `resume` → `resumed` → `agent_runner_resumed`), and `runner_supervisor.resume("no-such-runner")` returns False. Plus 2 new cases in `tests/test_cc_daemon_f9_budgets.py`: `agent.resume(name=…)` calls `runner_supervisor.resume`, and an empty `name` field is rejected with `-32602`. +Tests for the quota-pause hook in `tests/test_daemon_quota_pause.py` (2 cases): full IPC roundtrip (`paused_budget` → supervisor `quota_warn` → `resume` → `resumed` → `agent_runner_resumed`), and `runner_supervisor.resume("no-such-runner")` returns False. Plus 2 new cases in `tests/test_daemon_f9_budgets.py`: `agent.resume(name=…)` calls `runner_supervisor.resume`, and an empty `name` field is rejected with `-32602`. Cost-default knobs operators can override: @@ -812,17 +812,17 @@ Cost-default knobs operators can override: } ``` -REPL invariant: `cheetahclaws` (no `serve`) still imports `cc_config` +REPL invariant: `cheetahclaws` (no `serve`) still imports `config` directly, so the four budget keys remain `None` (unlimited) — F-9 only fires inside `cmd_serve`. Verified by the existing -`tests/test_cc_daemon_cli.py` round-trip plus the new `_apply_serve_defaults` +`tests/test_daemon_cli.py` round-trip plus the new `_apply_serve_defaults` unit tests (which don't depend on a daemon being up). ## Cross-cutting conventions - **Tests.** Every PR ships unit tests; F-1, F-3, F-4, F-6/7/8 also ship `tests/e2e_daemon_.py`. - **Docs.** Every PR updates the relevant section in `docs/architecture.md`. The "Daemon" header is created by F-1; subsequent PRs append. -- **Config keys.** New keys go in `cc_config.DEFAULTS`; documented in `docs/architecture.md`. +- **Config keys.** New keys go in `config.DEFAULTS`; documented in `docs/architecture.md`. - **Backwards compatibility.** Users who never run `cheetahclaws serve` see no behavior change until the eventual default flip — that flip is out of scope here and tracked in [#68](https://github.com/SafeRL-Lab/cheetahclaws/issues/68) as the "Phase D" item. ## Updating this document diff --git a/docs/RFC/0003-agent-process-and-event-log.md b/docs/RFC/0003-agent-process-and-event-log.md index 2dfbc899..551b89dc 100644 --- a/docs/RFC/0003-agent-process-and-event-log.md +++ b/docs/RFC/0003-agent-process-and-event-log.md @@ -410,7 +410,7 @@ opened, and no `kernel.*` RPC methods are registered. The daemon behaviour is byte-for-byte identical to the pre-RFC build. When present: -- `cc_kernel/` is imported. +- `kernel/` is imported. - `kernel.db` is opened (created on first run). - Recovery runs (§2 "Recovery semantics"). - `kernel.*` methods join the RPC registry. @@ -425,10 +425,10 @@ modifier; default is `suspend` (the safer choice). ### Existing tests -This RFC does not modify any existing module under `cc_daemon/` — the +This RFC does not modify any existing module under `daemon/` — the sole CLI patch is one argparse argument and a conditional call to -`cc_kernel.register_with_daemon()`. Existing tests -(`test_cc_daemon_cli.py`, `test_cc_daemon_system_methods.py`, +`kernel.register_with_daemon()`. Existing tests +(`test_daemon_cli.py`, `test_daemon_system_methods.py`, `test_daemon_spike.py`, `e2e_daemon_skeleton.py`) must continue to pass with no changes to their assertions; the flag is off in their setup. @@ -504,5 +504,5 @@ For a PR claiming to implement this RFC: --- -Once accepted, the implementation lives in `cc_kernel/` (new package). -No code outside `cc_kernel/` and `cc_daemon/cli.py` is modified. +Once accepted, the implementation lives in `kernel/` (new package). +No code outside `kernel/` and `daemon/cli.py` is modified. diff --git a/docs/RFC/0005-capability-model.md b/docs/RFC/0005-capability-model.md index 000bcbee..43fcb4fd 100644 --- a/docs/RFC/0005-capability-model.md +++ b/docs/RFC/0005-capability-model.md @@ -299,4 +299,4 @@ A PR claiming this RFC must: 5. Glob matching covers exact, single-level wildcard, multi-level wildcard. 6. Path matching covers prefix + mode subset. -7. No file outside `cc_kernel/` and `docs/RFC/` is modified. +7. No file outside `kernel/` and `docs/RFC/` is modified. diff --git a/docs/RFC/0006-resource-ledger.md b/docs/RFC/0006-resource-ledger.md index e1c504df..58413e00 100644 --- a/docs/RFC/0006-resource-ledger.md +++ b/docs/RFC/0006-resource-ledger.md @@ -291,4 +291,4 @@ A PR claiming this RFC must: over-again). 5. `list_breached` returns only currently-over-limit rows. 6. RPC error codes match table in §4. -7. No file outside `cc_kernel/` and `docs/RFC/` is modified. +7. No file outside `kernel/` and `docs/RFC/` is modified. diff --git a/docs/RFC/0007-agent-scheduler.md b/docs/RFC/0007-agent-scheduler.md index 14f65d3f..1ee130d8 100644 --- a/docs/RFC/0007-agent-scheduler.md +++ b/docs/RFC/0007-agent-scheduler.md @@ -349,4 +349,4 @@ A PR claiming this RFC must: 7. Admission filter skips entries whose owner has any over-limit ledger row, but does not modify the queue state. 8. `kernel.sched.*` RPCs work end-to-end through the daemon. -9. No file outside `cc_kernel/`, `tests/`, and `docs/RFC/` is modified. +9. No file outside `kernel/`, `tests/`, and `docs/RFC/` is modified. diff --git a/docs/RFC/0008-agent-sandbox.md b/docs/RFC/0008-agent-sandbox.md index 72ea4fdf..8ea72ea4 100644 --- a/docs/RFC/0008-agent-sandbox.md +++ b/docs/RFC/0008-agent-sandbox.md @@ -31,7 +31,7 @@ The design is layered: `SIGKILL`s the process group on expiry. This RFC ships as a **purely additive** helper module -(`cc_kernel/sandbox.py`). No existing code is touched. F-4 (subprocess +(`kernel/sandbox.py`). No existing code is touched. F-4 (subprocess agent runner, separate PR) will adopt it; until then the module is exercised only by tests. @@ -234,7 +234,7 @@ descendants behind. Default `new_session=True` is therefore load-bearing. ## 4. Public API ```python -# cc_kernel/sandbox.py +# kernel/sandbox.py def detect_isolation_tools() -> dict[str, str | None]: """Return {'bubblewrap': '/usr/bin/bwrap' or None, @@ -285,7 +285,7 @@ without telling the caller). ## 6. Backwards compatibility -This module is a brand-new file in `cc_kernel/`. Nothing else in the +This module is a brand-new file in `kernel/`. Nothing else in the codebase changes. F-4 (subprocess agent runner) will be the first consumer; until F-4 lands, the only callers are tests. @@ -293,14 +293,14 @@ The kernel sandbox **does not** replace `research/lab/sandbox.py`. That module is a one-shot Python-code-execution helper tightly coupled to the lab's experiment workflow; it has its own threat model and its own defaults. A future RFC can refactor lab/sandbox to layer on top of -`cc_kernel.sandbox` once F-4 has proven the API. Until then the two +`kernel.sandbox` once F-4 has proven the API. Until then the two coexist with no shared code. ## 7. Acceptance criteria A PR claiming this RFC must: -1. Run `pytest tests/` green on Linux. The cc_kernel.sandbox tests +1. Run `pytest tests/` green on Linux. The kernel.sandbox tests spawn real subprocesses and verify each enforced limit by provoking it. 2. Verify with bubblewrap installed: filesystem bind isolation works diff --git a/docs/RFC/0009-agent-mailbox.md b/docs/RFC/0009-agent-mailbox.md index cd2499a8..faef1cc5 100644 --- a/docs/RFC/0009-agent-mailbox.md +++ b/docs/RFC/0009-agent-mailbox.md @@ -280,7 +280,7 @@ kernel.mbox.gc_expired - No existing module modified. - No existing test changes required (unlike when SCHEMA_VERSION literal was hardcoded — past slices already swapped to - `cc_kernel.SCHEMA_VERSION`). + `kernel.SCHEMA_VERSION`). ## 7. Open questions @@ -316,4 +316,4 @@ A PR claiming this RFC must: 7. Concurrent send: 4 threads × 25 sends → 100 distinct `msg_id`s with no losses. 8. RPC surface works end-to-end through the daemon. -9. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +9. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0010-agent-registry.md b/docs/RFC/0010-agent-registry.md index 8013fb90..901ad6bd 100644 --- a/docs/RFC/0010-agent-registry.md +++ b/docs/RFC/0010-agent-registry.md @@ -188,4 +188,4 @@ A PR claiming this RFC must: returns only entries containing that tag. 4. `unregister_pid` clears all rows for the given pid. 5. RPC surface works end-to-end through the daemon. -6. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +6. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0011-agent-fs.md b/docs/RFC/0011-agent-fs.md index 78268ecf..a47bd392 100644 --- a/docs/RFC/0011-agent-fs.md +++ b/docs/RFC/0011-agent-fs.md @@ -319,4 +319,4 @@ A PR claiming this RFC must: 9. Concurrent writes from N threads to N distinct paths leave all N rows present and intact. 10. RPC surface works end-to-end through the daemon. -11. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +11. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0012-observability.md b/docs/RFC/0012-observability.md index f632919d..da6736aa 100644 --- a/docs/RFC/0012-observability.md +++ b/docs/RFC/0012-observability.md @@ -24,7 +24,7 @@ Four primitives ship in this RFC: 3. **Metrics** — Prometheus exposition format text that the daemon's existing `/metrics` endpoint surfaces alongside its current payload. No new HTTP endpoint; just additional content. -4. **Chaos primitives** — a small `cc_kernel/chaos.py` module for +4. **Chaos primitives** — a small `kernel/chaos.py` module for tests, with `ChaosMonkey.kill_random_agent`, `simulate_disk_full`, etc. Real chaos suite expansion (network partition, time skew, process tree fault) is a v1.1 deliverable; this RFC ships the @@ -249,7 +249,7 @@ kernel metrics through the RPC channel (or via a future text passthrough). ## 6. Chaos primitives -A new module `cc_kernel/chaos.py`, intended for use **only by +A new module `kernel/chaos.py`, intended for use **only by tests**. Production code paths must not import from it. ```python @@ -319,4 +319,4 @@ A PR claiming this RFC must: 5. `ChaosMonkey.kill_random_agent` is deterministic given a seed. 6. The smoke test demonstrates a daemon survives 3 chaos operations without crashing. -7. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +7. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0013-api-stability.md b/docs/RFC/0013-api-stability.md index 741482b5..58b8d15b 100644 --- a/docs/RFC/0013-api-stability.md +++ b/docs/RFC/0013-api-stability.md @@ -62,7 +62,7 @@ For v1.0, **every kernel.* method is stable**. The ## 3. Frozen method list (v1.0) -These methods, registered by `cc_kernel.register_with_daemon`, are +These methods, registered by `kernel.register_with_daemon`, are the v1.0 stable kernel API surface. ``` @@ -150,7 +150,7 @@ kernel.api.version_info `tests/test_kernel_api_contract.py`: ```python -from cc_kernel.contract import ( +from kernel.contract import ( STABLE_METHODS, EXPERIMENTAL_METHODS, DEPRECATED_METHODS, verify_contract, ) @@ -210,7 +210,7 @@ When a method becomes obsolete: pid=originator pid and `payload={"method": "..."}`. The supervisor / web UI can surface this to the user. 4. **Next minor version**: PR removes the method registration from - `cc_kernel/.py::register()` and removes the entry from + `kernel/.py::register()` and removes the entry from `DEPRECATED_METHODS`. 5. Clients that were ignoring the deprecation now see `METHOD_NOT_FOUND` (-32601) on call. @@ -251,4 +251,4 @@ the minimum. 5. `kernel.api.version_info` returns the documented shape. 6. The contract test fails if a developer adds a method to a kernel module's `register()` without updating `contract.py`. -7. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +7. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0016-subprocess-agent-runner.md b/docs/RFC/0016-subprocess-agent-runner.md index 5e3d81fa..f54af311 100644 --- a/docs/RFC/0016-subprocess-agent-runner.md +++ b/docs/RFC/0016-subprocess-agent-runner.md @@ -21,7 +21,7 @@ ledger surfaces. The runner is **purely additive** to existing code. The current `agent_runner.py` (autonomous Markdown agent loop) is **not modified** and continues to work unchanged. RFC 0016 introduces a parallel, -kernel-managed runner that lives in a new `cc_kernel/runner/` package. +kernel-managed runner that lives in a new `kernel/runner/` package. A future patch can choose to migrate `agent_runner.py` onto this substrate; this RFC does not commit to that migration. @@ -76,7 +76,7 @@ substrate; this RFC does not commit to that migration. │ daemon process │ │ │ │ ┌──────────────┐ ┌─────────────────────────┐ │ -│ │ cc_daemon │ │ cc_kernel.runner │ │ +│ │ daemon │ │ kernel.runner │ │ │ │ RPC server │ │ ┌───────────────────┐ │ │ │ └──────┬───────┘ │ │ RunnerSupervisor │ │ │ │ │ │ └─┬─────┬─────┬─────┘ │ │ @@ -221,7 +221,7 @@ class RunnerSupervisor: The supervisor builds the subprocess command line by: 1. If `policy.use_bubblewrap=True`: prepend `bwrap` arguments via - `cc_kernel.sandbox.wrap_with_bubblewrap`. + `kernel.sandbox.wrap_with_bubblewrap`. 2. Always: pass `apply_rlimits_in_child(policy)` as `preexec_fn`. 3. Always: redirect stdin/stdout to pipes; stderr to a pipe the supervisor reads with a tail. @@ -267,7 +267,7 @@ patch. ## 8. Backwards compatibility - No schema change. -- No file outside `cc_kernel/runner/` (new package), `tests/`, and +- No file outside `kernel/runner/` (new package), `tests/`, and `docs/RFC/` is modified. - Existing `agent_runner.py` is not touched. The new runner is a parallel surface for kernel-managed agents. @@ -311,4 +311,4 @@ A PR claiming this RFC must: 8. Custom `charge` messages from the runner translate to ledger charges; first_breach generates a `kernel.runner.first_breach` event. -9. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +9. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0017-worker-loop.md b/docs/RFC/0017-worker-loop.md index 97513a81..0d849c8e 100644 --- a/docs/RFC/0017-worker-loop.md +++ b/docs/RFC/0017-worker-loop.md @@ -105,7 +105,7 @@ These are caller-supplied because: - The kernel doesn't know what command to run for an arbitrary agent. The orchestrator owns that mapping. - The default factory (`lambda entry: [sys.executable, "-m", - "cc_kernel.runner.runner_main"]`) suffices for tests. + "kernel.runner.runner_main"]`) suffices for tests. - Policy / env can vary per agent — different sandboxes, different budgets — and the orchestrator decides. @@ -182,7 +182,7 @@ _run_one(entry, argv, policy, env): - No schema change. - No new RPC method. -- No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +- No file outside `kernel/`, `tests/`, `docs/RFC/` modified. ## 7. Open questions @@ -212,4 +212,4 @@ A PR claiming this RFC must: `complete(exit_kind="cancelled")` (or "crashed"). 5. Background `start()` keeps draining the queue without explicit tick calls until `stop()`. -6. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +6. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0018-bridge-mirror.md b/docs/RFC/0018-bridge-mirror.md index 8af64fa4..6ddc30f4 100644 --- a/docs/RFC/0018-bridge-mirror.md +++ b/docs/RFC/0018-bridge-mirror.md @@ -10,7 +10,7 @@ This RFC defines how existing bridges (Telegram, WeChat, Slack, …) expose their inbound / outbound message streams through the kernel mailbox primitive. It does **not** touch any bridge code under `bridges/`. The deliverable is a small helper module -(`cc_kernel/bridge_mirror.py`) that: +(`kernel/bridge_mirror.py`) that: 1. Defines a **canonical topic naming scheme** so any agent in the system can subscribe to bridge traffic without knowing which @@ -195,7 +195,7 @@ A bridge wrapper for Telegram would: ## 6. Backwards compatibility - Bridge code in `bridges/` is **not** touched by this RFC. -- The `cc_kernel/bridge_mirror.py` module is purely additive. +- The `kernel/bridge_mirror.py` module is purely additive. - Existing tests for bridges keep passing (they don't use the mirror). - The mirror requires only the existing `kernel.mbox` primitives; @@ -233,4 +233,4 @@ A PR claiming this RFC must: leak if `start()` is never called. 5. Custom (non-`BridgeKind.KNOWN`) kinds work, e.g. `"matrix"`. 6. Validation rejects upper-case kinds and reserved characters. -7. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +7. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0019-llm-runner.md b/docs/RFC/0019-llm-runner.md index 382d8bb4..5037b0a0 100644 --- a/docs/RFC/0019-llm-runner.md +++ b/docs/RFC/0019-llm-runner.md @@ -33,8 +33,8 @@ The MVP scope is intentionally narrow: The runner ships as a parallel `__main__` entry point: ``` -python -m cc_kernel.runner.runner_main # echo runner (existing) -python -m cc_kernel.runner.llm # LLM runner (this RFC) +python -m kernel.runner.runner_main # echo runner (existing) +python -m kernel.runner.llm # LLM runner (this RFC) ``` WorkerLoop / RunnerSupervisor are unchanged — they don't care which @@ -53,7 +53,7 @@ protocol from RFC 0016. 3. **Provider portability.** A Provider is a thin protocol; new providers (OpenAI, Gemini, Ollama) plug in by writing one file that implements `__call__(LlmRequest) -> LlmResponse`. -4. **Defensive imports.** Importing `cc_kernel.runner.llm` on a +4. **Defensive imports.** Importing `kernel.runner.llm` on a machine without `anthropic` installed must NOT fail. 5. **Reproducible tests without API keys.** `MockProvider` reads its response shape from an env var so the subprocess pipeline @@ -184,13 +184,13 @@ deterministically without network or API keys. ## 6. Backwards compatibility -- New file `cc_kernel/runner/llm/__init__.py` and submodules. - No file outside `cc_kernel/`, `tests/`, `docs/RFC/` is touched. +- New file `kernel/runner/llm/__init__.py` and submodules. + No file outside `kernel/`, `tests/`, `docs/RFC/` is touched. - The existing `runner_main.py` is unchanged; tests using it stay green. - `anthropic` is already a project dependency (`requirements.txt`), - but its import in `cc_kernel.runner.llm.anthropic_provider` - happens lazily — only on first call — so `from cc_kernel import + but its import in `kernel.runner.llm.anthropic_provider` + happens lazily — only on first call — so `from kernel import *` works on machines without the SDK. ## 7. Failure modes @@ -227,7 +227,7 @@ A PR claiming this RFC must: 1. `MockProvider` constructed with a frozen response returns it verbatim on every call. 2. `LlmRequest` / `LlmResponse` round-trip via dataclass. -3. `python -m cc_kernel.runner.llm` with `CC_LLM_PROVIDER=mock` +3. `python -m kernel.runner.llm` with `CC_LLM_PROVIDER=mock` and a fixed `CC_LLM_MOCK_RESPONSE_JSON` exits 0, sends ready, sends `charge` messages for tokens + cost_micro, sends `exit` with `completed`. @@ -236,7 +236,7 @@ A PR claiming this RFC must: 5. End-to-end via WorkerLoop: enqueue an LLM job, worker spawns, ledger gets charged, scheduler entry → completed. 6. AnthropicProvider import is lazy: importing - `cc_kernel.runner.llm` works without anthropic SDK installed. + `kernel.runner.llm` works without anthropic SDK installed. 7. CC_LLM_PROVIDER unset → runner exits 2, supervisor sees crashed/failed with non-zero exit code. -8. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +8. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0020-dialogue-orchestrator.md b/docs/RFC/0020-dialogue-orchestrator.md index 312d3824..f8bc8356 100644 --- a/docs/RFC/0020-dialogue-orchestrator.md +++ b/docs/RFC/0020-dialogue-orchestrator.md @@ -26,7 +26,7 @@ Each turn is **its own subprocess**. A long conversation is N subprocess spawns, each charged separately to the ledger, each sandboxed independently. -This RFC ships **purely additive** code in `cc_kernel/orchestrator/`, +This RFC ships **purely additive** code in `kernel/orchestrator/`, plus two **backwards-compatible** extensions: - `LlmRequest` gets an optional `messages: list[dict]` field @@ -127,7 +127,7 @@ class DialogueOrchestrator: system: str = "", max_tokens: int = 1024, temperature: float = 0.7, - runner_argv: Sequence[str] | None = None, # default: cc_kernel.runner.llm + runner_argv: Sequence[str] | None = None, # default: kernel.runner.llm runner_policy: SandboxPolicy | None = None, runner_env: Mapping[str, str] | None = None, history_path: str | None = None, # default: /conversations//history.json @@ -294,7 +294,7 @@ to "". - AgentFS: no schema change; orchestrator writes through the existing `kernel.fs.write` API. -The new `cc_kernel/orchestrator/` package is purely additive. +The new `kernel/orchestrator/` package is purely additive. ## 9. Open questions @@ -331,4 +331,4 @@ A PR claiming this RFC must: 6. Daemon restart simulation: re-instantiate the orchestrator with the same `agent_pid`; the next `turn()` includes prior history. 7. `reset()` clears history (file is overwritten). -8. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +8. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0021-tool-dispatch.md b/docs/RFC/0021-tool-dispatch.md index 058cc474..17c98198 100644 --- a/docs/RFC/0021-tool-dispatch.md +++ b/docs/RFC/0021-tool-dispatch.md @@ -292,4 +292,4 @@ A PR claiming this RFC must: error=tool_failed`, runner doesn't crash. 6. Audit events `tool.call.dispatched` / `tool.call.denied` land in the event log. -7. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` modified. +7. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0022-llm-tool-calling.md b/docs/RFC/0022-llm-tool-calling.md index b354fd42..ccd8e5b9 100644 --- a/docs/RFC/0022-llm-tool-calling.md +++ b/docs/RFC/0022-llm-tool-calling.md @@ -238,5 +238,5 @@ A PR claiming this RFC must: hits max_iterations → exit failed. 6. The supervisor's tool dispatch invariants from RFC 0021 still apply (cap + fs check, audit events). -7. No file outside `cc_kernel/`, `tests/`, `docs/RFC/` +7. No file outside `kernel/`, `tests/`, `docs/RFC/` modified. diff --git a/docs/RFC/0023-shell-exec-tool.md b/docs/RFC/0023-shell-exec-tool.md index 258a59db..1b2c4b4b 100644 --- a/docs/RFC/0023-shell-exec-tool.md +++ b/docs/RFC/0023-shell-exec-tool.md @@ -210,7 +210,7 @@ operators can grep the event log for what was run. ## 6. Backwards compatibility -- Strictly additive new file ``cc_kernel/tools/exec_tool.py``. +- Strictly additive new file ``kernel/tools/exec_tool.py``. - ``register_builtin_tools`` is unchanged; existing setups with no Exec capability see no new behaviour. - ``register_exec_tool(registry)`` is the explicit opt-in. @@ -252,5 +252,5 @@ A PR claiming this RFC must: with stdout length ≤ 1024. 9. End-to-end via runner_main + supervisor + dispatched audit event present. -10. No file outside ``cc_kernel/``, ``tests/``, +10. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0024-glob-list-tools.md b/docs/RFC/0024-glob-list-tools.md index 3cf19894..42a64670 100644 --- a/docs/RFC/0024-glob-list-tools.md +++ b/docs/RFC/0024-glob-list-tools.md @@ -146,5 +146,5 @@ A PR claiming this RFC must: 7. ``include_hidden=False`` (default) excludes dotfiles. 8. fs_grants denial → fs_denied. 9. Capability denial → permission_denied. -10. No file outside ``cc_kernel/``, ``tests/``, ``docs/RFC/`` +10. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0025-fetch-tool.md b/docs/RFC/0025-fetch-tool.md index 3004fcea..95d2f0db 100644 --- a/docs/RFC/0025-fetch-tool.md +++ b/docs/RFC/0025-fetch-tool.md @@ -197,7 +197,7 @@ def fetch_handler(args, ctx): ## 5. Backwards compatibility -- New file ``cc_kernel/tools/fetch_tool.py``. +- New file ``kernel/tools/fetch_tool.py``. - ``register_builtin_tools`` is unchanged; Fetch is opt-in via ``register_fetch_tool(registry)``. - ``ToolNetDenied`` is a new exception in ``registry.py`` — @@ -239,5 +239,5 @@ A PR claiming this RFC must: 10. Redirect strips Authorization header (verified by serving a redirect that echoes incoming headers). 11. POST with body works. -12. No file outside ``cc_kernel/``, ``tests/``, ``docs/RFC/`` +12. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0026-ipc-streaming.md b/docs/RFC/0026-ipc-streaming.md index c7102a80..6fdffdf6 100644 --- a/docs/RFC/0026-ipc-streaming.md +++ b/docs/RFC/0026-ipc-streaming.md @@ -120,5 +120,5 @@ A PR claiming this RFC must: 5. Callback raising → next chunks still delivered + tuple still appended. 6. Existing tests with no chunks involvement keep passing. -7. No file outside ``cc_kernel/``, ``tests/``, ``docs/RFC/`` +7. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0027-llm-streaming.md b/docs/RFC/0027-llm-streaming.md index df2a5c16..bdd500c3 100644 --- a/docs/RFC/0027-llm-streaming.md +++ b/docs/RFC/0027-llm-streaming.md @@ -96,7 +96,7 @@ emits them as a finished `tool_use` block at the end.) ## 3. LLM runner integration -In ``cc_kernel/runner/llm/__main__.py``: +In ``kernel/runner/llm/__main__.py``: ```python stream = bool(payload.get("stream", False)) @@ -149,5 +149,5 @@ A PR claiming this RFC must: chunks; final text iteration emits per-delta chunks. 8. ``info.text`` matches the assembled deltas (same text either way). -9. No file outside ``cc_kernel/``, ``tests/``, ``docs/RFC/`` +9. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0028-exec-streaming.md b/docs/RFC/0028-exec-streaming.md index c2b89640..8ff83fb2 100644 --- a/docs/RFC/0028-exec-streaming.md +++ b/docs/RFC/0028-exec-streaming.md @@ -170,5 +170,5 @@ A PR claiming this RFC must: Exec output reaches the wait()-level callback in real time (test asserts at least one chunk arrives before process exit). -10. No file outside ``cc_kernel/``, ``tests/``, ``docs/RFC/`` +10. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0029-fetch-streaming.md b/docs/RFC/0029-fetch-streaming.md index d3fa347c..a3861025 100644 --- a/docs/RFC/0029-fetch-streaming.md +++ b/docs/RFC/0029-fetch-streaming.md @@ -95,5 +95,5 @@ A PR claiming this RFC must: are emitted; ``truncated=True`` in result. 8. Bad ``on_chunk`` callback (raises) doesn't break the fetch — exception swallowed at the boundary. -9. No file outside ``cc_kernel/``, ``tests/``, +9. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0030-diff-tool.md b/docs/RFC/0030-diff-tool.md index 4a6c7c09..9a9974d0 100644 --- a/docs/RFC/0030-diff-tool.md +++ b/docs/RFC/0030-diff-tool.md @@ -92,5 +92,5 @@ unified diff would exceed that, it's truncated with a 8. Diff > 2 MB → truncated flag + truncation marker. 9. ``register_builtin_tools`` includes "Diff" in its return list. -10. No file outside ``cc_kernel/``, ``tests/``, +10. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0031-ast-tool.md b/docs/RFC/0031-ast-tool.md index 0d638467..8f81a8de 100644 --- a/docs/RFC/0031-ast-tool.md +++ b/docs/RFC/0031-ast-tool.md @@ -106,5 +106,5 @@ count + a structured failure rather than an opaque 9. Path mode without "r" fs grant raises fs_denied. 10. ``register_builtin_tools`` includes "AST" in its return list. -11. No file outside ``cc_kernel/``, ``tests/``, +11. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/RFC/0032-git-tool.md b/docs/RFC/0032-git-tool.md index 0667ec79..419b3c99 100644 --- a/docs/RFC/0032-git-tool.md +++ b/docs/RFC/0032-git-tool.md @@ -126,5 +126,5 @@ Exec already grants fork+exec on any binary the agent has commit's first line. 8. fs_denied raised when agent lacks "r" on repo. 9. timed_out returns True if op exceeds timeout. -10. No file outside ``cc_kernel/``, ``tests/``, +10. No file outside ``kernel/``, ``tests/``, ``docs/RFC/`` modified. diff --git a/docs/agent-os.md b/docs/agent-os.md index b6192263..2ad54473 100644 --- a/docs/agent-os.md +++ b/docs/agent-os.md @@ -1,7 +1,7 @@ -# Agent OS — `cc_kernel/` +# Agent OS — `kernel/` CheetahClaws ships a **single-node agent operating system** under -`cc_kernel/`. It's the substrate that turns the legacy REPL/bridge +`kernel/`. It's the substrate that turns the legacy REPL/bridge into a long-running, multi-agent kernel: process table, capability model, quota ledger, scheduler, mailbox/registry, virtual filesystem, observability, and a stable JSON-RPC contract — backed by a single @@ -14,7 +14,7 @@ and CLI users see no difference. ## Why this exists -Before `cc_kernel/`, cheetahclaws was an *agent runtime/middleware*: +Before `kernel/`, cheetahclaws was an *agent runtime/middleware*: single-user REPL → tool dispatch → LLM. There was no place to: - Run multiple agents concurrently with isolation between them. @@ -31,7 +31,7 @@ the legacy single-process REPL path intact. ## Layout ``` -cc_kernel/ +kernel/ api.py # `Kernel` facade — open(...), make_supervisor(), … store.py # SQLite WAL store, single shared connection schema.py # Forward-only migrations v1 → v7 @@ -76,7 +76,7 @@ cheetahclaws kernel prometheus # Prometheus exposition text ``` Without `--enable-kernel`, the daemon serves the same surface as -before and `cc_kernel/` code is dormant. +before and `kernel/` code is dormant. ## RFC roadmap @@ -177,7 +177,7 @@ sequence post-exit. - Kernel SQLite schema is forward-only (versioned migrations `v1 → v7`). Old kernel.db files upgrade in place. - The v1.0 RPC contract (58 stable methods) has CI drift guard - via `cc_kernel/contract.py` — accidental method removal fails + via `kernel/contract.py` — accidental method removal fails the build. - Tests: 1771 passing, zero regressions on the legacy code paths. diff --git a/docs/architecture.md b/docs/architecture.md index 5a5bb4b1..70ae0486 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -46,7 +46,7 @@ The high-level shape: │ ├──► tool_registry.py ──► tools/ (fs, shell, web, notebook, │ diagnostics, interaction, …) - │ + memory/, multi_agent/, skill/, cc_mcp/, + │ + memory/, multi_agent/, skill/, mcp_client/, │ task/, checkpoint/hooks, plugins, modular/ │ ├──► compaction.py ── snip + LLM-summarize old turns @@ -87,7 +87,7 @@ responsibility. | [`providers.py`](../providers.py) | Provider registry (`PROVIDERS` dict), auto-detection by model prefix, streaming adapters for Anthropic native + OpenAI-compatible APIs | | [`tool_registry.py`](../tool_registry.py) | Central `ToolDef` registry, dispatch, output truncation | | [`runtime.py`](../runtime.py) | `RuntimeContext` — per-session live state (callbacks, bridge flags, plan-mode state, streaming hooks). **Not** persisted. | -| [`cc_config.py`](../cc_config.py) | Defaults + `~/.cheetahclaws/config.json` load/save. Strips `_`-prefixed keys on save. | +| [`config.py`](../config.py) | Defaults + `~/.cheetahclaws/config.json` load/save. Strips `_`-prefixed keys on save. | | [`quota.py`](../quota.py) | Per-session and daily token/cost budgets. Checked before every API call. | | [`circuit_breaker.py`](../circuit_breaker.py) | Trip-open-after-N-failures protection around provider calls. | | [`error_classifier.py`](../error_classifier.py) | Categorize API errors (rate limit / context-too-long / network / transient) so `agent.run()` can pick the right retry strategy. | @@ -113,7 +113,7 @@ internal structure. | [`memory/`](../memory) | Persistent memory across sessions — `store.py` (CRUD), `scan.py`/`context.py` (index + freshness), `consolidator.py` (`/memory consolidate`), `tools.py` (`MemorySave` / `MemoryDelete` / `MemorySearch` / `MemoryList`). | | [`multi_agent/`](../multi_agent) | Sub-agent subsystem. `subagent.py` owns `SubAgentManager` (ThreadPoolExecutor), depth gating, git-worktree isolation; `tools.py` exposes `Agent` / `SendMessage` / `CheckAgentResult` / `ListAgentTasks` / `ListAgentTypes`. | | [`skill/`](../skill) | Markdown-based skill templates — `loader.py` parses frontmatter + resolves project→user→built-in precedence, `executor.py` runs a skill inline or in a fork, `builtin.py` ships a few default skills, `tools.py` exposes `Skill` / `SkillList`. | -| [`cc_mcp/`](../cc_mcp) | MCP (Model Context Protocol) client — `config.py` loads `.mcp.json`, `client.py` speaks stdio/SSE/HTTP JSON-RPC, `tools.py` connects servers and registers each remote tool as `mcp____`. Renamed from `mcp/` to avoid stdlib collision. | +| [`mcp_client/`](../mcp_client) | MCP (Model Context Protocol) client — `config.py` loads `.mcp.json`, `client.py` speaks stdio/SSE/HTTP JSON-RPC, `tools.py` connects servers and registers each remote tool as `mcp____`. Renamed from `mcp/` to avoid stdlib collision. | | [`task/`](../task) | In-session task list — `types.py` (model + status enum), `store.py` (thread-safe CRUD + dependency-edge maintenance), `tools.py` (`TaskCreate` / `TaskUpdate` / `TaskGet` / `TaskList`). | | [`checkpoint/`](../checkpoint) | Auto-snapshot of conversation + file state after every turn. `types.py` data models, `store.py` backup + rewind, `hooks.py` monkey-patches `Write` / `Edit` / `NotebookEdit` to snapshot pre-edit. Command wiring in `commands/checkpoint_plan.py`. | | [`plugin/`](../plugin) | Plugin install / enable / disable / update from git URLs or local paths. `loader.py` imports user plugins and registers their `TOOL_DEFS` / `COMMAND_DEFS`; `recommend.py` scores plugin marketplace by keyword/tag match. | @@ -163,7 +163,7 @@ class ToolDef: at the bottom of the file). 2. **Extension packages** — a `_EXTENSION_MODULES` list in `tools/__init__.py` (`memory.tools`, `multi_agent.tools`, - `skill.tools`, `cc_mcp.tools`, `task.tools`) is imported for side + `skill.tools`, `mcp_client.tools`, `task.tools`) is imported for side effects; each module calls `register_tool()` at its own import time. Failures are swallowed (extensions are best-effort). 3. **Plugins** — user-installed packages expose a `TOOL_DEFS` list; the @@ -437,7 +437,7 @@ model can enter/exit plan mode without interactive friction. Plus two security layers that apply regardless of mode: -- **`allowed_root`** (`cc_config.py` default `None`) — if set to a +- **`allowed_root`** (`config.py` default `None`) — if set to a path, restricts file tools (Read / Write / Edit / Glob / Grep) to that subtree. Null means unrestricted (CLI default). - **`shell_policy`** — `allow` (default) / `log` / `deny` for the @@ -473,7 +473,7 @@ call and records usage after. Budgets are: - `daily_token_budget`, `daily_cost_budget` — aggregated across all sessions for today. -All four default to `None` (unlimited) in `cc_config.DEFAULTS`. When +All four default to `None` (unlimited) in `config.DEFAULTS`. When exceeded, `agent.run()` yields a `TextChunk("[Quota exceeded — …]")` and breaks the loop. Long-running / autonomous workflows should turn these on. @@ -623,7 +623,7 @@ runs a cheap LLM pass over the current session and saves up to 3 high-confidence insights without overwriting higher-confidence user entries. -### MCP (`cc_mcp/`) +### MCP (`mcp_client/`) Standard MCP client. Supports stdio (subprocess), SSE, and streamable HTTP transports. `.mcp.json` in the project root or @@ -632,9 +632,9 @@ reconnects. Every discovered remote tool is registered as `mcp____` and participates in the normal permission / execution flow. -Renamed from `mcp/` to `cc_mcp/` to avoid import-time collision with +Renamed from `mcp/` to `mcp_client/` to avoid import-time collision with Python's stdlib namespace and the `modelcontextprotocol` package. -**Import from `cc_mcp`, not `mcp`.** +**Import from `mcp_client`, not `mcp`.** ### Tasks (`task/`) @@ -725,7 +725,7 @@ Per-iteration behavior: `CHEETAHCLAWS_ENABLE_F4=1` or `agent_runner_subprocess: true` flips `start_runner` from threading to subprocess-per-runner. Each runner becomes a `python -m agent_runner --pipe` child supervised by -`cc_daemon.runner_supervisor`; iteration boundaries and crashes are +`daemon.runner_supervisor`; iteration boundaries and crashes are observable on the daemon event bus and persisted to the `agent_runs` / `agent_iterations` SQLite tables. The threaded path stays the default so REPL behaviour is byte-for-byte unchanged. See @@ -738,7 +738,7 @@ readiness gaps (daemon mode, SQLite session store, cost guardrails). The daemon-mode work is tracked in [issue #68](https://github.com/SafeRL-Lab/cheetahclaws/issues/68); the IPC / permission-routing / local-auth contract is captured in [RFC 0001](RFC/0001-daemon-design-note.md) and validated end-to-end by -the `cc_daemon/` reference scaffolding ([spike notes](RFC/0001-spike-notes.md)). +the `daemon/` reference scaffolding ([spike notes](RFC/0001-spike-notes.md)). ### Modular ecosystem (`modular/`) @@ -769,7 +769,7 @@ The web UI will eventually become a client of the daemon described in the next section (per [RFC 0001](RFC/0001-daemon-design-note.md)); today it stands alone. -### Daemon (`cc_daemon/` + `commands/daemon_cmd.py`) +### Daemon (`daemon/` + `commands/daemon_cmd.py`) The headless `cheetahclaws serve` runtime — foundation for the "long-running services survive REPL exit" work tracked in @@ -786,38 +786,38 @@ top. Pulled in unchanged from the spike (these encode the wire contract): -- `cc_daemon/__init__.py` — `API_VERSION = "0"`, `API_VERSION_HEADER = +- `daemon/__init__.py` — `API_VERSION = "0"`, `API_VERSION_HEADER = "Cheetahclaws-Api-Version"`. -- `cc_daemon/server.py` — `ThreadedTCPServer` and `ThreadedUnixServer` +- `daemon/server.py` — `ThreadedTCPServer` and `ThreadedUnixServer` (the latter conditional on `socketserver.UnixStreamServer`, so Windows skips it cleanly), 256-deep listen backlog, per-connection request handler, SSE loop with 15 s heartbeat, `Cheetahclaws-Api-Version` gate that returns `426` on mismatch. -- `cc_daemon/auth.py` — `SO_PEERCRED` peer-cred check (Linux; macOS +- `daemon/auth.py` — `SO_PEERCRED` peer-cred check (Linux; macOS TODO), bearer-token auth for TCP, per-peer brute-force throttle, audit-log default-on for both transports. -- `cc_daemon/originator.py` — `client_id` mint / persist +- `daemon/originator.py` — `client_id` mint / persist (`~/.cheetahclaws/clients/.id`) / resume so disconnect-and- reconnect keeps the originator identity stable. -- `cc_daemon/rpc.py` — JSON-RPC 2.0 dispatcher. Application errors +- `daemon/rpc.py` — JSON-RPC 2.0 dispatcher. Application errors `-32001` (`not_originator`) and `-32002` (`unknown_request`) carry HTTP `403` so observers can't answer permission requests they don't own. -- `cc_daemon/events.py` — in-memory ring buffer + per-subscriber Queue; +- `daemon/events.py` — in-memory ring buffer + per-subscriber Queue; emits a `gap` event on overflow so SSE clients know to re-sync. F-2 swaps the ring for the `daemon_events` SQLite table without changing the channel API. -- `cc_daemon/permission.py` — pending-request store, originator-only +- `daemon/permission.py` — pending-request store, originator-only `answer`, 30 min default interactive timeout + `permission.refresh_timeout` RPC. -- `cc_daemon/methods.py` — spike's `echo.ping` / `permission.demo` / +- `daemon/methods.py` — spike's `echo.ping` / `permission.demo` / `permission.answer` / `permission.refresh_timeout` / `permission.list`. -- `cc_daemon/spike_client.py` — stdlib smoke client, useful for manual +- `daemon/spike_client.py` — stdlib smoke client, useful for manual debugging; not a runtime dependency. Added by the F-1 foundation: -- `cc_daemon/discovery.py` — atomic write/read of +- `daemon/discovery.py` — atomic write/read of `~/.cheetahclaws/daemon.json` (pid, transport, address, started_at, schema version, plus an optional `token_path` recorded only when `serve --token-path` overrides the default location) so REPL / Web / @@ -825,12 +825,12 @@ Added by the F-1 foundation: themselves — can locate the daemon and the token file it's actually using. Auto-clears stale files when the recorded pid is no longer alive. -- `cc_daemon/system_methods.py` — registers `system.ping` (RFC contract +- `daemon/system_methods.py` — registers `system.ping` (RFC contract name; coexists with spike's `echo.ping`) and `system.shutdown` (triggers `DaemonState.shutdown_event`, our cross-platform graceful exit since Windows can't deliver SIGTERM cleanly to another Python process). -- `cc_daemon/cli.py` — rewritten `serve_main(argv)` that calls +- `daemon/cli.py` — rewritten `serve_main(argv)` that calls `bootstrap()`, pins `log_file` to `/logs/daemon.log`, threads loaded config + `--unauthenticated-metrics` through `DaemonState`, writes the discovery file on bind, watches the @@ -843,14 +843,14 @@ Added by the F-1 foundation: - `health.py` — refactored: extracted `healthz_payload(config)` / `readyz_payload(config)` / `metrics_payload(config)` / `payload_for(path, config)` module-level helpers so both the existing - standalone health HTTP server and `cc_daemon/server.py` reuse the + standalone health HTTP server and `daemon/server.py` reuse the same circuit-breaker / quota / runtime-registry probes without starting a second listener. Added by the F-4 skeleton (subprocess-per-agent — branch `daemon/f-4`, [RFC 0002 §F-4](RFC/0002-daemon-foundation-roadmap.md#f-4--agent_runner-subprocess)): -- `cc_daemon/runner_supervisor.py` — owns the lifecycle of one or more +- `daemon/runner_supervisor.py` — owns the lifecycle of one or more `python -m agent_runner --pipe` subprocesses. `start` / `stop` / `stop_all` / `get` / `list_all`. Three-phase stop bounded ≤ 5 s (IPC `stop` → SIGTERM at 2 s → SIGKILL at 5 s). Per-runner @@ -860,10 +860,10 @@ Added by the F-4 skeleton (subprocess-per-agent — branch `daemon/f-4`, `agent_runner_stopped` / `agent_runner_crash`). All DB writes are best-effort; supervisor never crashes on persistence failure. POSIX only (`enabled()` returns False on Windows). -- `cc_daemon/runner_ipc.py` — thin re-export of - `cc_kernel.runner.ipc.JsonLineChannel` so both runner families share +- `daemon/runner_ipc.py` — thin re-export of + `kernel.runner.ipc.JsonLineChannel` so both runner families share one IPC implementation and one set of bug fixes. -- `cc_daemon/agent_methods.py` — JSON-RPC handlers `agent.start`, +- `daemon/agent_methods.py` — JSON-RPC handlers `agent.start`, `agent.stop`, `agent.list`, `agent.status`, registered from `DaemonState.__init__` alongside `system_methods` / `monitor_methods`. Param validation raises `TypeError` so the dispatcher returns the @@ -947,7 +947,7 @@ left untouched: so an operator sees disabled bridges from earlier daemon runs. - `schema_meta` — schema version + per-feature migration markers. -`cc_daemon/schema.py:init_schema()` is idempotent (CREATE IF NOT +`daemon/schema.py:init_schema()` is idempotent (CREATE IF NOT EXISTS only) and serialised by an internal lock, so concurrent serve attempts can't trip on each other. Schema version is recorded as `schema_meta.schema_version`; future bumps go through @@ -977,7 +977,7 @@ Behaviour: - **REPL detects daemon → skips local scheduler.** When the user types `/monitor start` in REPL while a daemon is running, - `commands/monitor_cmd.py` calls `cc_daemon.discovery.locate()`, sees + `commands/monitor_cmd.py` calls `daemon.discovery.locate()`, sees a live daemon, prints "scheduler is owned by the running daemon", and no-ops. Avoids the race of two schedulers fighting over `last_run_at` and double-firing subscriptions. `/monitor stop` @@ -985,7 +985,7 @@ Behaviour: - **`/monitor subscribe` / `unsubscribe` / `list` always work in REPL.** These hit SQLite directly through `monitor.store`; the daemon picks up the new state on its next 60 s poll. No RPC round-trip needed. -- **External clients use RPC.** `cc_daemon/monitor_methods.py` +- **External clients use RPC.** `daemon/monitor_methods.py` registers `monitor.subscribe`, `monitor.unsubscribe`, `monitor.list`, `monitor.run` for Web UI / third-party tools that don't share the process tree. @@ -1016,13 +1016,13 @@ the remaining acceptance gaps (RFC 0002 §F-4 #1/#2/#3): - **§F-4 #1 — permission routing.** When a runner is started with `auto_approve=False`, the supervisor routes the runner's `permission_request` IPC frame through - `cc_daemon/permission.py:PermissionStore`. The originator (the + `daemon/permission.py:PermissionStore`. The originator (the `client_id` that called `agent.start`) is the only client that can answer via `permission.answer`. Timeouts and denials feed back over IPC as `permission_response`; the runner unblocks within its 30-minute wait either way. - **§F-4 #2 — bridge `notify` forwarding.** The reader's `notify` IPC - branch now calls `cc_daemon/bridge_supervisor.notify(kind, text)` so + branch now calls `daemon/bridge_supervisor.notify(kind, text)` so a subprocess runner's iteration summary reaches the originating bridge (Telegram / Slack / WeChat). The runner can target a specific bridge via `msg["bridge"]` or omit it for a `"*"` broadcast. @@ -1042,7 +1042,7 @@ the remaining acceptance gaps (RFC 0002 §F-4 #1/#2/#3): #### Proactive watcher in daemon (F-5) `_proactive_watcher_loop` from `cheetahclaws.py` is now daemon-owned. -`cc_daemon/proactive_state.py` persists `proactive.enabled` / +`daemon/proactive_state.py` persists `proactive.enabled` / `proactive.interval_s` / `proactive.last_tick_at` in the F-2 `schema_meta` table (so the setting survives daemon restarts); a single background thread (`proactive-scheduler`) ticks at 1 s, @@ -1055,7 +1055,7 @@ double-firing across REPL + daemon. #### Bridges in daemon (F-6 / F-7 / F-8) -`cc_daemon/bridge_supervisor.py` owns the lifecycle of one or more +`daemon/bridge_supervisor.py` owns the lifecycle of one or more daemon-side bridge threads, gated per-kind by feature flags so REPL behaviour is byte-for-byte unchanged until the user opts in: @@ -1082,7 +1082,7 @@ Two modes per bridge: phone message instead of calling `session_ctx.run_query`. Wire-level RPCs: `bridge.start`, `bridge.stop`, `bridge.list`, -`bridge.send`, `bridge.status` (in `cc_daemon/bridge_methods.py`). +`bridge.send`, `bridge.status` (in `daemon/bridge_methods.py`). Persisted state lives in the F-2 `bridges` table (`kind`, `enabled`, `config_json`, `last_poll_at`, `last_error`); secrets are redacted to last 4 chars before any row write or bus publish (broad pattern: @@ -1095,7 +1095,7 @@ bridge-driven turn can use this as the PermissionStore originator #### Session message-passing primitives (F-6 Phase 2 support) -`cc_daemon/session_methods.py` registers three methods that any +`daemon/session_methods.py` registers three methods that any inbound / outbound source can talk: - **`session.send(session_id, text, origin?, message_id?)`** — @@ -1152,11 +1152,11 @@ Per-runner quota-pause hook: | Pre-iter check | `AgentRunner._run_loop` (top of each iter) | `quota.check_quota` against `_config`; raises `QuotaExceeded` → `_on_quota_exceeded(qe)`. | | Base impl | `AgentRunner._on_quota_exceeded` | No-op — REPL path keeps today's behaviour (agent.run catches internally, yields `[Quota exceeded …]` text). | | F-4 override | `_PipeAgentRunner._on_quota_exceeded` | Sends `paused_budget` IPC, sets `status='paused_budget'`, blocks on `_resume_event.wait()`. Wakes from `resume` IPC, sends `resumed` IPC, returns. | -| Supervisor inbound | `cc_daemon/runner_supervisor:_reader_loop` | New `paused_budget` / `resumed` branches: flip `agent_runs.status` in SQLite, publish `quota_warn` / `agent_runner_resumed` on the bus. | +| Supervisor inbound | `daemon/runner_supervisor:_reader_loop` | New `paused_budget` / `resumed` branches: flip `agent_runs.status` in SQLite, publish `quota_warn` / `agent_runner_resumed` on the bus. | | Supervisor outbound | `runner_supervisor.resume(name)` | Sends `resume` IPC to the named runner; called by `agent.resume(name=…)`. | | Control loop | `agent_runner._pipe_main:_control_loop` | New `resume` handler sets `_resume_event`. `stop` handler also sets it so a stop arriving while paused unblocks cleanly. | -### Agent OS kernel (`cc_kernel/`) +### Agent OS kernel (`kernel/`) Layer above the daemon and below the user-facing CLI/REPL/bridges. Turns cheetahclaws into a true single-node agent operating system: @@ -1172,56 +1172,56 @@ overview at [`docs/agent-os.md`](agent-os.md). **Module map.** -- `cc_kernel/api.py` — `Kernel` facade. `Kernel.open(...)` opens a +- `kernel/api.py` — `Kernel` facade. `Kernel.open(...)` opens a WAL-mode SQLite store and exposes the `cap` / `ledger` / `sched` / `mbox` / `registry` / `fs` / `events` substores. `make_supervisor()` constructs a `Supervisor` ready to spawn subprocess agents. -- `cc_kernel/store.py` + `cc_kernel/schema.py` — single-connection +- `kernel/store.py` + `kernel/schema.py` — single-connection store with forward-only migrations (v1 → v7); a `write_lock` serializes mutations across substores. -- `cc_kernel/capability.py` (RFC 0005) — `tool_grants` / `fs_grants` +- `kernel/capability.py` (RFC 0005) — `tool_grants` / `fs_grants` / `net_grants` / `model_grants` / `sub_agent` capability bag with `derive(...)` for sub-agent attenuation. -- `cc_kernel/ledger.py` (RFC 0006) — per-agent ResourceLedger with +- `kernel/ledger.py` (RFC 0006) — per-agent ResourceLedger with atomic `charge` + `first_breach` signal so the scheduler can shed load without polling. -- `cc_kernel/scheduler.py` (RFC 0007) — priority queue + +- `kernel/scheduler.py` (RFC 0007) — priority queue + admission filter (consults ledger before claim). -- `cc_kernel/mailbox.py` (RFC 0009) — direct + topic pub/sub +- `kernel/mailbox.py` (RFC 0009) — direct + topic pub/sub with at-least-once delivery semantics. -- `cc_kernel/registry.py` (RFC 0010) — name → pid lookup for +- `kernel/registry.py` (RFC 0010) — name → pid lookup for service discovery. -- `cc_kernel/agent_fs.py` (RFC 0011) — VFS unifying memory / +- `kernel/agent_fs.py` (RFC 0011) — VFS unifying memory / checkpoint / skill / task storage. -- `cc_kernel/sandbox.py` (RFC 0008) — RLIMIT (CPU/AS/FSIZE/ +- `kernel/sandbox.py` (RFC 0008) — RLIMIT (CPU/AS/FSIZE/ NOFILE) preexec_fn + optional bubblewrap wrapper + wall-clock killer thread + `new_session` (own process group). -- `cc_kernel/contract.py` (RFC 0013) — frozen v1.0 method +- `kernel/contract.py` (RFC 0013) — frozen v1.0 method registry; CI drift guard fails the build if a registered RPC method isn't classified `stable`/`experimental`/ `deprecated`. -- `cc_kernel/cli.py` — `cheetahclaws kernel ` subcommand +- `kernel/cli.py` — `cheetahclaws kernel ` subcommand for read-only inspection over the daemon's RPC: `summary`, `info`, `agents`, `proc `, `events`, `queue`, `registry`, `methods`, `prometheus`. -- `cc_kernel/runner/supervisor.py` (RFC 0016/0017) — spawns +- `kernel/runner/supervisor.py` (RFC 0016/0017) — spawns subprocess agents with a JSON-line IPC channel (`runner/ipc.py`); processes `init` / `ready` / `tool_call` / `chunk` / `iteration_done` / `exit` messages; integrates the streaming-chunk substrate (RFC 0026) so callers can subscribe to incremental output via `wait(pid, on_chunk=...)`. -- `cc_kernel/runner/llm/` (RFC 0019/0020/0022/0027) — LLM +- `kernel/runner/llm/` (RFC 0019/0020/0022/0027) — LLM agent runner. Provider protocol (callable returning `LlmResponse` + optional `stream(req, on_delta)`); Anthropic + scripted-mock adapters; multi-iteration tool-calling loop with per-iter chunk emission; multi-turn dialogue orchestrator. -- `cc_kernel/runner/bridge_mirror/` (RFC 0018) — mirrors +- `kernel/runner/bridge_mirror/` (RFC 0018) — mirrors bridges' inbound/outbound messages into `kernel.mbox` and back without touching `bridges/` source files (BC constraint). -- `cc_kernel/tools/` — tool registry + dispatch + handlers. +- `kernel/tools/` — tool registry + dispatch + handlers. Auto-registered: `Echo`, `Read`, `Write`, `Glob`, `List`, `Diff`, `AST`. Opt-in (operator must call `register_`): `Exec`, `Fetch`, `Git` — each with its @@ -1242,7 +1242,7 @@ sink: arrival order; bad callbacks are caught at the boundary so they can't break the wait loop. -**Backwards compatibility.** All surface in `cc_kernel/` is +**Backwards compatibility.** All surface in `kernel/` is isolated; the only edits outside the package are one-line opt-in hooks in `cheetahclaws.py` (the `cheetahclaws kernel ...` subcommand dispatcher). Schema is forward-only — old `kernel.db` @@ -1421,7 +1421,7 @@ corruption. Any new file-writing tool must mirror this. ## Data flow: end-to-end example -User types `Read cc_config.py and change session_daily_limit to 20` +User types `Read config.py and change session_daily_limit to 20` with Claude as the active model. ``` @@ -1433,7 +1433,7 @@ with Claude as the active model. 6. providers.stream() detects "claude-*" → stream_anthropic() 7. context already built system prompt = default.md + claude overlay + env 8. Model responds: "I'll read it first." - + tool_call[Read(file_path=".../cc_config.py")] + + tool_call[Read(file_path=".../config.py")] 9. agent._check_permission Read is read_only → auto-approve 10. tool_registry.execute_tool Read via tools.fs._read → file content 11. checkpoint hook: no-op (Read doesn't mutate, no snapshot) @@ -1486,9 +1486,13 @@ Sub-agent tests mock `_agent_run` to avoid real API calls. CI A collection of non-obvious traps; most bit someone at some point. -- **Renamed modules**: `config.py` → `cc_config.py`; `mcp/` → `cc_mcp/`. - Rename was forced by stdlib / package namespace collisions. Always - `import cc_config` / `from cc_mcp import ...`. +- **`cc_` prefix dropped**: modules once carried a `cc_` prefix + (`cc_config.py`, `cc_daemon/`, `cc_kernel/`, `cc_mcp/`); the prefix + was removed for readability. Three of the four reverted to plain + names (`config`, `daemon`, `kernel`). The MCP client could **not** + revert to bare `mcp` — that shadows Python's namespace and the + `modelcontextprotocol` package — so it is `mcp_client/`. Always + `import config` / `from mcp_client import ...`, never `import mcp`. - **`.nano_claude/plans/` vs `~/.cheetahclaws/`**: runtime state is under `~/.cheetahclaws/` (underscore), but plan mode writes to `.nano_claude/plans/.md` in cwd. The `.nano_claude` path diff --git a/docs/contributor_guide.md b/docs/contributor_guide.md index 89aa890d..da345b46 100644 --- a/docs/contributor_guide.md +++ b/docs/contributor_guide.md @@ -45,7 +45,7 @@ If you remember only one thing, remember this flow: - `context.py` — system prompt assembly entry point (`build_system_prompt`); injects env block + memory + tmux/plan fragments around the base prompt. - `prompts/` — system prompt assets as plain Markdown. `base/default.md` is the shared baseline for every model; `overlays/.md` (claude / gemini / openai-reasoning / qwen) appends short, vendor-documented quirks; `fragments/{tmux,plan}.md` are conditional blocks. `select.py::pick_base_prompt` assembles base + matched overlay. See `prompts/README.md` for the overlay-admission policy. - `compaction.py` — context window management (`snip_old_tool_results` + `compact_messages`). -- `cc_config.py` — defaults + persistent config file handling. +- `config.py` — defaults + persistent config file handling. --- @@ -120,13 +120,13 @@ Use this package for snapshot policies, backup strategies, file restore behavior Use this package for STT backend changes, audio capture behavior, and prompt-boosting vocabulary logic. -## Agent OS kernel (`cc_kernel/`) -- `cc_kernel/api.py` — `Kernel.open(...)` facade: SQLite-backed substores for capability, ledger, scheduler, mailbox, registry, AgentFS, events. -- `cc_kernel/contract.py` — frozen v1.0 RPC method registry (CI drift guard). -- `cc_kernel/runner/supervisor.py` — subprocess agent spawn + JSON-line IPC + streaming chunk relay. -- `cc_kernel/runner/llm/` — LLM agent runner (Anthropic + scripted-mock providers, multi-turn dialogue, tool-calling loop, token streaming). -- `cc_kernel/tools/` — tool registry + dispatch; auto-registered (Echo, Read, Write, Glob, List, Diff, AST) and opt-in (Exec, Fetch, Git). -- `cc_kernel/cli.py` — `cheetahclaws kernel ` subcommand (read-only inspection over the daemon RPC). +## Agent OS kernel (`kernel/`) +- `kernel/api.py` — `Kernel.open(...)` facade: SQLite-backed substores for capability, ledger, scheduler, mailbox, registry, AgentFS, events. +- `kernel/contract.py` — frozen v1.0 RPC method registry (CI drift guard). +- `kernel/runner/supervisor.py` — subprocess agent spawn + JSON-line IPC + streaming chunk relay. +- `kernel/runner/llm/` — LLM agent runner (Anthropic + scripted-mock providers, multi-turn dialogue, tool-calling loop, token streaming). +- `kernel/tools/` — tool registry + dispatch; auto-registered (Echo, Read, Write, Glob, List, Diff, AST) and opt-in (Exec, Fetch, Git). +- `kernel/cli.py` — `cheetahclaws kernel ` subcommand (read-only inspection over the daemon RPC). - Activated only when daemon runs with `--enable-kernel`. Default REPL/bridges path is byte-for-byte unchanged. Use this package for agent isolation, capability/quota policy, scheduler tuning, AgentFS storage, sandbox primitives, or new built-in tools. Every behavioural change MUST land with an RFC under `docs/RFC/` (acceptance criteria + BC story); see [`docs/agent-os.md`](agent-os.md) for the index of all 27 shipped RFCs. @@ -144,10 +144,10 @@ Use this package for agent isolation, capability/quota policy, scheduler tuning, ### Add a new kernel tool (under `--enable-kernel`) 1. Write a one-page RFC under `docs/RFC/00NN--tool.md` (problem, args, capability/fs/net checks, output shape, BC story, acceptance criteria). -2. Add `cc_kernel/tools/_tool.py` with a `_TOOL` `Tool` instance (fields: `name`, `description`, `handler`, `requires_capability`, `requires_fs`). -3. Auto-register (zero-side-effect inspectors only) by adding to `cc_kernel/tools/builtin.py::register_builtin_tools` AND to its return list. Otherwise expose `register__tool(registry)` and document it as **opt-in**. -4. Re-export from `cc_kernel/tools/__init__.py` `__all__`. -5. Append the RFC number to `cc_kernel/contract.py::RFCS_IMPLEMENTED`. +2. Add `kernel/tools/_tool.py` with a `_TOOL` `Tool` instance (fields: `name`, `description`, `handler`, `requires_capability`, `requires_fs`). +3. Auto-register (zero-side-effect inspectors only) by adding to `kernel/tools/builtin.py::register_builtin_tools` AND to its return list. Otherwise expose `register__tool(registry)` and document it as **opt-in**. +4. Re-export from `kernel/tools/__init__.py` `__all__`. +5. Append the RFC number to `kernel/contract.py::RFCS_IMPLEMENTED`. 6. Add tests under `tests/test_kernel__tool.py` covering args validation, capability/fs gates, success path, and the acceptance criteria from the RFC. 7. If the tool emits incremental output, route it through `ctx.on_chunk(payload)` so `Supervisor.wait(on_chunk=...)` callers see it (RFC 0028 substrate). diff --git a/docs/news.md b/docs/news.md index 25f266fb..a6850468 100644 --- a/docs/news.md +++ b/docs/news.md @@ -26,11 +26,11 @@ **Plugin loader hardening.** Two new env switches in `plugin/loader.py`: `CHEETAHCLAWS_DISABLE_PLUGINS=1` (kill switch) and `CHEETAHCLAWS_PLUGIN_ALLOWLIST=a,b,c` (whitelist). EXTERNAL-scope plugins (loaded via `$CHEETAHCLAWS_PLUGIN_PATH`) print a one-time stderr warning on first load so a stolen env-var-set doesn't silently execute. Module path resolution now uses `Path.resolve()` + `relative_to(install_dir)` to confine a malicious manifest's `"tools": ["../../etc/passwd_loader"]` style entry. - **MCP env sanitisation.** `cc_mcp/client.py:_sanitized_mcp_env` strips a fixed set of process-hijack keys (`LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, `PYTHONPATH`, `PYTHONSTARTUP`, `PYTHONHOME`, `PYTHONEXECUTABLE`, `NODE_OPTIONS`, `NODE_PATH`, `BASH_ENV`, `ENV`) from any `env` map an `.mcp.json` config supplies. Dropped keys print a one-line stderr notice. Bypass: `CHEETAHCLAWS_MCP_TRUST_ENV=1`. Closes a real local-priv-esc path on a host with multiple MCP server configs of varying trust. + **MCP env sanitisation.** `mcp_client/client.py:_sanitized_mcp_env` strips a fixed set of process-hijack keys (`LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, `PYTHONPATH`, `PYTHONSTARTUP`, `PYTHONHOME`, `PYTHONEXECUTABLE`, `NODE_OPTIONS`, `NODE_PATH`, `BASH_ENV`, `ENV`) from any `env` map an `.mcp.json` config supplies. Dropped keys print a one-line stderr notice. Bypass: `CHEETAHCLAWS_MCP_TRUST_ENV=1`. Closes a real local-priv-esc path on a host with multiple MCP server configs of varying trust. - **macOS daemon peer-cred.** `cc_daemon/auth.py:get_peer_uid` now branches on `sys.platform`: Linux keeps `SO_PEERCRED`, macOS / *BSD goes through ctypes-loaded `getpeereid(2)`. Closes a long-standing TODO that effectively reduced macOS Unix-socket auth to token-only (a stolen daemon-token implied full RCE without peer-uid validation). + **macOS daemon peer-cred.** `daemon/auth.py:get_peer_uid` now branches on `sys.platform`: Linux keeps `SO_PEERCRED`, macOS / *BSD goes through ctypes-loaded `getpeereid(2)`. Closes a long-standing TODO that effectively reduced macOS Unix-socket auth to token-only (a stolen daemon-token implied full RCE without peer-uid validation). - **Smaller fixes folded in.** Web JWT secret loader rewritten with `O_CREAT \| O_EXCL` + 0o600 + post-write mode verification (refuses to read a world-readable secret file; auto-falls-back to in-memory secret if chmod can't be enforced; override with `CHEETAHCLAWS_WEB_SECRET`). Terminal one-time password from `secrets.token_urlsafe(6)[:6]` (~30 bits, online-bruteable) to `secrets.token_urlsafe(32)` (~190 bits). `cc_config.save_config` strips `permission_mode=accept-all` before persisting — once-confirmed escape hatches no longer outlive the session that set them. `session_store.save_session` wrapped in a module-level `Lock` + explicit `BEGIN IMMEDIATE` / `ROLLBACK` so two threads writing the same `session_id` no longer silently drop one set of changes. `agent_runner.py` `err_msg` initialised before the try block (defends against a `NameError` on first iteration if `_handle_permission_request` returns `"error"`); `quota.QuotaExceeded` matched by `isinstance` instead of class-name string. `compaction.compact_messages` wraps `stream_auxiliary` in try/except + falls back to the original messages instead of crashing the agent loop. `providers._recover_args_from_text` caps the regex scan window to the last 32 KB of accumulated text (was scanning ~100 KB+ on every tool call). `context.get_git_info` + `get_claude_md` get TTL caches (30 s / 10 s, keyed by cwd) so the per-turn `git rev-parse / status / log` and CLAUDE.md re-read stop showing up in profiles. `cc_mcp/client.py` reader loops use `dict.pop()` instead of `in`+index so a late response after a timeout doesn't race the request side. `tool_registry._cache_key` adds `session_id` dimension so a `Read(/etc/...)` cached for one session never leaks to another. `session_store.search_sessions` LIKE-fallback path escapes `%`/`_`/`\` before interpolation. + **Smaller fixes folded in.** Web JWT secret loader rewritten with `O_CREAT \| O_EXCL` + 0o600 + post-write mode verification (refuses to read a world-readable secret file; auto-falls-back to in-memory secret if chmod can't be enforced; override with `CHEETAHCLAWS_WEB_SECRET`). Terminal one-time password from `secrets.token_urlsafe(6)[:6]` (~30 bits, online-bruteable) to `secrets.token_urlsafe(32)` (~190 bits). `config.save_config` strips `permission_mode=accept-all` before persisting — once-confirmed escape hatches no longer outlive the session that set them. `session_store.save_session` wrapped in a module-level `Lock` + explicit `BEGIN IMMEDIATE` / `ROLLBACK` so two threads writing the same `session_id` no longer silently drop one set of changes. `agent_runner.py` `err_msg` initialised before the try block (defends against a `NameError` on first iteration if `_handle_permission_request` returns `"error"`); `quota.QuotaExceeded` matched by `isinstance` instead of class-name string. `compaction.compact_messages` wraps `stream_auxiliary` in try/except + falls back to the original messages instead of crashing the agent loop. `providers._recover_args_from_text` caps the regex scan window to the last 32 KB of accumulated text (was scanning ~100 KB+ on every tool call). `context.get_git_info` + `get_claude_md` get TTL caches (30 s / 10 s, keyed by cwd) so the per-turn `git rev-parse / status / log` and CLAUDE.md re-read stop showing up in profiles. `mcp_client/client.py` reader loops use `dict.pop()` instead of `in`+index so a late response after a timeout doesn't race the request side. `tool_registry._cache_key` adds `session_id` dimension so a `Read(/etc/...)` cached for one session never leaks to another. `session_store.search_sessions` LIKE-fallback path escapes `%`/`_`/`\` before interpolation. **Frontend XSS audit.** Existing `_esc` (textContent-→-innerHTML) and `_renderMd` (HTML-tag-strip → marked) cover all user/model content paths. One deep-trust hole closed: `web/static/js/settings.js:_renderModels` previously injected server-supplied model names directly into an `onclick="app.selectModel('${full}')"` attribute — now uses `data-model` + a delegated click handler, so a malicious model registry entry cannot break out of the JS string literal. @@ -38,23 +38,23 @@ - May 12, 2026 (`daemon/f-4-followups-f-6-9` branch): **Daemon foundation roadmap finished — all nine F-1…F-9 items in RFC 0002 now LANDED.** Closes the remaining four scope items end-to-end (≈1500 LoC of code + ≈900 LoC of tests + docs). Drilldown: - **F-4 #2 — Bridge `notify` forwarding.** The subprocess-runner reader loop's `notify` IPC branch used to drop the payload on the floor (F-6/7/8 didn't exist yet). Now it routes through `cc_daemon.bridge_supervisor.notify(kind, text)`. The runner can target a specific bridge via `msg["bridge"]` (e.g. `"telegram"`) or omit it for a `"*"` broadcast. `agent_runner_notify` events on the bus carry `{name, run_id, bridge, delivered, text[:500]}` so observers can audit deliveries. Empty-text frames are silently dropped (common during agent shutdown). + **F-4 #2 — Bridge `notify` forwarding.** The subprocess-runner reader loop's `notify` IPC branch used to drop the payload on the floor (F-6/7/8 didn't exist yet). Now it routes through `daemon.bridge_supervisor.notify(kind, text)`. The runner can target a specific bridge via `msg["bridge"]` (e.g. `"telegram"`) or omit it for a `"*"` broadcast. `agent_runner_notify` events on the bus carry `{name, run_id, bridge, delivered, text[:500]}` so observers can audit deliveries. Empty-text frames are silently dropped (common during agent shutdown). **F-4 #3 — Restart policy.** New `RestartPolicy` dataclass: `mode` (`none` | `on-crash`), `max_restarts`, `backoff_base_s`, `backoff_cap_s`, `backoff_jitter_s`. Frozen + a pure `next_delay(restart_count)` so the decision matrix is unit-testable. `agent.start` accepts the five fields flat (validation rejects `cap < base` which would clamp every attempt down to a useless ceiling). On a crash the reader's `finally` arms a `threading.Timer(delay, _do_restart, ...)`; the Timer respawns via a swappable spawner hook (`_RESTART_SPAWNER` for tests) and carries `restart_count` forward. `stop()` cancels the Timer before the kill ladder, and the same `_unregister(name, expected=handle)` identity check protects against a Timer-fired respawn racing past a deliberate stop. Bus events: `agent_runner_restart_scheduled`, `agent_runner_restart`, `agent_runner_restart_failed`, `agent_runner_restart_exhausted`. - **F-6 / F-7 / F-8 Phase 1 — Telegram / Slack / WeChat in daemon.** Single `cc_daemon/bridge_supervisor.py` owns lifecycle for all three kinds, gated per-bridge by feature flags (`CHEETAHCLAWS_ENABLE_F6/7/8`, default off, REPL is byte-for-byte unchanged until the operator opts in). The Phase 1 worker invokes today's `bridges/.py:__supervisor` unchanged — same HTTP code, same reconnect/backoff, just owned by a daemon thread instead of a REPL one. Outbound `bridge.notify(kind, text)` dispatches via the per-kind sender (`_tg_send` / `_slack_send` / `_wx_send`); F-4 #2 plugs straight into it. Persistence in the F-2 `bridges` SQLite table (`kind`, `enabled`, `config_json` with secrets redacted, `last_poll_at`, `last_error`); `bridge.list` merges live workers with rows from previous daemon runs so disabled bridges remain visible in `daemon status`. Wire surface: `bridge.{start,stop,list,send,status}` RPCs in `cc_daemon/bridge_methods.py`. F-7 depends on F-6 (shared scaffolding); F-8 the same. WeChat keeps a clear-error path for missing token/base_url since the QR-login handshake is still REPL-driven (`/wechat login`). + **F-6 / F-7 / F-8 Phase 1 — Telegram / Slack / WeChat in daemon.** Single `daemon/bridge_supervisor.py` owns lifecycle for all three kinds, gated per-bridge by feature flags (`CHEETAHCLAWS_ENABLE_F6/7/8`, default off, REPL is byte-for-byte unchanged until the operator opts in). The Phase 1 worker invokes today's `bridges/.py:__supervisor` unchanged — same HTTP code, same reconnect/backoff, just owned by a daemon thread instead of a REPL one. Outbound `bridge.notify(kind, text)` dispatches via the per-kind sender (`_tg_send` / `_slack_send` / `_wx_send`); F-4 #2 plugs straight into it. Persistence in the F-2 `bridges` SQLite table (`kind`, `enabled`, `config_json` with secrets redacted, `last_poll_at`, `last_error`); `bridge.list` merges live workers with rows from previous daemon runs so disabled bridges remain visible in `daemon status`. Wire surface: `bridge.{start,stop,list,send,status}` RPCs in `daemon/bridge_methods.py`. F-7 depends on F-6 (shared scaffolding); F-8 the same. WeChat keeps a clear-error path for missing token/base_url since the QR-login handshake is still REPL-driven (`/wechat login`). - **F-6 Phase 2 — Inbound refactor.** When `bridge.start daemon_phase2=True` is passed, the legacy supervisor is bypassed for a slim daemon-driven loop: (a) outbound subscriber on the event bus, filters `session_outbound` events by `session_id` (`tg:` / `sl:` / `wc:`) + `target_bridges`, calls `handle.sender` for delivery; (b) per-kind inbound poller (`_phase2_telegram_inbound` / `_phase2_slack_inbound` / `_phase2_wechat_inbound`) that re-uses today's HTTP helpers but publishes `session_inbound` on every new phone message instead of calling `session_ctx.run_query`. The agent driver — REPL, Web, or a future automation client — subscribes to `session_inbound`, runs the agent, calls `session.reply(session_id, text, target_bridges?)` for outbound chunks. Three new RPCs in `cc_daemon/session_methods.py`: `session.send`, `session.reply`, `session.list_recent`. Permission requests born inside a bridge-driven turn route only back to the originating bridge via the existing `PermissionStore` originator stamp (`:`). + **F-6 Phase 2 — Inbound refactor.** When `bridge.start daemon_phase2=True` is passed, the legacy supervisor is bypassed for a slim daemon-driven loop: (a) outbound subscriber on the event bus, filters `session_outbound` events by `session_id` (`tg:` / `sl:` / `wc:`) + `target_bridges`, calls `handle.sender` for delivery; (b) per-kind inbound poller (`_phase2_telegram_inbound` / `_phase2_slack_inbound` / `_phase2_wechat_inbound`) that re-uses today's HTTP helpers but publishes `session_inbound` on every new phone message instead of calling `session_ctx.run_query`. The agent driver — REPL, Web, or a future automation client — subscribes to `session_inbound`, runs the agent, calls `session.reply(session_id, text, target_bridges?)` for outbound chunks. Three new RPCs in `daemon/session_methods.py`: `session.send`, `session.reply`, `session.list_recent`. Permission requests born inside a bridge-driven turn route only back to the originating bridge via the existing `PermissionStore` originator stamp (`:`). **F-9 — Cost-guardrail defaults + per-runner quota-pause.** Headless `cheetahclaws serve` now sets four conservative defaults (`session_token_budget=200_000`, `session_cost_budget=$2`, `daily_token_budget=2_000_000`, `daily_cost_budget=$20`) via `_apply_serve_defaults`; REPL `--in-process` keeps `None` (unlimited) for back-compat. New `system.status` RPC returns `{budgets, runners, bridges}` so `daemon status` prints the live ceilings. `agent.resume(budget_overrides, name?)` merges overrides into `daemon_state.config` and (when `name` is supplied) calls `runner_supervisor.resume(name)` to deliver a `resume` IPC frame to a paused runner. The hook itself: a new pre-iter `quota.check_quota` raises into `_on_quota_exceeded`; the base impl is a no-op (REPL keeps today's behaviour where `agent.run` catches internally and yields a quota text), while `_PipeAgentRunner` overrides it to ship a `paused_budget` IPC frame, set status, and block on `_resume_event.wait()`. Supervisor reader publishes `quota_warn` + flips `agent_runs.status='paused_budget'`. On resume, runner sends `resumed` IPC, supervisor publishes `agent_runner_resumed` + flips status back to `running`. Control loop's `stop` handler also sets `_resume_event` so a stop arriving while paused unblocks cleanly. **Post-implementation audit fixed 5 real bugs in the new code.** (1) `_phase2_wechat_inbound` used wrong field names (`messages` / `fromUserName` / `msgId` / `syncKey` instead of `msgs` / `from_user_id` / `message_id` / `get_updates_buf` per `bridges/wechat.py:411`). (2) `_phase2_slack_inbound` initialized cursor to `None`, so the first poll would replay the channel's recent backlog — fixed to seed at current wall-clock time (matches `bridges/slack.py:_slack_poll_loop`). (3) `_phase2_telegram_inbound` long-polled with `timeout=25 s`, meaning `stop()` had to wait up to 25 s for the HTTP call to return before observing `stop_event` — dropped to 5 s. (4) `_unregister(name)` was identity-blind; a Timer-fired `_do_restart` racing with `stop()` could see its freshly-spawned successor handle silently popped (orphaning the subprocess). Added an optional `expected=handle` identity check applied at every terminal stop site (runner_supervisor + bridge_supervisor have the symmetric fix). (5) `_safe_cfg` only matched `token` / `secret` keys; since `bridge.start` merges `daemon_state.config` into the bridge config, provider API keys (`anthropic_api_key`, etc.) and `password` / `auth_*` fields could bleed through to bridges SQLite rows and SSE events — extended to `(token, secret, api_key, apikey, password, passwd, auth)`. Two new regression tests pin both. - Full repo suite (three independent runs): **2347 passing, 3 skipped (env-gated live LiteLLM tests), 0 failed, ~3:32 each**. ~90 new daemon-specific tests across `test_cc_daemon_runner_{restart_policy,notify_routing,quota_pause}.py`, `test_cc_daemon_{bridge_supervisor,bridge_methods,bridge_phase2,session_methods,f9_budgets}.py`. RFC 0002 + `docs/architecture.md §Daemon` updated to reflect all of F-1 → F-9 landed. Details: [RFC 0002](RFC/0002-daemon-foundation-roadmap.md). + Full repo suite (three independent runs): **2347 passing, 3 skipped (env-gated live LiteLLM tests), 0 failed, ~3:32 each**. ~90 new daemon-specific tests across `test_daemon_runner_{restart_policy,notify_routing,quota_pause}.py`, `test_daemon_{bridge_supervisor,bridge_methods,bridge_phase2,session_methods,f9_budgets}.py`. RFC 0002 + `docs/architecture.md §Daemon` updated to reflect all of F-1 → F-9 landed. Details: [RFC 0002](RFC/0002-daemon-foundation-roadmap.md). -- May 12, 2026 (`fix/litellm-provider-followup` branch): **`litellm/` provider follow-up to PR #119 — make litellm a real optional dep, fix ledger / streaming, and wire it into the CLI / Web UI path.** PR #119 (RheagalFire) introduced `cc_kernel/runner/llm/litellm_provider.py` so CheetahClaws could route to 100+ LLM providers behind one SDK, but a careful re-review against the merge surfaced four classes of integration gap that the 12 mocked unit tests didn't catch. The follow-up branch (`fix/litellm-provider-followup`, 2 commits, 9 files, +1093/-229) fixes all of them and lands the docs the original PR was missing. **(1) Dependency classification — description said optional, diff put it in core.** Pyproject's `[project] dependencies` had grown a `litellm>=1.60.0,<2.0.0` line, and `requirements.txt`'s core block matched; every `pip install cheetahclaws` was force-pulling litellm and its transitive chain (`tokenizers`, `tiktoken`, pinned pydantic versions). Moved to `[project.optional-dependencies]` under a new `litellm` extra, also added to `all`; `requirements.txt` now only documents the optional install via a comment. Backed up by a `test_litellm_is_optional_dependency` regression. **(2) Not reachable through either user path.** `cc_kernel/runner/llm/__main__.py:_select_provider` only knew `mock` / `scripted` / `anthropic`, and the top-level `providers.PROVIDERS` registry (which the CLI + Web UI consult to resolve `--model `) had no `litellm` entry at all, so end-to-end the new class was reachable only by direct Python import. Added a `litellm` branch to `_select_provider` (reads `CC_LLM_API_KEY` as an optional explicit override), a `PROVIDERS["litellm"]` entry with `type: "litellm"`, and a new `stream_litellm()` generator in `providers.py` mirroring `stream_openai_compat`'s shape — yields `TextChunk` per delta then `AssistantTurn` at end. The dispatcher in `providers.stream()` branches on `prov["type"] == "litellm"`. `bare_model("litellm/openai/gpt-4o")` strips only the first `/`, leaving `openai/gpt-4o` — exactly what `litellm.completion(model=...)` expects. **(3) Streaming silently zeroed the ledger.** `stream()` returned `tokens_input=0`, `tokens_output=0`, `tool_calls=()`, `finish_reason="stop"` unconditionally. The kernel runner emits `charge` IPC messages from those fields and gates RFC 0022 tool dispatch on `response.is_tool_use`, so every streamed call bypassed quota and lost any tool_use the model emitted. Fix passes `stream_options={"include_usage": True}` to `litellm.completion` and reassembles the chunk list with `litellm.stream_chunk_builder(chunks, messages=...)` so the synthesized final response carries real token counts, tool_calls, and finish_reason. Two regression tests pin the contract (`test_stream_emits_deltas_and_returns_usage`, `test_stream_preserves_tool_calls`); a third (`test_cost_unknown_set_when_chunk_builder_fails`) covers the fallback when the builder returns None on very old litellm versions. **(4) `cost_micro` hard-coded to 0 — quota free pass.** Both `__call__` and `stream()` returned `cost_micro=0` regardless of model. Switched to `litellm.completion_cost(completion_response=resp, model=model)` which uses litellm's per-model price table (covers 100+ providers, kept in sync upstream); convert USD → micro-USD via the same `* 1_000_000` factor `AnthropicProvider` uses. On `completion_cost` raising (unknown model) or returning `None`, the response carries `metadata["cost_unknown"]=True` so the ledger can distinguish a real $0 (Ollama, free NIM tier) from an unpriced call. **Exception mapping.** `try: ... except Exception: raise ProviderUnavailable(...)` swallowed every error class into "their fault" — 401s, malformed requests and connection timeouts all looked the same to the runner. New `_map_exception` reads `self._litellm.exceptions.{AuthenticationError, BadRequestError, NotFoundError, UnsupportedParamsError}` and re-raises those as `ProviderInvalidRequest` ("your fault"); everything else stays `ProviderUnavailable` so the runner may retry. Reads exception classes off the already-imported `self._litellm` module (instead of `from litellm import exceptions`) so the mapper stays testable without a real SDK installed. **Lazy import.** Top-level `import litellm` violated the module-level contract in `cc_kernel/runner/llm/__init__.py` ("imported lazily so the absence of an SDK doesn't break this module's import") — every place that imported the runner's LLM package was implicitly importing litellm. Refactored to an `_ensure_litellm()` first-use pattern matching `AnthropicProvider._ensure_client`, with a `test_module_imports_without_litellm` that strongly verifies the property (the local dev env doesn't have litellm installed — the test passes). **Self-review caught 5 more bugs before pushing.** (a) `_parse_tool_calls` called `tc.function.name` outside the try block — a malformed `tool_call` with `function=None` would crash the whole response instead of the single bad call; fixed by `getattr` chain + `continue`-on-empty-name. (b) `json.loads("null")` and `json.loads("[1,2]")` return `None` / `list`, which trip `LlmResponse.__post_init__`'s `isinstance(tc["input"], dict)` validator; fixed by coercing non-dict to `{}`. (c) Same JSON-non-dict bug in `providers.stream_litellm`'s streaming tool-call assembly; same `isinstance` guard. (d) The streaming fallback (when `stream_chunk_builder` returns `None`) emitted `metadata={}` instead of `{"cost_unknown": True}`, breaking ledger consistency. (e) `tests/e2e_litellm_provider.py`'s fixture's `try/except ImportError` was dead code once the import was lazy — would confusingly fail on real assertions rather than `pytest.skip` if `CC_LITELLM_E2E=1` was set on a box without litellm. Replaced with an explicit `_ensure_litellm()` probe + `pytest.skip` on `ProviderUnavailable`. 6 new defensive tests pin all five fixes. **Tests.** 23 unit tests in `tests/test_litellm_provider.py` (was 12 mocked-only) — covers lazy import, registry wiring (both `_select_provider` and `providers.PROVIDERS`), cost computation with `cost_unknown` fallback, streaming usage + tool_calls preservation, exception class mapping (`AuthenticationError` → `ProviderInvalidRequest`), and 6 defensive tool-call parsing regressions. New `tests/e2e_litellm_provider.py` mirrors the 3 live-API tests the PR body claimed but never committed (basic call, streaming, system prompt steering); skipif-gated on `CC_LITELLM_E2E=1` AND per-provider credentials so CI / dev runs don't accidentally bill. **Full non-e2e suite: 2222 / 2222 passing, zero regressions** (up from 2154 baseline). **Docs.** New section in [`docs/guides/recipes.md`](guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider) under Section 1, between the vLLM/`custom/` walkthrough and Section 2 — covers Bedrock SigV4, Azure deployment routing, Vertex service-account JWTs with concrete env-var setup, plus a 5-row troubleshooting table mirroring the existing vLLM one (litellm not installed, `drop_params` masking, `cost_unknown` semantics, Bedrock 401 region mismatch, Azure 403 stale `api_version`). README gains a `pip install ".[litellm]"` line in Optional extras, three Supported Models table rows (Bedrock / Azure / Vertex via litellm), and a dedicated **LiteLLM (AWS Bedrock / Azure / Vertex AI)** subsection under Closed-Source API Models with concrete invocation examples and an explicit pointer toward `custom/` for plain OpenAI-shaped endpoints so users don't pull litellm when they don't need it. i18n READMEs (CN/JP/ES/DE/PT) intentionally left for the maintainer's translation cadence. **Branch:** `fix/litellm-provider-followup` (2 commits — `abc3357` code + tests + recipes, `f5f364d` README), open for review against `main`. +- May 12, 2026 (`fix/litellm-provider-followup` branch): **`litellm/` provider follow-up to PR #119 — make litellm a real optional dep, fix ledger / streaming, and wire it into the CLI / Web UI path.** PR #119 (RheagalFire) introduced `kernel/runner/llm/litellm_provider.py` so CheetahClaws could route to 100+ LLM providers behind one SDK, but a careful re-review against the merge surfaced four classes of integration gap that the 12 mocked unit tests didn't catch. The follow-up branch (`fix/litellm-provider-followup`, 2 commits, 9 files, +1093/-229) fixes all of them and lands the docs the original PR was missing. **(1) Dependency classification — description said optional, diff put it in core.** Pyproject's `[project] dependencies` had grown a `litellm>=1.60.0,<2.0.0` line, and `requirements.txt`'s core block matched; every `pip install cheetahclaws` was force-pulling litellm and its transitive chain (`tokenizers`, `tiktoken`, pinned pydantic versions). Moved to `[project.optional-dependencies]` under a new `litellm` extra, also added to `all`; `requirements.txt` now only documents the optional install via a comment. Backed up by a `test_litellm_is_optional_dependency` regression. **(2) Not reachable through either user path.** `kernel/runner/llm/__main__.py:_select_provider` only knew `mock` / `scripted` / `anthropic`, and the top-level `providers.PROVIDERS` registry (which the CLI + Web UI consult to resolve `--model `) had no `litellm` entry at all, so end-to-end the new class was reachable only by direct Python import. Added a `litellm` branch to `_select_provider` (reads `CC_LLM_API_KEY` as an optional explicit override), a `PROVIDERS["litellm"]` entry with `type: "litellm"`, and a new `stream_litellm()` generator in `providers.py` mirroring `stream_openai_compat`'s shape — yields `TextChunk` per delta then `AssistantTurn` at end. The dispatcher in `providers.stream()` branches on `prov["type"] == "litellm"`. `bare_model("litellm/openai/gpt-4o")` strips only the first `/`, leaving `openai/gpt-4o` — exactly what `litellm.completion(model=...)` expects. **(3) Streaming silently zeroed the ledger.** `stream()` returned `tokens_input=0`, `tokens_output=0`, `tool_calls=()`, `finish_reason="stop"` unconditionally. The kernel runner emits `charge` IPC messages from those fields and gates RFC 0022 tool dispatch on `response.is_tool_use`, so every streamed call bypassed quota and lost any tool_use the model emitted. Fix passes `stream_options={"include_usage": True}` to `litellm.completion` and reassembles the chunk list with `litellm.stream_chunk_builder(chunks, messages=...)` so the synthesized final response carries real token counts, tool_calls, and finish_reason. Two regression tests pin the contract (`test_stream_emits_deltas_and_returns_usage`, `test_stream_preserves_tool_calls`); a third (`test_cost_unknown_set_when_chunk_builder_fails`) covers the fallback when the builder returns None on very old litellm versions. **(4) `cost_micro` hard-coded to 0 — quota free pass.** Both `__call__` and `stream()` returned `cost_micro=0` regardless of model. Switched to `litellm.completion_cost(completion_response=resp, model=model)` which uses litellm's per-model price table (covers 100+ providers, kept in sync upstream); convert USD → micro-USD via the same `* 1_000_000` factor `AnthropicProvider` uses. On `completion_cost` raising (unknown model) or returning `None`, the response carries `metadata["cost_unknown"]=True` so the ledger can distinguish a real $0 (Ollama, free NIM tier) from an unpriced call. **Exception mapping.** `try: ... except Exception: raise ProviderUnavailable(...)` swallowed every error class into "their fault" — 401s, malformed requests and connection timeouts all looked the same to the runner. New `_map_exception` reads `self._litellm.exceptions.{AuthenticationError, BadRequestError, NotFoundError, UnsupportedParamsError}` and re-raises those as `ProviderInvalidRequest` ("your fault"); everything else stays `ProviderUnavailable` so the runner may retry. Reads exception classes off the already-imported `self._litellm` module (instead of `from litellm import exceptions`) so the mapper stays testable without a real SDK installed. **Lazy import.** Top-level `import litellm` violated the module-level contract in `kernel/runner/llm/__init__.py` ("imported lazily so the absence of an SDK doesn't break this module's import") — every place that imported the runner's LLM package was implicitly importing litellm. Refactored to an `_ensure_litellm()` first-use pattern matching `AnthropicProvider._ensure_client`, with a `test_module_imports_without_litellm` that strongly verifies the property (the local dev env doesn't have litellm installed — the test passes). **Self-review caught 5 more bugs before pushing.** (a) `_parse_tool_calls` called `tc.function.name` outside the try block — a malformed `tool_call` with `function=None` would crash the whole response instead of the single bad call; fixed by `getattr` chain + `continue`-on-empty-name. (b) `json.loads("null")` and `json.loads("[1,2]")` return `None` / `list`, which trip `LlmResponse.__post_init__`'s `isinstance(tc["input"], dict)` validator; fixed by coercing non-dict to `{}`. (c) Same JSON-non-dict bug in `providers.stream_litellm`'s streaming tool-call assembly; same `isinstance` guard. (d) The streaming fallback (when `stream_chunk_builder` returns `None`) emitted `metadata={}` instead of `{"cost_unknown": True}`, breaking ledger consistency. (e) `tests/e2e_litellm_provider.py`'s fixture's `try/except ImportError` was dead code once the import was lazy — would confusingly fail on real assertions rather than `pytest.skip` if `CC_LITELLM_E2E=1` was set on a box without litellm. Replaced with an explicit `_ensure_litellm()` probe + `pytest.skip` on `ProviderUnavailable`. 6 new defensive tests pin all five fixes. **Tests.** 23 unit tests in `tests/test_litellm_provider.py` (was 12 mocked-only) — covers lazy import, registry wiring (both `_select_provider` and `providers.PROVIDERS`), cost computation with `cost_unknown` fallback, streaming usage + tool_calls preservation, exception class mapping (`AuthenticationError` → `ProviderInvalidRequest`), and 6 defensive tool-call parsing regressions. New `tests/e2e_litellm_provider.py` mirrors the 3 live-API tests the PR body claimed but never committed (basic call, streaming, system prompt steering); skipif-gated on `CC_LITELLM_E2E=1` AND per-provider credentials so CI / dev runs don't accidentally bill. **Full non-e2e suite: 2222 / 2222 passing, zero regressions** (up from 2154 baseline). **Docs.** New section in [`docs/guides/recipes.md`](guides/recipes.md#alternative-cloud-providers-with-non-trivial-auth-via-the-litellm-provider) under Section 1, between the vLLM/`custom/` walkthrough and Section 2 — covers Bedrock SigV4, Azure deployment routing, Vertex service-account JWTs with concrete env-var setup, plus a 5-row troubleshooting table mirroring the existing vLLM one (litellm not installed, `drop_params` masking, `cost_unknown` semantics, Bedrock 401 region mismatch, Azure 403 stale `api_version`). README gains a `pip install ".[litellm]"` line in Optional extras, three Supported Models table rows (Bedrock / Azure / Vertex via litellm), and a dedicated **LiteLLM (AWS Bedrock / Azure / Vertex AI)** subsection under Closed-Source API Models with concrete invocation examples and an explicit pointer toward `custom/` for plain OpenAI-shaped endpoints so users don't pull litellm when they don't need it. i18n READMEs (CN/JP/ES/DE/PT) intentionally left for the maintainer's translation cadence. **Branch:** `fix/litellm-provider-followup` (2 commits — `abc3357` code + tests + recipes, `f5f364d` README), open for review against `main`. -- May 11, 2026 (`daemon/f-4` branch): **F-4 skeleton — `agent_runner` becomes a supervised subprocess (RFC 0002).** The fourth piece of the daemon foundation roadmap lands as a feature-flagged skeleton on the `daemon/f-4` branch. Today each `/agent