{
  "versions": [
    {
      "id": "s01",
      "filename": "s01_agent_loop/code.py",
      "title": "The Agent Loop",
      "subtitle": "One Loop Is All You Need",
      "loc": 102,
      "tools": [
        "bash"
      ],
      "newTools": [
        "bash"
      ],
      "coreAddition": "Minimal model/tool loop",
      "keyInsight": "The smallest useful agent is a loop that calls the model, runs tools, and feeds results back.",
      "classes": [],
      "functions": [
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 69
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 85
        }
      ],
      "layer": "tools",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns01_agent_loop.py - The Agent Loop\n\nThe entire secret of an AI coding agent in one pattern:\n\n    while stop_reason == \"tool_use\":\n        response = LLM(messages, tools)\n        execute tools\n        append results\n\n    +----------+      +-------+      +---------+\n    |   User   | ---> |  LLM  | ---> |  Tool   |\n    |  prompt  |      |       |      | execute |\n    +----------+      +---+---+      +----+----+\n                          ^               |\n                          |   tool_result |\n                          +---------------+\n                          (loop continues)\n\nThis is the core loop: feed tool results back to the model\nuntil the model decides to stop. Production agents layer\npolicy, hooks, and lifecycle controls on top.\n\nUsage:\n    pip install anthropic python-dotenv\n    ANTHROPIC_API_KEY=... python s01_agent_loop/code.py\n\"\"\"\n\nimport os\nimport subprocess\n\ntry:\n    import readline\n    # macOS 的 libedit 在处理中文输入时有退格问题，这四行修复它\n    readline.parse_and_bind('set bind-tty-special-chars off')\n    readline.parse_and_bind('set input-meta on')\n    readline.parse_and_bind('set output-meta on')\n    readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\n\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain.\"\n\n# ── Tool definition: just bash ────────────────────────────\nTOOLS = [{\n    \"name\": \"bash\",\n    \"description\": \"Run a shell command.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\"command\": {\"type\": \"string\"}},\n        \"required\": [\"command\"],\n    },\n}]\n\n\n# ── Tool execution ────────────────────────────────────────\ndef run_bash(command: str) -> str:\n    dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n    if any(d in command for d in dangerous):\n        return \"Error: Dangerous command blocked\"\n    try:\n        r = subprocess.run(command, shell=True, cwd=os.getcwd(),\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n    except (FileNotFoundError, OSError) as e:\n        return f\"Error: {e}\"\n\n\n# ── The core pattern: a while loop that calls tools until the model stops ──\ndef agent_loop(messages: list):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=TOOLS, max_tokens=8000,\n        )\n\n        # Append assistant turn\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        # If the model didn't call a tool, we're done\n        if response.stop_reason != \"tool_use\":\n            return\n\n        # Execute each tool call, collect results\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                print(f\"\\033[33m$ {block.input['command']}\\033[0m\")\n                output = run_bash(block.input[\"command\"])\n                print(output[:200])\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output,\n                })\n\n        # Feed tool results back, loop continues\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\n# ── Entry point ──────────────────────────────────────────\nif __name__ == \"__main__\":\n    print(\"s01: Agent Loop\")\n    print(\"输入问题，回车发送。输入 q 退出。\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms01 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        # Print the model's final text response\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if getattr(block, \"type\", None) == \"text\":\n                    print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s01_agent_loop/agent-loop.svg",
          "alt": "agent loop"
        }
      ]
    },
    {
      "id": "s02",
      "filename": "s02_tool_use/code.py",
      "title": "Tool Use",
      "subtitle": "Add a Tool, Add Just One Line",
      "loc": 135,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob"
      ],
      "newTools": [
        "read_file",
        "write_file",
        "edit_file",
        "glob"
      ],
      "coreAddition": "Tool dispatch map",
      "keyInsight": "The loop stays stable while capabilities register into a dispatch table.",
      "classes": [],
      "functions": [
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 46
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 66
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 73
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 83
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 93
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 105
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 150
        }
      ],
      "layer": "tools",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns02: Tool Use — 在 s01 基础上新增 4 个工具 + 分发映射。\n\n运行: python s02_tool_use/code.py\n需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY\n\n本文件 = s01 的全部代码 + 以下新增:\n  + run_read / run_write / run_edit / run_glob 四个工具实现\n  + TOOL_HANDLERS 分发映射（替代 s01 中硬编码的 run_bash 调用）\n  + safe_path 路径安全校验\n\n循环本身（agent_loop）与 s01 完全一致。\n\"\"\"\n\nimport os, subprocess\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\n    readline.parse_and_bind('set input-meta on')\n    readline.parse_and_bind('set output-meta on')\n    readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s01 (unchanged)\n# ═══════════════════════════════════════════════════════════\n\ndef run_bash(command: str) -> str:\n    dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n    if any(d in command for d in dangerous):\n        return \"Error: Dangerous command blocked\"\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True,\n                           encoding=\"utf-8\", errors=\"replace\", timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n    except (FileNotFoundError, OSError) as e:\n        return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s02: 4 个新工具\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text:\n            return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s02: 工具定义（s01 只有一个 bash，现在扩展到 5 个）\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s02: 工具分发映射（s01 是硬编码 run_bash，现在改为查表）\n# ═══════════════════════════════════════════════════════════\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — 与 s01 结构完全一致，只改了工具执行那部分\n#  s01: output = run_bash(block.input[\"command\"])\n#  s02: output = TOOL_HANDLERS[block.name](**block.input)\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                print(f\"\\033[33m> {block.name}\\033[0m\")\n                handler = TOOL_HANDLERS.get(block.name)\n                output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n                print(str(output)[:200])\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(\"s02: Tool Use — 在 s01 基础上加了 4 个工具\")\n    print(\"输入问题，回车发送。输入 q 退出。\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms02 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s02_tool_use/concurrency-comparison.svg",
          "alt": "concurrency comparison"
        },
        {
          "src": "/course-assets/s02_tool_use/tool-dispatch.svg",
          "alt": "tool dispatch"
        }
      ]
    },
    {
      "id": "s03",
      "filename": "s03_permission/code.py",
      "title": "Permission",
      "subtitle": "Check Permissions Before Execution",
      "loc": 180,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob"
      ],
      "newTools": [],
      "coreAddition": "Permission gate",
      "keyInsight": "Dangerous actions need a harness decision point before the shell runs.",
      "classes": [],
      "functions": [
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 60
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 67
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 77
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 87
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 97
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 109
        },
        {
          "name": "check_deny_list",
          "signature": "def check_deny_list(command: str)",
          "startLine": 151
        },
        {
          "name": "check_rules",
          "signature": "def check_rules(tool_name: str, args: dict)",
          "startLine": 168
        },
        {
          "name": "ask_user",
          "signature": "def ask_user(tool_name: str, args: dict, reason: str)",
          "startLine": 176
        },
        {
          "name": "check_permission",
          "signature": "def check_permission(block)",
          "startLine": 184
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 202
        }
      ],
      "layer": "tools",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns03_permission.py - Permission System\n\nThree gates inserted before tool execution:\n\n    Gate 1: Hard deny list (rm -rf /, sudo, ...)\n    Gate 2: Rule matching (write outside workspace? destructive cmd?)\n    Gate 3: User approval (pause and wait for confirmation)\n\n    +-------+    +--------+    +--------+    +--------+    +------+\n    | Tool  | -> | Gate 1 | -> | Gate 2 | -> | Gate 3 | -> | Exec |\n    | call  |    | deny?  |    | match? |    | allow? |    |      |\n    +-------+    +--------+    +--------+    +--------+    +------+\n         |            |             |             |\n         v            v             v             v\n      (normal)     (blocked)    (ask user)   (user says no?)\n\nOnly one line added to the agent loop:\n\n    if not check_permission(block):\n        continue\n\nBuilds on s02 (multi-tool). Usage:\n\n    python s03_permission/code.py\n    Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env\n\"\"\"\n\nimport os, subprocess\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\n    readline.parse_and_bind('set input-meta on')\n    readline.parse_and_bind('set output-meta on')\n    readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. All destructive operations require user approval.\"\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02 (unchanged): Tool Implementations\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text:\n            return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02 (unchanged): Tool Definitions & Dispatch\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s03: Three-Gate Permission Pipeline\n# ═══════════════════════════════════════════════════════════\n\n# Gate 1: Hard deny list — always forbidden\nDENY_LIST = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"mkfs\", \"dd if=\", \"> /dev/sda\"]\n\ndef check_deny_list(command: str) -> str | None:\n    for pattern in DENY_LIST:\n        if pattern in command:\n            return f\"Blocked: '{pattern}' is on the deny list\"\n    return None\n\n\n# Gate 2: Rule matching — context-dependent checks\nPERMISSION_RULES = [\n    {\"tools\": [\"write_file\", \"edit_file\"],\n     \"check\": lambda args: not (WORKDIR / args.get(\"path\", \"\")).resolve().is_relative_to(WORKDIR),\n     \"message\": \"Writing outside workspace\"},\n    {\"tools\": [\"bash\"],\n     \"check\": lambda args: any(kw in args.get(\"command\", \"\") for kw in [\"rm \", \"> /etc/\", \"chmod 777\"]),\n     \"message\": \"Potentially destructive command\"},\n]\n\ndef check_rules(tool_name: str, args: dict) -> str | None:\n    for rule in PERMISSION_RULES:\n        if tool_name in rule[\"tools\"] and rule[\"check\"](args):\n            return rule[\"message\"]\n    return None\n\n\n# Gate 3: User approval — wait for confirmation after rule match\ndef ask_user(tool_name: str, args: dict, reason: str) -> str:\n    print(f\"\\n\\033[33m⚠  {reason}\\033[0m\")\n    print(f\"   Tool: {tool_name}({args})\")\n    choice = input(\"   Allow? [y/N] \").strip().lower()\n    return \"allow\" if choice in (\"y\", \"yes\") else \"deny\"\n\n\n# Pipeline: all three gates chained\ndef check_permission(block) -> bool:\n    if block.name == \"bash\":\n        reason = check_deny_list(block.input.get(\"command\", \"\"))\n        if reason:\n            print(f\"\\n\\033[31m⛔ {reason}\\033[0m\")\n            return False\n    reason = check_rules(block.name, block.input)\n    if reason:\n        decision = ask_user(block.name, block.input, reason)\n        if decision == \"deny\":\n            return False\n    return True\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — same as s02, with check_permission() inserted\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n\n            # s03 change: run through permission pipeline before executing\n            if not check_permission(block):\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": \"Permission denied.\"})\n                continue\n\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n            print(str(output)[:200])\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(\"s03: Permission\")\n    print(\"输入问题，回车发送。输入 q 退出。\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms03 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s03_permission/permission-overview.svg",
          "alt": "permission overview"
        },
        {
          "src": "/course-assets/s03_permission/permission-pipeline.svg",
          "alt": "permission pipeline"
        }
      ]
    },
    {
      "id": "s04",
      "filename": "s04_hooks/code.py",
      "title": "Hooks",
      "subtitle": "Hang on the Loop, Don't Write into It",
      "loc": 232,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob"
      ],
      "newTools": [],
      "coreAddition": "Lifecycle hooks",
      "keyInsight": "Cross-cutting behavior belongs around the loop, not tangled inside it.",
      "classes": [],
      "functions": [
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 81
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 87
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 96
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 105
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 114
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 125
        },
        {
          "name": "register_hook",
          "signature": "def register_hook(event: str, callback)",
          "startLine": 161
        },
        {
          "name": "trigger_hooks",
          "signature": "def trigger_hooks(event: str, *args)",
          "startLine": 164
        },
        {
          "name": "permission_hook",
          "signature": "def permission_hook(block)",
          "startLine": 176
        },
        {
          "name": "log_hook",
          "signature": "def log_hook(block)",
          "startLine": 200
        },
        {
          "name": "large_output_hook",
          "signature": "def large_output_hook(block, output)",
          "startLine": 206
        },
        {
          "name": "context_inject_hook",
          "signature": "def context_inject_hook(query: str)",
          "startLine": 213
        },
        {
          "name": "summary_hook",
          "signature": "def summary_hook(messages: list)",
          "startLine": 218
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 238
        }
      ],
      "layer": "tools",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns04: Hooks — move extension logic out of the loop, onto hooks.\n\n  User types query\n       │\n       ▼\n  ┌──────────────────┐\n  │ UserPromptSubmit │ ── trigger_hooks() before LLM\n  └────────┬─────────┘\n           ▼\n  ┌────────────┐     ┌─────────────────────────────┐\n  │  messages  │────▶│  LLM (stop_reason=tool_use?)│\n  └────────────┘     │   No ──▶ Stop hooks ──▶ exit │\n                     │   Yes ──▶ tool_use block ──┐ │\n                     └────────────────────────────┘ │\n                                                    ▼\n                                          ┌──────────────────┐\n                                          │ trigger_hooks()   │\n                                          │  PreToolUse:      │\n                                          │   permission_hook │\n                                          │   log_hook        │\n                                          └───────┬──────────┘\n                                                  │ (not blocked)\n                                          ┌───────▼──────────┐\n                                          │ TOOL_HANDLERS[x]  │\n                                          └───────┬──────────┘\n                                                  │\n                                          ┌───────▼──────────┐\n                                          │ trigger_hooks()   │\n                                          │  PostToolUse:     │\n                                          │   large_output    │\n                                          └───────┬──────────┘\n                                                  │\n                                          results ──▶ back to messages\n\nChanges from s03:\n  + HOOKS registry (event -> list of callbacks)\n  + register_hook() / trigger_hooks()\n  + context_inject_hook (UserPromptSubmit)\n  + permission_hook, log_hook (PreToolUse)\n  + large_output_hook (PostToolUse)\n  + summary_hook (Stop)\n  - check_permission() removed from loop body\n    (logic moved into permission_hook, triggered via PreToolUse)\n\nRun: python s04_hooks/code.py\nNeeds: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env\n\"\"\"\n\nimport os, subprocess\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\n    readline.parse_and_bind('set input-meta on')\n    readline.parse_and_bind('set output-meta on')\n    readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02-s03 (unchanged): Tool Implementations\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text:\n            return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s04: Hook System (s03 permission logic now via hooks)\n# ═══════════════════════════════════════════════════════════\n\nHOOKS = {\"UserPromptSubmit\": [], \"PreToolUse\": [], \"PostToolUse\": [], \"Stop\": []}\n\ndef register_hook(event: str, callback):\n    HOOKS[event].append(callback)\n\ndef trigger_hooks(event: str, *args):\n    for callback in HOOKS[event]:\n        result = callback(*args)\n        if result is not None:  # teaching shortcut: block this tool call\n            return result\n    return None\n\n\n# s03 permission check logic, now wrapped as a hook\nDENY_LIST = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"mkfs\", \"dd if=\"]\nDESTRUCTIVE = [\"rm \", \"> /etc/\", \"chmod 777\"]\n\ndef permission_hook(block):\n    \"\"\"PreToolUse: s03 check_permission() logic moved here.\"\"\"\n    if block.name == \"bash\":\n        for pattern in DENY_LIST:\n            if pattern in block.input.get(\"command\", \"\"):\n                print(f\"\\n\\033[31m⛔ Blocked: '{pattern}'\\033[0m\")\n                return \"Permission denied by deny list\"\n        for kw in DESTRUCTIVE:\n            if kw in block.input.get(\"command\", \"\"):\n                print(f\"\\n\\033[33m⚠  Potentially destructive command\\033[0m\")\n                print(f\"   Tool: {block.name}({block.input})\")\n                choice = input(\"   Allow? [y/N] \").strip().lower()\n                if choice not in (\"y\", \"yes\"):\n                    return \"Permission denied by user\"\n    if block.name in (\"write_file\", \"edit_file\"):\n        path = block.input.get(\"path\", \"\")\n        if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):\n            print(f\"\\n\\033[33m⚠  Writing outside workspace\\033[0m\")\n            print(f\"   Tool: {block.name}({block.input})\")\n            choice = input(\"   Allow? [y/N] \").strip().lower()\n            if choice not in (\"y\", \"yes\"):\n                return \"Permission denied by user\"\n    return None\n\ndef log_hook(block):\n    \"\"\"PreToolUse: log every tool call.\"\"\"\n    args_preview = str(list(block.input.values())[:2])[:60]\n    print(f\"\\033[90m[HOOK] {block.name}({args_preview})\\033[0m\")\n    return None\n\ndef large_output_hook(block, output):\n    \"\"\"PostToolUse: warn on large output.\"\"\"\n    if len(str(output)) > 100000:\n        print(f\"\\033[33m[HOOK] ⚠ Large output from {block.name}: {len(str(output))} chars\\033[0m\")\n    return None\n\n# UserPromptSubmit hook: log user input before it reaches the LLM\ndef context_inject_hook(query: str):\n    print(f\"\\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\\033[0m\")\n    return None\n\n# Stop hook: print summary when loop is about to exit\ndef summary_hook(messages: list):\n    tool_count = sum(1 for m in messages\n                     for b in (m.get(\"content\") if isinstance(m.get(\"content\"), list) else [])\n                     if isinstance(b, dict) and b.get(\"type\") == \"tool_result\")\n    print(f\"\\033[90m[HOOK] Stop: session used {tool_count} tool calls\\033[0m\")\n    return None\n\nregister_hook(\"UserPromptSubmit\", context_inject_hook)\nregister_hook(\"PreToolUse\", permission_hook)\nregister_hook(\"PreToolUse\", log_hook)\nregister_hook(\"PostToolUse\", large_output_hook)\nregister_hook(\"Stop\", summary_hook)\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — same structure as s03, but no hard-coded check\n#  s03: if not check_permission(block): ...\n#  s04: if trigger_hooks(\"PreToolUse\", block): ...\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            force = trigger_hooks(\"Stop\", messages)\n            if force:\n                messages.append({\"role\": \"user\", \"content\": force})\n                continue\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n\n            # s04 change: hook replaces hard-coded check_permission()\n            blocked = trigger_hooks(\"PreToolUse\", block)\n            if blocked:\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": str(blocked)})\n                continue\n\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n\n            trigger_hooks(\"PostToolUse\", block, output)  # s04: post hook\n\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(\"s04: Hooks — extension logic on hooks, loop stays clean\")\n    print(\"Type a question, press Enter. Type q to quit.\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms04 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        trigger_hooks(\"UserPromptSubmit\", query)\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s04_hooks/hooks-overview.svg",
          "alt": "hooks overview"
        }
      ]
    },
    {
      "id": "s05",
      "filename": "s05_todo_write/code.py",
      "title": "TodoWrite",
      "subtitle": "An Agent Without a Plan Drifts Off Course",
      "loc": 236,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob",
        "todo_write"
      ],
      "newTools": [
        "todo_write"
      ],
      "coreAddition": "Todo manager",
      "keyInsight": "Explicit plans keep long-running work visible and correctable.",
      "classes": [],
      "functions": [
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 64
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 70
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 79
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 88
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 97
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 108
        },
        {
          "name": "_normalize_todos",
          "signature": "def _normalize_todos(todos)",
          "startLine": 124
        },
        {
          "name": "run_todo_write",
          "signature": "def run_todo_write(todos: list)",
          "startLine": 144
        },
        {
          "name": "register_hook",
          "signature": "def register_hook(event: str, callback)",
          "startLine": 185
        },
        {
          "name": "trigger_hooks",
          "signature": "def trigger_hooks(event: str, *args)",
          "startLine": 188
        },
        {
          "name": "permission_hook",
          "signature": "def permission_hook(block)",
          "startLine": 198
        },
        {
          "name": "log_hook",
          "signature": "def log_hook(block)",
          "startLine": 207
        },
        {
          "name": "context_inject_hook",
          "signature": "def context_inject_hook(query: str)",
          "startLine": 212
        },
        {
          "name": "summary_hook",
          "signature": "def summary_hook(messages: list)",
          "startLine": 217
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 237
        }
      ],
      "layer": "planning",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns05: TodoWrite — add a planning tool on top of s04 hooks.\n\n  +---------+      +-------+      +------------------+\n  |  User   | ---> |  LLM  | ---> | TOOL_HANDLERS    |\n  | prompt  |      |       |      |  bash            |\n  +---------+      +---+---+      |  read_file       |\n                        ^         |  write_file      |\n                        | result  |  edit_file       |\n                        +---------+  glob            |\n                                      todo_write ← NEW\n                                   +------------------+\n                                        |\n                         in-memory current_todos\n                                        |\n                        if rounds_since_todo >= 3:\n                          inject <reminder>\n\nChanges from s04:\n  + todo_write tool + run_todo_write() implementation\n  + Nag reminder (inject reminder after 3 rounds without todo update)\n  + SYSTEM prompt includes \"plan before execute\" guidance\n  + rounds_since_todo counter in agent_loop\n  Loop unchanged: new tool auto-dispatches via TOOL_HANDLERS.\n\nRun: python s05_todo_write/code.py\nNeeds: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env\n\"\"\"\n\nimport ast, json, os, subprocess\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\nCURRENT_TODOS: list[dict] = []\n\n# s05 change: SYSTEM prompt adds planning guidance\nSYSTEM = (\n    f\"You are a coding agent at {WORKDIR}. \"\n    \"Before starting any multi-step task, use todo_write to plan your steps. \"\n    \"Update status as you go.\"\n)\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02-s04 (unchanged): Tool Implementations\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text:\n            return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s05: todo_write tool — plan only, no execution\n# ═══════════════════════════════════════════════════════════\n\ndef _normalize_todos(todos):\n    if isinstance(todos, str):\n        try:\n            todos = json.loads(todos)\n        except json.JSONDecodeError:\n            try:\n                todos = ast.literal_eval(todos)\n            except (SyntaxError, ValueError):\n                return None, \"Error: todos must be a list or JSON array string\"\n    if not isinstance(todos, list):\n        return None, \"Error: todos must be a list\"\n    for i, t in enumerate(todos):\n        if not isinstance(t, dict):\n            return None, f\"Error: todos[{i}] must be an object\"\n        if \"content\" not in t or \"status\" not in t:\n            return None, f\"Error: todos[{i}] missing 'content' or 'status'\"\n        if t[\"status\"] not in (\"pending\", \"in_progress\", \"completed\"):\n            return None, f\"Error: todos[{i}] has invalid status '{t['status']}'\"\n    return todos, None\n\ndef run_todo_write(todos: list) -> str:\n    global CURRENT_TODOS\n    todos, error = _normalize_todos(todos)\n    if error:\n        return error\n    CURRENT_TODOS = todos\n    lines = [\"\\n\\033[33m## Current Tasks\\033[0m\"]\n    for t in CURRENT_TODOS:\n        icon = {\"pending\": \" \", \"in_progress\": \"\\033[36m▸\\033[0m\", \"completed\": \"\\033[32m✓\\033[0m\"}[t[\"status\"]]\n        lines.append(f\"  [{icon}] {t['content']}\")\n    print(\"\\n\".join(lines))\n    return f\"Updated {len(CURRENT_TODOS)} tasks\"\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n    # s05: new tool\n    {\"name\": \"todo_write\", \"description\": \"Create and manage a task list for your current coding session.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"todos\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}}, \"required\": [\"content\", \"status\"]}}}, \"required\": [\"todos\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob, \"todo_write\": run_todo_write,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s04 (unchanged): Hook System\n# ═══════════════════════════════════════════════════════════\n\nHOOKS = {\"UserPromptSubmit\": [], \"PreToolUse\": [], \"PostToolUse\": [], \"Stop\": []}\n\ndef register_hook(event: str, callback):\n    HOOKS[event].append(callback)\n\ndef trigger_hooks(event: str, *args):\n    for callback in HOOKS[event]:\n        result = callback(*args)\n        if result is not None:\n            return result\n    return None\n\n# s04 hooks preserved\nDENY_LIST = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"mkfs\", \"dd if=\"]\n\ndef permission_hook(block):\n    \"\"\"PreToolUse: deny list check.\"\"\"\n    if block.name == \"bash\":\n        for p in DENY_LIST:\n            if p in block.input.get(\"command\", \"\"):\n                print(f\"\\n\\033[31m⛔ Blocked: '{p}'\\033[0m\")\n                return \"Permission denied\"\n    return None\n\ndef log_hook(block):\n    \"\"\"PreToolUse: log tool calls.\"\"\"\n    print(f\"\\033[90m[HOOK] {block.name}\\033[0m\")\n    return None\n\ndef context_inject_hook(query: str):\n    \"\"\"UserPromptSubmit: log working directory.\"\"\"\n    print(f\"\\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\\033[0m\")\n    return None\n\ndef summary_hook(messages: list):\n    \"\"\"Stop: print tool call count.\"\"\"\n    tool_count = sum(1 for m in messages\n                     for b in (m.get(\"content\") if isinstance(m.get(\"content\"), list) else [])\n                     if isinstance(b, dict) and b.get(\"type\") == \"tool_result\")\n    print(f\"\\033[90m[HOOK] Stop: session used {tool_count} tool calls\\033[0m\")\n    return None\n\nregister_hook(\"UserPromptSubmit\", context_inject_hook)\nregister_hook(\"PreToolUse\", permission_hook)\nregister_hook(\"PreToolUse\", log_hook)\nregister_hook(\"Stop\", summary_hook)\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — same as s04 + nag reminder counter\n# ═══════════════════════════════════════════════════════════\n\nrounds_since_todo = 0\n\ndef agent_loop(messages: list):\n    global rounds_since_todo\n    while True:\n        # s05: nag reminder — inject if model hasn't updated todos for 3 rounds\n        if rounds_since_todo >= 3 and messages:\n            messages.append({\"role\": \"user\",\n                             \"content\": \"<reminder>Update your todos.</reminder>\"})\n            rounds_since_todo = 0\n\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            force = trigger_hooks(\"Stop\", messages)\n            if force:\n                messages.append({\"role\": \"user\", \"content\": force})\n                continue\n            return\n\n        rounds_since_todo += 1\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n\n            blocked = trigger_hooks(\"PreToolUse\", block)\n            if blocked:\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": str(blocked)})\n                continue\n\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n\n            trigger_hooks(\"PostToolUse\", block, output)\n\n            # s05: reset nag counter when todo_write is called\n            if block.name == \"todo_write\":\n                rounds_since_todo = 0\n\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                            \"content\": output})\n\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(\"s05: TodoWrite — plan before execute, nag if you forget\")\n    print(\"Type a question, press Enter. Type q to quit.\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms05 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        trigger_hooks(\"UserPromptSubmit\", query)\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s05_todo_write/todo-overview.svg",
          "alt": "todo overview"
        }
      ]
    },
    {
      "id": "s06",
      "filename": "s06_subagent/code.py",
      "title": "Subagent",
      "subtitle": "Break Large Tasks into Small Ones with Clean Context",
      "loc": 304,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob",
        "todo_write",
        "task"
      ],
      "newTools": [
        "task"
      ],
      "coreAddition": "Isolated subtask context",
      "keyInsight": "Subagents give each subtask a clean message history while preserving the main thread.",
      "classes": [],
      "functions": [
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 69
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 75
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 84
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 93
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 102
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 113
        },
        {
          "name": "_normalize_todos",
          "signature": "def _normalize_todos(todos)",
          "startLine": 124
        },
        {
          "name": "run_todo_write",
          "signature": "def run_todo_write(todos: list)",
          "startLine": 144
        },
        {
          "name": "extract_text",
          "signature": "def extract_text(content)",
          "startLine": 201
        },
        {
          "name": "spawn_subagent",
          "signature": "def spawn_subagent(description: str)",
          "startLine": 207
        },
        {
          "name": "register_hook",
          "signature": "def register_hook(event: str, callback)",
          "startLine": 266
        },
        {
          "name": "trigger_hooks",
          "signature": "def trigger_hooks(event: str, *args)",
          "startLine": 269
        },
        {
          "name": "permission_hook",
          "signature": "def permission_hook(block)",
          "startLine": 278
        },
        {
          "name": "log_hook",
          "signature": "def log_hook(block)",
          "startLine": 287
        },
        {
          "name": "context_inject_hook",
          "signature": "def context_inject_hook(query: str)",
          "startLine": 292
        },
        {
          "name": "summary_hook",
          "signature": "def summary_hook(messages: list)",
          "startLine": 297
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 317
        }
      ],
      "layer": "planning",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns06: Subagent — spawn sub-agents with fresh messages[] for context isolation.\n\n  Parent Agent                           Subagent\n  +------------------+                  +------------------+\n  | messages=[...]   |                  | messages=[task]  | <-- fresh\n  |                  |   dispatch       |                  |\n  | tool: task       | ---------------> | own while loop   |\n  |   prompt=\"...\"   |                  |   bash/read/...  |\n  |                  |   summary only   |   (max 30 turns) |\n  | result = \"...\"   | <--------------- | return last text |\n  +------------------+                  +------------------+\n        ^                                      |\n        |       intermediate results DISCARDED  |\n        +--------------------------------------+\n\n  Subagent tools: bash, read, write, edit, glob (NO task — no recursion)\n\nChanges from s05:\n  + task tool + spawn_subagent() with fresh messages[]\n  + Safety limit: max 30 turns per subagent\n  + extract_text() helper\n  Subagent cannot spawn sub-subagents (no task tool in sub_tools).\n  Main loop unchanged: task auto-dispatches via TOOL_HANDLERS.\n\nRun: python s06_subagent/code.py\nNeeds: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env\n\"\"\"\n\nimport ast, json, os, subprocess\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\nCURRENT_TODOS: list[dict] = []\n\nSYSTEM = (\n    f\"You are a coding agent at {WORKDIR}. \"\n    \"For complex sub-problems, use the task tool to spawn a subagent.\"\n)\n\n# s06: subagent gets its own system prompt — no task, no recursion\nSUB_SYSTEM = (\n    f\"You are a coding agent at {WORKDIR}. \"\n    \"Complete the task you were given, then return a concise summary. \"\n    \"Do not delegate further.\"\n)\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02-s05 (unchanged): Tool Implementations\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text:\n            return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef _normalize_todos(todos):\n    if isinstance(todos, str):\n        try:\n            todos = json.loads(todos)\n        except json.JSONDecodeError:\n            try:\n                todos = ast.literal_eval(todos)\n            except (SyntaxError, ValueError):\n                return None, \"Error: todos must be a list or JSON array string\"\n    if not isinstance(todos, list):\n        return None, \"Error: todos must be a list\"\n    for i, t in enumerate(todos):\n        if not isinstance(t, dict):\n            return None, f\"Error: todos[{i}] must be an object\"\n        if \"content\" not in t or \"status\" not in t:\n            return None, f\"Error: todos[{i}] missing 'content' or 'status'\"\n        if t[\"status\"] not in (\"pending\", \"in_progress\", \"completed\"):\n            return None, f\"Error: todos[{i}] has invalid status '{t['status']}'\"\n    return todos, None\n\ndef run_todo_write(todos: list) -> str:\n    global CURRENT_TODOS\n    todos, error = _normalize_todos(todos)\n    if error:\n        return error\n    CURRENT_TODOS = todos\n    lines = [\"\\n\\033[33m## Current Tasks\\033[0m\"]\n    for t in CURRENT_TODOS:\n        icon = {\"pending\": \" \", \"in_progress\": \"\\033[36m▸\\033[0m\", \"completed\": \"\\033[32m✓\\033[0m\"}[t[\"status\"]]\n        lines.append(f\"  [{icon}] {t['content']}\")\n    print(\"\\n\".join(lines))\n    return f\"Updated {len(CURRENT_TODOS)} tasks\"\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n    {\"name\": \"todo_write\", \"description\": \"Create and manage a task list for your current coding session.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"todos\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}}, \"required\": [\"content\", \"status\"]}}}, \"required\": [\"todos\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob, \"todo_write\": run_todo_write,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s06: Subagent — fresh messages[], summary only\n# ═══════════════════════════════════════════════════════════\n\nSUB_TOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\n# NO \"task\" tool — prevent recursive spawning\n\nSUB_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob,\n}\n\ndef extract_text(content) -> str:\n    \"\"\"Extract text from message content blocks.\"\"\"\n    if not isinstance(content, list):\n        return str(content)\n    return \"\\n\".join(getattr(b, \"text\", \"\") for b in content if getattr(b, \"type\", None) == \"text\")\n\ndef spawn_subagent(description: str) -> str:\n    \"\"\"Spawn a subagent with fresh messages[], return summary only.\"\"\"\n    print(f\"\\n\\033[35m[Subagent spawned]\\033[0m\")\n    messages = [{\"role\": \"user\", \"content\": description}]  # fresh context\n\n    for _ in range(30):  # safety limit\n        response = client.messages.create(\n            model=MODEL, system=SUB_SYSTEM,\n            messages=messages, tools=SUB_TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            break\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                # Issue 1: subagent also runs hooks (permissions apply)\n                blocked = trigger_hooks(\"PreToolUse\", block)\n                if blocked:\n                    results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                    \"content\": str(blocked)})\n                    continue\n                handler = SUB_HANDLERS.get(block.name)\n                output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n                trigger_hooks(\"PostToolUse\", block, output)\n                print(f\"  \\033[90m[sub] {block.name}: {str(output)[:100]}\\033[0m\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n    # Issue 5: fallback if safety limit hit during tool_use\n    result = extract_text(messages[-1][\"content\"])\n    if not result:\n        # last message is tool_result, look backwards for assistant text\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\":\n                result = extract_text(msg[\"content\"])\n                if result:\n                    break\n        if not result:\n            result = \"Subagent stopped after 30 turns without final answer.\"\n    print(f\"\\033[35m[Subagent done]\\033[0m\")\n    return result  # only summary, entire message history discarded\n\n# Add task tool to parent's tools\nTOOLS.append({\n    \"name\": \"task\",\n    \"description\": \"Launch a subagent to handle a complex subtask. Returns only the final conclusion.\",\n    \"input_schema\": {\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\"}}, \"required\": [\"description\"]},\n})\nTOOL_HANDLERS[\"task\"] = spawn_subagent\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s04 (unchanged): Hook System\n# ═══════════════════════════════════════════════════════════\n\nHOOKS = {\"UserPromptSubmit\": [], \"PreToolUse\": [], \"PostToolUse\": [], \"Stop\": []}\n\ndef register_hook(event: str, callback):\n    HOOKS[event].append(callback)\n\ndef trigger_hooks(event: str, *args):\n    for callback in HOOKS[event]:\n        result = callback(*args)\n        if result is not None:\n            return result\n    return None\n\nDENY_LIST = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"mkfs\", \"dd if=\"]\n\ndef permission_hook(block):\n    \"\"\"PreToolUse: deny list check.\"\"\"\n    if block.name == \"bash\":\n        for p in DENY_LIST:\n            if p in block.input.get(\"command\", \"\"):\n                print(f\"\\n\\033[31m⛔ Blocked: '{p}'\\033[0m\")\n                return \"Permission denied\"\n    return None\n\ndef log_hook(block):\n    \"\"\"PreToolUse: log tool calls.\"\"\"\n    print(f\"\\033[90m[HOOK] {block.name}\\033[0m\")\n    return None\n\ndef context_inject_hook(query: str):\n    \"\"\"UserPromptSubmit: log working directory.\"\"\"\n    print(f\"\\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\\033[0m\")\n    return None\n\ndef summary_hook(messages: list):\n    \"\"\"Stop: print tool call count.\"\"\"\n    tool_count = sum(1 for m in messages\n                     for b in (m.get(\"content\") if isinstance(m.get(\"content\"), list) else [])\n                     if isinstance(b, dict) and b.get(\"type\") == \"tool_result\")\n    print(f\"\\033[90m[HOOK] Stop: session used {tool_count} tool calls\\033[0m\")\n    return None\n\nregister_hook(\"UserPromptSubmit\", context_inject_hook)\nregister_hook(\"PreToolUse\", permission_hook)\nregister_hook(\"PreToolUse\", log_hook)\nregister_hook(\"Stop\", summary_hook)\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — same as s05 + nag reminder, task auto-dispatches\n# ═══════════════════════════════════════════════════════════\n\nrounds_since_todo = 0\n\ndef agent_loop(messages: list):\n    global rounds_since_todo\n    while True:\n        # s05: nag reminder\n        if rounds_since_todo >= 3 and messages:\n            messages.append({\"role\": \"user\",\n                             \"content\": \"<reminder>Update your todos.</reminder>\"})\n            rounds_since_todo = 0\n\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            force = trigger_hooks(\"Stop\", messages)\n            if force:\n                messages.append({\"role\": \"user\", \"content\": force})\n                continue\n            return\n\n        rounds_since_todo += 1\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n\n            blocked = trigger_hooks(\"PreToolUse\", block)\n            if blocked:\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": str(blocked)})\n                continue\n\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n\n            trigger_hooks(\"PostToolUse\", block, output)\n\n            if block.name == \"todo_write\":\n                rounds_since_todo = 0\n\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                            \"content\": output})\n\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(\"s06: Subagent — spawn sub-agents with fresh context, summary only\")\n    print(\"Type a question, press Enter. Type q to quit.\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms06 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        trigger_hooks(\"UserPromptSubmit\", query)\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s06_subagent/subagent-overview.svg",
          "alt": "subagent overview"
        }
      ]
    },
    {
      "id": "s07",
      "filename": "s07_skill_loading/code.py",
      "title": "Skill Loading",
      "subtitle": "Load Only When Needed",
      "loc": 335,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob",
        "todo_write",
        "task",
        "load_skill"
      ],
      "newTools": [
        "load_skill"
      ],
      "coreAddition": "On-demand skill loader",
      "keyInsight": "Inject specialized knowledge only when the task actually needs it.",
      "classes": [],
      "functions": [
        {
          "name": "_parse_frontmatter",
          "signature": "def _parse_frontmatter(text: str)",
          "startLine": 53
        },
        {
          "name": "_scan_skills",
          "signature": "def _scan_skills()",
          "startLine": 69
        },
        {
          "name": "list_skills",
          "signature": "def list_skills()",
          "startLine": 86
        },
        {
          "name": "build_system",
          "signature": "def build_system()",
          "startLine": 93
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 116
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 122
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 131
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 140
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 149
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 160
        },
        {
          "name": "_normalize_todos",
          "signature": "def _normalize_todos(todos)",
          "startLine": 171
        },
        {
          "name": "run_todo_write",
          "signature": "def run_todo_write(todos: list)",
          "startLine": 191
        },
        {
          "name": "extract_text",
          "signature": "def extract_text(content)",
          "startLine": 204
        },
        {
          "name": "spawn_subagent",
          "signature": "def spawn_subagent(description: str)",
          "startLine": 229
        },
        {
          "name": "load_skill",
          "signature": "def load_skill(name: str)",
          "startLine": 269
        },
        {
          "name": "register_hook",
          "signature": "def register_hook(event: str, callback)",
          "startLine": 314
        },
        {
          "name": "trigger_hooks",
          "signature": "def trigger_hooks(event: str, *args)",
          "startLine": 317
        },
        {
          "name": "permission_hook",
          "signature": "def permission_hook(block)",
          "startLine": 326
        },
        {
          "name": "log_hook",
          "signature": "def log_hook(block)",
          "startLine": 334
        },
        {
          "name": "context_inject_hook",
          "signature": "def context_inject_hook(query: str)",
          "startLine": 338
        },
        {
          "name": "summary_hook",
          "signature": "def summary_hook(messages: list)",
          "startLine": 342
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 361
        }
      ],
      "layer": "planning",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns07: Skill Loading — two-level on-demand knowledge injection.\n\n  Layer 1 (cheap, always present):\n    SYSTEM prompt includes skill names + one-line descriptions (~100 tokens/skill)\n    \"Skills available: agent-builder, code-review, mcp-builder, pdf\"\n\n  Layer 2 (expensive, on demand):\n    Agent calls load_skill(\"code-review\") → full SKILL.md content\n    injected via tool_result (~2000 tokens/skill)\n\n  skills/\n    agent-builder/SKILL.md\n    code-review/SKILL.md\n    mcp-builder/SKILL.md\n    pdf/SKILL.md\n\nChanges from s06:\n  + build_system() — scan skills/ dir at startup, inject catalog into SYSTEM\n  + load_skill(name) — return full SKILL.md content via tool_result\n  + SKILLS_DIR config\n  Loop unchanged: load_skill auto-dispatches via TOOL_HANDLERS.\n\nRun: python s07_skill_loading/code.py\nNeeds: pip install anthropic python-dotenv pyyaml + ANTHROPIC_API_KEY in .env\n\"\"\"\n\nimport ast, json, os, subprocess\nfrom pathlib import Path\nimport yaml\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nSKILLS_DIR = WORKDIR / \"skills\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\nCURRENT_TODOS: list[dict] = []\n\n# s07: Skill catalog scan (used by build_system below)\ndef _parse_frontmatter(text: str) -> tuple[dict, str]:\n    \"\"\"Parse YAML frontmatter from SKILL.md. Returns (meta, body).\"\"\"\n    if not text.startswith(\"---\"):\n        return {}, text\n    parts = text.split(\"---\", 2)\n    if len(parts) < 3:\n        return {}, text\n    try:\n        meta = yaml.safe_load(parts[1]) or {}\n    except yaml.YAMLError:\n        meta = {}\n    return meta, parts[2].strip()\n\n# Build skill registry at startup (used for safe lookup in load_skill)\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n    \"\"\"Scan skills/ dir, populate SKILL_REGISTRY with name/description/content.\"\"\"\n    if not SKILLS_DIR.exists():\n        return\n    for d in sorted(SKILLS_DIR.iterdir()):\n        if not d.is_dir():\n            continue\n        manifest = d / \"SKILL.md\"\n        if manifest.exists():\n            raw = manifest.read_text()\n            meta, body = _parse_frontmatter(raw)\n            name = meta.get(\"name\", d.name)\n            desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n            SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills()\n\ndef list_skills() -> str:\n    \"\"\"List all skills (name + one-line description).\"\"\"\n    if not SKILL_REGISTRY:\n        return \"(no skills found)\"\n    return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\n# s07: SYSTEM includes skill catalog (cheap — just names + descriptions)\ndef build_system() -> str:\n    \"\"\"Build SYSTEM prompt with skill catalog injected at startup.\"\"\"\n    catalog = list_skills()\n    return (\n        f\"You are a coding agent at {WORKDIR}. \"\n        f\"Skills available:\\n{catalog}\\n\"\n        \"Use load_skill to get full details when needed.\"\n    )\n\nSYSTEM = build_system()\n\n# s07: subagent gets its own system prompt — no skill loading, no task\nSUB_SYSTEM = (\n    f\"You are a coding agent at {WORKDIR}. \"\n    \"Complete the task you were given, then return a concise summary. \"\n    \"Do not delegate further.\"\n)\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02-s06 (unchanged): Tool Implementations\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text:\n            return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef _normalize_todos(todos):\n    if isinstance(todos, str):\n        try:\n            todos = json.loads(todos)\n        except json.JSONDecodeError:\n            try:\n                todos = ast.literal_eval(todos)\n            except (SyntaxError, ValueError):\n                return None, \"Error: todos must be a list or JSON array string\"\n    if not isinstance(todos, list):\n        return None, \"Error: todos must be a list\"\n    for i, t in enumerate(todos):\n        if not isinstance(t, dict):\n            return None, f\"Error: todos[{i}] must be an object\"\n        if \"content\" not in t or \"status\" not in t:\n            return None, f\"Error: todos[{i}] missing 'content' or 'status'\"\n        if t[\"status\"] not in (\"pending\", \"in_progress\", \"completed\"):\n            return None, f\"Error: todos[{i}] has invalid status '{t['status']}'\"\n    return todos, None\n\ndef run_todo_write(todos: list) -> str:\n    global CURRENT_TODOS\n    todos, error = _normalize_todos(todos)\n    if error:\n        return error\n    CURRENT_TODOS = todos\n    lines = [\"\\n\\033[33m## Current Tasks\\033[0m\"]\n    for t in CURRENT_TODOS:\n        icon = {\"pending\": \" \", \"in_progress\": \"\\033[36m▸\\033[0m\", \"completed\": \"\\033[32m✓\\033[0m\"}[t[\"status\"]]\n        lines.append(f\"  [{icon}] {t['content']}\")\n    print(\"\\n\".join(lines))\n    return f\"Updated {len(CURRENT_TODOS)} tasks\"\n\ndef extract_text(content) -> str:\n    if not isinstance(content, list):\n        return str(content)\n    return \"\\n\".join(getattr(b, \"text\", \"\") for b in content if getattr(b, \"type\", None) == \"text\")\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s06 (unchanged): Subagent\n# ═══════════════════════════════════════════════════════════\n\nSUB_TOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\nSUB_HANDLERS = {\"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n                \"edit_file\": run_edit, \"glob\": run_glob}\n\ndef spawn_subagent(description: str) -> str:\n    print(f\"\\n\\033[35m[Subagent spawned]\\033[0m\")\n    messages = [{\"role\": \"user\", \"content\": description}]\n    for _ in range(30):\n        response = client.messages.create(model=MODEL, system=SUB_SYSTEM,\n            messages=messages, tools=SUB_TOOLS, max_tokens=8000)\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            break\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                blocked = trigger_hooks(\"PreToolUse\", block)\n                if blocked:\n                    results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                    \"content\": str(blocked)})\n                    continue\n                handler = SUB_HANDLERS.get(block.name)\n                output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n                trigger_hooks(\"PostToolUse\", block, output)\n                print(f\"  \\033[90m[sub] {block.name}: {str(output)[:100]}\\033[0m\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n    result = extract_text(messages[-1][\"content\"])\n    if not result:\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\":\n                result = extract_text(msg[\"content\"])\n                if result:\n                    break\n        if not result:\n            result = \"Subagent stopped after 30 turns without final answer.\"\n    print(f\"\\033[35m[Subagent done]\\033[0m\")\n    return result\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s07: load_skill — runtime full content loading\n# ═══════════════════════════════════════════════════════════\n\ndef load_skill(name: str) -> str:\n    \"\"\"Load full skill content. Lookup via registry — no path traversal.\"\"\"\n    skill = SKILL_REGISTRY.get(name)\n    if not skill:\n        return f\"Skill not found: {name}\"\n    return skill[\"content\"]\n\n\n# ═══════════════════════════════════════════════════════════\n#  Tool Registry — all tools from s02-s07\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n    {\"name\": \"todo_write\", \"description\": \"Create and manage a task list for your current coding session.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"todos\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}}, \"required\": [\"content\", \"status\"]}}}, \"required\": [\"todos\"]}},\n    {\"name\": \"task\", \"description\": \"Launch a subagent to handle a complex subtask. Returns only the final conclusion.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\"}}, \"required\": [\"description\"]}},\n    # s07: skill tool (catalog is already in SYSTEM prompt, this loads full content)\n    {\"name\": \"load_skill\", \"description\": \"Load the full content of a skill by name.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}, \"required\": [\"name\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob, \"todo_write\": run_todo_write,\n    \"task\": spawn_subagent, \"load_skill\": load_skill,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s04 (unchanged): Hook System\n# ═══════════════════════════════════════════════════════════\n\nHOOKS = {\"UserPromptSubmit\": [], \"PreToolUse\": [], \"PostToolUse\": [], \"Stop\": []}\n\ndef register_hook(event: str, callback):\n    HOOKS[event].append(callback)\n\ndef trigger_hooks(event: str, *args):\n    for callback in HOOKS[event]:\n        result = callback(*args)\n        if result is not None:\n            return result\n    return None\n\nDENY_LIST = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"mkfs\", \"dd if=\"]\n\ndef permission_hook(block):\n    if block.name == \"bash\":\n        for p in DENY_LIST:\n            if p in block.input.get(\"command\", \"\"):\n                print(f\"\\n\\033[31m⛔ Blocked: '{p}'\\033[0m\")\n                return \"Permission denied\"\n    return None\n\ndef log_hook(block):\n    print(f\"\\033[90m[HOOK] {block.name}\\033[0m\")\n    return None\n\ndef context_inject_hook(query: str):\n    print(f\"\\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\\033[0m\")\n    return None\n\ndef summary_hook(messages: list):\n    tool_count = sum(1 for m in messages\n                     for b in (m.get(\"content\") if isinstance(m.get(\"content\"), list) else [])\n                     if isinstance(b, dict) and b.get(\"type\") == \"tool_result\")\n    print(f\"\\033[90m[HOOK] Stop: session used {tool_count} tool calls\\033[0m\")\n    return None\n\nregister_hook(\"UserPromptSubmit\", context_inject_hook)\nregister_hook(\"PreToolUse\", permission_hook)\nregister_hook(\"PreToolUse\", log_hook)\nregister_hook(\"Stop\", summary_hook)\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — same as s05-s06 + nag reminder\n# ═══════════════════════════════════════════════════════════\n\nrounds_since_todo = 0\n\ndef agent_loop(messages: list):\n    global rounds_since_todo\n    while True:\n        if rounds_since_todo >= 3 and messages:\n            messages.append({\"role\": \"user\",\n                             \"content\": \"<reminder>Update your todos.</reminder>\"})\n            rounds_since_todo = 0\n            \n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            force = trigger_hooks(\"Stop\", messages)\n            if force:\n                messages.append({\"role\": \"user\", \"content\": force})\n                continue\n            return\n\n        rounds_since_todo += 1\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n\n            blocked = trigger_hooks(\"PreToolUse\", block)\n            if blocked:\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": str(blocked)})\n                continue\n\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n\n            trigger_hooks(\"PostToolUse\", block, output)\n\n            if block.name == \"todo_write\":\n                rounds_since_todo = 0\n\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                            \"content\": output})\n\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(\"s07: Skill Loading — catalog in SYSTEM, content on demand\")\n    print(\"Type a question, press Enter. Type q to quit.\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms07 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        trigger_hooks(\"UserPromptSubmit\", query)\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s07_skill_loading/skill-overview.svg",
          "alt": "skill overview"
        }
      ]
    },
    {
      "id": "s08",
      "filename": "s08_context_compact/code.py",
      "title": "Context Compact",
      "subtitle": "Context Will Fill Up",
      "loc": 382,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob",
        "todo_write",
        "task",
        "load_skill",
        "compact"
      ],
      "newTools": [
        "compact"
      ],
      "coreAddition": "Context compaction",
      "keyInsight": "Compression keeps the conversation usable when the context window gets crowded.",
      "classes": [],
      "functions": [
        {
          "name": "_parse_frontmatter",
          "signature": "def _parse_frontmatter(text: str)",
          "startLine": 59
        },
        {
          "name": "_scan_skills",
          "signature": "def _scan_skills()",
          "startLine": 74
        },
        {
          "name": "list_skills",
          "signature": "def list_skills()",
          "startLine": 90
        },
        {
          "name": "load_skill",
          "signature": "def load_skill(name: str)",
          "startLine": 95
        },
        {
          "name": "build_system",
          "signature": "def build_system()",
          "startLine": 102
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 124
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 129
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 136
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 143
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 149
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 158
        },
        {
          "name": "_normalize_todos",
          "signature": "def _normalize_todos(todos)",
          "startLine": 168
        },
        {
          "name": "run_todo_write",
          "signature": "def run_todo_write(todos: list)",
          "startLine": 188
        },
        {
          "name": "extract_text",
          "signature": "def extract_text(content)",
          "startLine": 201
        },
        {
          "name": "spawn_subagent",
          "signature": "def spawn_subagent(task: str)",
          "startLine": 225
        },
        {
          "name": "estimate_size",
          "signature": "def estimate_size(msgs)",
          "startLine": 269
        },
        {
          "name": "snip_compact",
          "signature": "def snip_compact(messages, max_messages=50)",
          "startLine": 273
        },
        {
          "name": "collect_tool_results",
          "signature": "def collect_tool_results(messages)",
          "startLine": 281
        },
        {
          "name": "micro_compact",
          "signature": "def micro_compact(messages)",
          "startLine": 290
        },
        {
          "name": "persist_large_output",
          "signature": "def persist_large_output(tool_use_id, output)",
          "startLine": 300
        },
        {
          "name": "tool_result_budget",
          "signature": "def tool_result_budget(messages, max_bytes=200_000)",
          "startLine": 307
        },
        {
          "name": "write_transcript",
          "signature": "def write_transcript(messages)",
          "startLine": 325
        },
        {
          "name": "summarize_history",
          "signature": "def summarize_history(messages)",
          "startLine": 332
        },
        {
          "name": "compact_history",
          "signature": "def compact_history(messages)",
          "startLine": 343
        },
        {
          "name": "reactive_compact",
          "signature": "def reactive_compact(messages)",
          "startLine": 351
        },
        {
          "name": "trigger_hooks",
          "signature": "def trigger_hooks(event, *args)",
          "startLine": 391
        },
        {
          "name": "permission_hook",
          "signature": "def permission_hook(block)",
          "startLine": 398
        },
        {
          "name": "log_hook",
          "signature": "def log_hook(block)",
          "startLine": 403
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 417
        }
      ],
      "layer": "memory",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns08_context_compact.py - Context Compact\n\nFour-layer compaction pipeline inserted before LLM calls:\n\n    L1: snip_compact      — trim middle messages when count > 50\n    L2: micro_compact     — replace old tool_results with placeholders\n    L3: tool_result_budget — persist large results to disk\n    L4: compact_history   — LLM full summary (1 API call)\n\n    Emergency: reactive_compact — when API still returns prompt_too_long\n\n    ┌─────────────────────────────────────────────────────────────┐\n    │  messages[]                                                 │\n    │    ↓                                                        │\n    │  L3 budget ─→ L1 snip ─→ L2 micro ─→ [token > threshold?]  │\n    │                                      ├─ No  → LLM          │\n    │                                      └─ Yes → L4 summary   │\n    │                                              ↓              │\n    │                                          LLM call           │\n    │                                    [prompt_too_long?]        │\n    │                                      └─ Yes → reactive      │\n    └─────────────────────────────────────────────────────────────┘\n\nCore principle: cheap first, expensive last.\nExecution order matches CC source: budget → snip → micro → auto.\n\nBuilds on s07 (skill loading). Usage:\n\n    python s08_context_compact/code.py\n    Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env\n\"\"\"\n\nimport ast, json, os, subprocess, time\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"): os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nSKILLS_DIR = WORKDIR / \"skills\"\nTRANSCRIPT_DIR = WORKDIR / \".transcripts\"\nTOOL_RESULTS_DIR = WORKDIR / \".task_outputs\" / \"tool-results\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\nCURRENT_TODOS: list[dict] = []\n\n# s07: Skill catalog scan (inherited from s07)\ndef _parse_frontmatter(text: str) -> tuple[dict, str]:\n    if not text.startswith(\"---\"):\n        return {}, text\n    parts = text.split(\"---\", 2)\n    if len(parts) < 3:\n        return {}, text\n    meta = {}\n    for line in parts[1].strip().splitlines():\n        if \":\" in line:\n            k, v = line.split(\":\", 1)\n            meta[k.strip()] = v.strip().strip('\"').strip(\"'\")\n    return meta, parts[2].strip()\n\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n    if not SKILLS_DIR.exists():\n        return\n    for d in sorted(SKILLS_DIR.iterdir()):\n        if not d.is_dir():\n            continue\n        manifest = d / \"SKILL.md\"\n        if manifest.exists():\n            raw = manifest.read_text()\n            meta, body = _parse_frontmatter(raw)\n            name = meta.get(\"name\", d.name)\n            desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n            SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills()\n\ndef list_skills() -> str:\n    if not SKILL_REGISTRY:\n        return \"(no skills found)\"\n    return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\ndef load_skill(name: str) -> str:\n    skill = SKILL_REGISTRY.get(name)\n    if not skill:\n        return f\"Skill not found: {name}\"\n    return skill[\"content\"]\n\n# s08: SYSTEM includes skill catalog (inherited from s07 build_system)\ndef build_system() -> str:\n    catalog = list_skills()\n    return (\n        f\"You are a coding agent at {WORKDIR}. \"\n        f\"Skills available:\\n{catalog}\\n\"\n        \"Use load_skill to get full details when needed.\"\n    )\n\nSYSTEM = build_system()\n\n# s08: subagent gets its own system prompt — no compact, no skill loading\nSUB_SYSTEM = (\n    f\"You are a coding agent at {WORKDIR}. \"\n    \"Complete the task you were given, then return a concise summary. \"\n    \"Do not delegate further.\"\n)\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02-s07 (unchanged): Basic Tools\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR): raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired: return \"Error: Timeout (120s)\"\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines): lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e: return f\"Error: {e}\"\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path); file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content); return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e: return f\"Error: {e}\"\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text: return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e: return f\"Error: {e}\"\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e: return f\"Error: {e}\"\n\ndef _normalize_todos(todos):\n    if isinstance(todos, str):\n        try:\n            todos = json.loads(todos)\n        except json.JSONDecodeError:\n            try:\n                todos = ast.literal_eval(todos)\n            except (SyntaxError, ValueError):\n                return None, \"Error: todos must be a list or JSON array string\"\n    if not isinstance(todos, list):\n        return None, \"Error: todos must be a list\"\n    for i, t in enumerate(todos):\n        if not isinstance(t, dict):\n            return None, f\"Error: todos[{i}] must be an object\"\n        if \"content\" not in t or \"status\" not in t:\n            return None, f\"Error: todos[{i}] missing 'content' or 'status'\"\n        if t[\"status\"] not in (\"pending\", \"in_progress\", \"completed\"):\n            return None, f\"Error: todos[{i}] has invalid status '{t['status']}'\"\n    return todos, None\n\ndef run_todo_write(todos: list) -> str:\n    global CURRENT_TODOS\n    todos, error = _normalize_todos(todos)\n    if error:\n        return error\n    CURRENT_TODOS = todos\n    lines = [\"\\n\\033[33m## Current Tasks\\033[0m\"]\n    for t in CURRENT_TODOS:\n        icon = {\"pending\": \" \", \"in_progress\": \"\\033[36m▸\\033[0m\", \"completed\": \"\\033[32m✓\\033[0m\"}[t[\"status\"]]\n        lines.append(f\"  [{icon}] {t['content']}\")\n    print(\"\\n\".join(lines))\n    return f\"Updated {len(CURRENT_TODOS)} tasks\"\n\ndef extract_text(content) -> str:\n    if not isinstance(content, list): return str(content)\n    return \"\\n\".join(getattr(b, \"text\", \"\") for b in content if getattr(b, \"type\", None) == \"text\")\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s06-s07 (unchanged): Subagent\n# ═══════════════════════════════════════════════════════════\n\nSUB_TOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\nSUB_HANDLERS = {\"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n                \"edit_file\": run_edit, \"glob\": run_glob}\n\ndef spawn_subagent(task: str) -> str:\n    print(f\"\\n\\033[35m[Subagent spawned]\\033[0m\")\n    messages = [{\"role\": \"user\", \"content\": task}]\n    for _ in range(30):\n        response = client.messages.create(model=MODEL, system=SUB_SYSTEM,\n            messages=messages, tools=SUB_TOOLS, max_tokens=8000)\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            break\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                blocked = trigger_hooks(\"PreToolUse\", block)\n                if blocked:\n                    results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                    \"content\": str(blocked)})\n                    continue\n                handler = SUB_HANDLERS.get(block.name)\n                output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n                trigger_hooks(\"PostToolUse\", block, output)\n                print(f\"  \\033[90m[sub] {block.name}: {str(output)[:100]}\\033[0m\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n    result = extract_text(messages[-1][\"content\"])\n    if not result:\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\":\n                result = extract_text(msg[\"content\"])\n                if result:\n                    break\n        if not result:\n            result = \"Subagent stopped after 30 turns without final answer.\"\n    print(f\"\\033[35m[Subagent done]\\033[0m\")\n    return result\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s08: Four-Layer Compaction Pipeline\n# ═══════════════════════════════════════════════════════════\n\nCONTEXT_LIMIT = 50000\nKEEP_RECENT = 3\nPERSIST_THRESHOLD = 30000\n\ndef estimate_size(msgs): return len(str(msgs))\n\n\n# L1: snipCompact — trim middle messages\ndef snip_compact(messages, max_messages=50):\n    if len(messages) <= max_messages: return messages\n    keep_head, keep_tail = 3, max_messages - 3\n    snipped = len(messages) - keep_head - keep_tail\n    return messages[:keep_head] + [{\"role\": \"user\", \"content\": f\"[snipped {snipped} messages]\"}] + messages[-keep_tail:]\n\n\n# L2: microCompact — old result placeholders\ndef collect_tool_results(messages):\n    blocks = []\n    for mi, msg in enumerate(messages):\n        if msg.get(\"role\") != \"user\" or not isinstance(msg.get(\"content\"), list): continue\n        for bi, block in enumerate(msg[\"content\"]):\n            if isinstance(block, dict) and block.get(\"type\") == \"tool_result\":\n                blocks.append((mi, bi, block))\n    return blocks\n\ndef micro_compact(messages):\n    tool_results = collect_tool_results(messages)\n    if len(tool_results) <= KEEP_RECENT: return messages\n    for _, _, block in tool_results[:-KEEP_RECENT]:\n        if len(block.get(\"content\", \"\")) > 120:\n            block[\"content\"] = \"[Earlier tool result compacted. Re-run if needed.]\"\n    return messages\n\n\n# L3: toolResultBudget — persist large results to disk\ndef persist_large_output(tool_use_id, output):\n    if len(output) <= PERSIST_THRESHOLD: return output\n    TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)\n    path = TOOL_RESULTS_DIR / f\"{tool_use_id}.txt\"\n    if not path.exists(): path.write_text(output)\n    return f\"<persisted-output>\\nFull output: {path}\\nPreview:\\n{output[:2000]}\\n</persisted-output>\"\n\ndef tool_result_budget(messages, max_bytes=200_000):\n    last = messages[-1] if messages else None\n    if not last or last.get(\"role\") != \"user\" or not isinstance(last.get(\"content\"), list): return messages\n    blocks = [(i, b) for i, b in enumerate(last[\"content\"]) if isinstance(b, dict) and b.get(\"type\") == \"tool_result\"]\n    total = sum(len(str(b.get(\"content\", \"\"))) for _, b in blocks)\n    if total <= max_bytes: return messages\n    ranked = sorted(blocks, key=lambda p: len(str(p[1].get(\"content\", \"\"))), reverse=True)\n    for _, block in ranked:\n        if total <= max_bytes: break\n        content = str(block.get(\"content\", \"\"))\n        if len(content) <= PERSIST_THRESHOLD: continue\n        tid = block.get(\"tool_use_id\", \"unknown\")\n        block[\"content\"] = persist_large_output(tid, content)\n        total = sum(len(str(b.get(\"content\", \"\"))) for _, b in blocks)\n    return messages\n\n\n# L4: autoCompact — LLM full summary\ndef write_transcript(messages):\n    TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)\n    path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with path.open(\"w\") as f:\n        for msg in messages: f.write(json.dumps(msg, default=str) + \"\\n\")\n    return path\n\ndef summarize_history(messages):\n    conversation = json.dumps(messages, default=str)[:80000]\n    prompt = (\"Summarize this coding-agent conversation so work can continue.\\n\"\n              \"Preserve: 1. current goal, 2. key findings/decisions, 3. files read/changed, \"\n              \"4. remaining work, 5. user constraints.\\nBe compact but concrete.\\n\\n\" + conversation)\n    response = client.messages.create(model=MODEL, messages=[{\"role\": \"user\", \"content\": prompt}], max_tokens=2000)\n    return \"\\n\".join(\n        getattr(block, \"text\", \"\")\n        for block in response.content\n        if getattr(block, \"type\", None) == \"text\").strip() or \"(empty summary)\"\n\ndef compact_history(messages):\n    transcript_path = write_transcript(messages)\n    print(f\"[transcript saved: {transcript_path}]\")\n    summary = summarize_history(messages)\n    return [{\"role\": \"user\", \"content\": f\"[Compacted]\\n\\n{summary}\"}]\n\n\n# Emergency: reactiveCompact — on API error\ndef reactive_compact(messages):\n    transcript = write_transcript(messages)\n    summary = summarize_history(messages)\n    return [{\"role\": \"user\", \"content\": f\"[Reactive compact]\\n\\n{summary}\"}, *messages[-5:]]\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s07: Tool Definitions\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n    {\"name\": \"todo_write\", \"description\": \"Create and manage a task list for your current coding session.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"todos\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}}, \"required\": [\"content\", \"status\"]}}}, \"required\": [\"todos\"]}},\n    {\"name\": \"task\", \"description\": \"Launch a subagent to handle a complex subtask. Returns only the final conclusion.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\"}}, \"required\": [\"description\"]}},\n    {\"name\": \"load_skill\", \"description\": \"Load the full content of a skill by name.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}, \"required\": [\"name\"]}},\n    # s08 change: new compact tool — triggers compact_history, not a no-op\n    {\"name\": \"compact\", \"description\": \"Summarize earlier conversation to free context space.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"focus\": {\"type\": \"string\"}}}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob, \"todo_write\": run_todo_write,\n    \"task\": spawn_subagent, \"load_skill\": load_skill,\n}\n\n# FROM s04 (unchanged): Hooks\nHOOKS = {\"PreToolUse\": [], \"PostToolUse\": []}\ndef trigger_hooks(event, *args):\n    for cb in HOOKS[event]:\n        r = cb(*args)\n        if r is not None: return r\n    return None\n\nDENY_LIST = [\"rm -rf /\", \"sudo\", \"shutdown\"]\ndef permission_hook(block):\n    if block.name == \"bash\":\n        for p in DENY_LIST:\n            if p in block.input.get(\"command\", \"\"): return \"Permission denied\"\n    return None\ndef log_hook(block):\n    print(f\"\\033[90m[HOOK] {block.name}\\033[0m\")\n    return None\n\nHOOKS[\"PreToolUse\"].append(permission_hook)\nHOOKS[\"PreToolUse\"].append(log_hook)\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — s08 core: run compaction pipeline before LLM\n# ═══════════════════════════════════════════════════════════\n\nMAX_REACTIVE_RETRIES = 1  # retry limit for reactive compact\n\ndef agent_loop(messages: list):\n    reactive_retries = 0\n    while True:\n        # s08 change: three preprocessors (0 API calls, cheap first)\n        # Order matches CC source: budget → snip → micro\n        messages[:] = tool_result_budget(messages)    # L3: persist large results first\n        messages[:] = snip_compact(messages)          # L1: trim middle\n        messages[:] = micro_compact(messages)         # L2: old result placeholders\n\n        # s08 change: tokens still over threshold → LLM summary (1 API call)\n        if estimate_size(messages) > CONTEXT_LIMIT:\n            print(\"[auto compact]\")\n            messages[:] = compact_history(messages)\n\n        try:\n            response = client.messages.create(model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000)\n            reactive_retries = 0  # reset on successful API call\n        except Exception as e:\n            if (\"prompt_too_long\" in str(e).lower() or \"too many tokens\" in str(e).lower()) and reactive_retries < MAX_REACTIVE_RETRIES:\n                print(\"[reactive compact]\")\n                messages[:] = reactive_compact(messages)\n                reactive_retries += 1\n                continue\n            raise\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\": return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\": continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n\n            # s08: compact tool triggers compact_history, not a no-op string\n            if block.name == \"compact\":\n                messages[:] = compact_history(messages)\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": \"[Compacted. Conversation history has been summarized.]\"})\n                messages.append({\"role\": \"user\", \"content\": results})\n                break  # end current turn, start fresh with compacted context\n\n            blocked = trigger_hooks(\"PreToolUse\", block)\n            if blocked:\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(blocked)})\n                continue\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n            trigger_hooks(\"PostToolUse\", block, output)\n            print(str(output)[:200])\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n        else:\n            # normal path: no compact was called\n            messages.append({\"role\": \"user\", \"content\": results})\n            continue\n        # compact was called: results already appended above\n        continue\n\n\nif __name__ == \"__main__\":\n    print(\"s08: Context Compact — four-layer compaction pipeline\")\n    print(\"输入问题，回车发送。输入 q 退出。\\n\")\n    history = []\n    while True:\n        try: query = input(\"\\033[36ms08 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt): break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"): break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\": print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s08_context_compact/auto-compact.svg",
          "alt": "auto compact"
        },
        {
          "src": "/course-assets/s08_context_compact/compact-overview.svg",
          "alt": "compact overview"
        },
        {
          "src": "/course-assets/s08_context_compact/compaction-layers.svg",
          "alt": "compaction layers"
        },
        {
          "src": "/course-assets/s08_context_compact/layer1-budget.svg",
          "alt": "layer1 budget"
        },
        {
          "src": "/course-assets/s08_context_compact/micro-compact.svg",
          "alt": "micro compact"
        }
      ]
    },
    {
      "id": "s09",
      "filename": "s09_memory/code.py",
      "title": "Memory",
      "subtitle": "Keep a Layer That Doesn't Lose Details",
      "loc": 498,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "edit_file",
        "glob",
        "task"
      ],
      "newTools": [],
      "coreAddition": "Durable memory layer",
      "keyInsight": "Some facts should survive summarization and future sessions.",
      "classes": [],
      "functions": [
        {
          "name": "_parse_frontmatter",
          "signature": "def _parse_frontmatter(text: str)",
          "startLine": 58
        },
        {
          "name": "write_memory_file",
          "signature": "def write_memory_file(name: str, mem_type: str, description: str, body: str)",
          "startLine": 72
        },
        {
          "name": "_rebuild_index",
          "signature": "def _rebuild_index()",
          "startLine": 84
        },
        {
          "name": "read_memory_index",
          "signature": "def read_memory_index()",
          "startLine": 98
        },
        {
          "name": "read_memory_file",
          "signature": "def read_memory_file(filename: str)",
          "startLine": 106
        },
        {
          "name": "list_memory_files",
          "signature": "def list_memory_files()",
          "startLine": 114
        },
        {
          "name": "select_relevant_memories",
          "signature": "def select_relevant_memories(messages: list, max_items: int = 5)",
          "startLine": 132
        },
        {
          "name": "load_memories",
          "signature": "def load_memories(messages: list)",
          "startLine": 207
        },
        {
          "name": "extract_memories",
          "signature": "def extract_memories(messages: list)",
          "startLine": 222
        },
        {
          "name": "consolidate_memories",
          "signature": "def consolidate_memories()",
          "startLine": 287
        },
        {
          "name": "build_system",
          "signature": "def build_system()",
          "startLine": 337
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 360
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 365
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 372
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 379
        },
        {
          "name": "run_edit",
          "signature": "def run_edit(path: str, old_text: str, new_text: str)",
          "startLine": 385
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str)",
          "startLine": 394
        },
        {
          "name": "extract_text",
          "signature": "def extract_text(content)",
          "startLine": 404
        },
        {
          "name": "spawn_subagent",
          "signature": "def spawn_subagent(task: str)",
          "startLine": 419
        },
        {
          "name": "estimate_size",
          "signature": "def estimate_size(msgs)",
          "startLine": 452
        },
        {
          "name": "snip_compact",
          "signature": "def snip_compact(msgs, mx=50)",
          "startLine": 454
        },
        {
          "name": "collect_tool_results",
          "signature": "def collect_tool_results(msgs)",
          "startLine": 458
        },
        {
          "name": "micro_compact",
          "signature": "def micro_compact(msgs)",
          "startLine": 466
        },
        {
          "name": "persist_large",
          "signature": "def persist_large(tid, out)",
          "startLine": 473
        },
        {
          "name": "tool_result_budget",
          "signature": "def tool_result_budget(msgs, mx=200_000)",
          "startLine": 480
        },
        {
          "name": "write_transcript",
          "signature": "def write_transcript(msgs)",
          "startLine": 494
        },
        {
          "name": "summarize_history",
          "signature": "def summarize_history(msgs)",
          "startLine": 501
        },
        {
          "name": "compact_history",
          "signature": "def compact_history(msgs)",
          "startLine": 509
        },
        {
          "name": "reactive_compact",
          "signature": "def reactive_compact(msgs)",
          "startLine": 514
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list)",
          "startLine": 551
        }
      ],
      "layer": "memory",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns09_memory.py - Memory System\n\nPersistent, cross-session knowledge for the coding agent.\n\nStorage:\n    .memory/\n      MEMORY.md          ← index (one line per memory, ≤200 lines)\n      feedback_tabs.md    ← individual memory files (Markdown + YAML frontmatter)\n      user_profile.md\n      project_facts.md\n\nFlow in agent_loop:\n    1. Load MEMORY.md index into SYSTEM prompt (cheap, always present)\n    2. Select relevant memories by filename/description → inject content\n    3. Run compression pipeline from s08\n    4. After each turn ends → extract new memories from original messages\n    5. Periodically consolidate (Dream)\n\nBuilds on s08 (context compact). Usage:\n\n    python s09_memory/code.py\n    Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env\n\"\"\"\n\nimport os, subprocess, json, time, re\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"): os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"; MEMORY_DIR.mkdir(exist_ok=True)\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nSKILLS_DIR = WORKDIR / \"skills\"\nTRANSCRIPT_DIR = WORKDIR / \".transcripts\"\nTOOL_RESULTS_DIR = WORKDIR / \".task_outputs\" / \"tool-results\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n\n# ═══════════════════════════════════════════════════════════\n#  NEW in s09: Memory System\n# ═══════════════════════════════════════════════════════════\n\nMEMORY_TYPES = [\"user\", \"feedback\", \"project\", \"reference\"]\n\ndef _parse_frontmatter(text: str) -> tuple[dict, str]:\n    if not text.startswith(\"---\"):\n        return {}, text\n    parts = text.split(\"---\", 2)\n    if len(parts) < 3:\n        return {}, text\n    meta = {}\n    for line in parts[1].strip().splitlines():\n        if \":\" in line:\n            k, v = line.split(\":\", 1)\n            meta[k.strip()] = v.strip().strip('\"').strip(\"'\")\n    return meta, parts[2].strip()\n\n\ndef write_memory_file(name: str, mem_type: str, description: str, body: str):\n    \"\"\"Write a single memory file with YAML frontmatter.\"\"\"\n    slug = name.lower().replace(\" \", \"-\").replace(\"/\", \"-\")\n    filename = f\"{slug}.md\"\n    filepath = MEMORY_DIR / filename\n    filepath.write_text(\n        f\"---\\nname: {name}\\ndescription: {description}\\ntype: {mem_type}\\n---\\n\\n{body}\\n\"\n    )\n    _rebuild_index()\n    return filepath\n\n\ndef _rebuild_index():\n    \"\"\"Rebuild MEMORY.md index from all memory files.\"\"\"\n    lines = []\n    for f in sorted(MEMORY_DIR.glob(\"*.md\")):\n        if f.name == \"MEMORY.md\":\n            continue\n        raw = f.read_text()\n        meta, body = _parse_frontmatter(raw)\n        name = meta.get(\"name\", f.stem)\n        desc = meta.get(\"description\", body.split(\"\\n\")[0][:80])\n        lines.append(f\"- [{name}]({f.name}) — {desc}\")\n    MEMORY_INDEX.write_text(\"\\n\".join(lines) + \"\\n\" if lines else \"\")\n\n\ndef read_memory_index() -> str:\n    \"\"\"Read MEMORY.md index (injected into SYSTEM every turn).\"\"\"\n    if not MEMORY_INDEX.exists():\n        return \"\"\n    text = MEMORY_INDEX.read_text().strip()\n    return text if text else \"\"\n\n\ndef read_memory_file(filename: str) -> str | None:\n    \"\"\"Read a single memory file's full content.\"\"\"\n    path = MEMORY_DIR / filename\n    if not path.exists():\n        return None\n    return path.read_text()\n\n\ndef list_memory_files() -> list[dict]:\n    \"\"\"List all memory files with metadata.\"\"\"\n    result = []\n    for f in sorted(MEMORY_DIR.glob(\"*.md\")):\n        if f.name == \"MEMORY.md\":\n            continue\n        raw = f.read_text()\n        meta, body = _parse_frontmatter(raw)\n        result.append({\n            \"filename\": f.name,\n            \"name\": meta.get(\"name\", f.stem),\n            \"description\": meta.get(\"description\", \"\"),\n            \"type\": meta.get(\"type\", \"user\"),\n            \"body\": body,\n        })\n    return result\n\n\ndef select_relevant_memories(messages: list, max_items: int = 5) -> list[str]:\n    \"\"\"Select relevant memory filenames by matching recent conversation against\n    memory names/descriptions. Uses a simple LLM call (or falls back to keyword\n    matching on name+description).\"\"\"\n    files = list_memory_files()\n    if not files:\n        return []\n\n    # Collect recent user text for context\n    recent_texts = []\n    for msg in reversed(messages):\n        if msg.get(\"role\") == \"user\":\n            content = msg.get(\"content\", \"\")\n            if isinstance(content, list):\n                content = \" \".join(\n                    str(getattr(b, \"text\", \"\")) for b in content\n                    if getattr(b, \"type\", None) == \"text\"\n                )\n            if isinstance(content, str):\n                recent_texts.append(content)\n            if len(recent_texts) >= 3:\n                break\n    recent = \" \".join(reversed(recent_texts))[:2000]\n\n    if not recent.strip():\n        return []\n\n    # Build catalog of name + description for LLM to choose from\n    catalog_lines = []\n    for i, f in enumerate(files):\n        catalog_lines.append(f\"{i}: {f['name']} — {f['description']}\")\n    catalog = \"\\n\".join(catalog_lines)\n\n    prompt = (\n        \"Given the recent conversation and the memory catalog below, \"\n        \"select the indices of memories that are clearly relevant. \"\n        \"Return ONLY a JSON array of integers, e.g. [0, 3]. \"\n        \"If none are relevant, return [].\\n\\n\"\n        f\"Recent conversation:\\n{recent}\\n\\n\"\n        f\"Memory catalog:\\n{catalog}\"\n    )\n\n    try:\n        response = client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": prompt}],\n            max_tokens=200,\n        )\n        text = extract_text(response.content).strip()\n        # Extract JSON array from response\n        match = re.search(r'\\[.*?\\]', text, re.DOTALL)\n        if match:\n            indices = json.loads(match.group())\n            selected = []\n            for idx in indices:\n                if isinstance(idx, int) and 0 <= idx < len(files):\n                    selected.append(files[idx][\"filename\"])\n                    if len(selected) >= max_items:\n                        break\n            return selected\n    except Exception:\n        pass\n\n    # Fallback: keyword matching on name + description\n    keywords = [w.lower() for w in recent.split() if len(w) > 3]\n    selected = []\n    for f in files:\n        text = (f[\"name\"] + \" \" + f[\"description\"]).lower()\n        if any(kw in text for kw in keywords):\n            selected.append(f[\"filename\"])\n            if len(selected) >= max_items:\n                break\n    return selected\n\n\ndef load_memories(messages: list) -> str:\n    \"\"\"Load relevant memory content for injection into context.\"\"\"\n    selected_files = select_relevant_memories(messages)\n    if not selected_files:\n        return \"\"\n\n    parts = [\"<relevant_memories>\"]\n    for filename in selected_files:\n        content = read_memory_file(filename)\n        if content:\n            parts.append(content)\n    parts.append(\"</relevant_memories>\")\n    return \"\\n\\n\".join(parts)\n\n\ndef extract_memories(messages: list):\n    \"\"\"Extract new memories from recent dialogue. Runs after each turn.\"\"\"\n    # Collect recent conversation text\n    dialogue_parts = []\n    for msg in messages[-10:]:\n        role = msg.get(\"role\", \"?\")\n        content = msg.get(\"content\", \"\")\n        if isinstance(content, list):\n            content = \" \".join(\n                str(getattr(b, \"text\", \"\")) for b in content\n                if getattr(b, \"type\", None) == \"text\"\n            )\n        if isinstance(content, str) and content.strip():\n            dialogue_parts.append(f\"{role}: {content}\")\n    dialogue = \"\\n\".join(dialogue_parts)\n\n    if not dialogue.strip():\n        return\n\n    # Check existing memories to avoid duplicates\n    existing = list_memory_files()\n    existing_desc = \"\\n\".join(f\"- {m['name']}: {m['description']}\" for m in existing) if existing else \"(none)\"\n\n    prompt = (\n        \"Extract user preferences, constraints, or project facts from this dialogue.\\n\"\n        \"Return a JSON array. Each item: {name, type, description, body}.\\n\"\n        \"- name: short kebab-case identifier (e.g. 'user-preference-tabs')\\n\"\n        \"- type: one of 'user' (user preference), 'feedback' (guidance), \"\n        \"'project' (project fact), 'reference' (external pointer)\\n\"\n        \"- description: one-line summary for index lookup\\n\"\n        \"- body: full detail in markdown\\n\"\n        \"If nothing new or already covered by existing memories, return [].\\n\\n\"\n        f\"Existing memories:\\n{existing_desc}\\n\\n\"\n        f\"Dialogue:\\n{dialogue[:4000]}\"\n    )\n\n    try:\n        response = client.messages.create(\n            model=MODEL, messages=[{\"role\": \"user\", \"content\": prompt}], max_tokens=800\n        )\n        text = extract_text(response.content).strip()\n        # Extract JSON array from response\n        match = re.search(r'\\[.*\\]', text, re.DOTALL)\n        if not match:\n            return\n        items = json.loads(match.group())\n        if not items:\n            return\n        count = 0\n        for mem in items:\n            name = mem.get(\"name\", f\"memory_{int(time.time())}\")\n            mem_type = mem.get(\"type\", \"user\")\n            desc = mem.get(\"description\", \"\")\n            body = mem.get(\"body\", \"\")\n            if desc and body:\n                write_memory_file(name, mem_type, desc, body)\n                count += 1\n        if count:\n            print(f\"\\n\\033[33m[Memory: extracted {count} new memories]\\033[0m\")\n    except Exception:\n        pass\n\n\nCONSOLIDATE_THRESHOLD = 10\n\ndef consolidate_memories():\n    \"\"\"Merge duplicate/stale memories. Triggered when file count ≥ threshold.\"\"\"\n    files = list_memory_files()\n    if len(files) < CONSOLIDATE_THRESHOLD:\n        return\n\n    catalog = \"\\n\\n\".join(\n        f\"## {f['filename']}\\nname: {f['name']}\\ndescription: {f['description']}\\n{f['body']}\"\n        for f in files\n    )\n\n    prompt = (\n        \"Consolidate the following memory files. Rules:\\n\"\n        \"1. Merge duplicates into one\\n\"\n        \"2. Remove outdated/contradicted memories\\n\"\n        \"3. Keep the total under 30 memories\\n\"\n        \"4. Preserve important user preferences above all\\n\"\n        \"Return a JSON array. Each item: {name, type, description, body}.\\n\\n\"\n        f\"{catalog[:16000]}\"\n    )\n\n    try:\n        response = client.messages.create(\n            model=MODEL, messages=[{\"role\": \"user\", \"content\": prompt}], max_tokens=3000\n        )\n        text = extract_text(response.content).strip()\n        match = re.search(r'\\[.*\\]', text, re.DOTALL)\n        if not match:\n            return\n        items = json.loads(match.group())\n\n        # Remove old memory files (keep MEMORY.md)\n        for f in MEMORY_DIR.glob(\"*.md\"):\n            if f.name != \"MEMORY.md\":\n                f.unlink()\n\n        for mem in items:\n            name = mem.get(\"name\", f\"memory_{int(time.time())}\")\n            mem_type = mem.get(\"type\", \"user\")\n            desc = mem.get(\"description\", \"\")\n            body = mem.get(\"body\", \"\")\n            if desc and body:\n                write_memory_file(name, mem_type, desc, body)\n\n        print(f\"\\n\\033[33m[Memory: consolidated {len(files)} → {len(items)} memories]\\033[0m\")\n    except Exception:\n        pass\n\n\n# Build SYSTEM with memory index\ndef build_system() -> str:\n    index = read_memory_index()\n    memories_section = f\"\\n\\nMemories available:\\n{index}\" if index else \"\"\n    return (\n        f\"You are a coding agent at {WORKDIR}.\"\n        f\"{memories_section}\\n\"\n        \"Relevant memories are injected below. Respect user preferences from memory.\\n\"\n        \"When the user says 'remember' or expresses a clear preference, extract it as a memory.\"\n    )\n\nSYSTEM = build_system()\n\nSUB_SYSTEM = (\n    f\"You are a coding agent at {WORKDIR}. \"\n    \"Complete the task you were given, then return a concise summary. \"\n    \"Do not delegate further.\"\n)\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s02-s08 (skeleton): Basic tools\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR): raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired: return \"Error: Timeout (120s)\"\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines): lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e: return f\"Error: {e}\"\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path); file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content); return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e: return f\"Error: {e}\"\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        file_path = safe_path(path)\n        text = file_path.read_text()\n        if old_text not in text: return f\"Error: text not found in {path}\"\n        file_path.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e: return f\"Error: {e}\"\n\ndef run_glob(pattern: str) -> str:\n    import glob as g\n    try:\n        results = []\n        for match in g.glob(pattern, root_dir=WORKDIR):\n            if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e: return f\"Error: {e}\"\n\ndef extract_text(content) -> str:\n    if not isinstance(content, list): return str(content)\n    return \"\\n\".join(getattr(b, \"text\", \"\") for b in content if getattr(b, \"type\", None) == \"text\")\n\n# Subagent (simplified from s06-s07)\nSUB_TOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n]\nSUB_HANDLERS = {\"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write}\n\ndef spawn_subagent(task: str) -> str:\n    print(f\"\\n\\033[35m[Subagent spawned]\\033[0m\")\n    messages = [{\"role\": \"user\", \"content\": task}]\n    for _ in range(30):\n        response = client.messages.create(model=MODEL, system=SUB_SYSTEM,\n            messages=messages, tools=SUB_TOOLS, max_tokens=8000)\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\": break\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = SUB_HANDLERS.get(block.name)\n                output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n                print(f\"  \\033[90m[sub] {block.name}: {str(output)[:100]}\\033[0m\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n    result = extract_text(messages[-1][\"content\"])\n    if not result:\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\":\n                result = extract_text(msg[\"content\"])\n                if result: break\n        if not result: result = \"Subagent stopped after 30 turns without final answer.\"\n    print(f\"\\033[35m[Subagent done]\\033[0m\")\n    return result\n\n\n# ═══════════════════════════════════════════════════════════\n#  FROM s08 (skeleton): Compaction pipeline\n# ═══════════════════════════════════════════════════════════\n\nCONTEXT_LIMIT = 50000; KEEP_RECENT = 3; PERSIST_THRESHOLD = 30000\n\ndef estimate_size(msgs): return len(str(msgs))\n\ndef snip_compact(msgs, mx=50):\n    if len(msgs) <= mx: return msgs\n    return msgs[:3] + [{\"role\": \"user\", \"content\": f\"[snipped {len(msgs)-mx} msgs]\"}] + msgs[-(mx-3):]\n\ndef collect_tool_results(msgs):\n    blocks = []\n    for mi, msg in enumerate(msgs):\n        if msg.get(\"role\") != \"user\" or not isinstance(msg.get(\"content\"), list): continue\n        for bi, block in enumerate(msg[\"content\"]):\n            if isinstance(block, dict) and block.get(\"type\") == \"tool_result\": blocks.append((mi, bi, block))\n    return blocks\n\ndef micro_compact(msgs):\n    tr = collect_tool_results(msgs)\n    if len(tr) <= KEEP_RECENT: return msgs\n    for _, _, b in tr[:-KEEP_RECENT]:\n        if len(b.get(\"content\", \"\")) > 120: b[\"content\"] = \"[Earlier tool result compacted.]\"\n    return msgs\n\ndef persist_large(tid, out):\n    if len(out) <= PERSIST_THRESHOLD: return out\n    TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)\n    p = TOOL_RESULTS_DIR / f\"{tid}.txt\"\n    if not p.exists(): p.write_text(out)\n    return f\"<persisted-output>\\nFull: {p}\\nPreview:\\n{out[:2000]}\\n</persisted-output>\"\n\ndef tool_result_budget(msgs, mx=200_000):\n    last = msgs[-1] if msgs else None\n    if not last or last.get(\"role\") != \"user\" or not isinstance(last.get(\"content\"), list): return msgs\n    blocks = [(i, b) for i, b in enumerate(last[\"content\"]) if isinstance(b, dict) and b.get(\"type\") == \"tool_result\"]\n    total = sum(len(str(b.get(\"content\", \"\"))) for _, b in blocks)\n    if total <= mx: return msgs\n    for _, block in sorted(blocks, key=lambda p: len(str(p[1].get(\"content\", \"\"))), reverse=True):\n        if total <= mx: break\n        c = str(block.get(\"content\", \"\"))\n        if len(c) <= PERSIST_THRESHOLD: continue\n        block[\"content\"] = persist_large(block.get(\"tool_use_id\", \"?\"), c)\n        total = sum(len(str(b.get(\"content\", \"\"))) for _, b in blocks)\n    return msgs\n\ndef write_transcript(msgs):\n    TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)\n    p = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with p.open(\"w\") as f:\n        for m in msgs: f.write(json.dumps(m, default=str) + \"\\n\")\n    return p\n\ndef summarize_history(msgs):\n    conv = json.dumps(msgs, default=str)[:80000]\n    r = client.messages.create(model=MODEL, messages=[{\"role\": \"user\", \"content\":\n        \"Summarize this coding-agent conversation so work can continue.\\n\"\n        \"Preserve: 1. current goal, 2. key findings, 3. files changed, 4. remaining work, 5. user constraints.\\n\\n\" + conv}],\n        max_tokens=2000)\n    return extract_text(r.content).strip()\n\ndef compact_history(msgs):\n    write_transcript(msgs)\n    summary = summarize_history(msgs)\n    return [{\"role\": \"user\", \"content\": f\"[Compacted]\\n\\n{summary}\"}]\n\ndef reactive_compact(msgs):\n    write_transcript(msgs)\n    summary = summarize_history(msgs)\n    return [{\"role\": \"user\", \"content\": f\"[Reactive compact]\\n\\n{summary}\"}, *msgs[-5:]]\n\n\n# ═══════════════════════════════════════════════════════════\n#  Tool Definitions (skeleton — fewer tools to focus on memory)\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n    {\"name\": \"task\", \"description\": \"Launch a subagent to handle a subtask.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\"}}, \"required\": [\"description\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob, \"task\": spawn_subagent,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n#  agent_loop — s09: inject memories + extract after each turn\n# ═══════════════════════════════════════════════════════════\n\nMAX_REACTIVE_RETRIES = 1\n\ndef agent_loop(messages: list):\n    reactive_retries = 0\n    # s09: inject relevant memory content into the current user turn\n    memories_content = load_memories(messages)\n    memory_turn = len(messages) - 1 if messages and isinstance(messages[-1].get(\"content\"), str) else None\n    while True:\n        # s09: rebuild system with current memory index\n        system = build_system()\n\n        # s09: save pre-compression snapshot for accurate memory extraction\n        pre_compress = [m if isinstance(m, dict) else {\"role\": m.get(\"role\",\"\"),\n            \"content\": str(m.get(\"content\",\"\"))} for m in messages]\n\n        # s08: compression pipeline (budget → snip → micro)\n        messages[:] = tool_result_budget(messages)\n        messages[:] = snip_compact(messages)\n        messages[:] = micro_compact(messages)\n\n        if estimate_size(messages) > CONTEXT_LIMIT:\n            print(\"[auto compact]\")\n            messages[:] = compact_history(messages)\n\n        try:\n            request_messages = messages\n            if memories_content and memory_turn is not None and memory_turn < len(messages):\n                request_messages = messages.copy()\n                request_messages[memory_turn] = {\n                    **messages[memory_turn],\n                    \"content\": memories_content + \"\\n\\n\" + messages[memory_turn][\"content\"],\n                }\n            response = client.messages.create(\n                model=MODEL, system=system, messages=request_messages, tools=TOOLS, max_tokens=8000\n            )\n            reactive_retries = 0\n        except Exception as e:\n            if (\"prompt_too_long\" in str(e).lower() or \"too many tokens\" in str(e).lower()) and reactive_retries < MAX_REACTIVE_RETRIES:\n                print(\"[reactive compact]\")\n                messages[:] = reactive_compact(messages)\n                reactive_retries += 1\n                continue\n            raise\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            # s09: extract from pre-compression snapshot for full fidelity\n            extract_memories(pre_compress)\n            consolidate_memories()\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\": continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n            print(str(output)[:200])\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(\"s09: Memory — persistent cross-session knowledge\")\n    print(\"输入问题，回车发送。输入 q 退出。\\n\")\n    history = []\n    while True:\n        try: query = input(\"\\033[36ms09 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt): break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"): break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\": print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s09_memory/memory-overview.svg",
          "alt": "memory overview"
        },
        {
          "src": "/course-assets/s09_memory/memory-subsystems.svg",
          "alt": "memory subsystems"
        }
      ]
    },
    {
      "id": "s10",
      "filename": "s10_system_prompt/code.py",
      "title": "System Prompt",
      "subtitle": "Assembled at Runtime, Never Hardcoded",
      "loc": 166,
      "tools": [
        "bash",
        "read_file",
        "write_file"
      ],
      "newTools": [],
      "coreAddition": "Runtime prompt assembly",
      "keyInsight": "The system prompt is a generated product of policy, tools, skills, and context.",
      "classes": [],
      "functions": [
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 50
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 71
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 97
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 104
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 114
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 124
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 156
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 172
        }
      ],
      "layer": "planning",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns10: System Prompt — Runtime prompt assembly with caching.\n\nRun:  python s10_system_prompt/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s09:\n  - PROMPT_SECTIONS: topic-keyed dict of prompt fragments\n  - assemble_system_prompt(context): select + join sections by real state\n  - get_system_prompt(context): deterministic cache via json.dumps\n  - agent_loop uses get_system_prompt(context) instead of hardcoded SYSTEM\n\nMemory section loads when .memory/MEMORY.md exists (real state, not keywords).\n\"\"\"\n\nimport os, subprocess, json\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n\n# ── Prompt Sections ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    \"\"\"Select and join prompt sections based on current context.\"\"\"\n    sections = []\n\n    # Always loaded — identity, tools, workspace\n    sections.append(PROMPT_SECTIONS[\"identity\"])\n    sections.append(PROMPT_SECTIONS[\"tools\"])\n    sections.append(PROMPT_SECTIONS[\"workspace\"])\n\n    # Conditional — memory loaded when MEMORY.md exists and has content\n    memories = context.get(\"memories\", \"\")\n    if memories:\n        sections.append(f\"Relevant memories:\\n{memories}\")\n\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_key = None\n_last_prompt = None\n\n\ndef get_system_prompt(context: dict) -> str:\n    \"\"\"Cache wrapper — reassemble only when context changes.\n\n    Uses json.dumps for deterministic serialization, not Python's hash()\n    which has process randomization and fails on nested dicts/lists.\n    This cache only avoids redundant string assembly within a process.\n    Real Claude Code additionally protects API-level prompt cache via\n    stable section ordering and SYSTEM_PROMPT_DYNAMIC_BOUNDARY.\n    \"\"\"\n    global _last_context_key, _last_prompt\n    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)\n    if key == _last_context_key and _last_prompt:\n        print(\"  \\033[90m[cache hit] system prompt unchanged\\033[0m\")\n        return _last_prompt\n    _last_context_key = key\n    _last_prompt = assemble_system_prompt(context)\n\n    loaded = [\"identity\", \"tools\", \"workspace\"]\n    if context.get(\"memories\"):\n        loaded.append(\"memory\")\n    print(f\"  \\033[32m[assembled] sections: {', '.join(loaded)}\\033[0m\")\n    return _last_prompt\n\n\n# ── Tools ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n]\n\nTOOL_HANDLERS = {\"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write}\n\n\n# ── Context ──\n\ndef update_context(context: dict, messages: list) -> dict:\n    \"\"\"Derive context from real state: which tools exist, whether memory files exist.\"\"\"\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        content = MEMORY_INDEX.read_text().strip()\n        if content:\n            memories = content\n    return {\n        \"enabled_tools\": list(TOOL_HANDLERS.keys()),\n        \"workspace\": str(WORKDIR),\n        \"memories\": memories,\n    }\n\n\n# ── Agent Loop ──\n\ndef agent_loop(messages: list, context: dict):\n    \"\"\"Main loop — uses assembled system prompt instead of hardcoded SYSTEM.\"\"\"\n    system = get_system_prompt(context)\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=system, messages=messages,\n            tools=TOOLS, max_tokens=8000)\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n            print(str(output)[:200])\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n        # Re-evaluate context and prompt after each tool round\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s10: system prompt — runtime assembly\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = update_context({}, [])\n    while True:\n        try:\n            query = input(\"\\033[36ms10 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s10_system_prompt/system-prompt-overview.svg",
          "alt": "system prompt overview"
        }
      ]
    },
    {
      "id": "s11",
      "filename": "s11_error_recovery/code.py",
      "title": "Error Recovery",
      "subtitle": "Errors Are the Start of a Retry",
      "loc": 287,
      "tools": [
        "bash",
        "read_file",
        "write_file"
      ],
      "newTools": [],
      "coreAddition": "Retry strategy",
      "keyInsight": "A robust harness classifies failures and decides what kind of retry is worthwhile.",
      "classes": [
        {
          "name": "RecoveryState",
          "startLine": 163,
          "endLine": 172
        }
      ],
      "functions": [
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 73
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 86
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 104
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 111
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 121
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 131
        },
        {
          "name": "retry_delay",
          "signature": "def retry_delay(attempt, retry_after=None)",
          "startLine": 173
        },
        {
          "name": "with_retry",
          "signature": "def with_retry(fn, state: RecoveryState)",
          "startLine": 182
        },
        {
          "name": "is_prompt_too_long_error",
          "signature": "def is_prompt_too_long_error(e: Exception)",
          "startLine": 226
        },
        {
          "name": "reactive_compact",
          "signature": "def reactive_compact(messages: list)",
          "startLine": 235
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 249
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 265
        }
      ],
      "layer": "planning",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns11: Error Recovery — three recovery paths + exponential backoff.\n\nRun:  python s11_error_recovery/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s10:\n  - LLM call wrapped in try/except with three recovery paths\n  - Path 1: max_tokens -> escalate 8K->64K (no append on first escalation),\n            then continuation prompt (max 3)\n  - Path 2: prompt_too_long -> reactive compact -> retry (once)\n  - Path 3: 429/529 -> exponential backoff with jitter (max 10),\n            fallback model on consecutive 529\n  - with_retry wrapper for transient errors\n  - RecoveryState tracks escalation / compact / 529 / model\n\nASCII flow:\n  messages -> prompt assembly -> compress+load -> [try] LLM [except] -> tools -> loop\n                                                    |          |\n                                              stop_reason   error type\n                                              max_tokens?   prompt_too_long? -> compact\n                                              escalate /    429/529? -> backoff\n                                              continue      other? -> log + exit\n\"\"\"\n\nimport os, subprocess, time, random, json\nfrom pathlib import Path\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nPRIMARY_MODEL = os.environ[\"MODEL_ID\"]\nFALLBACK_MODEL = os.getenv(\"FALLBACK_MODEL_ID\")\n\n# ── Constants ──\n\nESCALATED_MAX_TOKENS = 64000\nDEFAULT_MAX_TOKENS = 8000\nMAX_RECOVERY_RETRIES = 3\nMAX_RETRIES = 10\nBASE_DELAY_MS = 500\nMAX_CONSECUTIVE_529 = 3\nCONTINUATION_PROMPT = (\n    \"Output token limit hit. Resume directly — \"\n    \"no apology, no recap. Pick up mid-thought.\"\n)\n\n# ── Prompt Assembly (from s10, synced) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    memories = context.get(\"memories\", \"\")\n    if memories:\n        sections.append(f\"Relevant memories:\\n{memories}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_key, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_key, _last_prompt\n    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)\n    if key == _last_context_key and _last_prompt:\n        print(\"  \\033[90m[cache hit] system prompt unchanged\\033[0m\")\n        return _last_prompt\n    _last_context_key = key\n    _last_prompt = assemble_system_prompt(context)\n\n    loaded = [\"identity\", \"tools\", \"workspace\"]\n    if context.get(\"memories\"):\n        loaded.append(\"memory\")\n    print(f\"  \\033[32m[assembled] sections: {', '.join(loaded)}\\033[0m\")\n    return _last_prompt\n\n\n# ── Tools (unchanged) ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n]\n\nTOOL_HANDLERS = {\"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write}\n\n\n# ── Error Recovery (s11 new) ──\n\nclass RecoveryState:\n    \"\"\"Track recovery attempts across the loop.\"\"\"\n    def __init__(self):\n        self.has_escalated = False\n        self.recovery_count = 0\n        self.consecutive_529 = 0\n        self.has_attempted_reactive_compact = False\n        self.current_model = PRIMARY_MODEL\n\n\ndef retry_delay(attempt, retry_after=None):\n    \"\"\"Exponential backoff with jitter. Retry-After takes priority.\"\"\"\n    if retry_after:\n        return retry_after\n    base = min(BASE_DELAY_MS * (2 ** attempt), 32000) / 1000\n    jitter = random.uniform(0, base * 0.25)\n    return base + jitter\n\n\ndef with_retry(fn, state: RecoveryState):\n    \"\"\"Exponential backoff for transient errors (429/529).\n    Non-transient errors are re-raised for the outer handler.\"\"\"\n    for attempt in range(MAX_RETRIES):\n        try:\n            result = fn()\n            state.consecutive_529 = 0\n            return result\n        except Exception as e:\n            name = type(e).__name__\n            msg = str(e).lower()\n\n            # 429 rate limit -> exponential backoff\n            if \"ratelimit\" in name.lower() or \"429\" in msg:\n                delay = retry_delay(attempt)\n                print(f\"  \\033[33m[429 rate limit] retry {attempt+1}/{MAX_RETRIES},\"\n                      f\" wait {delay:.1f}s\\033[0m\")\n                time.sleep(delay)\n                continue\n\n            # 529 overloaded -> exponential backoff + fallback model\n            if \"overloaded\" in name.lower() or \"529\" in msg or \"overloaded\" in msg:\n                state.consecutive_529 += 1\n                if state.consecutive_529 >= MAX_CONSECUTIVE_529:\n                    if FALLBACK_MODEL:\n                        state.current_model = FALLBACK_MODEL\n                        state.consecutive_529 = 0\n                        print(f\"  \\033[31m[529 x{MAX_CONSECUTIVE_529}]\"\n                              f\" switching to {FALLBACK_MODEL}\\033[0m\")\n                    else:\n                        state.consecutive_529 = 0\n                        print(f\"  \\033[31m[529 x{MAX_CONSECUTIVE_529}]\"\n                              f\" no FALLBACK_MODEL_ID configured, continuing retry\\033[0m\")\n                delay = retry_delay(attempt)\n                print(f\"  \\033[33m[529 overloaded] retry {attempt+1}/{MAX_RETRIES},\"\n                      f\" wait {delay:.1f}s\\033[0m\")\n                time.sleep(delay)\n                continue\n\n            # Not transient -> re-raise for outer try/except\n            raise\n    raise RuntimeError(f\"Max retries ({MAX_RETRIES}) exceeded\")\n\n\ndef is_prompt_too_long_error(e: Exception) -> bool:\n    \"\"\"Check whether an API error indicates prompt/context too long.\"\"\"\n    msg = str(e).lower()\n    return ((\"prompt\" in msg and \"long\" in msg)\n            or \"prompt_is_too_long\" in msg\n            or \"context_length_exceeded\" in msg\n            or \"max_context_window\" in msg)\n\n\ndef reactive_compact(messages: list) -> list:\n    \"\"\"Emergency compact — teaching version keeps last N messages.\n    Real CC generates a compact summary via LLM, then retries with\n    the compacted message list. Teaching version simplifies to tail\n    retention since s08/s09 already cover LLM-based compact.\"\"\"\n    print(\"  \\033[31m[reactive compact] trimming to last 5 messages\\033[0m\")\n    tail = messages[-5:]\n    return [{\"role\": \"user\",\n             \"content\": \"[Reactive compact] Earlier conversation trimmed. \"\n                        \"Continue from where you left off.\"}, *tail]\n\n\n# ── Context ──\n\ndef update_context(context: dict, messages: list) -> dict:\n    \"\"\"Derive context from real state: which tools exist, whether memory files exist.\"\"\"\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        content = MEMORY_INDEX.read_text().strip()\n        if content:\n            memories = content\n    return {\n        \"enabled_tools\": list(TOOL_HANDLERS.keys()),\n        \"workspace\": str(WORKDIR),\n        \"memories\": memories,\n    }\n\n\n# ── Agent Loop ──\n\ndef agent_loop(messages: list, context: dict):\n    \"\"\"Main loop with error recovery wrapping LLM calls.\"\"\"\n    system = get_system_prompt(context)\n    state = RecoveryState()\n    max_tokens = DEFAULT_MAX_TOKENS\n\n    while True:\n        # ── LLM call: with_retry handles 429/529, outer handles rest ──\n        try:\n            response = with_retry(\n                lambda mt=max_tokens, mdl=state.current_model:\n                    client.messages.create(\n                        model=mdl, system=system, messages=messages,\n                        tools=TOOLS, max_tokens=mt),\n                state)\n        except Exception as e:\n            # Path 2: prompt_too_long -> reactive compact (once)\n            if is_prompt_too_long_error(e):\n                if not state.has_attempted_reactive_compact:\n                    messages[:] = reactive_compact(messages)\n                    state.has_attempted_reactive_compact = True\n                    continue\n                print(\"  \\033[31m[unrecoverable] still too long after compact\\033[0m\")\n                messages.append({\"role\": \"assistant\", \"content\": [\n                    {\"type\": \"text\",\n                     \"text\": \"[Error] Context too large, cannot continue.\"}]})\n                return\n\n            # Unrecoverable\n            name = type(e).__name__\n            print(f\"  \\033[31m[unrecoverable] {name}: {str(e)[:100]}\\033[0m\")\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\", \"text\": f\"[Error] {name}: {str(e)[:200]}\"}]})\n            return\n\n        # ── Path 1: max_tokens -> escalate or continue ──\n        if response.stop_reason == \"max_tokens\":\n            # First escalation: don't append truncated output, retry same request\n            if not state.has_escalated:\n                max_tokens = ESCALATED_MAX_TOKENS\n                state.has_escalated = True\n                print(f\"  \\033[33m[max_tokens] escalating\"\n                      f\" {DEFAULT_MAX_TOKENS} -> {ESCALATED_MAX_TOKENS}\\033[0m\")\n                continue\n            # 64K still truncated: save truncated output + continuation prompt\n            messages.append({\"role\": \"assistant\", \"content\": response.content})\n            if state.recovery_count < MAX_RECOVERY_RETRIES:\n                messages.append({\"role\": \"user\", \"content\": CONTINUATION_PROMPT})\n                state.recovery_count += 1\n                print(f\"  \\033[33m[max_tokens] continuation\"\n                      f\" {state.recovery_count}/{MAX_RECOVERY_RETRIES}\\033[0m\")\n                continue\n            print(\"  \\033[31m[max_tokens] recovery limit reached\\033[0m\")\n            return\n\n        # Normal completion: append assistant response\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            return\n\n        # ── Tool execution ──\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n            print(str(output)[:200])\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s11: error recovery\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = update_context({}, [])\n    while True:\n        try:\n            query = input(\"\\033[36ms11 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        turn_start = len(history)\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for msg in history[turn_start:]:\n            if msg.get(\"role\") != \"assistant\":\n                continue\n            for block in msg[\"content\"]:\n                if getattr(block, \"type\", None) == \"text\":\n                    print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s11_error_recovery/error-recovery-overview.svg",
          "alt": "error recovery overview"
        }
      ]
    },
    {
      "id": "s12",
      "filename": "s12_task_system/code.py",
      "title": "Task System",
      "subtitle": "Break Big Goals into Small Tasks",
      "loc": 297,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "create_task",
        "list_tasks",
        "get_task",
        "claim_task",
        "complete_task"
      ],
      "newTools": [
        "create_task",
        "list_tasks",
        "get_task",
        "claim_task",
        "complete_task"
      ],
      "coreAddition": "Task board",
      "keyInsight": "A task graph turns vague goals into ordered, observable work.",
      "classes": [
        {
          "name": "Task",
          "startLine": 53,
          "endLine": 61
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 62
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 80
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 84
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 88
        },
        {
          "name": "get_task",
          "signature": "def get_task(task_id: str)",
          "startLine": 93
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 99
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 111
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 126
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 153
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 166
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 178
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 185
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 195
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 205
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 225
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 240
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 247
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 251
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 310
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 326
        }
      ],
      "layer": "collaboration",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns12: Task System — file-persisted task graph with blockedBy dependencies.\n\nRun:  python s12_task_system/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s11:\n  - Task dataclass (id, subject, description, status, owner, blockedBy)\n  - TASKS_DIR = .tasks/ for persistent JSON storage\n  - create_task / save_task / load_task / list_tasks / get_task\n  - can_start: checks blockedBy all completed (missing deps = blocked)\n  - claim_task: set owner + pending -> in_progress\n  - complete_task: set completed + report unblocked downstream\n  - 5 new tools: create_task, list_tasks, get_task, claim_task, complete_task\n\nNote: Teaching code keeps a basic agent loop to stay focused on the task\nsystem. S11's full error recovery (RecoveryState, backoff, escalation,\nreactive compact, fallback model) is omitted — in real CC, tasks.ts and\nwithRetry are independent layers that compose naturally.\n\"\"\"\n\nimport os, subprocess, json, time, random\nfrom pathlib import Path\nfrom dataclasses import dataclass, asdict\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str          # pending | in_progress | completed\n    owner: str | None    # Agent name (multi-agent scenarios)\n    blockedBy: list[str] # Dependency task IDs\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject,\n        description=description,\n        status=\"pending\",\n        owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task(task_id: str) -> str:\n    \"\"\"Return full task details as JSON.\"\"\"\n    task = load_task(task_id)\n    return json.dumps(asdict(task), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    \"\"\"Check if all blockedBy dependencies are completed.\n    Missing dependencies are treated as blocked.\"\"\"\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if not _task_path(d).exists() or load_task(d).status != \"completed\"]\n        return f\"Blocked by: {deps}\"\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress (owner: {owner})\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n        print(f\"  \\033[33m[unblocked] {', '.join(unblocked)}\\033[0m\")\n    return msg\n\n\n# ── Prompt Assembly (from s10, synced) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"create_task, list_tasks, get_task, claim_task, complete_task.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    memories = context.get(\"memories\", \"\")\n    if memories:\n        sections.append(f\"Relevant memories:\\n{memories}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_key, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_key, _last_prompt\n    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)\n    if key == _last_context_key and _last_prompt:\n        return _last_prompt\n    _last_context_key = key\n    _last_prompt = assemble_system_prompt(context)\n    return _last_prompt\n\n\n# ── Tools ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        fp = safe_path(path)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# Task tools\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks. Use create_task to add some.\"\n    lines = []\n    for t in tasks:\n        icon = {\"pending\": \"○\", \"in_progress\": \"●\",\n                \"completed\": \"✓\"}.get(t.status, \"?\")\n        deps = f\" (blockedBy: {', '.join(t.blockedBy)})\" if t.blockedBy else \"\"\n        owner = f\" [{t.owner}]\" if t.owner else \"\"\n        lines.append(f\"  {icon} {t.id}: {t.subject} \"\n                     f\"[{t.status}]{owner}{deps}\")\n    return \"\\n\".join(lines)\n\n\ndef run_get_task(task_id: str) -> str:\n    try:\n        return get_task(task_id)\n    except FileNotFoundError:\n        return f\"Error: Task {task_id} not found\"\n\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\",\n     \"description\": \"Create a new task with optional blockedBy dependencies.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"subject\": {\"type\": \"string\"},\n                          \"description\": {\"type\": \"string\"},\n                          \"blockedBy\": {\"type\": \"array\",\n                                        \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\",\n     \"description\": \"List all tasks with status, owner, and dependencies.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"get_task\",\n     \"description\": \"Get full details of a specific task by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\",\n     \"description\": \"Claim a pending task. Sets owner, changes status to in_progress.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\",\n     \"description\": \"Complete an in-progress task. Reports unblocked downstream tasks.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n    \"get_task\": run_get_task, \"claim_task\": run_claim_task,\n    \"complete_task\": run_complete_task,\n}\n\n\n# ── Context ──\n\ndef update_context(context: dict, messages: list) -> dict:\n    \"\"\"Derive context from real state.\"\"\"\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        content = MEMORY_INDEX.read_text().strip()\n        if content:\n            memories = content\n    return {\n        \"enabled_tools\": list(TOOL_HANDLERS.keys()),\n        \"workspace\": str(WORKDIR),\n        \"memories\": memories,\n    }\n\n\n# ── Agent Loop (simplified, focused on task system) ──\n\ndef agent_loop(messages: list, context: dict):\n    system = get_system_prompt(context)\n    while True:\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=TOOLS, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\",\n                 \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n            print(str(output)[:300])\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s12: task system\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = update_context({}, [])\n    while True:\n        try:\n            query = input(\"\\033[36ms12 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s12_task_system/task-dag.svg",
          "alt": "task dag"
        },
        {
          "src": "/course-assets/s12_task_system/task-system-overview.svg",
          "alt": "task system overview"
        }
      ]
    },
    {
      "id": "s13",
      "filename": "s13_background_tasks/code.py",
      "title": "Background Tasks",
      "subtitle": "Slow Operations Go to the Background",
      "loc": 379,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "create_task",
        "list_tasks",
        "get_task",
        "claim_task",
        "complete_task"
      ],
      "newTools": [],
      "coreAddition": "Background execution",
      "keyInsight": "The agent can keep reasoning while slow work completes elsewhere.",
      "classes": [
        {
          "name": "Task",
          "startLine": 54,
          "endLine": 62
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 63
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 79
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 83
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 87
        },
        {
          "name": "get_task",
          "signature": "def get_task(task_id: str)",
          "startLine": 92
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 98
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 110
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 125
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 152
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 165
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 177
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str, run_in_background: bool = False)",
          "startLine": 184
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 195
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 205
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 225
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 240
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 247
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 251
        },
        {
          "name": "is_slow_operation",
          "signature": "def is_slow_operation(tool_name: str, tool_input: dict)",
          "startLine": 318
        },
        {
          "name": "should_run_background",
          "signature": "def should_run_background(tool_name: str, tool_input: dict)",
          "startLine": 329
        },
        {
          "name": "execute_tool",
          "signature": "def execute_tool(block)",
          "startLine": 336
        },
        {
          "name": "start_background_task",
          "signature": "def start_background_task(block)",
          "startLine": 344
        },
        {
          "name": "collect_background_results",
          "signature": "def collect_background_results()",
          "startLine": 369
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 394
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 410
        }
      ],
      "layer": "concurrency",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns13: Background Tasks — thread-based async execution + notification injection.\n\nRun:  python s13_background_tasks/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s12:\n  - threading.Thread for background execution\n  - background_tasks dict for lifecycle tracking (bg_id, command, status)\n  - background_results dict + threading.Lock for thread-safe storage\n  - should_run_background: model explicit request via run_in_background param\n  - is_slow_operation: fallback heuristic when model doesn't specify\n  - start_background_task: dispatch to daemon thread, return bg task id\n  - collect_background_results: gather completed, return as notifications\n  - agent_loop: slow ops → background + placeholder, inject notifications\n  - Notifications use <task_notification> format, not reused tool_use_id\n\nNote: Teaching code keeps a basic agent loop to stay focused on background\ntasks. S11's full error recovery (RecoveryState, backoff, escalation,\nreactive compact, fallback model) is omitted.\n\"\"\"\n\nimport os, subprocess, json, time, random, threading\nfrom pathlib import Path\nfrom dataclasses import dataclass, asdict\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System (from s12, synced) ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str          # pending | in_progress | completed\n    owner: str | None\n    blockedBy: list[str]\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task(task_id: str) -> str:\n    \"\"\"Return full task details as JSON.\"\"\"\n    task = load_task(task_id)\n    return json.dumps(asdict(task), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    \"\"\"Check if all blockedBy dependencies are completed.\n    Missing dependencies are treated as blocked.\"\"\"\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if not _task_path(d).exists() or load_task(d).status != \"completed\"]\n        return f\"Blocked by: {deps}\"\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress (owner: {owner})\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n        print(f\"  \\033[33m[unblocked] {', '.join(unblocked)}\\033[0m\")\n    return msg\n\n\n# ── Prompt Assembly (from s10, synced) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"create_task, list_tasks, get_task, claim_task, complete_task.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    memories = context.get(\"memories\", \"\")\n    if memories:\n        sections.append(f\"Relevant memories:\\n{memories}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_key, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_key, _last_prompt\n    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)\n    if key == _last_context_key and _last_prompt:\n        return _last_prompt\n    _last_context_key = key\n    _last_prompt = assemble_system_prompt(context)\n    return _last_prompt\n\n\n# ── Tools ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str, run_in_background: bool = False) -> str:\n    # run_in_background is handled by agent_loop dispatch, not here\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        fp = safe_path(path)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# Task tools\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks. Use create_task to add some.\"\n    lines = []\n    for t in tasks:\n        icon = {\"pending\": \"○\", \"in_progress\": \"●\",\n                \"completed\": \"✓\"}.get(t.status, \"?\")\n        deps = f\" (blockedBy: {', '.join(t.blockedBy)})\" if t.blockedBy else \"\"\n        owner = f\" [{t.owner}]\" if t.owner else \"\"\n        lines.append(f\"  {icon} {t.id}: {t.subject} \"\n                     f\"[{t.status}]{owner}{deps}\")\n    return \"\\n\".join(lines)\n\n\ndef run_get_task(task_id: str) -> str:\n    try:\n        return get_task(task_id)\n    except FileNotFoundError:\n        return f\"Error: Task {task_id} not found\"\n\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"command\": {\"type\": \"string\"},\n                          \"run_in_background\": {\"type\": \"boolean\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\",\n     \"description\": \"Create a new task with optional blockedBy dependencies.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"subject\": {\"type\": \"string\"},\n                          \"description\": {\"type\": \"string\"},\n                          \"blockedBy\": {\"type\": \"array\",\n                                        \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\",\n     \"description\": \"List all tasks with status, owner, and dependencies.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"get_task\",\n     \"description\": \"Get full details of a specific task by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\",\n     \"description\": \"Claim a pending task. Sets owner, changes status to in_progress.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\",\n     \"description\": \"Complete an in-progress task. Reports unblocked downstream tasks.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n    \"get_task\": run_get_task, \"claim_task\": run_claim_task,\n    \"complete_task\": run_complete_task,\n}\n\n\n# ── Background Tasks (s13 new) ──\n\n_bg_counter = 0\nbackground_tasks: dict[str, dict] = {}   # bg_id → {tool_use_id, command, status}\nbackground_results: dict[str, str] = {}   # bg_id → output\nbackground_lock = threading.Lock()\n\n\ndef is_slow_operation(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Fallback heuristic: commands likely to take > 30s.\"\"\"\n    if tool_name != \"bash\":\n        return False\n    cmd = tool_input.get(\"command\", \"\").lower()\n    slow_keywords = [\"install\", \"build\", \"test\", \"deploy\", \"compile\",\n                     \"docker build\", \"pip install\", \"npm install\",\n                     \"cargo build\", \"pytest\", \"make\"]\n    return any(kw in cmd for kw in slow_keywords)\n\n\ndef should_run_background(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Model explicit request takes priority; fallback to heuristic.\"\"\"\n    if tool_input.get(\"run_in_background\"):\n        return True\n    return is_slow_operation(tool_name, tool_input)\n\n\ndef execute_tool(block) -> str:\n    \"\"\"Execute a tool call block, return output.\"\"\"\n    handler = TOOL_HANDLERS.get(block.name)\n    if handler:\n        return handler(**block.input)\n    return f\"Unknown tool: {block.name}\"\n\n\ndef start_background_task(block) -> str:\n    \"\"\"Run tool in a daemon thread. Returns background task ID.\"\"\"\n    global _bg_counter\n    _bg_counter += 1\n    bg_id = f\"bg_{_bg_counter:04d}\"\n    cmd = block.input.get(\"command\", block.name)\n\n    def worker():\n        result = execute_tool(block)\n        with background_lock:\n            background_tasks[bg_id][\"status\"] = \"completed\"\n            background_results[bg_id] = result\n\n    with background_lock:\n        background_tasks[bg_id] = {\n            \"tool_use_id\": block.id,\n            \"command\": cmd,\n            \"status\": \"running\",\n        }\n    thread = threading.Thread(target=worker, daemon=True)\n    thread.start()\n    print(f\"  \\033[33m[background] dispatched {bg_id}: {cmd[:40]}\\033[0m\")\n    return bg_id\n\n\ndef collect_background_results() -> list[str]:\n    \"\"\"Collect completed background results as task_notification messages.\"\"\"\n    with background_lock:\n        ready_ids = [bid for bid, task in background_tasks.items()\n                     if task[\"status\"] == \"completed\"]\n    notifications = []\n    for bg_id in ready_ids:\n        with background_lock:\n            task = background_tasks.pop(bg_id)\n            output = background_results.pop(bg_id, \"\")\n        summary = output[:200] if len(output) > 200 else output\n        notifications.append(\n            f\"<task_notification>\\n\"\n            f\"  <task_id>{bg_id}</task_id>\\n\"\n            f\"  <status>completed</status>\\n\"\n            f\"  <command>{task['command']}</command>\\n\"\n            f\"  <summary>{summary}</summary>\\n\"\n            f\"</task_notification>\")\n        print(f\"  \\033[32m[background done] {bg_id}: \"\n              f\"{task['command'][:40]} ({len(output)} chars)\\033[0m\")\n    return notifications\n\n\n# ── Context ──\n\ndef update_context(context: dict, messages: list) -> dict:\n    \"\"\"Derive context from real state.\"\"\"\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        content = MEMORY_INDEX.read_text().strip()\n        if content:\n            memories = content\n    return {\n        \"enabled_tools\": list(TOOL_HANDLERS.keys()),\n        \"workspace\": str(WORKDIR),\n        \"memories\": memories,\n    }\n\n\n# ── Agent Loop (simplified, focused on background tasks) ──\n\ndef agent_loop(messages: list, context: dict):\n    system = get_system_prompt(context)\n    while True:\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=TOOLS, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\",\n                 \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n\n            if should_run_background(block.name, block.input):\n                bg_id = start_background_task(block)\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": f\"[Background task {bg_id} started] \"\n                                           f\"Command: {block.input.get('command', '')}. \"\n                                           f\"Result will be available when complete.\"})\n            else:\n                output = execute_tool(block)\n                print(str(output)[:300])\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": output})\n\n        # Inject tool results + background notifications in one user message\n        user_content = list(results)\n        bg_notifications = collect_background_results()\n        if bg_notifications:\n            for notif in bg_notifications:\n                user_content.append({\"type\": \"text\", \"text\": notif})\n            print(f\"  \\033[32m[inject] {len(bg_notifications)} background \"\n                  f\"notification(s)\\033[0m\")\n        messages.append({\"role\": \"user\", \"content\": user_content})\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s13: background tasks\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = update_context({}, [])\n    while True:\n        try:\n            query = input(\"\\033[36ms13 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s13_background_tasks/background-tasks-overview.svg",
          "alt": "background tasks overview"
        }
      ]
    },
    {
      "id": "s14",
      "filename": "s14_cron_scheduler/code.py",
      "title": "Cron Scheduler",
      "subtitle": "Producing Work on a Schedule",
      "loc": 645,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "create_task",
        "list_tasks",
        "get_task",
        "claim_task",
        "complete_task",
        "schedule_cron",
        "list_crons",
        "cancel_cron"
      ],
      "newTools": [
        "schedule_cron",
        "list_crons",
        "cancel_cron"
      ],
      "coreAddition": "Scheduled task creation",
      "keyInsight": "Recurring work should be created by the harness, not remembered by the model.",
      "classes": [
        {
          "name": "Task",
          "startLine": 56,
          "endLine": 64
        },
        {
          "name": "CronJob",
          "startLine": 352,
          "endLine": 359
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 65
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 81
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 85
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 89
        },
        {
          "name": "get_task",
          "signature": "def get_task(task_id: str)",
          "startLine": 94
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 100
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 112
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 127
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 155
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 168
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 180
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str, run_in_background: bool = False)",
          "startLine": 187
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 198
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 208
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 228
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 243
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 250
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 254
        },
        {
          "name": "is_slow_operation",
          "signature": "def is_slow_operation(tool_name: str, tool_input: dict)",
          "startLine": 266
        },
        {
          "name": "should_run_background",
          "signature": "def should_run_background(tool_name: str, tool_input: dict)",
          "startLine": 277
        },
        {
          "name": "execute_tool",
          "signature": "def execute_tool(block)",
          "startLine": 284
        },
        {
          "name": "start_background_task",
          "signature": "def start_background_task(block)",
          "startLine": 299
        },
        {
          "name": "collect_background_results",
          "signature": "def collect_background_results()",
          "startLine": 323
        },
        {
          "name": "_cron_field_matches",
          "signature": "def _cron_field_matches(field: str, value: int)",
          "startLine": 367
        },
        {
          "name": "cron_matches",
          "signature": "def cron_matches(cron_expr: str, dt: datetime)",
          "startLine": 383
        },
        {
          "name": "_validate_cron_field",
          "signature": "def _validate_cron_field(field: str, lo: int, hi: int)",
          "startLine": 413
        },
        {
          "name": "validate_cron",
          "signature": "def validate_cron(cron_expr: str)",
          "startLine": 448
        },
        {
          "name": "save_durable_jobs",
          "signature": "def save_durable_jobs()",
          "startLine": 462
        },
        {
          "name": "load_durable_jobs",
          "signature": "def load_durable_jobs()",
          "startLine": 468
        },
        {
          "name": "cancel_job",
          "signature": "def cancel_job(job_id: str)",
          "startLine": 507
        },
        {
          "name": "cron_scheduler_loop",
          "signature": "def cron_scheduler_loop()",
          "startLine": 519
        },
        {
          "name": "consume_cron_queue",
          "signature": "def consume_cron_queue()",
          "startLine": 545
        },
        {
          "name": "has_cron_queue",
          "signature": "def has_cron_queue()",
          "startLine": 553
        },
        {
          "name": "run_list_crons",
          "signature": "def run_list_crons()",
          "startLine": 575
        },
        {
          "name": "run_cancel_cron",
          "signature": "def run_cancel_cron(job_id: str)",
          "startLine": 589
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 667
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 686
        },
        {
          "name": "print_latest_assistant_text",
          "signature": "def print_latest_assistant_text(messages: list)",
          "startLine": 744
        },
        {
          "name": "run_agent_turn_locked",
          "signature": "def run_agent_turn_locked(user_query: str | None = None)",
          "startLine": 762
        },
        {
          "name": "queue_processor_loop",
          "signature": "def queue_processor_loop()",
          "startLine": 773
        }
      ],
      "layer": "concurrency",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns14: Cron Scheduler — independent daemon thread + queue processor.\n\nRun:  python s14_cron_scheduler/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s13:\n  - CronJob dataclass (id, cron, prompt, recurring, durable)\n  - cron_matches: 5-field cron expression matching with DOM/DOW OR semantics\n  - schedule_job / cancel_job: register/remove cron jobs (with validation)\n  - cron_scheduler_loop: independent daemon thread, polls every 1s\n  - cron_queue: thread-safe queue, scheduler writes, queue processor delivers\n  - queue_processor_loop: auto-runs agent_loop when cron_queue has work\n  - Durable storage: .scheduled_tasks.json (survives restart)\n  - 3 new tools: schedule_cron, list_crons, cancel_cron\n\nFour layers:\n  1. Scheduler: daemon thread checks time → fires matching jobs\n  2. Queue: cron_queue decouples scheduler from agent loop\n  3. Queue processor: wakes the agent when queued work exists and it is idle\n  4. Consumer: agent_loop consumes queued jobs and injects them into messages\n\"\"\"\n\nimport os, subprocess, json, time, random, threading\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dataclasses import dataclass, asdict\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System (from s12, synced) ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str          # pending | in_progress | completed\n    owner: str | None\n    blockedBy: list[str]\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task(task_id: str) -> str:\n    \"\"\"Return full task details as JSON.\"\"\"\n    task = load_task(task_id)\n    return json.dumps(asdict(task), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    \"\"\"Check if all blockedBy dependencies are completed.\n    Missing dependencies are treated as blocked.\"\"\"\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if not _task_path(d).exists() or load_task(d).status != \"completed\"]\n        return f\"Blocked by: {deps}\"\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress (owner: {owner})\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n        print(f\"  \\033[33m[unblocked] {', '.join(unblocked)}\\033[0m\")\n    return msg\n\n\n# ── Prompt Assembly (from s10, synced) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"create_task, list_tasks, get_task, claim_task, complete_task, \"\n             \"schedule_cron, list_crons, cancel_cron.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    memories = context.get(\"memories\", \"\")\n    if memories:\n        sections.append(f\"Relevant memories:\\n{memories}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_key, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_key, _last_prompt\n    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)\n    if key == _last_context_key and _last_prompt:\n        return _last_prompt\n    _last_context_key = key\n    _last_prompt = assemble_system_prompt(context)\n    return _last_prompt\n\n\n# ── Tools ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str, run_in_background: bool = False) -> str:\n    # run_in_background is handled by agent_loop dispatch, not here\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        fp = safe_path(path)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# Task tools\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks. Use create_task to add some.\"\n    lines = []\n    for t in tasks:\n        icon = {\"pending\": \"○\", \"in_progress\": \"●\",\n                \"completed\": \"✓\"}.get(t.status, \"?\")\n        deps = f\" (blockedBy: {', '.join(t.blockedBy)})\" if t.blockedBy else \"\"\n        owner = f\" [{t.owner}]\" if t.owner else \"\"\n        lines.append(f\"  {icon} {t.id}: {t.subject} \"\n                     f\"[{t.status}]{owner}{deps}\")\n    return \"\\n\".join(lines)\n\n\ndef run_get_task(task_id: str) -> str:\n    try:\n        return get_task(task_id)\n    except FileNotFoundError:\n        return f\"Error: Task {task_id} not found\"\n\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\n\n# ── Background Tasks (from s13, synced) ──\n\n_bg_counter = 0\nbackground_tasks: dict[str, dict] = {}\nbackground_results: dict[str, str] = {}\nbackground_lock = threading.Lock()\n\n\ndef is_slow_operation(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Fallback heuristic: commands likely to take > 30s.\"\"\"\n    if tool_name != \"bash\":\n        return False\n    cmd = tool_input.get(\"command\", \"\").lower()\n    slow_keywords = [\"install\", \"build\", \"test\", \"deploy\", \"compile\",\n                     \"docker build\", \"pip install\", \"npm install\",\n                     \"cargo build\", \"pytest\", \"make\"]\n    return any(kw in cmd for kw in slow_keywords)\n\n\ndef should_run_background(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Model explicit request takes priority; fallback to heuristic.\"\"\"\n    if tool_input.get(\"run_in_background\"):\n        return True\n    return is_slow_operation(tool_name, tool_input)\n\n\ndef execute_tool(block) -> str:\n    \"\"\"Execute a tool call block, return output.\"\"\"\n    handler = {\n        \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n        \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n        \"get_task\": run_get_task, \"claim_task\": run_claim_task,\n        \"complete_task\": run_complete_task,\n        \"schedule_cron\": run_schedule_cron, \"list_crons\": run_list_crons,\n        \"cancel_cron\": run_cancel_cron,\n    }.get(block.name)\n    if handler:\n        return handler(**block.input)\n    return f\"Unknown tool: {block.name}\"\n\n\ndef start_background_task(block) -> str:\n    \"\"\"Run tool in a daemon thread. Returns background task ID.\"\"\"\n    global _bg_counter\n    _bg_counter += 1\n    bg_id = f\"bg_{_bg_counter:04d}\"\n    cmd = block.input.get(\"command\", block.name)\n\n    def worker():\n        result = execute_tool(block)\n        with background_lock:\n            background_tasks[bg_id][\"status\"] = \"completed\"\n            background_results[bg_id] = result\n\n    with background_lock:\n        background_tasks[bg_id] = {\n            \"tool_use_id\": block.id,\n            \"command\": cmd,\n            \"status\": \"running\",\n        }\n    threading.Thread(target=worker, daemon=True).start()\n    print(f\"  \\033[33m[background] dispatched {bg_id}: {cmd[:40]}\\033[0m\")\n    return bg_id\n\n\ndef collect_background_results() -> list[str]:\n    \"\"\"Collect completed background results as task_notification messages.\"\"\"\n    with background_lock:\n        ready_ids = [bid for bid, task in background_tasks.items()\n                     if task[\"status\"] == \"completed\"]\n    notifications = []\n    for bg_id in ready_ids:\n        with background_lock:\n            task = background_tasks.pop(bg_id)\n            output = background_results.pop(bg_id, \"\")\n        summary = output[:200] if len(output) > 200 else output\n        notifications.append(\n            f\"<task_notification>\\n\"\n            f\"  <task_id>{bg_id}</task_id>\\n\"\n            f\"  <status>completed</status>\\n\"\n            f\"  <command>{task['command']}</command>\\n\"\n            f\"  <summary>{summary}</summary>\\n\"\n            f\"</task_notification>\")\n        print(f\"  \\033[32m[background done] {bg_id}: \"\n              f\"{task['command'][:40]} ({len(output)} chars)\\033[0m\")\n    return notifications\n\n\n# ── Cron Scheduler (s14 new) ──\n\nDURABLE_PATH = WORKDIR / \".scheduled_tasks.json\"\n\n\n@dataclass\nclass CronJob:\n    id: str\n    cron: str        # \"0 9 * * *\"\n    prompt: str      # message to inject when fired\n    recurring: bool  # True = recurring, False = one-shot\n    durable: bool    # True = persist to disk\n\n\nscheduled_jobs: dict[str, CronJob] = {}\ncron_queue: list[CronJob] = []\ncron_lock = threading.Lock()\nagent_lock = threading.Lock()\n_last_fired: dict[str, str] = {}  # job_id → \"YYYY-MM-DD HH:MM\"\n\n\ndef _cron_field_matches(field: str, value: int) -> bool:\n    \"\"\"Match a single cron field against a value.\"\"\"\n    if field == \"*\":\n        return True\n    if field.startswith(\"*/\"):\n        step = int(field[2:])\n        return step > 0 and value % step == 0\n    if \",\" in field:\n        return any(_cron_field_matches(f.strip(), value)\n                   for f in field.split(\",\"))\n    if \"-\" in field:\n        lo, hi = field.split(\"-\", 1)\n        return int(lo) <= value <= int(hi)\n    return value == int(field)\n\n\ndef cron_matches(cron_expr: str, dt: datetime) -> bool:\n    \"\"\"Check if a 5-field cron expression matches the given datetime.\n    Standard cron semantics: DOM and DOW use OR when both are constrained.\"\"\"\n    fields = cron_expr.strip().split()\n    if len(fields) != 5:\n        return False\n    minute, hour, dom, month, dow = fields\n    dow_val = (dt.weekday() + 1) % 7  # Python Monday=0 → cron Sunday=0\n\n    m = _cron_field_matches(minute, dt.minute)\n    h = _cron_field_matches(hour, dt.hour)\n    dom_ok = _cron_field_matches(dom, dt.day)\n    month_ok = _cron_field_matches(month, dt.month)\n    dow_ok = _cron_field_matches(dow, dow_val)\n\n    # Minute, hour, month must all match\n    if not (m and h and month_ok):\n        return False\n    # DOM and DOW: if both constrained, either matching is enough (OR)\n    dom_unconstrained = dom == \"*\"\n    dow_unconstrained = dow == \"*\"\n    if dom_unconstrained and dow_unconstrained:\n        return True\n    if dom_unconstrained:\n        return dow_ok\n    if dow_unconstrained:\n        return dom_ok\n    return dom_ok or dow_ok\n\n\ndef _validate_cron_field(field: str, lo: int, hi: int) -> str | None:\n    \"\"\"Validate a single cron field value is within [lo, hi].\"\"\"\n    if field == \"*\":\n        return None\n    if field.startswith(\"*/\"):\n        step_str = field[2:]\n        if not step_str.isdigit():\n            return f\"Invalid step: {field}\"\n        step = int(step_str)\n        if step <= 0:\n            return f\"Step must be > 0: {field}\"\n        return None\n    if \",\" in field:\n        for part in field.split(\",\"):\n            err = _validate_cron_field(part.strip(), lo, hi)\n            if err: return err\n        return None\n    if \"-\" in field:\n        parts = field.split(\"-\", 1)\n        if not parts[0].isdigit() or not parts[1].isdigit():\n            return f\"Invalid range: {field}\"\n        a, b = int(parts[0]), int(parts[1])\n        if a < lo or a > hi or b < lo or b > hi:\n            return f\"Range {field} out of bounds [{lo}-{hi}]\"\n        if a > b:\n            return f\"Range start > end: {field}\"\n        return None\n    if not field.isdigit():\n        return f\"Invalid field: {field}\"\n    val = int(field)\n    if val < lo or val > hi:\n        return f\"Value {val} out of bounds [{lo}-{hi}]\"\n    return None\n\n\ndef validate_cron(cron_expr: str) -> str | None:\n    \"\"\"Validate a cron expression. Returns error message or None.\"\"\"\n    fields = cron_expr.strip().split()\n    if len(fields) != 5:\n        return f\"Expected 5 fields, got {len(fields)}\"\n    bounds = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)]\n    names = [\"minute\", \"hour\", \"day-of-month\", \"month\", \"day-of-week\"]\n    for i, (field, (lo, hi), name) in enumerate(zip(fields, bounds, names)):\n        err = _validate_cron_field(field, lo, hi)\n        if err:\n            return f\"{name}: {err}\"\n    return None\n\n\ndef save_durable_jobs():\n    \"\"\"Persist durable jobs to .scheduled_tasks.json.\"\"\"\n    durable = [asdict(j) for j in scheduled_jobs.values() if j.durable]\n    DURABLE_PATH.write_text(json.dumps(durable, indent=2))\n\n\ndef load_durable_jobs():\n    \"\"\"Load durable jobs from disk on startup.\"\"\"\n    if not DURABLE_PATH.exists():\n        return\n    try:\n        jobs = json.loads(DURABLE_PATH.read_text())\n        for j in jobs:\n            job = CronJob(**j)\n            err = validate_cron(job.cron)\n            if err:\n                print(f\"  \\033[31m[cron] skipping invalid job {job.id}: {err}\\033[0m\")\n                continue\n            scheduled_jobs[job.id] = job\n        valid = [j for j in jobs if j[\"id\"] in scheduled_jobs]\n        if valid:\n            print(f\"  \\033[35m[cron] loaded {len(valid)} durable job(s)\\033[0m\")\n    except Exception:\n        pass\n\n\ndef schedule_job(cron: str, prompt: str, recurring: bool = True,\n                 durable: bool = True) -> CronJob | str:\n    \"\"\"Register a new cron job. Returns CronJob or error string.\"\"\"\n    err = validate_cron(cron)\n    if err:\n        return err\n    job = CronJob(\n        id=f\"cron_{random.randint(0, 999999):06d}\",\n        cron=cron, prompt=prompt,\n        recurring=recurring, durable=durable,\n    )\n    with cron_lock:\n        scheduled_jobs[job.id] = job\n    if durable:\n        save_durable_jobs()\n    print(f\"  \\033[35m[cron register] {job.id} '{cron}' → {prompt[:40]}\\033[0m\")\n    return job\n\n\ndef cancel_job(job_id: str) -> str:\n    \"\"\"Cancel a cron job.\"\"\"\n    with cron_lock:\n        job = scheduled_jobs.pop(job_id, None)\n    if not job:\n        return f\"Job {job_id} not found\"\n    if job.durable:\n        save_durable_jobs()\n    print(f\"  \\033[31m[cron cancel] {job_id}\\033[0m\")\n    return f\"Cancelled {job_id}\"\n\n\ndef cron_scheduler_loop():\n    \"\"\"Independent daemon thread: poll every 1s, fire matching jobs.\n    Individual job errors are caught to prevent one bad job from\n    killing the entire scheduler thread.\"\"\"\n    while True:\n        time.sleep(1)\n        now = datetime.now()\n        # Date-aware marker prevents daily jobs from skipping on day 2+\n        minute_marker = now.strftime(\"%Y-%m-%d %H:%M\")\n        with cron_lock:\n            for job in list(scheduled_jobs.values()):\n                try:\n                    if cron_matches(job.cron, now):\n                        if _last_fired.get(job.id) != minute_marker:\n                            cron_queue.append(job)\n                            _last_fired[job.id] = minute_marker\n                            print(f\"  \\033[35m[cron fire] {job.id} → \"\n                                  f\"{job.prompt[:40]}\\033[0m\")\n                        if not job.recurring:\n                            scheduled_jobs.pop(job.id, None)\n                            if job.durable:\n                                save_durable_jobs()\n                except Exception as e:\n                    print(f\"  \\033[31m[cron error] {job.id}: {e}\\033[0m\")\n\n\ndef consume_cron_queue() -> list[CronJob]:\n    \"\"\"Consume fired jobs from cron_queue (called by agent_loop).\"\"\"\n    with cron_lock:\n        fired = list(cron_queue)\n        cron_queue.clear()\n    return fired\n\n\ndef has_cron_queue() -> bool:\n    \"\"\"Return whether fired cron jobs are waiting to be delivered.\"\"\"\n    with cron_lock:\n        return bool(cron_queue)\n\n\n# Load durable jobs on startup, then start scheduler thread\nload_durable_jobs()\nthreading.Thread(target=cron_scheduler_loop, daemon=True).start()\nprint(\"  \\033[35m[cron] scheduler thread started\\033[0m\")\n\n\n# ── Cron Tools ──\n\ndef run_schedule_cron(cron: str, prompt: str,\n                      recurring: bool = True, durable: bool = True) -> str:\n    result = schedule_job(cron, prompt, recurring, durable)\n    if isinstance(result, str):\n        return f\"Error: {result}\"\n    return f\"Scheduled {result.id}: '{cron}' → {prompt}\"\n\n\ndef run_list_crons() -> str:\n    with cron_lock:\n        jobs = list(scheduled_jobs.values())\n    if not jobs:\n        return \"No cron jobs. Use schedule_cron to add one.\"\n    lines = []\n    for j in jobs:\n        tag = \"recurring\" if j.recurring else \"one-shot\"\n        dur = \"durable\" if j.durable else \"session\"\n        lines.append(f\"  {j.id}: '{j.cron}' → {j.prompt[:40]} \"\n                     f\"[{tag}, {dur}]\")\n    return \"\\n\".join(lines)\n\n\ndef run_cancel_cron(job_id: str) -> str:\n    return cancel_job(job_id)\n\n\n# ── Tool Definitions ──\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"command\": {\"type\": \"string\"},\n                          \"run_in_background\": {\"type\": \"boolean\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\",\n     \"description\": \"Create a new task with optional blockedBy dependencies.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"subject\": {\"type\": \"string\"},\n                          \"description\": {\"type\": \"string\"},\n                          \"blockedBy\": {\"type\": \"array\",\n                                        \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\",\n     \"description\": \"List all tasks with status, owner, and dependencies.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"get_task\",\n     \"description\": \"Get full details of a specific task by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\",\n     \"description\": \"Claim a pending task. Sets owner, changes status to in_progress.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\",\n     \"description\": \"Complete an in-progress task. Reports unblocked downstream tasks.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"schedule_cron\",\n     \"description\": \"Schedule a cron job. cron is 5-field: min hour dom month dow.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"cron\": {\"type\": \"string\",\n                                   \"description\": \"5-field cron expression\"},\n                          \"prompt\": {\"type\": \"string\",\n                                     \"description\": \"Message to inject when fired\"},\n                          \"recurring\": {\"type\": \"boolean\",\n                                        \"description\": \"True=recurring, False=one-shot\"},\n                          \"durable\": {\"type\": \"boolean\",\n                                      \"description\": \"True=persist to disk\"}},\n                      \"required\": [\"cron\", \"prompt\"]}},\n    {\"name\": \"list_crons\",\n     \"description\": \"List all registered cron jobs.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"cancel_cron\",\n     \"description\": \"Cancel a cron job by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"job_id\": {\"type\": \"string\"}},\n                      \"required\": [\"job_id\"]}},\n]\n\n\n# ── Context ──\n\ndef update_context(context: dict, messages: list) -> dict:\n    \"\"\"Derive context from real state.\"\"\"\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        content = MEMORY_INDEX.read_text().strip()\n        if content:\n            memories = content\n    return {\n        \"enabled_tools\": [t[\"name\"] for t in TOOLS],\n        \"workspace\": str(WORKDIR),\n        \"memories\": memories,\n    }\n\n\n# ── Agent Loop (simplified, focused on cron scheduler) ──\n# Teaching code keeps a basic agent loop. S11's full error recovery is omitted.\n# cron_scheduler_loop produces work; queue_processor_loop wakes this loop when\n# queued work exists and no other agent turn is running.\n\ndef agent_loop(messages: list, context: dict) -> dict:\n    system = get_system_prompt(context)\n    while True:\n        # Layer 4: consume fired cron jobs → inject as messages\n        fired = consume_cron_queue()\n        for job in fired:\n            messages.append({\"role\": \"user\",\n                             \"content\": f\"[Scheduled] {job.prompt}\"})\n            print(f\"  \\033[35m[inject cron] {job.prompt[:50]}\\033[0m\")\n\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=TOOLS, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\",\n                 \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return context\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return context\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n\n            if should_run_background(block.name, block.input):\n                bg_id = start_background_task(block)\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": f\"[Background task {bg_id} started] \"\n                                           f\"Result will be available when complete.\"})\n            else:\n                output = execute_tool(block)\n                print(str(output)[:300])\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": output})\n\n        # Merge background tool results + notifications into one user message\n        user_content = list(results)\n        bg_notifications = collect_background_results()\n        if bg_notifications:\n            for notif in bg_notifications:\n                user_content.append({\"type\": \"text\", \"text\": notif})\n        messages.append({\"role\": \"user\", \"content\": user_content})\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nsession_history: list = []\nsession_context = update_context({}, [])\n\n\ndef print_latest_assistant_text(messages: list):\n    \"\"\"Print text blocks from the latest assistant message.\"\"\"\n    if not messages:\n        return\n    msg = messages[-1]\n    if not isinstance(msg, dict) or msg.get(\"role\") != \"assistant\":\n        return\n    content = msg.get(\"content\", \"\")\n    if isinstance(content, str):\n        print(content)\n        return\n    for block in content:\n        if getattr(block, \"type\", None) == \"text\":\n            print(block.text)\n        elif isinstance(block, dict) and block.get(\"type\") == \"text\":\n            print(block.get(\"text\", \"\"))\n\n\ndef run_agent_turn_locked(user_query: str | None = None):\n    \"\"\"Run one agent turn. Caller must hold agent_lock.\"\"\"\n    global session_context\n    if user_query is not None:\n        session_history.append({\"role\": \"user\", \"content\": user_query})\n    session_context = agent_loop(session_history, session_context)\n    session_context = update_context(session_context, session_history)\n    print_latest_assistant_text(session_history)\n    print()\n\n\ndef queue_processor_loop():\n    \"\"\"Auto-deliver fired cron jobs when the agent is idle.\"\"\"\n    global session_context\n    while True:\n        time.sleep(0.2)\n        if not has_cron_queue():\n            continue\n        if not agent_lock.acquire(blocking=False):\n            continue\n        try:\n            if not has_cron_queue():\n                continue\n            print(\"\\n  \\033[35m[queue processor] delivering scheduled work\\033[0m\")\n            run_agent_turn_locked()\n        finally:\n            agent_lock.release()\n\n\nif __name__ == \"__main__\":\n    print(\"s14: cron scheduler\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    threading.Thread(target=queue_processor_loop, daemon=True).start()\n    print(\"  \\033[35m[queue processor] started\\033[0m\")\n    while True:\n        try:\n            query = input(\"\\033[36ms14 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        with agent_lock:\n            run_agent_turn_locked(query)\n",
      "images": [
        {
          "src": "/course-assets/s14_cron_scheduler/cron-scheduler-overview.svg",
          "alt": "cron scheduler overview"
        }
      ]
    },
    {
      "id": "s15",
      "filename": "s15_agent_teams/code.py",
      "title": "Agent Teams",
      "subtitle": "One Agent Isn't Enough, Form a Team",
      "loc": 745,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "send_message",
        "create_task",
        "list_tasks",
        "get_task",
        "claim_task",
        "complete_task",
        "schedule_cron",
        "list_crons",
        "cancel_cron",
        "spawn_teammate",
        "check_inbox"
      ],
      "newTools": [
        "send_message",
        "spawn_teammate",
        "check_inbox"
      ],
      "coreAddition": "Teammate mailboxes",
      "keyInsight": "Persistent teammates let work continue in parallel without stuffing every thought into one context.",
      "classes": [
        {
          "name": "Task",
          "startLine": 54,
          "endLine": 62
        },
        {
          "name": "CronJob",
          "startLine": 353,
          "endLine": 360
        },
        {
          "name": "MessageBus",
          "startLine": 595,
          "endLine": 620
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 63
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 79
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 83
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 87
        },
        {
          "name": "get_task",
          "signature": "def get_task(task_id: str)",
          "startLine": 92
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 98
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 110
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 125
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 154
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 167
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 179
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str, run_in_background: bool = False)",
          "startLine": 186
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 197
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 207
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 227
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 242
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 249
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 253
        },
        {
          "name": "is_slow_operation",
          "signature": "def is_slow_operation(tool_name: str, tool_input: dict)",
          "startLine": 265
        },
        {
          "name": "should_run_background",
          "signature": "def should_run_background(tool_name: str, tool_input: dict)",
          "startLine": 276
        },
        {
          "name": "execute_tool",
          "signature": "def execute_tool(block)",
          "startLine": 283
        },
        {
          "name": "start_background_task",
          "signature": "def start_background_task(block)",
          "startLine": 300
        },
        {
          "name": "collect_background_results",
          "signature": "def collect_background_results()",
          "startLine": 324
        },
        {
          "name": "_cron_field_matches",
          "signature": "def _cron_field_matches(field: str, value: int)",
          "startLine": 367
        },
        {
          "name": "cron_matches",
          "signature": "def cron_matches(cron_expr: str, dt: datetime)",
          "startLine": 383
        },
        {
          "name": "_validate_cron_field",
          "signature": "def _validate_cron_field(field: str, lo: int, hi: int)",
          "startLine": 413
        },
        {
          "name": "validate_cron",
          "signature": "def validate_cron(cron_expr: str)",
          "startLine": 448
        },
        {
          "name": "save_durable_jobs",
          "signature": "def save_durable_jobs()",
          "startLine": 462
        },
        {
          "name": "load_durable_jobs",
          "signature": "def load_durable_jobs()",
          "startLine": 468
        },
        {
          "name": "cancel_job",
          "signature": "def cancel_job(job_id: str)",
          "startLine": 507
        },
        {
          "name": "cron_scheduler_loop",
          "signature": "def cron_scheduler_loop()",
          "startLine": 519
        },
        {
          "name": "consume_cron_queue",
          "signature": "def consume_cron_queue()",
          "startLine": 545
        },
        {
          "name": "run_list_crons",
          "signature": "def run_list_crons()",
          "startLine": 569
        },
        {
          "name": "run_cancel_cron",
          "signature": "def run_cancel_cron(job_id: str)",
          "startLine": 583
        },
        {
          "name": "spawn_teammate_thread",
          "signature": "def spawn_teammate_thread(name: str, role: str, prompt: str)",
          "startLine": 629
        },
        {
          "name": "run_spawn_teammate",
          "signature": "def run_spawn_teammate(name: str, role: str, prompt: str)",
          "startLine": 717
        },
        {
          "name": "run_send_message",
          "signature": "def run_send_message(to: str, content: str)",
          "startLine": 721
        },
        {
          "name": "run_check_inbox",
          "signature": "def run_check_inbox()",
          "startLine": 726
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 828
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 847
        }
      ],
      "layer": "collaboration",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns15: Agent Teams — MessageBus + spawn_teammate_thread + inbox injection.\n\nRun:  python s15_agent_teams/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s14:\n  - MessageBus class: file-based mailboxes (.mailboxes/*.jsonl)\n  - spawn_teammate_thread: creates teammate in background thread\n  - Teammate runs own simplified agent_loop (bash, read, write, send_message)\n  - Lead tools: spawn_teammate, send_message, check_inbox (3 new)\n  - Lead inbox: teammate messages injected into history (not just printed)\n  - Teaching version: teammates limited to 10 rounds (real CC uses idle loop)\n\nASCII flow:\n  Lead: cron_queue → messages → prompt → LLM → TOOLS ────→ loop\n                ↑                     ↓                        |\n                └── inbox ← MessageBus ← teammate.send_message ←┘\n  Teammate: inbox → LLM → bash/read/write/send → loop (max 10 turns)\n\"\"\"\n\nimport os, subprocess, json, time, random, threading\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dataclasses import dataclass, asdict\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System (from s12, synced) ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str          # pending | in_progress | completed\n    owner: str | None\n    blockedBy: list[str]\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task(task_id: str) -> str:\n    \"\"\"Return full task details as JSON.\"\"\"\n    task = load_task(task_id)\n    return json.dumps(asdict(task), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    \"\"\"Check if all blockedBy dependencies are completed.\n    Missing dependencies are treated as blocked.\"\"\"\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if not _task_path(d).exists() or load_task(d).status != \"completed\"]\n        return f\"Blocked by: {deps}\"\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress (owner: {owner})\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n        print(f\"  \\033[33m[unblocked] {', '.join(unblocked)}\\033[0m\")\n    return msg\n\n\n# ── Prompt Assembly (from s10, synced) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"get_task, create_task, list_tasks, claim_task, complete_task, \"\n             \"schedule_cron, list_crons, cancel_cron, \"\n             \"spawn_teammate, send_message, check_inbox.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    memories = context.get(\"memories\", \"\")\n    if memories:\n        sections.append(f\"Relevant memories:\\n{memories}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_key, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_key, _last_prompt\n    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)\n    if key == _last_context_key and _last_prompt:\n        return _last_prompt\n    _last_context_key = key\n    _last_prompt = assemble_system_prompt(context)\n    return _last_prompt\n\n\n# ── Tools ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str, run_in_background: bool = False) -> str:\n    # run_in_background is handled by agent_loop dispatch, not here\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        fp = safe_path(path)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# Task tools\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks. Use create_task to add some.\"\n    lines = []\n    for t in tasks:\n        icon = {\"pending\": \"○\", \"in_progress\": \"●\",\n                \"completed\": \"✓\"}.get(t.status, \"?\")\n        deps = f\" (blockedBy: {', '.join(t.blockedBy)})\" if t.blockedBy else \"\"\n        owner = f\" [{t.owner}]\" if t.owner else \"\"\n        lines.append(f\"  {icon} {t.id}: {t.subject} \"\n                     f\"[{t.status}]{owner}{deps}\")\n    return \"\\n\".join(lines)\n\n\ndef run_get_task(task_id: str) -> str:\n    try:\n        return get_task(task_id)\n    except FileNotFoundError:\n        return f\"Error: Task {task_id} not found\"\n\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\n\n# ── Background Tasks (from s13, synced) ──\n\n_bg_counter = 0\nbackground_tasks: dict[str, dict] = {}\nbackground_results: dict[str, str] = {}\nbackground_lock = threading.Lock()\n\n\ndef is_slow_operation(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Fallback heuristic: commands likely to take > 30s.\"\"\"\n    if tool_name != \"bash\":\n        return False\n    cmd = tool_input.get(\"command\", \"\").lower()\n    slow_keywords = [\"install\", \"build\", \"test\", \"deploy\", \"compile\",\n                     \"docker build\", \"pip install\", \"npm install\",\n                     \"cargo build\", \"pytest\", \"make\"]\n    return any(kw in cmd for kw in slow_keywords)\n\n\ndef should_run_background(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Model explicit request takes priority; fallback to heuristic.\"\"\"\n    if tool_input.get(\"run_in_background\"):\n        return True\n    return is_slow_operation(tool_name, tool_input)\n\n\ndef execute_tool(block) -> str:\n    \"\"\"Execute a tool call block, return output.\"\"\"\n    handler = {\n        \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n        \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n        \"get_task\": run_get_task, \"claim_task\": run_claim_task,\n        \"complete_task\": run_complete_task,\n        \"schedule_cron\": run_schedule_cron, \"list_crons\": run_list_crons,\n        \"cancel_cron\": run_cancel_cron,\n        \"spawn_teammate\": run_spawn_teammate,\n        \"send_message\": run_send_message, \"check_inbox\": run_check_inbox,\n    }.get(block.name)\n    if handler:\n        return handler(**block.input)\n    return f\"Unknown tool: {block.name}\"\n\n\ndef start_background_task(block) -> str:\n    \"\"\"Run tool in a daemon thread. Returns background task ID.\"\"\"\n    global _bg_counter\n    _bg_counter += 1\n    bg_id = f\"bg_{_bg_counter:04d}\"\n    cmd = block.input.get(\"command\", block.name)\n\n    def worker():\n        result = execute_tool(block)\n        with background_lock:\n            background_tasks[bg_id][\"status\"] = \"completed\"\n            background_results[bg_id] = result\n\n    with background_lock:\n        background_tasks[bg_id] = {\n            \"tool_use_id\": block.id,\n            \"command\": cmd,\n            \"status\": \"running\",\n        }\n    threading.Thread(target=worker, daemon=True).start()\n    print(f\"  \\033[33m[background] dispatched {bg_id}: {cmd[:40]}\\033[0m\")\n    return bg_id\n\n\ndef collect_background_results() -> list[str]:\n    \"\"\"Collect completed background results as task_notification messages.\"\"\"\n    with background_lock:\n        ready_ids = [bid for bid, task in background_tasks.items()\n                     if task[\"status\"] == \"completed\"]\n    notifications = []\n    for bg_id in ready_ids:\n        with background_lock:\n            task = background_tasks.pop(bg_id)\n            output = background_results.pop(bg_id, \"\")\n        summary = output[:200] if len(output) > 200 else output\n        notifications.append(\n            f\"<task_notification>\\n\"\n            f\"  <task_id>{bg_id}</task_id>\\n\"\n            f\"  <status>completed</status>\\n\"\n            f\"  <command>{task['command']}</command>\\n\"\n            f\"  <summary>{summary}</summary>\\n\"\n            f\"</task_notification>\")\n        print(f\"  \\033[32m[background done] {bg_id}: \"\n              f\"{task['command'][:40]} ({len(output)} chars)\\033[0m\")\n    return notifications\n\n\n# ── Cron Scheduler (from s14, synced) ──\n\nDURABLE_PATH = WORKDIR / \".scheduled_tasks.json\"\n\n\n@dataclass\nclass CronJob:\n    id: str\n    cron: str        # \"0 9 * * *\"\n    prompt: str      # message to inject when fired\n    recurring: bool  # True = recurring, False = one-shot\n    durable: bool    # True = persist to disk\n\n\nscheduled_jobs: dict[str, CronJob] = {}\ncron_queue: list[CronJob] = []\ncron_lock = threading.Lock()\n_last_fired: dict[str, str] = {}  # job_id → \"YYYY-MM-DD HH:MM\"\n\n\ndef _cron_field_matches(field: str, value: int) -> bool:\n    \"\"\"Match a single cron field against a value.\"\"\"\n    if field == \"*\":\n        return True\n    if field.startswith(\"*/\"):\n        step = int(field[2:])\n        return step > 0 and value % step == 0\n    if \",\" in field:\n        return any(_cron_field_matches(f.strip(), value)\n                   for f in field.split(\",\"))\n    if \"-\" in field:\n        lo, hi = field.split(\"-\", 1)\n        return int(lo) <= value <= int(hi)\n    return value == int(field)\n\n\ndef cron_matches(cron_expr: str, dt: datetime) -> bool:\n    \"\"\"Check if a 5-field cron expression matches the given datetime.\n    Standard cron semantics: DOM and DOW use OR when both are constrained.\"\"\"\n    fields = cron_expr.strip().split()\n    if len(fields) != 5:\n        return False\n    minute, hour, dom, month, dow = fields\n    dow_val = (dt.weekday() + 1) % 7  # Python Monday=0 → cron Sunday=0\n\n    m = _cron_field_matches(minute, dt.minute)\n    h = _cron_field_matches(hour, dt.hour)\n    dom_ok = _cron_field_matches(dom, dt.day)\n    month_ok = _cron_field_matches(month, dt.month)\n    dow_ok = _cron_field_matches(dow, dow_val)\n\n    # Minute, hour, month must all match\n    if not (m and h and month_ok):\n        return False\n    # DOM and DOW: if both constrained, either matching is enough (OR)\n    dom_unconstrained = dom == \"*\"\n    dow_unconstrained = dow == \"*\"\n    if dom_unconstrained and dow_unconstrained:\n        return True\n    if dom_unconstrained:\n        return dow_ok\n    if dow_unconstrained:\n        return dom_ok\n    return dom_ok or dow_ok\n\n\ndef _validate_cron_field(field: str, lo: int, hi: int) -> str | None:\n    \"\"\"Validate a single cron field value is within [lo, hi].\"\"\"\n    if field == \"*\":\n        return None\n    if field.startswith(\"*/\"):\n        step_str = field[2:]\n        if not step_str.isdigit():\n            return f\"Invalid step: {field}\"\n        step = int(step_str)\n        if step <= 0:\n            return f\"Step must be > 0: {field}\"\n        return None\n    if \",\" in field:\n        for part in field.split(\",\"):\n            err = _validate_cron_field(part.strip(), lo, hi)\n            if err: return err\n        return None\n    if \"-\" in field:\n        parts = field.split(\"-\", 1)\n        if not parts[0].isdigit() or not parts[1].isdigit():\n            return f\"Invalid range: {field}\"\n        a, b = int(parts[0]), int(parts[1])\n        if a < lo or a > hi or b < lo or b > hi:\n            return f\"Range {field} out of bounds [{lo}-{hi}]\"\n        if a > b:\n            return f\"Range start > end: {field}\"\n        return None\n    if not field.isdigit():\n        return f\"Invalid field: {field}\"\n    val = int(field)\n    if val < lo or val > hi:\n        return f\"Value {val} out of bounds [{lo}-{hi}]\"\n    return None\n\n\ndef validate_cron(cron_expr: str) -> str | None:\n    \"\"\"Validate a cron expression. Returns error message or None.\"\"\"\n    fields = cron_expr.strip().split()\n    if len(fields) != 5:\n        return f\"Expected 5 fields, got {len(fields)}\"\n    bounds = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)]\n    names = [\"minute\", \"hour\", \"day-of-month\", \"month\", \"day-of-week\"]\n    for i, (field, (lo, hi), name) in enumerate(zip(fields, bounds, names)):\n        err = _validate_cron_field(field, lo, hi)\n        if err:\n            return f\"{name}: {err}\"\n    return None\n\n\ndef save_durable_jobs():\n    \"\"\"Persist durable jobs to .scheduled_tasks.json.\"\"\"\n    durable = [asdict(j) for j in scheduled_jobs.values() if j.durable]\n    DURABLE_PATH.write_text(json.dumps(durable, indent=2))\n\n\ndef load_durable_jobs():\n    \"\"\"Load durable jobs from disk on startup.\"\"\"\n    if not DURABLE_PATH.exists():\n        return\n    try:\n        jobs = json.loads(DURABLE_PATH.read_text())\n        for j in jobs:\n            job = CronJob(**j)\n            err = validate_cron(job.cron)\n            if err:\n                print(f\"  \\033[31m[cron] skipping invalid job {job.id}: {err}\\033[0m\")\n                continue\n            scheduled_jobs[job.id] = job\n        valid = [j for j in jobs if j[\"id\"] in scheduled_jobs]\n        if valid:\n            print(f\"  \\033[35m[cron] loaded {len(valid)} durable job(s)\\033[0m\")\n    except Exception:\n        pass\n\n\ndef schedule_job(cron: str, prompt: str, recurring: bool = True,\n                 durable: bool = True) -> CronJob | str:\n    \"\"\"Register a new cron job. Returns CronJob or error string.\"\"\"\n    err = validate_cron(cron)\n    if err:\n        return err\n    job = CronJob(\n        id=f\"cron_{random.randint(0, 999999):06d}\",\n        cron=cron, prompt=prompt,\n        recurring=recurring, durable=durable,\n    )\n    with cron_lock:\n        scheduled_jobs[job.id] = job\n    if durable:\n        save_durable_jobs()\n    print(f\"  \\033[35m[cron register] {job.id} '{cron}' → {prompt[:40]}\\033[0m\")\n    return job\n\n\ndef cancel_job(job_id: str) -> str:\n    \"\"\"Cancel a cron job.\"\"\"\n    with cron_lock:\n        job = scheduled_jobs.pop(job_id, None)\n    if not job:\n        return f\"Job {job_id} not found\"\n    if job.durable:\n        save_durable_jobs()\n    print(f\"  \\033[31m[cron cancel] {job_id}\\033[0m\")\n    return f\"Cancelled {job_id}\"\n\n\ndef cron_scheduler_loop():\n    \"\"\"Independent daemon thread: poll every 1s, fire matching jobs.\n    Individual job errors are caught to prevent one bad job from\n    killing the entire scheduler thread.\"\"\"\n    while True:\n        time.sleep(1)\n        now = datetime.now()\n        # Date-aware marker prevents daily jobs from skipping on day 2+\n        minute_marker = now.strftime(\"%Y-%m-%d %H:%M\")\n        with cron_lock:\n            for job in list(scheduled_jobs.values()):\n                try:\n                    if cron_matches(job.cron, now):\n                        if _last_fired.get(job.id) != minute_marker:\n                            cron_queue.append(job)\n                            _last_fired[job.id] = minute_marker\n                            print(f\"  \\033[35m[cron fire] {job.id} → \"\n                                  f\"{job.prompt[:40]}\\033[0m\")\n                        if not job.recurring:\n                            scheduled_jobs.pop(job.id, None)\n                            if job.durable:\n                                save_durable_jobs()\n                except Exception as e:\n                    print(f\"  \\033[31m[cron error] {job.id}: {e}\\033[0m\")\n\n\ndef consume_cron_queue() -> list[CronJob]:\n    \"\"\"Consume fired jobs from cron_queue (called by agent_loop).\"\"\"\n    with cron_lock:\n        fired = list(cron_queue)\n        cron_queue.clear()\n    return fired\n\n\n# Load durable jobs on startup, then start scheduler thread\nload_durable_jobs()\nthreading.Thread(target=cron_scheduler_loop, daemon=True).start()\nprint(\"  \\033[35m[cron] scheduler thread started\\033[0m\")\n\n\n# Cron tool handlers\n\ndef run_schedule_cron(cron: str, prompt: str,\n                      recurring: bool = True, durable: bool = True) -> str:\n    result = schedule_job(cron, prompt, recurring, durable)\n    if isinstance(result, str):\n        return f\"Error: {result}\"\n    return f\"Scheduled {result.id}: '{cron}' → {prompt}\"\n\n\ndef run_list_crons() -> str:\n    with cron_lock:\n        jobs = list(scheduled_jobs.values())\n    if not jobs:\n        return \"No cron jobs. Use schedule_cron to add one.\"\n    lines = []\n    for j in jobs:\n        tag = \"recurring\" if j.recurring else \"one-shot\"\n        dur = \"durable\" if j.durable else \"session\"\n        lines.append(f\"  {j.id}: '{j.cron}' → {j.prompt[:40]} \"\n                     f\"[{tag}, {dur}]\")\n    return \"\\n\".join(lines)\n\n\ndef run_cancel_cron(job_id: str) -> str:\n    return cancel_job(job_id)\n\n\n# ── MessageBus (s15 new) ──\n# Teaching version uses simple file append + unlink.\n# Real CC uses proper-lockfile for concurrent write safety.\n\nMAILBOX_DIR = WORKDIR / \".mailboxes\"\nMAILBOX_DIR.mkdir(exist_ok=True)\n\n\nclass MessageBus:\n    \"\"\"File-based message bus. Each agent has a .jsonl inbox.\n    Read is destructive: read_text + unlink (consumes messages).\n    Teaching version: no file locking; real CC uses proper-lockfile.\"\"\"\n\n    def send(self, from_agent: str, to_agent: str, content: str,\n             msg_type: str = \"message\"):\n        msg = {\"from\": from_agent, \"to\": to_agent,\n               \"content\": content, \"type\": msg_type,\n               \"ts\": time.time()}\n        inbox = MAILBOX_DIR / f\"{to_agent}.jsonl\"\n        with open(inbox, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        print(f\"  \\033[33m[bus] {from_agent} → {to_agent}: \"\n              f\"{content[:50]}\\033[0m\")\n\n    def read_inbox(self, agent: str) -> list[dict]:\n        inbox = MAILBOX_DIR / f\"{agent}.jsonl\"\n        if not inbox.exists():\n            return []\n        msgs = [json.loads(line) for line in inbox.read_text().splitlines()\n                if line.strip()]\n        inbox.unlink()  # consume: read + delete\n        return msgs\n\n\nBUS = MessageBus()\n\n# Track spawned teammates\nactive_teammates: dict[str, bool] = {}\n\n\n# ── Teammate Thread (s15 new) ──\n\ndef spawn_teammate_thread(name: str, role: str, prompt: str) -> str:\n    \"\"\"Spawn a teammate agent in a background thread.\n    Teaching version: max 10 rounds per teammate.\n    Real CC: teammates use idle loop (wait for inbox, work, repeat)\n    until shutdown_request.\"\"\"\n    if name in active_teammates:\n        return f\"Teammate '{name}' already exists\"\n\n    system = (f\"You are '{name}', a {role}. \"\n              f\"Use tools to complete tasks. \"\n              f\"Send results via send_message to 'lead'.\")\n\n    def run():\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        sub_tools = [\n            {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"command\": {\"type\": \"string\"}},\n                              \"required\": [\"command\"]}},\n            {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"}},\n                              \"required\": [\"path\"]}},\n            {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"send_message\",\n             \"description\": \"Send a message to another agent.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"to\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"to\", \"content\"]}},\n        ]\n        sub_handlers = {\n            \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n            \"send_message\": lambda to, content: (BUS.send(name, to, content),\n                                                  \"Sent\")[1],\n        }\n\n        for _ in range(10):\n            inbox = BUS.read_inbox(name)\n            if inbox:\n                messages.append({\"role\": \"user\",\n                                 \"content\": f\"<inbox>{json.dumps(inbox)}</inbox>\"})\n            try:\n                response = client.messages.create(\n                    model=MODEL, system=system, messages=messages[-20:],\n                    tools=sub_tools, max_tokens=8000)\n            except Exception:\n                break\n            messages.append({\"role\": \"assistant\", \"content\": response.content})\n            if response.stop_reason != \"tool_use\":\n                break\n            results = []\n            for block in response.content:\n                if block.type == \"tool_use\":\n                    handler = sub_handlers.get(block.name)\n                    output = handler(**block.input) if handler else \"Unknown\"\n                    results.append({\"type\": \"tool_result\",\n                                    \"tool_use_id\": block.id,\n                                    \"content\": str(output)})\n            messages.append({\"role\": \"user\", \"content\": results})\n\n        # Send final summary to Lead\n        summary = \"Done.\"\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\" and isinstance(msg[\"content\"], list):\n                for b in msg[\"content\"]:\n                    if getattr(b, \"type\", None) == \"text\":\n                        summary = b.text\n                        break\n                else:\n                    continue\n                break\n        BUS.send(name, \"lead\", summary, \"result\")\n        active_teammates.pop(name, None)\n        print(f\"  \\033[32m[teammate] {name} finished\\033[0m\")\n\n    active_teammates[name] = True\n    threading.Thread(target=run, daemon=True).start()\n    print(f\"  \\033[36m[teammate] {name} spawned as {role}\\033[0m\")\n    return f\"Teammate '{name}' spawned as {role}\"\n\n\n# ── Team Tool Handlers (s15 new) ──\n\ndef run_spawn_teammate(name: str, role: str, prompt: str) -> str:\n    return spawn_teammate_thread(name, role, prompt)\n\n\ndef run_send_message(to: str, content: str) -> str:\n    BUS.send(\"lead\", to, content)\n    return f\"Sent to {to}\"\n\n\ndef run_check_inbox() -> str:\n    msgs = BUS.read_inbox(\"lead\")\n    if not msgs:\n        return \"(inbox empty)\"\n    lines = []\n    for m in msgs:\n        lines.append(f\"  [{m['from']}] {m['content'][:200]}\")\n    return \"\\n\".join(lines)\n\n\n# ── Tool Definitions ──\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"command\": {\"type\": \"string\"},\n                          \"run_in_background\": {\"type\": \"boolean\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\",\n     \"description\": \"Create a new task with optional blockedBy dependencies.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"subject\": {\"type\": \"string\"},\n                          \"description\": {\"type\": \"string\"},\n                          \"blockedBy\": {\"type\": \"array\",\n                                        \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\",\n     \"description\": \"List all tasks with status, owner, and dependencies.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"get_task\",\n     \"description\": \"Get full details of a specific task by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\",\n     \"description\": \"Claim a pending task. Sets owner, changes status to in_progress.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\",\n     \"description\": \"Complete an in-progress task. Reports unblocked downstream tasks.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"schedule_cron\",\n     \"description\": \"Schedule a cron job. cron is 5-field: min hour dom month dow.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"cron\": {\"type\": \"string\",\n                                   \"description\": \"5-field cron expression\"},\n                          \"prompt\": {\"type\": \"string\",\n                                     \"description\": \"Message to inject when fired\"},\n                          \"recurring\": {\"type\": \"boolean\",\n                                        \"description\": \"True=recurring, False=one-shot\"},\n                          \"durable\": {\"type\": \"boolean\",\n                                      \"description\": \"True=persist to disk\"}},\n                      \"required\": [\"cron\", \"prompt\"]}},\n    {\"name\": \"list_crons\",\n     \"description\": \"List all registered cron jobs.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"cancel_cron\",\n     \"description\": \"Cancel a cron job by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"job_id\": {\"type\": \"string\"}},\n                      \"required\": [\"job_id\"]}},\n    {\"name\": \"spawn_teammate\",\n     \"description\": \"Spawn a teammate agent in a background thread.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"name\": {\"type\": \"string\"},\n                          \"role\": {\"type\": \"string\"},\n                          \"prompt\": {\"type\": \"string\"}},\n                      \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"send_message\",\n     \"description\": \"Send a message to a teammate via MessageBus.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"to\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"check_inbox\",\n     \"description\": \"Check Lead's inbox for teammate messages.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n]\n\n\n# ── Context ──\n\ndef update_context(context: dict, messages: list) -> dict:\n    \"\"\"Derive context from real state.\"\"\"\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        content = MEMORY_INDEX.read_text().strip()\n        if content:\n            memories = content\n    return {\n        \"enabled_tools\": [t[\"name\"] for t in TOOLS],\n        \"workspace\": str(WORKDIR),\n        \"memories\": memories,\n    }\n\n\n# ── Agent Loop ──\n# Teaching code keeps a basic agent loop. S11's full error recovery is omitted.\n# Cron queue is consumed when agent_loop is called; real CC auto-wakes via\n# queue processor (useQueueProcessor.ts) when items arrive.\n\ndef agent_loop(messages: list, context: dict):\n    system = get_system_prompt(context)\n    while True:\n        # Consume fired cron jobs → inject as messages\n        fired = consume_cron_queue()\n        for job in fired:\n            messages.append({\"role\": \"user\",\n                             \"content\": f\"[Scheduled] {job.prompt}\"})\n            print(f\"  \\033[35m[inject cron] {job.prompt[:50]}\\033[0m\")\n\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=TOOLS, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\",\n                 \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n\n            if should_run_background(block.name, block.input):\n                bg_id = start_background_task(block)\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": f\"[Background task {bg_id} started] \"\n                                           f\"Result will be available when complete.\"})\n            else:\n                output = execute_tool(block)\n                print(str(output)[:300])\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": output})\n\n        # Merge background tool results + notifications into one user message\n        user_content = list(results)\n        bg_notifications = collect_background_results()\n        if bg_notifications:\n            for notif in bg_notifications:\n                user_content.append({\"type\": \"text\", \"text\": notif})\n        messages.append({\"role\": \"user\", \"content\": user_content})\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s15: agent teams\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = update_context({}, [])\n    while True:\n        try:\n            query = input(\"\\033[36ms15 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n\n        # Check inbox for teammate results → inject into history\n        inbox = BUS.read_inbox(\"lead\")\n        if inbox:\n            inbox_text = \"\\n\".join(\n                f\"From {m['from']}: {m['content'][:200]}\" for m in inbox)\n            history.append({\"role\": \"user\",\n                            \"content\": f\"[Inbox]\\n{inbox_text}\"})\n            print(f\"\\n\\033[33m[Inbox: {len(inbox)} messages injected]\\033[0m\")\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s15_agent_teams/agent-teams-overview.svg",
          "alt": "agent teams overview"
        },
        {
          "src": "/course-assets/s15_agent_teams/team-topology.svg",
          "alt": "team topology"
        }
      ]
    },
    {
      "id": "s16",
      "filename": "s16_team_protocols/code.py",
      "title": "Team Protocols",
      "subtitle": "Teammates Need Agreements",
      "loc": 709,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "send_message",
        "submit_plan",
        "create_task",
        "list_tasks",
        "get_task",
        "claim_task",
        "complete_task",
        "spawn_teammate",
        "check_inbox",
        "request_shutdown",
        "request_plan",
        "review_plan"
      ],
      "newTools": [
        "submit_plan",
        "request_shutdown",
        "request_plan",
        "review_plan"
      ],
      "coreAddition": "Shared coordination protocols",
      "keyInsight": "Multi-agent systems need explicit message contracts, not vibes.",
      "classes": [
        {
          "name": "Task",
          "startLine": 58,
          "endLine": 66
        },
        {
          "name": "MessageBus",
          "startLine": 340,
          "endLine": 365
        },
        {
          "name": "ProtocolState",
          "startLine": 372,
          "endLine": 381
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 67
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 83
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 87
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 91
        },
        {
          "name": "get_task",
          "signature": "def get_task(task_id: str)",
          "startLine": 96
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 102
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 114
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 129
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 158
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 171
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 183
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str, run_in_background: bool = False)",
          "startLine": 190
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 201
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 211
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 231
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 246
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 253
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 257
        },
        {
          "name": "is_slow_operation",
          "signature": "def is_slow_operation(tool_name: str, tool_input: dict)",
          "startLine": 269
        },
        {
          "name": "should_run_background",
          "signature": "def should_run_background(tool_name: str, tool_input: dict)",
          "startLine": 280
        },
        {
          "name": "start_background_task",
          "signature": "def start_background_task(block)",
          "startLine": 287
        },
        {
          "name": "collect_background_results",
          "signature": "def collect_background_results()",
          "startLine": 311
        },
        {
          "name": "new_request_id",
          "signature": "def new_request_id()",
          "startLine": 385
        },
        {
          "name": "match_response",
          "signature": "def match_response(response_type: str, request_id: str, approve: bool)",
          "startLine": 389
        },
        {
          "name": "consume_lead_inbox",
          "signature": "def consume_lead_inbox(route_protocol: bool = True)",
          "startLine": 420
        },
        {
          "name": "spawn_teammate_thread",
          "signature": "def spawn_teammate_thread(name: str, role: str, prompt: str)",
          "startLine": 440
        },
        {
          "name": "_teammate_submit_plan",
          "signature": "def _teammate_submit_plan(from_name: str, plan: str)",
          "startLine": 598
        },
        {
          "name": "run_request_shutdown",
          "signature": "def run_request_shutdown(teammate: str)",
          "startLine": 621
        },
        {
          "name": "run_request_plan",
          "signature": "def run_request_plan(teammate: str, task: str)",
          "startLine": 635
        },
        {
          "name": "run_review_plan",
          "signature": "def run_review_plan(request_id: str, approve: bool, feedback: str = \"\")",
          "startLine": 642
        },
        {
          "name": "run_spawn_teammate",
          "signature": "def run_spawn_teammate(name: str, role: str, prompt: str)",
          "startLine": 659
        },
        {
          "name": "run_send_message",
          "signature": "def run_send_message(to: str, content: str)",
          "startLine": 663
        },
        {
          "name": "run_check_inbox",
          "signature": "def run_check_inbox()",
          "startLine": 668
        },
        {
          "name": "execute_tool",
          "signature": "def execute_tool(block)",
          "startLine": 684
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 790
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 806
        }
      ],
      "layer": "collaboration",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns16: Team Protocols — request-response protocol + request_id + dispatch + state machine.\n\nRun:  python s16_team_protocols/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s15:\n  - ProtocolState dataclass (request_id, type, sender, status, created_at)\n  - pending_requests dict: tracks in-flight protocol requests\n  - dispatch_message: routes incoming messages by type to handlers\n  - request_shutdown: Lead sends shutdown protocol request\n  - request_plan: Lead asks teammate to submit plan\n  - handle_shutdown_request / handle_plan_response: teammate receives & responds\n  - match_response: Lead correlates response to request via request_id (with type validation)\n  - Teammate idle loop: waits for inbox messages instead of exiting after 10 rounds\n  - Unified consume_lead_inbox: protocol routing + injection into history\n  - 3 new Lead tools: request_shutdown, request_plan, review_plan\n  - 1 new teammate tool: submit_plan\n\nASCII flow:\n  Lead: BUS.send(\"shutdown_request\", {request_id}) ──────→ teammate inbox\n  Teammate: dispatch → handler → BUS.send(\"shutdown_response\", {request_id}) ─→ Lead inbox\n  Lead: consume_lead_inbox → match_response(request_id) → pending_requests[req_id].status = approved\n\"\"\"\n\nimport os, subprocess, json, time, random, threading\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dataclasses import dataclass, asdict, field\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System (from s12, synced) ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str          # pending | in_progress | completed\n    owner: str | None\n    blockedBy: list[str]\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task(task_id: str) -> str:\n    \"\"\"Return full task details as JSON.\"\"\"\n    task = load_task(task_id)\n    return json.dumps(asdict(task), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    \"\"\"Check if all blockedBy dependencies are completed.\n    Missing dependencies are treated as blocked.\"\"\"\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if not _task_path(d).exists() or load_task(d).status != \"completed\"]\n        return f\"Blocked by: {deps}\"\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress (owner: {owner})\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n        print(f\"  \\033[33m[unblocked] {', '.join(unblocked)}\\033[0m\")\n    return msg\n\n\n# ── Prompt Assembly (from s10, synced) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"get_task, create_task, list_tasks, claim_task, complete_task, \"\n             \"spawn_teammate, send_message, check_inbox, \"\n             \"request_shutdown, request_plan, review_plan.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    memories = context.get(\"memories\", \"\")\n    if memories:\n        sections.append(f\"Relevant memories:\\n{memories}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_key, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_key, _last_prompt\n    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)\n    if key == _last_context_key and _last_prompt:\n        return _last_prompt\n    _last_context_key = key\n    _last_prompt = assemble_system_prompt(context)\n    return _last_prompt\n\n\n# ── Tools ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str, run_in_background: bool = False) -> str:\n    # run_in_background is handled by agent_loop dispatch, not here\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        fp = safe_path(path)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# Task tools\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks. Use create_task to add some.\"\n    lines = []\n    for t in tasks:\n        icon = {\"pending\": \"○\", \"in_progress\": \"●\",\n                \"completed\": \"✓\"}.get(t.status, \"?\")\n        deps = f\" (blockedBy: {', '.join(t.blockedBy)})\" if t.blockedBy else \"\"\n        owner = f\" [{t.owner}]\" if t.owner else \"\"\n        lines.append(f\"  {icon} {t.id}: {t.subject} \"\n                     f\"[{t.status}]{owner}{deps}\")\n    return \"\\n\".join(lines)\n\n\ndef run_get_task(task_id: str) -> str:\n    try:\n        return get_task(task_id)\n    except FileNotFoundError:\n        return f\"Error: Task {task_id} not found\"\n\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\n\n# ── Background Tasks (from s13, synced) ──\n\n_bg_counter = 0\nbackground_tasks: dict[str, dict] = {}\nbackground_results: dict[str, str] = {}\nbackground_lock = threading.Lock()\n\n\ndef is_slow_operation(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Fallback heuristic: commands likely to take > 30s.\"\"\"\n    if tool_name != \"bash\":\n        return False\n    cmd = tool_input.get(\"command\", \"\").lower()\n    slow_keywords = [\"install\", \"build\", \"test\", \"deploy\", \"compile\",\n                     \"docker build\", \"pip install\", \"npm install\",\n                     \"cargo build\", \"pytest\", \"make\"]\n    return any(kw in cmd for kw in slow_keywords)\n\n\ndef should_run_background(tool_name: str, tool_input: dict) -> bool:\n    \"\"\"Model explicit request takes priority; fallback to heuristic.\"\"\"\n    if tool_input.get(\"run_in_background\"):\n        return True\n    return is_slow_operation(tool_name, tool_input)\n\n\ndef start_background_task(block) -> str:\n    \"\"\"Run tool in a daemon thread. Returns background task ID.\"\"\"\n    global _bg_counter\n    _bg_counter += 1\n    bg_id = f\"bg_{_bg_counter:04d}\"\n    cmd = block.input.get(\"command\", block.name)\n\n    def worker():\n        result = execute_tool(block)\n        with background_lock:\n            background_tasks[bg_id][\"status\"] = \"completed\"\n            background_results[bg_id] = result\n\n    with background_lock:\n        background_tasks[bg_id] = {\n            \"tool_use_id\": block.id,\n            \"command\": cmd,\n            \"status\": \"running\",\n        }\n    threading.Thread(target=worker, daemon=True).start()\n    print(f\"  \\033[33m[background] dispatched {bg_id}: {cmd[:40]}\\033[0m\")\n    return bg_id\n\n\ndef collect_background_results() -> list[str]:\n    \"\"\"Collect completed background results as task_notification messages.\"\"\"\n    with background_lock:\n        ready_ids = [bid for bid, task in background_tasks.items()\n                     if task[\"status\"] == \"completed\"]\n    notifications = []\n    for bg_id in ready_ids:\n        with background_lock:\n            task = background_tasks.pop(bg_id)\n            output = background_results.pop(bg_id, \"\")\n        summary = output[:200] if len(output) > 200 else output\n        notifications.append(\n            f\"<task_notification>\\n\"\n            f\"  <task_id>{bg_id}</task_id>\\n\"\n            f\"  <status>completed</status>\\n\"\n            f\"  <command>{task['command']}</command>\\n\"\n            f\"  <summary>{summary}</summary>\\n\"\n            f\"</task_notification>\")\n        print(f\"  \\033[32m[background done] {bg_id}: \"\n              f\"{task['command'][:40]} ({len(output)} chars)\\033[0m\")\n    return notifications\n\n\n# ── MessageBus (from s15) ──\n\nMAILBOX_DIR = WORKDIR / \".mailboxes\"\nMAILBOX_DIR.mkdir(exist_ok=True)\n\n\nclass MessageBus:\n    \"\"\"File-based message bus. Each agent has a .jsonl inbox.\n    Read is destructive: read_text + unlink (consumes messages).\n    Teaching version: no file locking; real CC uses proper-lockfile.\"\"\"\n\n    def send(self, from_agent: str, to_agent: str, content: str,\n             msg_type: str = \"message\", metadata: dict = None):\n        msg = {\"from\": from_agent, \"to\": to_agent,\n               \"content\": content, \"type\": msg_type,\n               \"ts\": time.time(), \"metadata\": metadata or {}}\n        inbox = MAILBOX_DIR / f\"{to_agent}.jsonl\"\n        with open(inbox, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        print(f\"  \\033[33m[bus] {from_agent} → {to_agent}: \"\n              f\"({msg_type}) {content[:50]}\\033[0m\")\n\n    def read_inbox(self, agent: str) -> list[dict]:\n        inbox = MAILBOX_DIR / f\"{agent}.jsonl\"\n        if not inbox.exists():\n            return []\n        msgs = [json.loads(line) for line in inbox.read_text().splitlines()\n                if line.strip()]\n        inbox.unlink()  # consume: read + delete\n        return msgs\n\n\nBUS = MessageBus()\nactive_teammates: dict[str, bool] = {}\n\n# ── Protocol State (s16 new) ──\n\n@dataclass\nclass ProtocolState:\n    request_id: str\n    type: str       # \"shutdown\" | \"plan_approval\"\n    sender: str\n    target: str\n    status: str     # pending | approved | rejected\n    payload: str    # plan text or shutdown reason\n    created_at: float = field(default_factory=time.time)\n\n\npending_requests: dict[str, ProtocolState] = {}\n\n\ndef new_request_id() -> str:\n    return f\"req_{random.randint(0, 999999):06d}\"\n\n\ndef match_response(response_type: str, request_id: str, approve: bool):\n    \"\"\"Correlate a response to the original request via request_id.\n    Validates that response_type matches the request type.\"\"\"\n    state = pending_requests.get(request_id)\n    if not state:\n        print(f\"  \\033[31m[protocol] unknown request_id: {request_id}\\033[0m\")\n        return\n    # Validate response type matches request type\n    if state.type == \"shutdown\" and response_type != \"shutdown_response\":\n        print(f\"  \\033[31m[protocol] type mismatch: expected shutdown_response, \"\n              f\"got {response_type}\\033[0m\")\n        return\n    if state.type == \"plan_approval\" and response_type != \"plan_approval_response\":\n        print(f\"  \\033[31m[protocol] type mismatch: expected plan_approval_response, \"\n              f\"got {response_type}\\033[0m\")\n        return\n    if state.status != \"pending\":\n        print(f\"  \\033[33m[protocol] {request_id} already {state.status}, \"\n              f\"ignoring duplicate\\033[0m\")\n        return\n    state.status = \"approved\" if approve else \"rejected\"\n    icon = \"✓\" if approve else \"✗\"\n    color = \"32\" if approve else \"31\"\n    print(f\"  \\033[{color}m[protocol] {state.type} {icon} \"\n          f\"({request_id}: {state.status})\\033[0m\")\n\n\n# ── Unified Lead Inbox Consumer (s16 fix) ──\n# Both check_inbox tool and main loop call this function.\n# Protocol responses are routed via match_response before returning.\n\ndef consume_lead_inbox(route_protocol: bool = True) -> list[dict]:\n    \"\"\"Read Lead's inbox. Route protocol responses, return all messages.\n    Called by both run_check_inbox() and main loop to avoid\n    messages being consumed without protocol routing.\"\"\"\n    msgs = BUS.read_inbox(\"lead\")\n    if not msgs:\n        return []\n    if route_protocol:\n        for msg in msgs:\n            meta = msg.get(\"metadata\", {})\n            req_id = meta.get(\"request_id\", \"\")\n            msg_type = msg.get(\"type\", \"\")\n            if req_id and msg_type.endswith(\"_response\"):\n                approve = meta.get(\"approve\", False)\n                match_response(msg_type, req_id, approve)\n    return msgs\n\n\n# ── Teammate Thread (s16: idle loop + dispatch) ──\n\ndef spawn_teammate_thread(name: str, role: str, prompt: str) -> str:\n    \"\"\"Spawn a teammate agent in a background thread.\n    Uses idle loop: after each LLM turn, waits for inbox messages\n    (shutdown_request, new task) instead of exiting.\"\"\"\n    if name in active_teammates:\n        return f\"Teammate '{name}' already exists\"\n\n    system = (f\"You are '{name}', a {role}. \"\n              f\"Use tools to complete tasks. \"\n              f\"Check inbox for protocol messages (shutdown_request, etc).\")\n\n    def handle_inbox_message(name: str, msg: dict, messages: list) -> bool:\n        \"\"\"Dispatch incoming protocol messages by type.\n        Returns True if teammate should stop.\"\"\"\n        msg_type = msg.get(\"type\", \"message\")\n        meta = msg.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n\n        if msg_type == \"shutdown_request\":\n            BUS.send(name, \"lead\", \"Shutting down gracefully.\",\n                     \"shutdown_response\",\n                     {\"request_id\": req_id, \"approve\": True})\n            print(f\"  \\033[35m[protocol] {name} approved shutdown \"\n                  f\"({req_id})\\033[0m\")\n            return True  # stop the loop\n\n        if msg_type == \"plan_approval_response\":\n            approve = meta.get(\"approve\", False)\n            if approve:\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"[Plan approved] Proceed with the task.\"})\n            else:\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"[Plan rejected] Feedback: {msg['content']}\"})\n\n        return False  # continue\n\n    def run():\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        sub_tools = [\n            {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"command\": {\"type\": \"string\"}},\n                              \"required\": [\"command\"]}},\n            {\"name\": \"read_file\", \"description\": \"Read file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"}},\n                              \"required\": [\"path\"]}},\n            {\"name\": \"write_file\", \"description\": \"Write file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"send_message\",\n             \"description\": \"Send message to another agent.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"to\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"submit_plan\",\n             \"description\": \"Submit a plan for Lead approval.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"plan\": {\"type\": \"string\"}},\n                              \"required\": [\"plan\"]}},\n        ]\n        sub_handlers = {\n            \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n            \"send_message\": lambda to, content: (BUS.send(name, to, content),\n                                                  \"Sent\")[1],\n            \"submit_plan\": lambda plan: _teammate_submit_plan(name, plan),\n        }\n\n        shutdown_requested = False\n        while not shutdown_requested:\n            # Check inbox for protocol messages\n            inbox = BUS.read_inbox(name)\n            should_stop = False\n            non_protocol = []\n            for msg in inbox:\n                if msg.get(\"type\") in (\"shutdown_request\", \"plan_approval_response\"):\n                    should_stop = handle_inbox_message(name, msg, messages)\n                    if should_stop:\n                        break\n                else:\n                    non_protocol.append(msg)\n            if should_stop:\n                shutdown_requested = True\n                break\n            if non_protocol:\n                inbox_json = json.dumps(non_protocol)\n                messages.append({\"role\": \"user\",\n                    \"content\": \"<inbox>\" + inbox_json + \"</inbox>\"})\n\n            # LLM turn\n            try:\n                response = client.messages.create(\n                    model=MODEL, system=system, messages=messages[-20:],\n                    tools=sub_tools, max_tokens=8000)\n            except Exception:\n                break\n\n            messages.append({\"role\": \"assistant\", \"content\": response.content})\n            if response.stop_reason != \"tool_use\":\n                # Idle: wait for inbox messages instead of exiting\n                # Real CC sends idle_notification to Lead here\n                while not shutdown_requested:\n                    time.sleep(1)\n                    inbox = BUS.read_inbox(name)\n                    if not inbox:\n                        continue\n                    for msg in inbox:\n                        if msg.get(\"type\") in (\"shutdown_request\", \"plan_approval_response\"):\n                            should_stop = handle_inbox_message(name, msg, messages)\n                            if should_stop:\n                                shutdown_requested = True\n                                break\n                        else:\n                            non_protocol.append(msg)\n                    if shutdown_requested:\n                        break\n                    if non_protocol:\n                        inbox_json = json.dumps(non_protocol)\n                        messages.append({\"role\": \"user\",\n                            \"content\": \"<inbox>\" + inbox_json + \"</inbox>\"})\n                        break  # back to LLM turn with new messages\n\n            # Execute tool calls\n            results = []\n            for block in response.content:\n                if block.type == \"tool_use\":\n                    handler = sub_handlers.get(block.name)\n                    output = handler(**block.input) if handler else \"Unknown\"\n                    results.append({\"type\": \"tool_result\",\n                                    \"tool_use_id\": block.id,\n                                    \"content\": str(output)})\n            messages.append({\"role\": \"user\", \"content\": results})\n\n        # Send final summary to Lead\n        summary = \"Done.\"\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\" and isinstance(msg[\"content\"], list):\n                for b in msg[\"content\"]:\n                    if getattr(b, \"type\", None) == \"text\":\n                        summary = b.text\n                        break\n                else:\n                    continue\n                break\n        BUS.send(name, \"lead\", summary, \"result\")\n        active_teammates.pop(name, None)\n        print(f\"  \\033[32m[teammate] {name} finished\\033[0m\")\n\n    active_teammates[name] = True\n    threading.Thread(target=run, daemon=True).start()\n    print(f\"  \\033[36m[teammate] {name} spawned as {role}\\033[0m\")\n    return f\"Teammate '{name}' spawned as {role}\"\n\n\ndef _teammate_submit_plan(from_name: str, plan: str) -> str:\n    \"\"\"Teammate submits a plan to Lead for approval.\n\n    Note: This is a protocol-level request, not a code-level gate.\n    After submitting, the teammate's thread continues running — it can\n    still call bash/write/etc. Real enforcement relies on the model\n    waiting for the approval response before acting. Code-level tool\n    gating would require blocking the teammate's tool dispatch until\n    approval arrives.\n    \"\"\"\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"plan_approval\",\n        sender=from_name, target=\"lead\",\n        status=\"pending\", payload=plan)\n    BUS.send(from_name, \"lead\", plan,\n             \"plan_approval_request\",\n             {\"request_id\": req_id})\n    return f\"Plan submitted ({req_id}). Waiting for approval...\"\n\n\n# ── Lead Protocol Tools (s16 new) ──\n\ndef run_request_shutdown(teammate: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"shutdown\",\n        sender=\"lead\", target=teammate,\n        status=\"pending\", payload=\"\")\n    BUS.send(\"lead\", teammate, \"Please shut down gracefully.\",\n             \"shutdown_request\",\n             {\"request_id\": req_id})\n    print(f\"  \\033[35m[protocol] shutdown_request → {teammate} \"\n          f\"({req_id})\\033[0m\")\n    return f\"Shutdown request sent to {teammate} (req: {req_id})\"\n\n\ndef run_request_plan(teammate: str, task: str) -> str:\n    \"\"\"Lead asks a teammate to submit a plan for a task.\"\"\"\n    BUS.send(\"lead\", teammate, f\"Please submit a plan for: {task}\",\n             \"message\")\n    return f\"Asked {teammate} to submit a plan\"\n\n\ndef run_review_plan(request_id: str, approve: bool, feedback: str = \"\") -> str:\n    state = pending_requests.get(request_id)\n    if not state:\n        return f\"Request {request_id} not found\"\n    if state.status != \"pending\":\n        return f\"Request {request_id} already {state.status}\"\n    state.status = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", state.sender, feedback or (\"Approved\" if approve else \"Rejected\"),\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n    icon = \"✓\" if approve else \"✗\"\n    print(f\"  \\033[32m[protocol] plan {icon} ({request_id})\\033[0m\")\n    return f\"Plan {'approved' if approve else 'rejected'} ({request_id})\"\n\n\n# ── Other Lead Tool Handlers ──\n\ndef run_spawn_teammate(name: str, role: str, prompt: str) -> str:\n    return spawn_teammate_thread(name, role, prompt)\n\n\ndef run_send_message(to: str, content: str) -> str:\n    BUS.send(\"lead\", to, content)\n    return f\"Sent to {to}\"\n\n\ndef run_check_inbox() -> str:\n    \"\"\"Check Lead's inbox. Routes protocol responses via match_response.\"\"\"\n    msgs = consume_lead_inbox(route_protocol=True)\n    if not msgs:\n        return \"(inbox empty)\"\n    lines = []\n    for m in msgs:\n        meta = m.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n        tag = f\" [{m['type']} req:{req_id}]\" if req_id else f\" [{m['type']}]\"\n        lines.append(f\"  [{m['from']}]{tag} {m['content'][:200]}\")\n    return \"\\n\".join(lines)\n\n\n# ── Tool Dispatch ──\n\ndef execute_tool(block) -> str:\n    \"\"\"Execute a tool call block, return output.\"\"\"\n    handler = {\n        \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n        \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n        \"get_task\": run_get_task, \"claim_task\": run_claim_task,\n        \"complete_task\": run_complete_task,\n        \"spawn_teammate\": run_spawn_teammate,\n        \"send_message\": run_send_message, \"check_inbox\": run_check_inbox,\n        \"request_shutdown\": run_request_shutdown,\n        \"request_plan\": run_request_plan, \"review_plan\": run_review_plan,\n    }.get(block.name)\n    if handler:\n        return handler(**block.input)\n    return f\"Unknown tool: {block.name}\"\n\n\n# ── Tool Definitions ──\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"command\": {\"type\": \"string\"},\n                          \"run_in_background\": {\"type\": \"boolean\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\",\n     \"description\": \"Create a new task with optional blockedBy dependencies.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"subject\": {\"type\": \"string\"},\n                          \"description\": {\"type\": \"string\"},\n                          \"blockedBy\": {\"type\": \"array\",\n                                        \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\",\n     \"description\": \"List all tasks with status, owner, and dependencies.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"get_task\",\n     \"description\": \"Get full details of a specific task by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\",\n     \"description\": \"Claim a pending task. Sets owner, changes status to in_progress.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\",\n     \"description\": \"Complete an in-progress task. Reports unblocked downstream tasks.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"spawn_teammate\",\n     \"description\": \"Spawn a teammate agent in a background thread.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"name\": {\"type\": \"string\"},\n                          \"role\": {\"type\": \"string\"},\n                          \"prompt\": {\"type\": \"string\"}},\n                      \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"send_message\",\n     \"description\": \"Send message to a teammate via MessageBus.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"to\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"check_inbox\",\n     \"description\": \"Check Lead's inbox. Routes protocol responses automatically.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                      \"required\": []}},\n    {\"name\": \"request_shutdown\",\n     \"description\": \"Request a teammate to shut down gracefully.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\"]}},\n    {\"name\": \"request_plan\",\n     \"description\": \"Ask a teammate to submit a plan for review.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"},\n                                     \"task\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\", \"task\"]}},\n    {\"name\": \"review_plan\",\n     \"description\": \"Approve or reject a submitted plan by request_id.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"request_id\": {\"type\": \"string\"},\n                          \"approve\": {\"type\": \"boolean\"},\n                          \"feedback\": {\"type\": \"string\"}},\n                      \"required\": [\"request_id\", \"approve\"]}},\n]\n\n\n# ── Context ──\n\ndef update_context(context: dict, messages: list) -> dict:\n    \"\"\"Derive context from real state.\"\"\"\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        content = MEMORY_INDEX.read_text().strip()\n        if content:\n            memories = content\n    return {\n        \"enabled_tools\": [t[\"name\"] for t in TOOLS],\n        \"workspace\": str(WORKDIR),\n        \"memories\": memories,\n    }\n\n\n# ── Agent Loop ──\n\ndef agent_loop(messages: list, context: dict):\n    system = get_system_prompt(context)\n    while True:\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=TOOLS, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\",\n                 \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n\n            if should_run_background(block.name, block.input):\n                bg_id = start_background_task(block)\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": f\"[Background task {bg_id} started] \"\n                                           f\"Result will be available when complete.\"})\n            else:\n                output = execute_tool(block)\n                print(str(output)[:300])\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": output})\n\n        # Merge background tool results + notifications into one user message\n        user_content = list(results)\n        bg_notifications = collect_background_results()\n        if bg_notifications:\n            for notif in bg_notifications:\n                user_content.append({\"type\": \"text\", \"text\": notif})\n        messages.append({\"role\": \"user\", \"content\": user_content})\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s16: team protocols\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = update_context({}, [])\n    while True:\n        try:\n            query = input(\"\\033[36ms16 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n\n        # Check inbox → route protocol + inject into history\n        inbox_msgs = consume_lead_inbox(route_protocol=True)\n        if inbox_msgs:\n            inbox_text = \"\\n\".join(\n                f\"From {m['from']}: {m['content'][:200]}\" for m in inbox_msgs)\n            history.append({\"role\": \"user\",\n                            \"content\": f\"[Inbox]\\n{inbox_text}\"})\n            print(f\"\\n\\033[33m[Inbox: {len(inbox_msgs)} messages injected]\\033[0m\")\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s16_team_protocols/team-protocols-overview.svg",
          "alt": "team protocols overview"
        }
      ]
    },
    {
      "id": "s17",
      "filename": "s17_autonomous_agents/code.py",
      "title": "Autonomous Agents",
      "subtitle": "Check the Board, Claim the Task",
      "loc": 648,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "send_message",
        "submit_plan",
        "list_tasks",
        "claim_task",
        "complete_task",
        "create_task",
        "get_task",
        "spawn_teammate",
        "check_inbox",
        "request_shutdown",
        "request_plan",
        "review_plan"
      ],
      "newTools": [],
      "coreAddition": "Autonomous task claiming",
      "keyInsight": "Teammates become useful when they can discover and claim work themselves.",
      "classes": [
        {
          "name": "Task",
          "startLine": 51,
          "endLine": 59
        },
        {
          "name": "MessageBus",
          "startLine": 219,
          "endLine": 240
        },
        {
          "name": "ProtocolState",
          "startLine": 248,
          "endLine": 257
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 60
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 76
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 80
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 84
        },
        {
          "name": "get_task",
          "signature": "def get_task(task_id: str)",
          "startLine": 89
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 94
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 104
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 125
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 153
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 165
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str)",
          "startLine": 176
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str)",
          "startLine": 183
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None)",
          "startLine": 193
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str)",
          "startLine": 203
        },
        {
          "name": "new_request_id",
          "signature": "def new_request_id()",
          "startLine": 261
        },
        {
          "name": "match_response",
          "signature": "def match_response(response_type: str, request_id: str, approve: bool)",
          "startLine": 265
        },
        {
          "name": "scan_unclaimed_tasks",
          "signature": "def scan_unclaimed_tasks()",
          "startLine": 292
        },
        {
          "name": "spawn_teammate_thread",
          "signature": "def spawn_teammate_thread(name: str, role: str, prompt: str)",
          "startLine": 351
        },
        {
          "name": "_teammate_submit_plan",
          "signature": "def _teammate_submit_plan(from_name: str, plan: str)",
          "startLine": 528
        },
        {
          "name": "run_request_shutdown",
          "signature": "def run_request_shutdown(teammate: str)",
          "startLine": 543
        },
        {
          "name": "run_request_plan",
          "signature": "def run_request_plan(teammate: str, task: str)",
          "startLine": 557
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 591
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 600
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 604
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 608
        },
        {
          "name": "run_spawn_teammate",
          "signature": "def run_spawn_teammate(name: str, role: str, prompt: str)",
          "startLine": 612
        },
        {
          "name": "run_send_message",
          "signature": "def run_send_message(to: str, content: str)",
          "startLine": 616
        },
        {
          "name": "consume_lead_inbox",
          "signature": "def consume_lead_inbox(route_protocol=True)",
          "startLine": 621
        },
        {
          "name": "run_check_inbox",
          "signature": "def run_check_inbox()",
          "startLine": 634
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 745
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 754
        }
      ],
      "layer": "collaboration",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns17: Autonomous Agents — idle poll + auto-claim + WORK/IDLE lifecycle.\n\nRun:  python s17_autonomous_agents/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s16:\n  - scan_unclaimed_tasks: find pending, unowned tasks with deps completed\n  - idle_poll: 60s polling loop (inbox + task board), dispatches shutdown in IDLE\n  - claim_task: owner check + return value verification\n  - Teammate lifecycle: WORK → IDLE → SHUTDOWN\n  - Teammate tools: + list_tasks, claim_task, complete_task (5→8)\n  - consume_lead_inbox: unified inbox consumer for protocol + context injection\n  - Identity re-injection after context compression\n\nASCII lifecycle:\n  WORK: inbox → LLM → tools → (tool_use? loop) → (done? → IDLE)\n  IDLE: 5s poll → inbox? → WORK / unclaimed? → claim → WORK / 60s? → SHUTDOWN\n\"\"\"\n\nimport os, subprocess, json, time, random, threading\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dataclasses import dataclass, asdict, field\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System (from s12) ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str\n    owner: str | None\n    blockedBy: list[str]\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task(task_id: str) -> str:\n    task = load_task(task_id)\n    return json.dumps(asdict(task), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if task.owner:\n        return f\"Task {task_id} already owned by {task.owner}\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if _task_path(d).exists() and load_task(d).status != \"completed\"]\n        missing = [d for d in task.blockedBy if not _task_path(d).exists()]\n        parts = []\n        if deps: parts.append(f\"blocked by: {deps}\")\n        if missing: parts.append(f\"missing deps: {missing}\")\n        return \"Cannot start — \" + \", \".join(parts)\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n    return msg\n\n\n# ── Prompt Assembly (from s10) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"create_task, list_tasks, get_task, claim_task, complete_task, \"\n             \"spawn_teammate, send_message, check_inbox, \"\n             \"request_shutdown, request_plan, review_plan.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    if context.get(\"memories\"):\n        sections.append(f\"Relevant memories:\\n{context['memories']}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_hash, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_hash, _last_prompt\n    h = json.dumps(context, sort_keys=True)\n    if h == _last_context_hash and _last_prompt:\n        return _last_prompt\n    _last_context_hash, _last_prompt = h, assemble_system_prompt(context)\n    return _last_prompt\n\n\n# ── Tools (from s15) ──\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        fp = safe_path(path)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# ── MessageBus (from s15) ──\n\nMAILBOX_DIR = WORKDIR / \".mailboxes\"\nMAILBOX_DIR.mkdir(exist_ok=True)\n\n\nclass MessageBus:\n    def send(self, from_agent: str, to_agent: str, content: str,\n             msg_type: str = \"message\", metadata: dict = None):\n        msg = {\"from\": from_agent, \"to\": to_agent,\n               \"content\": content, \"type\": msg_type,\n               \"ts\": time.time(), \"metadata\": metadata or {}}\n        inbox = MAILBOX_DIR / f\"{to_agent}.jsonl\"\n        with open(inbox, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        print(f\"  \\033[33m[bus] {from_agent} → {to_agent}: \"\n              f\"({msg_type}) {content[:50]}\\033[0m\")\n\n    def read_inbox(self, agent: str) -> list[dict]:\n        inbox = MAILBOX_DIR / f\"{agent}.jsonl\"\n        if not inbox.exists():\n            return []\n        msgs = [json.loads(line) for line in inbox.read_text().splitlines()\n                if line.strip()]\n        inbox.unlink()\n        return msgs\n\n\nBUS = MessageBus()\nactive_teammates: dict[str, bool] = {}\n\n\n# ── Protocol State (from s16) ──\n\n@dataclass\nclass ProtocolState:\n    request_id: str\n    type: str\n    sender: str\n    target: str\n    status: str\n    payload: str\n    created_at: float = field(default_factory=time.time)\n\n\npending_requests: dict[str, ProtocolState] = {}\n\n\ndef new_request_id() -> str:\n    return f\"req_{random.randint(0, 999999):06d}\"\n\n\ndef match_response(response_type: str, request_id: str, approve: bool):\n    \"\"\"Correlate a response to the original request via request_id.\"\"\"\n    state = pending_requests.get(request_id)\n    if not state:\n        print(f\"  \\033[31m[protocol] unknown request_id: {request_id}\\033[0m\")\n        return\n    if state.type == \"shutdown\" and response_type != \"shutdown_response\":\n        print(f\"  \\033[31m[protocol] type mismatch: expected shutdown_response, \"\n              f\"got {response_type}\\033[0m\")\n        return\n    if state.type == \"plan_approval\" and response_type != \"plan_approval_response\":\n        print(f\"  \\033[31m[protocol] type mismatch: expected plan_approval_response, \"\n              f\"got {response_type}\\033[0m\")\n        return\n    state.status = \"approved\" if approve else \"rejected\"\n    icon = \"✓\" if approve else \"✗\"\n    color = \"32\" if approve else \"31\"\n    print(f\"  \\033[{color}m[protocol] {state.type} {icon} \"\n          f\"({request_id}: {state.status})\\033[0m\")\n\n\n# ── Autonomous Agent (s17 new) ──\n\nIDLE_POLL_INTERVAL = 5   # seconds\nIDLE_TIMEOUT = 60         # seconds\n\n\ndef scan_unclaimed_tasks() -> list[dict]:\n    \"\"\"Find pending, unowned tasks with all dependencies completed.\"\"\"\n    unclaimed = []\n    for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n        task = json.loads(f.read_text())\n        if (task.get(\"status\") == \"pending\"\n                and not task.get(\"owner\")\n                and can_start(task[\"id\"])):\n            unclaimed.append(task)\n    return unclaimed\n\n\ndef idle_poll(agent_name: str, messages: list,\n              name: str, role: str) -> str:\n    \"\"\"Poll for 60s. Return 'work', 'shutdown', or 'timeout'.\"\"\"\n    for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):\n        time.sleep(IDLE_POLL_INTERVAL)\n\n        # Check inbox — dispatch protocol messages first\n        inbox = BUS.read_inbox(agent_name)\n        if inbox:\n            # Check for shutdown_request\n            for msg in inbox:\n                if msg.get(\"type\") == \"shutdown_request\":\n                    req_id = msg.get(\"metadata\", {}).get(\"request_id\", \"\")\n                    BUS.send(name, \"lead\", \"Shutting down gracefully.\",\n                             \"shutdown_response\",\n                             {\"request_id\": req_id, \"approve\": True})\n                    print(f\"  \\033[35m[protocol] {name} approved shutdown \"\n                          f\"in idle ({req_id})\\033[0m\")\n                    return \"shutdown\"\n\n            # Non-protocol inbox: inject and resume work\n            messages.append({\"role\": \"user\",\n                \"content\": \"<inbox>\" + json.dumps(inbox) + \"</inbox>\"})\n            print(f\"  \\033[36m[idle] {name} found inbox messages\\033[0m\")\n            return \"work\"\n\n        # Scan task board\n        unclaimed = scan_unclaimed_tasks()\n        if unclaimed:\n            task = unclaimed[0]\n            result = claim_task(task[\"id\"], agent_name)\n            if \"Claimed\" in result:\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"<auto-claimed>Task {task['id']}: \"\n                               f\"{task['subject']}</auto-claimed>\"})\n                print(f\"  \\033[32m[idle] {name} auto-claimed: \"\n                      f\"{task['subject']}\\033[0m\")\n                return \"work\"\n            print(f\"  \\033[33m[idle] {name} claim failed: \"\n                  f\"{result}\\033[0m\")\n\n    print(f\"  \\033[31m[idle] {name} timeout ({IDLE_TIMEOUT}s)\\033[0m\")\n    return \"timeout\"\n\n\n# ── Teammate Thread (from s15 + s16 + s17) ──\n\ndef spawn_teammate_thread(name: str, role: str, prompt: str) -> str:\n    if name in active_teammates:\n        return f\"Teammate '{name}' already exists\"\n\n    system = (f\"You are '{name}', a {role}. \"\n              f\"Use tools to complete tasks. \"\n              f\"You can list and claim tasks from the board. \"\n              f\"Check inbox for protocol messages.\")\n\n    def handle_inbox_message(name: str, msg: dict, messages: list):\n        \"\"\"Dispatch incoming protocol messages by type.\"\"\"\n        msg_type = msg.get(\"type\", \"message\")\n        meta = msg.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n\n        if msg_type == \"shutdown_request\":\n            BUS.send(name, \"lead\", \"Shutting down gracefully.\",\n                     \"shutdown_response\",\n                     {\"request_id\": req_id, \"approve\": True})\n            print(f\"  \\033[35m[protocol] {name} approved shutdown \"\n                  f\"({req_id})\\033[0m\")\n            return True\n\n        if msg_type == \"plan_approval_response\":\n            approve = meta.get(\"approve\", False)\n            if approve:\n                messages.append({\"role\": \"user\",\n                    \"content\": \"[Plan approved] Proceed with the task.\"})\n            else:\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"[Plan rejected] Feedback: {msg['content']}\"})\n        return False\n\n    def run():\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        sub_tools = [\n            {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"command\": {\"type\": \"string\"}},\n                              \"required\": [\"command\"]}},\n            {\"name\": \"read_file\", \"description\": \"Read file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"}},\n                              \"required\": [\"path\"]}},\n            {\"name\": \"write_file\", \"description\": \"Write file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"send_message\",\n             \"description\": \"Send message to another agent.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"to\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"submit_plan\",\n             \"description\": \"Submit a plan for Lead approval.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"plan\": {\"type\": \"string\"}},\n                              \"required\": [\"plan\"]}},\n            # s17 new: teammates can list, claim, and complete tasks\n            {\"name\": \"list_tasks\",\n             \"description\": \"List all tasks on the board.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                              \"required\": []}},\n            {\"name\": \"claim_task\",\n             \"description\": \"Claim a pending task.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n            {\"name\": \"complete_task\",\n             \"description\": \"Mark an in-progress task as completed.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n        ]\n\n        def _run_list_tasks():\n            tasks = list_tasks()\n            if not tasks:\n                return \"No tasks.\"\n            return \"\\n\".join(\n                f\"  {t.id}: {t.subject} [{t.status}]\"\n                for t in tasks)\n\n        def _run_claim_task(task_id: str):\n            return claim_task(task_id, owner=name)\n\n        def _run_complete_task(task_id: str):\n            return complete_task(task_id)\n\n        sub_handlers = {\n            \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n            \"send_message\": lambda to, content: (BUS.send(name, to, content),\n                                                  \"Sent\")[1],\n            \"submit_plan\": lambda plan: _teammate_submit_plan(name, plan),\n            \"list_tasks\": _run_list_tasks,\n            \"claim_task\": _run_claim_task,\n            \"complete_task\": _run_complete_task,\n        }\n\n        # Outer loop: WORK → IDLE cycle\n        while True:\n            # Identity re-injection (s17)\n            if len(messages) <= 3:\n                messages.insert(0, {\"role\": \"user\",\n                    \"content\": f\"<identity>You are '{name}', role: {role}. \"\n                               f\"Continue your work.</identity>\"})\n\n            # WORK phase\n            should_shutdown = False\n            for _ in range(10):\n                inbox = BUS.read_inbox(name)\n                for msg in inbox:\n                    stopped = handle_inbox_message(name, msg, messages)\n                    if stopped:\n                        should_shutdown = True\n                        break\n                if should_shutdown:\n                    break\n                if inbox and not should_shutdown:\n                    non_protocol = [m for m in inbox\n                                    if m.get(\"type\") == \"message\"]\n                    if non_protocol:\n                        messages.append({\"role\": \"user\",\n                            \"content\": f\"<inbox>{json.dumps(non_protocol)}</inbox>\"})\n\n                try:\n                    response = client.messages.create(\n                        model=MODEL, system=system, messages=messages[-20:],\n                        tools=sub_tools, max_tokens=8000)\n                except Exception:\n                    break\n                messages.append({\"role\": \"assistant\", \"content\": response.content})\n                if response.stop_reason != \"tool_use\":\n                    break\n                results = []\n                for block in response.content:\n                    if block.type == \"tool_use\":\n                        handler = sub_handlers.get(block.name)\n                        output = handler(**block.input) if handler else \"Unknown\"\n                        results.append({\"type\": \"tool_result\",\n                                        \"tool_use_id\": block.id,\n                                        \"content\": str(output)})\n                messages.append({\"role\": \"user\", \"content\": results})\n\n            if should_shutdown:\n                break\n\n            # IDLE phase (s17 new)\n            idle_result = idle_poll(name, messages, name, role)\n            if idle_result == \"shutdown\":\n                break\n            if idle_result == \"timeout\":\n                break\n\n        # Summary\n        summary = \"Done.\"\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\" and isinstance(msg[\"content\"], list):\n                for b in msg[\"content\"]:\n                    if getattr(b, \"type\", None) == \"text\":\n                        summary = b.text\n                        break\n                else:\n                    continue\n                break\n        BUS.send(name, \"lead\", summary, \"result\")\n        active_teammates.pop(name, None)\n        print(f\"  \\033[32m[teammate] {name} finished\\033[0m\")\n\n    active_teammates[name] = True\n    threading.Thread(target=run, daemon=True).start()\n    print(f\"  \\033[36m[teammate] {name} spawned as {role}\\033[0m\")\n    return f\"Teammate '{name}' spawned as {role} (autonomous)\"\n\n\ndef _teammate_submit_plan(from_name: str, plan: str) -> str:\n    \"\"\"Teammate submits a plan to Lead for approval.\"\"\"\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"plan_approval\",\n        sender=from_name, target=\"lead\",\n        status=\"pending\", payload=plan)\n    BUS.send(from_name, \"lead\", plan,\n             \"plan_approval_request\",\n             {\"request_id\": req_id})\n    return f\"Plan submitted ({req_id}). Waiting for approval...\"\n\n\n# ── Lead Protocol Tools (from s16) ──\n\ndef run_request_shutdown(teammate: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"shutdown\",\n        sender=\"lead\", target=teammate,\n        status=\"pending\", payload=\"\")\n    BUS.send(\"lead\", teammate, \"Please shut down gracefully.\",\n             \"shutdown_request\",\n             {\"request_id\": req_id})\n    print(f\"  \\033[35m[protocol] shutdown_request → {teammate} \"\n          f\"({req_id})\\033[0m\")\n    return f\"Shutdown request sent to {teammate} (req: {req_id})\"\n\n\ndef run_request_plan(teammate: str, task: str) -> str:\n    \"\"\"Lead asks a teammate to submit a plan.\"\"\"\n    BUS.send(\"lead\", teammate, f\"Please submit a plan for: {task}\",\n             \"message\")\n    return f\"Asked {teammate} to submit a plan\"\n\n\ndef run_review_plan(request_id: str, approve: bool,\n                    feedback: str = \"\") -> str:\n    state = pending_requests.get(request_id)\n    if not state:\n        return f\"Request {request_id} not found\"\n    if state.status != \"pending\":\n        return f\"Request {request_id} already {state.status}\"\n    state.status = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", state.sender,\n             feedback or (\"Approved\" if approve else \"Rejected\"),\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n    icon = \"✓\" if approve else \"✗\"\n    print(f\"  \\033[32m[protocol] plan {icon} ({request_id})\\033[0m\")\n    return f\"Plan {'approved' if approve else 'rejected'} ({request_id})\"\n\n\n# ── Basic tool handlers ──\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks.\"\n    return \"\\n\".join(\n        f\"  {t.id}: {t.subject} [{t.status}]\"\n        for t in tasks)\n\n\ndef run_get_task(task_id: str) -> str:\n    return get_task(task_id)\n\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\n\ndef run_spawn_teammate(name: str, role: str, prompt: str) -> str:\n    return spawn_teammate_thread(name, role, prompt)\n\n\ndef run_send_message(to: str, content: str) -> str:\n    BUS.send(\"lead\", to, content)\n    return f\"Sent to {to}\"\n\n\ndef consume_lead_inbox(route_protocol=True) -> list[dict]:\n    \"\"\"Read Lead inbox: route protocol responses, return all messages.\"\"\"\n    msgs = BUS.read_inbox(\"lead\")\n    if route_protocol:\n        for msg in msgs:\n            meta = msg.get(\"metadata\", {})\n            req_id = meta.get(\"request_id\", \"\")\n            msg_type = msg.get(\"type\", \"\")\n            if req_id and msg_type.endswith(\"_response\"):\n                match_response(msg_type, req_id, meta.get(\"approve\", False))\n    return msgs\n\n\ndef run_check_inbox() -> str:\n    msgs = consume_lead_inbox(route_protocol=True)\n    if not msgs:\n        return \"(inbox empty)\"\n    lines = []\n    for m in msgs:\n        meta = m.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n        tag = f\" [{m['type']} req:{req_id}]\" if req_id else f\" [{m['type']}]\"\n        lines.append(f\"  [{m['from']}]{tag} {m['content'][:200]}\")\n    return \"\\n\".join(lines)\n\n\n# ── Tool Definitions ──\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\",\n     \"description\": \"Create a task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"subject\": {\"type\": \"string\"},\n                                     \"description\": {\"type\": \"string\"},\n                                     \"blockedBy\": {\"type\": \"array\",\n                                                   \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\",\n     \"description\": \"List all tasks.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"get_task\",\n     \"description\": \"Get full details of a specific task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\",\n     \"description\": \"Claim a pending task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\",\n     \"description\": \"Complete an in-progress task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"spawn_teammate\",\n     \"description\": \"Spawn an autonomous teammate agent.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"role\": {\"type\": \"string\"},\n                                     \"prompt\": {\"type\": \"string\"}},\n                      \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"send_message\",\n     \"description\": \"Send message to a teammate.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"to\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"check_inbox\",\n     \"description\": \"Check inbox for messages and protocol responses.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"request_shutdown\",\n     \"description\": \"Request a teammate to shut down gracefully.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\"]}},\n    {\"name\": \"request_plan\",\n     \"description\": \"Ask a teammate to submit a plan for review.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"},\n                                     \"task\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\", \"task\"]}},\n    {\"name\": \"review_plan\",\n     \"description\": \"Approve or reject a submitted plan.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"request_id\": {\"type\": \"string\"},\n                          \"approve\": {\"type\": \"boolean\"},\n                          \"feedback\": {\"type\": \"string\"}},\n                      \"required\": [\"request_id\", \"approve\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n    \"get_task\": run_get_task,\n    \"claim_task\": run_claim_task, \"complete_task\": run_complete_task,\n    \"spawn_teammate\": run_spawn_teammate,\n    \"send_message\": run_send_message, \"check_inbox\": run_check_inbox,\n    \"request_shutdown\": run_request_shutdown,\n    \"request_plan\": run_request_plan, \"review_plan\": run_review_plan,\n}\n\n\n# ── Context ──\n\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\n\n\ndef update_context(context: dict, messages: list) -> dict:\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        memories = MEMORY_INDEX.read_text()[:2000]\n    return {\"memories\": memories}\n\n\n# ── Agent Loop ──\n\ndef agent_loop(messages: list, context: dict):\n    system = get_system_prompt(context)\n    while True:\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=TOOLS, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\", \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else \"Unknown\"\n            print(str(output)[:300])\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s17: autonomous agents\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = {\"memories\": \"\"}\n    while True:\n        try:\n            query = input(\"\\033[36ms17 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n\n        # Consume lead inbox: route protocol + inject into history\n        inbox = consume_lead_inbox(route_protocol=True)\n        if inbox:\n            inbox_text = \"\\n\".join(\n                f\"From {m['from']} [{m.get('type', 'message')}]: \"\n                f\"{m['content'][:200]}\" for m in inbox)\n            history.append({\"role\": \"user\",\n                            \"content\": f\"[Inbox]\\n{inbox_text}\"})\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s17_autonomous_agents/autonomous-agents-overview.svg",
          "alt": "autonomous agents overview"
        }
      ]
    },
    {
      "id": "s18",
      "filename": "s18_worktree_isolation/code.py",
      "title": "Worktree Isolation",
      "subtitle": "Separate Directories, No Conflicts",
      "loc": 802,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "send_message",
        "submit_plan",
        "list_tasks",
        "claim_task",
        "complete_task",
        "create_task",
        "get_task",
        "spawn_teammate",
        "check_inbox",
        "request_shutdown",
        "request_plan",
        "review_plan",
        "create_worktree",
        "remove_worktree",
        "keep_worktree"
      ],
      "newTools": [
        "create_worktree",
        "remove_worktree",
        "keep_worktree"
      ],
      "coreAddition": "Worktree lifecycle",
      "keyInsight": "Parallel agents need isolated filesystems as much as isolated conversations.",
      "classes": [
        {
          "name": "Task",
          "startLine": 58,
          "endLine": 67
        },
        {
          "name": "MessageBus",
          "startLine": 347,
          "endLine": 368
        },
        {
          "name": "ProtocolState",
          "startLine": 375,
          "endLine": 384
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 68
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 84
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 88
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 92
        },
        {
          "name": "get_task_json",
          "signature": "def get_task_json(task_id: str)",
          "startLine": 97
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 102
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 112
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 133
        },
        {
          "name": "validate_worktree_name",
          "signature": "def validate_worktree_name(name: str)",
          "startLine": 156
        },
        {
          "name": "run_git",
          "signature": "def run_git(args: list[str])",
          "startLine": 168
        },
        {
          "name": "log_event",
          "signature": "def log_event(event_type: str, worktree_name: str, task_id: str = \"\")",
          "startLine": 180
        },
        {
          "name": "create_worktree",
          "signature": "def create_worktree(name: str, task_id: str = \"\")",
          "startLine": 189
        },
        {
          "name": "bind_task_to_worktree",
          "signature": "def bind_task_to_worktree(task_id: str, worktree_name: str)",
          "startLine": 207
        },
        {
          "name": "_count_worktree_changes",
          "signature": "def _count_worktree_changes(path: Path)",
          "startLine": 215
        },
        {
          "name": "remove_worktree",
          "signature": "def remove_worktree(name: str, discard_changes: bool = False)",
          "startLine": 229
        },
        {
          "name": "keep_worktree",
          "signature": "def keep_worktree(name: str)",
          "startLine": 256
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 280
        },
        {
          "name": "get_system_prompt",
          "signature": "def get_system_prompt(context: dict)",
          "startLine": 292
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str, cwd: Path = None)",
          "startLine": 303
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str, cwd: Path = None)",
          "startLine": 311
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None, cwd: Path = None)",
          "startLine": 321
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str, cwd: Path = None)",
          "startLine": 331
        },
        {
          "name": "new_request_id",
          "signature": "def new_request_id()",
          "startLine": 388
        },
        {
          "name": "match_response",
          "signature": "def match_response(response_type: str, request_id: str, approve: bool)",
          "startLine": 392
        },
        {
          "name": "consume_lead_inbox",
          "signature": "def consume_lead_inbox(route_protocol=True)",
          "startLine": 412
        },
        {
          "name": "scan_unclaimed_tasks",
          "signature": "def scan_unclaimed_tasks()",
          "startLine": 430
        },
        {
          "name": "spawn_teammate_thread",
          "signature": "def spawn_teammate_thread(name: str, role: str, prompt: str)",
          "startLine": 489
        },
        {
          "name": "_teammate_submit_plan",
          "signature": "def _teammate_submit_plan(from_name: str, plan: str)",
          "startLine": 691
        },
        {
          "name": "run_request_shutdown",
          "signature": "def run_request_shutdown(teammate: str)",
          "startLine": 705
        },
        {
          "name": "run_request_plan",
          "signature": "def run_request_plan(teammate: str, task: str)",
          "startLine": 719
        },
        {
          "name": "run_create_worktree",
          "signature": "def run_create_worktree(name: str, task_id: str = \"\")",
          "startLine": 744
        },
        {
          "name": "run_remove_worktree",
          "signature": "def run_remove_worktree(name: str, discard_changes: bool = False)",
          "startLine": 748
        },
        {
          "name": "run_keep_worktree",
          "signature": "def run_keep_worktree(name: str)",
          "startLine": 752
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 766
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 776
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 780
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 784
        },
        {
          "name": "run_spawn_teammate",
          "signature": "def run_spawn_teammate(name: str, role: str, prompt: str)",
          "startLine": 788
        },
        {
          "name": "run_send_message",
          "signature": "def run_send_message(to: str, content: str)",
          "startLine": 792
        },
        {
          "name": "run_check_inbox",
          "signature": "def run_check_inbox()",
          "startLine": 797
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 929
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 938
        }
      ],
      "layer": "collaboration",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns18: Worktree Isolation — git worktree + task-directory binding + event log.\n\nRun:  python s18_worktree_isolation/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s17:\n  - Task dataclass gains worktree field (str | None)\n  - validate_worktree_name: reject path traversal and illegal chars\n  - create_worktree: validate name, git worktree add, optional task binding\n  - bind_task_to_worktree: write worktree field only, keep task pending\n  - remove_worktree: safety check before force, no auto-complete\n  - run_git returns (ok, output), events only on success\n  - Teammate tools: + complete_task, run in worktree cwd when bound\n  - scan_unclaimed_tasks: uses can_start() for dependency checking\n  - idle_poll: checks claim result, dispatches shutdown in IDLE\n  - consume_lead_inbox: unified inbox consumer\n  - 3 new Lead tools: create_worktree, remove_worktree, keep_worktree\n\nASCII topology:\n  Main repo (/)\n    ├── .worktrees/auth/  (branch: wt/auth)  ← Task #1\n    ├── .worktrees/ui/    (branch: wt/ui)     ← Task #2\n    ├── .tasks/task_xxx.json (worktree: \"auth\")\n    └── .worktrees/events.jsonl\n\"\"\"\n\nimport os, subprocess, json, time, random, threading, re\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dataclasses import dataclass, asdict, field\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System (from s12 + s18 worktree field) ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str\n    owner: str | None\n    blockedBy: list[str]\n    worktree: str | None = None      # s18: bound worktree name\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task_json(task_id: str) -> str:\n    task = load_task(task_id)\n    return json.dumps(asdict(task), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if task.owner:\n        return f\"Task {task_id} already owned by {task.owner}\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if _task_path(d).exists() and load_task(d).status != \"completed\"]\n        missing = [d for d in task.blockedBy if not _task_path(d).exists()]\n        parts = []\n        if deps: parts.append(f\"blocked by: {deps}\")\n        if missing: parts.append(f\"missing deps: {missing}\")\n        return \"Cannot start — \" + \", \".join(parts)\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n    return msg\n\n\n# ── Worktree System (s18 new) ──\n\nWORKTREES_DIR = WORKDIR / \".worktrees\"\nWORKTREES_DIR.mkdir(exist_ok=True)\n\nVALID_WT_NAME = re.compile(r'^[A-Za-z0-9._-]{1,64}$')\n\n\ndef validate_worktree_name(name: str) -> str | None:\n    \"\"\"Return error message if invalid, None if valid.\"\"\"\n    if not name:\n        return \"Worktree name cannot be empty\"\n    if name == \".\" or name == \"..\":\n        return f\"'{name}' is not a valid worktree name\"\n    if not VALID_WT_NAME.match(name):\n        return (f\"Invalid worktree name '{name}': \"\n                \"only letters, digits, dots, underscores, dashes (1-64 chars)\")\n    return None\n\n\ndef run_git(args: list[str]) -> tuple[bool, str]:\n    \"\"\"Run git command. Return (ok, output).\"\"\"\n    try:\n        r = subprocess.run([\"git\"] + args, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=30)\n        out = (r.stdout + r.stderr).strip()\n        out = out[:5000] if out else \"(no output)\"\n        return r.returncode == 0, out\n    except subprocess.TimeoutExpired:\n        return False, \"Error: git timeout\"\n\n\ndef log_event(event_type: str, worktree_name: str, task_id: str = \"\"):\n    \"\"\"Append a lifecycle event to events.jsonl.\"\"\"\n    event = {\"type\": event_type, \"worktree\": worktree_name,\n             \"task_id\": task_id, \"ts\": time.time()}\n    events_file = WORKTREES_DIR / \"events.jsonl\"\n    with open(events_file, \"a\") as f:\n        f.write(json.dumps(event) + \"\\n\")\n\n\ndef create_worktree(name: str, task_id: str = \"\") -> str:\n    \"\"\"Create a git worktree with a dedicated branch. Optionally bind to a task.\"\"\"\n    err = validate_worktree_name(name)\n    if err:\n        return f\"Error: {err}\"\n    path = WORKTREES_DIR / name\n    if path.exists():\n        return f\"Worktree '{name}' already exists at {path}\"\n    ok, result = run_git([\"worktree\", \"add\", str(path), \"-b\", f\"wt/{name}\", \"HEAD\"])\n    if not ok:\n        return f\"Git error: {result}\"\n    if task_id:\n        bind_task_to_worktree(task_id, name)\n    log_event(\"create\", name, task_id)\n    print(f\"  \\033[33m[worktree] created: {name} at {path}\\033[0m\")\n    return f\"Worktree '{name}' created at {path}\"\n\n\ndef bind_task_to_worktree(task_id: str, worktree_name: str):\n    \"\"\"Write worktree field to task. Keep status as pending for auto-claim.\"\"\"\n    task = load_task(task_id)\n    task.worktree = worktree_name\n    save_task(task)\n    print(f\"  \\033[33m[bind] {task.subject} → worktree:{worktree_name}\\033[0m\")\n\n\ndef _count_worktree_changes(path: Path) -> tuple[int, int]:\n    \"\"\"Count uncommitted files and commits in a worktree.\"\"\"\n    try:\n        r1 = subprocess.run([\"git\", \"status\", \"--porcelain\"],\n                            cwd=path, capture_output=True, text=True, timeout=10)\n        files = len([l for l in r1.stdout.strip().splitlines() if l.strip()])\n        r2 = subprocess.run([\"git\", \"log\", \"@{push}..HEAD\", \"--oneline\"],\n                            cwd=path, capture_output=True, text=True, timeout=10)\n        commits = len([l for l in r2.stdout.strip().splitlines() if l.strip()])\n        return files, commits\n    except Exception:\n        return -1, -1\n\n\ndef remove_worktree(name: str, discard_changes: bool = False) -> str:\n    \"\"\"Remove worktree. Refuses if uncommitted changes unless discard_changes.\"\"\"\n    err = validate_worktree_name(name)\n    if err:\n        return err\n    path = WORKTREES_DIR / name\n    if not path.exists():\n        return f\"Worktree '{name}' not found\"\n    if not discard_changes:\n        files, commits = _count_worktree_changes(path)\n        if files < 0:\n            return (f\"Cannot verify worktree '{name}' status. \"\n                    \"Use discard_changes=true to force removal.\")\n        if files > 0 or commits > 0:\n            return (f\"Worktree '{name}' has {files} uncommitted file(s) \"\n                    f\"and {commits} unpushed commit(s). \"\n                    \"Use discard_changes=true to force removal, \"\n                    \"or keep_worktree to preserve for review.\")\n    ok1, _ = run_git([\"worktree\", \"remove\", str(path), \"--force\"])\n    if not ok1:\n        return f\"Failed to remove worktree directory for '{name}'\"\n    run_git([\"branch\", \"-D\", f\"wt/{name}\"])\n    log_event(\"remove\", name)\n    print(f\"  \\033[33m[worktree] removed: {name}\\033[0m\")\n    return f\"Worktree '{name}' removed\"\n\n\ndef keep_worktree(name: str) -> str:\n    \"\"\"Keep worktree for manual review. Branch preserved.\"\"\"\n    err = validate_worktree_name(name)\n    if err:\n        return err\n    log_event(\"keep\", name)\n    print(f\"  \\033[36m[worktree] kept: {name}\\033[0m\")\n    return f\"Worktree '{name}' kept for review (branch: wt/{name})\"\n\n\n# ── Prompt Assembly (from s10) ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"create_task, list_tasks, get_task, claim_task, complete_task, \"\n             \"spawn_teammate, send_message, check_inbox, \"\n             \"request_shutdown, request_plan, review_plan, \"\n             \"create_worktree, remove_worktree, keep_worktree.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    if context.get(\"memories\"):\n        sections.append(f\"Relevant memories:\\n{context['memories']}\")\n    return \"\\n\\n\".join(sections)\n\n\n_last_context_hash, _last_prompt = None, None\n\n\ndef get_system_prompt(context: dict) -> str:\n    global _last_context_hash, _last_prompt\n    h = json.dumps(context, sort_keys=True)\n    if h == _last_context_hash and _last_prompt:\n        return _last_prompt\n    _last_context_hash, _last_prompt = h, assemble_system_prompt(context)\n    return _last_prompt\n\n\n# ── Basic Tools ──\n\ndef safe_path(p: str, cwd: Path = None) -> Path:\n    base = cwd or WORKDIR\n    path = (base / p).resolve()\n    if not path.is_relative_to(base):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str, cwd: Path = None) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=cwd or WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None, cwd: Path = None) -> str:\n    try:\n        lines = safe_path(path, cwd).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str, cwd: Path = None) -> str:\n    try:\n        fp = safe_path(path, cwd)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# ── MessageBus (from s15) ──\n\nMAILBOX_DIR = WORKDIR / \".mailboxes\"\nMAILBOX_DIR.mkdir(exist_ok=True)\n\n\nclass MessageBus:\n    def send(self, from_agent: str, to_agent: str, content: str,\n             msg_type: str = \"message\", metadata: dict = None):\n        msg = {\"from\": from_agent, \"to\": to_agent,\n               \"content\": content, \"type\": msg_type,\n               \"ts\": time.time(), \"metadata\": metadata or {}}\n        inbox = MAILBOX_DIR / f\"{to_agent}.jsonl\"\n        with open(inbox, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        print(f\"  \\033[33m[bus] {from_agent} → {to_agent}: \"\n              f\"({msg_type}) {content[:50]}\\033[0m\")\n\n    def read_inbox(self, agent: str) -> list[dict]:\n        inbox = MAILBOX_DIR / f\"{agent}.jsonl\"\n        if not inbox.exists():\n            return []\n        msgs = [json.loads(line) for line in inbox.read_text().splitlines()\n                if line.strip()]\n        inbox.unlink()\n        return msgs\n\n\nBUS = MessageBus()\nactive_teammates: dict[str, bool] = {}\n\n# ── Protocol State (from s16) ──\n\n@dataclass\nclass ProtocolState:\n    request_id: str\n    type: str\n    sender: str\n    target: str\n    status: str\n    payload: str\n    created_at: float = field(default_factory=time.time)\n\n\npending_requests: dict[str, ProtocolState] = {}\n\n\ndef new_request_id() -> str:\n    return f\"req_{random.randint(0, 999999):06d}\"\n\n\ndef match_response(response_type: str, request_id: str, approve: bool):\n    state = pending_requests.get(request_id)\n    if not state:\n        print(f\"  \\033[31m[protocol] unknown request_id: {request_id}\\033[0m\")\n        return\n    if state.type == \"shutdown\" and response_type != \"shutdown_response\":\n        print(f\"  \\033[31m[protocol] type mismatch: expected shutdown_response, \"\n              f\"got {response_type}\\033[0m\")\n        return\n    if state.type == \"plan_approval\" and response_type != \"plan_approval_response\":\n        print(f\"  \\033[31m[protocol] type mismatch: expected plan_approval_response, \"\n              f\"got {response_type}\\033[0m\")\n        return\n    state.status = \"approved\" if approve else \"rejected\"\n    icon = \"✓\" if approve else \"✗\"\n    color = \"32\" if approve else \"31\"\n    print(f\"  \\033[{color}m[protocol] {state.type} {icon} \"\n          f\"({request_id}: {state.status})\\033[0m\")\n\n\ndef consume_lead_inbox(route_protocol=True) -> list[dict]:\n    msgs = BUS.read_inbox(\"lead\")\n    if route_protocol:\n        for msg in msgs:\n            meta = msg.get(\"metadata\", {})\n            req_id = meta.get(\"request_id\", \"\")\n            msg_type = msg.get(\"type\", \"\")\n            if req_id and msg_type.endswith(\"_response\"):\n                match_response(msg_type, req_id, meta.get(\"approve\", False))\n    return msgs\n\n\n# ── Autonomous Agent (from s17, + worktree cwd) ──\n\nIDLE_POLL_INTERVAL = 5\nIDLE_TIMEOUT = 60\n\n\ndef scan_unclaimed_tasks() -> list[dict]:\n    \"\"\"Find pending, unowned tasks with all dependencies completed.\"\"\"\n    unclaimed = []\n    for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n        task = json.loads(f.read_text())\n        if (task.get(\"status\") == \"pending\"\n                and not task.get(\"owner\")\n                and can_start(task[\"id\"])):\n            unclaimed.append(task)\n    return unclaimed\n\n\ndef idle_poll(agent_name: str, messages: list,\n              name: str, role: str) -> str:\n    \"\"\"Poll for 60s. Return 'work', 'shutdown', or 'timeout'.\"\"\"\n    for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):\n        time.sleep(IDLE_POLL_INTERVAL)\n\n        inbox = BUS.read_inbox(agent_name)\n        if inbox:\n            for msg in inbox:\n                if msg.get(\"type\") == \"shutdown_request\":\n                    req_id = msg.get(\"metadata\", {}).get(\"request_id\", \"\")\n                    BUS.send(name, \"lead\", \"Shutting down gracefully.\",\n                             \"shutdown_response\",\n                             {\"request_id\": req_id, \"approve\": True})\n                    print(f\"  \\033[35m[protocol] {name} approved shutdown \"\n                          f\"in idle ({req_id})\\033[0m\")\n                    return \"shutdown\"\n\n            messages.append({\"role\": \"user\",\n                \"content\": \"<inbox>\" + json.dumps(inbox) + \"</inbox>\"})\n            print(f\"  \\033[36m[idle] {name} found inbox messages\\033[0m\")\n            return \"work\"\n\n        unclaimed = scan_unclaimed_tasks()\n        if unclaimed:\n            task_data = unclaimed[0]\n            result = claim_task(task_data[\"id\"], agent_name)\n            if \"Claimed\" in result:\n                wt_info = \"\"\n                if task_data.get(\"worktree\"):\n                    wt_path = WORKTREES_DIR / task_data[\"worktree\"]\n                    wt_info = f\"\\nWork directory: {wt_path}\"\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"<auto-claimed>Task {task_data['id']}: \"\n                               f\"{task_data['subject']}{wt_info}</auto-claimed>\"})\n                print(f\"  \\033[32m[idle] {name} auto-claimed: \"\n                      f\"{task_data['subject']}\\033[0m\")\n                return \"work\"\n            print(f\"  \\033[33m[idle] {name} claim failed: \"\n                  f\"{result}\\033[0m\")\n\n    print(f\"  \\033[31m[idle] {name} timeout ({IDLE_TIMEOUT}s)\\033[0m\")\n    return \"timeout\"\n\n\n# ── Teammate Thread (from s15 + s16 + s17 + s18) ──\n\ndef spawn_teammate_thread(name: str, role: str, prompt: str) -> str:\n    if name in active_teammates:\n        return f\"Teammate '{name}' already exists\"\n\n    system = (f\"You are '{name}', a {role}. \"\n              f\"Use tools to complete tasks. \"\n              f\"You can list and claim tasks from the board. \"\n              f\"If a task has a worktree, work in that directory.\")\n\n    def handle_inbox_message(name: str, msg: dict, messages: list):\n        msg_type = msg.get(\"type\", \"message\")\n        meta = msg.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n\n        if msg_type == \"shutdown_request\":\n            BUS.send(name, \"lead\", \"Shutting down gracefully.\",\n                     \"shutdown_response\",\n                     {\"request_id\": req_id, \"approve\": True})\n            print(f\"  \\033[35m[protocol] {name} approved shutdown \"\n                  f\"({req_id})\\033[0m\")\n            return True\n\n        if msg_type == \"plan_approval_response\":\n            approve = meta.get(\"approve\", False)\n            if approve:\n                messages.append({\"role\": \"user\",\n                    \"content\": \"[Plan approved] Proceed with the task.\"})\n            else:\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"[Plan rejected] Feedback: {msg['content']}\"})\n        return False\n\n    def run():\n        # Track current worktree for this teammate's cwd\n        wt_ctx = {\"path\": None}\n\n        def _wt_cwd() -> Path | None:\n            p = wt_ctx[\"path\"]\n            return Path(p) if p else None\n\n        def _run_bash(command: str) -> str:\n            return run_bash(command, cwd=_wt_cwd())\n\n        def _run_read(path: str) -> str:\n            return run_read(path, cwd=_wt_cwd())\n\n        def _run_write(path: str, content: str) -> str:\n            return run_write(path, content, cwd=_wt_cwd())\n\n        def _run_list_tasks():\n            tasks = list_tasks()\n            if not tasks:\n                return \"No tasks.\"\n            return \"\\n\".join(\n                f\"  {t.id}: {t.subject} [{t.status}]\"\n                + (f\" (wt:{t.worktree})\" if t.worktree else \"\")\n                for t in tasks)\n\n        def _run_claim_task(task_id: str):\n            result = claim_task(task_id, owner=name)\n            if \"Claimed\" in result:\n                # Set worktree cwd if task has one\n                task = load_task(task_id)\n                if task.worktree:\n                    wt_ctx[\"path\"] = str(WORKTREES_DIR / task.worktree)\n                else:\n                    wt_ctx[\"path\"] = None\n            return result\n\n        def _run_complete_task(task_id: str):\n            result = complete_task(task_id)\n            wt_ctx[\"path\"] = None\n            return result\n\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        sub_tools = [\n            {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"command\": {\"type\": \"string\"}},\n                              \"required\": [\"command\"]}},\n            {\"name\": \"read_file\", \"description\": \"Read file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"}},\n                              \"required\": [\"path\"]}},\n            {\"name\": \"write_file\", \"description\": \"Write file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"send_message\",\n             \"description\": \"Send message to another agent.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"to\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"submit_plan\",\n             \"description\": \"Submit a plan for Lead approval.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"plan\": {\"type\": \"string\"}},\n                              \"required\": [\"plan\"]}},\n            {\"name\": \"list_tasks\",\n             \"description\": \"List all tasks on the board.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                              \"required\": []}},\n            {\"name\": \"claim_task\",\n             \"description\": \"Claim a pending task.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n            {\"name\": \"complete_task\",\n             \"description\": \"Mark an in-progress task as completed.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n        ]\n\n        sub_handlers = {\n            \"bash\": _run_bash, \"read_file\": _run_read,\n            \"write_file\": _run_write,\n            \"send_message\": lambda to, content: (BUS.send(name, to, content),\n                                                  \"Sent\")[1],\n            \"submit_plan\": lambda plan: _teammate_submit_plan(name, plan),\n            \"list_tasks\": _run_list_tasks,\n            \"claim_task\": _run_claim_task,\n            \"complete_task\": _run_complete_task,\n        }\n\n        # Outer loop: WORK → IDLE cycle\n        while True:\n            if len(messages) <= 3:\n                messages.insert(0, {\"role\": \"user\",\n                    \"content\": f\"<identity>You are '{name}', role: {role}. \"\n                               f\"Continue your work.</identity>\"})\n\n            # WORK phase\n            should_shutdown = False\n            for _ in range(10):\n                inbox = BUS.read_inbox(name)\n                for msg in inbox:\n                    stopped = handle_inbox_message(name, msg, messages)\n                    if stopped:\n                        should_shutdown = True\n                        break\n                if should_shutdown:\n                    break\n                if inbox and not should_shutdown:\n                    non_protocol = [m for m in inbox\n                                    if m.get(\"type\") == \"message\"]\n                    if non_protocol:\n                        messages.append({\"role\": \"user\",\n                            \"content\": \"<inbox>\" + json.dumps(non_protocol) + \"</inbox>\"})\n\n                try:\n                    response = client.messages.create(\n                        model=MODEL, system=system, messages=messages[-20:],\n                        tools=sub_tools, max_tokens=8000)\n                except Exception:\n                    break\n                messages.append({\"role\": \"assistant\", \"content\": response.content})\n                if response.stop_reason != \"tool_use\":\n                    break\n                results = []\n                for block in response.content:\n                    if block.type == \"tool_use\":\n                        handler = sub_handlers.get(block.name)\n                        output = handler(**block.input) if handler else \"Unknown\"\n                        results.append({\"type\": \"tool_result\",\n                                        \"tool_use_id\": block.id,\n                                        \"content\": str(output)})\n                messages.append({\"role\": \"user\", \"content\": results})\n\n            if should_shutdown:\n                break\n\n            # IDLE phase\n            idle_result = idle_poll(name, messages, name, role)\n            if idle_result == \"shutdown\":\n                break\n            if idle_result == \"timeout\":\n                break\n\n        # Summary\n        summary = \"Done.\"\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\" and isinstance(msg[\"content\"], list):\n                for b in msg[\"content\"]:\n                    if getattr(b, \"type\", None) == \"text\":\n                        summary = b.text\n                        break\n                else:\n                    continue\n                break\n        BUS.send(name, \"lead\", summary, \"result\")\n        active_teammates.pop(name, None)\n        print(f\"  \\033[32m[teammate] {name} finished\\033[0m\")\n\n    active_teammates[name] = True\n    threading.Thread(target=run, daemon=True).start()\n    print(f\"  \\033[36m[teammate] {name} spawned as {role}\\033[0m\")\n    return f\"Teammate '{name}' spawned as {role} (autonomous)\"\n\n\ndef _teammate_submit_plan(from_name: str, plan: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"plan_approval\",\n        sender=from_name, target=\"lead\",\n        status=\"pending\", payload=plan)\n    BUS.send(from_name, \"lead\", plan,\n             \"plan_approval_request\",\n             {\"request_id\": req_id})\n    return f\"Plan submitted ({req_id}). Waiting for approval...\"\n\n\n# ── Lead Protocol Tools (from s16) ──\n\ndef run_request_shutdown(teammate: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"shutdown\",\n        sender=\"lead\", target=teammate,\n        status=\"pending\", payload=\"\")\n    BUS.send(\"lead\", teammate, \"Please shut down gracefully.\",\n             \"shutdown_request\",\n             {\"request_id\": req_id})\n    print(f\"  \\033[35m[protocol] shutdown_request → {teammate} \"\n          f\"({req_id})\\033[0m\")\n    return f\"Shutdown request sent to {teammate} (req: {req_id})\"\n\n\ndef run_request_plan(teammate: str, task: str) -> str:\n    BUS.send(\"lead\", teammate, f\"Please submit a plan for: {task}\",\n             \"message\")\n    return f\"Asked {teammate} to submit a plan\"\n\n\ndef run_review_plan(request_id: str, approve: bool,\n                    feedback: str = \"\") -> str:\n    state = pending_requests.get(request_id)\n    if not state:\n        return f\"Request {request_id} not found\"\n    if state.status != \"pending\":\n        return f\"Request {request_id} already {state.status}\"\n    state.status = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", state.sender,\n             feedback or (\"Approved\" if approve else \"Rejected\"),\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n    icon = \"✓\" if approve else \"✗\"\n    print(f\"  \\033[32m[protocol] plan {icon} ({request_id})\\033[0m\")\n    return f\"Plan {'approved' if approve else 'rejected'} ({request_id})\"\n\n\n# ── Lead Worktree Tools (s18 new) ──\n\ndef run_create_worktree(name: str, task_id: str = \"\") -> str:\n    return create_worktree(name, task_id)\n\n\ndef run_remove_worktree(name: str, discard_changes: bool = False) -> str:\n    return remove_worktree(name, discard_changes)\n\n\ndef run_keep_worktree(name: str) -> str:\n    return keep_worktree(name)\n\n\n# ── Basic tool handlers ──\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks.\"\n    return \"\\n\".join(\n        f\"  {t.id}: {t.subject} [{t.status}]\"\n        + (f\" (wt:{t.worktree})\" if t.worktree else \"\")\n        for t in tasks)\n\n\ndef run_get_task(task_id: str) -> str:\n    return get_task_json(task_id)\n\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\n\ndef run_spawn_teammate(name: str, role: str, prompt: str) -> str:\n    return spawn_teammate_thread(name, role, prompt)\n\n\ndef run_send_message(to: str, content: str) -> str:\n    BUS.send(\"lead\", to, content)\n    return f\"Sent to {to}\"\n\n\ndef run_check_inbox() -> str:\n    msgs = consume_lead_inbox(route_protocol=True)\n    if not msgs:\n        return \"(inbox empty)\"\n    lines = []\n    for m in msgs:\n        meta = m.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n        tag = f\" [{m['type']} req:{req_id}]\" if req_id else f\" [{m['type']}]\"\n        lines.append(f\"  [{m['from']}]{tag} {m['content'][:200]}\")\n    return \"\\n\".join(lines)\n\n\n# ── Tool Definitions ──\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\",\n     \"description\": \"Create a task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"subject\": {\"type\": \"string\"},\n                                     \"description\": {\"type\": \"string\"},\n                                     \"blockedBy\": {\"type\": \"array\",\n                                                   \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\",\n     \"description\": \"List all tasks.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"get_task\",\n     \"description\": \"Get full details of a specific task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\",\n     \"description\": \"Claim a pending task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\",\n     \"description\": \"Complete an in-progress task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"spawn_teammate\",\n     \"description\": \"Spawn an autonomous teammate agent.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"role\": {\"type\": \"string\"},\n                                     \"prompt\": {\"type\": \"string\"}},\n                      \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"send_message\",\n     \"description\": \"Send message to a teammate.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"to\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"check_inbox\",\n     \"description\": \"Check inbox for messages and protocol responses.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"request_shutdown\",\n     \"description\": \"Request a teammate to shut down gracefully.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\"]}},\n    {\"name\": \"request_plan\",\n     \"description\": \"Ask a teammate to submit a plan for review.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"},\n                                     \"task\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\", \"task\"]}},\n    {\"name\": \"review_plan\",\n     \"description\": \"Approve or reject a submitted plan.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\n                          \"request_id\": {\"type\": \"string\"},\n                          \"approve\": {\"type\": \"boolean\"},\n                          \"feedback\": {\"type\": \"string\"}},\n                      \"required\": [\"request_id\", \"approve\"]}},\n    # s18 new: worktree tools\n    {\"name\": \"create_worktree\",\n     \"description\": \"Create an isolated git worktree with its own branch.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"remove_worktree\",\n     \"description\": \"Remove a worktree. Refuses if uncommitted changes unless discard_changes=true.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"discard_changes\": {\"type\": \"boolean\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"keep_worktree\",\n     \"description\": \"Keep a worktree for manual review.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n]\n\nTOOL_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n    \"get_task\": run_get_task,\n    \"claim_task\": run_claim_task, \"complete_task\": run_complete_task,\n    \"spawn_teammate\": run_spawn_teammate,\n    \"send_message\": run_send_message, \"check_inbox\": run_check_inbox,\n    \"request_shutdown\": run_request_shutdown,\n    \"request_plan\": run_request_plan, \"review_plan\": run_review_plan,\n    \"create_worktree\": run_create_worktree,\n    \"remove_worktree\": run_remove_worktree,\n    \"keep_worktree\": run_keep_worktree,\n}\n\n\n# ── Context ──\n\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\n\n\ndef update_context(context: dict, messages: list) -> dict:\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        memories = MEMORY_INDEX.read_text()[:2000]\n    return {\"memories\": memories}\n\n\n# ── Agent Loop ──\n\ndef agent_loop(messages: list, context: dict):\n    system = get_system_prompt(context)\n    while True:\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=TOOLS, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\", \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n            handler = TOOL_HANDLERS.get(block.name)\n            output = handler(**block.input) if handler else \"Unknown\"\n            print(str(output)[:300])\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n        context = update_context(context, messages)\n        system = get_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s18: worktree isolation\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = {\"memories\": \"\"}\n    while True:\n        try:\n            query = input(\"\\033[36ms18 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n\n        # Consume lead inbox: route protocol + inject into history\n        inbox = consume_lead_inbox(route_protocol=True)\n        if inbox:\n            inbox_text = \"\\n\".join(\n                f\"From {m['from']} [{m.get('type', 'message')}]: \"\n                f\"{m['content'][:200]}\" for m in inbox)\n            history.append({\"role\": \"user\",\n                            \"content\": f\"[Inbox]\\n{inbox_text}\"})\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s18_worktree_isolation/worktree-overview.svg",
          "alt": "worktree overview"
        }
      ]
    },
    {
      "id": "s19",
      "filename": "s19_mcp_plugin/code.py",
      "title": "MCP Tools",
      "subtitle": "External Tools, Standard Protocol",
      "loc": 835,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "send_message",
        "submit_plan",
        "list_tasks",
        "claim_task",
        "complete_task",
        "search",
        "get_version",
        "trigger",
        "status",
        "create_task",
        "get_task",
        "spawn_teammate",
        "check_inbox",
        "request_shutdown",
        "request_plan",
        "review_plan",
        "create_worktree",
        "remove_worktree",
        "keep_worktree",
        "connect_mcp"
      ],
      "newTools": [
        "search",
        "get_version",
        "trigger",
        "status",
        "connect_mcp"
      ],
      "coreAddition": "MCP tool bridge",
      "keyInsight": "External services can become agent tools through a standard discovery and call protocol.",
      "classes": [
        {
          "name": "Task",
          "startLine": 53,
          "endLine": 62
        },
        {
          "name": "MessageBus",
          "startLine": 319,
          "endLine": 340
        },
        {
          "name": "ProtocolState",
          "startLine": 347,
          "endLine": 356
        },
        {
          "name": "MCPClient",
          "startLine": 660,
          "endLine": 682
        }
      ],
      "functions": [
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 63
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 79
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 83
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 87
        },
        {
          "name": "get_task_json",
          "signature": "def get_task_json(task_id: str)",
          "startLine": 92
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 96
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 106
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 127
        },
        {
          "name": "validate_worktree_name",
          "signature": "def validate_worktree_name(name: str)",
          "startLine": 150
        },
        {
          "name": "run_git",
          "signature": "def run_git(args: list[str])",
          "startLine": 161
        },
        {
          "name": "log_event",
          "signature": "def log_event(event_type: str, worktree_name: str, task_id: str = \"\")",
          "startLine": 171
        },
        {
          "name": "create_worktree",
          "signature": "def create_worktree(name: str, task_id: str = \"\")",
          "startLine": 179
        },
        {
          "name": "bind_task_to_worktree",
          "signature": "def bind_task_to_worktree(task_id: str, worktree_name: str)",
          "startLine": 196
        },
        {
          "name": "_count_worktree_changes",
          "signature": "def _count_worktree_changes(path: Path)",
          "startLine": 202
        },
        {
          "name": "remove_worktree",
          "signature": "def remove_worktree(name: str, discard_changes: bool = False)",
          "startLine": 215
        },
        {
          "name": "keep_worktree",
          "signature": "def keep_worktree(name: str)",
          "startLine": 238
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 261
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str, cwd: Path = None)",
          "startLine": 275
        },
        {
          "name": "run_bash",
          "signature": "def run_bash(command: str, cwd: Path = None)",
          "startLine": 283
        },
        {
          "name": "run_read",
          "signature": "def run_read(path: str, limit: int | None = None, cwd: Path = None)",
          "startLine": 293
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str, cwd: Path = None)",
          "startLine": 303
        },
        {
          "name": "new_request_id",
          "signature": "def new_request_id()",
          "startLine": 360
        },
        {
          "name": "match_response",
          "signature": "def match_response(response_type: str, request_id: str, approve: bool)",
          "startLine": 364
        },
        {
          "name": "consume_lead_inbox",
          "signature": "def consume_lead_inbox(route_protocol=True)",
          "startLine": 375
        },
        {
          "name": "scan_unclaimed_tasks",
          "signature": "def scan_unclaimed_tasks()",
          "startLine": 393
        },
        {
          "name": "spawn_teammate_thread",
          "signature": "def spawn_teammate_thread(name: str, role: str, prompt: str)",
          "startLine": 437
        },
        {
          "name": "_teammate_submit_plan",
          "signature": "def _teammate_submit_plan(from_name: str, plan: str)",
          "startLine": 615
        },
        {
          "name": "run_request_shutdown",
          "signature": "def run_request_shutdown(teammate: str)",
          "startLine": 629
        },
        {
          "name": "run_request_plan",
          "signature": "def run_request_plan(teammate: str, task: str)",
          "startLine": 640
        },
        {
          "name": "normalize_mcp_name",
          "signature": "def normalize_mcp_name(name: str)",
          "startLine": 688
        },
        {
          "name": "_mock_server_docs",
          "signature": "def _mock_server_docs()",
          "startLine": 693
        },
        {
          "name": "_mock_server_deploy",
          "signature": "def _mock_server_deploy()",
          "startLine": 712
        },
        {
          "name": "connect_mcp",
          "signature": "def connect_mcp(name: str)",
          "startLine": 739
        },
        {
          "name": "assemble_tool_pool",
          "signature": "def assemble_tool_pool()",
          "startLine": 754
        },
        {
          "name": "run_create_worktree",
          "signature": "def run_create_worktree(name: str, task_id: str = \"\")",
          "startLine": 775
        },
        {
          "name": "run_remove_worktree",
          "signature": "def run_remove_worktree(name: str, discard_changes: bool = False)",
          "startLine": 778
        },
        {
          "name": "run_keep_worktree",
          "signature": "def run_keep_worktree(name: str)",
          "startLine": 781
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 795
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 805
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 808
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 811
        },
        {
          "name": "run_spawn_teammate",
          "signature": "def run_spawn_teammate(name: str, role: str, prompt: str)",
          "startLine": 814
        },
        {
          "name": "run_send_message",
          "signature": "def run_send_message(to: str, content: str)",
          "startLine": 817
        },
        {
          "name": "run_check_inbox",
          "signature": "def run_check_inbox()",
          "startLine": 821
        },
        {
          "name": "run_connect_mcp",
          "signature": "def run_connect_mcp(name: str)",
          "startLine": 833
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 953
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 962
        }
      ],
      "layer": "collaboration",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns19: MCP Tools — MCPClient + tool discovery + assemble_tool_pool.\n\nRun:  python s19_mcp_plugin/code.py\nNeed: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY\n\nChanges from s18:\n  - MCPClient class: discovers tools, calls tools via mock handler\n  - normalize_mcp_name: normalize tool/server names\n  - assemble_tool_pool: assembles builtin + MCP tools into one pool\n  - connect_mcp: connect to an MCP server, discover tools\n  - Tool naming: mcp__{server}__{tool} with normalization\n  - MCP tools have readOnly/destructive annotations\n  - agent_loop uses dynamic tool pool (builtin + MCP), no prompt cache\n  - Teammate tools: complete_task, worktree cwd (from s17/s18 fixes)\n\nASCII flow:\n  connect_mcp(\"docs\") → MCPClient discovers tools →\n  assemble_tool_pool → [builtin... , mcp__docs__search, mcp__docs__get_version]\n  agent_loop uses assembled pool\n\"\"\"\n\nimport os, subprocess, json, time, random, threading, re\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dataclasses import dataclass, asdict, field\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\nexcept ImportError:\n    pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n# ── Task System ──\n\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str\n    owner: str | None\n    blockedBy: list[str]\n    worktree: str | None = None\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task_json(task_id: str) -> str:\n    return json.dumps(asdict(load_task(task_id)), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if task.owner:\n        return f\"Task {task_id} already owned by {task.owner}\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if _task_path(d).exists() and load_task(d).status != \"completed\"]\n        missing = [d for d in task.blockedBy if not _task_path(d).exists()]\n        parts = []\n        if deps: parts.append(f\"blocked by: {deps}\")\n        if missing: parts.append(f\"missing deps: {missing}\")\n        return \"Cannot start — \" + \", \".join(parts)\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n    return msg\n\n\n# ── Worktree System ──\n\nWORKTREES_DIR = WORKDIR / \".worktrees\"\nWORKTREES_DIR.mkdir(exist_ok=True)\n\nVALID_WT_NAME = re.compile(r'^[A-Za-z0-9._-]{1,64}$')\n\n\ndef validate_worktree_name(name: str) -> str | None:\n    if not name:\n        return \"Worktree name cannot be empty\"\n    if name in (\".\", \"..\"):\n        return f\"'{name}' is not a valid worktree name\"\n    if not VALID_WT_NAME.match(name):\n        return (f\"Invalid worktree name '{name}': \"\n                \"only letters, digits, dots, underscores, dashes (1-64 chars)\")\n    return None\n\n\ndef run_git(args: list[str]) -> tuple[bool, str]:\n    try:\n        r = subprocess.run([\"git\"] + args, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=30)\n        out = (r.stdout + r.stderr).strip()\n        return r.returncode == 0, out[:5000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return False, \"Error: git timeout\"\n\n\ndef log_event(event_type: str, worktree_name: str, task_id: str = \"\"):\n    event = {\"type\": event_type, \"worktree\": worktree_name,\n             \"task_id\": task_id, \"ts\": time.time()}\n    events_file = WORKTREES_DIR / \"events.jsonl\"\n    with open(events_file, \"a\") as f:\n        f.write(json.dumps(event) + \"\\n\")\n\n\ndef create_worktree(name: str, task_id: str = \"\") -> str:\n    err = validate_worktree_name(name)\n    if err:\n        return f\"Error: {err}\"\n    path = WORKTREES_DIR / name\n    if path.exists():\n        return f\"Worktree '{name}' already exists at {path}\"\n    ok, result = run_git([\"worktree\", \"add\", str(path), \"-b\", f\"wt/{name}\", \"HEAD\"])\n    if not ok:\n        return f\"Git error: {result}\"\n    if task_id:\n        bind_task_to_worktree(task_id, name)\n    log_event(\"create\", name, task_id)\n    print(f\"  \\033[33m[worktree] created: {name} at {path}\\033[0m\")\n    return f\"Worktree '{name}' created at {path}\"\n\n\ndef bind_task_to_worktree(task_id: str, worktree_name: str):\n    task = load_task(task_id)\n    task.worktree = worktree_name\n    save_task(task)\n\n\ndef _count_worktree_changes(path: Path) -> tuple[int, int]:\n    try:\n        r1 = subprocess.run([\"git\", \"status\", \"--porcelain\"],\n                            cwd=path, capture_output=True, text=True, timeout=10)\n        files = len([l for l in r1.stdout.strip().splitlines() if l.strip()])\n        r2 = subprocess.run([\"git\", \"log\", \"@{push}..HEAD\", \"--oneline\"],\n                            cwd=path, capture_output=True, text=True, timeout=10)\n        commits = len([l for l in r2.stdout.strip().splitlines() if l.strip()])\n        return files, commits\n    except Exception:\n        return -1, -1\n\n\ndef remove_worktree(name: str, discard_changes: bool = False) -> str:\n    err = validate_worktree_name(name)\n    if err:\n        return err\n    path = WORKTREES_DIR / name\n    if not path.exists():\n        return f\"Worktree '{name}' not found\"\n    if not discard_changes:\n        files, commits = _count_worktree_changes(path)\n        if files < 0:\n            return \"Cannot verify status. Use discard_changes=true to force.\"\n        if files > 0 or commits > 0:\n            return (f\"Worktree '{name}' has {files} file(s), {commits} commit(s). \"\n                    \"Use discard_changes=true or keep_worktree.\")\n    ok1, _ = run_git([\"worktree\", \"remove\", str(path), \"--force\"])\n    if not ok1:\n        return f\"Failed to remove worktree '{name}'\"\n    run_git([\"branch\", \"-D\", f\"wt/{name}\"])\n    log_event(\"remove\", name)\n    print(f\"  \\033[33m[worktree] removed: {name}\\033[0m\")\n    return f\"Worktree '{name}' removed\"\n\n\ndef keep_worktree(name: str) -> str:\n    err = validate_worktree_name(name)\n    if err:\n        return err\n    log_event(\"keep\", name)\n    return f\"Worktree '{name}' kept for review (branch: wt/{name})\"\n\n\n# ── Prompt Assembly ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, \"\n             \"create_task, list_tasks, get_task, claim_task, complete_task, \"\n             \"spawn_teammate, send_message, check_inbox, \"\n             \"request_shutdown, request_plan, review_plan, \"\n             \"create_worktree, remove_worktree, keep_worktree, \"\n             \"connect_mcp. MCP tools are prefixed mcp__{server}__{tool}.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    if context.get(\"memories\"):\n        sections.append(f\"Relevant memories:\\n{context['memories']}\")\n    mcp_names = list(mcp_clients.keys())\n    if mcp_names:\n        sections.append(f\"Connected MCP servers: {', '.join(mcp_names)}\")\n    return \"\\n\\n\".join(sections)\n\n\n# ── Basic Tools ──\n\ndef safe_path(p: str, cwd: Path = None) -> Path:\n    base = cwd or WORKDIR\n    path = (base / p).resolve()\n    if not path.is_relative_to(base):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str, cwd: Path = None) -> str:\n    try:\n        r = subprocess.run(command, shell=True, cwd=cwd or WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None, cwd: Path = None) -> str:\n    try:\n        lines = safe_path(path, cwd).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str, cwd: Path = None) -> str:\n    try:\n        fp = safe_path(path, cwd)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# ── MessageBus ──\n\nMAILBOX_DIR = WORKDIR / \".mailboxes\"\nMAILBOX_DIR.mkdir(exist_ok=True)\n\n\nclass MessageBus:\n    def send(self, from_agent: str, to_agent: str, content: str,\n             msg_type: str = \"message\", metadata: dict = None):\n        msg = {\"from\": from_agent, \"to\": to_agent,\n               \"content\": content, \"type\": msg_type,\n               \"ts\": time.time(), \"metadata\": metadata or {}}\n        inbox = MAILBOX_DIR / f\"{to_agent}.jsonl\"\n        with open(inbox, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        print(f\"  \\033[33m[bus] {from_agent} → {to_agent}: \"\n              f\"({msg_type}) {content[:50]}\\033[0m\")\n\n    def read_inbox(self, agent: str) -> list[dict]:\n        inbox = MAILBOX_DIR / f\"{agent}.jsonl\"\n        if not inbox.exists():\n            return []\n        msgs = [json.loads(line) for line in inbox.read_text().splitlines()\n                if line.strip()]\n        inbox.unlink()\n        return msgs\n\n\nBUS = MessageBus()\nactive_teammates: dict[str, bool] = {}\n\n# ── Protocol State ──\n\n@dataclass\nclass ProtocolState:\n    request_id: str\n    type: str\n    sender: str\n    target: str\n    status: str\n    payload: str\n    created_at: float = field(default_factory=time.time)\n\n\npending_requests: dict[str, ProtocolState] = {}\n\n\ndef new_request_id() -> str:\n    return f\"req_{random.randint(0, 999999):06d}\"\n\n\ndef match_response(response_type: str, request_id: str, approve: bool):\n    state = pending_requests.get(request_id)\n    if not state:\n        return\n    if state.type == \"shutdown\" and response_type != \"shutdown_response\":\n        return\n    if state.type == \"plan_approval\" and response_type != \"plan_approval_response\":\n        return\n    state.status = \"approved\" if approve else \"rejected\"\n\n\ndef consume_lead_inbox(route_protocol=True) -> list[dict]:\n    msgs = BUS.read_inbox(\"lead\")\n    if route_protocol:\n        for msg in msgs:\n            meta = msg.get(\"metadata\", {})\n            req_id = meta.get(\"request_id\", \"\")\n            msg_type = msg.get(\"type\", \"\")\n            if req_id and msg_type.endswith(\"_response\"):\n                match_response(msg_type, req_id, meta.get(\"approve\", False))\n    return msgs\n\n\n# ── Autonomous Agent ──\n\nIDLE_POLL_INTERVAL = 5\nIDLE_TIMEOUT = 60\n\n\ndef scan_unclaimed_tasks() -> list[dict]:\n    unclaimed = []\n    for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n        task = json.loads(f.read_text())\n        if (task.get(\"status\") == \"pending\"\n                and not task.get(\"owner\")\n                and can_start(task[\"id\"])):\n            unclaimed.append(task)\n    return unclaimed\n\n\ndef idle_poll(agent_name: str, messages: list,\n              name: str, role: str) -> str:\n    for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):\n        time.sleep(IDLE_POLL_INTERVAL)\n        inbox = BUS.read_inbox(agent_name)\n        if inbox:\n            for msg in inbox:\n                if msg.get(\"type\") == \"shutdown_request\":\n                    req_id = msg.get(\"metadata\", {}).get(\"request_id\", \"\")\n                    BUS.send(name, \"lead\", \"Shutting down.\",\n                             \"shutdown_response\",\n                             {\"request_id\": req_id, \"approve\": True})\n                    return \"shutdown\"\n            messages.append({\"role\": \"user\",\n                \"content\": \"<inbox>\" + json.dumps(inbox) + \"</inbox>\"})\n            return \"work\"\n        unclaimed = scan_unclaimed_tasks()\n        if unclaimed:\n            task_data = unclaimed[0]\n            result = claim_task(task_data[\"id\"], agent_name)\n            if \"Claimed\" in result:\n                wt_info = \"\"\n                if task_data.get(\"worktree\"):\n                    wt_info = f\"\\nWork directory: {WORKTREES_DIR / task_data['worktree']}\"\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"<auto-claimed>Task {task_data['id']}: \"\n                               f\"{task_data['subject']}{wt_info}</auto-claimed>\"})\n                return \"work\"\n    return \"timeout\"\n\n\n# ── Teammate Thread ──\n\ndef spawn_teammate_thread(name: str, role: str, prompt: str) -> str:\n    if name in active_teammates:\n        return f\"Teammate '{name}' already exists\"\n\n    system = (f\"You are '{name}', a {role}. \"\n              f\"Use tools to complete tasks. \"\n              f\"If a task has a worktree, work in that directory.\")\n\n    def handle_inbox_message(name: str, msg: dict, messages: list):\n        msg_type = msg.get(\"type\", \"message\")\n        meta = msg.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n        if msg_type == \"shutdown_request\":\n            BUS.send(name, \"lead\", \"Shutting down.\",\n                     \"shutdown_response\",\n                     {\"request_id\": req_id, \"approve\": True})\n            return True\n        if msg_type == \"plan_approval_response\":\n            approve = meta.get(\"approve\", False)\n            messages.append({\"role\": \"user\",\n                \"content\": \"[Plan approved]\" if approve\n                           else f\"[Plan rejected] {msg['content']}\"})\n        return False\n\n    def run():\n        wt_ctx = {\"path\": None}\n\n        def _wt_cwd():\n            p = wt_ctx[\"path\"]\n            return Path(p) if p else None\n\n        def _run_bash(command: str) -> str:\n            return run_bash(command, cwd=_wt_cwd())\n\n        def _run_read(path: str) -> str:\n            return run_read(path, cwd=_wt_cwd())\n\n        def _run_write(path: str, content: str) -> str:\n            return run_write(path, content, cwd=_wt_cwd())\n\n        def _run_list_tasks():\n            tasks = list_tasks()\n            if not tasks:\n                return \"No tasks.\"\n            return \"\\n\".join(\n                f\"  {t.id}: {t.subject} [{t.status}]\"\n                + (f\" (wt:{t.worktree})\" if t.worktree else \"\")\n                for t in tasks)\n\n        def _run_claim_task(task_id: str):\n            result = claim_task(task_id, owner=name)\n            if \"Claimed\" in result:\n                task = load_task(task_id)\n                wt_ctx[\"path\"] = (str(WORKTREES_DIR / task.worktree)\n                                  if task.worktree else None)\n            return result\n\n        def _run_complete_task(task_id: str):\n            result = complete_task(task_id)\n            wt_ctx[\"path\"] = None\n            return result\n\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        sub_tools = [\n            {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"command\": {\"type\": \"string\"}},\n                              \"required\": [\"command\"]}},\n            {\"name\": \"read_file\", \"description\": \"Read file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"}},\n                              \"required\": [\"path\"]}},\n            {\"name\": \"write_file\", \"description\": \"Write file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"send_message\",\n             \"description\": \"Send message to another agent.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"to\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"submit_plan\",\n             \"description\": \"Submit a plan for Lead approval.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"plan\": {\"type\": \"string\"}},\n                              \"required\": [\"plan\"]}},\n            {\"name\": \"list_tasks\",\n             \"description\": \"List all tasks.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                              \"required\": []}},\n            {\"name\": \"claim_task\",\n             \"description\": \"Claim a pending task.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n            {\"name\": \"complete_task\",\n             \"description\": \"Mark an in-progress task as completed.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n        ]\n\n        sub_handlers = {\n            \"bash\": _run_bash, \"read_file\": _run_read,\n            \"write_file\": _run_write,\n            \"send_message\": lambda to, content: (BUS.send(name, to, content),\n                                                  \"Sent\")[1],\n            \"submit_plan\": lambda plan: _teammate_submit_plan(name, plan),\n            \"list_tasks\": _run_list_tasks,\n            \"claim_task\": _run_claim_task,\n            \"complete_task\": _run_complete_task,\n        }\n\n        while True:\n            if len(messages) <= 3:\n                messages.insert(0, {\"role\": \"user\",\n                    \"content\": f\"<identity>You are '{name}', role: {role}. \"\n                               f\"Continue your work.</identity>\"})\n            should_shutdown = False\n            for _ in range(10):\n                inbox = BUS.read_inbox(name)\n                for msg in inbox:\n                    stopped = handle_inbox_message(name, msg, messages)\n                    if stopped:\n                        should_shutdown = True\n                        break\n                if should_shutdown:\n                    break\n                if inbox and not should_shutdown:\n                    non_protocol = [m for m in inbox\n                                    if m.get(\"type\") == \"message\"]\n                    if non_protocol:\n                        messages.append({\"role\": \"user\",\n                            \"content\": \"<inbox>\" + json.dumps(non_protocol) + \"</inbox>\"})\n                try:\n                    response = client.messages.create(\n                        model=MODEL, system=system, messages=messages[-20:],\n                        tools=sub_tools, max_tokens=8000)\n                except Exception:\n                    break\n                messages.append({\"role\": \"assistant\", \"content\": response.content})\n                if response.stop_reason != \"tool_use\":\n                    break\n                results = []\n                for block in response.content:\n                    if block.type == \"tool_use\":\n                        handler = sub_handlers.get(block.name)\n                        output = handler(**block.input) if handler else \"Unknown\"\n                        results.append({\"type\": \"tool_result\",\n                                        \"tool_use_id\": block.id,\n                                        \"content\": str(output)})\n                messages.append({\"role\": \"user\", \"content\": results})\n            if should_shutdown:\n                break\n            idle_result = idle_poll(name, messages, name, role)\n            if idle_result in (\"shutdown\", \"timeout\"):\n                break\n\n        summary = \"Done.\"\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\" and isinstance(msg[\"content\"], list):\n                for b in msg[\"content\"]:\n                    if getattr(b, \"type\", None) == \"text\":\n                        summary = b.text\n                        break\n                else:\n                    continue\n                break\n        BUS.send(name, \"lead\", summary, \"result\")\n        active_teammates.pop(name, None)\n\n    active_teammates[name] = True\n    threading.Thread(target=run, daemon=True).start()\n    return f\"Teammate '{name}' spawned as {role}\"\n\n\ndef _teammate_submit_plan(from_name: str, plan: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"plan_approval\",\n        sender=from_name, target=\"lead\",\n        status=\"pending\", payload=plan)\n    BUS.send(from_name, \"lead\", plan,\n             \"plan_approval_request\",\n             {\"request_id\": req_id})\n    return f\"Plan submitted ({req_id})\"\n\n\n# ── Lead Protocol Tools ──\n\ndef run_request_shutdown(teammate: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"shutdown\",\n        sender=\"lead\", target=teammate,\n        status=\"pending\", payload=\"\")\n    BUS.send(\"lead\", teammate, \"Shut down.\", \"shutdown_request\",\n             {\"request_id\": req_id})\n    return f\"Shutdown request sent to {teammate}\"\n\n\ndef run_request_plan(teammate: str, task: str) -> str:\n    BUS.send(\"lead\", teammate, f\"Submit plan for: {task}\", \"message\")\n    return f\"Asked {teammate} to submit a plan\"\n\n\ndef run_review_plan(request_id: str, approve: bool,\n                    feedback: str = \"\") -> str:\n    state = pending_requests.get(request_id)\n    if not state:\n        return f\"Request {request_id} not found\"\n    state.status = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", state.sender,\n             feedback or (\"Approved\" if approve else \"Rejected\"),\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n    return f\"Plan {'approved' if approve else 'rejected'}\"\n\n\n# ── MCP System (s19 new) ──\n\nclass MCPClient:\n    \"\"\"Discovers and calls tools on an MCP server (mock for teaching).\"\"\"\n\n    def __init__(self, name: str):\n        self.name = name\n        self.tools: list[dict] = []\n        self._handlers: dict[str, callable] = {}\n\n    def register(self, tool_defs: list[dict],\n                 handlers: dict[str, callable]):\n        self.tools = tool_defs\n        self._handlers = handlers\n\n    def call_tool(self, tool_name: str, args: dict) -> str:\n        handler = self._handlers.get(tool_name)\n        if not handler:\n            return f\"MCP error: unknown tool '{tool_name}'\"\n        try:\n            return handler(**args)\n        except Exception as e:\n            return f\"MCP error: {e}\"\n\n\nmcp_clients: dict[str, MCPClient] = {}\n\n_DISALLOWED_CHARS = re.compile(r'[^a-zA-Z0-9_-]')\n\n\ndef normalize_mcp_name(name: str) -> str:\n    \"\"\"Replace non [a-zA-Z0-9_-] with underscore.\"\"\"\n    return _DISALLOWED_CHARS.sub('_', name)\n\n\ndef _mock_server_docs():\n    client = MCPClient(\"docs\")\n    client.register(\n        tool_defs=[\n            {\"name\": \"search\", \"description\": \"Search documentation. (readOnly)\",\n             \"inputSchema\": {\"type\": \"object\",\n                             \"properties\": {\"query\": {\"type\": \"string\"}},\n                             \"required\": [\"query\"]}},\n            {\"name\": \"get_version\", \"description\": \"Get API version. (readOnly)\",\n             \"inputSchema\": {\"type\": \"object\", \"properties\": {},\n                             \"required\": []}},\n        ],\n        handlers={\n            \"search\": lambda query: f\"[docs] Found 3 results for '{query}'\",\n            \"get_version\": lambda: \"[docs] API v2.1.0\",\n        })\n    return client\n\n\ndef _mock_server_deploy():\n    client = MCPClient(\"deploy\")\n    client.register(\n        tool_defs=[\n            {\"name\": \"trigger\",\n             \"description\": \"Trigger a deployment. (destructive — requires approval in real CC)\",\n             \"inputSchema\": {\"type\": \"object\",\n                             \"properties\": {\"service\": {\"type\": \"string\"}},\n                             \"required\": [\"service\"]}},\n            {\"name\": \"status\", \"description\": \"Check deployment status. (readOnly)\",\n             \"inputSchema\": {\"type\": \"object\",\n                             \"properties\": {\"service\": {\"type\": \"string\"}},\n                             \"required\": [\"service\"]}},\n        ],\n        handlers={\n            \"trigger\": lambda service: f\"[deploy] Triggered: {service}\",\n            \"status\": lambda service: f\"[deploy] {service}: running (v1.4.2)\",\n        })\n    return client\n\n\nMOCK_SERVERS = {\n    \"docs\": _mock_server_docs,\n    \"deploy\": _mock_server_deploy,\n}\n\n\ndef connect_mcp(name: str) -> str:\n    if name in mcp_clients:\n        return f\"MCP server '{name}' already connected\"\n    factory = MOCK_SERVERS.get(name)\n    if not factory:\n        available = \", \".join(MOCK_SERVERS.keys())\n        return f\"Unknown server '{name}'. Available: {available}\"\n    mcp_client = factory()\n    mcp_clients[name] = mcp_client\n    tool_names = [t[\"name\"] for t in mcp_client.tools]\n    print(f\"  \\033[31m[mcp] connected: {name} → {tool_names}\\033[0m\")\n    return (f\"Connected to MCP server '{name}'. \"\n            f\"Discovered {len(mcp_client.tools)} tools: {', '.join(tool_names)}\")\n\n\ndef assemble_tool_pool() -> tuple[list[dict], dict]:\n    \"\"\"Assemble builtin tools + all MCP tools into one pool.\"\"\"\n    tools = list(BUILTIN_TOOLS)\n    handlers = dict(BUILTIN_HANDLERS)\n    for server_name, mcp_client in mcp_clients.items():\n        safe_server = normalize_mcp_name(server_name)\n        for tool_def in mcp_client.tools:\n            safe_tool = normalize_mcp_name(tool_def[\"name\"])\n            prefixed = f\"mcp__{safe_server}__{safe_tool}\"\n            tools.append({\n                \"name\": prefixed,\n                \"description\": tool_def.get(\"description\", \"\"),\n                \"input_schema\": tool_def.get(\"inputSchema\", {}),\n            })\n            handlers[prefixed] = (\n                lambda *, c=mcp_client, t=tool_def[\"name\"], **kw: c.call_tool(t, kw))\n    return tools, handlers\n\n\n# ── Lead Worktree Tools ──\n\ndef run_create_worktree(name: str, task_id: str = \"\") -> str:\n    return create_worktree(name, task_id)\n\ndef run_remove_worktree(name: str, discard_changes: bool = False) -> str:\n    return remove_worktree(name, discard_changes)\n\ndef run_keep_worktree(name: str) -> str:\n    return keep_worktree(name)\n\n\n# ── Basic tool handlers ──\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks.\"\n    return \"\\n\".join(\n        f\"  {t.id}: {t.subject} [{t.status}]\"\n        + (f\" (wt:{t.worktree})\" if t.worktree else \"\")\n        for t in tasks)\n\n\ndef run_get_task(task_id: str) -> str:\n    return get_task_json(task_id)\n\ndef run_claim_task(task_id: str) -> str:\n    return claim_task(task_id, owner=\"agent\")\n\ndef run_complete_task(task_id: str) -> str:\n    return complete_task(task_id)\n\ndef run_spawn_teammate(name: str, role: str, prompt: str) -> str:\n    return spawn_teammate_thread(name, role, prompt)\n\ndef run_send_message(to: str, content: str) -> str:\n    BUS.send(\"lead\", to, content)\n    return f\"Sent to {to}\"\n\ndef run_check_inbox() -> str:\n    msgs = consume_lead_inbox(route_protocol=True)\n    if not msgs:\n        return \"(inbox empty)\"\n    lines = []\n    for m in msgs:\n        meta = m.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n        tag = f\" [{m['type']} req:{req_id}]\" if req_id else f\" [{m['type']}]\"\n        lines.append(f\"  [{m['from']}]{tag} {m['content'][:200]}\")\n    return \"\\n\".join(lines)\n\ndef run_connect_mcp(name: str) -> str:\n    return connect_mcp(name)\n\n\n# ── Tool Definitions ──\n\nBUILTIN_TOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"create_task\", \"description\": \"Create a task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"subject\": {\"type\": \"string\"},\n                                     \"description\": {\"type\": \"string\"},\n                                     \"blockedBy\": {\"type\": \"array\",\n                                                   \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\", \"description\": \"List all tasks.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"get_task\", \"description\": \"Get full task details.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\", \"description\": \"Claim a pending task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\", \"description\": \"Complete an in-progress task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"spawn_teammate\", \"description\": \"Spawn an autonomous teammate.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"role\": {\"type\": \"string\"},\n                                     \"prompt\": {\"type\": \"string\"}},\n                      \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"send_message\", \"description\": \"Send message to a teammate.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"to\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"check_inbox\",\n     \"description\": \"Check inbox for messages and protocol responses.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"request_shutdown\",\n     \"description\": \"Request a teammate to shut down.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\"]}},\n    {\"name\": \"request_plan\",\n     \"description\": \"Ask a teammate to submit a plan.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"},\n                                     \"task\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\", \"task\"]}},\n    {\"name\": \"review_plan\",\n     \"description\": \"Approve or reject a submitted plan.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"request_id\": {\"type\": \"string\"},\n                                     \"approve\": {\"type\": \"boolean\"},\n                                     \"feedback\": {\"type\": \"string\"}},\n                      \"required\": [\"request_id\", \"approve\"]}},\n    {\"name\": \"create_worktree\",\n     \"description\": \"Create an isolated git worktree.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"remove_worktree\",\n     \"description\": \"Remove a worktree. Refuses if changes exist.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"discard_changes\": {\"type\": \"boolean\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"keep_worktree\",\n     \"description\": \"Keep a worktree for manual review.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"connect_mcp\",\n     \"description\": \"Connect to an MCP server (docs, deploy) and discover tools.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n]\n\nBUILTIN_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n    \"get_task\": run_get_task,\n    \"claim_task\": run_claim_task, \"complete_task\": run_complete_task,\n    \"spawn_teammate\": run_spawn_teammate,\n    \"send_message\": run_send_message, \"check_inbox\": run_check_inbox,\n    \"request_shutdown\": run_request_shutdown,\n    \"request_plan\": run_request_plan, \"review_plan\": run_review_plan,\n    \"create_worktree\": run_create_worktree,\n    \"remove_worktree\": run_remove_worktree,\n    \"keep_worktree\": run_keep_worktree,\n    \"connect_mcp\": run_connect_mcp,\n}\n\n\n# ── Context ──\n\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\n\n\ndef update_context(context: dict, messages: list) -> dict:\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        memories = MEMORY_INDEX.read_text()[:2000]\n    return {\"memories\": memories}\n\n\n# ── Agent Loop (s19: dynamic tool pool, no prompt cache) ──\n\ndef agent_loop(messages: list, context: dict):\n    tools, handlers = assemble_tool_pool()\n    system = assemble_system_prompt(context)\n    while True:\n        try:\n            response = client.messages.create(\n                model=MODEL, system=system, messages=messages,\n                tools=tools, max_tokens=8000)\n        except Exception as e:\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\", \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n            handler = handlers.get(block.name)\n            output = handler(**block.input) if handler else \"Unknown\"\n            print(str(output)[:300])\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id, \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n        if any(b.name == \"connect_mcp\" for b in response.content\n               if b.type == \"tool_use\"):\n            tools, handlers = assemble_tool_pool()\n            context = update_context(context, messages)\n            system = assemble_system_prompt(context)\n\n\nif __name__ == \"__main__\":\n    print(\"s19: mcp tools\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = {\"memories\": \"\"}\n    while True:\n        try:\n            query = input(\"\\033[36ms19 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history, context)\n        context = update_context(context, history)\n        for block in history[-1][\"content\"]:\n            if getattr(block, \"type\", None) == \"text\":\n                print(block.text)\n\n        inbox = consume_lead_inbox(route_protocol=True)\n        if inbox:\n            inbox_text = \"\\n\".join(\n                f\"From {m['from']} [{m.get('type', 'message')}]: \"\n                f\"{m['content'][:200]}\" for m in inbox)\n            history.append({\"role\": \"user\",\n                            \"content\": f\"[Inbox]\\n{inbox_text}\"})\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s19_mcp_plugin/mcp-architecture.svg",
          "alt": "mcp architecture"
        }
      ]
    },
    {
      "id": "s20",
      "filename": "s20_comprehensive/code.py",
      "title": "Comprehensive Agent",
      "subtitle": "All Mechanisms, One Loop",
      "loc": 1677,
      "tools": [
        "bash",
        "read_file",
        "write_file",
        "send_message",
        "submit_plan",
        "list_tasks",
        "claim_task",
        "complete_task",
        "edit_file",
        "glob",
        "search",
        "get_version",
        "trigger",
        "status",
        "todo_write",
        "task",
        "load_skill",
        "compact",
        "create_task",
        "get_task",
        "schedule_cron",
        "list_crons",
        "cancel_cron",
        "spawn_teammate",
        "check_inbox",
        "request_shutdown",
        "request_plan",
        "review_plan",
        "create_worktree",
        "remove_worktree",
        "keep_worktree",
        "connect_mcp"
      ],
      "newTools": [
        "edit_file",
        "glob",
        "todo_write",
        "task",
        "load_skill",
        "compact",
        "schedule_cron",
        "list_crons",
        "cancel_cron"
      ],
      "coreAddition": "Integrated harness",
      "keyInsight": "The final harness is still one loop, now surrounded by the systems that make it production-shaped.",
      "classes": [
        {
          "name": "Task",
          "startLine": 81,
          "endLine": 90
        },
        {
          "name": "MessageBus",
          "startLine": 498,
          "endLine": 519
        },
        {
          "name": "ProtocolState",
          "startLine": 526,
          "endLine": 535
        },
        {
          "name": "RecoveryState",
          "startLine": 1172,
          "endLine": 1180
        },
        {
          "name": "CronJob",
          "startLine": 1302,
          "endLine": 1309
        },
        {
          "name": "MCPClient",
          "startLine": 1499,
          "endLine": 1521
        }
      ],
      "functions": [
        {
          "name": "terminal_print",
          "signature": "def terminal_print(text: str)",
          "startLine": 58
        },
        {
          "name": "_task_path",
          "signature": "def _task_path(task_id: str)",
          "startLine": 91
        },
        {
          "name": "save_task",
          "signature": "def save_task(task: Task)",
          "startLine": 107
        },
        {
          "name": "load_task",
          "signature": "def load_task(task_id: str)",
          "startLine": 111
        },
        {
          "name": "list_tasks",
          "signature": "def list_tasks()",
          "startLine": 115
        },
        {
          "name": "get_task_json",
          "signature": "def get_task_json(task_id: str)",
          "startLine": 120
        },
        {
          "name": "can_start",
          "signature": "def can_start(task_id: str)",
          "startLine": 124
        },
        {
          "name": "claim_task",
          "signature": "def claim_task(task_id: str, owner: str = \"agent\")",
          "startLine": 136
        },
        {
          "name": "complete_task",
          "signature": "def complete_task(task_id: str)",
          "startLine": 157
        },
        {
          "name": "validate_worktree_name",
          "signature": "def validate_worktree_name(name: str)",
          "startLine": 182
        },
        {
          "name": "run_git",
          "signature": "def run_git(args: list[str])",
          "startLine": 193
        },
        {
          "name": "log_event",
          "signature": "def log_event(event_type: str, worktree_name: str, task_id: str = \"\")",
          "startLine": 203
        },
        {
          "name": "create_worktree",
          "signature": "def create_worktree(name: str, task_id: str = \"\")",
          "startLine": 211
        },
        {
          "name": "bind_task_to_worktree",
          "signature": "def bind_task_to_worktree(task_id: str, worktree_name: str)",
          "startLine": 235
        },
        {
          "name": "_count_worktree_changes",
          "signature": "def _count_worktree_changes(path: Path)",
          "startLine": 241
        },
        {
          "name": "remove_worktree",
          "signature": "def remove_worktree(name: str, discard_changes: bool = False)",
          "startLine": 254
        },
        {
          "name": "keep_worktree",
          "signature": "def keep_worktree(name: str)",
          "startLine": 277
        },
        {
          "name": "_parse_frontmatter",
          "signature": "def _parse_frontmatter(text: str)",
          "startLine": 290
        },
        {
          "name": "scan_skills",
          "signature": "def scan_skills()",
          "startLine": 303
        },
        {
          "name": "list_skills",
          "signature": "def list_skills()",
          "startLine": 327
        },
        {
          "name": "load_skill",
          "signature": "def load_skill(name: str)",
          "startLine": 335
        },
        {
          "name": "assemble_system_prompt",
          "signature": "def assemble_system_prompt(context: dict)",
          "startLine": 360
        },
        {
          "name": "safe_path",
          "signature": "def safe_path(p: str, cwd: Path = None)",
          "startLine": 379
        },
        {
          "name": "run_write",
          "signature": "def run_write(path: str, content: str, cwd: Path = None)",
          "startLine": 415
        },
        {
          "name": "run_glob",
          "signature": "def run_glob(pattern: str, cwd: Path = None)",
          "startLine": 438
        },
        {
          "name": "call_tool_handler",
          "signature": "def call_tool_handler(handler, args: dict, name: str)",
          "startLine": 451
        },
        {
          "name": "_normalize_todos",
          "signature": "def _normalize_todos(todos)",
          "startLine": 460
        },
        {
          "name": "run_todo_write",
          "signature": "def run_todo_write(todos: list)",
          "startLine": 480
        },
        {
          "name": "new_request_id",
          "signature": "def new_request_id()",
          "startLine": 539
        },
        {
          "name": "match_response",
          "signature": "def match_response(response_type: str, request_id: str, approve: bool)",
          "startLine": 543
        },
        {
          "name": "consume_lead_inbox",
          "signature": "def consume_lead_inbox(route_protocol=True)",
          "startLine": 556
        },
        {
          "name": "scan_unclaimed_tasks",
          "signature": "def scan_unclaimed_tasks()",
          "startLine": 574
        },
        {
          "name": "spawn_teammate_thread",
          "signature": "def spawn_teammate_thread(name: str, role: str, prompt: str)",
          "startLine": 624
        },
        {
          "name": "_teammate_submit_plan",
          "signature": "def _teammate_submit_plan(from_name: str, plan: str)",
          "startLine": 831
        },
        {
          "name": "run_request_shutdown",
          "signature": "def run_request_shutdown(teammate: str)",
          "startLine": 845
        },
        {
          "name": "run_request_plan",
          "signature": "def run_request_plan(teammate: str, task: str)",
          "startLine": 856
        },
        {
          "name": "register_hook",
          "signature": "def register_hook(event: str, callback)",
          "startLine": 882
        },
        {
          "name": "trigger_hooks",
          "signature": "def trigger_hooks(event: str, *args)",
          "startLine": 886
        },
        {
          "name": "permission_hook",
          "signature": "def permission_hook(block)",
          "startLine": 898
        },
        {
          "name": "log_hook",
          "signature": "def log_hook(block)",
          "startLine": 926
        },
        {
          "name": "large_output_hook",
          "signature": "def large_output_hook(block, output)",
          "startLine": 931
        },
        {
          "name": "user_prompt_hook",
          "signature": "def user_prompt_hook(query: str)",
          "startLine": 938
        },
        {
          "name": "stop_hook",
          "signature": "def stop_hook(messages: list)",
          "startLine": 943
        },
        {
          "name": "extract_text",
          "signature": "def extract_text(content)",
          "startLine": 1007
        },
        {
          "name": "has_tool_use",
          "signature": "def has_tool_use(content)",
          "startLine": 1016
        },
        {
          "name": "spawn_subagent",
          "signature": "def spawn_subagent(description: str)",
          "startLine": 1023
        },
        {
          "name": "estimate_size",
          "signature": "def estimate_size(messages: list)",
          "startLine": 1060
        },
        {
          "name": "collect_tool_results",
          "signature": "def collect_tool_results(messages: list)",
          "startLine": 1064
        },
        {
          "name": "persist_large_output",
          "signature": "def persist_large_output(tool_use_id: str, output: str)",
          "startLine": 1076
        },
        {
          "name": "tool_result_budget",
          "signature": "def tool_result_budget(messages: list, max_bytes: int = 200_000)",
          "startLine": 1087
        },
        {
          "name": "snip_compact",
          "signature": "def snip_compact(messages: list, max_messages: int = 50)",
          "startLine": 1111
        },
        {
          "name": "micro_compact",
          "signature": "def micro_compact(messages: list)",
          "startLine": 1121
        },
        {
          "name": "write_transcript",
          "signature": "def write_transcript(messages: list)",
          "startLine": 1131
        },
        {
          "name": "summarize_history",
          "signature": "def summarize_history(messages: list)",
          "startLine": 1140
        },
        {
          "name": "compact_history",
          "signature": "def compact_history(messages: list)",
          "startLine": 1152
        },
        {
          "name": "reactive_compact",
          "signature": "def reactive_compact(messages: list)",
          "startLine": 1159
        },
        {
          "name": "retry_delay",
          "signature": "def retry_delay(attempt: int)",
          "startLine": 1181
        },
        {
          "name": "with_retry",
          "signature": "def with_retry(fn, state: RecoveryState)",
          "startLine": 1186
        },
        {
          "name": "is_prompt_too_long_error",
          "signature": "def is_prompt_too_long_error(e: Exception)",
          "startLine": 1216
        },
        {
          "name": "is_slow_operation",
          "signature": "def is_slow_operation(tool_name: str, tool_input: dict)",
          "startLine": 1233
        },
        {
          "name": "should_run_background",
          "signature": "def should_run_background(tool_name: str, tool_input: dict)",
          "startLine": 1243
        },
        {
          "name": "start_background_task",
          "signature": "def start_background_task(block, handlers: dict)",
          "startLine": 1249
        },
        {
          "name": "collect_background_results",
          "signature": "def collect_background_results()",
          "startLine": 1274
        },
        {
          "name": "_cron_field_matches",
          "signature": "def _cron_field_matches(field: str, value: int)",
          "startLine": 1316
        },
        {
          "name": "cron_matches",
          "signature": "def cron_matches(cron_expr: str, dt: datetime)",
          "startLine": 1331
        },
        {
          "name": "_validate_cron_field",
          "signature": "def _validate_cron_field(field: str, lo: int, hi: int)",
          "startLine": 1353
        },
        {
          "name": "validate_cron",
          "signature": "def validate_cron(cron_expr: str)",
          "startLine": 1385
        },
        {
          "name": "save_durable_jobs",
          "signature": "def save_durable_jobs()",
          "startLine": 1398
        },
        {
          "name": "load_durable_jobs",
          "signature": "def load_durable_jobs()",
          "startLine": 1403
        },
        {
          "name": "cancel_job",
          "signature": "def cancel_job(job_id: str)",
          "startLine": 1431
        },
        {
          "name": "cron_scheduler_loop",
          "signature": "def cron_scheduler_loop()",
          "startLine": 1441
        },
        {
          "name": "consume_cron_queue",
          "signature": "def consume_cron_queue()",
          "startLine": 1460
        },
        {
          "name": "run_list_crons",
          "signature": "def run_list_crons()",
          "startLine": 1475
        },
        {
          "name": "run_cancel_cron",
          "signature": "def run_cancel_cron(job_id: str)",
          "startLine": 1487
        },
        {
          "name": "normalize_mcp_name",
          "signature": "def normalize_mcp_name(name: str)",
          "startLine": 1527
        },
        {
          "name": "_mock_server_docs",
          "signature": "def _mock_server_docs()",
          "startLine": 1532
        },
        {
          "name": "_mock_server_deploy",
          "signature": "def _mock_server_deploy()",
          "startLine": 1551
        },
        {
          "name": "connect_mcp",
          "signature": "def connect_mcp(name: str)",
          "startLine": 1578
        },
        {
          "name": "assemble_tool_pool",
          "signature": "def assemble_tool_pool()",
          "startLine": 1593
        },
        {
          "name": "run_create_worktree",
          "signature": "def run_create_worktree(name: str, task_id: str = \"\")",
          "startLine": 1614
        },
        {
          "name": "run_remove_worktree",
          "signature": "def run_remove_worktree(name: str, discard_changes: bool = False)",
          "startLine": 1617
        },
        {
          "name": "run_keep_worktree",
          "signature": "def run_keep_worktree(name: str)",
          "startLine": 1620
        },
        {
          "name": "run_list_tasks",
          "signature": "def run_list_tasks()",
          "startLine": 1634
        },
        {
          "name": "run_get_task",
          "signature": "def run_get_task(task_id: str)",
          "startLine": 1644
        },
        {
          "name": "run_claim_task",
          "signature": "def run_claim_task(task_id: str)",
          "startLine": 1650
        },
        {
          "name": "run_complete_task",
          "signature": "def run_complete_task(task_id: str)",
          "startLine": 1656
        },
        {
          "name": "run_spawn_teammate",
          "signature": "def run_spawn_teammate(name: str, role: str, prompt: str)",
          "startLine": 1662
        },
        {
          "name": "run_send_message",
          "signature": "def run_send_message(to: str, content: str)",
          "startLine": 1665
        },
        {
          "name": "run_check_inbox",
          "signature": "def run_check_inbox()",
          "startLine": 1669
        },
        {
          "name": "run_connect_mcp",
          "signature": "def run_connect_mcp(name: str)",
          "startLine": 1681
        },
        {
          "name": "update_context",
          "signature": "def update_context(context: dict, messages: list)",
          "startLine": 1863
        },
        {
          "name": "prepare_context",
          "signature": "def prepare_context(messages: list)",
          "startLine": 1880
        },
        {
          "name": "build_user_content",
          "signature": "def build_user_content(results: list[dict])",
          "startLine": 1890
        },
        {
          "name": "inject_background_notifications",
          "signature": "def inject_background_notifications(messages: list)",
          "startLine": 1899
        },
        {
          "name": "agent_loop",
          "signature": "def agent_loop(messages: list, context: dict)",
          "startLine": 1919
        },
        {
          "name": "print_turn_assistants",
          "signature": "def print_turn_assistants(messages: list, turn_start: int)",
          "startLine": 2025
        },
        {
          "name": "cron_autorun_loop",
          "signature": "def cron_autorun_loop(history: list, context: dict)",
          "startLine": 2034
        }
      ],
      "layer": "collaboration",
      "source": "#!/usr/bin/env python3\n\"\"\"\ns20: Comprehensive Agent — all teaching components in one loop.\n\nRun:  python s20_comprehensive/code.py\nNeed: pip install anthropic python-dotenv pyyaml + .env with ANTHROPIC_API_KEY\n\nThis final chapter intentionally puts the earlier teaching mechanisms back\ntogether: dispatch, permission, hooks, todo, subagent, skills, compaction,\nmemory, prompt assembly, error recovery, task graph, background tasks, cron,\nteams, protocols, autonomous agents, worktrees, and MCP.\n\"\"\"\n\nimport ast, json, os, subprocess, time, random, threading, re\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dataclasses import dataclass, asdict, field\nimport yaml\n\ntry:\n    import readline\n    readline.parse_and_bind('set bind-tty-special-chars off')\n    READLINE_AVAILABLE = True\nexcept ImportError:\n    READLINE_AVAILABLE = False\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\nPRIMARY_MODEL = MODEL\nFALLBACK_MODEL = os.getenv(\"FALLBACK_MODEL_ID\")\n\nSKILLS_DIR = WORKDIR / \"skills\"\nTRANSCRIPT_DIR = WORKDIR / \".transcripts\"\nTOOL_RESULTS_DIR = WORKDIR / \".task_outputs\" / \"tool-results\"\n\nDEFAULT_MAX_TOKENS = 8000\nESCALATED_MAX_TOKENS = 16000\nMAX_RETRIES = 3\nMAX_CONSECUTIVE_529 = 2\nMAX_RECOVERY_RETRIES = 2\nBASE_DELAY_MS = 500\nCONTEXT_LIMIT = 50000\nKEEP_RECENT_TOOL_RESULTS = 3\nPERSIST_THRESHOLD = 30000\nCONTINUATION_PROMPT = \"Continue from the previous response. Do not repeat completed work.\"\nPROMPT = \"\\033[36ms20 >> \\033[0m\"\nCLI_ACTIVE = False\n\n\ndef terminal_print(text: str):\n    if threading.current_thread() is threading.main_thread() or not CLI_ACTIVE:\n        print(text)\n        return\n    line = \"\"\n    if READLINE_AVAILABLE:\n        try:\n            line = readline.get_line_buffer()\n        except Exception:\n            line = \"\"\n    print(f\"\\r\\033[K{text}\")\n    print(PROMPT + line, end=\"\", flush=True)\n\n# ── Task System ──\n\n# Tasks are tiny durable records. Later systems add ownership, dependencies,\n# worktrees, and teammates on top of this same file-backed state.\nTASKS_DIR = WORKDIR / \".tasks\"\nTASKS_DIR.mkdir(exist_ok=True)\nCURRENT_TODOS: list[dict] = []\n\n\n@dataclass\nclass Task:\n    id: str\n    subject: str\n    description: str\n    status: str\n    owner: str | None\n    blockedBy: list[str]\n    worktree: str | None = None\n\n\ndef _task_path(task_id: str) -> Path:\n    return TASKS_DIR / f\"{task_id}.json\"\n\n\ndef create_task(subject: str, description: str = \"\",\n                blockedBy: list[str] | None = None) -> Task:\n    task = Task(\n        id=f\"task_{int(time.time())}_{random.randint(0, 9999):04d}\",\n        subject=subject, description=description,\n        status=\"pending\", owner=None,\n        blockedBy=blockedBy or [],\n    )\n    save_task(task)\n    return task\n\n\ndef save_task(task: Task):\n    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))\n\n\ndef load_task(task_id: str) -> Task:\n    return Task(**json.loads(_task_path(task_id).read_text()))\n\n\ndef list_tasks() -> list[Task]:\n    return [Task(**json.loads(p.read_text()))\n            for p in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n\n\ndef get_task_json(task_id: str) -> str:\n    return json.dumps(asdict(load_task(task_id)), indent=2)\n\n\ndef can_start(task_id: str) -> bool:\n    # Dependencies are intentionally simple: every blocker must exist and be\n    # completed before the task can be claimed.\n    task = load_task(task_id)\n    for dep_id in task.blockedBy:\n        if not _task_path(dep_id).exists():\n            return False\n        if load_task(dep_id).status != \"completed\":\n            return False\n    return True\n\n\ndef claim_task(task_id: str, owner: str = \"agent\") -> str:\n    task = load_task(task_id)\n    if task.status != \"pending\":\n        return f\"Task {task_id} is {task.status}, cannot claim\"\n    if task.owner:\n        return f\"Task {task_id} already owned by {task.owner}\"\n    if not can_start(task_id):\n        deps = [d for d in task.blockedBy\n                if _task_path(d).exists() and load_task(d).status != \"completed\"]\n        missing = [d for d in task.blockedBy if not _task_path(d).exists()]\n        parts = []\n        if deps: parts.append(f\"blocked by: {deps}\")\n        if missing: parts.append(f\"missing deps: {missing}\")\n        return \"Cannot start — \" + \", \".join(parts)\n    task.owner = owner\n    task.status = \"in_progress\"\n    save_task(task)\n    print(f\"  \\033[36m[claim] {task.subject} → in_progress\\033[0m\")\n    return f\"Claimed {task.id} ({task.subject})\"\n\n\ndef complete_task(task_id: str) -> str:\n    task = load_task(task_id)\n    if task.status != \"in_progress\":\n        return f\"Task {task_id} is {task.status}, cannot complete\"\n    task.status = \"completed\"\n    save_task(task)\n    unblocked = [t.subject for t in list_tasks()\n                 if t.status == \"pending\" and t.blockedBy and can_start(t.id)]\n    print(f\"  \\033[32m[complete] {task.subject} ✓\\033[0m\")\n    msg = f\"Completed {task.id} ({task.subject})\"\n    if unblocked:\n        msg += f\"\\nUnblocked: {', '.join(unblocked)}\"\n    return msg\n\n\n# ── Worktree System ──\n\n# Worktree names become filesystem paths, so the teaching version keeps the\n# validation rules strict and reuses them for create/remove/keep.\nWORKTREES_DIR = WORKDIR / \".worktrees\"\nWORKTREES_DIR.mkdir(exist_ok=True)\n\nVALID_WT_NAME = re.compile(r'^[A-Za-z0-9._-]{1,64}$')\n\n\ndef validate_worktree_name(name: str) -> str | None:\n    if not name:\n        return \"Worktree name cannot be empty\"\n    if name in (\".\", \"..\"):\n        return f\"'{name}' is not a valid worktree name\"\n    if not VALID_WT_NAME.match(name):\n        return (f\"Invalid worktree name '{name}': \"\n                \"only letters, digits, dots, underscores, dashes (1-64 chars)\")\n    return None\n\n\ndef run_git(args: list[str]) -> tuple[bool, str]:\n    try:\n        r = subprocess.run([\"git\"] + args, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=30)\n        out = (r.stdout + r.stderr).strip()\n        return r.returncode == 0, out[:5000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return False, \"Error: git timeout\"\n\n\ndef log_event(event_type: str, worktree_name: str, task_id: str = \"\"):\n    event = {\"type\": event_type, \"worktree\": worktree_name,\n             \"task_id\": task_id, \"ts\": time.time()}\n    events_file = WORKTREES_DIR / \"events.jsonl\"\n    with open(events_file, \"a\") as f:\n        f.write(json.dumps(event) + \"\\n\")\n\n\ndef create_worktree(name: str, task_id: str = \"\") -> str:\n    # Tool-layer validation is part of the safety boundary; do it before git\n    # sees the name, not only after git happens to reject something.\n    err = validate_worktree_name(name)\n    if err:\n        return f\"Error: {err}\"\n    if task_id:\n        try:\n            load_task(task_id)\n        except FileNotFoundError:\n            return f\"Error: task {task_id} not found\"\n    path = WORKTREES_DIR / name\n    if path.exists():\n        return f\"Worktree '{name}' already exists at {path}\"\n    ok, result = run_git([\"worktree\", \"add\", str(path), \"-b\", f\"wt/{name}\", \"HEAD\"])\n    if not ok:\n        return f\"Git error: {result}\"\n    if task_id:\n        bind_task_to_worktree(task_id, name)\n    log_event(\"create\", name, task_id)\n    print(f\"  \\033[33m[worktree] created: {name} at {path}\\033[0m\")\n    return f\"Worktree '{name}' created at {path}\"\n\n\ndef bind_task_to_worktree(task_id: str, worktree_name: str):\n    task = load_task(task_id)\n    task.worktree = worktree_name\n    save_task(task)\n\n\ndef _count_worktree_changes(path: Path) -> tuple[int, int]:\n    try:\n        r1 = subprocess.run([\"git\", \"status\", \"--porcelain\"],\n                            cwd=path, capture_output=True, text=True, timeout=10)\n        files = len([l for l in r1.stdout.strip().splitlines() if l.strip()])\n        r2 = subprocess.run([\"git\", \"log\", \"@{push}..HEAD\", \"--oneline\"],\n                            cwd=path, capture_output=True, text=True, timeout=10)\n        commits = len([l for l in r2.stdout.strip().splitlines() if l.strip()])\n        return files, commits\n    except Exception:\n        return -1, -1\n\n\ndef remove_worktree(name: str, discard_changes: bool = False) -> str:\n    err = validate_worktree_name(name)\n    if err:\n        return err\n    path = WORKTREES_DIR / name\n    if not path.exists():\n        return f\"Worktree '{name}' not found\"\n    if not discard_changes:\n        files, commits = _count_worktree_changes(path)\n        if files < 0:\n            return \"Cannot verify status. Use discard_changes=true to force.\"\n        if files > 0 or commits > 0:\n            return (f\"Worktree '{name}' has {files} file(s), {commits} commit(s). \"\n                    \"Use discard_changes=true or keep_worktree.\")\n    ok1, _ = run_git([\"worktree\", \"remove\", str(path), \"--force\"])\n    if not ok1:\n        return f\"Failed to remove worktree '{name}'\"\n    run_git([\"branch\", \"-D\", f\"wt/{name}\"])\n    log_event(\"remove\", name)\n    print(f\"  \\033[33m[worktree] removed: {name}\\033[0m\")\n    return f\"Worktree '{name}' removed\"\n\n\ndef keep_worktree(name: str) -> str:\n    err = validate_worktree_name(name)\n    if err:\n        return err\n    log_event(\"keep\", name)\n    return f\"Worktree '{name}' kept for review (branch: wt/{name})\"\n\n\n# ── Skill Loading ──\n\nSKILL_REGISTRY: dict[str, dict] = {}\n\n\ndef _parse_frontmatter(text: str) -> tuple[dict, str]:\n    if not text.startswith(\"---\"):\n        return {}, text\n    parts = text.split(\"---\", 2)\n    if len(parts) < 3:\n        return {}, text\n    try:\n        meta = yaml.safe_load(parts[1]) or {}\n    except yaml.YAMLError:\n        meta = {}\n    return meta, parts[2].strip()\n\n\ndef scan_skills():\n    SKILL_REGISTRY.clear()\n    if not SKILLS_DIR.exists():\n        return\n    for directory in sorted(SKILLS_DIR.iterdir()):\n        if not directory.is_dir():\n            continue\n        manifest = directory / \"SKILL.md\"\n        if not manifest.exists():\n            continue\n        raw = manifest.read_text()\n        meta, _ = _parse_frontmatter(raw)\n        name = meta.get(\"name\", directory.name)\n        desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n        SKILL_REGISTRY[name] = {\n            \"name\": name,\n            \"description\": desc,\n            \"content\": raw,\n        }\n\n\nscan_skills()\n\n\ndef list_skills() -> str:\n    if not SKILL_REGISTRY:\n        return \"(no skills found)\"\n    return \"\\n\".join(\n        f\"- {skill['name']}: {skill['description']}\"\n        for skill in SKILL_REGISTRY.values())\n\n\ndef load_skill(name: str) -> str:\n    skill = SKILL_REGISTRY.get(name)\n    if not skill:\n        available = \", \".join(SKILL_REGISTRY.keys()) or \"(none)\"\n        return f\"Skill not found: {name}. Available: {available}\"\n    return skill[\"content\"]\n\n\n# ── Prompt Assembly ──\n\nPROMPT_SECTIONS = {\n    \"identity\": \"You are a coding agent. Act, don't explain.\",\n    \"tools\": \"Available tools: bash, read_file, write_file, edit_file, glob, \"\n             \"todo_write, task, load_skill, compact, \"\n             \"create_task, list_tasks, get_task, claim_task, complete_task, \"\n             \"schedule_cron, list_crons, cancel_cron, \"\n             \"spawn_teammate, send_message, check_inbox, \"\n             \"request_shutdown, request_plan, review_plan, \"\n             \"create_worktree, remove_worktree, keep_worktree, \"\n             \"connect_mcp. MCP tools are prefixed mcp__{server}__{tool}.\",\n    \"workspace\": f\"Working directory: {WORKDIR}\",\n    \"memory\": \"Relevant memories are injected below when available.\",\n}\n\n\ndef assemble_system_prompt(context: dict) -> str:\n    # The system prompt is rebuilt each turn from live context. This is where\n    # memory, skill catalog, MCP state, and active teammates become visible.\n    sections = [PROMPT_SECTIONS[\"identity\"],\n                PROMPT_SECTIONS[\"tools\"],\n                PROMPT_SECTIONS[\"workspace\"]]\n    sections.append(f\"Current time: {datetime.now().isoformat(timespec='seconds')}\")\n    sections.append(\"Skills catalog:\\n\" + list_skills() +\n                    \"\\nUse load_skill(name) when a skill is relevant.\")\n    if context.get(\"memories\"):\n        sections.append(f\"Relevant memories:\\n{context['memories']}\")\n    mcp_names = list(mcp_clients.keys())\n    if mcp_names:\n        sections.append(f\"Connected MCP servers: {', '.join(mcp_names)}\")\n    return \"\\n\\n\".join(sections)\n\n\n# ── Basic Tools ──\n\ndef safe_path(p: str, cwd: Path = None) -> Path:\n    # File tools stay inside the workspace or teammate worktree. Bash remains\n    # powerful on purpose and is controlled by the permission hook instead.\n    base = cwd or WORKDIR\n    path = (base / p).resolve()\n    if not path.is_relative_to(base):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_bash(command: str, cwd: Path = None,\n             run_in_background: bool = False) -> str:\n    # run_in_background is consumed by the dispatcher; direct execution ignores it.\n    try:\n        r = subprocess.run(command, shell=True, cwd=cwd or WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\n\ndef run_read(path: str, limit: int | None = None,\n             offset: int = 0, cwd: Path = None) -> str:\n    try:\n        lines = safe_path(path, cwd).read_text().splitlines()\n        offset = max(int(offset or 0), 0)\n        limit = int(limit) if limit is not None else None\n        lines = lines[offset:]\n        if limit is not None and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str, cwd: Path = None) -> str:\n    try:\n        fp = safe_path(path, cwd)\n        fp.parent.mkdir(parents=True, exist_ok=True)\n        fp.write_text(content)\n        return f\"Wrote {len(content)} bytes to {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str,\n             cwd: Path = None) -> str:\n    try:\n        fp = safe_path(path, cwd)\n        text = fp.read_text()\n        if old_text not in text:\n            return f\"Error: text not found in {path}\"\n        fp.write_text(text.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str, cwd: Path = None) -> str:\n    import glob as g\n    try:\n        base = cwd or WORKDIR\n        results = []\n        for match in g.glob(pattern, root_dir=base):\n            if (base / match).resolve().is_relative_to(base):\n                results.append(match)\n        return \"\\n\".join(results) if results else \"(no matches)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef call_tool_handler(handler, args: dict, name: str) -> str:\n    if not handler:\n        return f\"Unknown: {name}\"\n    try:\n        return handler(**(args or {}))\n    except TypeError as e:\n        return f\"Error: {e}\"\n\n\ndef _normalize_todos(todos):\n    if isinstance(todos, str):\n        try:\n            todos = json.loads(todos)\n        except json.JSONDecodeError:\n            try:\n                todos = ast.literal_eval(todos)\n            except (SyntaxError, ValueError):\n                return None, \"Error: todos must be a list or JSON array string\"\n    if not isinstance(todos, list):\n        return None, \"Error: todos must be a list\"\n    for i, todo in enumerate(todos):\n        if not isinstance(todo, dict):\n            return None, f\"Error: todos[{i}] must be an object\"\n        if \"content\" not in todo or \"status\" not in todo:\n            return None, f\"Error: todos[{i}] missing 'content' or 'status'\"\n        if todo[\"status\"] not in (\"pending\", \"in_progress\", \"completed\"):\n            return None, f\"Error: todos[{i}] has invalid status '{todo['status']}'\"\n    return todos, None\n\ndef run_todo_write(todos: list) -> str:\n    global CURRENT_TODOS\n    todos, error = _normalize_todos(todos)\n    if error:\n        return error\n    CURRENT_TODOS = todos\n    print(f\"  \\033[33m[todo] updated {len(CURRENT_TODOS)} item(s)\\033[0m\")\n    return f\"Updated {len(CURRENT_TODOS)} todos\"\n\n\n# ── MessageBus ──\n\n# Team communication is append-only JSONL mailboxes. This keeps the protocol\n# inspectable on disk and lets background teammates send messages.\nMAILBOX_DIR = WORKDIR / \".mailboxes\"\nMAILBOX_DIR.mkdir(exist_ok=True)\n\n\nclass MessageBus:\n    def send(self, from_agent: str, to_agent: str, content: str,\n             msg_type: str = \"message\", metadata: dict = None):\n        msg = {\"from\": from_agent, \"to\": to_agent,\n               \"content\": content, \"type\": msg_type,\n               \"ts\": time.time(), \"metadata\": metadata or {}}\n        inbox = MAILBOX_DIR / f\"{to_agent}.jsonl\"\n        with open(inbox, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        terminal_print(f\"  \\033[33m[bus] {from_agent} → {to_agent}: \"\n                       f\"({msg_type}) {content[:50]}\\033[0m\")\n\n    def read_inbox(self, agent: str) -> list[dict]:\n        inbox = MAILBOX_DIR / f\"{agent}.jsonl\"\n        if not inbox.exists():\n            return []\n        msgs = [json.loads(line) for line in inbox.read_text().splitlines()\n                if line.strip()]\n        inbox.unlink()\n        return msgs\n\n\nBUS = MessageBus()\nactive_teammates: dict[str, bool] = {}\n\n# ── Protocol State ──\n\n@dataclass\nclass ProtocolState:\n    request_id: str\n    type: str\n    sender: str\n    target: str\n    status: str\n    payload: str\n    created_at: float = field(default_factory=time.time)\n\n\npending_requests: dict[str, ProtocolState] = {}\n\n\ndef new_request_id() -> str:\n    return f\"req_{random.randint(0, 999999):06d}\"\n\n\ndef match_response(response_type: str, request_id: str, approve: bool):\n    # Responses are matched by request_id so one protocol reply cannot approve\n    # a different pending request.\n    state = pending_requests.get(request_id)\n    if not state:\n        return\n    if state.type == \"shutdown\" and response_type != \"shutdown_response\":\n        return\n    if state.type == \"plan_approval\" and response_type != \"plan_approval_response\":\n        return\n    state.status = \"approved\" if approve else \"rejected\"\n\n\ndef consume_lead_inbox(route_protocol=True) -> list[dict]:\n    msgs = BUS.read_inbox(\"lead\")\n    if route_protocol:\n        for msg in msgs:\n            meta = msg.get(\"metadata\", {})\n            req_id = meta.get(\"request_id\", \"\")\n            msg_type = msg.get(\"type\", \"\")\n            if req_id and msg_type.endswith(\"_response\"):\n                match_response(msg_type, req_id, meta.get(\"approve\", False))\n    return msgs\n\n\n# ── Autonomous Agent ──\n\nIDLE_POLL_INTERVAL = 5\nIDLE_TIMEOUT = 60\n\n\ndef scan_unclaimed_tasks() -> list[dict]:\n    unclaimed = []\n    for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n        task = json.loads(f.read_text())\n        if (task.get(\"status\") == \"pending\"\n                and not task.get(\"owner\")\n                and can_start(task[\"id\"])):\n            unclaimed.append(task)\n    return unclaimed\n\n\ndef idle_poll(agent_name: str, messages: list,\n              name: str, role: str,\n              worktree_context: dict | None = None) -> str:\n    # Autonomous teammates wake up for inbox messages first, then look for\n    # unclaimed tasks. This keeps direct protocol messages higher priority.\n    for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):\n        time.sleep(IDLE_POLL_INTERVAL)\n        inbox = BUS.read_inbox(agent_name)\n        if inbox:\n            for msg in inbox:\n                if msg.get(\"type\") == \"shutdown_request\":\n                    req_id = msg.get(\"metadata\", {}).get(\"request_id\", \"\")\n                    BUS.send(name, \"lead\", \"Shutting down.\",\n                             \"shutdown_response\",\n                             {\"request_id\": req_id, \"approve\": True})\n                    return \"shutdown\"\n            messages.append({\"role\": \"user\",\n                \"content\": \"<inbox>\" + json.dumps(inbox) + \"</inbox>\"})\n            return \"work\"\n        unclaimed = scan_unclaimed_tasks()\n        if unclaimed:\n            task_data = unclaimed[0]\n            result = claim_task(task_data[\"id\"], agent_name)\n            if \"Claimed\" in result:\n                wt_info = \"\"\n                if task_data.get(\"worktree\"):\n                    wt_path = WORKTREES_DIR / task_data[\"worktree\"]\n                    wt_info = f\"\\nWork directory: {wt_path}\"\n                    if worktree_context is not None:\n                        worktree_context[\"path\"] = str(wt_path)\n                messages.append({\"role\": \"user\",\n                    \"content\": f\"<auto-claimed>Task {task_data['id']}: \"\n                               f\"{task_data['subject']}{wt_info}</auto-claimed>\"})\n                return \"work\"\n    return \"timeout\"\n\n\n# ── Teammate Thread ──\n\ndef spawn_teammate_thread(name: str, role: str, prompt: str) -> str:\n    if name in active_teammates:\n        return f\"Teammate '{name}' already exists\"\n\n    # Plan approval is a real gate: after submit_plan, the teammate stops\n    # taking model/tool steps until lead sends plan_approval_response.\n    protocol_ctx = {\"waiting_plan\": None}\n    system = (f\"You are '{name}', a {role}. \"\n              f\"Use tools to complete tasks. \"\n              f\"If a task has a worktree, work in that directory.\")\n\n    def handle_inbox_message(name: str, msg: dict, messages: list):\n        msg_type = msg.get(\"type\", \"message\")\n        meta = msg.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n        if msg_type == \"shutdown_request\":\n            BUS.send(name, \"lead\", \"Shutting down.\",\n                     \"shutdown_response\",\n                     {\"request_id\": req_id, \"approve\": True})\n            return True\n        if msg_type == \"plan_approval_response\":\n            approve = meta.get(\"approve\", False)\n            if req_id == protocol_ctx[\"waiting_plan\"]:\n                protocol_ctx[\"waiting_plan\"] = None\n            messages.append({\"role\": \"user\",\n                \"content\": \"[Plan approved]\" if approve\n                           else f\"[Plan rejected] {msg['content']}\"})\n        return False\n\n    def run():\n        wt_ctx = {\"path\": None}\n\n        def _wt_cwd():\n            # Once a task with a worktree is claimed, all teammate file tools\n            # transparently run inside that isolated directory.\n            p = wt_ctx[\"path\"]\n            return Path(p) if p else None\n\n        def _run_bash(command: str) -> str:\n            return run_bash(command, cwd=_wt_cwd())\n\n        def _run_read(path: str) -> str:\n            return run_read(path, cwd=_wt_cwd())\n\n        def _run_write(path: str, content: str) -> str:\n            return run_write(path, content, cwd=_wt_cwd())\n\n        def _run_list_tasks():\n            tasks = list_tasks()\n            if not tasks:\n                return \"No tasks.\"\n            return \"\\n\".join(\n                f\"  {t.id}: {t.subject} [{t.status}]\"\n                + (f\" (wt:{t.worktree})\" if t.worktree else \"\")\n                for t in tasks)\n\n        def _run_claim_task(task_id: str):\n            result = claim_task(task_id, owner=name)\n            if \"Claimed\" in result:\n                task = load_task(task_id)\n                wt_ctx[\"path\"] = (str(WORKTREES_DIR / task.worktree)\n                                  if task.worktree else None)\n            return result\n\n        def _run_complete_task(task_id: str):\n            result = complete_task(task_id)\n            wt_ctx[\"path\"] = None\n            return result\n\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        sub_tools = [\n            {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"command\": {\"type\": \"string\"}},\n                              \"required\": [\"command\"]}},\n            {\"name\": \"read_file\", \"description\": \"Read file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"},\n                                             \"limit\": {\"type\": \"integer\"},\n                                             \"offset\": {\"type\": \"integer\"}},\n                              \"required\": [\"path\"]}},\n            {\"name\": \"write_file\", \"description\": \"Write file.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"path\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"send_message\",\n             \"description\": \"Send message to another agent.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"to\": {\"type\": \"string\"},\n                                             \"content\": {\"type\": \"string\"}},\n                              \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"submit_plan\",\n             \"description\": \"Submit a plan for Lead approval.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"plan\": {\"type\": \"string\"}},\n                              \"required\": [\"plan\"]}},\n            {\"name\": \"list_tasks\",\n             \"description\": \"List all tasks.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {},\n                              \"required\": []}},\n            {\"name\": \"claim_task\",\n             \"description\": \"Claim a pending task.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n            {\"name\": \"complete_task\",\n             \"description\": \"Mark an in-progress task as completed.\",\n             \"input_schema\": {\"type\": \"object\",\n                              \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                              \"required\": [\"task_id\"]}},\n        ]\n\n        sub_handlers = {\n            \"bash\": _run_bash, \"read_file\": _run_read,\n            \"write_file\": _run_write,\n            \"send_message\": lambda to, content: (BUS.send(name, to, content),\n                                                  \"Sent\")[1],\n            \"list_tasks\": _run_list_tasks,\n            \"claim_task\": _run_claim_task,\n            \"complete_task\": _run_complete_task,\n        }\n\n        while True:\n            if len(messages) <= 3:\n                messages.insert(0, {\"role\": \"user\",\n                    \"content\": f\"<identity>You are '{name}', role: {role}. \"\n                               f\"Continue your work.</identity>\"})\n            should_shutdown = False\n            for _ in range(10):\n                inbox = BUS.read_inbox(name)\n                for msg in inbox:\n                    stopped = handle_inbox_message(name, msg, messages)\n                    if stopped:\n                        should_shutdown = True\n                        break\n                if should_shutdown:\n                    break\n                if protocol_ctx[\"waiting_plan\"]:\n                    # Poll only for protocol replies while the approval gate is\n                    # closed; do not let the model continue with the task.\n                    time.sleep(IDLE_POLL_INTERVAL)\n                    continue\n                if inbox and not should_shutdown:\n                    non_protocol = [m for m in inbox\n                                    if m.get(\"type\") == \"message\"]\n                    if non_protocol:\n                        messages.append({\"role\": \"user\",\n                            \"content\": \"<inbox>\" + json.dumps(non_protocol) + \"</inbox>\"})\n                try:\n                    response = client.messages.create(\n                        model=MODEL, system=system, messages=messages[-20:],\n                        tools=sub_tools, max_tokens=8000)\n                except Exception:\n                    break\n                messages.append({\"role\": \"assistant\", \"content\": response.content})\n                if not has_tool_use(response.content):\n                    break\n                results = []\n                for block in response.content:\n                    if block.type == \"tool_use\":\n                        if block.name == \"submit_plan\":\n                            output = _teammate_submit_plan(\n                                name, block.input.get(\"plan\", \"\"))\n                            match = re.search(r\"\\((req_\\d+)\\)\", output)\n                            protocol_ctx[\"waiting_plan\"] = (\n                                match.group(1) if match else output)\n                        else:\n                            handler = sub_handlers.get(block.name)\n                            output = call_tool_handler(handler, block.input,\n                                                       block.name)\n                        results.append({\"type\": \"tool_result\",\n                                        \"tool_use_id\": block.id,\n                                        \"content\": str(output)})\n                        if protocol_ctx[\"waiting_plan\"]:\n                            # Ignore later tool_use blocks from the same model\n                            # response; they belong after approval, not before.\n                            break\n                messages.append({\"role\": \"user\", \"content\": results})\n                if protocol_ctx[\"waiting_plan\"]:\n                    break\n            if should_shutdown:\n                break\n            if protocol_ctx[\"waiting_plan\"]:\n                continue\n            idle_result = idle_poll(name, messages, name, role, wt_ctx)\n            if idle_result in (\"shutdown\", \"timeout\"):\n                break\n\n        summary = \"Done.\"\n        for msg in reversed(messages):\n            if msg[\"role\"] == \"assistant\" and isinstance(msg[\"content\"], list):\n                for b in msg[\"content\"]:\n                    if getattr(b, \"type\", None) == \"text\":\n                        summary = b.text\n                        break\n                else:\n                    continue\n                break\n        BUS.send(name, \"lead\", summary, \"result\")\n        active_teammates.pop(name, None)\n\n    active_teammates[name] = True\n    threading.Thread(target=run, daemon=True).start()\n    return f\"Teammate '{name}' spawned as {role}\"\n\n\ndef _teammate_submit_plan(from_name: str, plan: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"plan_approval\",\n        sender=from_name, target=\"lead\",\n        status=\"pending\", payload=plan)\n    BUS.send(from_name, \"lead\", plan,\n             \"plan_approval_request\",\n             {\"request_id\": req_id})\n    return f\"Plan submitted ({req_id})\"\n\n\n# ── Lead Protocol Tools ──\n\ndef run_request_shutdown(teammate: str) -> str:\n    req_id = new_request_id()\n    pending_requests[req_id] = ProtocolState(\n        request_id=req_id, type=\"shutdown\",\n        sender=\"lead\", target=teammate,\n        status=\"pending\", payload=\"\")\n    BUS.send(\"lead\", teammate, \"Shut down.\", \"shutdown_request\",\n             {\"request_id\": req_id})\n    return f\"Shutdown request sent to {teammate}\"\n\n\ndef run_request_plan(teammate: str, task: str) -> str:\n    BUS.send(\"lead\", teammate, f\"Submit plan for: {task}\", \"message\")\n    return f\"Asked {teammate} to submit a plan\"\n\n\ndef run_review_plan(request_id: str, approve: bool,\n                    feedback: str = \"\") -> str:\n    state = pending_requests.get(request_id)\n    if not state:\n        return f\"Request {request_id} not found\"\n    state.status = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", state.sender,\n             feedback or (\"Approved\" if approve else \"Rejected\"),\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n    return f\"Plan {'approved' if approve else 'rejected'}\"\n\n\n# ── Hooks + Permission Pipeline ──\n\n# Hooks are intentionally outside tool handlers. The loop can add permission,\n# logging, and stop behavior without changing each individual tool.\nHOOKS = {\"UserPromptSubmit\": [], \"PreToolUse\": [],\n         \"PostToolUse\": [], \"Stop\": []}\n\n\ndef register_hook(event: str, callback):\n    HOOKS[event].append(callback)\n\n\ndef trigger_hooks(event: str, *args):\n    for callback in HOOKS[event]:\n        result = callback(*args)\n        if result is not None:\n            return result\n    return None\n\n\nDENY_LIST = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"mkfs\", \"dd if=\"]\nDESTRUCTIVE = [\"rm \", \"> /etc/\", \"chmod 777\"]\n\n\ndef permission_hook(block):\n    # The permission layer sees the raw tool_use before dispatch. It can deny,\n    # ask the user, or allow execution to continue.\n    if block.name == \"bash\":\n        command = block.input.get(\"command\", \"\")\n        for pattern in DENY_LIST:\n            if pattern in command:\n                return f\"Permission denied: '{pattern}' is on the deny list\"\n        if any(token in command for token in DESTRUCTIVE):\n            print(f\"\\n\\033[33m[permission] destructive command\\033[0m\")\n            print(f\"  {command}\")\n            choice = input(\"  Allow? [y/N] \").strip().lower()\n            if choice not in (\"y\", \"yes\"):\n                return \"Permission denied by user\"\n    if block.name in (\"write_file\", \"edit_file\"):\n        path = block.input.get(\"path\", \"\")\n        try:\n            safe_path(path)\n        except Exception:\n            return f\"Permission denied: path escapes workspace: {path}\"\n    if block.name.startswith(\"mcp__\") and \"deploy\" in block.name:\n        print(f\"\\n\\033[33m[permission] MCP destructive-looking tool: {block.name}\\033[0m\")\n        choice = input(\"  Allow? [y/N] \").strip().lower()\n        if choice not in (\"y\", \"yes\"):\n            return \"Permission denied by user\"\n    return None\n\n\ndef log_hook(block):\n    print(f\"\\033[90m[HOOK] {block.name}\\033[0m\")\n    return None\n\n\ndef large_output_hook(block, output):\n    if len(str(output)) > 100000:\n        print(f\"\\033[33m[HOOK] large output from {block.name}: \"\n              f\"{len(str(output))} chars\\033[0m\")\n    return None\n\n\ndef user_prompt_hook(query: str):\n    print(f\"\\033[90m[HOOK] UserPromptSubmit: {WORKDIR}\\033[0m\")\n    return None\n\n\ndef stop_hook(messages: list):\n    tool_count = 0\n    for msg in messages:\n        content = msg.get(\"content\")\n        if isinstance(content, list):\n            tool_count += sum(1 for item in content\n                              if isinstance(item, dict)\n                              and item.get(\"type\") == \"tool_result\")\n    print(f\"\\033[90m[HOOK] Stop: {tool_count} tool result(s)\\033[0m\")\n    return None\n\n\nregister_hook(\"UserPromptSubmit\", user_prompt_hook)\nregister_hook(\"PreToolUse\", permission_hook)\nregister_hook(\"PreToolUse\", log_hook)\nregister_hook(\"PostToolUse\", large_output_hook)\nregister_hook(\"Stop\", stop_hook)\n\n\n# ── Subagent Tool ──\n\nSUB_SYSTEM = (\n    f\"You are a coding subagent at {WORKDIR}. \"\n    \"Complete the task, then return a concise final summary. \"\n    \"Do not spawn more agents.\"\n)\n\n\nSUB_TOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"},\n                                     \"offset\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"old_text\": {\"type\": \"string\"},\n                                     \"new_text\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"pattern\": {\"type\": \"string\"}},\n                      \"required\": [\"pattern\"]}},\n]\n\n\nSUB_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read,\n    \"write_file\": run_write, \"edit_file\": run_edit,\n    \"glob\": run_glob,\n}\n\n\ndef extract_text(content) -> str:\n    if not isinstance(content, list):\n        return str(content)\n    return \"\\n\".join(\n        getattr(block, \"text\", \"\")\n        for block in content\n        if getattr(block, \"type\", None) == \"text\").strip()\n\n\ndef has_tool_use(content) -> bool:\n    # Do not rely on stop_reason alone; the concrete tool_use block is the\n    # continuation signal used by the loop.\n    return any(getattr(block, \"type\", None) == \"tool_use\"\n               for block in content)\n\n\ndef spawn_subagent(description: str) -> str:\n    messages = [{\"role\": \"user\", \"content\": description}]\n    for _ in range(30):\n        response = client.messages.create(\n            model=MODEL, system=SUB_SYSTEM, messages=messages,\n            tools=SUB_TOOLS, max_tokens=8000)\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if not has_tool_use(response.content):\n            break\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            blocked = trigger_hooks(\"PreToolUse\", block)\n            if blocked:\n                output = str(blocked)\n            else:\n                handler = SUB_HANDLERS.get(block.name)\n                output = call_tool_handler(handler, block.input, block.name)\n                trigger_hooks(\"PostToolUse\", block, output)\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id,\n                            \"content\": str(output)})\n        messages.append({\"role\": \"user\", \"content\": results})\n    for msg in reversed(messages):\n        if msg[\"role\"] == \"assistant\":\n            text = extract_text(msg[\"content\"])\n            if text:\n                return text\n    return \"Subagent finished without a text summary.\"\n\n\n# ── Context Compaction ──\n\n# Compaction is layered: first shrink oversized tool results, then trim old\n# message ranges, and only call the model for a summary when the context is\n# still too large or the model explicitly asks for compact.\ndef estimate_size(messages: list) -> int:\n    return len(json.dumps(messages, default=str))\n\n\ndef collect_tool_results(messages: list):\n    found = []\n    for mi, msg in enumerate(messages):\n        content = msg.get(\"content\")\n        if msg.get(\"role\") != \"user\" or not isinstance(content, list):\n            continue\n        for bi, block in enumerate(content):\n            if isinstance(block, dict) and block.get(\"type\") == \"tool_result\":\n                found.append((mi, bi, block))\n    return found\n\n\ndef persist_large_output(tool_use_id: str, output: str) -> str:\n    if len(output) <= PERSIST_THRESHOLD:\n        return output\n    TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)\n    path = TOOL_RESULTS_DIR / f\"{tool_use_id}.txt\"\n    if not path.exists():\n        path.write_text(output)\n    return (f\"<persisted-output>\\nFull output: {path}\\n\"\n            f\"Preview:\\n{output[:2000]}\\n</persisted-output>\")\n\n\ndef tool_result_budget(messages: list, max_bytes: int = 200_000) -> list:\n    if not messages:\n        return messages\n    last = messages[-1]\n    content = last.get(\"content\")\n    if last.get(\"role\") != \"user\" or not isinstance(content, list):\n        return messages\n    blocks = [(i, b) for i, b in enumerate(content)\n              if isinstance(b, dict) and b.get(\"type\") == \"tool_result\"]\n    total = sum(len(str(b.get(\"content\", \"\"))) for _, b in blocks)\n    if total <= max_bytes:\n        return messages\n    for _, block in sorted(blocks,\n                           key=lambda pair: len(str(pair[1].get(\"content\", \"\"))),\n                           reverse=True):\n        if total <= max_bytes:\n            break\n        text = str(block.get(\"content\", \"\"))\n        block[\"content\"] = persist_large_output(\n            block.get(\"tool_use_id\", \"unknown\"), text)\n        total = sum(len(str(b.get(\"content\", \"\"))) for _, b in blocks)\n    return messages\n\n\ndef snip_compact(messages: list, max_messages: int = 50) -> list:\n    if len(messages) <= max_messages:\n        return messages\n    keep_head, keep_tail = 3, max_messages - 3\n    snipped = len(messages) - keep_head - keep_tail\n    return (messages[:keep_head]\n            + [{\"role\": \"user\", \"content\": f\"[snipped {snipped} messages]\"}]\n            + messages[-keep_tail:])\n\n\ndef micro_compact(messages: list) -> list:\n    tool_results = collect_tool_results(messages)\n    if len(tool_results) <= KEEP_RECENT_TOOL_RESULTS:\n        return messages\n    for _, _, block in tool_results[:-KEEP_RECENT_TOOL_RESULTS]:\n        if len(str(block.get(\"content\", \"\"))) > 120:\n            block[\"content\"] = \"[Earlier tool result compacted. Re-run if needed.]\"\n    return messages\n\n\ndef write_transcript(messages: list) -> Path:\n    TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)\n    path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with path.open(\"w\") as f:\n        for msg in messages:\n            f.write(json.dumps(msg, default=str) + \"\\n\")\n    return path\n\n\ndef summarize_history(messages: list) -> str:\n    conversation = json.dumps(messages, default=str)[:80000]\n    prompt = (\"Summarize this coding-agent conversation so work can continue. \"\n              \"Preserve current goal, key findings, changed files, remaining work, \"\n              \"and user constraints.\\n\\n\" + conversation)\n    response = client.messages.create(\n        model=MODEL,\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n        max_tokens=2000)\n    return extract_text(response.content) or \"(empty summary)\"\n\n\ndef compact_history(messages: list) -> list:\n    transcript = write_transcript(messages)\n    print(f\"  \\033[36m[compact] transcript saved: {transcript}\\033[0m\")\n    summary = summarize_history(messages)\n    return [{\"role\": \"user\", \"content\": f\"[Compacted]\\n\\n{summary}\"}]\n\n\ndef reactive_compact(messages: list) -> list:\n    transcript = write_transcript(messages)\n    print(f\"  \\033[31m[reactive compact] transcript saved: {transcript}\\033[0m\")\n    try:\n        summary = summarize_history(messages)\n    except Exception:\n        summary = \"Earlier conversation was trimmed after a prompt-too-long error.\"\n    return [{\"role\": \"user\", \"content\": f\"[Reactive compact]\\n\\n{summary}\"},\n            *messages[-5:]]\n\n\n# ── Error Recovery ──\n\nclass RecoveryState:\n    def __init__(self):\n        self.has_escalated = False\n        self.recovery_count = 0\n        self.consecutive_529 = 0\n        self.has_attempted_reactive_compact = False\n        self.current_model = PRIMARY_MODEL\n\n\ndef retry_delay(attempt: int) -> float:\n    base = min(BASE_DELAY_MS * (2 ** attempt), 32000) / 1000\n    return base + random.uniform(0, base * 0.25)\n\n\ndef with_retry(fn, state: RecoveryState):\n    for attempt in range(MAX_RETRIES):\n        try:\n            result = fn()\n            state.consecutive_529 = 0\n            return result\n        except Exception as e:\n            name = type(e).__name__.lower()\n            msg = str(e).lower()\n            if \"ratelimit\" in name or \"429\" in msg:\n                delay = retry_delay(attempt)\n                print(f\"  \\033[33m[429] retry {attempt + 1}/{MAX_RETRIES} \"\n                      f\"after {delay:.1f}s\\033[0m\")\n                time.sleep(delay)\n                continue\n            if \"overloaded\" in name or \"529\" in msg or \"overloaded\" in msg:\n                state.consecutive_529 += 1\n                if state.consecutive_529 >= MAX_CONSECUTIVE_529 and FALLBACK_MODEL:\n                    state.current_model = FALLBACK_MODEL\n                    state.consecutive_529 = 0\n                    print(f\"  \\033[31m[529] switching to {FALLBACK_MODEL}\\033[0m\")\n                delay = retry_delay(attempt)\n                print(f\"  \\033[33m[529] retry {attempt + 1}/{MAX_RETRIES} \"\n                      f\"after {delay:.1f}s\\033[0m\")\n                time.sleep(delay)\n                continue\n            raise\n    raise RuntimeError(f\"Max retries ({MAX_RETRIES}) exceeded\")\n\n\ndef is_prompt_too_long_error(e: Exception) -> bool:\n    msg = str(e).lower()\n    return ((\"prompt\" in msg and \"long\" in msg)\n            or \"context_length_exceeded\" in msg\n            or \"max_context_window\" in msg)\n\n\n# ── Background Tasks ──\n\n# Slow tools return a placeholder tool_result immediately. Their real output is\n# later injected as a task_notification, so the main loop can keep moving.\n_bg_counter = 0\nbackground_tasks: dict[str, dict] = {}\nbackground_results: dict[str, str] = {}\nbackground_lock = threading.Lock()\n\n\ndef is_slow_operation(tool_name: str, tool_input: dict) -> bool:\n    if tool_name != \"bash\":\n        return False\n    command = tool_input.get(\"command\", \"\").lower()\n    slow_keywords = [\"install\", \"build\", \"test\", \"deploy\", \"compile\",\n                     \"docker build\", \"pip install\", \"npm install\",\n                     \"cargo build\", \"pytest\", \"make\"]\n    return any(keyword in command for keyword in slow_keywords)\n\n\ndef should_run_background(tool_name: str, tool_input: dict) -> bool:\n    if tool_name != \"bash\":\n        return False\n    return bool(tool_input.get(\"run_in_background\")) or is_slow_operation(tool_name, tool_input)\n\n\ndef start_background_task(block, handlers: dict) -> str:\n    global _bg_counter\n    _bg_counter += 1\n    bg_id = f\"bg_{_bg_counter:04d}\"\n    command = block.input.get(\"command\", block.name)\n\n    def worker():\n        handler = handlers.get(block.name)\n        result = call_tool_handler(handler, block.input, block.name)\n        trigger_hooks(\"PostToolUse\", block, result)\n        with background_lock:\n            background_tasks[bg_id][\"status\"] = \"completed\"\n            background_results[bg_id] = str(result)\n\n    with background_lock:\n        background_tasks[bg_id] = {\n            \"tool_use_id\": block.id,\n            \"command\": command,\n            \"status\": \"running\",\n        }\n    threading.Thread(target=worker, daemon=True).start()\n    print(f\"  \\033[33m[background] {bg_id}: {str(command)[:60]}\\033[0m\")\n    return bg_id\n\n\ndef collect_background_results() -> list[str]:\n    with background_lock:\n        ready = [bg_id for bg_id, task in background_tasks.items()\n                 if task[\"status\"] == \"completed\"]\n    notifications = []\n    for bg_id in ready:\n        with background_lock:\n            task = background_tasks.pop(bg_id)\n            output = background_results.pop(bg_id, \"\")\n        summary = output[:200] if len(output) > 200 else output\n        notifications.append(\n            f\"<task_notification>\\n\"\n            f\"  <task_id>{bg_id}</task_id>\\n\"\n            f\"  <status>completed</status>\\n\"\n            f\"  <command>{task['command']}</command>\\n\"\n            f\"  <summary>{summary}</summary>\\n\"\n            f\"</task_notification>\")\n    return notifications\n\n\n# ── Cron Scheduler ──\n\n# Cron jobs are stored separately from conversation history. When a job fires,\n# it becomes a scheduled prompt that is injected back into the same agent loop.\nDURABLE_PATH = WORKDIR / \".scheduled_tasks.json\"\n\n\n@dataclass\nclass CronJob:\n    id: str\n    cron: str\n    prompt: str\n    recurring: bool\n    durable: bool\n\n\nscheduled_jobs: dict[str, CronJob] = {}\ncron_queue: list[CronJob] = []\ncron_lock = threading.Lock()\n_last_fired: dict[str, str] = {}\n\n\ndef _cron_field_matches(field: str, value: int) -> bool:\n    if field == \"*\":\n        return True\n    if field.startswith(\"*/\"):\n        step = int(field[2:])\n        return step > 0 and value % step == 0\n    if \",\" in field:\n        return any(_cron_field_matches(part.strip(), value)\n                   for part in field.split(\",\"))\n    if \"-\" in field:\n        lo, hi = field.split(\"-\", 1)\n        return int(lo) <= value <= int(hi)\n    return value == int(field)\n\n\ndef cron_matches(cron_expr: str, dt: datetime) -> bool:\n    fields = cron_expr.strip().split()\n    if len(fields) != 5:\n        return False\n    minute, hour, dom, month, dow = fields\n    dow_val = (dt.weekday() + 1) % 7\n    m = _cron_field_matches(minute, dt.minute)\n    h = _cron_field_matches(hour, dt.hour)\n    dom_ok = _cron_field_matches(dom, dt.day)\n    month_ok = _cron_field_matches(month, dt.month)\n    dow_ok = _cron_field_matches(dow, dow_val)\n    if not (m and h and month_ok):\n        return False\n    if dom == \"*\" and dow == \"*\":\n        return True\n    if dom == \"*\":\n        return dow_ok\n    if dow == \"*\":\n        return dom_ok\n    return dom_ok or dow_ok\n\n\ndef _validate_cron_field(field: str, lo: int, hi: int) -> str | None:\n    if field == \"*\":\n        return None\n    if field.startswith(\"*/\"):\n        step = field[2:]\n        if not step.isdigit() or int(step) <= 0:\n            return f\"Invalid step: {field}\"\n        return None\n    if \",\" in field:\n        for part in field.split(\",\"):\n            err = _validate_cron_field(part.strip(), lo, hi)\n            if err:\n                return err\n        return None\n    if \"-\" in field:\n        left, right = field.split(\"-\", 1)\n        if not left.isdigit() or not right.isdigit():\n            return f\"Invalid range: {field}\"\n        a, b = int(left), int(right)\n        if a < lo or a > hi or b < lo or b > hi:\n            return f\"Range {field} out of bounds [{lo}-{hi}]\"\n        if a > b:\n            return f\"Range start > end: {field}\"\n        return None\n    if not field.isdigit():\n        return f\"Invalid field: {field}\"\n    value = int(field)\n    if value < lo or value > hi:\n        return f\"Value {value} out of bounds [{lo}-{hi}]\"\n    return None\n\n\ndef validate_cron(cron_expr: str) -> str | None:\n    fields = cron_expr.strip().split()\n    if len(fields) != 5:\n        return f\"Expected 5 fields, got {len(fields)}\"\n    bounds = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)]\n    names = [\"minute\", \"hour\", \"day-of-month\", \"month\", \"day-of-week\"]\n    for field, (lo, hi), name in zip(fields, bounds, names):\n        err = _validate_cron_field(field, lo, hi)\n        if err:\n            return f\"{name}: {err}\"\n    return None\n\n\ndef save_durable_jobs():\n    durable = [asdict(job) for job in scheduled_jobs.values() if job.durable]\n    DURABLE_PATH.write_text(json.dumps(durable, indent=2))\n\n\ndef load_durable_jobs():\n    if not DURABLE_PATH.exists():\n        return\n    try:\n        for item in json.loads(DURABLE_PATH.read_text()):\n            job = CronJob(**item)\n            if not validate_cron(job.cron):\n                scheduled_jobs[job.id] = job\n    except Exception:\n        pass\n\n\ndef schedule_job(cron: str, prompt: str,\n                 recurring: bool = True, durable: bool = True) -> CronJob | str:\n    err = validate_cron(cron)\n    if err:\n        return err\n    job = CronJob(\n        id=f\"cron_{random.randint(0, 999999):06d}\",\n        cron=cron, prompt=prompt,\n        recurring=recurring, durable=durable)\n    with cron_lock:\n        scheduled_jobs[job.id] = job\n    if durable:\n        save_durable_jobs()\n    return job\n\n\ndef cancel_job(job_id: str) -> str:\n    with cron_lock:\n        job = scheduled_jobs.pop(job_id, None)\n    if not job:\n        return f\"Job {job_id} not found\"\n    if job.durable:\n        save_durable_jobs()\n    return f\"Cancelled {job_id}\"\n\n\ndef cron_scheduler_loop():\n    while True:\n        time.sleep(1)\n        now = datetime.now()\n        marker = now.strftime(\"%Y-%m-%d %H:%M\")\n        with cron_lock:\n            for job in list(scheduled_jobs.values()):\n                try:\n                    if cron_matches(job.cron, now) and _last_fired.get(job.id) != marker:\n                        cron_queue.append(job)\n                        _last_fired[job.id] = marker\n                        if not job.recurring:\n                            scheduled_jobs.pop(job.id, None)\n                            if job.durable:\n                                save_durable_jobs()\n                except Exception as e:\n                    print(f\"  \\033[31m[cron error] {job.id}: {e}\\033[0m\")\n\n\ndef consume_cron_queue() -> list[CronJob]:\n    with cron_lock:\n        fired = list(cron_queue)\n        cron_queue.clear()\n    return fired\n\n\ndef run_schedule_cron(cron: str, prompt: str,\n                      recurring: bool = True, durable: bool = True) -> str:\n    result = schedule_job(cron, prompt, recurring, durable)\n    if isinstance(result, str):\n        return f\"Error: {result}\"\n    return f\"Scheduled {result.id}: '{cron}' -> {prompt}\"\n\n\ndef run_list_crons() -> str:\n    with cron_lock:\n        jobs = list(scheduled_jobs.values())\n    if not jobs:\n        return \"No cron jobs.\"\n    return \"\\n\".join(\n        f\"  {job.id}: '{job.cron}' -> {job.prompt[:40]} \"\n        f\"[{'recurring' if job.recurring else 'one-shot'}, \"\n        f\"{'durable' if job.durable else 'session'}]\"\n        for job in jobs)\n\n\ndef run_cancel_cron(job_id: str) -> str:\n    return cancel_job(job_id)\n\n\nload_durable_jobs()\nthreading.Thread(target=cron_scheduler_loop, daemon=True).start()\n\n\n# ── MCP System ──\n\n# MCP is modeled as late-bound tools: connect first, then discovered server\n# tools are merged into the normal tool pool with mcp__server__tool names.\nclass MCPClient:\n    \"\"\"Discovers and calls tools on an MCP server (mock for teaching).\"\"\"\n\n    def __init__(self, name: str):\n        self.name = name\n        self.tools: list[dict] = []\n        self._handlers: dict[str, callable] = {}\n\n    def register(self, tool_defs: list[dict],\n                 handlers: dict[str, callable]):\n        self.tools = tool_defs\n        self._handlers = handlers\n\n    def call_tool(self, tool_name: str, args: dict) -> str:\n        handler = self._handlers.get(tool_name)\n        if not handler:\n            return f\"MCP error: unknown tool '{tool_name}'\"\n        try:\n            return handler(**args)\n        except Exception as e:\n            return f\"MCP error: {e}\"\n\n\nmcp_clients: dict[str, MCPClient] = {}\n\n_DISALLOWED_CHARS = re.compile(r'[^a-zA-Z0-9_-]')\n\n\ndef normalize_mcp_name(name: str) -> str:\n    \"\"\"Replace non [a-zA-Z0-9_-] with underscore.\"\"\"\n    return _DISALLOWED_CHARS.sub('_', name)\n\n\ndef _mock_server_docs():\n    client = MCPClient(\"docs\")\n    client.register(\n        tool_defs=[\n            {\"name\": \"search\", \"description\": \"Search documentation. (readOnly)\",\n             \"inputSchema\": {\"type\": \"object\",\n                             \"properties\": {\"query\": {\"type\": \"string\"}},\n                             \"required\": [\"query\"]}},\n            {\"name\": \"get_version\", \"description\": \"Get API version. (readOnly)\",\n             \"inputSchema\": {\"type\": \"object\", \"properties\": {},\n                             \"required\": []}},\n        ],\n        handlers={\n            \"search\": lambda query: f\"[docs] Found 3 results for '{query}'\",\n            \"get_version\": lambda: \"[docs] API v2.1.0\",\n        })\n    return client\n\n\ndef _mock_server_deploy():\n    client = MCPClient(\"deploy\")\n    client.register(\n        tool_defs=[\n            {\"name\": \"trigger\",\n             \"description\": \"Trigger a deployment. (destructive — requires approval in real CC)\",\n             \"inputSchema\": {\"type\": \"object\",\n                             \"properties\": {\"service\": {\"type\": \"string\"}},\n                             \"required\": [\"service\"]}},\n            {\"name\": \"status\", \"description\": \"Check deployment status. (readOnly)\",\n             \"inputSchema\": {\"type\": \"object\",\n                             \"properties\": {\"service\": {\"type\": \"string\"}},\n                             \"required\": [\"service\"]}},\n        ],\n        handlers={\n            \"trigger\": lambda service: f\"[deploy] Triggered: {service}\",\n            \"status\": lambda service: f\"[deploy] {service}: running (v1.4.2)\",\n        })\n    return client\n\n\nMOCK_SERVERS = {\n    \"docs\": _mock_server_docs,\n    \"deploy\": _mock_server_deploy,\n}\n\n\ndef connect_mcp(name: str) -> str:\n    if name in mcp_clients:\n        return f\"MCP server '{name}' already connected\"\n    factory = MOCK_SERVERS.get(name)\n    if not factory:\n        available = \", \".join(MOCK_SERVERS.keys())\n        return f\"Unknown server '{name}'. Available: {available}\"\n    mcp_client = factory()\n    mcp_clients[name] = mcp_client\n    tool_names = [t[\"name\"] for t in mcp_client.tools]\n    print(f\"  \\033[31m[mcp] connected: {name} → {tool_names}\\033[0m\")\n    return (f\"Connected to MCP server '{name}'. \"\n            f\"Discovered {len(mcp_client.tools)} tools: {', '.join(tool_names)}\")\n\n\ndef assemble_tool_pool() -> tuple[list[dict], dict]:\n    \"\"\"Merge builtin tools + all MCP tools into one pool.\"\"\"\n    tools = list(BUILTIN_TOOLS)\n    handlers = dict(BUILTIN_HANDLERS)\n    for server_name, mcp_client in mcp_clients.items():\n        safe_server = normalize_mcp_name(server_name)\n        for tool_def in mcp_client.tools:\n            safe_tool = normalize_mcp_name(tool_def[\"name\"])\n            prefixed = f\"mcp__{safe_server}__{safe_tool}\"\n            tools.append({\n                \"name\": prefixed,\n                \"description\": tool_def.get(\"description\", \"\"),\n                \"input_schema\": tool_def.get(\"inputSchema\", {}),\n            })\n            handlers[prefixed] = (\n                lambda *, c=mcp_client, t=tool_def[\"name\"], **kw: c.call_tool(t, kw))\n    return tools, handlers\n\n\n# ── Lead Worktree Tools ──\n\ndef run_create_worktree(name: str, task_id: str = \"\") -> str:\n    return create_worktree(name, task_id)\n\ndef run_remove_worktree(name: str, discard_changes: bool = False) -> str:\n    return remove_worktree(name, discard_changes)\n\ndef run_keep_worktree(name: str) -> str:\n    return keep_worktree(name)\n\n\n# ── Basic tool handlers ──\n\ndef run_create_task(subject: str, description: str = \"\",\n                    blockedBy: list[str] | None = None) -> str:\n    task = create_task(subject, description, blockedBy)\n    deps = f\" (blockedBy: {', '.join(blockedBy)})\" if blockedBy else \"\"\n    print(f\"  \\033[34m[create] {task.subject}{deps}\\033[0m\")\n    return f\"Created {task.id}: {task.subject}{deps}\"\n\n\ndef run_list_tasks() -> str:\n    tasks = list_tasks()\n    if not tasks:\n        return \"No tasks.\"\n    return \"\\n\".join(\n        f\"  {t.id}: {t.subject} [{t.status}]\"\n        + (f\" (wt:{t.worktree})\" if t.worktree else \"\")\n        for t in tasks)\n\n\ndef run_get_task(task_id: str) -> str:\n    try:\n        return get_task_json(task_id)\n    except FileNotFoundError:\n        return f\"Error: task {task_id} not found\"\n\ndef run_claim_task(task_id: str) -> str:\n    try:\n        return claim_task(task_id, owner=\"agent\")\n    except FileNotFoundError:\n        return f\"Error: task {task_id} not found\"\n\ndef run_complete_task(task_id: str) -> str:\n    try:\n        return complete_task(task_id)\n    except FileNotFoundError:\n        return f\"Error: task {task_id} not found\"\n\ndef run_spawn_teammate(name: str, role: str, prompt: str) -> str:\n    return spawn_teammate_thread(name, role, prompt)\n\ndef run_send_message(to: str, content: str) -> str:\n    BUS.send(\"lead\", to, content)\n    return f\"Sent to {to}\"\n\ndef run_check_inbox() -> str:\n    msgs = consume_lead_inbox(route_protocol=True)\n    if not msgs:\n        return \"(inbox empty)\"\n    lines = []\n    for m in msgs:\n        meta = m.get(\"metadata\", {})\n        req_id = meta.get(\"request_id\", \"\")\n        tag = f\" [{m['type']} req:{req_id}]\" if req_id else f\" [{m['type']}]\"\n        lines.append(f\"  [{m['from']}]{tag} {m['content'][:200]}\")\n    return \"\\n\".join(lines)\n\ndef run_connect_mcp(name: str) -> str:\n    return connect_mcp(name)\n\n\n# ── Tool Definitions ──\n\n# The model sees tool schemas; Python executes handlers. S20 keeps both tables\n# explicit so every added capability is visible in one place.\nBUILTIN_TOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"command\": {\"type\": \"string\"},\n                                     \"run_in_background\": {\"type\": \"boolean\"}},\n                      \"required\": [\"command\"]}},\n    {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"limit\": {\"type\": \"integer\"},\n                                     \"offset\": {\"type\": \"integer\"}},\n                      \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"content\"]}},\n    {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"path\": {\"type\": \"string\"},\n                                     \"old_text\": {\"type\": \"string\"},\n                                     \"new_text\": {\"type\": \"string\"}},\n                      \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n    {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"pattern\": {\"type\": \"string\"}},\n                      \"required\": [\"pattern\"]}},\n    {\"name\": \"todo_write\",\n     \"description\": \"Create and manage a task list for the current session.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"todos\": {\"type\": \"array\",\n                          \"items\": {\"type\": \"object\",\n                                    \"properties\": {\n                                        \"content\": {\"type\": \"string\"},\n                                        \"status\": {\"type\": \"string\",\n                                                   \"enum\": [\"pending\", \"in_progress\", \"completed\"]}},\n                                    \"required\": [\"content\", \"status\"]}}},\n                      \"required\": [\"todos\"]}},\n    {\"name\": \"task\",\n     \"description\": \"Launch a focused subagent. Returns only its final summary.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"description\": {\"type\": \"string\"}},\n                      \"required\": [\"description\"]}},\n    {\"name\": \"load_skill\",\n     \"description\": \"Load the full content of a skill by name.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"compact\",\n     \"description\": \"Summarize earlier conversation and continue with compacted context.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"focus\": {\"type\": \"string\"}},\n                      \"required\": []}},\n    {\"name\": \"create_task\", \"description\": \"Create a task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"subject\": {\"type\": \"string\"},\n                                     \"description\": {\"type\": \"string\"},\n                                     \"blockedBy\": {\"type\": \"array\",\n                                                   \"items\": {\"type\": \"string\"}}},\n                      \"required\": [\"subject\"]}},\n    {\"name\": \"list_tasks\", \"description\": \"List all tasks.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"get_task\", \"description\": \"Get full task details.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"claim_task\", \"description\": \"Claim a pending task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"complete_task\", \"description\": \"Complete an in-progress task.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"task_id\"]}},\n    {\"name\": \"schedule_cron\",\n     \"description\": (\"Schedule a cron job. cron is 5-field: min hour dom \"\n                     \"month dow. For one-shot reminders, compute the target \"\n                     \"minute and set recurring=false.\"),\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"cron\": {\"type\": \"string\"},\n                                     \"prompt\": {\"type\": \"string\"},\n                                     \"recurring\": {\"type\": \"boolean\"},\n                                     \"durable\": {\"type\": \"boolean\"}},\n                      \"required\": [\"cron\", \"prompt\"]}},\n    {\"name\": \"list_crons\", \"description\": \"List registered cron jobs.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"cancel_cron\", \"description\": \"Cancel a cron job by ID.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"job_id\": {\"type\": \"string\"}},\n                      \"required\": [\"job_id\"]}},\n    {\"name\": \"spawn_teammate\", \"description\": \"Spawn an autonomous teammate.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"role\": {\"type\": \"string\"},\n                                     \"prompt\": {\"type\": \"string\"}},\n                      \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"send_message\", \"description\": \"Send message to a teammate.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"to\": {\"type\": \"string\"},\n                                     \"content\": {\"type\": \"string\"}},\n                      \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"check_inbox\",\n     \"description\": \"Check inbox for messages and protocol responses.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"required\": []}},\n    {\"name\": \"request_shutdown\",\n     \"description\": \"Request a teammate to shut down.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\"]}},\n    {\"name\": \"request_plan\",\n     \"description\": \"Ask a teammate to submit a plan.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"teammate\": {\"type\": \"string\"},\n                                     \"task\": {\"type\": \"string\"}},\n                      \"required\": [\"teammate\", \"task\"]}},\n    {\"name\": \"review_plan\",\n     \"description\": \"Approve or reject a submitted plan.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"request_id\": {\"type\": \"string\"},\n                                     \"approve\": {\"type\": \"boolean\"},\n                                     \"feedback\": {\"type\": \"string\"}},\n                      \"required\": [\"request_id\", \"approve\"]}},\n    {\"name\": \"create_worktree\",\n     \"description\": \"Create an isolated git worktree.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"task_id\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"remove_worktree\",\n     \"description\": \"Remove a worktree. Refuses if changes exist.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"},\n                                     \"discard_changes\": {\"type\": \"boolean\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"keep_worktree\",\n     \"description\": \"Keep a worktree for manual review.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n    {\"name\": \"connect_mcp\",\n     \"description\": \"Connect to an MCP server (docs, deploy) and discover tools.\",\n     \"input_schema\": {\"type\": \"object\",\n                      \"properties\": {\"name\": {\"type\": \"string\"}},\n                      \"required\": [\"name\"]}},\n]\n\nBUILTIN_HANDLERS = {\n    \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n    \"edit_file\": run_edit, \"glob\": run_glob,\n    \"todo_write\": run_todo_write, \"task\": spawn_subagent,\n    \"load_skill\": load_skill,\n    \"create_task\": run_create_task, \"list_tasks\": run_list_tasks,\n    \"get_task\": run_get_task,\n    \"claim_task\": run_claim_task, \"complete_task\": run_complete_task,\n    \"schedule_cron\": run_schedule_cron,\n    \"list_crons\": run_list_crons,\n    \"cancel_cron\": run_cancel_cron,\n    \"spawn_teammate\": run_spawn_teammate,\n    \"send_message\": run_send_message, \"check_inbox\": run_check_inbox,\n    \"request_shutdown\": run_request_shutdown,\n    \"request_plan\": run_request_plan, \"review_plan\": run_review_plan,\n    \"create_worktree\": run_create_worktree,\n    \"remove_worktree\": run_remove_worktree,\n    \"keep_worktree\": run_keep_worktree,\n    \"connect_mcp\": run_connect_mcp,\n}\n\n\n# ── Context ──\n\nMEMORY_DIR = WORKDIR / \".memory\"\nMEMORY_INDEX = MEMORY_DIR / \"MEMORY.md\"\n\n\ndef update_context(context: dict, messages: list) -> dict:\n    memories = \"\"\n    if MEMORY_INDEX.exists():\n        memories = MEMORY_INDEX.read_text()[:2000]\n    return {\n        \"memories\": memories,\n        \"connected_mcp\": list(mcp_clients.keys()),\n        \"active_teammates\": list(active_teammates.keys()),\n    }\n\n\n# ── Agent Loop ──\n\nrounds_since_todo = 0\nagent_lock = threading.Lock()\n\n\ndef prepare_context(messages: list) -> list:\n    # Every LLM turn enters through the same context budget pipeline.\n    messages[:] = tool_result_budget(messages)\n    messages[:] = snip_compact(messages)\n    messages[:] = micro_compact(messages)\n    if estimate_size(messages) > CONTEXT_LIMIT:\n        messages[:] = compact_history(messages)\n    return messages\n\n\ndef build_user_content(results: list[dict]) -> list[dict]:\n    # Tool results and completed background notifications are both returned to\n    # the model as user-side content, matching the tool_result feedback loop.\n    content = list(results)\n    for note in collect_background_results():\n        content.append({\"type\": \"text\", \"text\": note})\n    return content\n\n\ndef inject_background_notifications(messages: list):\n    notes = collect_background_results()\n    if notes:\n        messages.append({\"role\": \"user\", \"content\": [\n            {\"type\": \"text\", \"text\": note} for note in notes]})\n\n\ndef call_llm(messages: list, context: dict, tools: list,\n             state: RecoveryState, max_tokens: int):\n    system = assemble_system_prompt(context)\n    return with_retry(\n        lambda: client.messages.create(\n            model=state.current_model,\n            system=system,\n            messages=messages,\n            tools=tools,\n            max_tokens=max_tokens),\n        state)\n\n\ndef agent_loop(messages: list, context: dict):\n    global rounds_since_todo\n    tools, handlers = assemble_tool_pool()\n    state = RecoveryState()\n    max_tokens = DEFAULT_MAX_TOKENS\n\n    while True:\n        # One cycle: inject scheduled/background work, prepare context, call\n        # the model, execute tool_use blocks, append tool_results, repeat.\n        fired = consume_cron_queue()\n        for job in fired:\n            messages.append({\"role\": \"user\",\n                             \"content\": f\"[Scheduled] {job.prompt}\"})\n            print(f\"  \\033[35m[cron inject] {job.prompt[:60]}\\033[0m\")\n\n        inject_background_notifications(messages)\n\n        if rounds_since_todo >= 3:\n            messages.append({\"role\": \"user\",\n                             \"content\": \"<reminder>Update your todos.</reminder>\"})\n            rounds_since_todo = 0\n\n        prepare_context(messages)\n        context = update_context(context, messages)\n        tools, handlers = assemble_tool_pool()\n\n        try:\n            response = call_llm(messages, context, tools, state, max_tokens)\n        except Exception as e:\n            if is_prompt_too_long_error(e) and not state.has_attempted_reactive_compact:\n                messages[:] = reactive_compact(messages)\n                state.has_attempted_reactive_compact = True\n                continue\n            messages.append({\"role\": \"assistant\", \"content\": [\n                {\"type\": \"text\", \"text\": f\"[Error] {type(e).__name__}: {e}\"}]})\n            return\n\n        if response.stop_reason == \"max_tokens\":\n            if not state.has_escalated:\n                max_tokens = ESCALATED_MAX_TOKENS\n                state.has_escalated = True\n                print(f\"  \\033[33m[max_tokens] retry with {max_tokens}\\033[0m\")\n                continue\n            messages.append({\"role\": \"assistant\", \"content\": response.content})\n            if state.recovery_count < MAX_RECOVERY_RETRIES:\n                messages.append({\"role\": \"user\", \"content\": CONTINUATION_PROMPT})\n                state.recovery_count += 1\n                continue\n            return\n\n        max_tokens = DEFAULT_MAX_TOKENS\n        state.has_escalated = False\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if not has_tool_use(response.content):\n            trigger_hooks(\"Stop\", messages)\n            return\n\n        results = []\n        compacted_now = False\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"\\033[36m> {block.name}\\033[0m\")\n\n            if block.name == \"compact\":\n                messages[:] = compact_history(messages)\n                messages.append({\"role\": \"user\",\n                                 \"content\": \"[Compacted. Continue with summarized context.]\"})\n                compacted_now = True\n                break\n\n            blocked = trigger_hooks(\"PreToolUse\", block)\n            if blocked:\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": str(blocked)})\n                continue\n\n            if should_run_background(block.name, block.input):\n                bg_id = start_background_task(block, handlers)\n                output = (f\"[Background task {bg_id} started] \"\n                          \"Result will arrive as a task_notification.\")\n                results.append({\"type\": \"tool_result\",\n                                \"tool_use_id\": block.id,\n                                \"content\": output})\n                continue\n\n            handler = handlers.get(block.name)\n            output = call_tool_handler(handler, block.input, block.name)\n            trigger_hooks(\"PostToolUse\", block, output)\n            print(str(output)[:300])\n\n            if block.name == \"todo_write\":\n                rounds_since_todo = 0\n            else:\n                rounds_since_todo += 1\n\n            results.append({\"type\": \"tool_result\",\n                            \"tool_use_id\": block.id, \"content\": output})\n\n        if compacted_now:\n            continue\n\n        messages.append({\"role\": \"user\", \"content\": build_user_content(results)})\n\n\ndef print_turn_assistants(messages: list, turn_start: int):\n    for msg in messages[turn_start:]:\n        if msg.get(\"role\") != \"assistant\":\n            continue\n        for block in msg.get(\"content\", []):\n            if getattr(block, \"type\", None) == \"text\":\n                terminal_print(block.text)\n\n\ndef cron_autorun_loop(history: list, context: dict):\n    while True:\n        time.sleep(1)\n        fired = consume_cron_queue()\n        if not fired:\n            continue\n        with agent_lock:\n            turn_start = len(history)\n            for job in fired:\n                history.append({\"role\": \"user\",\n                                \"content\": f\"[Scheduled] {job.prompt}\"})\n                terminal_print(\n                    f\"  \\033[35m[cron auto] {job.prompt[:60]}\\033[0m\")\n            agent_loop(history, context)\n            context.update(update_context(context, history))\n            print_turn_assistants(history, turn_start)\n\n\nif __name__ == \"__main__\":\n    CLI_ACTIVE = True\n    print(\"s20: comprehensive agent\")\n    print(\"Enter a question, press Enter to send. Type q to quit.\\n\")\n    history = []\n    context = update_context({}, [])\n    threading.Thread(target=cron_autorun_loop,\n                     args=(history, context), daemon=True).start()\n    while True:\n        try:\n            query = input(PROMPT)\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        trigger_hooks(\"UserPromptSubmit\", query)\n        turn_start = len(history)\n        history.append({\"role\": \"user\", \"content\": query})\n        with agent_lock:\n            agent_loop(history, context)\n            context = update_context(context, history)\n            print_turn_assistants(history, turn_start)\n\n        inbox = consume_lead_inbox(route_protocol=True)\n        if inbox:\n            def inbox_label(msg):\n                req_id = msg.get(\"metadata\", {}).get(\"request_id\", \"\")\n                suffix = f\" req:{req_id}\" if req_id else \"\"\n                return f\"{msg.get('type', 'message')}{suffix}\"\n\n            inbox_text = \"\\n\".join(\n                f\"From {m['from']} [{inbox_label(m)}]: \"\n                f\"{m['content'][:200]}\" for m in inbox)\n            history.append({\"role\": \"user\",\n                            \"content\": f\"[Inbox]\\n{inbox_text}\"})\n        print()\n",
      "images": [
        {
          "src": "/course-assets/s20_comprehensive/system-architecture.svg",
          "alt": "system architecture"
        }
      ]
    }
  ],
  "diffs": [
    {
      "from": "s01",
      "to": "s02",
      "newClasses": [],
      "newFunctions": [
        "safe_path",
        "run_read",
        "run_write",
        "run_edit",
        "run_glob"
      ],
      "newTools": [
        "read_file",
        "write_file",
        "edit_file",
        "glob"
      ],
      "locDelta": 33
    },
    {
      "from": "s02",
      "to": "s03",
      "newClasses": [],
      "newFunctions": [
        "check_deny_list",
        "check_rules",
        "ask_user",
        "check_permission"
      ],
      "newTools": [],
      "locDelta": 45
    },
    {
      "from": "s03",
      "to": "s04",
      "newClasses": [],
      "newFunctions": [
        "register_hook",
        "trigger_hooks",
        "permission_hook",
        "log_hook",
        "large_output_hook",
        "context_inject_hook",
        "summary_hook"
      ],
      "newTools": [],
      "locDelta": 52
    },
    {
      "from": "s04",
      "to": "s05",
      "newClasses": [],
      "newFunctions": [
        "_normalize_todos",
        "run_todo_write"
      ],
      "newTools": [
        "todo_write"
      ],
      "locDelta": 4
    },
    {
      "from": "s05",
      "to": "s06",
      "newClasses": [],
      "newFunctions": [
        "extract_text",
        "spawn_subagent"
      ],
      "newTools": [
        "task"
      ],
      "locDelta": 68
    },
    {
      "from": "s06",
      "to": "s07",
      "newClasses": [],
      "newFunctions": [
        "_parse_frontmatter",
        "_scan_skills",
        "list_skills",
        "build_system",
        "load_skill"
      ],
      "newTools": [
        "load_skill"
      ],
      "locDelta": 31
    },
    {
      "from": "s07",
      "to": "s08",
      "newClasses": [],
      "newFunctions": [
        "estimate_size",
        "snip_compact",
        "collect_tool_results",
        "micro_compact",
        "persist_large_output",
        "tool_result_budget",
        "write_transcript",
        "summarize_history",
        "compact_history",
        "reactive_compact"
      ],
      "newTools": [
        "compact"
      ],
      "locDelta": 47
    },
    {
      "from": "s08",
      "to": "s09",
      "newClasses": [],
      "newFunctions": [
        "write_memory_file",
        "_rebuild_index",
        "read_memory_index",
        "read_memory_file",
        "list_memory_files",
        "select_relevant_memories",
        "load_memories",
        "extract_memories",
        "consolidate_memories",
        "persist_large"
      ],
      "newTools": [],
      "locDelta": 116
    },
    {
      "from": "s09",
      "to": "s10",
      "newClasses": [],
      "newFunctions": [
        "assemble_system_prompt",
        "get_system_prompt",
        "update_context"
      ],
      "newTools": [],
      "locDelta": -332
    },
    {
      "from": "s10",
      "to": "s11",
      "newClasses": [
        "RecoveryState"
      ],
      "newFunctions": [
        "retry_delay",
        "with_retry",
        "is_prompt_too_long_error",
        "reactive_compact"
      ],
      "newTools": [],
      "locDelta": 121
    },
    {
      "from": "s11",
      "to": "s12",
      "newClasses": [
        "Task"
      ],
      "newFunctions": [
        "_task_path",
        "save_task",
        "load_task",
        "list_tasks",
        "get_task",
        "can_start",
        "claim_task",
        "complete_task",
        "run_list_tasks",
        "run_get_task",
        "run_claim_task",
        "run_complete_task"
      ],
      "newTools": [
        "create_task",
        "list_tasks",
        "get_task",
        "claim_task",
        "complete_task"
      ],
      "locDelta": 10
    },
    {
      "from": "s12",
      "to": "s13",
      "newClasses": [],
      "newFunctions": [
        "is_slow_operation",
        "should_run_background",
        "execute_tool",
        "start_background_task",
        "collect_background_results"
      ],
      "newTools": [],
      "locDelta": 82
    },
    {
      "from": "s13",
      "to": "s14",
      "newClasses": [
        "CronJob"
      ],
      "newFunctions": [
        "_cron_field_matches",
        "cron_matches",
        "_validate_cron_field",
        "validate_cron",
        "save_durable_jobs",
        "load_durable_jobs",
        "cancel_job",
        "cron_scheduler_loop",
        "consume_cron_queue",
        "has_cron_queue",
        "run_list_crons",
        "run_cancel_cron",
        "print_latest_assistant_text",
        "run_agent_turn_locked",
        "queue_processor_loop"
      ],
      "newTools": [
        "schedule_cron",
        "list_crons",
        "cancel_cron"
      ],
      "locDelta": 266
    },
    {
      "from": "s14",
      "to": "s15",
      "newClasses": [
        "MessageBus"
      ],
      "newFunctions": [
        "spawn_teammate_thread",
        "run_spawn_teammate",
        "run_send_message",
        "run_check_inbox"
      ],
      "newTools": [
        "send_message",
        "spawn_teammate",
        "check_inbox"
      ],
      "locDelta": 100
    },
    {
      "from": "s15",
      "to": "s16",
      "newClasses": [
        "ProtocolState"
      ],
      "newFunctions": [
        "new_request_id",
        "match_response",
        "consume_lead_inbox",
        "_teammate_submit_plan",
        "run_request_shutdown",
        "run_request_plan",
        "run_review_plan"
      ],
      "newTools": [
        "submit_plan",
        "request_shutdown",
        "request_plan",
        "review_plan"
      ],
      "locDelta": -36
    },
    {
      "from": "s16",
      "to": "s17",
      "newClasses": [],
      "newFunctions": [
        "scan_unclaimed_tasks"
      ],
      "newTools": [],
      "locDelta": -61
    },
    {
      "from": "s17",
      "to": "s18",
      "newClasses": [],
      "newFunctions": [
        "get_task_json",
        "validate_worktree_name",
        "run_git",
        "log_event",
        "create_worktree",
        "bind_task_to_worktree",
        "_count_worktree_changes",
        "remove_worktree",
        "keep_worktree",
        "run_create_worktree",
        "run_remove_worktree",
        "run_keep_worktree"
      ],
      "newTools": [
        "create_worktree",
        "remove_worktree",
        "keep_worktree"
      ],
      "locDelta": 154
    },
    {
      "from": "s18",
      "to": "s19",
      "newClasses": [
        "MCPClient"
      ],
      "newFunctions": [
        "normalize_mcp_name",
        "_mock_server_docs",
        "_mock_server_deploy",
        "connect_mcp",
        "assemble_tool_pool",
        "run_connect_mcp"
      ],
      "newTools": [
        "search",
        "get_version",
        "trigger",
        "status",
        "connect_mcp"
      ],
      "locDelta": 33
    },
    {
      "from": "s19",
      "to": "s20",
      "newClasses": [
        "RecoveryState",
        "CronJob"
      ],
      "newFunctions": [
        "terminal_print",
        "_parse_frontmatter",
        "scan_skills",
        "list_skills",
        "load_skill",
        "run_glob",
        "call_tool_handler",
        "_normalize_todos",
        "run_todo_write",
        "register_hook",
        "trigger_hooks",
        "permission_hook",
        "log_hook",
        "large_output_hook",
        "user_prompt_hook",
        "stop_hook",
        "extract_text",
        "has_tool_use",
        "spawn_subagent",
        "estimate_size",
        "collect_tool_results",
        "persist_large_output",
        "tool_result_budget",
        "snip_compact",
        "micro_compact",
        "write_transcript",
        "summarize_history",
        "compact_history",
        "reactive_compact",
        "retry_delay",
        "with_retry",
        "is_prompt_too_long_error",
        "is_slow_operation",
        "should_run_background",
        "start_background_task",
        "collect_background_results",
        "_cron_field_matches",
        "cron_matches",
        "_validate_cron_field",
        "validate_cron",
        "save_durable_jobs",
        "load_durable_jobs",
        "cancel_job",
        "cron_scheduler_loop",
        "consume_cron_queue",
        "run_list_crons",
        "run_cancel_cron",
        "prepare_context",
        "build_user_content",
        "inject_background_notifications",
        "print_turn_assistants",
        "cron_autorun_loop"
      ],
      "newTools": [
        "edit_file",
        "glob",
        "todo_write",
        "task",
        "load_skill",
        "compact",
        "schedule_cron",
        "list_crons",
        "cancel_cron"
      ],
      "locDelta": 842
    }
  ]
}
