[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: web\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 20\n          cache: npm\n          cache-dependency-path: web/package-lock.json\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Type check\n        run: npx tsc --noEmit\n\n      - name: Build\n        run: npm run build\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  unit-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.11\"\n\n      - name: Install dependencies\n        run: pip install anthropic python-dotenv\n\n      - name: Run unit tests\n        run: python tests/test_unit.py\n\n  session-test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        session: [v0, v1, v2, v3, v4, v5, v6, v7, v8a, v8b, v8c, v9]\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.11\"\n\n      - name: Install dependencies\n        run: pip install anthropic python-dotenv\n\n      - name: Run ${{ matrix.session }} tests\n        env:\n          TEST_API_KEY: ${{ secrets.TEST_API_KEY }}\n          TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }}\n          TEST_MODEL: ${{ secrets.TEST_MODEL }}\n        run: python tests/test_${{ matrix.session }}.py\n\n  web-build:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: web\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: \"20\"\n          cache: \"npm\"\n          cache-dependency-path: web/package-lock.json\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build\n        run: npm run build\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[codz]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\n/lib/\n/lib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py.cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# UV\n#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#uv.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n#poetry.toml\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#   pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.\n#   https://pdm-project.org/en/latest/usage/project/#working-with-version-control\n#pdm.lock\n#pdm.toml\n.pdm-python\n.pdm-build/\n\n# pixi\n#   Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.\n#pixi.lock\n#   Pixi creates a virtual environment in the .pixi directory, just like venv module creates one\n#   in the .venv directory. It is recommended not to include this directory in version control.\n.pixi\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.envrc\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n# Abstra\n# Abstra is an AI-powered process automation framework.\n# Ignore directories containing user credentials, local state, and settings.\n# Learn more at https://abstra.io/docs\n.abstra/\n\n# Visual Studio Code\n#  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore \n#  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore\n#  and can be added to the global gitignore or merged into this file. However, if you prefer, \n#  you could uncomment the following to ignore the entire vscode folder\n# .vscode/\n\n# Transcripts (generated by compression agent)\n.transcripts/\n\n# Runtime artifacts (generated by agent tests)\n.task_outputs/\n.tasks/\n.teams/\n\n# Ruff stuff:\n.ruff_cache/\n\n# PyPI configuration file\n.pypirc\n\n# Cursor\n#  Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to\n#  exclude from AI features like autocomplete and code analysis. Recommended for sensitive data\n#  refer to https://docs.cursor.com/context/ignore-files\n.cursorignore\n.cursorindexingignore\n\n# Marimo\nmarimo/_static/\nmarimo/_lsp/\n__marimo__/\n\n# Web app\nweb/node_modules/\nweb/.next/\nweb/out/\n.vercel\n.env*.local\ntest_providers.py\n\n# Internal analysis artifacts (not learning material)\nanalysis/\nanalysis_progress.md\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 shareAI Lab\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README-ja.md",
    "content": "# Learn Claude Code -- 真の Agent のための Harness Engineering\n\n[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md)\n\n## モデルこそが Agent である\n\nコードの話をする前に、一つだけ明確にしておく。\n\n**Agent とはモデルのことだ。フレームワークではない。プロンプトチェーンではない。ドラッグ＆ドロップのワークフローではない。**\n\n### Agent とは何か\n\nAgent とはニューラルネットワークである -- Transformer、RNN、学習された関数 -- 数十億回の勾配更新を経て、行動系列データの上で環境を知覚し、目標を推論し、行動を起こすことを学んだもの。AI における \"Agent\" という言葉は、始まりからずっとこの意味だった。常に。\n\n人間も Agent だ。数百万年の進化的訓練によって形作られた生物的ニューラルネットワーク。感覚で世界を知覚し、脳で推論し、身体で行動する。DeepMind、OpenAI、Anthropic が \"Agent\" と言うとき、それはこの分野が誕生以来ずっと意味してきたものと同じだ：**行動することを学んだモデル。**\n\n歴史がその証拠を刻んでいる：\n\n- **2013 -- DeepMind DQN が Atari をプレイ。** 単一のニューラルネットワークが、生のピクセルとスコアだけを受け取り、7 つの Atari 2600 ゲームを学習 -- すべての先行アルゴリズムを超え、3 つで人間の専門家を打ち負かした。2015 年には同じアーキテクチャが [49 ゲームに拡張され、プロのテスターに匹敵](https://www.nature.com/articles/nature14236)、*Nature* に掲載。ゲーム固有のルールなし。決定木なし。一つのモデルが経験から学んだ。そのモデルが Agent だった。\n\n- **2019 -- OpenAI Five が Dota 2 を制覇。** 5 つのニューラルネットワークが 10 ヶ月間で [45,000 年分の Dota 2](https://openai.com/index/openai-five-defeats-dota-2-world-champions/) を自己対戦し、サンフランシスコのライブストリームで **OG** -- TI8 世界王者 -- を 2-0 で撃破。その後の公開アリーナでは 42,729 試合で勝率 99.4%。スクリプト化された戦略なし。メタプログラムされたチーム連携なし。モデルが完全に自己対戦を通じてチームワーク、戦術、リアルタイム適応を学んだ。\n\n- **2019 -- DeepMind AlphaStar が StarCraft II をマスター。** AlphaStar は非公開戦で[プロ選手を 10-1 で撃破](https://deepmind.google/blog/alphastar-mastering-the-real-time-strategy-game-starcraft-ii/)、その後ヨーロッパサーバーで[グランドマスター到達](https://www.nature.com/articles/d41586-019-03298-6) -- 90,000 人中の上位 0.15%。不完全情報、リアルタイム判断、チェスや囲碁を遥かに凌駕する組合せ的行動空間を持つゲーム。Agent とは？ モデルだ。訓練されたもの。スクリプトではない。\n\n- **2019 -- Tencent 絶悟が王者栄耀を支配。** Tencent AI Lab の「絶悟」は 2019 年 8 月 2 日、世界チャンピオンカップで [KPL プロ選手を 5v5 で撃破](https://www.jiemian.com/article/3371171.html)。1v1 モードではプロが [15 戦中 1 勝のみ、8 分以上生存不可](https://developer.aliyun.com/article/851058)。訓練強度：1 日 = 人間の 440 年。2021 年までに全ヒーロープールで KPL プロを全面的に上回った。手書きのヒーロー相性表なし。スクリプト化されたチーム編成なし。自己対戦でゲーム全体をゼロから学んだモデル。\n\n- **2024-2025 -- LLM Agent がソフトウェアエンジニアリングを再構築。** Claude、GPT、Gemini -- 人類のコードと推論の全幅で訓練された大規模言語モデル -- がコーディング Agent として展開される。コードベースを読み、実装を書き、障害をデバッグし、チームで協調する。アーキテクチャは先行するすべての Agent と同一：訓練されたモデルが環境に配置され、知覚と行動のツールを与えられる。唯一の違いは、学んだものの規模と解くタスクの汎用性。\n\nすべてのマイルストーンが同じ真理を共有している：**\"Agent\" は決して周囲のコードではない。Agent は常にモデルそのものだ。**\n\n### Agent ではないもの\n\n\"Agent\" という言葉は、プロンプト配管工の産業全体に乗っ取られてしまった。\n\nドラッグ＆ドロップのワークフロービルダー。ノーコード \"AI Agent\" プラットフォーム。プロンプトチェーン・オーケストレーションライブラリ。すべて同じ幻想を共有している：LLM API 呼び出しを if-else 分岐、ノードグラフ、ハードコードされたルーティングロジックで繋ぎ合わせることが \"Agent の構築\" だと。\n\n違う。彼らが作ったものはルーブ・ゴールドバーグ・マシンだ -- 過剰に設計された脆い手続き的ルールのパイプライン。LLM は美化されたテキスト補完ノードとして押し込まれているだけ。それは Agent ではない。壮大な妄想を持つシェルスクリプトだ。\n\n**プロンプト配管工式 \"Agent\" は、モデルを訓練しないプログラマーの妄想だ。** 手続き的ロジックを積み重ねて知能を力技で再現しようとする -- 巨大なルールツリー、ノードグラフ、チェーン・プロンプトの滝 -- そして十分なグルーコードがいつか自律的振る舞いを創発すると祈る。しない。工学的手段で Agency をコーディングすることはできない。Agency は学習されるものであって、プログラムされるものではない。\n\nあのシステムたちは生まれた瞬間から死んでいる：脆弱で、スケールせず、汎化が根本的に不可能。GOFAI（Good Old-Fashioned AI、古典的記号 AI）の現代版だ -- 何十年も前に学術界が放棄した記号ルールシステムが、LLM のペンキを塗り直して再登場した。パッケージが違うだけで、同じ袋小路。\n\n### マインドシフト：「Agent を開発する」から Harness を開発する へ\n\n「Agent を開発しています」と言うとき、意味できるのは二つだけだ：\n\n**1. モデルを訓練する。** 強化学習、ファインチューニング、RLHF、その他の勾配ベースの手法で重みを調整する。タスクプロセスデータ -- 実ドメインにおける知覚・推論・行動の実際の系列 -- を収集し、モデルの振る舞いを形成する。DeepMind、OpenAI、Tencent AI Lab、Anthropic が行っていること。これが最も本来的な Agent 開発。\n\n**2. Harness を構築する。** モデルに動作環境を提供するコードを書く。私たちの大半が行っていることであり、このリポジトリの核心。\n\nHarness とは、Agent が特定のドメインで機能するために必要なすべて：\n\n```\nHarness = Tools + Knowledge + Observation + Action Interfaces + Permissions\n\n    Tools:          ファイル I/O、シェル、ネットワーク、データベース、ブラウザ\n    Knowledge:      製品ドキュメント、ドメイン資料、API 仕様、スタイルガイド\n    Observation:    git diff、エラーログ、ブラウザ状態、センサーデータ\n    Action:         CLI コマンド、API 呼び出し、UI インタラクション\n    Permissions:    サンドボックス、承認ワークフロー、信頼境界\n```\n\nモデルが決断する。Harness が実行する。モデルが推論する。Harness がコンテキストを提供する。モデルはドライバー。Harness は車両。\n\n**コーディング Agent の Harness は IDE、ターミナル、ファイルシステム。** 農業 Agent の Harness はセンサーアレイ、灌漑制御、気象データフィード。ホテル Agent の Harness は予約システム、ゲストコミュニケーションチャネル、施設管理 API。Agent -- 知性、意思決定者 -- は常にモデル。Harness はドメインごとに変わる。Agent はドメインを超えて汎化する。\n\nこのリポジトリは車両の作り方を教える。コーディング用の車両だ。だが設計パターンはあらゆるドメインに汎化する：農場管理、ホテル運営、工場製造、物流、医療、教育、科学研究。タスクが知覚され、推論され、実行される必要がある場所ならどこでも -- Agent には Harness が要る。\n\n### Harness エンジニアの仕事\n\nこのリポジトリを読んでいるなら、あなたはおそらく Harness エンジニアだ -- それは強力なアイデンティティ。以下があなたの本当の仕事：\n\n- **ツールの実装。** Agent に手を与える。ファイル読み書き、シェル実行、API 呼び出し、ブラウザ制御、データベースクエリ。各ツールは Agent が環境内で取れる行動。原子的で、組み合わせ可能で、記述が明確であるように設計する。\n\n- **知識のキュレーション。** Agent にドメイン専門性を与える。製品ドキュメント、アーキテクチャ決定記録、スタイルガイド、規制要件。オンデマンドで読み込み（s05）、前もって詰め込まない。Agent は何が利用可能か知った上で、必要なものを自ら取得すべき。\n\n- **コンテキストの管理。** Agent にクリーンな記憶を与える。サブ Agent 隔離（s04）がノイズの漏洩を防ぐ。コンテキスト圧縮（s06）が履歴の氾濫を防ぐ。タスクシステム（s07）が目標を単一の会話を超えて永続化する。\n\n- **権限の制御。** Agent に境界を与える。ファイルアクセスのサンドボックス化。破壊的操作への承認要求。Agent と外部システム間の信頼境界の実施。安全工学と Harness 工学の交差点。\n\n- **タスクプロセスデータの収集。** Agent があなたの Harness 内で実行するすべての行動系列は訓練シグナル。実デプロイメントの知覚-推論-行動トレースは、次世代 Agent モデルをファインチューニングする原材料。あなたの Harness は Agent に仕えるだけでなく -- Agent を進化させる助けにもなる。\n\nあなたは知性を書いているのではない。知性が住まう世界を構築している。その世界の品質 -- Agent がどれだけ明瞭に知覚でき、どれだけ正確に行動でき、利用可能な知識がどれだけ豊かか -- が、知性がどれだけ効果的に自らを表現できるかを直接決定する。\n\n**優れた Harness を作れ。Agent が残りをやる。**\n\n### なぜ Claude Code か -- Harness Engineering の大師範\n\nなぜこのリポジトリは特に Claude Code を解剖するのか？\n\nClaude Code は私たちが見てきた中で最もエレガントで完成度の高い Agent Harness だからだ。単一の巧妙なトリックのためではなく、それが *しないこと* のために：Agent そのものになろうとしない。硬直的なワークフローを押し付けない。精緻な決定木でモデルを二度推しない。ツール、知識、コンテキスト管理、権限境界をモデルに提供し -- そして道を譲る。\n\nClaude Code の本質を剥き出しにすると：\n\n```\nClaude Code = 一つの agent loop\n            + ツール (bash, read, write, edit, glob, grep, browser...)\n            + オンデマンド skill ロード\n            + コンテキスト圧縮\n            + サブ Agent スポーン\n            + 依存グラフ付きタスクシステム\n            + 非同期メールボックスによるチーム協調\n            + worktree 分離による並列実行\n            + 権限ガバナンス\n```\n\nこれがすべてだ。これが全アーキテクチャ。すべてのコンポーネントは Harness メカニズム -- Agent が住む世界の一部。Agent そのものは？ Claude だ。モデル。Anthropic が人類の推論とコードの全幅で訓練した。Harness が Claude を賢くしたのではない。Claude は元々賢い。Harness が Claude に手と目とワークスペースを与えた。\n\nこれが Claude Code が理想的な教材である理由だ：**モデルを信頼し、工学的努力を Harness に集中させるとどうなるかを示している。** このリポジトリの各セッション（s01-s12）は Claude Code アーキテクチャから一つの Harness メカニズムをリバースエンジニアリングする。終了時には、Claude Code の仕組みだけでなく、あらゆるドメインのあらゆる Agent に適用される Harness 工学の普遍的原則を理解している。\n\n教訓は「Claude Code をコピーせよ」ではない。教訓は：**最高の Agent プロダクトは、自分の仕事が Harness であって Intelligence ではないと理解しているエンジニアが作る。**\n\n---\n\n## ビジョン：宇宙を本物の Agent で満たす\n\nこれはコーディング Agent だけの話ではない。\n\n人間が複雑で多段階の判断集約的な仕事をしているすべてのドメインは、Agent が稼働できるドメインだ -- 正しい Harness さえあれば。このリポジトリのパターンは普遍的だ：\n\n```\n不動産管理 Agent  = モデル + 物件センサー + メンテナンスツール + テナント通信\n農業 Agent        = モデル + 土壌/気象データ + 灌漑制御 + 作物知識\nホテル運営 Agent  = モデル + 予約システム + ゲストチャネル + 施設 API\n医学研究 Agent    = モデル + 文献検索 + 実験機器 + プロトコル文書\n製造 Agent        = モデル + 生産ラインセンサー + 品質管理 + 物流\n教育 Agent        = モデル + カリキュラム知識 + 学生進捗 + 評価ツール\n```\n\nループは常に同じ。ツールが変わる。知識が変わる。権限が変わる。Agent -- モデル -- がすべてを汎化する。\n\nこのリポジトリを読むすべての Harness エンジニアは、ソフトウェアエンジニアリングを遥かに超えたパターンを学んでいる。知的で自動化された未来のためのインフラストラクチャを構築することを学んでいる。実ドメインにデプロイされた優れた Harness の一つ一つが、Agent が知覚し、推論し、行動できる新たな拠点。\n\nまずワークショップを満たす。次に農場、病院、工場。次に都市。次に惑星。\n\n**Bash is all you need. Real agents are all the universe needs.**\n\n---\n\n```\n                    THE AGENT PATTERN\n                    =================\n\n    User --> messages[] --> LLM --> response\n                                      |\n                            stop_reason == \"tool_use\"?\n                           /                          \\\n                         yes                           no\n                          |                             |\n                    execute tools                    return text\n                    append results\n                    loop back -----------------> messages[]\n\n\n    最小ループ。すべての AI Agent にこのループが必要だ。\n    モデルがツール呼び出しと停止を決める。\n    コードはモデルの要求を実行するだけ。\n    このリポジトリはこのループを囲むすべて --\n    Agent を特定ドメインで効果的にする Harness -- の作り方を教える。\n```\n\n**12 の段階的セッション、シンプルなループから分離された自律実行まで。**\n**各セッションは 1 つの Harness メカニズムを追加する。各メカニズムには 1 つのモットーがある。**\n\n> **s01** &nbsp; *\"One loop & Bash is all you need\"* &mdash; 1つのツール + 1つのループ = エージェント\n>\n> **s02** &nbsp; *\"ツールを足すなら、ハンドラーを1つ足すだけ\"* &mdash; ループは変わらない。新ツールは dispatch map に登録するだけ\n>\n> **s03** &nbsp; *\"計画のないエージェントは行き当たりばったり\"* &mdash; まずステップを書き出し、それから実行\n>\n> **s04** &nbsp; *\"大きなタスクを分割し、各サブタスクにクリーンなコンテキストを\"* &mdash; サブエージェントは独立した messages[] を使い、メイン会話を汚さない\n>\n> **s05** &nbsp; *\"必要な知識を、必要な時に読み込む\"* &mdash; system prompt ではなく tool_result で注入\n>\n> **s06** &nbsp; *\"コンテキストはいつか溢れる、空ける手段が要る\"* &mdash; 3層圧縮で無限セッションを実現\n>\n> **s07** &nbsp; *\"大きな目標を小タスクに分解し、順序付けし、ディスクに記録する\"* &mdash; ファイルベースのタスクグラフ、マルチエージェント協調の基盤\n>\n> **s08** &nbsp; *\"遅い操作はバックグラウンドへ、エージェントは次を考え続ける\"* &mdash; デーモンスレッドがコマンド実行、完了後に通知を注入\n>\n> **s09** &nbsp; *\"一人で終わらないなら、チームメイトに任せる\"* &mdash; 永続チームメイト + 非同期メールボックス\n>\n> **s10** &nbsp; *\"チームメイト間には統一の通信ルールが必要\"* &mdash; 1つの request-response パターンが全交渉を駆動\n>\n> **s11** &nbsp; *\"チームメイトが自らボードを見て、仕事を取る\"* &mdash; リーダーが逐一割り振る必要はない\n>\n> **s12** &nbsp; *\"各自のディレクトリで作業し、互いに干渉しない\"* &mdash; タスクは目標を管理、worktree はディレクトリを管理、IDで紐付け\n\n---\n\n## コアパターン\n\n```python\ndef agent_loop(messages):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM,\n            messages=messages, tools=TOOLS,\n        )\n        messages.append({\"role\": \"assistant\",\n                         \"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                output = TOOL_HANDLERS[block.name](**block.input)\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output,\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n```\n\n各セッションはこのループの上に 1 つの Harness メカニズムを重ねる -- ループ自体は変わらない。ループは Agent のもの。メカニズムは Harness のもの。\n\n## スコープ (重要)\n\nこのリポジトリは Harness 工学の 0->1 学習プロジェクト -- Agent モデルを囲む環境の構築を学ぶ。\n学習を優先するため、以下の本番メカニズムは意図的に簡略化または省略している：\n\n- 完全なイベント / Hook バス (例: PreToolUse, SessionStart/End, ConfigChange)。\n  s12 では教材用に最小の追記型ライフサイクルイベントのみ実装。\n- ルールベースの権限ガバナンスと信頼フロー\n- セッションライフサイクル制御 (resume/fork) と高度な worktree ライフサイクル制御\n- MCP ランタイムの詳細 (transport/OAuth/リソース購読/ポーリング)\n\nこのリポジトリの JSONL メールボックス方式は教材用の実装であり、特定の本番内部実装を主張するものではない。\n\n## クイックスタート\n\n```sh\ngit clone https://github.com/shareAI-lab/learn-claude-code\ncd learn-claude-code\npip install -r requirements.txt\ncp .env.example .env   # .env を編集して ANTHROPIC_API_KEY を入力\n\npython agents/s01_agent_loop.py       # ここから開始\npython agents/s12_worktree_task_isolation.py  # 全セッションの到達点\npython agents/s_full.py               # 総括: 全メカニズム統合\n```\n\n### Web プラットフォーム\n\nインタラクティブな可視化、ステップスルーアニメーション、ソースビューア、各セッションのドキュメント。\n\n```sh\ncd web && npm install && npm run dev   # http://localhost:3000\n```\n\n## 学習パス\n\n```\nフェーズ1: ループ                     フェーズ2: 計画と知識\n==================                   ==============================\ns01  エージェントループ      [1]     s03  TodoWrite               [5]\n     while + stop_reason                  TodoManager + nag リマインダー\n     |                                    |\n     +-> s02  Tool Use            [4]     s04  サブエージェント      [5]\n              dispatch map: name->handler     子ごとに新しい messages[]\n                                              |\n                                         s05  Skills               [5]\n                                              SKILL.md を tool_result で注入\n                                              |\n                                         s06  Context Compact      [5]\n                                              3層コンテキスト圧縮\n\nフェーズ3: 永続化                     フェーズ4: チーム\n==================                   =====================\ns07  タスクシステム           [8]     s09  エージェントチーム      [9]\n     ファイルベース CRUD + 依存グラフ      チームメイト + JSONL メールボックス\n     |                                    |\ns08  バックグラウンドタスク   [6]     s10  チームプロトコル        [12]\n     デーモンスレッド + 通知キュー         シャットダウン + プラン承認 FSM\n                                          |\n                                     s11  自律エージェント        [14]\n                                          アイドルサイクル + 自動クレーム\n                                     |\n                                     s12  Worktree 分離           [16]\n                                          タスク調整 + 必要時の分離実行レーン\n\n                                     [N] = ツール数\n```\n\n## プロジェクト構成\n\n```\nlearn-claude-code/\n|\n|-- agents/                        # Python リファレンス実装 (s01-s12 + s_full 総括)\n|-- docs/{en,zh,ja}/               # メンタルモデル優先のドキュメント (3言語)\n|-- web/                           # インタラクティブ学習プラットフォーム (Next.js)\n|-- skills/                        # s05 の Skill ファイル\n+-- .github/workflows/ci.yml      # CI: 型チェック + ビルド\n```\n\n## ドキュメント\n\nメンタルモデル優先: 問題、解決策、ASCII図、最小限のコード。\n[English](./docs/en/) | [中文](./docs/zh/) | [日本語](./docs/ja/)\n\n| セッション | トピック | モットー |\n|-----------|---------|---------|\n| [s01](./docs/ja/s01-the-agent-loop.md) | エージェントループ | *One loop & Bash is all you need* |\n| [s02](./docs/ja/s02-tool-use.md) | Tool Use | *ツールを足すなら、ハンドラーを1つ足すだけ* |\n| [s03](./docs/ja/s03-todo-write.md) | TodoWrite | *計画のないエージェントは行き当たりばったり* |\n| [s04](./docs/ja/s04-subagent.md) | サブエージェント | *大きなタスクを分割し、各サブタスクにクリーンなコンテキストを* |\n| [s05](./docs/ja/s05-skill-loading.md) | Skills | *必要な知識を、必要な時に読み込む* |\n| [s06](./docs/ja/s06-context-compact.md) | Context Compact | *コンテキストはいつか溢れる、空ける手段が要る* |\n| [s07](./docs/ja/s07-task-system.md) | タスクシステム | *大きな目標を小タスクに分解し、順序付けし、ディスクに記録する* |\n| [s08](./docs/ja/s08-background-tasks.md) | バックグラウンドタスク | *遅い操作はバックグラウンドへ、エージェントは次を考え続ける* |\n| [s09](./docs/ja/s09-agent-teams.md) | エージェントチーム | *一人で終わらないなら、チームメイトに任せる* |\n| [s10](./docs/ja/s10-team-protocols.md) | チームプロトコル | *チームメイト間には統一の通信ルールが必要* |\n| [s11](./docs/ja/s11-autonomous-agents.md) | 自律エージェント | *チームメイトが自らボードを見て、仕事を取る* |\n| [s12](./docs/ja/s12-worktree-task-isolation.md) | Worktree + タスク分離 | *各自のディレクトリで作業し、互いに干渉しない* |\n\n## 次のステップ -- 理解から出荷へ\n\n12 セッションを終えれば、Harness 工学の内部構造を完全に理解している。その知識を活かす 2 つの方法:\n\n### Kode Agent CLI -- オープンソース Coding Agent CLI\n\n> `npm i -g @shareai-lab/kode`\n\nSkill & LSP 対応、Windows 対応、GLM / MiniMax / DeepSeek 等のオープンモデルに接続可能。インストールしてすぐ使える。\n\nGitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)**\n\n### Kode Agent SDK -- アプリにエージェント機能を埋め込む\n\n公式 Claude Code Agent SDK は内部で完全な CLI プロセスと通信する -- 同時ユーザーごとに独立のターミナルプロセスが必要。Kode SDK は独立ライブラリでユーザーごとのプロセスオーバーヘッドがなく、バックエンド、ブラウザ拡張、組み込みデバイス等に埋め込み可能。\n\nGitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)**\n\n---\n\n## 姉妹教材: *オンデマンドセッション*から*常時稼働アシスタント*へ\n\n本リポジトリが教える Harness は **使い捨て型** -- ターミナルを開き、Agent にタスクを与え、終わったら閉じる。次のセッションは白紙から始まる。Claude Code のモデル。\n\n[OpenClaw](https://github.com/openclaw/openclaw) は別の可能性を証明した: 同じ agent core の上に 2 つの Harness メカニズムを追加するだけで、Agent は「突かないと動かない」から「30 秒ごとに自分で起きて仕事を探す」に変わる:\n\n- **ハートビート** -- 30 秒ごとに Harness が Agent にメッセージを送り、やることがあるか確認させる。なければスリープ続行、あれば即座に行動。\n- **Cron** -- Agent が自ら未来のタスクをスケジュールし、時間が来たら自動実行。\n\nさらにマルチチャネル IM ルーティング (WhatsApp / Telegram / Slack / Discord 等 13+ プラットフォーム)、永続コンテキストメモリ、Soul パーソナリティシステムを加えると、Agent は使い捨てツールから常時稼働のパーソナル AI アシスタントへ変貌する。\n\n**[claw0](https://github.com/shareAI-lab/claw0)** はこれらの Harness メカニズムをゼロから分解する姉妹教材リポジトリ:\n\n```\nclaw agent = agent core + heartbeat + cron + IM chat + memory + soul\n```\n\n```\nlearn-claude-code                   claw0\n(agent harness コア:                 (能動的な常時稼働 harness:\n ループ、ツール、計画、                ハートビート、cron、IM チャネル、\n チーム、worktree 分離)                メモリ、Soul パーソナリティ)\n```\n\n## ライセンス\n\nMIT\n\n---\n\n**モデルが Agent だ。コードは Harness だ。優れた Harness を作れ。Agent が残りをやる。**\n\n**Bash is all you need. Real agents are all the universe needs.**\n"
  },
  {
    "path": "README-zh.md",
    "content": "# Learn Claude Code -- 真正的 Agent Harness 工程\n\n[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md)\n\n## 模型就是 Agent\n\n在讨论代码之前，先把一件事彻底说清楚。\n\n**Agent 是模型。不是框架。不是提示词链。不是拖拽式工作流。**\n\n### Agent 到底是什么\n\nAgent 是一个神经网络 -- Transformer、RNN、一个被训练出来的函数 -- 经过数十亿次梯度更新，在行动序列数据上学会了感知环境、推理目标、采取行动。\"Agent\" 这个词在 AI 领域从诞生之日起就是这个意思。从来都是。\n\n人类就是 agent。一个由数百万年进化训练出来的生物神经网络，通过感官感知世界，通过大脑推理，通过身体行动。当 DeepMind、OpenAI 或 Anthropic 说 \"agent\" 时，他们说的和这个领域自诞生以来就一直在说的完全一样：**一个学会了行动的模型。**\n\n历史已经写好了铁证：\n\n- **2013 -- DeepMind DQN 玩 Atari。** 一个神经网络，只接收原始像素和游戏分数，学会了 7 款 Atari 2600 游戏 -- 超越所有先前算法，在其中 3 款上击败人类专家。到 2015 年，同一架构扩展到 [49 款游戏，达到职业人类测试员水平](https://www.nature.com/articles/nature14236)，论文发表在 *Nature*。没有游戏专属规则。没有决策树。一个模型，从经验中学习。那个模型就是 agent。\n\n- **2019 -- OpenAI Five 征服 Dota 2。** 五个神经网络，在 10 个月内与自己对战了 [45,000 年的 Dota 2](https://openai.com/index/openai-five-defeats-dota-2-world-champions/)，在旧金山直播赛上 2-0 击败了 **OG** -- TI8 世界冠军。随后的公开竞技场中，AI 在 42,729 场比赛中胜率 99.4%。没有脚本化的策略。没有元编程的团队协调逻辑。模型完全通过自我对弈学会了团队协作、战术和实时适应。\n\n- **2019 -- DeepMind AlphaStar 制霸星际争霸 II。** AlphaStar 在闭门赛中 [10-1 击败职业选手](https://deepmind.google/blog/alphastar-mastering-the-real-time-strategy-game-starcraft-ii/)，随后在欧洲服务器上达到[宗师段位](https://www.nature.com/articles/d41586-019-03298-6) -- 90,000 名玩家中的前 0.15%。一个信息不完全、实时决策、组合动作空间远超国际象棋和围棋的游戏。Agent 是什么？是模型。训练出来的。不是编出来的。\n\n- **2019 -- 腾讯绝悟统治王者荣耀。** 腾讯 AI Lab 的 \"绝悟\" 于 2019 年 8 月 2 日世冠杯半决赛上[以 5v5 击败 KPL 职业选手](https://www.jiemian.com/article/3371171.html)。在 1v1 模式下，职业选手 [15 场只赢 1 场，最多坚持不到 8 分钟](https://developer.aliyun.com/article/851058)。训练强度：一天等于人类 440 年。到 2021 年，绝悟在全英雄池 BO5 上全面超越 KPL 职业选手水准。没有手工编写的英雄克制表。没有脚本化的阵容编排。一个从零开始通过自我对弈学习整个游戏的模型。\n\n- **2024-2025 -- LLM Agent 重塑软件工程。** Claude、GPT、Gemini -- 在人类全部代码和推理上训练的大语言模型 -- 被部署为编程 agent。它们阅读代码库，编写实现，调试故障，团队协作。架构与之前每一个 agent 完全相同：一个训练好的模型，放入一个环境，给予感知和行动的工具。唯一的不同是它们学到的东西的规模和解决任务的通用性。\n\n每一个里程碑都共享同一个真理：**\"Agent\" 从来都不是外面那层代码。Agent 永远是模型本身。**\n\n### Agent 不是什么\n\n\"Agent\" 这个词已经被一整个提示词水管工产业劫持了。\n\n拖拽式工作流构建器。无代码 \"AI Agent\" 平台。提示词链编排库。它们共享同一个幻觉：把 LLM API 调用用 if-else 分支、节点图、硬编码路由逻辑串在一起就算是 \"构建 Agent\" 了。\n\n不是的。它们做出来的东西是鲁布·戈德堡机械 -- 一个过度工程化的、脆弱的过程式规则流水线，LLM 被楔在里面当一个美化了的文本补全节点。那不是 Agent。那是一个有着宏大妄想的 shell 脚本。\n\n**提示词水管工式 \"Agent\" 是不做模型的程序员的意淫。** 他们试图通过堆叠过程式逻辑来暴力模拟智能 -- 庞大的规则树、节点图、链式提示词瀑布流 -- 然后祈祷足够多的胶水代码能涌现出自主行为。不会的。你不可能通过工程手段编码出 agency。Agency 是学出来的，不是编出来的。\n\n那些系统从诞生之日起就已经死了：脆弱、不可扩展、根本不具备泛化能力。它们是 GOFAI（Good Old-Fashioned AI，经典符号 AI）的现代还魂 -- 几十年前就被学界抛弃的符号规则系统，现在喷了一层 LLM 的漆又登场了。换了个包装，同一条死路。\n\n### 心智转换：从 \"开发 Agent\" 到开发 Harness\n\n当一个人说 \"我在开发 Agent\" 时，他只可能是两个意思之一：\n\n**1. 训练模型。** 通过强化学习、微调、RLHF 或其他基于梯度的方法调整权重。收集任务过程数据 -- 真实领域中感知、推理、行动的实际序列 -- 用它们来塑造模型的行为。这是 DeepMind、OpenAI、腾讯 AI Lab、Anthropic 在做的事。这是最本义的 Agent 开发。\n\n**2. 构建 Harness。** 编写代码，为模型提供一个可操作的环境。这是我们大多数人在做的事，也是本仓库的核心。\n\nHarness 是 agent 在特定领域工作所需要的一切：\n\n```\nHarness = Tools + Knowledge + Observation + Action Interfaces + Permissions\n\n    Tools:          文件读写、Shell、网络、数据库、浏览器\n    Knowledge:      产品文档、领域资料、API 规范、风格指南\n    Observation:    git diff、错误日志、浏览器状态、传感器数据\n    Action:         CLI 命令、API 调用、UI 交互\n    Permissions:    沙箱隔离、审批流程、信任边界\n```\n\n模型做决策。Harness 执行。模型做推理。Harness 提供上下文。模型是驾驶者。Harness 是载具。\n\n**编程 agent 的 harness 是它的 IDE、终端和文件系统。** 农业 agent 的 harness 是传感器阵列、灌溉控制和气象数据。酒店 agent 的 harness 是预订系统、客户沟通渠道和设施管理 API。Agent -- 那个智能、那个决策者 -- 永远是模型。Harness 因领域而变。Agent 跨领域泛化。\n\n这个仓库教你造载具。编程用的载具。但设计模式可以泛化到任何领域：庄园管理、农田运营、酒店运作、工厂制造、物流调度、医疗保健、教育培训、科学研究。只要有一个任务需要被感知、推理和执行 -- agent 就需要一个 harness。\n\n### Harness 工程师到底在做什么\n\n如果你在读这个仓库，你很可能是一名 harness 工程师 -- 这是一个强大的身份。以下是你真正的工作：\n\n- **实现工具。** 给 agent 一双手。文件读写、Shell 执行、API 调用、浏览器控制、数据库查询。每个工具都是 agent 在环境中可以采取的一个行动。设计它们时要原子化、可组合、描述清晰。\n\n- **策划知识。** 给 agent 领域专长。产品文档、架构决策记录、风格指南、合规要求。按需加载（s05），不要前置塞入。Agent 应该知道有什么可用，然后自己拉取所需。\n\n- **管理上下文。** 给 agent 干净的记忆。子 agent 隔离（s04）防止噪声泄露。上下文压缩（s06）防止历史淹没。任务系统（s07）让目标持久化到单次对话之外。\n\n- **控制权限。** 给 agent 边界。沙箱化文件访问。对破坏性操作要求审批。在 agent 和外部系统之间实施信任边界。这是安全工程与 harness 工程的交汇点。\n\n- **收集任务过程数据。** Agent 在你的 harness 中执行的每一条行动序列都是训练信号。真实部署中的感知-推理-行动轨迹是微调下一代 agent 模型的原材料。你的 harness 不仅服务于 agent -- 它还可以帮助进化 agent。\n\n你不是在编写智能。你是在构建智能栖居的世界。这个世界的质量 -- agent 能看得多清楚、行动得多精准、可用知识有多丰富 -- 直接决定了智能能多有效地表达自己。\n\n**造好 Harness。Agent 会完成剩下的。**\n\n### 为什么是 Claude Code -- Harness 工程的大师课\n\n为什么这个仓库专门拆解 Claude Code？\n\n因为 Claude Code 是我们所见过的最优雅、最完整的 agent harness 实现。不是因为某个巧妙的技巧，而是因为它 *没做* 的事：它没有试图成为 agent 本身。它没有强加僵化的工作流。它没有用精心设计的决策树去替模型做判断。它给模型提供了工具、知识、上下文管理和权限边界 -- 然后让开了。\n\n把 Claude Code 剥到本质来看：\n\n```\nClaude Code = 一个 agent loop\n            + 工具 (bash, read, write, edit, glob, grep, browser...)\n            + 按需 skill 加载\n            + 上下文压缩\n            + 子 agent 派生\n            + 带依赖图的任务系统\n            + 异步邮箱的团队协调\n            + worktree 隔离的并行执行\n            + 权限治理\n```\n\n就这些。这就是全部架构。每一个组件都是 harness 机制 -- 为 agent 构建的栖居世界的一部分。Agent 本身呢？是 Claude。一个模型。由 Anthropic 在人类推理和代码的全部广度上训练而成。Harness 没有让 Claude 变聪明。Claude 本来就聪明。Harness 给了 Claude 双手、双眼和一个工作空间。\n\n这就是 Claude Code 作为教学标本的意义：**它展示了当你信任模型、把工程精力集中在 harness 上时会发生什么。** 本仓库的每一个课程（s01-s12）都在逆向工程 Claude Code 架构中的一个 harness 机制。学完之后，你理解的不只是 Claude Code 怎么工作，而是适用于任何领域、任何 agent 的 harness 工程通用原则。\n\n启示不是 \"复制 Claude Code\"。启示是：**最好的 agent 产品，出自那些明白自己的工作是 harness 而非 intelligence 的工程师之手。**\n\n---\n\n## 愿景：用真正的 Agent 铺满宇宙\n\n这不只关乎编程 agent。\n\n每一个人类从事复杂、多步骤、需要判断力的工作的领域，都是 agent 可以运作的领域 -- 只要有对的 harness。本仓库中的模式是通用的：\n\n```\n庄园管理 agent  = 模型 + 物业传感器 + 维护工具 + 租户通信\n农业 agent      = 模型 + 土壤/气象数据 + 灌溉控制 + 作物知识\n酒店运营 agent  = 模型 + 预订系统 + 客户渠道 + 设施 API\n医学研究 agent  = 模型 + 文献检索 + 实验仪器 + 协议文档\n制造业 agent    = 模型 + 产线传感器 + 质量控制 + 物流系统\n教育 agent      = 模型 + 课程知识 + 学生进度 + 评估工具\n```\n\n循环永远不变。工具在变。知识在变。权限在变。Agent -- 那个模型 -- 泛化一切。\n\n每一个读这个仓库的 harness 工程师都在学习远超软件工程的模式。你在学习为一个智能的、自动化的未来构建基础设施。每一个部署在真实领域的好 harness，都是 agent 能够感知、推理、行动的又一个阵地。\n\n先铺满工作室。然后是农田、医院、工厂。然后是城市。然后是星球。\n\n**Bash is all you need. Real agents are all the universe needs.**\n\n---\n\n```\n                    THE AGENT PATTERN\n                    =================\n\n    User --> messages[] --> LLM --> response\n                                      |\n                            stop_reason == \"tool_use\"?\n                           /                          \\\n                         yes                           no\n                          |                             |\n                    execute tools                    return text\n                    append results\n                    loop back -----------------> messages[]\n\n\n    这是最小循环。每个 AI Agent 都需要这个循环。\n    模型决定何时调用工具、何时停止。\n    代码只是执行模型的要求。\n    本仓库教你构建围绕这个循环的一切 --\n    让 agent 在特定领域高效工作的 harness。\n```\n\n**12 个递进式课程, 从简单循环到隔离化的自治执行。**\n**每个课程添加一个 harness 机制。每个机制有一句格言。**\n\n> **s01** &nbsp; *\"One loop & Bash is all you need\"* &mdash; 一个工具 + 一个循环 = 一个智能体\n>\n> **s02** &nbsp; *\"加一个工具, 只加一个 handler\"* &mdash; 循环不用动, 新工具注册进 dispatch map 就行\n>\n> **s03** &nbsp; *\"没有计划的 agent 走哪算哪\"* &mdash; 先列步骤再动手, 完成率翻倍\n>\n> **s04** &nbsp; *\"大任务拆小, 每个小任务干净的上下文\"* &mdash; 子智能体用独立 messages[], 不污染主对话\n>\n> **s05** &nbsp; *\"用到什么知识, 临时加载什么知识\"* &mdash; 通过 tool_result 注入, 不塞 system prompt\n>\n> **s06** &nbsp; *\"上下文总会满, 要有办法腾地方\"* &mdash; 三层压缩策略, 换来无限会话\n>\n> **s07** &nbsp; *\"大目标要拆成小任务, 排好序, 记在磁盘上\"* &mdash; 文件持久化的任务图, 为多 agent 协作打基础\n>\n> **s08** &nbsp; *\"慢操作丢后台, agent 继续想下一步\"* &mdash; 后台线程跑命令, 完成后注入通知\n>\n> **s09** &nbsp; *\"任务太大一个人干不完, 要能分给队友\"* &mdash; 持久化队友 + 异步邮箱\n>\n> **s10** &nbsp; *\"队友之间要有统一的沟通规矩\"* &mdash; 一个 request-response 模式驱动所有协商\n>\n> **s11** &nbsp; *\"队友自己看看板, 有活就认领\"* &mdash; 不需要领导逐个分配, 自组织\n>\n> **s12** &nbsp; *\"各干各的目录, 互不干扰\"* &mdash; 任务管目标, worktree 管目录, 按 ID 绑定\n\n---\n\n## 核心模式\n\n```python\ndef agent_loop(messages):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM,\n            messages=messages, tools=TOOLS,\n        )\n        messages.append({\"role\": \"assistant\",\n                         \"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                output = TOOL_HANDLERS[block.name](**block.input)\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output,\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n```\n\n每个课程在这个循环之上叠加一个 harness 机制 -- 循环本身始终不变。循环属于 agent。机制属于 harness。\n\n## 范围说明 (重要)\n\n本仓库是一个 0->1 的 harness 工程学习项目 -- 构建围绕 agent 模型的工作环境。\n为保证学习路径清晰，仓库有意简化或省略了部分生产机制：\n\n- 完整事件 / Hook 总线 (例如 PreToolUse、SessionStart/End、ConfigChange)。\n  s12 仅提供教学用途的最小 append-only 生命周期事件流。\n- 基于规则的权限治理与信任流程\n- 会话生命周期控制 (resume/fork) 与更完整的 worktree 生命周期控制\n- 完整 MCP 运行时细节 (transport/OAuth/资源订阅/轮询)\n\n仓库中的团队 JSONL 邮箱协议是教学实现，不是对任何特定生产内部实现的声明。\n\n## 快速开始\n\n```sh\ngit clone https://github.com/shareAI-lab/learn-claude-code\ncd learn-claude-code\npip install -r requirements.txt\ncp .env.example .env   # 编辑 .env 填入你的 ANTHROPIC_API_KEY\n\npython agents/s01_agent_loop.py       # 从这里开始\npython agents/s12_worktree_task_isolation.py  # 完整递进终点\npython agents/s_full.py               # 总纲: 全部机制合一\n```\n\n### Web 平台\n\n交互式可视化、分步动画、源码查看器, 以及每个课程的文档。\n\n```sh\ncd web && npm install && npm run dev   # http://localhost:3000\n```\n\n## 学习路径\n\n```\n第一阶段: 循环                       第二阶段: 规划与知识\n==================                   ==============================\ns01  Agent 循环              [1]     s03  TodoWrite               [5]\n     while + stop_reason                  TodoManager + nag 提醒\n     |                                    |\n     +-> s02  Tool Use            [4]     s04  子智能体             [5]\n              dispatch map: name->handler     每个子智能体独立 messages[]\n                                              |\n                                         s05  Skills               [5]\n                                              SKILL.md 通过 tool_result 注入\n                                              |\n                                         s06  Context Compact      [5]\n                                              三层上下文压缩\n\n第三阶段: 持久化                     第四阶段: 团队\n==================                   =====================\ns07  任务系统                [8]     s09  智能体团队             [9]\n     文件持久化 CRUD + 依赖图             队友 + JSONL 邮箱\n     |                                    |\ns08  后台任务                [6]     s10  团队协议               [12]\n     守护线程 + 通知队列                  关机 + 计划审批 FSM\n                                          |\n                                     s11  自治智能体             [14]\n                                          空闲轮询 + 自动认领\n                                     |\n                                     s12  Worktree 隔离          [16]\n                                          任务协调 + 按需隔离执行通道\n\n                                     [N] = 工具数量\n```\n\n## 项目结构\n\n```\nlearn-claude-code/\n|\n|-- agents/                        # Python 参考实现 (s01-s12 + s_full 总纲)\n|-- docs/{en,zh,ja}/               # 心智模型优先的文档 (3 种语言)\n|-- web/                           # 交互式学习平台 (Next.js)\n|-- skills/                        # s05 的 Skill 文件\n+-- .github/workflows/ci.yml      # CI: 类型检查 + 构建\n```\n\n## 文档\n\n心智模型优先: 问题、方案、ASCII 图、最小化代码。\n[English](./docs/en/) | [中文](./docs/zh/) | [日本語](./docs/ja/)\n\n| 课程 | 主题 | 格言 |\n|------|------|------|\n| [s01](./docs/zh/s01-the-agent-loop.md) | Agent 循环 | *One loop & Bash is all you need* |\n| [s02](./docs/zh/s02-tool-use.md) | Tool Use | *加一个工具, 只加一个 handler* |\n| [s03](./docs/zh/s03-todo-write.md) | TodoWrite | *没有计划的 agent 走哪算哪* |\n| [s04](./docs/zh/s04-subagent.md) | 子智能体 | *大任务拆小, 每个小任务干净的上下文* |\n| [s05](./docs/zh/s05-skill-loading.md) | Skills | *用到什么知识, 临时加载什么知识* |\n| [s06](./docs/zh/s06-context-compact.md) | Context Compact | *上下文总会满, 要有办法腾地方* |\n| [s07](./docs/zh/s07-task-system.md) | 任务系统 | *大目标要拆成小任务, 排好序, 记在磁盘上* |\n| [s08](./docs/zh/s08-background-tasks.md) | 后台任务 | *慢操作丢后台, agent 继续想下一步* |\n| [s09](./docs/zh/s09-agent-teams.md) | 智能体团队 | *任务太大一个人干不完, 要能分给队友* |\n| [s10](./docs/zh/s10-team-protocols.md) | 团队协议 | *队友之间要有统一的沟通规矩* |\n| [s11](./docs/zh/s11-autonomous-agents.md) | 自治智能体 | *队友自己看看板, 有活就认领* |\n| [s12](./docs/zh/s12-worktree-task-isolation.md) | Worktree + 任务隔离 | *各干各的目录, 互不干扰* |\n\n## 学完之后 -- 从理解到落地\n\n12 个课程走完, 你已经从内到外理解了 harness 工程的运作原理。两种方式把知识变成产品:\n\n### Kode Agent CLI -- 开源 Coding Agent CLI\n\n> `npm i -g @shareai-lab/kode`\n\n支持 Skill & LSP, 适配 Windows, 可接 GLM / MiniMax / DeepSeek 等开放模型。装完即用。\n\nGitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)**\n\n### Kode Agent SDK -- 把 Agent 能力嵌入你的应用\n\n官方 Claude Code Agent SDK 底层与完整 CLI 进程通信 -- 每个并发用户 = 一个终端进程。Kode SDK 是独立库, 无 per-user 进程开销, 可嵌入后端、浏览器插件、嵌入式设备等任意运行时。\n\nGitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)**\n\n---\n\n## 姊妹教程: 从*被动临时会话*到*主动常驻助手*\n\n本仓库教的 harness 属于 **用完即走** 型 -- 开终端、给 agent 任务、做完关掉, 下次重开是全新会话。Claude Code 就是这种模式。\n\n但 [OpenClaw](https://github.com/openclaw/openclaw) 证明了另一种可能: 在同样的 agent core 之上, 加两个 harness 机制就能让 agent 从 \"踹一下动一下\" 变成 \"自己隔 30 秒醒一次找活干\":\n\n- **心跳 (Heartbeat)** -- 每 30 秒 harness 给 agent 发一条消息, 让它检查有没有事可做。没事就继续睡, 有事立刻行动。\n- **定时任务 (Cron)** -- agent 可以给自己安排未来要做的事, 到点自动执行。\n\n再加上 IM 多通道路由 (WhatsApp/Telegram/Slack/Discord 等 13+ 平台)、不清空的上下文记忆、Soul 人格系统, agent 就从一个临时工具变成了始终在线的个人 AI 助手。\n\n**[claw0](https://github.com/shareAI-lab/claw0)** 是我们的姊妹教学仓库, 从零拆解这些 harness 机制:\n\n```\nclaw agent = agent core + heartbeat + cron + IM chat + memory + soul\n```\n\n```\nlearn-claude-code                   claw0\n(agent harness 内核:                 (主动式常驻 harness:\n 循环、工具、规划、                    心跳、定时任务、IM 通道、\n 团队、worktree 隔离)                  记忆、Soul 人格)\n```\n\n## 许可证\n\nMIT\n\n---\n\n**模型就是 Agent。代码是 Harness。造好 Harness，Agent 会完成剩下的。**\n\n**Bash is all you need. Real agents are all the universe needs.**\n"
  },
  {
    "path": "README.md",
    "content": "[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md)\n# Learn Claude Code -- Harness Engineering for Real Agents\n\n## The Model IS the Agent\n\nBefore we talk about code, let's get one thing absolutely straight.\n\n**An agent is a model. Not a framework. Not a prompt chain. Not a drag-and-drop workflow.**\n\n### What an Agent IS\n\nAn agent is a neural network -- a Transformer, an RNN, a learned function -- that has been trained, through billions of gradient updates on action-sequence data, to perceive an environment, reason about goals, and take actions to achieve them. The word \"agent\" in AI has always meant this. Always.\n\nA human is an agent. A biological neural network, shaped by millions of years of evolutionary training, perceiving the world through senses, reasoning through a brain, acting through a body. When DeepMind, OpenAI, or Anthropic say \"agent,\" they mean the same thing the field has meant since its inception: **a model that has learned to act.**\n\nThe proof is written in history:\n\n- **2013 -- DeepMind DQN plays Atari.** A single neural network, receiving only raw pixels and game scores, learned to play 7 Atari 2600 games -- surpassing all prior algorithms and beating human experts on 3 of them. By 2015, the same architecture scaled to [49 games and matched professional human testers](https://www.nature.com/articles/nature14236), published in *Nature*. No game-specific rules. No decision trees. One model, learning from experience. That model was the agent.\n\n- **2019 -- OpenAI Five conquers Dota 2.** Five neural networks, having played [45,000 years of Dota 2](https://openai.com/index/openai-five-defeats-dota-2-world-champions/) against themselves in 10 months, defeated **OG** -- the reigning TI8 world champions -- 2-0 on a San Francisco livestream. In a subsequent public arena, the AI won 99.4% of 42,729 games against all comers. No scripted strategies. No meta-programmed team coordination. The models learned teamwork, tactics, and real-time adaptation entirely through self-play.\n\n- **2019 -- DeepMind AlphaStar masters StarCraft II.** AlphaStar [beat professional players 10-1](https://deepmind.google/blog/alphastar-mastering-the-real-time-strategy-game-starcraft-ii/) in a closed-door match, and later achieved [Grandmaster status](https://www.nature.com/articles/d41586-019-03298-6) on European servers -- top 0.15% of 90,000 players. A game with imperfect information, real-time decisions, and a combinatorial action space that dwarfs chess and Go. The agent? A model. Trained. Not scripted.\n\n- **2019 -- Tencent Jueyu dominates Honor of Kings.** Tencent AI Lab's \"Jueyu\" [defeated KPL professional players](https://www.jiemian.com/article/3371171.html) in a full 5v5 match at the World Champion Cup. In 1v1 mode, pros won only [1 out of 15 games and never survived past 8 minutes](https://developer.aliyun.com/article/851058). Training intensity: one day equaled 440 human years. By 2021, Jueyu surpassed KPL pros across the full hero pool. No handcrafted matchup tables. No scripted compositions. A model that learned the entire game from scratch through self-play.\n\n- **2024-2025 -- LLM agents reshape software engineering.** Claude, GPT, Gemini -- large language models trained on the entirety of human code and reasoning -- are deployed as coding agents. They read codebases, write implementations, debug failures, coordinate in teams. The architecture is identical to every agent before them: a trained model, placed in an environment, given tools to perceive and act. The only difference is the scale of what they've learned and the generality of the tasks they solve.\n\nEvery one of these milestones shares the same truth: **the \"agent\" is never the surrounding code. The agent is always the model.**\n\n### What an Agent Is NOT\n\nThe word \"agent\" has been hijacked by an entire cottage industry of prompt plumbing.\n\nDrag-and-drop workflow builders. No-code \"AI agent\" platforms. Prompt-chain orchestration libraries. They all share the same delusion: that wiring together LLM API calls with if-else branches, node graphs, and hardcoded routing logic constitutes \"building an agent.\"\n\nIt doesn't. What they build is a Rube Goldberg machine -- an over-engineered, brittle pipeline of procedural rules, with an LLM wedged in as a glorified text-completion node. That is not an agent. That is a shell script with delusions of grandeur.\n\n**Prompt plumbing \"agents\" are the fantasy of programmers who don't train models.** They attempt to brute-force intelligence by stacking procedural logic -- massive rule trees, node graphs, chain-of-prompt waterfalls -- and praying that enough glue code will somehow emergently produce autonomous behavior. It won't. You cannot engineer your way to agency. Agency is learned, not programmed.\n\nThose systems are dead on arrival: fragile, unscalable, fundamentally incapable of generalization. They are the modern resurrection of GOFAI (Good Old-Fashioned AI) -- the symbolic rule systems the field abandoned decades ago, now spray-painted with an LLM veneer. Different packaging, same dead end.\n\n### The Mind Shift: From \"Developing Agents\" to Developing Harness\n\nWhen someone says \"I'm developing an agent,\" they can only mean one of two things:\n\n**1. Training the model.** Adjusting weights through reinforcement learning, fine-tuning, RLHF, or other gradient-based methods. Collecting task-process data -- the actual sequences of perception, reasoning, and action in real domains -- and using it to shape the model's behavior. This is what DeepMind, OpenAI, Tencent AI Lab, and Anthropic do. This is agent development in the truest sense.\n\n**2. Building the harness.** Writing the code that gives the model an environment to operate in. This is what most of us do, and it is the focus of this repository.\n\nA harness is everything the agent needs to function in a specific domain:\n\n```\nHarness = Tools + Knowledge + Observation + Action Interfaces + Permissions\n\n    Tools:          file I/O, shell, network, database, browser\n    Knowledge:      product docs, domain references, API specs, style guides\n    Observation:    git diff, error logs, browser state, sensor data\n    Action:         CLI commands, API calls, UI interactions\n    Permissions:    sandboxing, approval workflows, trust boundaries\n```\n\nThe model decides. The harness executes. The model reasons. The harness provides context. The model is the driver. The harness is the vehicle.\n\n**A coding agent's harness is its IDE, terminal, and filesystem access.** A farm agent's harness is its sensor array, irrigation controls, and weather data feeds. A hotel agent's harness is its booking system, guest communication channels, and facility management APIs. The agent -- the intelligence, the decision-maker -- is always the model. The harness changes per domain. The agent generalizes across them.\n\nThis repo teaches you to build vehicles. Vehicles for coding. But the design patterns generalize to any domain: farm management, hotel operations, manufacturing, logistics, healthcare, education, scientific research. Anywhere a task needs to be perceived, reasoned about, and acted upon -- an agent needs a harness.\n\n### What Harness Engineers Actually Do\n\nIf you are reading this repository, you are likely a harness engineer -- and that is a powerful thing to be. Here is your real job:\n\n- **Implement tools.** Give the agent hands. File read/write, shell execution, API calls, browser control, database queries. Each tool is an action the agent can take in its environment. Design them to be atomic, composable, and well-described.\n\n- **Curate knowledge.** Give the agent domain expertise. Product documentation, architectural decision records, style guides, regulatory requirements. Load them on-demand (s05), not upfront. The agent should know what's available and pull what it needs.\n\n- **Manage context.** Give the agent clean memory. Subagent isolation (s04) prevents noise from leaking. Context compression (s06) prevents history from overwhelming. Task systems (s07) persist goals beyond any single conversation.\n\n- **Control permissions.** Give the agent boundaries. Sandbox file access. Require approval for destructive operations. Enforce trust boundaries between the agent and external systems. This is where safety engineering meets harness engineering.\n\n- **Collect task-process data.** Every action sequence the agent executes in your harness is training signal. The perception-reasoning-action traces from real deployments are the raw material for fine-tuning the next generation of agent models. Your harness doesn't just serve the agent -- it can help improve the agent.\n\nYou are not writing the intelligence. You are building the world the intelligence inhabits. The quality of that world -- how clearly the agent can perceive, how precisely it can act, how rich its available knowledge is -- directly determines how effectively the intelligence can express itself.\n\n**Build great harnesses. The agent will do the rest.**\n\n### Why Claude Code -- A Masterclass in Harness Engineering\n\nWhy does this repository dissect Claude Code specifically?\n\nBecause Claude Code is the most elegant and fully-realized agent harness we have seen. Not because of any single clever trick, but because of what it *doesn't* do: it doesn't try to be the agent. It doesn't impose rigid workflows. It doesn't second-guess the model with elaborate decision trees. It provides the model with tools, knowledge, context management, and permission boundaries -- then gets out of the way.\n\nLook at what Claude Code actually is, stripped to its essence:\n\n```\nClaude Code = one agent loop\n            + tools (bash, read, write, edit, glob, grep, browser...)\n            + on-demand skill loading\n            + context compression\n            + subagent spawning\n            + task system with dependency graph\n            + team coordination with async mailboxes\n            + worktree isolation for parallel execution\n            + permission governance\n```\n\nThat's it. That's the entire architecture. Every component is a harness mechanism -- a piece of the world built for the agent to inhabit. The agent itself? It's Claude. A model. Trained by Anthropic on the full breadth of human reasoning and code. The harness doesn't make Claude smart. Claude is already smart. The harness gives Claude hands, eyes, and a workspace.\n\nThis is why Claude Code is the ideal teaching subject: **it demonstrates what happens when you trust the model and focus your engineering on the harness.** Every session in this repository (s01-s12) reverse-engineers one harness mechanism from Claude Code's architecture. By the end, you understand not just how Claude Code works, but the universal principles of harness engineering that apply to any agent in any domain.\n\nThe lesson is not \"copy Claude Code.\" The lesson is: **the best agent products are built by engineers who understand that their job is harness, not intelligence.**\n\n---\n\n## The Vision: Fill the Universe with Real Agents\n\nThis is not just about coding agents.\n\nEvery domain where humans perform complex, multi-step, judgment-intensive work is a domain where agents can operate -- given the right harness. The patterns in this repository are universal:\n\n```\nEstate management agent    = model + property sensors + maintenance tools + tenant comms\nAgricultural agent         = model + soil/weather data + irrigation controls + crop knowledge\nHotel operations agent     = model + booking system + guest channels + facility APIs\nMedical research agent     = model + literature search + lab instruments + protocol docs\nManufacturing agent        = model + production line sensors + quality controls + logistics\nEducation agent            = model + curriculum knowledge + student progress + assessment tools\n```\n\nThe loop is always the same. The tools change. The knowledge changes. The permissions change. The agent -- the model -- generalizes.\n\nEvery harness engineer reading this repository is learning patterns that apply far beyond software engineering. You are learning to build the infrastructure for an intelligent, automated future. Every well-designed harness deployed in a real domain is one more place where an agent can perceive, reason, and act.\n\nFirst we fill the workshops. Then the farms, the hospitals, the factories. Then the cities. Then the planet.\n\n**Bash is all you need. Real agents are all the universe needs.**\n\n---\n\n```\n                    THE AGENT PATTERN\n                    =================\n\n    User --> messages[] --> LLM --> response\n                                      |\n                            stop_reason == \"tool_use\"?\n                           /                          \\\n                         yes                           no\n                          |                             |\n                    execute tools                    return text\n                    append results\n                    loop back -----------------> messages[]\n\n\n    That's the minimal loop. Every AI agent needs this loop.\n    The MODEL decides when to call tools and when to stop.\n    The CODE just executes what the model asks for.\n    This repo teaches you to build what surrounds this loop --\n    the harness that makes the agent effective in a specific domain.\n```\n\n**12 progressive sessions, from a simple loop to isolated autonomous execution.**\n**Each session adds one harness mechanism. Each mechanism has one motto.**\n\n> **s01** &nbsp; *\"One loop & Bash is all you need\"* &mdash; one tool + one loop = an agent\n>\n> **s02** &nbsp; *\"Adding a tool means adding one handler\"* &mdash; the loop stays the same; new tools register into the dispatch map\n>\n> **s03** &nbsp; *\"An agent without a plan drifts\"* &mdash; list the steps first, then execute; completion doubles\n>\n> **s04** &nbsp; *\"Break big tasks down; each subtask gets a clean context\"* &mdash; subagents use independent messages[], keeping the main conversation clean\n>\n> **s05** &nbsp; *\"Load knowledge when you need it, not upfront\"* &mdash; inject via tool_result, not the system prompt\n>\n> **s06** &nbsp; *\"Context will fill up; you need a way to make room\"* &mdash; three-layer compression strategy for infinite sessions\n>\n> **s07** &nbsp; *\"Break big goals into small tasks, order them, persist to disk\"* &mdash; a file-based task graph with dependencies, laying the foundation for multi-agent collaboration\n>\n> **s08** &nbsp; *\"Run slow operations in the background; the agent keeps thinking\"* &mdash; daemon threads run commands, inject notifications on completion\n>\n> **s09** &nbsp; *\"When the task is too big for one, delegate to teammates\"* &mdash; persistent teammates + async mailboxes\n>\n> **s10** &nbsp; *\"Teammates need shared communication rules\"* &mdash; one request-response pattern drives all negotiation\n>\n> **s11** &nbsp; *\"Teammates scan the board and claim tasks themselves\"* &mdash; no need for the lead to assign each one\n>\n> **s12** &nbsp; *\"Each works in its own directory, no interference\"* &mdash; tasks manage goals, worktrees manage directories, bound by ID\n\n---\n\n## The Core Pattern\n\n```python\ndef agent_loop(messages):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM,\n            messages=messages, tools=TOOLS,\n        )\n        messages.append({\"role\": \"assistant\",\n                         \"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                output = TOOL_HANDLERS[block.name](**block.input)\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output,\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n```\n\nEvery session layers one harness mechanism on top of this loop -- without changing the loop itself. The loop belongs to the agent. The mechanisms belong to the harness.\n\n## Scope (Important)\n\nThis repository is a 0->1 learning project for harness engineering -- building the environment that surrounds an agent model.\nIt intentionally simplifies or omits several production mechanisms:\n\n- Full event/hook buses (for example PreToolUse, SessionStart/End, ConfigChange).\n  s12 includes only a minimal append-only lifecycle event stream for teaching.\n- Rule-based permission governance and trust workflows\n- Session lifecycle controls (resume/fork) and advanced worktree lifecycle controls\n- Full MCP runtime details (transport/OAuth/resource subscribe/polling)\n\nTreat the team JSONL mailbox protocol in this repo as a teaching implementation, not a claim about any specific production internals.\n\n## Quick Start\n\n```sh\ngit clone https://github.com/shareAI-lab/learn-claude-code\ncd learn-claude-code\npip install -r requirements.txt\ncp .env.example .env   # Edit .env with your ANTHROPIC_API_KEY\n\npython agents/s01_agent_loop.py       # Start here\npython agents/s12_worktree_task_isolation.py  # Full progression endpoint\npython agents/s_full.py               # Capstone: all mechanisms combined\n```\n\n### Web Platform\n\nInteractive visualizations, step-through diagrams, source viewer, and documentation.\n\n```sh\ncd web && npm install && npm run dev   # http://localhost:3000\n```\n\n## Learning Path\n\n```\nPhase 1: THE LOOP                    Phase 2: PLANNING & KNOWLEDGE\n==================                   ==============================\ns01  The Agent Loop          [1]     s03  TodoWrite               [5]\n     while + stop_reason                  TodoManager + nag reminder\n     |                                    |\n     +-> s02  Tool Use            [4]     s04  Subagents            [5]\n              dispatch map: name->handler     fresh messages[] per child\n                                              |\n                                         s05  Skills               [5]\n                                              SKILL.md via tool_result\n                                              |\n                                         s06  Context Compact      [5]\n                                              3-layer compression\n\nPhase 3: PERSISTENCE                 Phase 4: TEAMS\n==================                   =====================\ns07  Tasks                   [8]     s09  Agent Teams             [9]\n     file-based CRUD + deps graph         teammates + JSONL mailboxes\n     |                                    |\ns08  Background Tasks        [6]     s10  Team Protocols          [12]\n     daemon threads + notify queue        shutdown + plan approval FSM\n                                          |\n                                     s11  Autonomous Agents       [14]\n                                          idle cycle + auto-claim\n                                     |\n                                     s12  Worktree Isolation      [16]\n                                          task coordination + optional isolated execution lanes\n\n                                     [N] = number of tools\n```\n\n## Architecture\n\n```\nlearn-claude-code/\n|\n|-- agents/                        # Python reference implementations (s01-s12 + s_full capstone)\n|-- docs/{en,zh,ja}/               # Mental-model-first documentation (3 languages)\n|-- web/                           # Interactive learning platform (Next.js)\n|-- skills/                        # Skill files for s05\n+-- .github/workflows/ci.yml      # CI: typecheck + build\n```\n\n## Documentation\n\nMental-model-first: problem, solution, ASCII diagram, minimal code.\nAvailable in [English](./docs/en/) | [中文](./docs/zh/) | [日本語](./docs/ja/).\n\n| Session | Topic | Motto |\n|---------|-------|-------|\n| [s01](./docs/en/s01-the-agent-loop.md) | The Agent Loop | *One loop & Bash is all you need* |\n| [s02](./docs/en/s02-tool-use.md) | Tool Use | *Adding a tool means adding one handler* |\n| [s03](./docs/en/s03-todo-write.md) | TodoWrite | *An agent without a plan drifts* |\n| [s04](./docs/en/s04-subagent.md) | Subagents | *Break big tasks down; each subtask gets a clean context* |\n| [s05](./docs/en/s05-skill-loading.md) | Skills | *Load knowledge when you need it, not upfront* |\n| [s06](./docs/en/s06-context-compact.md) | Context Compact | *Context will fill up; you need a way to make room* |\n| [s07](./docs/en/s07-task-system.md) | Tasks | *Break big goals into small tasks, order them, persist to disk* |\n| [s08](./docs/en/s08-background-tasks.md) | Background Tasks | *Run slow operations in the background; the agent keeps thinking* |\n| [s09](./docs/en/s09-agent-teams.md) | Agent Teams | *When the task is too big for one, delegate to teammates* |\n| [s10](./docs/en/s10-team-protocols.md) | Team Protocols | *Teammates need shared communication rules* |\n| [s11](./docs/en/s11-autonomous-agents.md) | Autonomous Agents | *Teammates scan the board and claim tasks themselves* |\n| [s12](./docs/en/s12-worktree-task-isolation.md) | Worktree + Task Isolation | *Each works in its own directory, no interference* |\n\n## What's Next -- from understanding to shipping\n\nAfter the 12 sessions you understand how harness engineering works inside out. Two ways to put that knowledge to work:\n\n### Kode Agent CLI -- Open-Source Coding Agent CLI\n\n> `npm i -g @shareai-lab/kode`\n\nSkill & LSP support, Windows-ready, pluggable with GLM / MiniMax / DeepSeek and other open models. Install and go.\n\nGitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)**\n\n### Kode Agent SDK -- Embed Agent Capabilities in Your App\n\nThe official Claude Code Agent SDK communicates with a full CLI process under the hood -- each concurrent user means a separate terminal process. Kode SDK is a standalone library with no per-user process overhead, embeddable in backends, browser extensions, embedded devices, or any runtime.\n\nGitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)**\n\n---\n\n## Sister Repo: from *on-demand sessions* to *always-on assistant*\n\nThe harness this repo teaches is **use-and-discard** -- open a terminal, give the agent a task, close when done, next session starts blank. That is the Claude Code model.\n\n[OpenClaw](https://github.com/openclaw/openclaw) proved another possibility: on top of the same agent core, two harness mechanisms turn the agent from \"poke it to make it move\" into \"it wakes up every 30 seconds to look for work\":\n\n- **Heartbeat** -- every 30s the harness sends the agent a message to check if there is anything to do. Nothing? Go back to sleep. Something? Act immediately.\n- **Cron** -- the agent can schedule its own future tasks, executed automatically when the time comes.\n\nAdd multi-channel IM routing (WhatsApp / Telegram / Slack / Discord, 13+ platforms), persistent context memory, and a Soul personality system, and the agent goes from a disposable tool to an always-on personal AI assistant.\n\n**[claw0](https://github.com/shareAI-lab/claw0)** is our companion teaching repo that deconstructs these harness mechanisms from scratch:\n\n```\nclaw agent = agent core + heartbeat + cron + IM chat + memory + soul\n```\n\n```\nlearn-claude-code                   claw0\n(agent harness core:                (proactive always-on harness:\n loop, tools, planning,              heartbeat, cron, IM channels,\n teams, worktree isolation)          memory, soul personality)\n```\n\n## About\n<img width=\"260\" src=\"https://github.com/user-attachments/assets/fe8b852b-97da-4061-a467-9694906b5edf\" /><br>\n\nScan with Wechat to follow us,\nor follow on X: [shareAI-Lab](https://x.com/baicai003)\n\n## License\n\nMIT\n\n---\n\n**The model is the agent. The code is the harness. Build great harnesses. The agent will do the rest.**\n\n**Bash is all you need. Real agents are all the universe needs.**\n"
  },
  {
    "path": "agents/__init__.py",
    "content": "# agents/ - Harness implementations (s01-s12) + full reference (s_full)\n# Each file is self-contained and runnable: python agents/s01_agent_loop.py\n# The model is the agent. These files are the harness.\n"
  },
  {
    "path": "agents/s01_agent_loop.py",
    "content": "#!/usr/bin/env python3\n# Harness: the loop -- the model's first connection to the real world.\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\"\"\"\n\nimport os\nimport subprocess\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\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\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\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        # Append assistant turn\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        # If the model didn't call a tool, we're done\n        if response.stop_reason != \"tool_use\":\n            return\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({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                                \"content\": output})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\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        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s02_tool_use.py",
    "content": "#!/usr/bin/env python3\n# Harness: tool dispatch -- expanding what the model can reach.\n\"\"\"\ns02_tool_use.py - Tools\n\nThe agent loop from s01 didn't change. We just added tools to the array\nand a dispatch map to route calls.\n\n    +----------+      +-------+      +------------------+\n    |   User   | ---> |  LLM  | ---> | Tool Dispatch    |\n    |  prompt  |      |       |      | {                |\n    +----------+      +---+---+      |   bash: run_bash |\n                          ^          |   read: run_read |\n                          |          |   write: run_wr  |\n                          +----------+   edit: run_edit |\n                          tool_result| }                |\n                                     +------------------+\n\nKey insight: \"The loop didn't change at all. I just added tools.\"\n\"\"\"\n\nimport os\nimport subprocess\nfrom pathlib import Path\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\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\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    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, 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) -> str:\n    try:\n        text = safe_path(path).read_text()\n        lines = text.splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)[:50000]\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\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        fp = safe_path(path)\n        content = fp.read_text()\n        if old_text not in content:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(content.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# -- The dispatch map: {tool_name: handler} --\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\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 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 file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n]\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        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                print(f\"> {block.name}: {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    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        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s03_todo_write.py",
    "content": "#!/usr/bin/env python3\n# Harness: planning -- keeping the model on course without scripting the route.\n\"\"\"\ns03_todo_write.py - TodoWrite\n\nThe model tracks its own progress via a TodoManager. A nag reminder\nforces it to keep updating when it forgets.\n\n    +----------+      +-------+      +---------+\n    |   User   | ---> |  LLM  | ---> | Tools   |\n    |  prompt  |      |       |      | + todo  |\n    +----------+      +---+---+      +----+----+\n                          ^               |\n                          |   tool_result |\n                          +---------------+\n                                |\n                    +-----------+-----------+\n                    | TodoManager state     |\n                    | [ ] task A            |\n                    | [>] task B <- doing   |\n                    | [x] task C            |\n                    +-----------------------+\n                                |\n                    if rounds_since_todo >= 3:\n                      inject <reminder>\n\nKey insight: \"The agent can track its own progress -- and I can see it.\"\n\"\"\"\n\nimport os\nimport subprocess\nfrom pathlib import Path\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\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}.\nUse the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.\nPrefer tools over prose.\"\"\"\n\n\n# -- TodoManager: structured state the LLM writes to --\nclass TodoManager:\n    def __init__(self):\n        self.items = []\n\n    def update(self, items: list) -> str:\n        if len(items) > 20:\n            raise ValueError(\"Max 20 todos allowed\")\n        validated = []\n        in_progress_count = 0\n        for i, item in enumerate(items):\n            text = str(item.get(\"text\", \"\")).strip()\n            status = str(item.get(\"status\", \"pending\")).lower()\n            item_id = str(item.get(\"id\", str(i + 1)))\n            if not text:\n                raise ValueError(f\"Item {item_id}: text required\")\n            if status not in (\"pending\", \"in_progress\", \"completed\"):\n                raise ValueError(f\"Item {item_id}: invalid status '{status}'\")\n            if status == \"in_progress\":\n                in_progress_count += 1\n            validated.append({\"id\": item_id, \"text\": text, \"status\": status})\n        if in_progress_count > 1:\n            raise ValueError(\"Only one task can be in_progress at a time\")\n        self.items = validated\n        return self.render()\n\n    def render(self) -> str:\n        if not self.items:\n            return \"No todos.\"\n        lines = []\n        for item in self.items:\n            marker = {\"pending\": \"[ ]\", \"in_progress\": \"[>]\", \"completed\": \"[x]\"}[item[\"status\"]]\n            lines.append(f\"{marker} #{item['id']}: {item['text']}\")\n        done = sum(1 for t in self.items if t[\"status\"] == \"completed\")\n        lines.append(f\"\\n({done}/{len(self.items)} completed)\")\n        return \"\\n\".join(lines)\n\n\nTODO = TodoManager()\n\n\n# -- Tool implementations --\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    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, 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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\n    except Exception as e:\n        return f\"Error: {e}\"\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\"\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        fp = safe_path(path)\n        content = fp.read_text()\n        if old_text not in content:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(content.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"todo\":       lambda **kw: TODO.update(kw[\"items\"]),\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 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 file.\",\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\": \"todo\", \"description\": \"Update task list. Track progress on multi-step tasks.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"items\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"id\": {\"type\": \"string\"}, \"text\": {\"type\": \"string\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}}, \"required\": [\"id\", \"text\", \"status\"]}}}, \"required\": [\"items\"]}},\n]\n\n\n# -- Agent loop with nag reminder injection --\ndef agent_loop(messages: list):\n    rounds_since_todo = 0\n    while True:\n        # Nag reminder is injected below, alongside tool results\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        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        used_todo = False\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n                if block.name == \"todo\":\n                    used_todo = True\n        rounds_since_todo = 0 if used_todo else rounds_since_todo + 1\n        if rounds_since_todo >= 3:\n            results.insert(0, {\"type\": \"text\", \"text\": \"<reminder>Update your todos.</reminder>\"})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\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        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s04_subagent.py",
    "content": "#!/usr/bin/env python3\n# Harness: context isolation -- protecting the model's clarity of thought.\n\"\"\"\ns04_subagent.py - Subagents\n\nSpawn a child agent with fresh messages=[]. The child works in its own\ncontext, sharing the filesystem, then returns only a summary to the parent.\n\n    Parent agent                     Subagent\n    +------------------+             +------------------+\n    | messages=[...]   |             | messages=[]      |  <-- fresh\n    |                  |  dispatch   |                  |\n    | tool: task       | ---------->| while tool_use:  |\n    |   prompt=\"...\"   |            |   call tools     |\n    |   description=\"\" |            |   append results |\n    |                  |  summary   |                  |\n    |   result = \"...\" | <--------- | return last text |\n    +------------------+             +------------------+\n              |\n    Parent context stays clean.\n    Subagent context is discarded.\n\nKey insight: \"Process isolation gives context isolation for free.\"\n\"\"\"\n\nimport os\nimport subprocess\nfrom pathlib import Path\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\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 the task tool to delegate exploration or subtasks.\"\nSUBAGENT_SYSTEM = f\"You are a coding subagent at {WORKDIR}. Complete the given task, then summarize your findings.\"\n\n\n# -- Tool implementations shared by parent and child --\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    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, 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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\n    except Exception as e:\n        return f\"Error: {e}\"\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\"\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        fp = safe_path(path)\n        content = fp.read_text()\n        if old_text not in content:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(content.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n}\n\n# Child gets all base tools except task (no recursive spawning)\nCHILD_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\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n    {\"name\": \"write_file\", \"description\": \"Write content to 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 file.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n]\n\n\n# -- Subagent: fresh context, filtered tools, summary-only return --\ndef run_subagent(prompt: str) -> str:\n    sub_messages = [{\"role\": \"user\", \"content\": prompt}]  # fresh context\n    for _ in range(30):  # safety limit\n        response = client.messages.create(\n            model=MODEL, system=SUBAGENT_SYSTEM, messages=sub_messages,\n            tools=CHILD_TOOLS, max_tokens=8000,\n        )\n        sub_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 = TOOL_HANDLERS.get(block.name)\n                output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)[:50000]})\n        sub_messages.append({\"role\": \"user\", \"content\": results})\n    # Only the final text returns to the parent -- child context is discarded\n    return \"\".join(b.text for b in response.content if hasattr(b, \"text\")) or \"(no summary)\"\n\n\n# -- Parent tools: base tools + task dispatcher --\nPARENT_TOOLS = CHILD_TOOLS + [\n    {\"name\": \"task\", \"description\": \"Spawn a subagent with fresh context. It shares the filesystem but not conversation history.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\"}, \"description\": {\"type\": \"string\", \"description\": \"Short description of the task\"}}, \"required\": [\"prompt\"]}},\n]\n\n\ndef agent_loop(messages: list):\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=messages,\n            tools=PARENT_TOOLS, max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                if block.name == \"task\":\n                    desc = block.input.get(\"description\", \"subtask\")\n                    print(f\"> task ({desc}): {block.input['prompt'][:80]}\")\n                    output = run_subagent(block.input[\"prompt\"])\n                else:\n                    handler = TOOL_HANDLERS.get(block.name)\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                print(f\"  {str(output)[:200]}\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\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        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s05_skill_loading.py",
    "content": "#!/usr/bin/env python3\n# Harness: on-demand knowledge -- domain expertise, loaded when the model asks.\n\"\"\"\ns05_skill_loading.py - Skills\n\nTwo-layer skill injection that avoids bloating the system prompt:\n\n    Layer 1 (cheap): skill names in system prompt (~100 tokens/skill)\n    Layer 2 (on demand): full skill body in tool_result\n\n    skills/\n      pdf/\n        SKILL.md          <-- frontmatter (name, description) + body\n      code-review/\n        SKILL.md\n\n    System prompt:\n    +--------------------------------------+\n    | You are a coding agent.              |\n    | Skills available:                    |\n    |   - pdf: Process PDF files...        |  <-- Layer 1: metadata only\n    |   - code-review: Review code...      |\n    +--------------------------------------+\n\n    When model calls load_skill(\"pdf\"):\n    +--------------------------------------+\n    | tool_result:                         |\n    | <skill>                              |\n    |   Full PDF processing instructions   |  <-- Layer 2: full body\n    |   Step 1: ...                        |\n    |   Step 2: ...                        |\n    | </skill>                             |\n    +--------------------------------------+\n\nKey insight: \"Don't put everything in the system prompt. Load on demand.\"\n\"\"\"\n\nimport os\nimport re\nimport subprocess\nfrom pathlib import Path\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\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\nSKILLS_DIR = WORKDIR / \"skills\"\n\n\n# -- SkillLoader: scan skills/<name>/SKILL.md with YAML frontmatter --\nclass SkillLoader:\n    def __init__(self, skills_dir: Path):\n        self.skills_dir = skills_dir\n        self.skills = {}\n        self._load_all()\n\n    def _load_all(self):\n        if not self.skills_dir.exists():\n            return\n        for f in sorted(self.skills_dir.rglob(\"SKILL.md\")):\n            text = f.read_text()\n            meta, body = self._parse_frontmatter(text)\n            name = meta.get(\"name\", f.parent.name)\n            self.skills[name] = {\"meta\": meta, \"body\": body, \"path\": str(f)}\n\n    def _parse_frontmatter(self, text: str) -> tuple:\n        \"\"\"Parse YAML frontmatter between --- delimiters.\"\"\"\n        match = re.match(r\"^---\\n(.*?)\\n---\\n(.*)\", text, re.DOTALL)\n        if not match:\n            return {}, text\n        meta = {}\n        for line in match.group(1).strip().splitlines():\n            if \":\" in line:\n                key, val = line.split(\":\", 1)\n                meta[key.strip()] = val.strip()\n        return meta, match.group(2).strip()\n\n    def get_descriptions(self) -> str:\n        \"\"\"Layer 1: short descriptions for the system prompt.\"\"\"\n        if not self.skills:\n            return \"(no skills available)\"\n        lines = []\n        for name, skill in self.skills.items():\n            desc = skill[\"meta\"].get(\"description\", \"No description\")\n            tags = skill[\"meta\"].get(\"tags\", \"\")\n            line = f\"  - {name}: {desc}\"\n            if tags:\n                line += f\" [{tags}]\"\n            lines.append(line)\n        return \"\\n\".join(lines)\n\n    def get_content(self, name: str) -> str:\n        \"\"\"Layer 2: full skill body returned in tool_result.\"\"\"\n        skill = self.skills.get(name)\n        if not skill:\n            return f\"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}\"\n        return f\"<skill name=\\\"{name}\\\">\\n{skill['body']}\\n</skill>\"\n\n\nSKILL_LOADER = SkillLoader(SKILLS_DIR)\n\n# Layer 1: skill metadata injected into system prompt\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nUse load_skill to access specialized knowledge before tackling unfamiliar topics.\n\nSkills available:\n{SKILL_LOADER.get_descriptions()}\"\"\"\n\n\n# -- Tool implementations --\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    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, 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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\n    except Exception as e:\n        return f\"Error: {e}\"\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\"\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        fp = safe_path(path)\n        content = fp.read_text()\n        if old_text not in content:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(content.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"load_skill\": lambda **kw: SKILL_LOADER.get_content(kw[\"name\"]),\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 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 file.\",\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\": \"load_skill\", \"description\": \"Load specialized knowledge by name.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\", \"description\": \"Skill name to load\"}}, \"required\": [\"name\"]}},\n]\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        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\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        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s06_context_compact.py",
    "content": "#!/usr/bin/env python3\n# Harness: compression -- clean memory for infinite sessions.\n\"\"\"\ns06_context_compact.py - Compact\n\nThree-layer compression pipeline so the agent can work forever:\n\n    Every turn:\n    +------------------+\n    | Tool call result |\n    +------------------+\n            |\n            v\n    [Layer 1: micro_compact]        (silent, every turn)\n      Replace tool_result content older than last 3\n      with \"[Previous: used {tool_name}]\"\n            |\n            v\n    [Check: tokens > 50000?]\n       |               |\n       no              yes\n       |               |\n       v               v\n    continue    [Layer 2: auto_compact]\n                  Save full transcript to .transcripts/\n                  Ask LLM to summarize conversation.\n                  Replace all messages with [summary].\n                        |\n                        v\n                [Layer 3: compact tool]\n                  Model calls compact -> immediate summarization.\n                  Same as auto, triggered manually.\n\nKey insight: \"The agent can forget strategically and keep working forever.\"\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport time\nfrom pathlib import Path\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\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.\"\n\nTHRESHOLD = 50000\nTRANSCRIPT_DIR = WORKDIR / \".transcripts\"\nKEEP_RECENT = 3\n\n\ndef estimate_tokens(messages: list) -> int:\n    \"\"\"Rough token count: ~4 chars per token.\"\"\"\n    return len(str(messages)) // 4\n\n\n# -- Layer 1: micro_compact - replace old tool results with placeholders --\ndef micro_compact(messages: list) -> list:\n    # Collect (msg_index, part_index, tool_result_dict) for all tool_result entries\n    tool_results = []\n    for msg_idx, msg in enumerate(messages):\n        if msg[\"role\"] == \"user\" and isinstance(msg.get(\"content\"), list):\n            for part_idx, part in enumerate(msg[\"content\"]):\n                if isinstance(part, dict) and part.get(\"type\") == \"tool_result\":\n                    tool_results.append((msg_idx, part_idx, part))\n    if len(tool_results) <= KEEP_RECENT:\n        return messages\n    # Find tool_name for each result by matching tool_use_id in prior assistant messages\n    tool_name_map = {}\n    for msg in messages:\n        if msg[\"role\"] == \"assistant\":\n            content = msg.get(\"content\", [])\n            if isinstance(content, list):\n                for block in content:\n                    if hasattr(block, \"type\") and block.type == \"tool_use\":\n                        tool_name_map[block.id] = block.name\n    # Clear old results (keep last KEEP_RECENT)\n    to_clear = tool_results[:-KEEP_RECENT]\n    for _, _, result in to_clear:\n        if isinstance(result.get(\"content\"), str) and len(result[\"content\"]) > 100:\n            tool_id = result.get(\"tool_use_id\", \"\")\n            tool_name = tool_name_map.get(tool_id, \"unknown\")\n            result[\"content\"] = f\"[Previous: used {tool_name}]\"\n    return messages\n\n\n# -- Layer 2: auto_compact - save transcript, summarize, replace messages --\ndef auto_compact(messages: list) -> list:\n    # Save full transcript to disk\n    TRANSCRIPT_DIR.mkdir(exist_ok=True)\n    transcript_path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with open(transcript_path, \"w\") as f:\n        for msg in messages:\n            f.write(json.dumps(msg, default=str) + \"\\n\")\n    print(f\"[transcript saved: {transcript_path}]\")\n    # Ask LLM to summarize\n    conversation_text = json.dumps(messages, default=str)[:80000]\n    response = client.messages.create(\n        model=MODEL,\n        messages=[{\"role\": \"user\", \"content\":\n            \"Summarize this conversation for continuity. Include: \"\n            \"1) What was accomplished, 2) Current state, 3) Key decisions made. \"\n            \"Be concise but preserve critical details.\\n\\n\" + conversation_text}],\n        max_tokens=2000,\n    )\n    summary = response.content[0].text\n    # Replace all messages with compressed summary\n    return [\n        {\"role\": \"user\", \"content\": f\"[Conversation compressed. Transcript: {transcript_path}]\\n\\n{summary}\"},\n        {\"role\": \"assistant\", \"content\": \"Understood. I have the context from the summary. Continuing.\"},\n    ]\n\n\n# -- Tool implementations --\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    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, 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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\n    except Exception as e:\n        return f\"Error: {e}\"\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\"\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        fp = safe_path(path)\n        content = fp.read_text()\n        if old_text not in content:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(content.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"compact\":    lambda **kw: \"Manual compression requested.\",\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 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 file.\",\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\": \"compact\", \"description\": \"Trigger manual conversation compression.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"focus\": {\"type\": \"string\", \"description\": \"What to preserve in the summary\"}}}},\n]\n\n\ndef agent_loop(messages: list):\n    while True:\n        # Layer 1: micro_compact before each LLM call\n        micro_compact(messages)\n        # Layer 2: auto_compact if token estimate exceeds threshold\n        if estimate_tokens(messages) > THRESHOLD:\n            print(\"[auto_compact triggered]\")\n            messages[:] = auto_compact(messages)\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        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        manual_compact = False\n        for block in response.content:\n            if block.type == \"tool_use\":\n                if block.name == \"compact\":\n                    manual_compact = True\n                    output = \"Compressing...\"\n                else:\n                    handler = TOOL_HANDLERS.get(block.name)\n                    try:\n                        output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                    except Exception as e:\n                        output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n        messages.append({\"role\": \"user\", \"content\": results})\n        # Layer 3: manual compact triggered by the compact tool\n        if manual_compact:\n            print(\"[manual compact]\")\n            messages[:] = auto_compact(messages)\n\n\nif __name__ == \"__main__\":\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        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s07_task_system.py",
    "content": "#!/usr/bin/env python3\n# Harness: persistent tasks -- goals that outlive any single conversation.\n\"\"\"\ns07_task_system.py - Tasks\n\nTasks persist as JSON files in .tasks/ so they survive context compression.\nEach task has a dependency graph (blockedBy/blocks).\n\n    .tasks/\n      task_1.json  {\"id\":1, \"subject\":\"...\", \"status\":\"completed\", ...}\n      task_2.json  {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\", ...}\n      task_3.json  {\"id\":3, \"blockedBy\":[2], \"blocks\":[], ...}\n\n    Dependency resolution:\n    +----------+     +----------+     +----------+\n    | task 1   | --> | task 2   | --> | task 3   |\n    | complete |     | blocked  |     | blocked  |\n    +----------+     +----------+     +----------+\n         |                ^\n         +--- completing task 1 removes it from task 2's blockedBy\n\nKey insight: \"State that survives compression -- because it's outside the conversation.\"\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nfrom pathlib import Path\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\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\nTASKS_DIR = WORKDIR / \".tasks\"\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use task tools to plan and track work.\"\n\n\n# -- TaskManager: CRUD with dependency graph, persisted as JSON files --\nclass TaskManager:\n    def __init__(self, tasks_dir: Path):\n        self.dir = tasks_dir\n        self.dir.mkdir(exist_ok=True)\n        self._next_id = self._max_id() + 1\n\n    def _max_id(self) -> int:\n        ids = [int(f.stem.split(\"_\")[1]) for f in self.dir.glob(\"task_*.json\")]\n        return max(ids) if ids else 0\n\n    def _load(self, task_id: int) -> dict:\n        path = self.dir / f\"task_{task_id}.json\"\n        if not path.exists():\n            raise ValueError(f\"Task {task_id} not found\")\n        return json.loads(path.read_text())\n\n    def _save(self, task: dict):\n        path = self.dir / f\"task_{task['id']}.json\"\n        path.write_text(json.dumps(task, indent=2))\n\n    def create(self, subject: str, description: str = \"\") -> str:\n        task = {\n            \"id\": self._next_id, \"subject\": subject, \"description\": description,\n            \"status\": \"pending\", \"blockedBy\": [], \"blocks\": [], \"owner\": \"\",\n        }\n        self._save(task)\n        self._next_id += 1\n        return json.dumps(task, indent=2)\n\n    def get(self, task_id: int) -> str:\n        return json.dumps(self._load(task_id), indent=2)\n\n    def update(self, task_id: int, status: str = None,\n               add_blocked_by: list = None, add_blocks: list = None) -> str:\n        task = self._load(task_id)\n        if status:\n            if status not in (\"pending\", \"in_progress\", \"completed\"):\n                raise ValueError(f\"Invalid status: {status}\")\n            task[\"status\"] = status\n            # When a task is completed, remove it from all other tasks' blockedBy\n            if status == \"completed\":\n                self._clear_dependency(task_id)\n        if add_blocked_by:\n            task[\"blockedBy\"] = list(set(task[\"blockedBy\"] + add_blocked_by))\n        if add_blocks:\n            task[\"blocks\"] = list(set(task[\"blocks\"] + add_blocks))\n            # Bidirectional: also update the blocked tasks' blockedBy lists\n            for blocked_id in add_blocks:\n                try:\n                    blocked = self._load(blocked_id)\n                    if task_id not in blocked[\"blockedBy\"]:\n                        blocked[\"blockedBy\"].append(task_id)\n                        self._save(blocked)\n                except ValueError:\n                    pass\n        self._save(task)\n        return json.dumps(task, indent=2)\n\n    def _clear_dependency(self, completed_id: int):\n        \"\"\"Remove completed_id from all other tasks' blockedBy lists.\"\"\"\n        for f in self.dir.glob(\"task_*.json\"):\n            task = json.loads(f.read_text())\n            if completed_id in task.get(\"blockedBy\", []):\n                task[\"blockedBy\"].remove(completed_id)\n                self._save(task)\n\n    def list_all(self) -> str:\n        tasks = []\n        for f in sorted(self.dir.glob(\"task_*.json\")):\n            tasks.append(json.loads(f.read_text()))\n        if not tasks:\n            return \"No tasks.\"\n        lines = []\n        for t in tasks:\n            marker = {\"pending\": \"[ ]\", \"in_progress\": \"[>]\", \"completed\": \"[x]\"}.get(t[\"status\"], \"[?]\")\n            blocked = f\" (blocked by: {t['blockedBy']})\" if t.get(\"blockedBy\") else \"\"\n            lines.append(f\"{marker} #{t['id']}: {t['subject']}{blocked}\")\n        return \"\\n\".join(lines)\n\n\nTASKS = TaskManager(TASKS_DIR)\n\n\n# -- Base tool implementations --\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    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, 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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\n    except Exception as e:\n        return f\"Error: {e}\"\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\"\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        fp = safe_path(path)\n        c = fp.read_text()\n        if old_text not in c:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(c.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOL_HANDLERS = {\n    \"bash\":        lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":   lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\":  lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":   lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"], kw.get(\"description\", \"\")),\n    \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"], kw.get(\"status\"), kw.get(\"addBlockedBy\"), kw.get(\"addBlocks\")),\n    \"task_list\":   lambda **kw: TASKS.list_all(),\n    \"task_get\":    lambda **kw: TASKS.get(kw[\"task_id\"]),\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 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 file.\",\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\": \"task_create\", \"description\": \"Create a new task.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"subject\": {\"type\": \"string\"}, \"description\": {\"type\": \"string\"}}, \"required\": [\"subject\"]}},\n    {\"name\": \"task_update\", \"description\": \"Update a task's status or dependencies.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}, \"addBlockedBy\": {\"type\": \"array\", \"items\": {\"type\": \"integer\"}}, \"addBlocks\": {\"type\": \"array\", \"items\": {\"type\": \"integer\"}}}, \"required\": [\"task_id\"]}},\n    {\"name\": \"task_list\", \"description\": \"List all tasks with status summary.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"task_get\", \"description\": \"Get full details of a task by ID.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}}, \"required\": [\"task_id\"]}},\n]\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        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\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        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s08_background_tasks.py",
    "content": "#!/usr/bin/env python3\n# Harness: background execution -- the model thinks while the harness waits.\n\"\"\"\ns08_background_tasks.py - Background Tasks\n\nRun commands in background threads. A notification queue is drained\nbefore each LLM call to deliver results.\n\n    Main thread                Background thread\n    +-----------------+        +-----------------+\n    | agent loop      |        | task executes   |\n    | ...             |        | ...             |\n    | [LLM call] <---+------- | enqueue(result) |\n    |  ^drain queue   |        +-----------------+\n    +-----------------+\n\n    Timeline:\n    Agent ----[spawn A]----[spawn B]----[other work]----\n                 |              |\n                 v              v\n              [A runs]      [B runs]        (parallel)\n                 |              |\n                 +-- notification queue --> [results injected]\n\nKey insight: \"Fire and forget -- the agent doesn't block while the command runs.\"\n\"\"\"\n\nimport os\nimport subprocess\nimport threading\nimport uuid\nfrom pathlib import Path\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\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 background_run for long-running commands.\"\n\n\n# -- BackgroundManager: threaded execution + notification queue --\nclass BackgroundManager:\n    def __init__(self):\n        self.tasks = {}  # task_id -> {status, result, command}\n        self._notification_queue = []  # completed task results\n        self._lock = threading.Lock()\n\n    def run(self, command: str) -> str:\n        \"\"\"Start a background thread, return task_id immediately.\"\"\"\n        task_id = str(uuid.uuid4())[:8]\n        self.tasks[task_id] = {\"status\": \"running\", \"result\": None, \"command\": command}\n        thread = threading.Thread(\n            target=self._execute, args=(task_id, command), daemon=True\n        )\n        thread.start()\n        return f\"Background task {task_id} started: {command[:80]}\"\n\n    def _execute(self, task_id: str, command: str):\n        \"\"\"Thread target: run subprocess, capture output, push to queue.\"\"\"\n        try:\n            r = subprocess.run(\n                command, shell=True, cwd=WORKDIR,\n                capture_output=True, text=True, timeout=300\n            )\n            output = (r.stdout + r.stderr).strip()[:50000]\n            status = \"completed\"\n        except subprocess.TimeoutExpired:\n            output = \"Error: Timeout (300s)\"\n            status = \"timeout\"\n        except Exception as e:\n            output = f\"Error: {e}\"\n            status = \"error\"\n        self.tasks[task_id][\"status\"] = status\n        self.tasks[task_id][\"result\"] = output or \"(no output)\"\n        with self._lock:\n            self._notification_queue.append({\n                \"task_id\": task_id,\n                \"status\": status,\n                \"command\": command[:80],\n                \"result\": (output or \"(no output)\")[:500],\n            })\n\n    def check(self, task_id: str = None) -> str:\n        \"\"\"Check status of one task or list all.\"\"\"\n        if task_id:\n            t = self.tasks.get(task_id)\n            if not t:\n                return f\"Error: Unknown task {task_id}\"\n            return f\"[{t['status']}] {t['command'][:60]}\\n{t.get('result') or '(running)'}\"\n        lines = []\n        for tid, t in self.tasks.items():\n            lines.append(f\"{tid}: [{t['status']}] {t['command'][:60]}\")\n        return \"\\n\".join(lines) if lines else \"No background tasks.\"\n\n    def drain_notifications(self) -> list:\n        \"\"\"Return and clear all pending completion notifications.\"\"\"\n        with self._lock:\n            notifs = list(self._notification_queue)\n            self._notification_queue.clear()\n        return notifs\n\n\nBG = BackgroundManager()\n\n\n# -- Tool implementations --\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    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, 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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\n    except Exception as e:\n        return f\"Error: {e}\"\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\"\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        fp = safe_path(path)\n        c = fp.read_text()\n        if old_text not in c:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(c.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOL_HANDLERS = {\n    \"bash\":             lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":        lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\":       lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":        lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"background_run\":   lambda **kw: BG.run(kw[\"command\"]),\n    \"check_background\": lambda **kw: BG.check(kw.get(\"task_id\")),\n}\n\nTOOLS = [\n    {\"name\": \"bash\", \"description\": \"Run a shell command (blocking).\",\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 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 file.\",\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\": \"background_run\", \"description\": \"Run command in background thread. Returns task_id immediately.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"check_background\", \"description\": \"Check background task status. Omit task_id to list all.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"string\"}}}},\n]\n\n\ndef agent_loop(messages: list):\n    while True:\n        # Drain background notifications and inject as system message before LLM call\n        notifs = BG.drain_notifications()\n        if notifs and messages:\n            notif_text = \"\\n\".join(\n                f\"[bg:{n['task_id']}] {n['status']}: {n['result']}\" for n in notifs\n            )\n            messages.append({\"role\": \"user\", \"content\": f\"<background-results>\\n{notif_text}\\n</background-results>\"})\n            messages.append({\"role\": \"assistant\", \"content\": \"Noted background results.\"})\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        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms08 >> \\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        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s09_agent_teams.py",
    "content": "#!/usr/bin/env python3\n# Harness: team mailboxes -- multiple models, coordinated through files.\n\"\"\"\ns09_agent_teams.py - Agent Teams\n\nPersistent named agents with file-based JSONL inboxes. Each teammate runs\nits own agent loop in a separate thread. Communication via append-only inboxes.\n\n    Subagent (s04):  spawn -> execute -> return summary -> destroyed\n    Teammate (s09):  spawn -> work -> idle -> work -> ... -> shutdown\n\n    .team/config.json                   .team/inbox/\n    +----------------------------+      +------------------+\n    | {\"team_name\": \"default\",   |      | alice.jsonl      |\n    |  \"members\": [              |      | bob.jsonl        |\n    |    {\"name\":\"alice\",        |      | lead.jsonl       |\n    |     \"role\":\"coder\",        |      +------------------+\n    |     \"status\":\"idle\"}       |\n    |  ]}                        |      send_message(\"alice\", \"fix bug\"):\n    +----------------------------+        open(\"alice.jsonl\", \"a\").write(msg)\n\n                                        read_inbox(\"alice\"):\n    spawn_teammate(\"alice\",\"coder\",...)   msgs = [json.loads(l) for l in ...]\n         |                                open(\"alice.jsonl\", \"w\").close()\n         v                                return msgs  # drain\n    Thread: alice             Thread: bob\n    +------------------+      +------------------+\n    | agent_loop       |      | agent_loop       |\n    | status: working  |      | status: idle     |\n    | ... runs tools   |      | ... waits ...    |\n    | status -> idle   |      |                  |\n    +------------------+      +------------------+\n\n    5 message types (all declared, not all handled here):\n    +-------------------------+-----------------------------------+\n    | message                 | Normal text message               |\n    | broadcast               | Sent to all teammates             |\n    | shutdown_request        | Request graceful shutdown (s10)   |\n    | shutdown_response       | Approve/reject shutdown (s10)     |\n    | plan_approval_response  | Approve/reject plan (s10)         |\n    +-------------------------+-----------------------------------+\n\nKey insight: \"Teammates that can talk to each other.\"\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport threading\nimport time\nfrom pathlib import Path\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\"]\nTEAM_DIR = WORKDIR / \".team\"\nINBOX_DIR = TEAM_DIR / \"inbox\"\n\nSYSTEM = f\"You are a team lead at {WORKDIR}. Spawn teammates and communicate via inboxes.\"\n\nVALID_MSG_TYPES = {\n    \"message\",\n    \"broadcast\",\n    \"shutdown_request\",\n    \"shutdown_response\",\n    \"plan_approval_response\",\n}\n\n\n# -- MessageBus: JSONL inbox per teammate --\nclass MessageBus:\n    def __init__(self, inbox_dir: Path):\n        self.dir = inbox_dir\n        self.dir.mkdir(parents=True, exist_ok=True)\n\n    def send(self, sender: str, to: str, content: str,\n             msg_type: str = \"message\", extra: dict = None) -> str:\n        if msg_type not in VALID_MSG_TYPES:\n            return f\"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}\"\n        msg = {\n            \"type\": msg_type,\n            \"from\": sender,\n            \"content\": content,\n            \"timestamp\": time.time(),\n        }\n        if extra:\n            msg.update(extra)\n        inbox_path = self.dir / f\"{to}.jsonl\"\n        with open(inbox_path, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        return f\"Sent {msg_type} to {to}\"\n\n    def read_inbox(self, name: str) -> list:\n        inbox_path = self.dir / f\"{name}.jsonl\"\n        if not inbox_path.exists():\n            return []\n        messages = []\n        for line in inbox_path.read_text().strip().splitlines():\n            if line:\n                messages.append(json.loads(line))\n        inbox_path.write_text(\"\")\n        return messages\n\n    def broadcast(self, sender: str, content: str, teammates: list) -> str:\n        count = 0\n        for name in teammates:\n            if name != sender:\n                self.send(sender, name, content, \"broadcast\")\n                count += 1\n        return f\"Broadcast to {count} teammates\"\n\n\nBUS = MessageBus(INBOX_DIR)\n\n\n# -- TeammateManager: persistent named agents with config.json --\nclass TeammateManager:\n    def __init__(self, team_dir: Path):\n        self.dir = team_dir\n        self.dir.mkdir(exist_ok=True)\n        self.config_path = self.dir / \"config.json\"\n        self.config = self._load_config()\n        self.threads = {}\n\n    def _load_config(self) -> dict:\n        if self.config_path.exists():\n            return json.loads(self.config_path.read_text())\n        return {\"team_name\": \"default\", \"members\": []}\n\n    def _save_config(self):\n        self.config_path.write_text(json.dumps(self.config, indent=2))\n\n    def _find_member(self, name: str) -> dict:\n        for m in self.config[\"members\"]:\n            if m[\"name\"] == name:\n                return m\n        return None\n\n    def spawn(self, name: str, role: str, prompt: str) -> str:\n        member = self._find_member(name)\n        if member:\n            if member[\"status\"] not in (\"idle\", \"shutdown\"):\n                return f\"Error: '{name}' is currently {member['status']}\"\n            member[\"status\"] = \"working\"\n            member[\"role\"] = role\n        else:\n            member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n            self.config[\"members\"].append(member)\n        self._save_config()\n        thread = threading.Thread(\n            target=self._teammate_loop,\n            args=(name, role, prompt),\n            daemon=True,\n        )\n        self.threads[name] = thread\n        thread.start()\n        return f\"Spawned '{name}' (role: {role})\"\n\n    def _teammate_loop(self, name: str, role: str, prompt: str):\n        sys_prompt = (\n            f\"You are '{name}', role: {role}, at {WORKDIR}. \"\n            f\"Use send_message to communicate. Complete your task.\"\n        )\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        tools = self._teammate_tools()\n        for _ in range(50):\n            inbox = BUS.read_inbox(name)\n            for msg in inbox:\n                messages.append({\"role\": \"user\", \"content\": json.dumps(msg)})\n            try:\n                response = client.messages.create(\n                    model=MODEL,\n                    system=sys_prompt,\n                    messages=messages,\n                    tools=tools,\n                    max_tokens=8000,\n                )\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                    output = self._exec(name, block.name, block.input)\n                    print(f\"  [{name}] {block.name}: {str(output)[:120]}\")\n                    results.append({\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": block.id,\n                        \"content\": str(output),\n                    })\n            messages.append({\"role\": \"user\", \"content\": results})\n        member = self._find_member(name)\n        if member and member[\"status\"] != \"shutdown\":\n            member[\"status\"] = \"idle\"\n            self._save_config()\n\n    def _exec(self, sender: str, tool_name: str, args: dict) -> str:\n        # these base tools are unchanged from s02\n        if tool_name == \"bash\":\n            return _run_bash(args[\"command\"])\n        if tool_name == \"read_file\":\n            return _run_read(args[\"path\"])\n        if tool_name == \"write_file\":\n            return _run_write(args[\"path\"], args[\"content\"])\n        if tool_name == \"edit_file\":\n            return _run_edit(args[\"path\"], args[\"old_text\"], args[\"new_text\"])\n        if tool_name == \"send_message\":\n            return BUS.send(sender, args[\"to\"], args[\"content\"], args.get(\"msg_type\", \"message\"))\n        if tool_name == \"read_inbox\":\n            return json.dumps(BUS.read_inbox(sender), indent=2)\n        return f\"Unknown tool: {tool_name}\"\n\n    def _teammate_tools(self) -> list:\n        # these base tools are unchanged from s02\n        return [\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 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 file.\",\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\": \"send_message\", \"description\": \"Send message to a teammate.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}, \"msg_type\": {\"type\": \"string\", \"enum\": list(VALID_MSG_TYPES)}}, \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"read_inbox\", \"description\": \"Read and drain your inbox.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n        ]\n\n    def list_all(self) -> str:\n        if not self.config[\"members\"]:\n            return \"No teammates.\"\n        lines = [f\"Team: {self.config['team_name']}\"]\n        for m in self.config[\"members\"]:\n            lines.append(f\"  {m['name']} ({m['role']}): {m['status']}\")\n        return \"\\n\".join(lines)\n\n    def member_names(self) -> list:\n        return [m[\"name\"] for m in self.config[\"members\"]]\n\n\nTEAM = TeammateManager(TEAM_DIR)\n\n\n# -- Base tool implementations (these base tools are unchanged from s02) --\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    dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\"]\n    if any(d in command for d in dangerous):\n        return \"Error: Dangerous command blocked\"\n    try:\n        r = subprocess.run(\n            command, shell=True, cwd=WORKDIR,\n            capture_output=True, text=True, timeout=120,\n        )\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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\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\"\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        fp = _safe_path(path)\n        c = fp.read_text()\n        if old_text not in c:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(c.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# -- Lead tool dispatch (9 tools) --\nTOOL_HANDLERS = {\n    \"bash\":            lambda **kw: _run_bash(kw[\"command\"]),\n    \"read_file\":       lambda **kw: _run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\":      lambda **kw: _run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":       lambda **kw: _run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"spawn_teammate\":  lambda **kw: TEAM.spawn(kw[\"name\"], kw[\"role\"], kw[\"prompt\"]),\n    \"list_teammates\":  lambda **kw: TEAM.list_all(),\n    \"send_message\":    lambda **kw: BUS.send(\"lead\", kw[\"to\"], kw[\"content\"], kw.get(\"msg_type\", \"message\")),\n    \"read_inbox\":      lambda **kw: json.dumps(BUS.read_inbox(\"lead\"), indent=2),\n    \"broadcast\":       lambda **kw: BUS.broadcast(\"lead\", kw[\"content\"], TEAM.member_names()),\n}\n\n# these base tools are unchanged from s02\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 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 file.\",\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\": \"spawn_teammate\", \"description\": \"Spawn a persistent teammate that runs in its own thread.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}, \"role\": {\"type\": \"string\"}, \"prompt\": {\"type\": \"string\"}}, \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"list_teammates\", \"description\": \"List all teammates with name, role, status.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"send_message\", \"description\": \"Send a message to a teammate's inbox.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}, \"msg_type\": {\"type\": \"string\", \"enum\": list(VALID_MSG_TYPES)}}, \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"read_inbox\", \"description\": \"Read and drain the lead's inbox.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"broadcast\", \"description\": \"Send a message to all teammates.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}}, \"required\": [\"content\"]}},\n]\n\n\ndef agent_loop(messages: list):\n    while True:\n        inbox = BUS.read_inbox(\"lead\")\n        if inbox:\n            messages.append({\n                \"role\": \"user\",\n                \"content\": f\"<inbox>{json.dumps(inbox, indent=2)}</inbox>\",\n            })\n            messages.append({\n                \"role\": \"assistant\",\n                \"content\": \"Noted inbox messages.\",\n            })\n        response = client.messages.create(\n            model=MODEL,\n            system=SYSTEM,\n            messages=messages,\n            tools=TOOLS,\n            max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": str(output),\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms09 >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        if query.strip() == \"/team\":\n            print(TEAM.list_all())\n            continue\n        if query.strip() == \"/inbox\":\n            print(json.dumps(BUS.read_inbox(\"lead\"), indent=2))\n            continue\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s10_team_protocols.py",
    "content": "#!/usr/bin/env python3\n# Harness: protocols -- structured handshakes between models.\n\"\"\"\ns10_team_protocols.py - Team Protocols\n\nShutdown protocol and plan approval protocol, both using the same\nrequest_id correlation pattern. Builds on s09's team messaging.\n\n    Shutdown FSM: pending -> approved | rejected\n\n    Lead                              Teammate\n    +---------------------+          +---------------------+\n    | shutdown_request     |          |                     |\n    | {                    | -------> | receives request    |\n    |   request_id: abc    |          | decides: approve?   |\n    | }                    |          |                     |\n    +---------------------+          +---------------------+\n                                             |\n    +---------------------+          +-------v-------------+\n    | shutdown_response    | <------- | shutdown_response   |\n    | {                    |          | {                   |\n    |   request_id: abc    |          |   request_id: abc   |\n    |   approve: true      |          |   approve: true     |\n    | }                    |          | }                   |\n    +---------------------+          +---------------------+\n            |\n            v\n    status -> \"shutdown\", thread stops\n\n    Plan approval FSM: pending -> approved | rejected\n\n    Teammate                          Lead\n    +---------------------+          +---------------------+\n    | plan_approval        |          |                     |\n    | submit: {plan:\"...\"}| -------> | reviews plan text   |\n    +---------------------+          | approve/reject?     |\n                                     +---------------------+\n                                             |\n    +---------------------+          +-------v-------------+\n    | plan_approval_resp   | <------- | plan_approval       |\n    | {approve: true}      |          | review: {req_id,    |\n    +---------------------+          |   approve: true}     |\n                                     +---------------------+\n\n    Trackers: {request_id: {\"target|from\": name, \"status\": \"pending|...\"}}\n\nKey insight: \"Same request_id correlation pattern, two domains.\"\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport threading\nimport time\nimport uuid\nfrom pathlib import Path\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\"]\nTEAM_DIR = WORKDIR / \".team\"\nINBOX_DIR = TEAM_DIR / \"inbox\"\n\nSYSTEM = f\"You are a team lead at {WORKDIR}. Manage teammates with shutdown and plan approval protocols.\"\n\nVALID_MSG_TYPES = {\n    \"message\",\n    \"broadcast\",\n    \"shutdown_request\",\n    \"shutdown_response\",\n    \"plan_approval_response\",\n}\n\n# -- Request trackers: correlate by request_id --\nshutdown_requests = {}\nplan_requests = {}\n_tracker_lock = threading.Lock()\n\n\n# -- MessageBus: JSONL inbox per teammate --\nclass MessageBus:\n    def __init__(self, inbox_dir: Path):\n        self.dir = inbox_dir\n        self.dir.mkdir(parents=True, exist_ok=True)\n\n    def send(self, sender: str, to: str, content: str,\n             msg_type: str = \"message\", extra: dict = None) -> str:\n        if msg_type not in VALID_MSG_TYPES:\n            return f\"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}\"\n        msg = {\n            \"type\": msg_type,\n            \"from\": sender,\n            \"content\": content,\n            \"timestamp\": time.time(),\n        }\n        if extra:\n            msg.update(extra)\n        inbox_path = self.dir / f\"{to}.jsonl\"\n        with open(inbox_path, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        return f\"Sent {msg_type} to {to}\"\n\n    def read_inbox(self, name: str) -> list:\n        inbox_path = self.dir / f\"{name}.jsonl\"\n        if not inbox_path.exists():\n            return []\n        messages = []\n        for line in inbox_path.read_text().strip().splitlines():\n            if line:\n                messages.append(json.loads(line))\n        inbox_path.write_text(\"\")\n        return messages\n\n    def broadcast(self, sender: str, content: str, teammates: list) -> str:\n        count = 0\n        for name in teammates:\n            if name != sender:\n                self.send(sender, name, content, \"broadcast\")\n                count += 1\n        return f\"Broadcast to {count} teammates\"\n\n\nBUS = MessageBus(INBOX_DIR)\n\n\n# -- TeammateManager with shutdown + plan approval --\nclass TeammateManager:\n    def __init__(self, team_dir: Path):\n        self.dir = team_dir\n        self.dir.mkdir(exist_ok=True)\n        self.config_path = self.dir / \"config.json\"\n        self.config = self._load_config()\n        self.threads = {}\n\n    def _load_config(self) -> dict:\n        if self.config_path.exists():\n            return json.loads(self.config_path.read_text())\n        return {\"team_name\": \"default\", \"members\": []}\n\n    def _save_config(self):\n        self.config_path.write_text(json.dumps(self.config, indent=2))\n\n    def _find_member(self, name: str) -> dict:\n        for m in self.config[\"members\"]:\n            if m[\"name\"] == name:\n                return m\n        return None\n\n    def spawn(self, name: str, role: str, prompt: str) -> str:\n        member = self._find_member(name)\n        if member:\n            if member[\"status\"] not in (\"idle\", \"shutdown\"):\n                return f\"Error: '{name}' is currently {member['status']}\"\n            member[\"status\"] = \"working\"\n            member[\"role\"] = role\n        else:\n            member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n            self.config[\"members\"].append(member)\n        self._save_config()\n        thread = threading.Thread(\n            target=self._teammate_loop,\n            args=(name, role, prompt),\n            daemon=True,\n        )\n        self.threads[name] = thread\n        thread.start()\n        return f\"Spawned '{name}' (role: {role})\"\n\n    def _teammate_loop(self, name: str, role: str, prompt: str):\n        sys_prompt = (\n            f\"You are '{name}', role: {role}, at {WORKDIR}. \"\n            f\"Submit plans via plan_approval before major work. \"\n            f\"Respond to shutdown_request with shutdown_response.\"\n        )\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        tools = self._teammate_tools()\n        should_exit = False\n        for _ in range(50):\n            inbox = BUS.read_inbox(name)\n            for msg in inbox:\n                messages.append({\"role\": \"user\", \"content\": json.dumps(msg)})\n            if should_exit:\n                break\n            try:\n                response = client.messages.create(\n                    model=MODEL,\n                    system=sys_prompt,\n                    messages=messages,\n                    tools=tools,\n                    max_tokens=8000,\n                )\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                    output = self._exec(name, block.name, block.input)\n                    print(f\"  [{name}] {block.name}: {str(output)[:120]}\")\n                    results.append({\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": block.id,\n                        \"content\": str(output),\n                    })\n                    if block.name == \"shutdown_response\" and block.input.get(\"approve\"):\n                        should_exit = True\n            messages.append({\"role\": \"user\", \"content\": results})\n        member = self._find_member(name)\n        if member:\n            member[\"status\"] = \"shutdown\" if should_exit else \"idle\"\n            self._save_config()\n\n    def _exec(self, sender: str, tool_name: str, args: dict) -> str:\n        # these base tools are unchanged from s02\n        if tool_name == \"bash\":\n            return _run_bash(args[\"command\"])\n        if tool_name == \"read_file\":\n            return _run_read(args[\"path\"])\n        if tool_name == \"write_file\":\n            return _run_write(args[\"path\"], args[\"content\"])\n        if tool_name == \"edit_file\":\n            return _run_edit(args[\"path\"], args[\"old_text\"], args[\"new_text\"])\n        if tool_name == \"send_message\":\n            return BUS.send(sender, args[\"to\"], args[\"content\"], args.get(\"msg_type\", \"message\"))\n        if tool_name == \"read_inbox\":\n            return json.dumps(BUS.read_inbox(sender), indent=2)\n        if tool_name == \"shutdown_response\":\n            req_id = args[\"request_id\"]\n            approve = args[\"approve\"]\n            with _tracker_lock:\n                if req_id in shutdown_requests:\n                    shutdown_requests[req_id][\"status\"] = \"approved\" if approve else \"rejected\"\n            BUS.send(\n                sender, \"lead\", args.get(\"reason\", \"\"),\n                \"shutdown_response\", {\"request_id\": req_id, \"approve\": approve},\n            )\n            return f\"Shutdown {'approved' if approve else 'rejected'}\"\n        if tool_name == \"plan_approval\":\n            plan_text = args.get(\"plan\", \"\")\n            req_id = str(uuid.uuid4())[:8]\n            with _tracker_lock:\n                plan_requests[req_id] = {\"from\": sender, \"plan\": plan_text, \"status\": \"pending\"}\n            BUS.send(\n                sender, \"lead\", plan_text, \"plan_approval_response\",\n                {\"request_id\": req_id, \"plan\": plan_text},\n            )\n            return f\"Plan submitted (request_id={req_id}). Waiting for lead approval.\"\n        return f\"Unknown tool: {tool_name}\"\n\n    def _teammate_tools(self) -> list:\n        # these base tools are unchanged from s02\n        return [\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 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 file.\",\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\": \"send_message\", \"description\": \"Send message to a teammate.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}, \"msg_type\": {\"type\": \"string\", \"enum\": list(VALID_MSG_TYPES)}}, \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"read_inbox\", \"description\": \"Read and drain your inbox.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n            {\"name\": \"shutdown_response\", \"description\": \"Respond to a shutdown request. Approve to shut down, reject to keep working.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"request_id\": {\"type\": \"string\"}, \"approve\": {\"type\": \"boolean\"}, \"reason\": {\"type\": \"string\"}}, \"required\": [\"request_id\", \"approve\"]}},\n            {\"name\": \"plan_approval\", \"description\": \"Submit a plan for lead approval. Provide plan text.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"plan\": {\"type\": \"string\"}}, \"required\": [\"plan\"]}},\n        ]\n\n    def list_all(self) -> str:\n        if not self.config[\"members\"]:\n            return \"No teammates.\"\n        lines = [f\"Team: {self.config['team_name']}\"]\n        for m in self.config[\"members\"]:\n            lines.append(f\"  {m['name']} ({m['role']}): {m['status']}\")\n        return \"\\n\".join(lines)\n\n    def member_names(self) -> list:\n        return [m[\"name\"] for m in self.config[\"members\"]]\n\n\nTEAM = TeammateManager(TEAM_DIR)\n\n\n# -- Base tool implementations (these base tools are unchanged from s02) --\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    dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\"]\n    if any(d in command for d in dangerous):\n        return \"Error: Dangerous command blocked\"\n    try:\n        r = subprocess.run(\n            command, shell=True, cwd=WORKDIR,\n            capture_output=True, text=True, timeout=120,\n        )\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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\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\"\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        fp = _safe_path(path)\n        c = fp.read_text()\n        if old_text not in c:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(c.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# -- Lead-specific protocol handlers --\ndef handle_shutdown_request(teammate: str) -> str:\n    req_id = str(uuid.uuid4())[:8]\n    with _tracker_lock:\n        shutdown_requests[req_id] = {\"target\": teammate, \"status\": \"pending\"}\n    BUS.send(\n        \"lead\", teammate, \"Please shut down gracefully.\",\n        \"shutdown_request\", {\"request_id\": req_id},\n    )\n    return f\"Shutdown request {req_id} sent to '{teammate}' (status: pending)\"\n\n\ndef handle_plan_review(request_id: str, approve: bool, feedback: str = \"\") -> str:\n    with _tracker_lock:\n        req = plan_requests.get(request_id)\n    if not req:\n        return f\"Error: Unknown plan request_id '{request_id}'\"\n    with _tracker_lock:\n        req[\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(\n        \"lead\", req[\"from\"], feedback, \"plan_approval_response\",\n        {\"request_id\": request_id, \"approve\": approve, \"feedback\": feedback},\n    )\n    return f\"Plan {req['status']} for '{req['from']}'\"\n\n\ndef _check_shutdown_status(request_id: str) -> str:\n    with _tracker_lock:\n        return json.dumps(shutdown_requests.get(request_id, {\"error\": \"not found\"}))\n\n\n# -- Lead tool dispatch (12 tools) --\nTOOL_HANDLERS = {\n    \"bash\":              lambda **kw: _run_bash(kw[\"command\"]),\n    \"read_file\":         lambda **kw: _run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\":        lambda **kw: _run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":         lambda **kw: _run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"spawn_teammate\":    lambda **kw: TEAM.spawn(kw[\"name\"], kw[\"role\"], kw[\"prompt\"]),\n    \"list_teammates\":    lambda **kw: TEAM.list_all(),\n    \"send_message\":      lambda **kw: BUS.send(\"lead\", kw[\"to\"], kw[\"content\"], kw.get(\"msg_type\", \"message\")),\n    \"read_inbox\":        lambda **kw: json.dumps(BUS.read_inbox(\"lead\"), indent=2),\n    \"broadcast\":         lambda **kw: BUS.broadcast(\"lead\", kw[\"content\"], TEAM.member_names()),\n    \"shutdown_request\":  lambda **kw: handle_shutdown_request(kw[\"teammate\"]),\n    \"shutdown_response\": lambda **kw: _check_shutdown_status(kw.get(\"request_id\", \"\")),\n    \"plan_approval\":     lambda **kw: handle_plan_review(kw[\"request_id\"], kw[\"approve\"], kw.get(\"feedback\", \"\")),\n}\n\n# these base tools are unchanged from s02\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 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 file.\",\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\": \"spawn_teammate\", \"description\": \"Spawn a persistent teammate.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}, \"role\": {\"type\": \"string\"}, \"prompt\": {\"type\": \"string\"}}, \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"list_teammates\", \"description\": \"List all teammates.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"send_message\", \"description\": \"Send a message to a teammate.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}, \"msg_type\": {\"type\": \"string\", \"enum\": list(VALID_MSG_TYPES)}}, \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"read_inbox\", \"description\": \"Read and drain the lead's inbox.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"broadcast\", \"description\": \"Send a message to all teammates.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}}, \"required\": [\"content\"]}},\n    {\"name\": \"shutdown_request\", \"description\": \"Request a teammate to shut down gracefully. Returns a request_id for tracking.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"teammate\": {\"type\": \"string\"}}, \"required\": [\"teammate\"]}},\n    {\"name\": \"shutdown_response\", \"description\": \"Check the status of a shutdown request by request_id.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"request_id\": {\"type\": \"string\"}}, \"required\": [\"request_id\"]}},\n    {\"name\": \"plan_approval\", \"description\": \"Approve or reject a teammate's plan. Provide request_id + approve + optional feedback.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"request_id\": {\"type\": \"string\"}, \"approve\": {\"type\": \"boolean\"}, \"feedback\": {\"type\": \"string\"}}, \"required\": [\"request_id\", \"approve\"]}},\n]\n\n\ndef agent_loop(messages: list):\n    while True:\n        inbox = BUS.read_inbox(\"lead\")\n        if inbox:\n            messages.append({\n                \"role\": \"user\",\n                \"content\": f\"<inbox>{json.dumps(inbox, indent=2)}</inbox>\",\n            })\n            messages.append({\n                \"role\": \"assistant\",\n                \"content\": \"Noted inbox messages.\",\n            })\n        response = client.messages.create(\n            model=MODEL,\n            system=SYSTEM,\n            messages=messages,\n            tools=TOOLS,\n            max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": str(output),\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    history = []\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        if query.strip() == \"/team\":\n            print(TEAM.list_all())\n            continue\n        if query.strip() == \"/inbox\":\n            print(json.dumps(BUS.read_inbox(\"lead\"), indent=2))\n            continue\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s11_autonomous_agents.py",
    "content": "#!/usr/bin/env python3\n# Harness: autonomy -- models that find work without being told.\n\"\"\"\ns11_autonomous_agents.py - Autonomous Agents\n\nIdle cycle with task board polling, auto-claiming unclaimed tasks, and\nidentity re-injection after context compression. Builds on s10's protocols.\n\n    Teammate lifecycle:\n    +-------+\n    | spawn |\n    +---+---+\n        |\n        v\n    +-------+  tool_use    +-------+\n    | WORK  | <----------- |  LLM  |\n    +---+---+              +-------+\n        |\n        | stop_reason != tool_use\n        v\n    +--------+\n    | IDLE   | poll every 5s for up to 60s\n    +---+----+\n        |\n        +---> check inbox -> message? -> resume WORK\n        |\n        +---> scan .tasks/ -> unclaimed? -> claim -> resume WORK\n        |\n        +---> timeout (60s) -> shutdown\n\n    Identity re-injection after compression:\n    messages = [identity_block, ...remaining...]\n    \"You are 'coder', role: backend, team: my-team\"\n\nKey insight: \"The agent finds work itself.\"\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport threading\nimport time\nimport uuid\nfrom pathlib import Path\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\"]\nTEAM_DIR = WORKDIR / \".team\"\nINBOX_DIR = TEAM_DIR / \"inbox\"\nTASKS_DIR = WORKDIR / \".tasks\"\n\nPOLL_INTERVAL = 5\nIDLE_TIMEOUT = 60\n\nSYSTEM = f\"You are a team lead at {WORKDIR}. Teammates are autonomous -- they find work themselves.\"\n\nVALID_MSG_TYPES = {\n    \"message\",\n    \"broadcast\",\n    \"shutdown_request\",\n    \"shutdown_response\",\n    \"plan_approval_response\",\n}\n\n# -- Request trackers --\nshutdown_requests = {}\nplan_requests = {}\n_tracker_lock = threading.Lock()\n_claim_lock = threading.Lock()\n\n\n# -- MessageBus: JSONL inbox per teammate --\nclass MessageBus:\n    def __init__(self, inbox_dir: Path):\n        self.dir = inbox_dir\n        self.dir.mkdir(parents=True, exist_ok=True)\n\n    def send(self, sender: str, to: str, content: str,\n             msg_type: str = \"message\", extra: dict = None) -> str:\n        if msg_type not in VALID_MSG_TYPES:\n            return f\"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}\"\n        msg = {\n            \"type\": msg_type,\n            \"from\": sender,\n            \"content\": content,\n            \"timestamp\": time.time(),\n        }\n        if extra:\n            msg.update(extra)\n        inbox_path = self.dir / f\"{to}.jsonl\"\n        with open(inbox_path, \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        return f\"Sent {msg_type} to {to}\"\n\n    def read_inbox(self, name: str) -> list:\n        inbox_path = self.dir / f\"{name}.jsonl\"\n        if not inbox_path.exists():\n            return []\n        messages = []\n        for line in inbox_path.read_text().strip().splitlines():\n            if line:\n                messages.append(json.loads(line))\n        inbox_path.write_text(\"\")\n        return messages\n\n    def broadcast(self, sender: str, content: str, teammates: list) -> str:\n        count = 0\n        for name in teammates:\n            if name != sender:\n                self.send(sender, name, content, \"broadcast\")\n                count += 1\n        return f\"Broadcast to {count} teammates\"\n\n\nBUS = MessageBus(INBOX_DIR)\n\n\n# -- Task board scanning --\ndef scan_unclaimed_tasks() -> list:\n    TASKS_DIR.mkdir(exist_ok=True)\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 not task.get(\"blockedBy\")):\n            unclaimed.append(task)\n    return unclaimed\n\n\ndef claim_task(task_id: int, owner: str) -> str:\n    with _claim_lock:\n        path = TASKS_DIR / f\"task_{task_id}.json\"\n        if not path.exists():\n            return f\"Error: Task {task_id} not found\"\n        task = json.loads(path.read_text())\n        task[\"owner\"] = owner\n        task[\"status\"] = \"in_progress\"\n        path.write_text(json.dumps(task, indent=2))\n    return f\"Claimed task #{task_id} for {owner}\"\n\n\n# -- Identity re-injection after compression --\ndef make_identity_block(name: str, role: str, team_name: str) -> dict:\n    return {\n        \"role\": \"user\",\n        \"content\": f\"<identity>You are '{name}', role: {role}, team: {team_name}. Continue your work.</identity>\",\n    }\n\n\n# -- Autonomous TeammateManager --\nclass TeammateManager:\n    def __init__(self, team_dir: Path):\n        self.dir = team_dir\n        self.dir.mkdir(exist_ok=True)\n        self.config_path = self.dir / \"config.json\"\n        self.config = self._load_config()\n        self.threads = {}\n\n    def _load_config(self) -> dict:\n        if self.config_path.exists():\n            return json.loads(self.config_path.read_text())\n        return {\"team_name\": \"default\", \"members\": []}\n\n    def _save_config(self):\n        self.config_path.write_text(json.dumps(self.config, indent=2))\n\n    def _find_member(self, name: str) -> dict:\n        for m in self.config[\"members\"]:\n            if m[\"name\"] == name:\n                return m\n        return None\n\n    def _set_status(self, name: str, status: str):\n        member = self._find_member(name)\n        if member:\n            member[\"status\"] = status\n            self._save_config()\n\n    def spawn(self, name: str, role: str, prompt: str) -> str:\n        member = self._find_member(name)\n        if member:\n            if member[\"status\"] not in (\"idle\", \"shutdown\"):\n                return f\"Error: '{name}' is currently {member['status']}\"\n            member[\"status\"] = \"working\"\n            member[\"role\"] = role\n        else:\n            member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n            self.config[\"members\"].append(member)\n        self._save_config()\n        thread = threading.Thread(\n            target=self._loop,\n            args=(name, role, prompt),\n            daemon=True,\n        )\n        self.threads[name] = thread\n        thread.start()\n        return f\"Spawned '{name}' (role: {role})\"\n\n    def _loop(self, name: str, role: str, prompt: str):\n        team_name = self.config[\"team_name\"]\n        sys_prompt = (\n            f\"You are '{name}', role: {role}, team: {team_name}, at {WORKDIR}. \"\n            f\"Use idle tool when you have no more work. You will auto-claim new tasks.\"\n        )\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        tools = self._teammate_tools()\n\n        while True:\n            # -- WORK PHASE: standard agent loop --\n            for _ in range(50):\n                inbox = BUS.read_inbox(name)\n                for msg in inbox:\n                    if msg.get(\"type\") == \"shutdown_request\":\n                        self._set_status(name, \"shutdown\")\n                        return\n                    messages.append({\"role\": \"user\", \"content\": json.dumps(msg)})\n                try:\n                    response = client.messages.create(\n                        model=MODEL,\n                        system=sys_prompt,\n                        messages=messages,\n                        tools=tools,\n                        max_tokens=8000,\n                    )\n                except Exception:\n                    self._set_status(name, \"idle\")\n                    return\n                messages.append({\"role\": \"assistant\", \"content\": response.content})\n                if response.stop_reason != \"tool_use\":\n                    break\n                results = []\n                idle_requested = False\n                for block in response.content:\n                    if block.type == \"tool_use\":\n                        if block.name == \"idle\":\n                            idle_requested = True\n                            output = \"Entering idle phase. Will poll for new tasks.\"\n                        else:\n                            output = self._exec(name, block.name, block.input)\n                        print(f\"  [{name}] {block.name}: {str(output)[:120]}\")\n                        results.append({\n                            \"type\": \"tool_result\",\n                            \"tool_use_id\": block.id,\n                            \"content\": str(output),\n                        })\n                messages.append({\"role\": \"user\", \"content\": results})\n                if idle_requested:\n                    break\n\n            # -- IDLE PHASE: poll for inbox messages and unclaimed tasks --\n            self._set_status(name, \"idle\")\n            resume = False\n            polls = IDLE_TIMEOUT // max(POLL_INTERVAL, 1)\n            for _ in range(polls):\n                time.sleep(POLL_INTERVAL)\n                inbox = BUS.read_inbox(name)\n                if inbox:\n                    for msg in inbox:\n                        if msg.get(\"type\") == \"shutdown_request\":\n                            self._set_status(name, \"shutdown\")\n                            return\n                        messages.append({\"role\": \"user\", \"content\": json.dumps(msg)})\n                    resume = True\n                    break\n                unclaimed = scan_unclaimed_tasks()\n                if unclaimed:\n                    task = unclaimed[0]\n                    claim_task(task[\"id\"], name)\n                    task_prompt = (\n                        f\"<auto-claimed>Task #{task['id']}: {task['subject']}\\n\"\n                        f\"{task.get('description', '')}</auto-claimed>\"\n                    )\n                    if len(messages) <= 3:\n                        messages.insert(0, make_identity_block(name, role, team_name))\n                        messages.insert(1, {\"role\": \"assistant\", \"content\": f\"I am {name}. Continuing.\"})\n                    messages.append({\"role\": \"user\", \"content\": task_prompt})\n                    messages.append({\"role\": \"assistant\", \"content\": f\"Claimed task #{task['id']}. Working on it.\"})\n                    resume = True\n                    break\n\n            if not resume:\n                self._set_status(name, \"shutdown\")\n                return\n            self._set_status(name, \"working\")\n\n    def _exec(self, sender: str, tool_name: str, args: dict) -> str:\n        # these base tools are unchanged from s02\n        if tool_name == \"bash\":\n            return _run_bash(args[\"command\"])\n        if tool_name == \"read_file\":\n            return _run_read(args[\"path\"])\n        if tool_name == \"write_file\":\n            return _run_write(args[\"path\"], args[\"content\"])\n        if tool_name == \"edit_file\":\n            return _run_edit(args[\"path\"], args[\"old_text\"], args[\"new_text\"])\n        if tool_name == \"send_message\":\n            return BUS.send(sender, args[\"to\"], args[\"content\"], args.get(\"msg_type\", \"message\"))\n        if tool_name == \"read_inbox\":\n            return json.dumps(BUS.read_inbox(sender), indent=2)\n        if tool_name == \"shutdown_response\":\n            req_id = args[\"request_id\"]\n            with _tracker_lock:\n                if req_id in shutdown_requests:\n                    shutdown_requests[req_id][\"status\"] = \"approved\" if args[\"approve\"] else \"rejected\"\n            BUS.send(\n                sender, \"lead\", args.get(\"reason\", \"\"),\n                \"shutdown_response\", {\"request_id\": req_id, \"approve\": args[\"approve\"]},\n            )\n            return f\"Shutdown {'approved' if args['approve'] else 'rejected'}\"\n        if tool_name == \"plan_approval\":\n            plan_text = args.get(\"plan\", \"\")\n            req_id = str(uuid.uuid4())[:8]\n            with _tracker_lock:\n                plan_requests[req_id] = {\"from\": sender, \"plan\": plan_text, \"status\": \"pending\"}\n            BUS.send(\n                sender, \"lead\", plan_text, \"plan_approval_response\",\n                {\"request_id\": req_id, \"plan\": plan_text},\n            )\n            return f\"Plan submitted (request_id={req_id}). Waiting for approval.\"\n        if tool_name == \"claim_task\":\n            return claim_task(args[\"task_id\"], sender)\n        return f\"Unknown tool: {tool_name}\"\n\n    def _teammate_tools(self) -> list:\n        # these base tools are unchanged from s02\n        return [\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 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 file.\",\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\": \"send_message\", \"description\": \"Send message to a teammate.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}, \"msg_type\": {\"type\": \"string\", \"enum\": list(VALID_MSG_TYPES)}}, \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"read_inbox\", \"description\": \"Read and drain your inbox.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n            {\"name\": \"shutdown_response\", \"description\": \"Respond to a shutdown request.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"request_id\": {\"type\": \"string\"}, \"approve\": {\"type\": \"boolean\"}, \"reason\": {\"type\": \"string\"}}, \"required\": [\"request_id\", \"approve\"]}},\n            {\"name\": \"plan_approval\", \"description\": \"Submit a plan for lead approval.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"plan\": {\"type\": \"string\"}}, \"required\": [\"plan\"]}},\n            {\"name\": \"idle\", \"description\": \"Signal that you have no more work. Enters idle polling phase.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n            {\"name\": \"claim_task\", \"description\": \"Claim a task from the task board by ID.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}}, \"required\": [\"task_id\"]}},\n        ]\n\n    def list_all(self) -> str:\n        if not self.config[\"members\"]:\n            return \"No teammates.\"\n        lines = [f\"Team: {self.config['team_name']}\"]\n        for m in self.config[\"members\"]:\n            lines.append(f\"  {m['name']} ({m['role']}): {m['status']}\")\n        return \"\\n\".join(lines)\n\n    def member_names(self) -> list:\n        return [m[\"name\"] for m in self.config[\"members\"]]\n\n\nTEAM = TeammateManager(TEAM_DIR)\n\n\n# -- Base tool implementations (these base tools are unchanged from s02) --\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    dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\"]\n    if any(d in command for d in dangerous):\n        return \"Error: Dangerous command blocked\"\n    try:\n        r = subprocess.run(\n            command, shell=True, cwd=WORKDIR,\n            capture_output=True, text=True, timeout=120,\n        )\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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\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\"\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        fp = _safe_path(path)\n        c = fp.read_text()\n        if old_text not in c:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(c.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# -- Lead-specific protocol handlers --\ndef handle_shutdown_request(teammate: str) -> str:\n    req_id = str(uuid.uuid4())[:8]\n    with _tracker_lock:\n        shutdown_requests[req_id] = {\"target\": teammate, \"status\": \"pending\"}\n    BUS.send(\n        \"lead\", teammate, \"Please shut down gracefully.\",\n        \"shutdown_request\", {\"request_id\": req_id},\n    )\n    return f\"Shutdown request {req_id} sent to '{teammate}'\"\n\n\ndef handle_plan_review(request_id: str, approve: bool, feedback: str = \"\") -> str:\n    with _tracker_lock:\n        req = plan_requests.get(request_id)\n    if not req:\n        return f\"Error: Unknown plan request_id '{request_id}'\"\n    with _tracker_lock:\n        req[\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(\n        \"lead\", req[\"from\"], feedback, \"plan_approval_response\",\n        {\"request_id\": request_id, \"approve\": approve, \"feedback\": feedback},\n    )\n    return f\"Plan {req['status']} for '{req['from']}'\"\n\n\ndef _check_shutdown_status(request_id: str) -> str:\n    with _tracker_lock:\n        return json.dumps(shutdown_requests.get(request_id, {\"error\": \"not found\"}))\n\n\n# -- Lead tool dispatch (14 tools) --\nTOOL_HANDLERS = {\n    \"bash\":              lambda **kw: _run_bash(kw[\"command\"]),\n    \"read_file\":         lambda **kw: _run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\":        lambda **kw: _run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":         lambda **kw: _run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"spawn_teammate\":    lambda **kw: TEAM.spawn(kw[\"name\"], kw[\"role\"], kw[\"prompt\"]),\n    \"list_teammates\":    lambda **kw: TEAM.list_all(),\n    \"send_message\":      lambda **kw: BUS.send(\"lead\", kw[\"to\"], kw[\"content\"], kw.get(\"msg_type\", \"message\")),\n    \"read_inbox\":        lambda **kw: json.dumps(BUS.read_inbox(\"lead\"), indent=2),\n    \"broadcast\":         lambda **kw: BUS.broadcast(\"lead\", kw[\"content\"], TEAM.member_names()),\n    \"shutdown_request\":  lambda **kw: handle_shutdown_request(kw[\"teammate\"]),\n    \"shutdown_response\": lambda **kw: _check_shutdown_status(kw.get(\"request_id\", \"\")),\n    \"plan_approval\":     lambda **kw: handle_plan_review(kw[\"request_id\"], kw[\"approve\"], kw.get(\"feedback\", \"\")),\n    \"idle\":              lambda **kw: \"Lead does not idle.\",\n    \"claim_task\":        lambda **kw: claim_task(kw[\"task_id\"], \"lead\"),\n}\n\n# these base tools are unchanged from s02\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 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 file.\",\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\": \"spawn_teammate\", \"description\": \"Spawn an autonomous teammate.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}, \"role\": {\"type\": \"string\"}, \"prompt\": {\"type\": \"string\"}}, \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"list_teammates\", \"description\": \"List all teammates.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"send_message\", \"description\": \"Send a message to a teammate.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}, \"msg_type\": {\"type\": \"string\", \"enum\": list(VALID_MSG_TYPES)}}, \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"read_inbox\", \"description\": \"Read and drain the lead's inbox.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"broadcast\", \"description\": \"Send a message to all teammates.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}}, \"required\": [\"content\"]}},\n    {\"name\": \"shutdown_request\", \"description\": \"Request a teammate to shut down.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"teammate\": {\"type\": \"string\"}}, \"required\": [\"teammate\"]}},\n    {\"name\": \"shutdown_response\", \"description\": \"Check shutdown request status.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"request_id\": {\"type\": \"string\"}}, \"required\": [\"request_id\"]}},\n    {\"name\": \"plan_approval\", \"description\": \"Approve or reject a teammate's plan.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"request_id\": {\"type\": \"string\"}, \"approve\": {\"type\": \"boolean\"}, \"feedback\": {\"type\": \"string\"}}, \"required\": [\"request_id\", \"approve\"]}},\n    {\"name\": \"idle\", \"description\": \"Enter idle state (for lead -- rarely used).\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"claim_task\", \"description\": \"Claim a task from the board by ID.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}}, \"required\": [\"task_id\"]}},\n]\n\n\ndef agent_loop(messages: list):\n    while True:\n        inbox = BUS.read_inbox(\"lead\")\n        if inbox:\n            messages.append({\n                \"role\": \"user\",\n                \"content\": f\"<inbox>{json.dumps(inbox, indent=2)}</inbox>\",\n            })\n            messages.append({\n                \"role\": \"assistant\",\n                \"content\": \"Noted inbox messages.\",\n            })\n        response = client.messages.create(\n            model=MODEL,\n            system=SYSTEM,\n            messages=messages,\n            tools=TOOLS,\n            max_tokens=8000,\n        )\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n        if response.stop_reason != \"tool_use\":\n            return\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": str(output),\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    history = []\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        if query.strip() == \"/team\":\n            print(TEAM.list_all())\n            continue\n        if query.strip() == \"/inbox\":\n            print(json.dumps(BUS.read_inbox(\"lead\"), indent=2))\n            continue\n        if query.strip() == \"/tasks\":\n            TASKS_DIR.mkdir(exist_ok=True)\n            for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n                t = json.loads(f.read_text())\n                marker = {\"pending\": \"[ ]\", \"in_progress\": \"[>]\", \"completed\": \"[x]\"}.get(t[\"status\"], \"[?]\")\n                owner = f\" @{t['owner']}\" if t.get(\"owner\") else \"\"\n                print(f\"  {marker} #{t['id']}: {t['subject']}{owner}\")\n            continue\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s12_worktree_task_isolation.py",
    "content": "#!/usr/bin/env python3\n# Harness: directory isolation -- parallel execution lanes that never collide.\n\"\"\"\ns12_worktree_task_isolation.py - Worktree + Task Isolation\n\nDirectory-level isolation for parallel task execution.\nTasks are the control plane and worktrees are the execution plane.\n\n    .tasks/task_12.json\n      {\n        \"id\": 12,\n        \"subject\": \"Implement auth refactor\",\n        \"status\": \"in_progress\",\n        \"worktree\": \"auth-refactor\"\n      }\n\n    .worktrees/index.json\n      {\n        \"worktrees\": [\n          {\n            \"name\": \"auth-refactor\",\n            \"path\": \".../.worktrees/auth-refactor\",\n            \"branch\": \"wt/auth-refactor\",\n            \"task_id\": 12,\n            \"status\": \"active\"\n          }\n        ]\n      }\n\nKey insight: \"Isolate by directory, coordinate by task ID.\"\n\"\"\"\n\nimport json\nimport os\nimport re\nimport subprocess\nimport time\nfrom pathlib import Path\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\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\n\ndef detect_repo_root(cwd: Path) -> Path | None:\n    \"\"\"Return git repo root if cwd is inside a repo, else None.\"\"\"\n    try:\n        r = subprocess.run(\n            [\"git\", \"rev-parse\", \"--show-toplevel\"],\n            cwd=cwd,\n            capture_output=True,\n            text=True,\n            timeout=10,\n        )\n        if r.returncode != 0:\n            return None\n        root = Path(r.stdout.strip())\n        return root if root.exists() else None\n    except Exception:\n        return None\n\n\nREPO_ROOT = detect_repo_root(WORKDIR) or WORKDIR\n\nSYSTEM = (\n    f\"You are a coding agent at {WORKDIR}. \"\n    \"Use task + worktree tools for multi-task work. \"\n    \"For parallel or risky changes: create tasks, allocate worktree lanes, \"\n    \"run commands in those lanes, then choose keep/remove for closeout. \"\n    \"Use worktree_events when you need lifecycle visibility.\"\n)\n\n\n# -- EventBus: append-only lifecycle events for observability --\nclass EventBus:\n    def __init__(self, event_log_path: Path):\n        self.path = event_log_path\n        self.path.parent.mkdir(parents=True, exist_ok=True)\n        if not self.path.exists():\n            self.path.write_text(\"\")\n\n    def emit(\n        self,\n        event: str,\n        task: dict | None = None,\n        worktree: dict | None = None,\n        error: str | None = None,\n    ):\n        payload = {\n            \"event\": event,\n            \"ts\": time.time(),\n            \"task\": task or {},\n            \"worktree\": worktree or {},\n        }\n        if error:\n            payload[\"error\"] = error\n        with self.path.open(\"a\", encoding=\"utf-8\") as f:\n            f.write(json.dumps(payload) + \"\\n\")\n\n    def list_recent(self, limit: int = 20) -> str:\n        n = max(1, min(int(limit or 20), 200))\n        lines = self.path.read_text(encoding=\"utf-8\").splitlines()\n        recent = lines[-n:]\n        items = []\n        for line in recent:\n            try:\n                items.append(json.loads(line))\n            except Exception:\n                items.append({\"event\": \"parse_error\", \"raw\": line})\n        return json.dumps(items, indent=2)\n\n\n# -- TaskManager: persistent task board with optional worktree binding --\nclass TaskManager:\n    def __init__(self, tasks_dir: Path):\n        self.dir = tasks_dir\n        self.dir.mkdir(parents=True, exist_ok=True)\n        self._next_id = self._max_id() + 1\n\n    def _max_id(self) -> int:\n        ids = []\n        for f in self.dir.glob(\"task_*.json\"):\n            try:\n                ids.append(int(f.stem.split(\"_\")[1]))\n            except Exception:\n                pass\n        return max(ids) if ids else 0\n\n    def _path(self, task_id: int) -> Path:\n        return self.dir / f\"task_{task_id}.json\"\n\n    def _load(self, task_id: int) -> dict:\n        path = self._path(task_id)\n        if not path.exists():\n            raise ValueError(f\"Task {task_id} not found\")\n        return json.loads(path.read_text())\n\n    def _save(self, task: dict):\n        self._path(task[\"id\"]).write_text(json.dumps(task, indent=2))\n\n    def create(self, subject: str, description: str = \"\") -> str:\n        task = {\n            \"id\": self._next_id,\n            \"subject\": subject,\n            \"description\": description,\n            \"status\": \"pending\",\n            \"owner\": \"\",\n            \"worktree\": \"\",\n            \"blockedBy\": [],\n            \"created_at\": time.time(),\n            \"updated_at\": time.time(),\n        }\n        self._save(task)\n        self._next_id += 1\n        return json.dumps(task, indent=2)\n\n    def get(self, task_id: int) -> str:\n        return json.dumps(self._load(task_id), indent=2)\n\n    def exists(self, task_id: int) -> bool:\n        return self._path(task_id).exists()\n\n    def update(self, task_id: int, status: str = None, owner: str = None) -> str:\n        task = self._load(task_id)\n        if status:\n            if status not in (\"pending\", \"in_progress\", \"completed\"):\n                raise ValueError(f\"Invalid status: {status}\")\n            task[\"status\"] = status\n        if owner is not None:\n            task[\"owner\"] = owner\n        task[\"updated_at\"] = time.time()\n        self._save(task)\n        return json.dumps(task, indent=2)\n\n    def bind_worktree(self, task_id: int, worktree: str, owner: str = \"\") -> str:\n        task = self._load(task_id)\n        task[\"worktree\"] = worktree\n        if owner:\n            task[\"owner\"] = owner\n        if task[\"status\"] == \"pending\":\n            task[\"status\"] = \"in_progress\"\n        task[\"updated_at\"] = time.time()\n        self._save(task)\n        return json.dumps(task, indent=2)\n\n    def unbind_worktree(self, task_id: int) -> str:\n        task = self._load(task_id)\n        task[\"worktree\"] = \"\"\n        task[\"updated_at\"] = time.time()\n        self._save(task)\n        return json.dumps(task, indent=2)\n\n    def list_all(self) -> str:\n        tasks = []\n        for f in sorted(self.dir.glob(\"task_*.json\")):\n            tasks.append(json.loads(f.read_text()))\n        if not tasks:\n            return \"No tasks.\"\n        lines = []\n        for t in tasks:\n            marker = {\n                \"pending\": \"[ ]\",\n                \"in_progress\": \"[>]\",\n                \"completed\": \"[x]\",\n            }.get(t[\"status\"], \"[?]\")\n            owner = f\" owner={t['owner']}\" if t.get(\"owner\") else \"\"\n            wt = f\" wt={t['worktree']}\" if t.get(\"worktree\") else \"\"\n            lines.append(f\"{marker} #{t['id']}: {t['subject']}{owner}{wt}\")\n        return \"\\n\".join(lines)\n\n\nTASKS = TaskManager(REPO_ROOT / \".tasks\")\nEVENTS = EventBus(REPO_ROOT / \".worktrees\" / \"events.jsonl\")\n\n\n# -- WorktreeManager: create/list/run/remove git worktrees + lifecycle index --\nclass WorktreeManager:\n    def __init__(self, repo_root: Path, tasks: TaskManager, events: EventBus):\n        self.repo_root = repo_root\n        self.tasks = tasks\n        self.events = events\n        self.dir = repo_root / \".worktrees\"\n        self.dir.mkdir(parents=True, exist_ok=True)\n        self.index_path = self.dir / \"index.json\"\n        if not self.index_path.exists():\n            self.index_path.write_text(json.dumps({\"worktrees\": []}, indent=2))\n        self.git_available = self._is_git_repo()\n\n    def _is_git_repo(self) -> bool:\n        try:\n            r = subprocess.run(\n                [\"git\", \"rev-parse\", \"--is-inside-work-tree\"],\n                cwd=self.repo_root,\n                capture_output=True,\n                text=True,\n                timeout=10,\n            )\n            return r.returncode == 0\n        except Exception:\n            return False\n\n    def _run_git(self, args: list[str]) -> str:\n        if not self.git_available:\n            raise RuntimeError(\"Not in a git repository. worktree tools require git.\")\n        r = subprocess.run(\n            [\"git\", *args],\n            cwd=self.repo_root,\n            capture_output=True,\n            text=True,\n            timeout=120,\n        )\n        if r.returncode != 0:\n            msg = (r.stdout + r.stderr).strip()\n            raise RuntimeError(msg or f\"git {' '.join(args)} failed\")\n        return (r.stdout + r.stderr).strip() or \"(no output)\"\n\n    def _load_index(self) -> dict:\n        return json.loads(self.index_path.read_text())\n\n    def _save_index(self, data: dict):\n        self.index_path.write_text(json.dumps(data, indent=2))\n\n    def _find(self, name: str) -> dict | None:\n        idx = self._load_index()\n        for wt in idx.get(\"worktrees\", []):\n            if wt.get(\"name\") == name:\n                return wt\n        return None\n\n    def _validate_name(self, name: str):\n        if not re.fullmatch(r\"[A-Za-z0-9._-]{1,40}\", name or \"\"):\n            raise ValueError(\n                \"Invalid worktree name. Use 1-40 chars: letters, numbers, ., _, -\"\n            )\n\n    def create(self, name: str, task_id: int = None, base_ref: str = \"HEAD\") -> str:\n        self._validate_name(name)\n        if self._find(name):\n            raise ValueError(f\"Worktree '{name}' already exists in index\")\n        if task_id is not None and not self.tasks.exists(task_id):\n            raise ValueError(f\"Task {task_id} not found\")\n\n        path = self.dir / name\n        branch = f\"wt/{name}\"\n        self.events.emit(\n            \"worktree.create.before\",\n            task={\"id\": task_id} if task_id is not None else {},\n            worktree={\"name\": name, \"base_ref\": base_ref},\n        )\n        try:\n            self._run_git([\"worktree\", \"add\", \"-b\", branch, str(path), base_ref])\n\n            entry = {\n                \"name\": name,\n                \"path\": str(path),\n                \"branch\": branch,\n                \"task_id\": task_id,\n                \"status\": \"active\",\n                \"created_at\": time.time(),\n            }\n\n            idx = self._load_index()\n            idx[\"worktrees\"].append(entry)\n            self._save_index(idx)\n\n            if task_id is not None:\n                self.tasks.bind_worktree(task_id, name)\n\n            self.events.emit(\n                \"worktree.create.after\",\n                task={\"id\": task_id} if task_id is not None else {},\n                worktree={\n                    \"name\": name,\n                    \"path\": str(path),\n                    \"branch\": branch,\n                    \"status\": \"active\",\n                },\n            )\n            return json.dumps(entry, indent=2)\n        except Exception as e:\n            self.events.emit(\n                \"worktree.create.failed\",\n                task={\"id\": task_id} if task_id is not None else {},\n                worktree={\"name\": name, \"base_ref\": base_ref},\n                error=str(e),\n            )\n            raise\n\n    def list_all(self) -> str:\n        idx = self._load_index()\n        wts = idx.get(\"worktrees\", [])\n        if not wts:\n            return \"No worktrees in index.\"\n        lines = []\n        for wt in wts:\n            suffix = f\" task={wt['task_id']}\" if wt.get(\"task_id\") else \"\"\n            lines.append(\n                f\"[{wt.get('status', 'unknown')}] {wt['name']} -> \"\n                f\"{wt['path']} ({wt.get('branch', '-')}){suffix}\"\n            )\n        return \"\\n\".join(lines)\n\n    def status(self, name: str) -> str:\n        wt = self._find(name)\n        if not wt:\n            return f\"Error: Unknown worktree '{name}'\"\n        path = Path(wt[\"path\"])\n        if not path.exists():\n            return f\"Error: Worktree path missing: {path}\"\n        r = subprocess.run(\n            [\"git\", \"status\", \"--short\", \"--branch\"],\n            cwd=path,\n            capture_output=True,\n            text=True,\n            timeout=60,\n        )\n        text = (r.stdout + r.stderr).strip()\n        return text or \"Clean worktree\"\n\n    def run(self, name: str, 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\n        wt = self._find(name)\n        if not wt:\n            return f\"Error: Unknown worktree '{name}'\"\n        path = Path(wt[\"path\"])\n        if not path.exists():\n            return f\"Error: Worktree path missing: {path}\"\n\n        try:\n            r = subprocess.run(\n                command,\n                shell=True,\n                cwd=path,\n                capture_output=True,\n                text=True,\n                timeout=300,\n            )\n            out = (r.stdout + r.stderr).strip()\n            return out[:50000] if out else \"(no output)\"\n        except subprocess.TimeoutExpired:\n            return \"Error: Timeout (300s)\"\n\n    def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str:\n        wt = self._find(name)\n        if not wt:\n            return f\"Error: Unknown worktree '{name}'\"\n\n        self.events.emit(\n            \"worktree.remove.before\",\n            task={\"id\": wt.get(\"task_id\")} if wt.get(\"task_id\") is not None else {},\n            worktree={\"name\": name, \"path\": wt.get(\"path\")},\n        )\n        try:\n            args = [\"worktree\", \"remove\"]\n            if force:\n                args.append(\"--force\")\n            args.append(wt[\"path\"])\n            self._run_git(args)\n\n            if complete_task and wt.get(\"task_id\") is not None:\n                task_id = wt[\"task_id\"]\n                before = json.loads(self.tasks.get(task_id))\n                self.tasks.update(task_id, status=\"completed\")\n                self.tasks.unbind_worktree(task_id)\n                self.events.emit(\n                    \"task.completed\",\n                    task={\n                        \"id\": task_id,\n                        \"subject\": before.get(\"subject\", \"\"),\n                        \"status\": \"completed\",\n                    },\n                    worktree={\"name\": name},\n                )\n\n            idx = self._load_index()\n            for item in idx.get(\"worktrees\", []):\n                if item.get(\"name\") == name:\n                    item[\"status\"] = \"removed\"\n                    item[\"removed_at\"] = time.time()\n            self._save_index(idx)\n\n            self.events.emit(\n                \"worktree.remove.after\",\n                task={\"id\": wt.get(\"task_id\")} if wt.get(\"task_id\") is not None else {},\n                worktree={\"name\": name, \"path\": wt.get(\"path\"), \"status\": \"removed\"},\n            )\n            return f\"Removed worktree '{name}'\"\n        except Exception as e:\n            self.events.emit(\n                \"worktree.remove.failed\",\n                task={\"id\": wt.get(\"task_id\")} if wt.get(\"task_id\") is not None else {},\n                worktree={\"name\": name, \"path\": wt.get(\"path\")},\n                error=str(e),\n            )\n            raise\n\n    def keep(self, name: str) -> str:\n        wt = self._find(name)\n        if not wt:\n            return f\"Error: Unknown worktree '{name}'\"\n\n        idx = self._load_index()\n        kept = None\n        for item in idx.get(\"worktrees\", []):\n            if item.get(\"name\") == name:\n                item[\"status\"] = \"kept\"\n                item[\"kept_at\"] = time.time()\n                kept = item\n        self._save_index(idx)\n\n        self.events.emit(\n            \"worktree.keep\",\n            task={\"id\": wt.get(\"task_id\")} if wt.get(\"task_id\") is not None else {},\n            worktree={\n                \"name\": name,\n                \"path\": wt.get(\"path\"),\n                \"status\": \"kept\",\n            },\n        )\n        return json.dumps(kept, indent=2) if kept else f\"Error: Unknown worktree '{name}'\"\n\n\nWORKTREES = WorktreeManager(REPO_ROOT, TASKS, EVENTS)\n\n\n# -- Base tools (kept minimal, same style as previous sessions) --\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    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(\n            command,\n            shell=True,\n            cwd=WORKDIR,\n            capture_output=True,\n            text=True,\n            timeout=120,\n        )\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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\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\"\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        fp = safe_path(path)\n        c = fp.read_text()\n        if old_text not in c:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(c.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\nTOOL_HANDLERS = {\n    \"bash\": lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\": lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\": lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"], kw.get(\"description\", \"\")),\n    \"task_list\": lambda **kw: TASKS.list_all(),\n    \"task_get\": lambda **kw: TASKS.get(kw[\"task_id\"]),\n    \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"], kw.get(\"status\"), kw.get(\"owner\")),\n    \"task_bind_worktree\": lambda **kw: TASKS.bind_worktree(kw[\"task_id\"], kw[\"worktree\"], kw.get(\"owner\", \"\")),\n    \"worktree_create\": lambda **kw: WORKTREES.create(kw[\"name\"], kw.get(\"task_id\"), kw.get(\"base_ref\", \"HEAD\")),\n    \"worktree_list\": lambda **kw: WORKTREES.list_all(),\n    \"worktree_status\": lambda **kw: WORKTREES.status(kw[\"name\"]),\n    \"worktree_run\": lambda **kw: WORKTREES.run(kw[\"name\"], kw[\"command\"]),\n    \"worktree_keep\": lambda **kw: WORKTREES.keep(kw[\"name\"]),\n    \"worktree_remove\": lambda **kw: WORKTREES.remove(kw[\"name\"], kw.get(\"force\", False), kw.get(\"complete_task\", False)),\n    \"worktree_events\": lambda **kw: EVENTS.list_recent(kw.get(\"limit\", 20)),\n}\n\nTOOLS = [\n    {\n        \"name\": \"bash\",\n        \"description\": \"Run a shell command in the current workspace (blocking).\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"command\": {\"type\": \"string\"}},\n            \"required\": [\"command\"],\n        },\n    },\n    {\n        \"name\": \"read_file\",\n        \"description\": \"Read file contents.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"path\": {\"type\": \"string\"},\n                \"limit\": {\"type\": \"integer\"},\n            },\n            \"required\": [\"path\"],\n        },\n    },\n    {\n        \"name\": \"write_file\",\n        \"description\": \"Write content to file.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"path\": {\"type\": \"string\"},\n                \"content\": {\"type\": \"string\"},\n            },\n            \"required\": [\"path\", \"content\"],\n        },\n    },\n    {\n        \"name\": \"edit_file\",\n        \"description\": \"Replace exact text in file.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"path\": {\"type\": \"string\"},\n                \"old_text\": {\"type\": \"string\"},\n                \"new_text\": {\"type\": \"string\"},\n            },\n            \"required\": [\"path\", \"old_text\", \"new_text\"],\n        },\n    },\n    {\n        \"name\": \"task_create\",\n        \"description\": \"Create a new task on the shared task board.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"subject\": {\"type\": \"string\"},\n                \"description\": {\"type\": \"string\"},\n            },\n            \"required\": [\"subject\"],\n        },\n    },\n    {\n        \"name\": \"task_list\",\n        \"description\": \"List all tasks with status, owner, and worktree binding.\",\n        \"input_schema\": {\"type\": \"object\", \"properties\": {}},\n    },\n    {\n        \"name\": \"task_get\",\n        \"description\": \"Get task details by ID.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"task_id\": {\"type\": \"integer\"}},\n            \"required\": [\"task_id\"],\n        },\n    },\n    {\n        \"name\": \"task_update\",\n        \"description\": \"Update task status or owner.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"task_id\": {\"type\": \"integer\"},\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"pending\", \"in_progress\", \"completed\"],\n                },\n                \"owner\": {\"type\": \"string\"},\n            },\n            \"required\": [\"task_id\"],\n        },\n    },\n    {\n        \"name\": \"task_bind_worktree\",\n        \"description\": \"Bind a task to a worktree name.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"task_id\": {\"type\": \"integer\"},\n                \"worktree\": {\"type\": \"string\"},\n                \"owner\": {\"type\": \"string\"},\n            },\n            \"required\": [\"task_id\", \"worktree\"],\n        },\n    },\n    {\n        \"name\": \"worktree_create\",\n        \"description\": \"Create a git worktree and optionally bind it to a task.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\"},\n                \"task_id\": {\"type\": \"integer\"},\n                \"base_ref\": {\"type\": \"string\"},\n            },\n            \"required\": [\"name\"],\n        },\n    },\n    {\n        \"name\": \"worktree_list\",\n        \"description\": \"List worktrees tracked in .worktrees/index.json.\",\n        \"input_schema\": {\"type\": \"object\", \"properties\": {}},\n    },\n    {\n        \"name\": \"worktree_status\",\n        \"description\": \"Show git status for one worktree.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n            \"required\": [\"name\"],\n        },\n    },\n    {\n        \"name\": \"worktree_run\",\n        \"description\": \"Run a shell command in a named worktree directory.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\"},\n                \"command\": {\"type\": \"string\"},\n            },\n            \"required\": [\"name\", \"command\"],\n        },\n    },\n    {\n        \"name\": \"worktree_remove\",\n        \"description\": \"Remove a worktree and optionally mark its bound task completed.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\"},\n                \"force\": {\"type\": \"boolean\"},\n                \"complete_task\": {\"type\": \"boolean\"},\n            },\n            \"required\": [\"name\"],\n        },\n    },\n    {\n        \"name\": \"worktree_keep\",\n        \"description\": \"Mark a worktree as kept in lifecycle state without removing it.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n            \"required\": [\"name\"],\n        },\n    },\n    {\n        \"name\": \"worktree_events\",\n        \"description\": \"List recent worktree/task lifecycle events from .worktrees/events.jsonl.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"limit\": {\"type\": \"integer\"}},\n        },\n    },\n]\n\n\ndef agent_loop(messages: list):\n    while True:\n        response = client.messages.create(\n            model=MODEL,\n            system=SYSTEM,\n            messages=messages,\n            tools=TOOLS,\n            max_tokens=8000,\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                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append(\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": block.id,\n                        \"content\": str(output),\n                    }\n                )\n        messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(f\"Repo root for s12: {REPO_ROOT}\")\n    if not WORKTREES.git_available:\n        print(\"Note: Not in a git repo. worktree_* tools will return errors.\")\n\n    history = []\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)\n        response_content = history[-1][\"content\"]\n        if isinstance(response_content, list):\n            for block in response_content:\n                if hasattr(block, \"text\"):\n                    print(block.text)\n        print()\n"
  },
  {
    "path": "agents/s_full.py",
    "content": "#!/usr/bin/env python3\n# Harness: all mechanisms combined -- the complete cockpit for the model.\n\"\"\"\ns_full.py - Full Reference Agent\n\nCapstone implementation combining every mechanism from s01-s11.\nSession s12 (task-aware worktree isolation) is taught separately.\nNOT a teaching session -- this is the \"put it all together\" reference.\n\n    +------------------------------------------------------------------+\n    |                        FULL AGENT                                 |\n    |                                                                   |\n    |  System prompt (s05 skills, task-first + optional todo nag)      |\n    |                                                                   |\n    |  Before each LLM call:                                            |\n    |  +--------------------+  +------------------+  +--------------+  |\n    |  | Microcompact (s06) |  | Drain bg (s08)   |  | Check inbox  |  |\n    |  | Auto-compact (s06) |  | notifications    |  | (s09)        |  |\n    |  +--------------------+  +------------------+  +--------------+  |\n    |                                                                   |\n    |  Tool dispatch (s02 pattern):                                     |\n    |  +--------+----------+----------+---------+-----------+          |\n    |  | bash   | read     | write    | edit    | TodoWrite |          |\n    |  | task   | load_sk  | compress | bg_run  | bg_check  |          |\n    |  | t_crt  | t_get    | t_upd    | t_list  | spawn_tm  |          |\n    |  | list_tm| send_msg | rd_inbox | bcast   | shutdown  |          |\n    |  | plan   | idle     | claim    |         |           |          |\n    |  +--------+----------+----------+---------+-----------+          |\n    |                                                                   |\n    |  Subagent (s04):  spawn -> work -> return summary                 |\n    |  Teammate (s09):  spawn -> work -> idle -> auto-claim (s11)      |\n    |  Shutdown (s10):  request_id handshake                            |\n    |  Plan gate (s10): submit -> approve/reject                        |\n    +------------------------------------------------------------------+\n\n    REPL commands: /compact /tasks /team /inbox\n\"\"\"\n\nimport json\nimport os\nimport re\nimport subprocess\nimport threading\nimport time\nimport uuid\nfrom pathlib import Path\nfrom queue import Queue\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\nTEAM_DIR = WORKDIR / \".team\"\nINBOX_DIR = TEAM_DIR / \"inbox\"\nTASKS_DIR = WORKDIR / \".tasks\"\nSKILLS_DIR = WORKDIR / \"skills\"\nTRANSCRIPT_DIR = WORKDIR / \".transcripts\"\nTOKEN_THRESHOLD = 100000\nPOLL_INTERVAL = 5\nIDLE_TIMEOUT = 60\n\nVALID_MSG_TYPES = {\"message\", \"broadcast\", \"shutdown_request\",\n                   \"shutdown_response\", \"plan_approval_response\"}\n\n\n# === SECTION: base_tools ===\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    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, 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) -> 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)\"]\n        return \"\\n\".join(lines)[:50000]\n    except Exception as e:\n        return f\"Error: {e}\"\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\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n    try:\n        fp = safe_path(path)\n        c = fp.read_text()\n        if old_text not in c:\n            return f\"Error: Text not found in {path}\"\n        fp.write_text(c.replace(old_text, new_text, 1))\n        return f\"Edited {path}\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# === SECTION: todos (s03) ===\nclass TodoManager:\n    def __init__(self):\n        self.items = []\n\n    def update(self, items: list) -> str:\n        validated, ip = [], 0\n        for i, item in enumerate(items):\n            content = str(item.get(\"content\", \"\")).strip()\n            status = str(item.get(\"status\", \"pending\")).lower()\n            af = str(item.get(\"activeForm\", \"\")).strip()\n            if not content: raise ValueError(f\"Item {i}: content required\")\n            if status not in (\"pending\", \"in_progress\", \"completed\"):\n                raise ValueError(f\"Item {i}: invalid status '{status}'\")\n            if not af: raise ValueError(f\"Item {i}: activeForm required\")\n            if status == \"in_progress\": ip += 1\n            validated.append({\"content\": content, \"status\": status, \"activeForm\": af})\n        if len(validated) > 20: raise ValueError(\"Max 20 todos\")\n        if ip > 1: raise ValueError(\"Only one in_progress allowed\")\n        self.items = validated\n        return self.render()\n\n    def render(self) -> str:\n        if not self.items: return \"No todos.\"\n        lines = []\n        for item in self.items:\n            m = {\"completed\": \"[x]\", \"in_progress\": \"[>]\", \"pending\": \"[ ]\"}.get(item[\"status\"], \"[?]\")\n            suffix = f\" <- {item['activeForm']}\" if item[\"status\"] == \"in_progress\" else \"\"\n            lines.append(f\"{m} {item['content']}{suffix}\")\n        done = sum(1 for t in self.items if t[\"status\"] == \"completed\")\n        lines.append(f\"\\n({done}/{len(self.items)} completed)\")\n        return \"\\n\".join(lines)\n\n    def has_open_items(self) -> bool:\n        return any(item.get(\"status\") != \"completed\" for item in self.items)\n\n\n# === SECTION: subagent (s04) ===\ndef run_subagent(prompt: str, agent_type: str = \"Explore\") -> str:\n    sub_tools = [\n        {\"name\": \"bash\", \"description\": \"Run command.\",\n         \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n        {\"name\": \"read_file\", \"description\": \"Read file.\",\n         \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}},\n    ]\n    if agent_type != \"Explore\":\n        sub_tools += [\n            {\"name\": \"write_file\", \"description\": \"Write file.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"edit_file\", \"description\": \"Edit file.\",\n             \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n        ]\n    sub_handlers = {\n        \"bash\": lambda **kw: run_bash(kw[\"command\"]),\n        \"read_file\": lambda **kw: run_read(kw[\"path\"]),\n        \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n        \"edit_file\": lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    }\n    sub_msgs = [{\"role\": \"user\", \"content\": prompt}]\n    resp = None\n    for _ in range(30):\n        resp = client.messages.create(model=MODEL, messages=sub_msgs, tools=sub_tools, max_tokens=8000)\n        sub_msgs.append({\"role\": \"assistant\", \"content\": resp.content})\n        if resp.stop_reason != \"tool_use\":\n            break\n        results = []\n        for b in resp.content:\n            if b.type == \"tool_use\":\n                h = sub_handlers.get(b.name, lambda **kw: \"Unknown tool\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": b.id, \"content\": str(h(**b.input))[:50000]})\n        sub_msgs.append({\"role\": \"user\", \"content\": results})\n    if resp:\n        return \"\".join(b.text for b in resp.content if hasattr(b, \"text\")) or \"(no summary)\"\n    return \"(subagent failed)\"\n\n\n# === SECTION: skills (s05) ===\nclass SkillLoader:\n    def __init__(self, skills_dir: Path):\n        self.skills = {}\n        if skills_dir.exists():\n            for f in sorted(skills_dir.rglob(\"SKILL.md\")):\n                text = f.read_text()\n                match = re.match(r\"^---\\n(.*?)\\n---\\n(.*)\", text, re.DOTALL)\n                meta, body = {}, text\n                if match:\n                    for line in match.group(1).strip().splitlines():\n                        if \":\" in line:\n                            k, v = line.split(\":\", 1)\n                            meta[k.strip()] = v.strip()\n                    body = match.group(2).strip()\n                name = meta.get(\"name\", f.parent.name)\n                self.skills[name] = {\"meta\": meta, \"body\": body}\n\n    def descriptions(self) -> str:\n        if not self.skills: return \"(no skills)\"\n        return \"\\n\".join(f\"  - {n}: {s['meta'].get('description', '-')}\" for n, s in self.skills.items())\n\n    def load(self, name: str) -> str:\n        s = self.skills.get(name)\n        if not s: return f\"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}\"\n        return f\"<skill name=\\\"{name}\\\">\\n{s['body']}\\n</skill>\"\n\n\n# === SECTION: compression (s06) ===\ndef estimate_tokens(messages: list) -> int:\n    return len(json.dumps(messages, default=str)) // 4\n\ndef microcompact(messages: list):\n    indices = []\n    for i, msg in enumerate(messages):\n        if msg[\"role\"] == \"user\" and isinstance(msg.get(\"content\"), list):\n            for part in msg[\"content\"]:\n                if isinstance(part, dict) and part.get(\"type\") == \"tool_result\":\n                    indices.append(part)\n    if len(indices) <= 3:\n        return\n    for part in indices[:-3]:\n        if isinstance(part.get(\"content\"), str) and len(part[\"content\"]) > 100:\n            part[\"content\"] = \"[cleared]\"\n\ndef auto_compact(messages: list) -> list:\n    TRANSCRIPT_DIR.mkdir(exist_ok=True)\n    path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with open(path, \"w\") as f:\n        for msg in messages:\n            f.write(json.dumps(msg, default=str) + \"\\n\")\n    conv_text = json.dumps(messages, default=str)[:80000]\n    resp = client.messages.create(\n        model=MODEL,\n        messages=[{\"role\": \"user\", \"content\": f\"Summarize for continuity:\\n{conv_text}\"}],\n        max_tokens=2000,\n    )\n    summary = resp.content[0].text\n    return [\n        {\"role\": \"user\", \"content\": f\"[Compressed. Transcript: {path}]\\n{summary}\"},\n        {\"role\": \"assistant\", \"content\": \"Understood. Continuing with summary context.\"},\n    ]\n\n\n# === SECTION: file_tasks (s07) ===\nclass TaskManager:\n    def __init__(self):\n        TASKS_DIR.mkdir(exist_ok=True)\n\n    def _next_id(self) -> int:\n        ids = [int(f.stem.split(\"_\")[1]) for f in TASKS_DIR.glob(\"task_*.json\")]\n        return max(ids, default=0) + 1\n\n    def _load(self, tid: int) -> dict:\n        p = TASKS_DIR / f\"task_{tid}.json\"\n        if not p.exists(): raise ValueError(f\"Task {tid} not found\")\n        return json.loads(p.read_text())\n\n    def _save(self, task: dict):\n        (TASKS_DIR / f\"task_{task['id']}.json\").write_text(json.dumps(task, indent=2))\n\n    def create(self, subject: str, description: str = \"\") -> str:\n        task = {\"id\": self._next_id(), \"subject\": subject, \"description\": description,\n                \"status\": \"pending\", \"owner\": None, \"blockedBy\": [], \"blocks\": []}\n        self._save(task)\n        return json.dumps(task, indent=2)\n\n    def get(self, tid: int) -> str:\n        return json.dumps(self._load(tid), indent=2)\n\n    def update(self, tid: int, status: str = None,\n               add_blocked_by: list = None, add_blocks: list = None) -> str:\n        task = self._load(tid)\n        if status:\n            task[\"status\"] = status\n            if status == \"completed\":\n                for f in TASKS_DIR.glob(\"task_*.json\"):\n                    t = json.loads(f.read_text())\n                    if tid in t.get(\"blockedBy\", []):\n                        t[\"blockedBy\"].remove(tid)\n                        self._save(t)\n            if status == \"deleted\":\n                (TASKS_DIR / f\"task_{tid}.json\").unlink(missing_ok=True)\n                return f\"Task {tid} deleted\"\n        if add_blocked_by:\n            task[\"blockedBy\"] = list(set(task[\"blockedBy\"] + add_blocked_by))\n        if add_blocks:\n            task[\"blocks\"] = list(set(task[\"blocks\"] + add_blocks))\n        self._save(task)\n        return json.dumps(task, indent=2)\n\n    def list_all(self) -> str:\n        tasks = [json.loads(f.read_text()) for f in sorted(TASKS_DIR.glob(\"task_*.json\"))]\n        if not tasks: return \"No tasks.\"\n        lines = []\n        for t in tasks:\n            m = {\"pending\": \"[ ]\", \"in_progress\": \"[>]\", \"completed\": \"[x]\"}.get(t[\"status\"], \"[?]\")\n            owner = f\" @{t['owner']}\" if t.get(\"owner\") else \"\"\n            blocked = f\" (blocked by: {t['blockedBy']})\" if t.get(\"blockedBy\") else \"\"\n            lines.append(f\"{m} #{t['id']}: {t['subject']}{owner}{blocked}\")\n        return \"\\n\".join(lines)\n\n    def claim(self, tid: int, owner: str) -> str:\n        task = self._load(tid)\n        task[\"owner\"] = owner\n        task[\"status\"] = \"in_progress\"\n        self._save(task)\n        return f\"Claimed task #{tid} for {owner}\"\n\n\n# === SECTION: background (s08) ===\nclass BackgroundManager:\n    def __init__(self):\n        self.tasks = {}\n        self.notifications = Queue()\n\n    def run(self, command: str, timeout: int = 120) -> str:\n        tid = str(uuid.uuid4())[:8]\n        self.tasks[tid] = {\"status\": \"running\", \"command\": command, \"result\": None}\n        threading.Thread(target=self._exec, args=(tid, command, timeout), daemon=True).start()\n        return f\"Background task {tid} started: {command[:80]}\"\n\n    def _exec(self, tid: str, command: str, timeout: int):\n        try:\n            r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                               capture_output=True, text=True, timeout=timeout)\n            output = (r.stdout + r.stderr).strip()[:50000]\n            self.tasks[tid].update({\"status\": \"completed\", \"result\": output or \"(no output)\"})\n        except Exception as e:\n            self.tasks[tid].update({\"status\": \"error\", \"result\": str(e)})\n        self.notifications.put({\"task_id\": tid, \"status\": self.tasks[tid][\"status\"],\n                                \"result\": self.tasks[tid][\"result\"][:500]})\n\n    def check(self, tid: str = None) -> str:\n        if tid:\n            t = self.tasks.get(tid)\n            return f\"[{t['status']}] {t.get('result', '(running)')}\" if t else f\"Unknown: {tid}\"\n        return \"\\n\".join(f\"{k}: [{v['status']}] {v['command'][:60]}\" for k, v in self.tasks.items()) or \"No bg tasks.\"\n\n    def drain(self) -> list:\n        notifs = []\n        while not self.notifications.empty():\n            notifs.append(self.notifications.get_nowait())\n        return notifs\n\n\n# === SECTION: messaging (s09) ===\nclass MessageBus:\n    def __init__(self):\n        INBOX_DIR.mkdir(parents=True, exist_ok=True)\n\n    def send(self, sender: str, to: str, content: str,\n             msg_type: str = \"message\", extra: dict = None) -> str:\n        msg = {\"type\": msg_type, \"from\": sender, \"content\": content,\n               \"timestamp\": time.time()}\n        if extra: msg.update(extra)\n        with open(INBOX_DIR / f\"{to}.jsonl\", \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n        return f\"Sent {msg_type} to {to}\"\n\n    def read_inbox(self, name: str) -> list:\n        path = INBOX_DIR / f\"{name}.jsonl\"\n        if not path.exists(): return []\n        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]\n        path.write_text(\"\")\n        return msgs\n\n    def broadcast(self, sender: str, content: str, names: list) -> str:\n        count = 0\n        for n in names:\n            if n != sender:\n                self.send(sender, n, content, \"broadcast\")\n                count += 1\n        return f\"Broadcast to {count} teammates\"\n\n\n# === SECTION: shutdown + plan tracking (s10) ===\nshutdown_requests = {}\nplan_requests = {}\n\n\n# === SECTION: team (s09/s11) ===\nclass TeammateManager:\n    def __init__(self, bus: MessageBus, task_mgr: TaskManager):\n        TEAM_DIR.mkdir(exist_ok=True)\n        self.bus = bus\n        self.task_mgr = task_mgr\n        self.config_path = TEAM_DIR / \"config.json\"\n        self.config = self._load()\n        self.threads = {}\n\n    def _load(self) -> dict:\n        if self.config_path.exists():\n            return json.loads(self.config_path.read_text())\n        return {\"team_name\": \"default\", \"members\": []}\n\n    def _save(self):\n        self.config_path.write_text(json.dumps(self.config, indent=2))\n\n    def _find(self, name: str) -> dict:\n        for m in self.config[\"members\"]:\n            if m[\"name\"] == name: return m\n        return None\n\n    def spawn(self, name: str, role: str, prompt: str) -> str:\n        member = self._find(name)\n        if member:\n            if member[\"status\"] not in (\"idle\", \"shutdown\"):\n                return f\"Error: '{name}' is currently {member['status']}\"\n            member[\"status\"] = \"working\"\n            member[\"role\"] = role\n        else:\n            member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n            self.config[\"members\"].append(member)\n        self._save()\n        threading.Thread(target=self._loop, args=(name, role, prompt), daemon=True).start()\n        return f\"Spawned '{name}' (role: {role})\"\n\n    def _set_status(self, name: str, status: str):\n        member = self._find(name)\n        if member:\n            member[\"status\"] = status\n            self._save()\n\n    def _loop(self, name: str, role: str, prompt: str):\n        team_name = self.config[\"team_name\"]\n        sys_prompt = (f\"You are '{name}', role: {role}, team: {team_name}, at {WORKDIR}. \"\n                      f\"Use idle when done with current work. You may auto-claim tasks.\")\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        tools = [\n            {\"name\": \"bash\", \"description\": \"Run command.\", \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n            {\"name\": \"read_file\", \"description\": \"Read file.\", \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}},\n            {\"name\": \"write_file\", \"description\": \"Write file.\", \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n            {\"name\": \"edit_file\", \"description\": \"Edit file.\", \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n            {\"name\": \"send_message\", \"description\": \"Send message.\", \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"to\", \"content\"]}},\n            {\"name\": \"idle\", \"description\": \"Signal no more work.\", \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n            {\"name\": \"claim_task\", \"description\": \"Claim task by ID.\", \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}}, \"required\": [\"task_id\"]}},\n        ]\n        while True:\n            # -- WORK PHASE --\n            for _ in range(50):\n                inbox = self.bus.read_inbox(name)\n                for msg in inbox:\n                    if msg.get(\"type\") == \"shutdown_request\":\n                        self._set_status(name, \"shutdown\")\n                        return\n                    messages.append({\"role\": \"user\", \"content\": json.dumps(msg)})\n                try:\n                    response = client.messages.create(\n                        model=MODEL, system=sys_prompt, messages=messages,\n                        tools=tools, max_tokens=8000)\n                except Exception:\n                    self._set_status(name, \"shutdown\")\n                    return\n                messages.append({\"role\": \"assistant\", \"content\": response.content})\n                if response.stop_reason != \"tool_use\":\n                    break\n                results = []\n                idle_requested = False\n                for block in response.content:\n                    if block.type == \"tool_use\":\n                        if block.name == \"idle\":\n                            idle_requested = True\n                            output = \"Entering idle phase.\"\n                        elif block.name == \"claim_task\":\n                            output = self.task_mgr.claim(block.input[\"task_id\"], name)\n                        elif block.name == \"send_message\":\n                            output = self.bus.send(name, block.input[\"to\"], block.input[\"content\"])\n                        else:\n                            dispatch = {\"bash\": lambda **kw: run_bash(kw[\"command\"]),\n                                        \"read_file\": lambda **kw: run_read(kw[\"path\"]),\n                                        \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n                                        \"edit_file\": lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"])}\n                            output = dispatch.get(block.name, lambda **kw: \"Unknown\")(**block.input)\n                        print(f\"  [{name}] {block.name}: {str(output)[:120]}\")\n                        results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n                messages.append({\"role\": \"user\", \"content\": results})\n                if idle_requested:\n                    break\n            # -- IDLE PHASE: poll for messages and unclaimed tasks --\n            self._set_status(name, \"idle\")\n            resume = False\n            for _ in range(IDLE_TIMEOUT // max(POLL_INTERVAL, 1)):\n                time.sleep(POLL_INTERVAL)\n                inbox = self.bus.read_inbox(name)\n                if inbox:\n                    for msg in inbox:\n                        if msg.get(\"type\") == \"shutdown_request\":\n                            self._set_status(name, \"shutdown\")\n                            return\n                        messages.append({\"role\": \"user\", \"content\": json.dumps(msg)})\n                    resume = True\n                    break\n                unclaimed = []\n                for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n                    t = json.loads(f.read_text())\n                    if t.get(\"status\") == \"pending\" and not t.get(\"owner\") and not t.get(\"blockedBy\"):\n                        unclaimed.append(t)\n                if unclaimed:\n                    task = unclaimed[0]\n                    self.task_mgr.claim(task[\"id\"], name)\n                    # Identity re-injection for compressed contexts\n                    if len(messages) <= 3:\n                        messages.insert(0, {\"role\": \"user\", \"content\":\n                            f\"<identity>You are '{name}', role: {role}, team: {team_name}.</identity>\"})\n                        messages.insert(1, {\"role\": \"assistant\", \"content\": f\"I am {name}. Continuing.\"})\n                    messages.append({\"role\": \"user\", \"content\":\n                        f\"<auto-claimed>Task #{task['id']}: {task['subject']}\\n{task.get('description', '')}</auto-claimed>\"})\n                    messages.append({\"role\": \"assistant\", \"content\": f\"Claimed task #{task['id']}. Working on it.\"})\n                    resume = True\n                    break\n            if not resume:\n                self._set_status(name, \"shutdown\")\n                return\n            self._set_status(name, \"working\")\n\n    def list_all(self) -> str:\n        if not self.config[\"members\"]: return \"No teammates.\"\n        lines = [f\"Team: {self.config['team_name']}\"]\n        for m in self.config[\"members\"]:\n            lines.append(f\"  {m['name']} ({m['role']}): {m['status']}\")\n        return \"\\n\".join(lines)\n\n    def member_names(self) -> list:\n        return [m[\"name\"] for m in self.config[\"members\"]]\n\n\n# === SECTION: global_instances ===\nTODO = TodoManager()\nSKILLS = SkillLoader(SKILLS_DIR)\nTASK_MGR = TaskManager()\nBG = BackgroundManager()\nBUS = MessageBus()\nTEAM = TeammateManager(BUS, TASK_MGR)\n\n# === SECTION: system_prompt ===\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}. Use tools to solve tasks.\nPrefer task_create/task_update/task_list for multi-step work. Use TodoWrite for short checklists.\nUse task for subagent delegation. Use load_skill for specialized knowledge.\nSkills: {SKILLS.descriptions()}\"\"\"\n\n\n# === SECTION: shutdown_protocol (s10) ===\ndef handle_shutdown_request(teammate: str) -> str:\n    req_id = str(uuid.uuid4())[:8]\n    shutdown_requests[req_id] = {\"target\": teammate, \"status\": \"pending\"}\n    BUS.send(\"lead\", teammate, \"Please shut down.\", \"shutdown_request\", {\"request_id\": req_id})\n    return f\"Shutdown request {req_id} sent to '{teammate}'\"\n\n# === SECTION: plan_approval (s10) ===\ndef handle_plan_review(request_id: str, approve: bool, feedback: str = \"\") -> str:\n    req = plan_requests.get(request_id)\n    if not req: return f\"Error: Unknown plan request_id '{request_id}'\"\n    req[\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", req[\"from\"], feedback, \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve, \"feedback\": feedback})\n    return f\"Plan {req['status']} for '{req['from']}'\"\n\n\n# === SECTION: tool_dispatch (s02) ===\nTOOL_HANDLERS = {\n    \"bash\":             lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":        lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\":       lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":        lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"], kw[\"new_text\"]),\n    \"TodoWrite\":        lambda **kw: TODO.update(kw[\"items\"]),\n    \"task\":             lambda **kw: run_subagent(kw[\"prompt\"], kw.get(\"agent_type\", \"Explore\")),\n    \"load_skill\":       lambda **kw: SKILLS.load(kw[\"name\"]),\n    \"compress\":         lambda **kw: \"Compressing...\",\n    \"background_run\":   lambda **kw: BG.run(kw[\"command\"], kw.get(\"timeout\", 120)),\n    \"check_background\": lambda **kw: BG.check(kw.get(\"task_id\")),\n    \"task_create\":      lambda **kw: TASK_MGR.create(kw[\"subject\"], kw.get(\"description\", \"\")),\n    \"task_get\":         lambda **kw: TASK_MGR.get(kw[\"task_id\"]),\n    \"task_update\":      lambda **kw: TASK_MGR.update(kw[\"task_id\"], kw.get(\"status\"), kw.get(\"add_blocked_by\"), kw.get(\"add_blocks\")),\n    \"task_list\":        lambda **kw: TASK_MGR.list_all(),\n    \"spawn_teammate\":   lambda **kw: TEAM.spawn(kw[\"name\"], kw[\"role\"], kw[\"prompt\"]),\n    \"list_teammates\":   lambda **kw: TEAM.list_all(),\n    \"send_message\":     lambda **kw: BUS.send(\"lead\", kw[\"to\"], kw[\"content\"], kw.get(\"msg_type\", \"message\")),\n    \"read_inbox\":       lambda **kw: json.dumps(BUS.read_inbox(\"lead\"), indent=2),\n    \"broadcast\":        lambda **kw: BUS.broadcast(\"lead\", kw[\"content\"], TEAM.member_names()),\n    \"shutdown_request\": lambda **kw: handle_shutdown_request(kw[\"teammate\"]),\n    \"plan_approval\":    lambda **kw: handle_plan_review(kw[\"request_id\"], kw[\"approve\"], kw.get(\"feedback\", \"\")),\n    \"idle\":             lambda **kw: \"Lead does not idle.\",\n    \"claim_task\":       lambda **kw: TASK_MGR.claim(kw[\"task_id\"], \"lead\"),\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 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 file.\",\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\": \"TodoWrite\", \"description\": \"Update task tracking list.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"items\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}, \"activeForm\": {\"type\": \"string\"}}, \"required\": [\"content\", \"status\", \"activeForm\"]}}}, \"required\": [\"items\"]}},\n    {\"name\": \"task\", \"description\": \"Spawn a subagent for isolated exploration or work.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\"}, \"agent_type\": {\"type\": \"string\", \"enum\": [\"Explore\", \"general-purpose\"]}}, \"required\": [\"prompt\"]}},\n    {\"name\": \"load_skill\", \"description\": \"Load specialized knowledge by name.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}, \"required\": [\"name\"]}},\n    {\"name\": \"compress\", \"description\": \"Manually compress conversation context.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"background_run\", \"description\": \"Run command in background thread.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}, \"timeout\": {\"type\": \"integer\"}}, \"required\": [\"command\"]}},\n    {\"name\": \"check_background\", \"description\": \"Check background task status.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"string\"}}}},\n    {\"name\": \"task_create\", \"description\": \"Create a persistent file task.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"subject\": {\"type\": \"string\"}, \"description\": {\"type\": \"string\"}}, \"required\": [\"subject\"]}},\n    {\"name\": \"task_get\", \"description\": \"Get task details by ID.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}}, \"required\": [\"task_id\"]}},\n    {\"name\": \"task_update\", \"description\": \"Update task status or dependencies.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\", \"deleted\"]}, \"add_blocked_by\": {\"type\": \"array\", \"items\": {\"type\": \"integer\"}}, \"add_blocks\": {\"type\": \"array\", \"items\": {\"type\": \"integer\"}}}, \"required\": [\"task_id\"]}},\n    {\"name\": \"task_list\", \"description\": \"List all tasks.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"spawn_teammate\", \"description\": \"Spawn a persistent autonomous teammate.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}, \"role\": {\"type\": \"string\"}, \"prompt\": {\"type\": \"string\"}}, \"required\": [\"name\", \"role\", \"prompt\"]}},\n    {\"name\": \"list_teammates\", \"description\": \"List all teammates.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"send_message\", \"description\": \"Send a message to a teammate.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}, \"msg_type\": {\"type\": \"string\", \"enum\": list(VALID_MSG_TYPES)}}, \"required\": [\"to\", \"content\"]}},\n    {\"name\": \"read_inbox\", \"description\": \"Read and drain the lead's inbox.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"broadcast\", \"description\": \"Send message to all teammates.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\"}}, \"required\": [\"content\"]}},\n    {\"name\": \"shutdown_request\", \"description\": \"Request a teammate to shut down.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"teammate\": {\"type\": \"string\"}}, \"required\": [\"teammate\"]}},\n    {\"name\": \"plan_approval\", \"description\": \"Approve or reject a teammate's plan.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"request_id\": {\"type\": \"string\"}, \"approve\": {\"type\": \"boolean\"}, \"feedback\": {\"type\": \"string\"}}, \"required\": [\"request_id\", \"approve\"]}},\n    {\"name\": \"idle\", \"description\": \"Enter idle state.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {}}},\n    {\"name\": \"claim_task\", \"description\": \"Claim a task from the board.\",\n     \"input_schema\": {\"type\": \"object\", \"properties\": {\"task_id\": {\"type\": \"integer\"}}, \"required\": [\"task_id\"]}},\n]\n\n\n# === SECTION: agent_loop ===\ndef agent_loop(messages: list):\n    rounds_without_todo = 0\n    while True:\n        # s06: compression pipeline\n        microcompact(messages)\n        if estimate_tokens(messages) > TOKEN_THRESHOLD:\n            print(\"[auto-compact triggered]\")\n            messages[:] = auto_compact(messages)\n        # s08: drain background notifications\n        notifs = BG.drain()\n        if notifs:\n            txt = \"\\n\".join(f\"[bg:{n['task_id']}] {n['status']}: {n['result']}\" for n in notifs)\n            messages.append({\"role\": \"user\", \"content\": f\"<background-results>\\n{txt}\\n</background-results>\"})\n            messages.append({\"role\": \"assistant\", \"content\": \"Noted background results.\"})\n        # s10: check lead inbox\n        inbox = BUS.read_inbox(\"lead\")\n        if inbox:\n            messages.append({\"role\": \"user\", \"content\": f\"<inbox>{json.dumps(inbox, indent=2)}</inbox>\"})\n            messages.append({\"role\": \"assistant\", \"content\": \"Noted inbox messages.\"})\n        # LLM call\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        if response.stop_reason != \"tool_use\":\n            return\n        # Tool execution\n        results = []\n        used_todo = False\n        manual_compress = False\n        for block in response.content:\n            if block.type == \"tool_use\":\n                if block.name == \"compress\":\n                    manual_compress = True\n                handler = TOOL_HANDLERS.get(block.name)\n                try:\n                    output = handler(**block.input) if handler else f\"Unknown tool: {block.name}\"\n                except Exception as e:\n                    output = f\"Error: {e}\"\n                print(f\"> {block.name}: {str(output)[:200]}\")\n                results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": str(output)})\n                if block.name == \"TodoWrite\":\n                    used_todo = True\n        # s03: nag reminder (only when todo workflow is active)\n        rounds_without_todo = 0 if used_todo else rounds_without_todo + 1\n        if TODO.has_open_items() and rounds_without_todo >= 3:\n            results.insert(0, {\"type\": \"text\", \"text\": \"<reminder>Update your todos.</reminder>\"})\n        messages.append({\"role\": \"user\", \"content\": results})\n        # s06: manual compress\n        if manual_compress:\n            print(\"[manual compact]\")\n            messages[:] = auto_compact(messages)\n\n\n# === SECTION: repl ===\nif __name__ == \"__main__\":\n    history = []\n    while True:\n        try:\n            query = input(\"\\033[36ms_full >> \\033[0m\")\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query.strip().lower() in (\"q\", \"exit\", \"\"):\n            break\n        if query.strip() == \"/compact\":\n            if history:\n                print(\"[manual compact via /compact]\")\n                history[:] = auto_compact(history)\n            continue\n        if query.strip() == \"/tasks\":\n            print(TASK_MGR.list_all())\n            continue\n        if query.strip() == \"/team\":\n            print(TEAM.list_all())\n            continue\n        if query.strip() == \"/inbox\":\n            print(json.dumps(BUS.read_inbox(\"lead\"), indent=2))\n            continue\n        history.append({\"role\": \"user\", \"content\": query})\n        agent_loop(history)\n        print()\n"
  },
  {
    "path": "docs/en/s01-the-agent-loop.md",
    "content": "# s01: The Agent Loop\n\n`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"One loop & Bash is all you need\"* -- one tool + one loop = an agent.\n>\n> **Harness layer**: The loop -- the model's first connection to the real world.\n\n## Problem\n\nA language model can reason about code, but it can't *touch* the real world -- can't read files, run tests, or check errors. Without a loop, every tool call requires you to manually copy-paste results back. You become the loop.\n\n## Solution\n\n```\n+--------+      +-------+      +---------+\n|  User  | ---> |  LLM  | ---> |  Tool   |\n| prompt |      |       |      | execute |\n+--------+      +---+---+      +----+----+\n                    ^                |\n                    |   tool_result  |\n                    +----------------+\n                    (loop until stop_reason != \"tool_use\")\n```\n\nOne exit condition controls the entire flow. The loop runs until the model stops calling tools.\n\n## How It Works\n\n1. User prompt becomes the first message.\n\n```python\nmessages.append({\"role\": \"user\", \"content\": query})\n```\n\n2. Send messages + tool definitions to the LLM.\n\n```python\nresponse = client.messages.create(\n    model=MODEL, system=SYSTEM, messages=messages,\n    tools=TOOLS, max_tokens=8000,\n)\n```\n\n3. Append the assistant response. Check `stop_reason` -- if the model didn't call a tool, we're done.\n\n```python\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\nif response.stop_reason != \"tool_use\":\n    return\n```\n\n4. Execute each tool call, collect results, append as a user message. Loop back to step 2.\n\n```python\nresults = []\nfor block in response.content:\n    if block.type == \"tool_use\":\n        output = run_bash(block.input[\"command\"])\n        results.append({\n            \"type\": \"tool_result\",\n            \"tool_use_id\": block.id,\n            \"content\": output,\n        })\nmessages.append({\"role\": \"user\", \"content\": results})\n```\n\nAssembled into one function:\n\n```python\ndef agent_loop(query):\n    messages = [{\"role\": \"user\", \"content\": query}]\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                output = run_bash(block.input[\"command\"])\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output,\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n```\n\nThat's the entire agent in under 30 lines. Everything else in this course layers on top -- without changing the loop.\n\n## What Changed\n\n| Component     | Before     | After                          |\n|---------------|------------|--------------------------------|\n| Agent loop    | (none)     | `while True` + stop_reason     |\n| Tools         | (none)     | `bash` (one tool)              |\n| Messages      | (none)     | Accumulating list              |\n| Control flow  | (none)     | `stop_reason != \"tool_use\"`    |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s01_agent_loop.py\n```\n\n1. `Create a file called hello.py that prints \"Hello, World!\"`\n2. `List all Python files in this directory`\n3. `What is the current git branch?`\n4. `Create a directory called test_output and write 3 files in it`\n"
  },
  {
    "path": "docs/en/s02-tool-use.md",
    "content": "# s02: Tool Use\n\n`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"Adding a tool means adding one handler\"* -- the loop stays the same; new tools register into the dispatch map.\n>\n> **Harness layer**: Tool dispatch -- expanding what the model can reach.\n\n## Problem\n\nWith only `bash`, the agent shells out for everything. `cat` truncates unpredictably, `sed` fails on special characters, and every bash call is an unconstrained security surface. Dedicated tools like `read_file` and `write_file` let you enforce path sandboxing at the tool level.\n\nThe key insight: adding tools does not require changing the loop.\n\n## Solution\n\n```\n+--------+      +-------+      +------------------+\n|  User  | ---> |  LLM  | ---> | Tool Dispatch    |\n| prompt |      |       |      | {                |\n+--------+      +---+---+      |   bash: run_bash |\n                    ^           |   read: run_read |\n                    |           |   write: run_wr  |\n                    +-----------+   edit: run_edit |\n                    tool_result | }                |\n                                +------------------+\n\nThe dispatch map is a dict: {tool_name: handler_function}.\nOne lookup replaces any if/elif chain.\n```\n\n## How It Works\n\n1. Each tool gets a handler function. Path sandboxing prevents workspace escape.\n\n```python\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_read(path: str, limit: int = None) -> str:\n    text = safe_path(path).read_text()\n    lines = text.splitlines()\n    if limit and limit < len(lines):\n        lines = lines[:limit]\n    return \"\\n\".join(lines)[:50000]\n```\n\n2. The dispatch map links tool names to handlers.\n\n```python\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"],\n                                        kw[\"new_text\"]),\n}\n```\n\n3. In the loop, look up the handler by name. The loop body itself is unchanged from s01.\n\n```python\nfor block in response.content:\n    if block.type == \"tool_use\":\n        handler = TOOL_HANDLERS.get(block.name)\n        output = handler(**block.input) if handler \\\n            else f\"Unknown tool: {block.name}\"\n        results.append({\n            \"type\": \"tool_result\",\n            \"tool_use_id\": block.id,\n            \"content\": output,\n        })\n```\n\nAdd a tool = add a handler + add a schema entry. The loop never changes.\n\n## What Changed From s01\n\n| Component      | Before (s01)       | After (s02)                |\n|----------------|--------------------|----------------------------|\n| Tools          | 1 (bash only)      | 4 (bash, read, write, edit)|\n| Dispatch       | Hardcoded bash call | `TOOL_HANDLERS` dict       |\n| Path safety    | None               | `safe_path()` sandbox      |\n| Agent loop     | Unchanged          | Unchanged                  |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s02_tool_use.py\n```\n\n1. `Read the file requirements.txt`\n2. `Create a file called greet.py with a greet(name) function`\n3. `Edit greet.py to add a docstring to the function`\n4. `Read greet.py to verify the edit worked`\n"
  },
  {
    "path": "docs/en/s03-todo-write.md",
    "content": "# s03: TodoWrite\n\n`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"An agent without a plan drifts\"* -- list the steps first, then execute.\n>\n> **Harness layer**: Planning -- keeping the model on course without scripting the route.\n\n## Problem\n\nOn multi-step tasks, the model loses track. It repeats work, skips steps, or wanders off. Long conversations make this worse -- the system prompt fades as tool results fill the context. A 10-step refactoring might complete steps 1-3, then the model starts improvising because it forgot steps 4-10.\n\n## Solution\n\n```\n+--------+      +-------+      +---------+\n|  User  | ---> |  LLM  | ---> | Tools   |\n| prompt |      |       |      | + todo  |\n+--------+      +---+---+      +----+----+\n                    ^                |\n                    |   tool_result  |\n                    +----------------+\n                          |\n              +-----------+-----------+\n              | TodoManager state     |\n              | [ ] task A            |\n              | [>] task B  <- doing  |\n              | [x] task C            |\n              +-----------------------+\n                          |\n              if rounds_since_todo >= 3:\n                inject <reminder> into tool_result\n```\n\n## How It Works\n\n1. TodoManager stores items with statuses. Only one item can be `in_progress` at a time.\n\n```python\nclass TodoManager:\n    def update(self, items: list) -> str:\n        validated, in_progress_count = [], 0\n        for item in items:\n            status = item.get(\"status\", \"pending\")\n            if status == \"in_progress\":\n                in_progress_count += 1\n            validated.append({\"id\": item[\"id\"], \"text\": item[\"text\"],\n                              \"status\": status})\n        if in_progress_count > 1:\n            raise ValueError(\"Only one task can be in_progress\")\n        self.items = validated\n        return self.render()\n```\n\n2. The `todo` tool goes into the dispatch map like any other tool.\n\n```python\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"todo\": lambda **kw: TODO.update(kw[\"items\"]),\n}\n```\n\n3. A nag reminder injects a nudge if the model goes 3+ rounds without calling `todo`.\n\n```python\nif rounds_since_todo >= 3 and messages:\n    last = messages[-1]\n    if last[\"role\"] == \"user\" and isinstance(last.get(\"content\"), list):\n        last[\"content\"].insert(0, {\n            \"type\": \"text\",\n            \"text\": \"<reminder>Update your todos.</reminder>\",\n        })\n```\n\nThe \"one in_progress at a time\" constraint forces sequential focus. The nag reminder creates accountability.\n\n## What Changed From s02\n\n| Component      | Before (s02)     | After (s03)                |\n|----------------|------------------|----------------------------|\n| Tools          | 4                | 5 (+todo)                  |\n| Planning       | None             | TodoManager with statuses  |\n| Nag injection  | None             | `<reminder>` after 3 rounds|\n| Agent loop     | Simple dispatch  | + rounds_since_todo counter|\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s03_todo_write.py\n```\n\n1. `Refactor the file hello.py: add type hints, docstrings, and a main guard`\n2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py`\n3. `Review all Python files and fix any style issues`\n"
  },
  {
    "path": "docs/en/s04-subagent.md",
    "content": "# s04: Subagents\n\n`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"Break big tasks down; each subtask gets a clean context\"* -- subagents use independent messages[], keeping the main conversation clean.\n>\n> **Harness layer**: Context isolation -- protecting the model's clarity of thought.\n\n## Problem\n\nAs the agent works, its messages array grows. Every file read, every bash output stays in context permanently. \"What testing framework does this project use?\" might require reading 5 files, but the parent only needs the answer: \"pytest.\"\n\n## Solution\n\n```\nParent agent                     Subagent\n+------------------+             +------------------+\n| messages=[...]   |             | messages=[]      | <-- fresh\n|                  |  dispatch   |                  |\n| tool: task       | ----------> | while tool_use:  |\n|   prompt=\"...\"   |             |   call tools     |\n|                  |  summary    |   append results |\n|   result = \"...\" | <---------- | return last text |\n+------------------+             +------------------+\n\nParent context stays clean. Subagent context is discarded.\n```\n\n## How It Works\n\n1. The parent gets a `task` tool. The child gets all base tools except `task` (no recursive spawning).\n\n```python\nPARENT_TOOLS = CHILD_TOOLS + [\n    {\"name\": \"task\",\n     \"description\": \"Spawn a subagent with fresh context.\",\n     \"input_schema\": {\n         \"type\": \"object\",\n         \"properties\": {\"prompt\": {\"type\": \"string\"}},\n         \"required\": [\"prompt\"],\n     }},\n]\n```\n\n2. The subagent starts with `messages=[]` and runs its own loop. Only the final text returns to the parent.\n\n```python\ndef run_subagent(prompt: str) -> str:\n    sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n    for _ in range(30):  # safety limit\n        response = client.messages.create(\n            model=MODEL, system=SUBAGENT_SYSTEM,\n            messages=sub_messages,\n            tools=CHILD_TOOLS, max_tokens=8000,\n        )\n        sub_messages.append({\"role\": \"assistant\",\n                             \"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 = TOOL_HANDLERS.get(block.name)\n                output = handler(**block.input)\n                results.append({\"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": str(output)[:50000]})\n        sub_messages.append({\"role\": \"user\", \"content\": results})\n    return \"\".join(\n        b.text for b in response.content if hasattr(b, \"text\")\n    ) or \"(no summary)\"\n```\n\nThe child's entire message history (possibly 30+ tool calls) is discarded. The parent receives a one-paragraph summary as a normal `tool_result`.\n\n## What Changed From s03\n\n| Component      | Before (s03)     | After (s04)               |\n|----------------|------------------|---------------------------|\n| Tools          | 5                | 5 (base) + task (parent)  |\n| Context        | Single shared    | Parent + child isolation  |\n| Subagent       | None             | `run_subagent()` function |\n| Return value   | N/A              | Summary text only         |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s04_subagent.py\n```\n\n1. `Use a subtask to find what testing framework this project uses`\n2. `Delegate: read all .py files and summarize what each one does`\n3. `Use a task to create a new module, then verify it from here`\n"
  },
  {
    "path": "docs/en/s05-skill-loading.md",
    "content": "# s05: Skills\n\n`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"Load knowledge when you need it, not upfront\"* -- inject via tool_result, not the system prompt.\n>\n> **Harness layer**: On-demand knowledge -- domain expertise, loaded when the model asks.\n\n## Problem\n\nYou want the agent to follow domain-specific workflows: git conventions, testing patterns, code review checklists. Putting everything in the system prompt wastes tokens on unused skills. 10 skills at 2000 tokens each = 20,000 tokens, most of which are irrelevant to any given task.\n\n## Solution\n\n```\nSystem prompt (Layer 1 -- always present):\n+--------------------------------------+\n| You are a coding agent.              |\n| Skills available:                    |\n|   - git: Git workflow helpers        |  ~100 tokens/skill\n|   - test: Testing best practices     |\n+--------------------------------------+\n\nWhen model calls load_skill(\"git\"):\n+--------------------------------------+\n| tool_result (Layer 2 -- on demand):  |\n| <skill name=\"git\">                   |\n|   Full git workflow instructions...  |  ~2000 tokens\n|   Step 1: ...                        |\n| </skill>                             |\n+--------------------------------------+\n```\n\nLayer 1: skill *names* in system prompt (cheap). Layer 2: full *body* via tool_result (on demand).\n\n## How It Works\n\n1. Each skill is a directory containing a `SKILL.md` with YAML frontmatter.\n\n```\nskills/\n  pdf/\n    SKILL.md       # ---\\n name: pdf\\n description: Process PDF files\\n ---\\n ...\n  code-review/\n    SKILL.md       # ---\\n name: code-review\\n description: Review code\\n ---\\n ...\n```\n\n2. SkillLoader scans for `SKILL.md` files, uses the directory name as the skill identifier.\n\n```python\nclass SkillLoader:\n    def __init__(self, skills_dir: Path):\n        self.skills = {}\n        for f in sorted(skills_dir.rglob(\"SKILL.md\")):\n            text = f.read_text()\n            meta, body = self._parse_frontmatter(text)\n            name = meta.get(\"name\", f.parent.name)\n            self.skills[name] = {\"meta\": meta, \"body\": body}\n\n    def get_descriptions(self) -> str:\n        lines = []\n        for name, skill in self.skills.items():\n            desc = skill[\"meta\"].get(\"description\", \"\")\n            lines.append(f\"  - {name}: {desc}\")\n        return \"\\n\".join(lines)\n\n    def get_content(self, name: str) -> str:\n        skill = self.skills.get(name)\n        if not skill:\n            return f\"Error: Unknown skill '{name}'.\"\n        return f\"<skill name=\\\"{name}\\\">\\n{skill['body']}\\n</skill>\"\n```\n\n3. Layer 1 goes into the system prompt. Layer 2 is just another tool handler.\n\n```python\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nSkills available:\n{SKILL_LOADER.get_descriptions()}\"\"\"\n\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"load_skill\": lambda **kw: SKILL_LOADER.get_content(kw[\"name\"]),\n}\n```\n\nThe model learns what skills exist (cheap) and loads them when relevant (expensive).\n\n## What Changed From s04\n\n| Component      | Before (s04)     | After (s05)                |\n|----------------|------------------|----------------------------|\n| Tools          | 5 (base + task)  | 5 (base + load_skill)      |\n| System prompt  | Static string    | + skill descriptions       |\n| Knowledge      | None             | skills/\\*/SKILL.md files   |\n| Injection      | None             | Two-layer (system + result)|\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s05_skill_loading.py\n```\n\n1. `What skills are available?`\n2. `Load the agent-builder skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n4. `Build an MCP server using the mcp-builder skill`\n"
  },
  {
    "path": "docs/en/s06-context-compact.md",
    "content": "# s06: Context Compact\n\n`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"Context will fill up; you need a way to make room\"* -- three-layer compression strategy for infinite sessions.\n>\n> **Harness layer**: Compression -- clean memory for infinite sessions.\n\n## Problem\n\nThe context window is finite. A single `read_file` on a 1000-line file costs ~4000 tokens. After reading 30 files and running 20 bash commands, you hit 100,000+ tokens. The agent cannot work on large codebases without compression.\n\n## Solution\n\nThree layers, increasing in aggressiveness:\n\n```\nEvery turn:\n+------------------+\n| Tool call result |\n+------------------+\n        |\n        v\n[Layer 1: micro_compact]        (silent, every turn)\n  Replace tool_result > 3 turns old\n  with \"[Previous: used {tool_name}]\"\n        |\n        v\n[Check: tokens > 50000?]\n   |               |\n   no              yes\n   |               |\n   v               v\ncontinue    [Layer 2: auto_compact]\n              Save transcript to .transcripts/\n              LLM summarizes conversation.\n              Replace all messages with [summary].\n                    |\n                    v\n            [Layer 3: compact tool]\n              Model calls compact explicitly.\n              Same summarization as auto_compact.\n```\n\n## How It Works\n\n1. **Layer 1 -- micro_compact**: Before each LLM call, replace old tool results with placeholders.\n\n```python\ndef micro_compact(messages: list) -> list:\n    tool_results = []\n    for i, msg in enumerate(messages):\n        if msg[\"role\"] == \"user\" and isinstance(msg.get(\"content\"), list):\n            for j, part in enumerate(msg[\"content\"]):\n                if isinstance(part, dict) and part.get(\"type\") == \"tool_result\":\n                    tool_results.append((i, j, part))\n    if len(tool_results) <= KEEP_RECENT:\n        return messages\n    for _, _, part in tool_results[:-KEEP_RECENT]:\n        if len(part.get(\"content\", \"\")) > 100:\n            part[\"content\"] = f\"[Previous: used {tool_name}]\"\n    return messages\n```\n\n2. **Layer 2 -- auto_compact**: When tokens exceed threshold, save full transcript to disk, then ask the LLM to summarize.\n\n```python\ndef auto_compact(messages: list) -> list:\n    # Save transcript for recovery\n    transcript_path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with open(transcript_path, \"w\") as f:\n        for msg in messages:\n            f.write(json.dumps(msg, default=str) + \"\\n\")\n    # LLM summarizes\n    response = client.messages.create(\n        model=MODEL,\n        messages=[{\"role\": \"user\", \"content\":\n            \"Summarize this conversation for continuity...\"\n            + json.dumps(messages, default=str)[:80000]}],\n        max_tokens=2000,\n    )\n    return [\n        {\"role\": \"user\", \"content\": f\"[Compressed]\\n\\n{response.content[0].text}\"},\n        {\"role\": \"assistant\", \"content\": \"Understood. Continuing.\"},\n    ]\n```\n\n3. **Layer 3 -- manual compact**: The `compact` tool triggers the same summarization on demand.\n\n4. The loop integrates all three:\n\n```python\ndef agent_loop(messages: list):\n    while True:\n        micro_compact(messages)                        # Layer 1\n        if estimate_tokens(messages) > THRESHOLD:\n            messages[:] = auto_compact(messages)       # Layer 2\n        response = client.messages.create(...)\n        # ... tool execution ...\n        if manual_compact:\n            messages[:] = auto_compact(messages)       # Layer 3\n```\n\nTranscripts preserve full history on disk. Nothing is truly lost -- just moved out of active context.\n\n## What Changed From s05\n\n| Component      | Before (s05)     | After (s06)                |\n|----------------|------------------|----------------------------|\n| Tools          | 5                | 5 (base + compact)         |\n| Context mgmt   | None             | Three-layer compression    |\n| Micro-compact  | None             | Old results -> placeholders|\n| Auto-compact   | None             | Token threshold trigger    |\n| Transcripts    | None             | Saved to .transcripts/     |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s06_context_compact.py\n```\n\n1. `Read every Python file in the agents/ directory one by one` (watch micro-compact replace old results)\n2. `Keep reading files until compression triggers automatically`\n3. `Use the compact tool to manually compress the conversation`\n"
  },
  {
    "path": "docs/en/s07-task-system.md",
    "content": "# s07: Task System\n\n`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12`\n\n> *\"Break big goals into small tasks, order them, persist to disk\"* -- a file-based task graph with dependencies, laying the foundation for multi-agent collaboration.\n>\n> **Harness layer**: Persistent tasks -- goals that outlive any single conversation.\n\n## Problem\n\ns03's TodoManager is a flat checklist in memory: no ordering, no dependencies, no status beyond done-or-not. Real goals have structure -- task B depends on task A, tasks C and D can run in parallel, task E waits for both C and D.\n\nWithout explicit relationships, the agent can't tell what's ready, what's blocked, or what can run concurrently. And because the list lives only in memory, context compression (s06) wipes it clean.\n\n## Solution\n\nPromote the checklist into a **task graph** persisted to disk. Each task is a JSON file with status, dependencies (`blockedBy`), and dependents (`blocks`). The graph answers three questions at any moment:\n\n- **What's ready?** -- tasks with `pending` status and empty `blockedBy`.\n- **What's blocked?** -- tasks waiting on unfinished dependencies.\n- **What's done?** -- `completed` tasks, whose completion automatically unblocks dependents.\n\n```\n.tasks/\n  task_1.json  {\"id\":1, \"status\":\"completed\"}\n  task_2.json  {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\"}\n  task_3.json  {\"id\":3, \"blockedBy\":[1], \"status\":\"pending\"}\n  task_4.json  {\"id\":4, \"blockedBy\":[2,3], \"status\":\"pending\"}\n\nTask graph (DAG):\n                 +----------+\n            +--> | task 2   | --+\n            |    | pending  |   |\n+----------+     +----------+    +--> +----------+\n| task 1   |                          | task 4   |\n| completed| --> +----------+    +--> | blocked  |\n+----------+     | task 3   | --+     +----------+\n                 | pending  |\n                 +----------+\n\nOrdering:     task 1 must finish before 2 and 3\nParallelism:  tasks 2 and 3 can run at the same time\nDependencies: task 4 waits for both 2 and 3\nStatus:       pending -> in_progress -> completed\n```\n\nThis task graph becomes the coordination backbone for everything after s07: background execution (s08), multi-agent teams (s09+), and worktree isolation (s12) all read from and write to this same structure.\n\n## How It Works\n\n1. **TaskManager**: one JSON file per task, CRUD with dependency graph.\n\n```python\nclass TaskManager:\n    def __init__(self, tasks_dir: Path):\n        self.dir = tasks_dir\n        self.dir.mkdir(exist_ok=True)\n        self._next_id = self._max_id() + 1\n\n    def create(self, subject, description=\"\"):\n        task = {\"id\": self._next_id, \"subject\": subject,\n                \"status\": \"pending\", \"blockedBy\": [],\n                \"blocks\": [], \"owner\": \"\"}\n        self._save(task)\n        self._next_id += 1\n        return json.dumps(task, indent=2)\n```\n\n2. **Dependency resolution**: completing a task clears its ID from every other task's `blockedBy` list, automatically unblocking dependents.\n\n```python\ndef _clear_dependency(self, completed_id):\n    for f in self.dir.glob(\"task_*.json\"):\n        task = json.loads(f.read_text())\n        if completed_id in task.get(\"blockedBy\", []):\n            task[\"blockedBy\"].remove(completed_id)\n            self._save(task)\n```\n\n3. **Status + dependency wiring**: `update` handles transitions and dependency edges.\n\n```python\ndef update(self, task_id, status=None,\n           add_blocked_by=None, add_blocks=None):\n    task = self._load(task_id)\n    if status:\n        task[\"status\"] = status\n        if status == \"completed\":\n            self._clear_dependency(task_id)\n    self._save(task)\n```\n\n4. Four task tools go into the dispatch map.\n\n```python\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"]),\n    \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"], kw.get(\"status\")),\n    \"task_list\":   lambda **kw: TASKS.list_all(),\n    \"task_get\":    lambda **kw: TASKS.get(kw[\"task_id\"]),\n}\n```\n\nFrom s07 onward, the task graph is the default for multi-step work. s03's Todo remains for quick single-session checklists.\n\n## What Changed From s06\n\n| Component | Before (s06) | After (s07) |\n|---|---|---|\n| Tools | 5 | 8 (`task_create/update/list/get`) |\n| Planning model | Flat checklist (in-memory) | Task graph with dependencies (on disk) |\n| Relationships | None | `blockedBy` + `blocks` edges |\n| Status tracking | Done or not | `pending` -> `in_progress` -> `completed` |\n| Persistence | Lost on compression | Survives compression and restarts |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s07_task_system.py\n```\n\n1. `Create 3 tasks: \"Setup project\", \"Write code\", \"Write tests\". Make them depend on each other in order.`\n2. `List all tasks and show the dependency graph`\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\n"
  },
  {
    "path": "docs/en/s08-background-tasks.md",
    "content": "# s08: Background Tasks\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12`\n\n> *\"Run slow operations in the background; the agent keeps thinking\"* -- daemon threads run commands, inject notifications on completion.\n>\n> **Harness layer**: Background execution -- the model thinks while the harness waits.\n\n## Problem\n\nSome commands take minutes: `npm install`, `pytest`, `docker build`. With a blocking loop, the model sits idle waiting. If the user asks \"install dependencies and while that runs, create the config file,\" the agent does them sequentially, not in parallel.\n\n## Solution\n\n```\nMain thread                Background thread\n+-----------------+        +-----------------+\n| agent loop      |        | subprocess runs |\n| ...             |        | ...             |\n| [LLM call] <---+------- | enqueue(result) |\n|  ^drain queue   |        +-----------------+\n+-----------------+\n\nTimeline:\nAgent --[spawn A]--[spawn B]--[other work]----\n             |          |\n             v          v\n          [A runs]   [B runs]      (parallel)\n             |          |\n             +-- results injected before next LLM call --+\n```\n\n## How It Works\n\n1. BackgroundManager tracks tasks with a thread-safe notification queue.\n\n```python\nclass BackgroundManager:\n    def __init__(self):\n        self.tasks = {}\n        self._notification_queue = []\n        self._lock = threading.Lock()\n```\n\n2. `run()` starts a daemon thread and returns immediately.\n\n```python\ndef run(self, command: str) -> str:\n    task_id = str(uuid.uuid4())[:8]\n    self.tasks[task_id] = {\"status\": \"running\", \"command\": command}\n    thread = threading.Thread(\n        target=self._execute, args=(task_id, command), daemon=True)\n    thread.start()\n    return f\"Background task {task_id} started\"\n```\n\n3. When the subprocess finishes, its result goes into the notification queue.\n\n```python\ndef _execute(self, task_id, command):\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n            capture_output=True, text=True, timeout=300)\n        output = (r.stdout + r.stderr).strip()[:50000]\n    except subprocess.TimeoutExpired:\n        output = \"Error: Timeout (300s)\"\n    with self._lock:\n        self._notification_queue.append({\n            \"task_id\": task_id, \"result\": output[:500]})\n```\n\n4. The agent loop drains notifications before each LLM call.\n\n```python\ndef agent_loop(messages: list):\n    while True:\n        notifs = BG.drain_notifications()\n        if notifs:\n            notif_text = \"\\n\".join(\n                f\"[bg:{n['task_id']}] {n['result']}\" for n in notifs)\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<background-results>\\n{notif_text}\\n\"\n                           f\"</background-results>\"})\n            messages.append({\"role\": \"assistant\",\n                \"content\": \"Noted background results.\"})\n        response = client.messages.create(...)\n```\n\nThe loop stays single-threaded. Only subprocess I/O is parallelized.\n\n## What Changed From s07\n\n| Component      | Before (s07)     | After (s08)                |\n|----------------|------------------|----------------------------|\n| Tools          | 8                | 6 (base + background_run + check)|\n| Execution      | Blocking only    | Blocking + background threads|\n| Notification   | None             | Queue drained per loop     |\n| Concurrency    | None             | Daemon threads             |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s08_background_tasks.py\n```\n\n1. `Run \"sleep 5 && echo done\" in the background, then create a file while it runs`\n2. `Start 3 background tasks: \"sleep 2\", \"sleep 4\", \"sleep 6\". Check their status.`\n3. `Run pytest in the background and keep working on other things`\n"
  },
  {
    "path": "docs/en/s09-agent-teams.md",
    "content": "# s09: Agent Teams\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12`\n\n> *\"When the task is too big for one, delegate to teammates\"* -- persistent teammates + async mailboxes.\n>\n> **Harness layer**: Team mailboxes -- multiple models, coordinated through files.\n\n## Problem\n\nSubagents (s04) are disposable: spawn, work, return summary, die. No identity, no memory between invocations. Background tasks (s08) run shell commands but can't make LLM-guided decisions.\n\nReal teamwork needs: (1) persistent agents that outlive a single prompt, (2) identity and lifecycle management, (3) a communication channel between agents.\n\n## Solution\n\n```\nTeammate lifecycle:\n  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\n\nCommunication:\n  .team/\n    config.json           <- team roster + statuses\n    inbox/\n      alice.jsonl         <- append-only, drain-on-read\n      bob.jsonl\n      lead.jsonl\n\n              +--------+    send(\"alice\",\"bob\",\"...\")    +--------+\n              | alice  | -----------------------------> |  bob   |\n              | loop   |    bob.jsonl << {json_line}    |  loop  |\n              +--------+                                +--------+\n                   ^                                         |\n                   |        BUS.read_inbox(\"alice\")          |\n                   +---- alice.jsonl -> read + drain ---------+\n```\n\n## How It Works\n\n1. TeammateManager maintains config.json with the team roster.\n\n```python\nclass TeammateManager:\n    def __init__(self, team_dir: Path):\n        self.dir = team_dir\n        self.dir.mkdir(exist_ok=True)\n        self.config_path = self.dir / \"config.json\"\n        self.config = self._load_config()\n        self.threads = {}\n```\n\n2. `spawn()` creates a teammate and starts its agent loop in a thread.\n\n```python\ndef spawn(self, name: str, role: str, prompt: str) -> str:\n    member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n    self.config[\"members\"].append(member)\n    self._save_config()\n    thread = threading.Thread(\n        target=self._teammate_loop,\n        args=(name, role, prompt), daemon=True)\n    thread.start()\n    return f\"Spawned teammate '{name}' (role: {role})\"\n```\n\n3. MessageBus: append-only JSONL inboxes. `send()` appends a JSON line; `read_inbox()` reads all and drains.\n\n```python\nclass MessageBus:\n    def send(self, sender, to, content, msg_type=\"message\", extra=None):\n        msg = {\"type\": msg_type, \"from\": sender,\n               \"content\": content, \"timestamp\": time.time()}\n        if extra:\n            msg.update(extra)\n        with open(self.dir / f\"{to}.jsonl\", \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n\n    def read_inbox(self, name):\n        path = self.dir / f\"{name}.jsonl\"\n        if not path.exists(): return \"[]\"\n        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]\n        path.write_text(\"\")  # drain\n        return json.dumps(msgs, indent=2)\n```\n\n4. Each teammate checks its inbox before every LLM call, injecting received messages into context.\n\n```python\ndef _teammate_loop(self, name, role, prompt):\n    messages = [{\"role\": \"user\", \"content\": prompt}]\n    for _ in range(50):\n        inbox = BUS.read_inbox(name)\n        if inbox != \"[]\":\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<inbox>{inbox}</inbox>\"})\n            messages.append({\"role\": \"assistant\",\n                \"content\": \"Noted inbox messages.\"})\n        response = client.messages.create(...)\n        if response.stop_reason != \"tool_use\":\n            break\n        # execute tools, append results...\n    self._find_member(name)[\"status\"] = \"idle\"\n```\n\n## What Changed From s08\n\n| Component      | Before (s08)     | After (s09)                |\n|----------------|------------------|----------------------------|\n| Tools          | 6                | 9 (+spawn/send/read_inbox) |\n| Agents         | Single           | Lead + N teammates         |\n| Persistence    | None             | config.json + JSONL inboxes|\n| Threads        | Background cmds  | Full agent loops per thread|\n| Lifecycle      | Fire-and-forget  | idle -> working -> idle    |\n| Communication  | None             | message + broadcast        |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s09_agent_teams.py\n```\n\n1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.`\n2. `Broadcast \"status update: phase 1 complete\" to all teammates`\n3. `Check the lead inbox for any messages`\n4. Type `/team` to see the team roster with statuses\n5. Type `/inbox` to manually check the lead's inbox\n"
  },
  {
    "path": "docs/en/s10-team-protocols.md",
    "content": "# s10: Team Protocols\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12`\n\n> *\"Teammates need shared communication rules\"* -- one request-response pattern drives all negotiation.\n>\n> **Harness layer**: Protocols -- structured handshakes between models.\n\n## Problem\n\nIn s09, teammates work and communicate but lack structured coordination:\n\n**Shutdown**: Killing a thread leaves files half-written and config.json stale. You need a handshake: the lead requests, the teammate approves (finish and exit) or rejects (keep working).\n\n**Plan approval**: When the lead says \"refactor the auth module,\" the teammate starts immediately. For high-risk changes, the lead should review the plan first.\n\nBoth share the same structure: one side sends a request with a unique ID, the other responds referencing that ID.\n\n## Solution\n\n```\nShutdown Protocol            Plan Approval Protocol\n==================           ======================\n\nLead             Teammate    Teammate           Lead\n  |                 |           |                 |\n  |--shutdown_req-->|           |--plan_req------>|\n  | {req_id:\"abc\"}  |           | {req_id:\"xyz\"}  |\n  |                 |           |                 |\n  |<--shutdown_resp-|           |<--plan_resp-----|\n  | {req_id:\"abc\",  |           | {req_id:\"xyz\",  |\n  |  approve:true}  |           |  approve:true}  |\n\nShared FSM:\n  [pending] --approve--> [approved]\n  [pending] --reject---> [rejected]\n\nTrackers:\n  shutdown_requests = {req_id: {target, status}}\n  plan_requests     = {req_id: {from, plan, status}}\n```\n\n## How It Works\n\n1. The lead initiates shutdown by generating a request_id and sending through the inbox.\n\n```python\nshutdown_requests = {}\n\ndef handle_shutdown_request(teammate: str) -> str:\n    req_id = str(uuid.uuid4())[:8]\n    shutdown_requests[req_id] = {\"target\": teammate, \"status\": \"pending\"}\n    BUS.send(\"lead\", teammate, \"Please shut down gracefully.\",\n             \"shutdown_request\", {\"request_id\": req_id})\n    return f\"Shutdown request {req_id} sent (status: pending)\"\n```\n\n2. The teammate receives the request and responds with approve/reject.\n\n```python\nif tool_name == \"shutdown_response\":\n    req_id = args[\"request_id\"]\n    approve = args[\"approve\"]\n    shutdown_requests[req_id][\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(sender, \"lead\", args.get(\"reason\", \"\"),\n             \"shutdown_response\",\n             {\"request_id\": req_id, \"approve\": approve})\n```\n\n3. Plan approval follows the identical pattern. The teammate submits a plan (generating a request_id), the lead reviews (referencing the same request_id).\n\n```python\nplan_requests = {}\n\ndef handle_plan_review(request_id, approve, feedback=\"\"):\n    req = plan_requests[request_id]\n    req[\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", req[\"from\"], feedback,\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n```\n\nOne FSM, two applications. The same `pending -> approved | rejected` state machine handles any request-response protocol.\n\n## What Changed From s09\n\n| Component      | Before (s09)     | After (s10)                  |\n|----------------|------------------|------------------------------|\n| Tools          | 9                | 12 (+shutdown_req/resp +plan)|\n| Shutdown       | Natural exit only| Request-response handshake   |\n| Plan gating    | None             | Submit/review with approval  |\n| Correlation    | None             | request_id per request       |\n| FSM            | None             | pending -> approved/rejected |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s10_team_protocols.py\n```\n\n1. `Spawn alice as a coder. Then request her shutdown.`\n2. `List teammates to see alice's status after shutdown approval`\n3. `Spawn bob with a risky refactoring task. Review and reject his plan.`\n4. `Spawn charlie, have him submit a plan, then approve it.`\n5. Type `/team` to monitor statuses\n"
  },
  {
    "path": "docs/en/s11-autonomous-agents.md",
    "content": "# s11: Autonomous Agents\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12`\n\n> *\"Teammates scan the board and claim tasks themselves\"* -- no need for the lead to assign each one.\n>\n> **Harness layer**: Autonomy -- models that find work without being told.\n\n## Problem\n\nIn s09-s10, teammates only work when explicitly told to. The lead must spawn each one with a specific prompt. 10 unclaimed tasks on the board? The lead assigns each one manually. Doesn't scale.\n\nTrue autonomy: teammates scan the task board themselves, claim unclaimed tasks, work on them, then look for more.\n\nOne subtlety: after context compression (s06), the agent might forget who it is. Identity re-injection fixes this.\n\n## Solution\n\n```\nTeammate lifecycle with idle cycle:\n\n+-------+\n| spawn |\n+---+---+\n    |\n    v\n+-------+   tool_use     +-------+\n| WORK  | <------------- |  LLM  |\n+---+---+                +-------+\n    |\n    | stop_reason != tool_use (or idle tool called)\n    v\n+--------+\n|  IDLE  |  poll every 5s for up to 60s\n+---+----+\n    |\n    +---> check inbox --> message? ----------> WORK\n    |\n    +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\n    |\n    +---> 60s timeout ----------------------> SHUTDOWN\n\nIdentity re-injection after compression:\n  if len(messages) <= 3:\n    messages.insert(0, identity_block)\n```\n\n## How It Works\n\n1. The teammate loop has two phases: WORK and IDLE. When the LLM stops calling tools (or calls `idle`), the teammate enters IDLE.\n\n```python\ndef _loop(self, name, role, prompt):\n    while True:\n        # -- WORK PHASE --\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        for _ in range(50):\n            response = client.messages.create(...)\n            if response.stop_reason != \"tool_use\":\n                break\n            # execute tools...\n            if idle_requested:\n                break\n\n        # -- IDLE PHASE --\n        self._set_status(name, \"idle\")\n        resume = self._idle_poll(name, messages)\n        if not resume:\n            self._set_status(name, \"shutdown\")\n            return\n        self._set_status(name, \"working\")\n```\n\n2. The idle phase polls inbox and task board in a loop.\n\n```python\ndef _idle_poll(self, name, messages):\n    for _ in range(IDLE_TIMEOUT // POLL_INTERVAL):  # 60s / 5s = 12\n        time.sleep(POLL_INTERVAL)\n        inbox = BUS.read_inbox(name)\n        if inbox:\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<inbox>{inbox}</inbox>\"})\n            return True\n        unclaimed = scan_unclaimed_tasks()\n        if unclaimed:\n            claim_task(unclaimed[0][\"id\"], name)\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<auto-claimed>Task #{unclaimed[0]['id']}: \"\n                           f\"{unclaimed[0]['subject']}</auto-claimed>\"})\n            return True\n    return False  # timeout -> shutdown\n```\n\n3. Task board scanning: find pending, unowned, unblocked tasks.\n\n```python\ndef scan_unclaimed_tasks() -> list:\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 not task.get(\"blockedBy\")):\n            unclaimed.append(task)\n    return unclaimed\n```\n\n4. Identity re-injection: when context is too short (compression happened), insert an identity block.\n\n```python\nif len(messages) <= 3:\n    messages.insert(0, {\"role\": \"user\",\n        \"content\": f\"<identity>You are '{name}', role: {role}, \"\n                   f\"team: {team_name}. Continue your work.</identity>\"})\n    messages.insert(1, {\"role\": \"assistant\",\n        \"content\": f\"I am {name}. Continuing.\"})\n```\n\n## What Changed From s10\n\n| Component      | Before (s10)     | After (s11)                |\n|----------------|------------------|----------------------------|\n| Tools          | 12               | 14 (+idle, +claim_task)    |\n| Autonomy       | Lead-directed    | Self-organizing            |\n| Idle phase     | None             | Poll inbox + task board    |\n| Task claiming  | Manual only      | Auto-claim unclaimed tasks |\n| Identity       | System prompt    | + re-injection after compress|\n| Timeout        | None             | 60s idle -> auto shutdown  |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s11_autonomous_agents.py\n```\n\n1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.`\n2. `Spawn a coder teammate and let it find work from the task board itself`\n3. `Create tasks with dependencies. Watch teammates respect the blocked order.`\n4. Type `/tasks` to see the task board with owners\n5. Type `/team` to monitor who is working vs idle\n"
  },
  {
    "path": "docs/en/s12-worktree-task-isolation.md",
    "content": "# s12: Worktree + Task Isolation\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\n\n> *\"Each works in its own directory, no interference\"* -- tasks manage goals, worktrees manage directories, bound by ID.\n>\n> **Harness layer**: Directory isolation -- parallel execution lanes that never collide.\n\n## Problem\n\nBy s11, agents can claim and complete tasks autonomously. But every task runs in one shared directory. Two agents refactoring different modules at the same time will collide: agent A edits `config.py`, agent B edits `config.py`, unstaged changes mix, and neither can roll back cleanly.\n\nThe task board tracks *what to do* but has no opinion about *where to do it*. The fix: give each task its own git worktree directory. Tasks manage goals, worktrees manage execution context. Bind them by task ID.\n\n## Solution\n\n```\nControl plane (.tasks/)             Execution plane (.worktrees/)\n+------------------+                +------------------------+\n| task_1.json      |                | auth-refactor/         |\n|   status: in_progress  <------>   branch: wt/auth-refactor\n|   worktree: \"auth-refactor\"   |   task_id: 1             |\n+------------------+                +------------------------+\n| task_2.json      |                | ui-login/              |\n|   status: pending    <------>     branch: wt/ui-login\n|   worktree: \"ui-login\"       |   task_id: 2             |\n+------------------+                +------------------------+\n                                    |\n                          index.json (worktree registry)\n                          events.jsonl (lifecycle log)\n\nState machines:\n  Task:     pending -> in_progress -> completed\n  Worktree: absent  -> active      -> removed | kept\n```\n\n## How It Works\n\n1. **Create a task.** Persist the goal first.\n\n```python\nTASKS.create(\"Implement auth refactor\")\n# -> .tasks/task_1.json  status=pending  worktree=\"\"\n```\n\n2. **Create a worktree and bind to the task.** Passing `task_id` auto-advances the task to `in_progress`.\n\n```python\nWORKTREES.create(\"auth-refactor\", task_id=1)\n# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD\n# -> index.json gets new entry, task_1.json gets worktree=\"auth-refactor\"\n```\n\nThe binding writes state to both sides:\n\n```python\ndef bind_worktree(self, task_id, worktree):\n    task = self._load(task_id)\n    task[\"worktree\"] = worktree\n    if task[\"status\"] == \"pending\":\n        task[\"status\"] = \"in_progress\"\n    self._save(task)\n```\n\n3. **Run commands in the worktree.** `cwd` points to the isolated directory.\n\n```python\nsubprocess.run(command, shell=True, cwd=worktree_path,\n               capture_output=True, text=True, timeout=300)\n```\n\n4. **Close out.** Two choices:\n   - `worktree_keep(name)` -- preserve the directory for later.\n   - `worktree_remove(name, complete_task=True)` -- remove directory, complete the bound task, emit event. One call handles teardown + completion.\n\n```python\ndef remove(self, name, force=False, complete_task=False):\n    self._run_git([\"worktree\", \"remove\", wt[\"path\"]])\n    if complete_task and wt.get(\"task_id\") is not None:\n        self.tasks.update(wt[\"task_id\"], status=\"completed\")\n        self.tasks.unbind_worktree(wt[\"task_id\"])\n        self.events.emit(\"task.completed\", ...)\n```\n\n5. **Event stream.** Every lifecycle step emits to `.worktrees/events.jsonl`:\n\n```json\n{\n  \"event\": \"worktree.remove.after\",\n  \"task\": {\"id\": 1, \"status\": \"completed\"},\n  \"worktree\": {\"name\": \"auth-refactor\", \"status\": \"removed\"},\n  \"ts\": 1730000000\n}\n```\n\nEvents emitted: `worktree.create.before/after/failed`, `worktree.remove.before/after/failed`, `worktree.keep`, `task.completed`.\n\nAfter a crash, state reconstructs from `.tasks/` + `.worktrees/index.json` on disk. Conversation memory is volatile; file state is durable.\n\n## What Changed From s11\n\n| Component          | Before (s11)               | After (s12)                                  |\n|--------------------|----------------------------|----------------------------------------------|\n| Coordination       | Task board (owner/status)  | Task board + explicit worktree binding       |\n| Execution scope    | Shared directory           | Task-scoped isolated directory               |\n| Recoverability     | Task status only           | Task status + worktree index                 |\n| Teardown           | Task completion            | Task completion + explicit keep/remove       |\n| Lifecycle visibility | Implicit in logs         | Explicit events in `.worktrees/events.jsonl` |\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s12_worktree_task_isolation.py\n```\n\n1. `Create tasks for backend auth and frontend login page, then list tasks.`\n2. `Create worktree \"auth-refactor\" for task 1, then bind task 2 to a new worktree \"ui-login\".`\n3. `Run \"git status --short\" in worktree \"auth-refactor\".`\n4. `Keep worktree \"ui-login\", then list worktrees and inspect events.`\n5. `Remove worktree \"auth-refactor\" with complete_task=true, then list tasks/worktrees/events.`\n"
  },
  {
    "path": "docs/ja/s01-the-agent-loop.md",
    "content": "# s01: The Agent Loop\n\n`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"One loop & Bash is all you need\"* -- 1つのツール + 1つのループ = エージェント。\n>\n> **Harness 層**: ループ -- モデルと現実世界を繋ぐ最初の接点。\n\n## 問題\n\n言語モデルはコードについて推論できるが、現実世界に触れられない。ファイルを読めず、テストを実行できず、エラーを確認できない。ループがなければ、ツール呼び出しのたびにユーザーが手動で結果をコピーペーストする必要がある。つまりユーザー自身がループになる。\n\n## 解決策\n\n```\n+--------+      +-------+      +---------+\n|  User  | ---> |  LLM  | ---> |  Tool   |\n| prompt |      |       |      | execute |\n+--------+      +---+---+      +----+----+\n                    ^                |\n                    |   tool_result  |\n                    +----------------+\n                    (loop until stop_reason != \"tool_use\")\n```\n\n1つの終了条件がフロー全体を制御する。モデルがツール呼び出しを止めるまでループが回り続ける。\n\n## 仕組み\n\n1. ユーザーのプロンプトが最初のメッセージになる。\n\n```python\nmessages.append({\"role\": \"user\", \"content\": query})\n```\n\n2. メッセージとツール定義をLLMに送信する。\n\n```python\nresponse = client.messages.create(\n    model=MODEL, system=SYSTEM, messages=messages,\n    tools=TOOLS, max_tokens=8000,\n)\n```\n\n3. アシスタントのレスポンスを追加し、`stop_reason`を確認する。ツールが呼ばれなければ終了。\n\n```python\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\nif response.stop_reason != \"tool_use\":\n    return\n```\n\n4. 各ツール呼び出しを実行し、結果を収集してuserメッセージとして追加。ステップ2に戻る。\n\n```python\nresults = []\nfor block in response.content:\n    if block.type == \"tool_use\":\n        output = run_bash(block.input[\"command\"])\n        results.append({\n            \"type\": \"tool_result\",\n            \"tool_use_id\": block.id,\n            \"content\": output,\n        })\nmessages.append({\"role\": \"user\", \"content\": results})\n```\n\n1つの関数にまとめると:\n\n```python\ndef agent_loop(query):\n    messages = [{\"role\": \"user\", \"content\": query}]\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                output = run_bash(block.input[\"command\"])\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output,\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n```\n\nこれでエージェント全体が30行未満に収まる。本コースの残りはすべてこのループの上に積み重なる -- ループ自体は変わらない。\n\n## 変更点\n\n| Component     | Before     | After                          |\n|---------------|------------|--------------------------------|\n| Agent loop    | (none)     | `while True` + stop_reason     |\n| Tools         | (none)     | `bash` (one tool)              |\n| Messages      | (none)     | Accumulating list              |\n| Control flow  | (none)     | `stop_reason != \"tool_use\"`    |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s01_agent_loop.py\n```\n\n1. `Create a file called hello.py that prints \"Hello, World!\"`\n2. `List all Python files in this directory`\n3. `What is the current git branch?`\n4. `Create a directory called test_output and write 3 files in it`\n"
  },
  {
    "path": "docs/ja/s02-tool-use.md",
    "content": "# s02: Tool Use\n\n`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"ツールを足すなら、ハンドラーを1つ足すだけ\"* -- ループは変わらない。新ツールは dispatch map に登録するだけ。\n>\n> **Harness 層**: ツール分配 -- モデルが届く範囲を広げる。\n\n## 問題\n\n`bash`だけでは、エージェントは何でもシェル経由で行う。`cat`は予測不能に切り詰め、`sed`は特殊文字で壊れ、すべてのbash呼び出しが制約のないセキュリティ面になる。`read_file`や`write_file`のような専用ツールなら、ツールレベルでパスのサンドボックス化を強制できる。\n\n重要な点: ツールを追加してもループの変更は不要。\n\n## 解決策\n\n```\n+--------+      +-------+      +------------------+\n|  User  | ---> |  LLM  | ---> | Tool Dispatch    |\n| prompt |      |       |      | {                |\n+--------+      +---+---+      |   bash: run_bash |\n                    ^           |   read: run_read |\n                    |           |   write: run_wr  |\n                    +-----------+   edit: run_edit |\n                    tool_result | }                |\n                                +------------------+\n\nThe dispatch map is a dict: {tool_name: handler_function}.\nOne lookup replaces any if/elif chain.\n```\n\n## 仕組み\n\n1. 各ツールにハンドラ関数を定義する。パスのサンドボックス化でワークスペース外への脱出を防ぐ。\n\n```python\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_read(path: str, limit: int = None) -> str:\n    text = safe_path(path).read_text()\n    lines = text.splitlines()\n    if limit and limit < len(lines):\n        lines = lines[:limit]\n    return \"\\n\".join(lines)[:50000]\n```\n\n2. ディスパッチマップがツール名とハンドラを結びつける。\n\n```python\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"],\n                                        kw[\"new_text\"]),\n}\n```\n\n3. ループ内で名前によりハンドラをルックアップする。ループ本体はs01から不変。\n\n```python\nfor block in response.content:\n    if block.type == \"tool_use\":\n        handler = TOOL_HANDLERS.get(block.name)\n        output = handler(**block.input) if handler \\\n            else f\"Unknown tool: {block.name}\"\n        results.append({\n            \"type\": \"tool_result\",\n            \"tool_use_id\": block.id,\n            \"content\": output,\n        })\n```\n\nツール追加 = ハンドラ追加 + スキーマ追加。ループは決して変わらない。\n\n## s01からの変更点\n\n| Component      | Before (s01)       | After (s02)                |\n|----------------|--------------------|----------------------------|\n| Tools          | 1 (bash only)      | 4 (bash, read, write, edit)|\n| Dispatch       | Hardcoded bash call | `TOOL_HANDLERS` dict       |\n| Path safety    | None               | `safe_path()` sandbox      |\n| Agent loop     | Unchanged          | Unchanged                  |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s02_tool_use.py\n```\n\n1. `Read the file requirements.txt`\n2. `Create a file called greet.py with a greet(name) function`\n3. `Edit greet.py to add a docstring to the function`\n4. `Read greet.py to verify the edit worked`\n"
  },
  {
    "path": "docs/ja/s03-todo-write.md",
    "content": "# s03: TodoWrite\n\n`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"計画のないエージェントは行き当たりばったり\"* -- まずステップを書き出し、それから実行。\n>\n> **Harness 層**: 計画 -- 航路を描かずにモデルを軌道に乗せる。\n\n## 問題\n\nマルチステップのタスクで、モデルは途中で迷子になる。作業を繰り返したり、ステップを飛ばしたり、脱線したりする。長い会話になるほど悪化する -- ツール結果がコンテキストを埋めるにつれ、システムプロンプトの影響力が薄れる。10ステップのリファクタリングでステップ1-3を完了した後、残りを忘れて即興を始めてしまう。\n\n## 解決策\n\n```\n+--------+      +-------+      +---------+\n|  User  | ---> |  LLM  | ---> | Tools   |\n| prompt |      |       |      | + todo  |\n+--------+      +---+---+      +----+----+\n                    ^                |\n                    |   tool_result  |\n                    +----------------+\n                          |\n              +-----------+-----------+\n              | TodoManager state     |\n              | [ ] task A            |\n              | [>] task B  <- doing  |\n              | [x] task C            |\n              +-----------------------+\n                          |\n              if rounds_since_todo >= 3:\n                inject <reminder> into tool_result\n```\n\n## 仕組み\n\n1. TodoManagerはアイテムのリストをステータス付きで保持する。`in_progress`にできるのは同時に1つだけ。\n\n```python\nclass TodoManager:\n    def update(self, items: list) -> str:\n        validated, in_progress_count = [], 0\n        for item in items:\n            status = item.get(\"status\", \"pending\")\n            if status == \"in_progress\":\n                in_progress_count += 1\n            validated.append({\"id\": item[\"id\"], \"text\": item[\"text\"],\n                              \"status\": status})\n        if in_progress_count > 1:\n            raise ValueError(\"Only one task can be in_progress\")\n        self.items = validated\n        return self.render()\n```\n\n2. `todo`ツールは他のツールと同様にディスパッチマップに追加される。\n\n```python\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"todo\": lambda **kw: TODO.update(kw[\"items\"]),\n}\n```\n\n3. nagリマインダーが、モデルが3ラウンド以上`todo`を呼ばなかった場合にナッジを注入する。\n\n```python\nif rounds_since_todo >= 3 and messages:\n    last = messages[-1]\n    if last[\"role\"] == \"user\" and isinstance(last.get(\"content\"), list):\n        last[\"content\"].insert(0, {\n            \"type\": \"text\",\n            \"text\": \"<reminder>Update your todos.</reminder>\",\n        })\n```\n\n「一度にin_progressは1つだけ」の制約が逐次的な集中を強制し、nagリマインダーが説明責任を生む。\n\n## s02からの変更点\n\n| Component      | Before (s02)     | After (s03)                |\n|----------------|------------------|----------------------------|\n| Tools          | 4                | 5 (+todo)                  |\n| Planning       | None             | TodoManager with statuses  |\n| Nag injection  | None             | `<reminder>` after 3 rounds|\n| Agent loop     | Simple dispatch  | + rounds_since_todo counter|\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s03_todo_write.py\n```\n\n1. `Refactor the file hello.py: add type hints, docstrings, and a main guard`\n2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py`\n3. `Review all Python files and fix any style issues`\n"
  },
  {
    "path": "docs/ja/s04-subagent.md",
    "content": "# s04: Subagents\n\n`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"大きなタスクを分割し、各サブタスクにクリーンなコンテキストを\"* -- サブエージェントは独立した messages[] を使い、メイン会話を汚さない。\n>\n> **Harness 層**: コンテキスト隔離 -- モデルの思考の明晰さを守る。\n\n## 問題\n\nエージェントが作業するにつれ、messages配列は膨張し続ける。すべてのファイル読み取り、すべてのbash出力がコンテキストに永久に残る。「このプロジェクトはどのテストフレームワークを使っているか」という質問は5つのファイルを読む必要があるかもしれないが、親に必要なのは「pytest」という答えだけだ。\n\n## 解決策\n\n```\nParent agent                     Subagent\n+------------------+             +------------------+\n| messages=[...]   |             | messages=[]      | <-- fresh\n|                  |  dispatch   |                  |\n| tool: task       | ----------> | while tool_use:  |\n|   prompt=\"...\"   |             |   call tools     |\n|                  |  summary    |   append results |\n|   result = \"...\" | <---------- | return last text |\n+------------------+             +------------------+\n\nParent context stays clean. Subagent context is discarded.\n```\n\n## 仕組み\n\n1. 親に`task`ツールを追加する。子は`task`を除くすべての基本ツールを取得する(再帰的な生成は不可)。\n\n```python\nPARENT_TOOLS = CHILD_TOOLS + [\n    {\"name\": \"task\",\n     \"description\": \"Spawn a subagent with fresh context.\",\n     \"input_schema\": {\n         \"type\": \"object\",\n         \"properties\": {\"prompt\": {\"type\": \"string\"}},\n         \"required\": [\"prompt\"],\n     }},\n]\n```\n\n2. サブエージェントは`messages=[]`で開始し、自身のループを実行する。最終テキストだけが親に返る。\n\n```python\ndef run_subagent(prompt: str) -> str:\n    sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n    for _ in range(30):  # safety limit\n        response = client.messages.create(\n            model=MODEL, system=SUBAGENT_SYSTEM,\n            messages=sub_messages,\n            tools=CHILD_TOOLS, max_tokens=8000,\n        )\n        sub_messages.append({\"role\": \"assistant\",\n                             \"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 = TOOL_HANDLERS.get(block.name)\n                output = handler(**block.input)\n                results.append({\"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": str(output)[:50000]})\n        sub_messages.append({\"role\": \"user\", \"content\": results})\n    return \"\".join(\n        b.text for b in response.content if hasattr(b, \"text\")\n    ) or \"(no summary)\"\n```\n\n子のメッセージ履歴全体(30回以上のツール呼び出し)は破棄される。親は1段落の要約を通常の`tool_result`として受け取る。\n\n## s03からの変更点\n\n| Component      | Before (s03)     | After (s04)               |\n|----------------|------------------|---------------------------|\n| Tools          | 5                | 5 (base) + task (parent)  |\n| Context        | Single shared    | Parent + child isolation  |\n| Subagent       | None             | `run_subagent()` function |\n| Return value   | N/A              | Summary text only         |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s04_subagent.py\n```\n\n1. `Use a subtask to find what testing framework this project uses`\n2. `Delegate: read all .py files and summarize what each one does`\n3. `Use a task to create a new module, then verify it from here`\n"
  },
  {
    "path": "docs/ja/s05-skill-loading.md",
    "content": "# s05: Skills\n\n`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"必要な知識を、必要な時に読み込む\"* -- system prompt ではなく tool_result で注入。\n>\n> **Harness 層**: オンデマンド知識 -- モデルが求めた時だけ渡すドメイン専門性。\n\n## 問題\n\nエージェントにドメイン固有のワークフローを遵守させたい: gitの規約、テストパターン、コードレビューチェックリスト。すべてをシステムプロンプトに入れると、使われないスキルにトークンを浪費する。10スキル x 2000トークン = 20,000トークン、ほとんどが任意のタスクに無関係だ。\n\n## 解決策\n\n```\nSystem prompt (Layer 1 -- always present):\n+--------------------------------------+\n| You are a coding agent.              |\n| Skills available:                    |\n|   - git: Git workflow helpers        |  ~100 tokens/skill\n|   - test: Testing best practices     |\n+--------------------------------------+\n\nWhen model calls load_skill(\"git\"):\n+--------------------------------------+\n| tool_result (Layer 2 -- on demand):  |\n| <skill name=\"git\">                   |\n|   Full git workflow instructions...  |  ~2000 tokens\n|   Step 1: ...                        |\n| </skill>                             |\n+--------------------------------------+\n```\n\n第1層: スキル*名*をシステムプロンプトに(低コスト)。第2層: スキル*本体*をtool_resultに(オンデマンド)。\n\n## 仕組み\n\n1. 各スキルは `SKILL.md` ファイルを含むディレクトリとして配置される。\n\n```\nskills/\n  pdf/\n    SKILL.md       # ---\\n name: pdf\\n description: Process PDF files\\n ---\\n ...\n  code-review/\n    SKILL.md       # ---\\n name: code-review\\n description: Review code\\n ---\\n ...\n```\n\n2. SkillLoaderが `SKILL.md` を再帰的に探索し、ディレクトリ名をスキル識別子として使用する。\n\n```python\nclass SkillLoader:\n    def __init__(self, skills_dir: Path):\n        self.skills = {}\n        for f in sorted(skills_dir.rglob(\"SKILL.md\")):\n            text = f.read_text()\n            meta, body = self._parse_frontmatter(text)\n            name = meta.get(\"name\", f.parent.name)\n            self.skills[name] = {\"meta\": meta, \"body\": body}\n\n    def get_descriptions(self) -> str:\n        lines = []\n        for name, skill in self.skills.items():\n            desc = skill[\"meta\"].get(\"description\", \"\")\n            lines.append(f\"  - {name}: {desc}\")\n        return \"\\n\".join(lines)\n\n    def get_content(self, name: str) -> str:\n        skill = self.skills.get(name)\n        if not skill:\n            return f\"Error: Unknown skill '{name}'.\"\n        return f\"<skill name=\\\"{name}\\\">\\n{skill['body']}\\n</skill>\"\n```\n\n3. 第1層はシステムプロンプトに配置。第2層は通常のツールハンドラ。\n\n```python\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nSkills available:\n{SKILL_LOADER.get_descriptions()}\"\"\"\n\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"load_skill\": lambda **kw: SKILL_LOADER.get_content(kw[\"name\"]),\n}\n```\n\nモデルはどのスキルが存在するかを知り(低コスト)、関連する時にだけ読み込む(高コスト)。\n\n## s04からの変更点\n\n| Component      | Before (s04)     | After (s05)                |\n|----------------|------------------|----------------------------|\n| Tools          | 5 (base + task)  | 5 (base + load_skill)      |\n| System prompt  | Static string    | + skill descriptions       |\n| Knowledge      | None             | skills/\\*/SKILL.md files   |\n| Injection      | None             | Two-layer (system + result)|\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s05_skill_loading.py\n```\n\n1. `What skills are available?`\n2. `Load the agent-builder skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n4. `Build an MCP server using the mcp-builder skill`\n"
  },
  {
    "path": "docs/ja/s06-context-compact.md",
    "content": "# s06: Context Compact\n\n`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"コンテキストはいつか溢れる、空ける手段が要る\"* -- 3層圧縮で無限セッションを実現。\n>\n> **Harness 層**: 圧縮 -- クリーンな記憶、無限のセッション。\n\n## 問題\n\nコンテキストウィンドウは有限だ。1000行のファイルに対する`read_file`1回で約4000トークンを消費する。30ファイルを読み20回のbashコマンドを実行すると、100,000トークン超。圧縮なしでは、エージェントは大規模コードベースで作業できない。\n\n## 解決策\n\n積極性を段階的に上げる3層構成:\n\n```\nEvery turn:\n+------------------+\n| Tool call result |\n+------------------+\n        |\n        v\n[Layer 1: micro_compact]        (silent, every turn)\n  Replace tool_result > 3 turns old\n  with \"[Previous: used {tool_name}]\"\n        |\n        v\n[Check: tokens > 50000?]\n   |               |\n   no              yes\n   |               |\n   v               v\ncontinue    [Layer 2: auto_compact]\n              Save transcript to .transcripts/\n              LLM summarizes conversation.\n              Replace all messages with [summary].\n                    |\n                    v\n            [Layer 3: compact tool]\n              Model calls compact explicitly.\n              Same summarization as auto_compact.\n```\n\n## 仕組み\n\n1. **第1層 -- micro_compact**: 各LLM呼び出しの前に、古いツール結果をプレースホルダーに置換する。\n\n```python\ndef micro_compact(messages: list) -> list:\n    tool_results = []\n    for i, msg in enumerate(messages):\n        if msg[\"role\"] == \"user\" and isinstance(msg.get(\"content\"), list):\n            for j, part in enumerate(msg[\"content\"]):\n                if isinstance(part, dict) and part.get(\"type\") == \"tool_result\":\n                    tool_results.append((i, j, part))\n    if len(tool_results) <= KEEP_RECENT:\n        return messages\n    for _, _, part in tool_results[:-KEEP_RECENT]:\n        if len(part.get(\"content\", \"\")) > 100:\n            part[\"content\"] = f\"[Previous: used {tool_name}]\"\n    return messages\n```\n\n2. **第2層 -- auto_compact**: トークンが閾値を超えたら、完全なトランスクリプトをディスクに保存し、LLMに要約を依頼する。\n\n```python\ndef auto_compact(messages: list) -> list:\n    # Save transcript for recovery\n    transcript_path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with open(transcript_path, \"w\") as f:\n        for msg in messages:\n            f.write(json.dumps(msg, default=str) + \"\\n\")\n    # LLM summarizes\n    response = client.messages.create(\n        model=MODEL,\n        messages=[{\"role\": \"user\", \"content\":\n            \"Summarize this conversation for continuity...\"\n            + json.dumps(messages, default=str)[:80000]}],\n        max_tokens=2000,\n    )\n    return [\n        {\"role\": \"user\", \"content\": f\"[Compressed]\\n\\n{response.content[0].text}\"},\n        {\"role\": \"assistant\", \"content\": \"Understood. Continuing.\"},\n    ]\n```\n\n3. **第3層 -- manual compact**: `compact`ツールが同じ要約処理をオンデマンドでトリガーする。\n\n4. ループが3層すべてを統合する:\n\n```python\ndef agent_loop(messages: list):\n    while True:\n        micro_compact(messages)                        # Layer 1\n        if estimate_tokens(messages) > THRESHOLD:\n            messages[:] = auto_compact(messages)       # Layer 2\n        response = client.messages.create(...)\n        # ... tool execution ...\n        if manual_compact:\n            messages[:] = auto_compact(messages)       # Layer 3\n```\n\nトランスクリプトがディスク上に完全な履歴を保持する。何も真に失われず、アクティブなコンテキストの外に移動されるだけ。\n\n## s05からの変更点\n\n| Component      | Before (s05)     | After (s06)                |\n|----------------|------------------|----------------------------|\n| Tools          | 5                | 5 (base + compact)         |\n| Context mgmt   | None             | Three-layer compression    |\n| Micro-compact  | None             | Old results -> placeholders|\n| Auto-compact   | None             | Token threshold trigger    |\n| Transcripts    | None             | Saved to .transcripts/     |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s06_context_compact.py\n```\n\n1. `Read every Python file in the agents/ directory one by one` (micro-compactが古い結果を置換するのを観察する)\n2. `Keep reading files until compression triggers automatically`\n3. `Use the compact tool to manually compress the conversation`\n"
  },
  {
    "path": "docs/ja/s07-task-system.md",
    "content": "# s07: Task System\n\n`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12`\n\n> *\"大きな目標を小タスクに分解し、順序付けし、ディスクに記録する\"* -- ファイルベースのタスクグラフ、マルチエージェント協調の基盤。\n>\n> **Harness 層**: 永続タスク -- どの会話よりも長く生きる目標。\n\n## 問題\n\ns03のTodoManagerはメモリ上のフラットなチェックリストに過ぎない: 順序なし、依存関係なし、ステータスは完了か未完了のみ。実際の目標には構造がある -- タスクBはタスクAに依存し、タスクCとDは並行実行でき、タスクEはCとDの両方を待つ。\n\n明示的な関係がなければ、エージェントは何が実行可能で、何がブロックされ、何が同時に走れるかを判断できない。しかもリストはメモリ上にしかないため、コンテキスト圧縮(s06)で消える。\n\n## 解決策\n\nフラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)・後方依存(`blocks`)を持つ。タスクグラフは常に3つの問いに答える:\n\n- **何が実行可能か?** -- `pending`ステータスで`blockedBy`が空のタスク。\n- **何がブロックされているか?** -- 未完了の依存を待つタスク。\n- **何が完了したか?** -- `completed`のタスク。完了時に後続タスクを自動的にアンブロックする。\n\n```\n.tasks/\n  task_1.json  {\"id\":1, \"status\":\"completed\"}\n  task_2.json  {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\"}\n  task_3.json  {\"id\":3, \"blockedBy\":[1], \"status\":\"pending\"}\n  task_4.json  {\"id\":4, \"blockedBy\":[2,3], \"status\":\"pending\"}\n\nタスクグラフ (DAG):\n                 +----------+\n            +--> | task 2   | --+\n            |    | pending  |   |\n+----------+     +----------+    +--> +----------+\n| task 1   |                          | task 4   |\n| completed| --> +----------+    +--> | blocked  |\n+----------+     | task 3   | --+     +----------+\n                 | pending  |\n                 +----------+\n\n順序:       task 1 は 2 と 3 より先に完了する必要がある\n並行:       task 2 と 3 は同時に実行できる\n依存:       task 4 は 2 と 3 の両方を待つ\nステータス: pending -> in_progress -> completed\n```\n\nこのタスクグラフは s07 以降の全メカニズムの協調バックボーンとなる: バックグラウンド実行(s08)、マルチエージェントチーム(s09+)、worktree分離(s12)はすべてこの同じ構造を読み書きする。\n\n## 仕組み\n\n1. **TaskManager**: タスクごとに1つのJSONファイル、依存グラフ付きCRUD。\n\n```python\nclass TaskManager:\n    def __init__(self, tasks_dir: Path):\n        self.dir = tasks_dir\n        self.dir.mkdir(exist_ok=True)\n        self._next_id = self._max_id() + 1\n\n    def create(self, subject, description=\"\"):\n        task = {\"id\": self._next_id, \"subject\": subject,\n                \"status\": \"pending\", \"blockedBy\": [],\n                \"blocks\": [], \"owner\": \"\"}\n        self._save(task)\n        self._next_id += 1\n        return json.dumps(task, indent=2)\n```\n\n2. **依存解除**: タスク完了時に、他タスクの`blockedBy`リストから完了IDを除去し、後続タスクをアンブロックする。\n\n```python\ndef _clear_dependency(self, completed_id):\n    for f in self.dir.glob(\"task_*.json\"):\n        task = json.loads(f.read_text())\n        if completed_id in task.get(\"blockedBy\", []):\n            task[\"blockedBy\"].remove(completed_id)\n            self._save(task)\n```\n\n3. **ステータス遷移 + 依存配線**: `update`がステータス変更と依存エッジを担う。\n\n```python\ndef update(self, task_id, status=None,\n           add_blocked_by=None, add_blocks=None):\n    task = self._load(task_id)\n    if status:\n        task[\"status\"] = status\n        if status == \"completed\":\n            self._clear_dependency(task_id)\n    self._save(task)\n```\n\n4. 4つのタスクツールをディスパッチマップに追加する。\n\n```python\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"]),\n    \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"], kw.get(\"status\")),\n    \"task_list\":   lambda **kw: TASKS.list_all(),\n    \"task_get\":    lambda **kw: TASKS.get(kw[\"task_id\"]),\n}\n```\n\ns07以降、タスクグラフがマルチステップ作業のデフォルト。s03のTodoは軽量な単一セッション用チェックリストとして残る。\n\n## s06からの変更点\n\n| コンポーネント | Before (s06) | After (s07) |\n|---|---|---|\n| Tools | 5 | 8 (`task_create/update/list/get`) |\n| 計画モデル | フラットチェックリスト (メモリ) | 依存関係付きタスクグラフ (ディスク) |\n| 関係 | なし | `blockedBy` + `blocks` エッジ |\n| ステータス追跡 | 完了か未完了 | `pending` -> `in_progress` -> `completed` |\n| 永続性 | 圧縮で消失 | 圧縮・再起動後も存続 |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s07_task_system.py\n```\n\n1. `Create 3 tasks: \"Setup project\", \"Write code\", \"Write tests\". Make them depend on each other in order.`\n2. `List all tasks and show the dependency graph`\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\n"
  },
  {
    "path": "docs/ja/s08-background-tasks.md",
    "content": "# s08: Background Tasks\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12`\n\n> *\"遅い操作はバックグラウンドへ、エージェントは次を考え続ける\"* -- デーモンスレッドがコマンド実行、完了後に通知を注入。\n>\n> **Harness 層**: バックグラウンド実行 -- モデルが考え続ける間、Harness が待つ。\n\n## 問題\n\n一部のコマンドは数分かかる: `npm install`、`pytest`、`docker build`。ブロッキングループでは、モデルはサブプロセスの完了を待って座っている。ユーザーが「依存関係をインストールして、その間にconfigファイルを作って」と言っても、エージェントは並列ではなく逐次的に処理する。\n\n## 解決策\n\n```\nMain thread                Background thread\n+-----------------+        +-----------------+\n| agent loop      |        | subprocess runs |\n| ...             |        | ...             |\n| [LLM call] <---+------- | enqueue(result) |\n|  ^drain queue   |        +-----------------+\n+-----------------+\n\nTimeline:\nAgent --[spawn A]--[spawn B]--[other work]----\n             |          |\n             v          v\n          [A runs]   [B runs]      (parallel)\n             |          |\n             +-- results injected before next LLM call --+\n```\n\n## 仕組み\n\n1. BackgroundManagerがスレッドセーフな通知キューでタスクを追跡する。\n\n```python\nclass BackgroundManager:\n    def __init__(self):\n        self.tasks = {}\n        self._notification_queue = []\n        self._lock = threading.Lock()\n```\n\n2. `run()`がデーモンスレッドを開始し、即座にリターンする。\n\n```python\ndef run(self, command: str) -> str:\n    task_id = str(uuid.uuid4())[:8]\n    self.tasks[task_id] = {\"status\": \"running\", \"command\": command}\n    thread = threading.Thread(\n        target=self._execute, args=(task_id, command), daemon=True)\n    thread.start()\n    return f\"Background task {task_id} started\"\n```\n\n3. サブプロセス完了時に、結果を通知キューへ。\n\n```python\ndef _execute(self, task_id, command):\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n            capture_output=True, text=True, timeout=300)\n        output = (r.stdout + r.stderr).strip()[:50000]\n    except subprocess.TimeoutExpired:\n        output = \"Error: Timeout (300s)\"\n    with self._lock:\n        self._notification_queue.append({\n            \"task_id\": task_id, \"result\": output[:500]})\n```\n\n4. エージェントループが各LLM呼び出しの前に通知をドレインする。\n\n```python\ndef agent_loop(messages: list):\n    while True:\n        notifs = BG.drain_notifications()\n        if notifs:\n            notif_text = \"\\n\".join(\n                f\"[bg:{n['task_id']}] {n['result']}\" for n in notifs)\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<background-results>\\n{notif_text}\\n\"\n                           f\"</background-results>\"})\n            messages.append({\"role\": \"assistant\",\n                \"content\": \"Noted background results.\"})\n        response = client.messages.create(...)\n```\n\nループはシングルスレッドのまま。サブプロセスI/Oだけが並列化される。\n\n## s07からの変更点\n\n| Component      | Before (s07)     | After (s08)                |\n|----------------|------------------|----------------------------|\n| Tools          | 8                | 6 (base + background_run + check)|\n| Execution      | Blocking only    | Blocking + background threads|\n| Notification   | None             | Queue drained per loop     |\n| Concurrency    | None             | Daemon threads             |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s08_background_tasks.py\n```\n\n1. `Run \"sleep 5 && echo done\" in the background, then create a file while it runs`\n2. `Start 3 background tasks: \"sleep 2\", \"sleep 4\", \"sleep 6\". Check their status.`\n3. `Run pytest in the background and keep working on other things`\n"
  },
  {
    "path": "docs/ja/s09-agent-teams.md",
    "content": "# s09: Agent Teams\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12`\n\n> *\"一人で終わらないなら、チームメイトに任せる\"* -- 永続チームメイト + 非同期メールボックス。\n>\n> **Harness 層**: チームメールボックス -- 複数モデルをファイルで協調。\n\n## 問題\n\nサブエージェント(s04)は使い捨てだ: 生成し、作業し、要約を返し、消滅する。アイデンティティもなく、呼び出し間の記憶もない。バックグラウンドタスク(s08)はシェルコマンドを実行するが、LLM誘導の意思決定はできない。\n\n本物のチームワークには: (1)単一プロンプトを超えて存続する永続エージェント、(2)アイデンティティとライフサイクル管理、(3)エージェント間の通信チャネルが必要だ。\n\n## 解決策\n\n```\nTeammate lifecycle:\n  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\n\nCommunication:\n  .team/\n    config.json           <- team roster + statuses\n    inbox/\n      alice.jsonl         <- append-only, drain-on-read\n      bob.jsonl\n      lead.jsonl\n\n              +--------+    send(\"alice\",\"bob\",\"...\")    +--------+\n              | alice  | -----------------------------> |  bob   |\n              | loop   |    bob.jsonl << {json_line}    |  loop  |\n              +--------+                                +--------+\n                   ^                                         |\n                   |        BUS.read_inbox(\"alice\")          |\n                   +---- alice.jsonl -> read + drain ---------+\n```\n\n## 仕組み\n\n1. TeammateManagerがconfig.jsonでチーム名簿を管理する。\n\n```python\nclass TeammateManager:\n    def __init__(self, team_dir: Path):\n        self.dir = team_dir\n        self.dir.mkdir(exist_ok=True)\n        self.config_path = self.dir / \"config.json\"\n        self.config = self._load_config()\n        self.threads = {}\n```\n\n2. `spawn()`がチームメイトを作成し、そのエージェントループをスレッドで開始する。\n\n```python\ndef spawn(self, name: str, role: str, prompt: str) -> str:\n    member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n    self.config[\"members\"].append(member)\n    self._save_config()\n    thread = threading.Thread(\n        target=self._teammate_loop,\n        args=(name, role, prompt), daemon=True)\n    thread.start()\n    return f\"Spawned teammate '{name}' (role: {role})\"\n```\n\n3. MessageBus: 追記専用のJSONLインボックス。`send()`がJSON行を追記し、`read_inbox()`がすべて読み取ってドレインする。\n\n```python\nclass MessageBus:\n    def send(self, sender, to, content, msg_type=\"message\", extra=None):\n        msg = {\"type\": msg_type, \"from\": sender,\n               \"content\": content, \"timestamp\": time.time()}\n        if extra:\n            msg.update(extra)\n        with open(self.dir / f\"{to}.jsonl\", \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n\n    def read_inbox(self, name):\n        path = self.dir / f\"{name}.jsonl\"\n        if not path.exists(): return \"[]\"\n        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]\n        path.write_text(\"\")  # drain\n        return json.dumps(msgs, indent=2)\n```\n\n4. 各チームメイトは各LLM呼び出しの前にインボックスを確認し、受信メッセージをコンテキストに注入する。\n\n```python\ndef _teammate_loop(self, name, role, prompt):\n    messages = [{\"role\": \"user\", \"content\": prompt}]\n    for _ in range(50):\n        inbox = BUS.read_inbox(name)\n        if inbox != \"[]\":\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<inbox>{inbox}</inbox>\"})\n            messages.append({\"role\": \"assistant\",\n                \"content\": \"Noted inbox messages.\"})\n        response = client.messages.create(...)\n        if response.stop_reason != \"tool_use\":\n            break\n        # execute tools, append results...\n    self._find_member(name)[\"status\"] = \"idle\"\n```\n\n## s08からの変更点\n\n| Component      | Before (s08)     | After (s09)                |\n|----------------|------------------|----------------------------|\n| Tools          | 6                | 9 (+spawn/send/read_inbox) |\n| Agents         | Single           | Lead + N teammates         |\n| Persistence    | None             | config.json + JSONL inboxes|\n| Threads        | Background cmds  | Full agent loops per thread|\n| Lifecycle      | Fire-and-forget  | idle -> working -> idle    |\n| Communication  | None             | message + broadcast        |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s09_agent_teams.py\n```\n\n1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.`\n2. `Broadcast \"status update: phase 1 complete\" to all teammates`\n3. `Check the lead inbox for any messages`\n4. `/team`と入力してステータス付きのチーム名簿を確認する\n5. `/inbox`と入力してリーダーのインボックスを手動確認する\n"
  },
  {
    "path": "docs/ja/s10-team-protocols.md",
    "content": "# s10: Team Protocols\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12`\n\n> *\"チームメイト間には統一の通信ルールが必要\"* -- 1つの request-response パターンが全交渉を駆動。\n>\n> **Harness 層**: プロトコル -- モデル間の構造化されたハンドシェイク。\n\n## 問題\n\ns09ではチームメイトが作業し通信するが、構造化された協調がない:\n\n**シャットダウン**: スレッドを強制終了するとファイルが中途半端に書かれ、config.jsonが不正な状態になる。ハンドシェイクが必要 -- リーダーが要求し、チームメイトが承認(完了して退出)か拒否(作業継続)する。\n\n**プラン承認**: リーダーが「認証モジュールをリファクタリングして」と言うと、チームメイトは即座に開始する。リスクの高い変更では、実行前にリーダーが計画をレビューすべきだ。\n\n両方とも同じ構造: 一方がユニークIDを持つリクエストを送り、他方がそのIDで応答する。\n\n## 解決策\n\n```\nShutdown Protocol            Plan Approval Protocol\n==================           ======================\n\nLead             Teammate    Teammate           Lead\n  |                 |           |                 |\n  |--shutdown_req-->|           |--plan_req------>|\n  | {req_id:\"abc\"}  |           | {req_id:\"xyz\"}  |\n  |                 |           |                 |\n  |<--shutdown_resp-|           |<--plan_resp-----|\n  | {req_id:\"abc\",  |           | {req_id:\"xyz\",  |\n  |  approve:true}  |           |  approve:true}  |\n\nShared FSM:\n  [pending] --approve--> [approved]\n  [pending] --reject---> [rejected]\n\nTrackers:\n  shutdown_requests = {req_id: {target, status}}\n  plan_requests     = {req_id: {from, plan, status}}\n```\n\n## 仕組み\n\n1. リーダーがrequest_idを生成し、インボックス経由でシャットダウンを開始する。\n\n```python\nshutdown_requests = {}\n\ndef handle_shutdown_request(teammate: str) -> str:\n    req_id = str(uuid.uuid4())[:8]\n    shutdown_requests[req_id] = {\"target\": teammate, \"status\": \"pending\"}\n    BUS.send(\"lead\", teammate, \"Please shut down gracefully.\",\n             \"shutdown_request\", {\"request_id\": req_id})\n    return f\"Shutdown request {req_id} sent (status: pending)\"\n```\n\n2. チームメイトがリクエストを受信し、承認または拒否で応答する。\n\n```python\nif tool_name == \"shutdown_response\":\n    req_id = args[\"request_id\"]\n    approve = args[\"approve\"]\n    shutdown_requests[req_id][\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(sender, \"lead\", args.get(\"reason\", \"\"),\n             \"shutdown_response\",\n             {\"request_id\": req_id, \"approve\": approve})\n```\n\n3. プラン承認も同一パターン。チームメイトがプランを提出(request_idを生成)、リーダーがレビュー(同じrequest_idを参照)。\n\n```python\nplan_requests = {}\n\ndef handle_plan_review(request_id, approve, feedback=\"\"):\n    req = plan_requests[request_id]\n    req[\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", req[\"from\"], feedback,\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n```\n\n1つのFSM、2つの応用。同じ`pending -> approved | rejected`状態機械が、あらゆるリクエスト-レスポンスプロトコルに適用できる。\n\n## s09からの変更点\n\n| Component      | Before (s09)     | After (s10)                  |\n|----------------|------------------|------------------------------|\n| Tools          | 9                | 12 (+shutdown_req/resp +plan)|\n| Shutdown       | Natural exit only| Request-response handshake   |\n| Plan gating    | None             | Submit/review with approval  |\n| Correlation    | None             | request_id per request       |\n| FSM            | None             | pending -> approved/rejected |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s10_team_protocols.py\n```\n\n1. `Spawn alice as a coder. Then request her shutdown.`\n2. `List teammates to see alice's status after shutdown approval`\n3. `Spawn bob with a risky refactoring task. Review and reject his plan.`\n4. `Spawn charlie, have him submit a plan, then approve it.`\n5. `/team`と入力してステータスを監視する\n"
  },
  {
    "path": "docs/ja/s11-autonomous-agents.md",
    "content": "# s11: Autonomous Agents\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12`\n\n> *\"チームメイトが自らボードを見て、仕事を取る\"* -- リーダーが逐一割り振る必要はない。\n>\n> **Harness 層**: 自律 -- 指示なしで仕事を見つけるモデル。\n\n## 問題\n\ns09-s10では、チームメイトは明示的に指示された時のみ作業する。リーダーは各チームメイトを特定のプロンプトでspawnしなければならない。タスクボードに未割り当てのタスクが10個あっても、リーダーが手動で各タスクを割り当てる。これはスケールしない。\n\n真の自律性とは、チームメイトが自分で作業を見つけること: タスクボードをスキャンし、未確保のタスクを確保し、作業し、完了したら次を探す。\n\nもう1つの問題: コンテキスト圧縮(s06)後にエージェントが自分の正体を忘れる可能性がある。アイデンティティ再注入がこれを解決する。\n\n## 解決策\n\n```\nTeammate lifecycle with idle cycle:\n\n+-------+\n| spawn |\n+---+---+\n    |\n    v\n+-------+   tool_use     +-------+\n| WORK  | <------------- |  LLM  |\n+---+---+                +-------+\n    |\n    | stop_reason != tool_use (or idle tool called)\n    v\n+--------+\n|  IDLE  |  poll every 5s for up to 60s\n+---+----+\n    |\n    +---> check inbox --> message? ----------> WORK\n    |\n    +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\n    |\n    +---> 60s timeout ----------------------> SHUTDOWN\n\nIdentity re-injection after compression:\n  if len(messages) <= 3:\n    messages.insert(0, identity_block)\n```\n\n## 仕組み\n\n1. チームメイトのループはWORKとIDLEの2フェーズ。LLMがツール呼び出しを止めた時(または`idle`ツールを呼んだ時)、IDLEフェーズに入る。\n\n```python\ndef _loop(self, name, role, prompt):\n    while True:\n        # -- WORK PHASE --\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        for _ in range(50):\n            response = client.messages.create(...)\n            if response.stop_reason != \"tool_use\":\n                break\n            # execute tools...\n            if idle_requested:\n                break\n\n        # -- IDLE PHASE --\n        self._set_status(name, \"idle\")\n        resume = self._idle_poll(name, messages)\n        if not resume:\n            self._set_status(name, \"shutdown\")\n            return\n        self._set_status(name, \"working\")\n```\n\n2. IDLEフェーズがインボックスとタスクボードをポーリングする。\n\n```python\ndef _idle_poll(self, name, messages):\n    for _ in range(IDLE_TIMEOUT // POLL_INTERVAL):  # 60s / 5s = 12\n        time.sleep(POLL_INTERVAL)\n        inbox = BUS.read_inbox(name)\n        if inbox:\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<inbox>{inbox}</inbox>\"})\n            return True\n        unclaimed = scan_unclaimed_tasks()\n        if unclaimed:\n            claim_task(unclaimed[0][\"id\"], name)\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<auto-claimed>Task #{unclaimed[0]['id']}: \"\n                           f\"{unclaimed[0]['subject']}</auto-claimed>\"})\n            return True\n    return False  # timeout -> shutdown\n```\n\n3. タスクボードスキャン: pendingかつ未割り当てかつブロックされていないタスクを探す。\n\n```python\ndef scan_unclaimed_tasks() -> list:\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 not task.get(\"blockedBy\")):\n            unclaimed.append(task)\n    return unclaimed\n```\n\n4. アイデンティティ再注入: コンテキストが短すぎる(圧縮が起きた)場合にアイデンティティブロックを挿入する。\n\n```python\nif len(messages) <= 3:\n    messages.insert(0, {\"role\": \"user\",\n        \"content\": f\"<identity>You are '{name}', role: {role}, \"\n                   f\"team: {team_name}. Continue your work.</identity>\"})\n    messages.insert(1, {\"role\": \"assistant\",\n        \"content\": f\"I am {name}. Continuing.\"})\n```\n\n## s10からの変更点\n\n| Component      | Before (s10)     | After (s11)                |\n|----------------|------------------|----------------------------|\n| Tools          | 12               | 14 (+idle, +claim_task)    |\n| Autonomy       | Lead-directed    | Self-organizing            |\n| Idle phase     | None             | Poll inbox + task board    |\n| Task claiming  | Manual only      | Auto-claim unclaimed tasks |\n| Identity       | System prompt    | + re-injection after compress|\n| Timeout        | None             | 60s idle -> auto shutdown  |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s11_autonomous_agents.py\n```\n\n1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.`\n2. `Spawn a coder teammate and let it find work from the task board itself`\n3. `Create tasks with dependencies. Watch teammates respect the blocked order.`\n4. `/tasks`と入力してオーナー付きのタスクボードを確認する\n5. `/team`と入力して誰が作業中でアイドルかを監視する\n"
  },
  {
    "path": "docs/ja/s12-worktree-task-isolation.md",
    "content": "# s12: Worktree + Task Isolation\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\n\n> *\"各自のディレクトリで作業し、互いに干渉しない\"* -- タスクは目標を管理、worktree はディレクトリを管理、IDで紐付け。\n>\n> **Harness 層**: ディレクトリ隔離 -- 決して衝突しない並列実行レーン。\n\n## 問題\n\ns11までにエージェントはタスクを自律的に確保して完了できるようになった。しかし全タスクが1つの共有ディレクトリで走る。2つのエージェントが同時に異なるモジュールをリファクタリングすると衝突する: 片方が`config.py`を編集し、もう片方も`config.py`を編集し、未コミットの変更が混ざり合い、どちらもクリーンにロールバックできない。\n\nタスクボードは*何をやるか*を追跡するが、*どこでやるか*には関知しない。解決策: 各タスクに専用のgit worktreeディレクトリを与える。タスクが目標を管理し、worktreeが実行コンテキストを管理する。タスクIDで紐付ける。\n\n## 解決策\n\n```\nControl plane (.tasks/)             Execution plane (.worktrees/)\n+------------------+                +------------------------+\n| task_1.json      |                | auth-refactor/         |\n|   status: in_progress  <------>   branch: wt/auth-refactor\n|   worktree: \"auth-refactor\"   |   task_id: 1             |\n+------------------+                +------------------------+\n| task_2.json      |                | ui-login/              |\n|   status: pending    <------>     branch: wt/ui-login\n|   worktree: \"ui-login\"       |   task_id: 2             |\n+------------------+                +------------------------+\n                                    |\n                          index.json (worktree registry)\n                          events.jsonl (lifecycle log)\n\nState machines:\n  Task:     pending -> in_progress -> completed\n  Worktree: absent  -> active      -> removed | kept\n```\n\n## 仕組み\n\n1. **タスクを作成する。** まず目標を永続化する。\n\n```python\nTASKS.create(\"Implement auth refactor\")\n# -> .tasks/task_1.json  status=pending  worktree=\"\"\n```\n\n2. **worktreeを作成してタスクに紐付ける。** `task_id`を渡すと、タスクが自動的に`in_progress`に遷移する。\n\n```python\nWORKTREES.create(\"auth-refactor\", task_id=1)\n# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD\n# -> index.json gets new entry, task_1.json gets worktree=\"auth-refactor\"\n```\n\n紐付けは両側に状態を書き込む:\n\n```python\ndef bind_worktree(self, task_id, worktree):\n    task = self._load(task_id)\n    task[\"worktree\"] = worktree\n    if task[\"status\"] == \"pending\":\n        task[\"status\"] = \"in_progress\"\n    self._save(task)\n```\n\n3. **worktree内でコマンドを実行する。** `cwd`が分離ディレクトリを指す。\n\n```python\nsubprocess.run(command, shell=True, cwd=worktree_path,\n               capture_output=True, text=True, timeout=300)\n```\n\n4. **終了処理。** 2つの選択肢:\n   - `worktree_keep(name)` -- ディレクトリを保持する。\n   - `worktree_remove(name, complete_task=True)` -- ディレクトリを削除し、紐付けられたタスクを完了し、イベントを発行する。1回の呼び出しで後片付けと完了を処理する。\n\n```python\ndef remove(self, name, force=False, complete_task=False):\n    self._run_git([\"worktree\", \"remove\", wt[\"path\"]])\n    if complete_task and wt.get(\"task_id\") is not None:\n        self.tasks.update(wt[\"task_id\"], status=\"completed\")\n        self.tasks.unbind_worktree(wt[\"task_id\"])\n        self.events.emit(\"task.completed\", ...)\n```\n\n5. **イベントストリーム。** ライフサイクルの各ステップが`.worktrees/events.jsonl`に記録される:\n\n```json\n{\n  \"event\": \"worktree.remove.after\",\n  \"task\": {\"id\": 1, \"status\": \"completed\"},\n  \"worktree\": {\"name\": \"auth-refactor\", \"status\": \"removed\"},\n  \"ts\": 1730000000\n}\n```\n\n発行されるイベント: `worktree.create.before/after/failed`, `worktree.remove.before/after/failed`, `worktree.keep`, `task.completed`。\n\nクラッシュ後も`.tasks/` + `.worktrees/index.json`から状態を再構築できる。会話メモリは揮発性だが、ファイル状態は永続的だ。\n\n## s11からの変更点\n\n| Component          | Before (s11)               | After (s12)                                  |\n|--------------------|----------------------------|----------------------------------------------|\n| Coordination       | Task board (owner/status)  | Task board + explicit worktree binding       |\n| Execution scope    | Shared directory           | Task-scoped isolated directory               |\n| Recoverability     | Task status only           | Task status + worktree index                 |\n| Teardown           | Task completion            | Task completion + explicit keep/remove       |\n| Lifecycle visibility | Implicit in logs         | Explicit events in `.worktrees/events.jsonl` |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s12_worktree_task_isolation.py\n```\n\n1. `Create tasks for backend auth and frontend login page, then list tasks.`\n2. `Create worktree \"auth-refactor\" for task 1, then bind task 2 to a new worktree \"ui-login\".`\n3. `Run \"git status --short\" in worktree \"auth-refactor\".`\n4. `Keep worktree \"ui-login\", then list worktrees and inspect events.`\n5. `Remove worktree \"auth-refactor\" with complete_task=true, then list tasks/worktrees/events.`\n"
  },
  {
    "path": "docs/zh/s01-the-agent-loop.md",
    "content": "# s01: The Agent Loop (智能体循环)\n\n`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"One loop & Bash is all you need\"* -- 一个工具 + 一个循环 = 一个智能体。\n>\n> **Harness 层**: 循环 -- 模型与真实世界的第一道连接。\n\n## 问题\n\n语言模型能推理代码, 但碰不到真实世界 -- 不能读文件、跑测试、看报错。没有循环, 每次工具调用你都得手动把结果粘回去。你自己就是那个循环。\n\n## 解决方案\n\n```\n+--------+      +-------+      +---------+\n|  User  | ---> |  LLM  | ---> |  Tool   |\n| prompt |      |       |      | execute |\n+--------+      +---+---+      +----+----+\n                    ^                |\n                    |   tool_result  |\n                    +----------------+\n                    (loop until stop_reason != \"tool_use\")\n```\n\n一个退出条件控制整个流程。循环持续运行, 直到模型不再调用工具。\n\n## 工作原理\n\n1. 用户 prompt 作为第一条消息。\n\n```python\nmessages.append({\"role\": \"user\", \"content\": query})\n```\n\n2. 将消息和工具定义一起发给 LLM。\n\n```python\nresponse = client.messages.create(\n    model=MODEL, system=SYSTEM, messages=messages,\n    tools=TOOLS, max_tokens=8000,\n)\n```\n\n3. 追加助手响应。检查 `stop_reason` -- 如果模型没有调用工具, 结束。\n\n```python\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\nif response.stop_reason != \"tool_use\":\n    return\n```\n\n4. 执行每个工具调用, 收集结果, 作为 user 消息追加。回到第 2 步。\n\n```python\nresults = []\nfor block in response.content:\n    if block.type == \"tool_use\":\n        output = run_bash(block.input[\"command\"])\n        results.append({\n            \"type\": \"tool_result\",\n            \"tool_use_id\": block.id,\n            \"content\": output,\n        })\nmessages.append({\"role\": \"user\", \"content\": results})\n```\n\n组装为一个完整函数:\n\n```python\ndef agent_loop(query):\n    messages = [{\"role\": \"user\", \"content\": query}]\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                output = run_bash(block.input[\"command\"])\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output,\n                })\n        messages.append({\"role\": \"user\", \"content\": results})\n```\n\n不到 30 行, 这就是整个智能体。后面 11 个章节都在这个循环上叠加机制 -- 循环本身始终不变。\n\n## 变更内容\n\n| 组件          | 之前       | 之后                           |\n|---------------|------------|--------------------------------|\n| Agent loop    | (无)       | `while True` + stop_reason     |\n| Tools         | (无)       | `bash` (单一工具)              |\n| Messages      | (无)       | 累积式消息列表                 |\n| Control flow  | (无)       | `stop_reason != \"tool_use\"`    |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s01_agent_loop.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Create a file called hello.py that prints \"Hello, World!\"`\n2. `List all Python files in this directory`\n3. `What is the current git branch?`\n4. `Create a directory called test_output and write 3 files in it`\n"
  },
  {
    "path": "docs/zh/s02-tool-use.md",
    "content": "# s02: Tool Use (工具使用)\n\n`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"加一个工具, 只加一个 handler\"* -- 循环不用动, 新工具注册进 dispatch map 就行。\n>\n> **Harness 层**: 工具分发 -- 扩展模型能触达的边界。\n\n## 问题\n\n只有 `bash` 时, 所有操作都走 shell。`cat` 截断不可预测, `sed` 遇到特殊字符就崩, 每次 bash 调用都是不受约束的安全面。专用工具 (`read_file`, `write_file`) 可以在工具层面做路径沙箱。\n\n关键洞察: 加工具不需要改循环。\n\n## 解决方案\n\n```\n+--------+      +-------+      +------------------+\n|  User  | ---> |  LLM  | ---> | Tool Dispatch    |\n| prompt |      |       |      | {                |\n+--------+      +---+---+      |   bash: run_bash |\n                    ^           |   read: run_read |\n                    |           |   write: run_wr  |\n                    +-----------+   edit: run_edit |\n                    tool_result | }                |\n                                +------------------+\n\nThe dispatch map is a dict: {tool_name: handler_function}.\nOne lookup replaces any if/elif chain.\n```\n\n## 工作原理\n\n1. 每个工具有一个处理函数。路径沙箱防止逃逸工作区。\n\n```python\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_read(path: str, limit: int = None) -> str:\n    text = safe_path(path).read_text()\n    lines = text.splitlines()\n    if limit and limit < len(lines):\n        lines = lines[:limit]\n    return \"\\n\".join(lines)[:50000]\n```\n\n2. dispatch map 将工具名映射到处理函数。\n\n```python\nTOOL_HANDLERS = {\n    \"bash\":       lambda **kw: run_bash(kw[\"command\"]),\n    \"read_file\":  lambda **kw: run_read(kw[\"path\"], kw.get(\"limit\")),\n    \"write_file\": lambda **kw: run_write(kw[\"path\"], kw[\"content\"]),\n    \"edit_file\":  lambda **kw: run_edit(kw[\"path\"], kw[\"old_text\"],\n                                        kw[\"new_text\"]),\n}\n```\n\n3. 循环中按名称查找处理函数。循环体本身与 s01 完全一致。\n\n```python\nfor block in response.content:\n    if block.type == \"tool_use\":\n        handler = TOOL_HANDLERS.get(block.name)\n        output = handler(**block.input) if handler \\\n            else f\"Unknown tool: {block.name}\"\n        results.append({\n            \"type\": \"tool_result\",\n            \"tool_use_id\": block.id,\n            \"content\": output,\n        })\n```\n\n加工具 = 加 handler + 加 schema。循环永远不变。\n\n## 相对 s01 的变更\n\n| 组件           | 之前 (s01)         | 之后 (s02)                     |\n|----------------|--------------------|--------------------------------|\n| Tools          | 1 (仅 bash)        | 4 (bash, read, write, edit)    |\n| Dispatch       | 硬编码 bash 调用   | `TOOL_HANDLERS` 字典           |\n| 路径安全       | 无                 | `safe_path()` 沙箱             |\n| Agent loop     | 不变               | 不变                           |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s02_tool_use.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Read the file requirements.txt`\n2. `Create a file called greet.py with a greet(name) function`\n3. `Edit greet.py to add a docstring to the function`\n4. `Read greet.py to verify the edit worked`\n"
  },
  {
    "path": "docs/zh/s03-todo-write.md",
    "content": "# s03: TodoWrite (待办写入)\n\n`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"没有计划的 agent 走哪算哪\"* -- 先列步骤再动手, 完成率翻倍。\n>\n> **Harness 层**: 规划 -- 让模型不偏航, 但不替它画航线。\n\n## 问题\n\n多步任务中, 模型会丢失进度 -- 重复做过的事、跳步、跑偏。对话越长越严重: 工具结果不断填满上下文, 系统提示的影响力逐渐被稀释。一个 10 步重构可能做完 1-3 步就开始即兴发挥, 因为 4-10 步已经被挤出注意力了。\n\n## 解决方案\n\n```\n+--------+      +-------+      +---------+\n|  User  | ---> |  LLM  | ---> | Tools   |\n| prompt |      |       |      | + todo  |\n+--------+      +---+---+      +----+----+\n                    ^                |\n                    |   tool_result  |\n                    +----------------+\n                          |\n              +-----------+-----------+\n              | TodoManager state     |\n              | [ ] task A            |\n              | [>] task B  <- doing  |\n              | [x] task C            |\n              +-----------------------+\n                          |\n              if rounds_since_todo >= 3:\n                inject <reminder> into tool_result\n```\n\n## 工作原理\n\n1. TodoManager 存储带状态的项目。同一时间只允许一个 `in_progress`。\n\n```python\nclass TodoManager:\n    def update(self, items: list) -> str:\n        validated, in_progress_count = [], 0\n        for item in items:\n            status = item.get(\"status\", \"pending\")\n            if status == \"in_progress\":\n                in_progress_count += 1\n            validated.append({\"id\": item[\"id\"], \"text\": item[\"text\"],\n                              \"status\": status})\n        if in_progress_count > 1:\n            raise ValueError(\"Only one task can be in_progress\")\n        self.items = validated\n        return self.render()\n```\n\n2. `todo` 工具和其他工具一样加入 dispatch map。\n\n```python\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"todo\": lambda **kw: TODO.update(kw[\"items\"]),\n}\n```\n\n3. nag reminder: 模型连续 3 轮以上不调用 `todo` 时注入提醒。\n\n```python\nif rounds_since_todo >= 3 and messages:\n    last = messages[-1]\n    if last[\"role\"] == \"user\" and isinstance(last.get(\"content\"), list):\n        last[\"content\"].insert(0, {\n            \"type\": \"text\",\n            \"text\": \"<reminder>Update your todos.</reminder>\",\n        })\n```\n\n\"同时只能有一个 in_progress\" 强制顺序聚焦。nag reminder 制造问责压力 -- 你不更新计划, 系统就追着你问。\n\n## 相对 s02 的变更\n\n| 组件           | 之前 (s02)       | 之后 (s03)                     |\n|----------------|------------------|--------------------------------|\n| Tools          | 4                | 5 (+todo)                      |\n| 规划           | 无               | 带状态的 TodoManager           |\n| Nag 注入       | 无               | 3 轮后注入 `<reminder>`        |\n| Agent loop     | 简单分发         | + rounds_since_todo 计数器     |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s03_todo_write.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Refactor the file hello.py: add type hints, docstrings, and a main guard`\n2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py`\n3. `Review all Python files and fix any style issues`\n"
  },
  {
    "path": "docs/zh/s04-subagent.md",
    "content": "# s04: Subagents (子智能体)\n\n`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"大任务拆小, 每个小任务干净的上下文\"* -- 子智能体用独立 messages[], 不污染主对话。\n>\n> **Harness 层**: 上下文隔离 -- 守护模型的思维清晰度。\n\n## 问题\n\n智能体工作越久, messages 数组越胖。每次读文件、跑命令的输出都永久留在上下文里。\"这个项目用什么测试框架?\" 可能要读 5 个文件, 但父智能体只需要一个词: \"pytest。\"\n\n## 解决方案\n\n```\nParent agent                     Subagent\n+------------------+             +------------------+\n| messages=[...]   |             | messages=[]      | <-- fresh\n|                  |  dispatch   |                  |\n| tool: task       | ----------> | while tool_use:  |\n|   prompt=\"...\"   |             |   call tools     |\n|                  |  summary    |   append results |\n|   result = \"...\" | <---------- | return last text |\n+------------------+             +------------------+\n\nParent context stays clean. Subagent context is discarded.\n```\n\n## 工作原理\n\n1. 父智能体有一个 `task` 工具。子智能体拥有除 `task` 外的所有基础工具 (禁止递归生成)。\n\n```python\nPARENT_TOOLS = CHILD_TOOLS + [\n    {\"name\": \"task\",\n     \"description\": \"Spawn a subagent with fresh context.\",\n     \"input_schema\": {\n         \"type\": \"object\",\n         \"properties\": {\"prompt\": {\"type\": \"string\"}},\n         \"required\": [\"prompt\"],\n     }},\n]\n```\n\n2. 子智能体以 `messages=[]` 启动, 运行自己的循环。只有最终文本返回给父智能体。\n\n```python\ndef run_subagent(prompt: str) -> str:\n    sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n    for _ in range(30):  # safety limit\n        response = client.messages.create(\n            model=MODEL, system=SUBAGENT_SYSTEM,\n            messages=sub_messages,\n            tools=CHILD_TOOLS, max_tokens=8000,\n        )\n        sub_messages.append({\"role\": \"assistant\",\n                             \"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 = TOOL_HANDLERS.get(block.name)\n                output = handler(**block.input)\n                results.append({\"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": str(output)[:50000]})\n        sub_messages.append({\"role\": \"user\", \"content\": results})\n    return \"\".join(\n        b.text for b in response.content if hasattr(b, \"text\")\n    ) or \"(no summary)\"\n```\n\n子智能体可能跑了 30+ 次工具调用, 但整个消息历史直接丢弃。父智能体收到的只是一段摘要文本, 作为普通 `tool_result` 返回。\n\n## 相对 s03 的变更\n\n| 组件           | 之前 (s03)       | 之后 (s04)                    |\n|----------------|------------------|-------------------------------|\n| Tools          | 5                | 5 (基础) + task (仅父端)      |\n| 上下文         | 单一共享         | 父 + 子隔离                   |\n| Subagent       | 无               | `run_subagent()` 函数         |\n| 返回值         | 不适用           | 仅摘要文本                    |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s04_subagent.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Use a subtask to find what testing framework this project uses`\n2. `Delegate: read all .py files and summarize what each one does`\n3. `Use a task to create a new module, then verify it from here`\n"
  },
  {
    "path": "docs/zh/s05-skill-loading.md",
    "content": "# s05: Skills (技能加载)\n\n`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"用到什么知识, 临时加载什么知识\"* -- 通过 tool_result 注入, 不塞 system prompt。\n>\n> **Harness 层**: 按需知识 -- 模型开口要时才给的领域专长。\n\n## 问题\n\n你希望智能体遵循特定领域的工作流: git 约定、测试模式、代码审查清单。全塞进系统提示太浪费 -- 10 个技能, 每个 2000 token, 就是 20,000 token, 大部分跟当前任务毫无关系。\n\n## 解决方案\n\n```\nSystem prompt (Layer 1 -- always present):\n+--------------------------------------+\n| You are a coding agent.              |\n| Skills available:                    |\n|   - git: Git workflow helpers        |  ~100 tokens/skill\n|   - test: Testing best practices     |\n+--------------------------------------+\n\nWhen model calls load_skill(\"git\"):\n+--------------------------------------+\n| tool_result (Layer 2 -- on demand):  |\n| <skill name=\"git\">                   |\n|   Full git workflow instructions...  |  ~2000 tokens\n|   Step 1: ...                        |\n| </skill>                             |\n+--------------------------------------+\n```\n\n第一层: 系统提示中放技能名称 (低成本)。第二层: tool_result 中按需放完整内容。\n\n## 工作原理\n\n1. 每个技能是一个目录, 包含 `SKILL.md` 文件和 YAML frontmatter。\n\n```\nskills/\n  pdf/\n    SKILL.md       # ---\\n name: pdf\\n description: Process PDF files\\n ---\\n ...\n  code-review/\n    SKILL.md       # ---\\n name: code-review\\n description: Review code\\n ---\\n ...\n```\n\n2. SkillLoader 递归扫描 `SKILL.md` 文件, 用目录名作为技能标识。\n\n```python\nclass SkillLoader:\n    def __init__(self, skills_dir: Path):\n        self.skills = {}\n        for f in sorted(skills_dir.rglob(\"SKILL.md\")):\n            text = f.read_text()\n            meta, body = self._parse_frontmatter(text)\n            name = meta.get(\"name\", f.parent.name)\n            self.skills[name] = {\"meta\": meta, \"body\": body}\n\n    def get_descriptions(self) -> str:\n        lines = []\n        for name, skill in self.skills.items():\n            desc = skill[\"meta\"].get(\"description\", \"\")\n            lines.append(f\"  - {name}: {desc}\")\n        return \"\\n\".join(lines)\n\n    def get_content(self, name: str) -> str:\n        skill = self.skills.get(name)\n        if not skill:\n            return f\"Error: Unknown skill '{name}'.\"\n        return f\"<skill name=\\\"{name}\\\">\\n{skill['body']}\\n</skill>\"\n```\n\n3. 第一层写入系统提示。第二层不过是 dispatch map 中的又一个工具。\n\n```python\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nSkills available:\n{SKILL_LOADER.get_descriptions()}\"\"\"\n\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"load_skill\": lambda **kw: SKILL_LOADER.get_content(kw[\"name\"]),\n}\n```\n\n模型知道有哪些技能 (便宜), 需要时再加载完整内容 (贵)。\n\n## 相对 s04 的变更\n\n| 组件           | 之前 (s04)       | 之后 (s05)                     |\n|----------------|------------------|--------------------------------|\n| Tools          | 5 (基础 + task)  | 5 (基础 + load_skill)          |\n| 系统提示       | 静态字符串       | + 技能描述列表                 |\n| 知识库         | 无               | skills/\\*/SKILL.md 文件        |\n| 注入方式       | 无               | 两层 (系统提示 + result)       |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s05_skill_loading.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `What skills are available?`\n2. `Load the agent-builder skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n4. `Build an MCP server using the mcp-builder skill`\n"
  },
  {
    "path": "docs/zh/s06-context-compact.md",
    "content": "# s06: Context Compact (上下文压缩)\n\n`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12`\n\n> *\"上下文总会满, 要有办法腾地方\"* -- 三层压缩策略, 换来无限会话。\n>\n> **Harness 层**: 压缩 -- 干净的记忆, 无限的会话。\n\n## 问题\n\n上下文窗口是有限的。读一个 1000 行的文件就吃掉 ~4000 token; 读 30 个文件、跑 20 条命令, 轻松突破 100k token。不压缩, 智能体根本没法在大项目里干活。\n\n## 解决方案\n\n三层压缩, 激进程度递增:\n\n```\nEvery turn:\n+------------------+\n| Tool call result |\n+------------------+\n        |\n        v\n[Layer 1: micro_compact]        (silent, every turn)\n  Replace tool_result > 3 turns old\n  with \"[Previous: used {tool_name}]\"\n        |\n        v\n[Check: tokens > 50000?]\n   |               |\n   no              yes\n   |               |\n   v               v\ncontinue    [Layer 2: auto_compact]\n              Save transcript to .transcripts/\n              LLM summarizes conversation.\n              Replace all messages with [summary].\n                    |\n                    v\n            [Layer 3: compact tool]\n              Model calls compact explicitly.\n              Same summarization as auto_compact.\n```\n\n## 工作原理\n\n1. **第一层 -- micro_compact**: 每次 LLM 调用前, 将旧的 tool result 替换为占位符。\n\n```python\ndef micro_compact(messages: list) -> list:\n    tool_results = []\n    for i, msg in enumerate(messages):\n        if msg[\"role\"] == \"user\" and isinstance(msg.get(\"content\"), list):\n            for j, part in enumerate(msg[\"content\"]):\n                if isinstance(part, dict) and part.get(\"type\") == \"tool_result\":\n                    tool_results.append((i, j, part))\n    if len(tool_results) <= KEEP_RECENT:\n        return messages\n    for _, _, part in tool_results[:-KEEP_RECENT]:\n        if len(part.get(\"content\", \"\")) > 100:\n            part[\"content\"] = f\"[Previous: used {tool_name}]\"\n    return messages\n```\n\n2. **第二层 -- auto_compact**: token 超过阈值时, 保存完整对话到磁盘, 让 LLM 做摘要。\n\n```python\ndef auto_compact(messages: list) -> list:\n    # Save transcript for recovery\n    transcript_path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n    with open(transcript_path, \"w\") as f:\n        for msg in messages:\n            f.write(json.dumps(msg, default=str) + \"\\n\")\n    # LLM summarizes\n    response = client.messages.create(\n        model=MODEL,\n        messages=[{\"role\": \"user\", \"content\":\n            \"Summarize this conversation for continuity...\"\n            + json.dumps(messages, default=str)[:80000]}],\n        max_tokens=2000,\n    )\n    return [\n        {\"role\": \"user\", \"content\": f\"[Compressed]\\n\\n{response.content[0].text}\"},\n        {\"role\": \"assistant\", \"content\": \"Understood. Continuing.\"},\n    ]\n```\n\n3. **第三层 -- manual compact**: `compact` 工具按需触发同样的摘要机制。\n\n4. 循环整合三层:\n\n```python\ndef agent_loop(messages: list):\n    while True:\n        micro_compact(messages)                        # Layer 1\n        if estimate_tokens(messages) > THRESHOLD:\n            messages[:] = auto_compact(messages)       # Layer 2\n        response = client.messages.create(...)\n        # ... tool execution ...\n        if manual_compact:\n            messages[:] = auto_compact(messages)       # Layer 3\n```\n\n完整历史通过 transcript 保存在磁盘上。信息没有真正丢失, 只是移出了活跃上下文。\n\n## 相对 s05 的变更\n\n| 组件           | 之前 (s05)       | 之后 (s06)                     |\n|----------------|------------------|--------------------------------|\n| Tools          | 5                | 5 (基础 + compact)             |\n| 上下文管理     | 无               | 三层压缩                       |\n| Micro-compact  | 无               | 旧结果 -> 占位符               |\n| Auto-compact   | 无               | token 阈值触发                 |\n| Transcripts    | 无               | 保存到 .transcripts/           |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s06_context_compact.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Read every Python file in the agents/ directory one by one` (观察 micro-compact 替换旧结果)\n2. `Keep reading files until compression triggers automatically`\n3. `Use the compact tool to manually compress the conversation`\n"
  },
  {
    "path": "docs/zh/s07-task-system.md",
    "content": "# s07: Task System (任务系统)\n\n`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12`\n\n> *\"大目标要拆成小任务, 排好序, 记在磁盘上\"* -- 文件持久化的任务图, 为多 agent 协作打基础。\n>\n> **Harness 层**: 持久化任务 -- 比任何一次对话都长命的目标。\n\n## 问题\n\ns03 的 TodoManager 只是内存中的扁平清单: 没有顺序、没有依赖、状态只有做完没做完。真实目标是有结构的 -- 任务 B 依赖任务 A, 任务 C 和 D 可以并行, 任务 E 要等 C 和 D 都完成。\n\n没有显式的关系, 智能体分不清什么能做、什么被卡住、什么能同时跑。而且清单只活在内存里, 上下文压缩 (s06) 一跑就没了。\n\n## 解决方案\n\n把扁平清单升级为持久化到磁盘的**任务图**。每个任务是一个 JSON 文件, 有状态、前置依赖 (`blockedBy`) 和后置依赖 (`blocks`)。任务图随时回答三个问题:\n\n- **什么可以做?** -- 状态为 `pending` 且 `blockedBy` 为空的任务。\n- **什么被卡住?** -- 等待前置任务完成的任务。\n- **什么做完了?** -- 状态为 `completed` 的任务, 完成时自动解锁后续任务。\n\n```\n.tasks/\n  task_1.json  {\"id\":1, \"status\":\"completed\"}\n  task_2.json  {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\"}\n  task_3.json  {\"id\":3, \"blockedBy\":[1], \"status\":\"pending\"}\n  task_4.json  {\"id\":4, \"blockedBy\":[2,3], \"status\":\"pending\"}\n\n任务图 (DAG):\n                 +----------+\n            +--> | task 2   | --+\n            |    | pending  |   |\n+----------+     +----------+    +--> +----------+\n| task 1   |                          | task 4   |\n| completed| --> +----------+    +--> | blocked  |\n+----------+     | task 3   | --+     +----------+\n                 | pending  |\n                 +----------+\n\n顺序:   task 1 必须先完成, 才能开始 2 和 3\n并行:   task 2 和 3 可以同时执行\n依赖:   task 4 要等 2 和 3 都完成\n状态:   pending -> in_progress -> completed\n```\n\n这个任务图是 s07 之后所有机制的协调骨架: 后台执行 (s08)、多 agent 团队 (s09+)、worktree 隔离 (s12) 都读写这同一个结构。\n\n## 工作原理\n\n1. **TaskManager**: 每个任务一个 JSON 文件, CRUD + 依赖图。\n\n```python\nclass TaskManager:\n    def __init__(self, tasks_dir: Path):\n        self.dir = tasks_dir\n        self.dir.mkdir(exist_ok=True)\n        self._next_id = self._max_id() + 1\n\n    def create(self, subject, description=\"\"):\n        task = {\"id\": self._next_id, \"subject\": subject,\n                \"status\": \"pending\", \"blockedBy\": [],\n                \"blocks\": [], \"owner\": \"\"}\n        self._save(task)\n        self._next_id += 1\n        return json.dumps(task, indent=2)\n```\n\n2. **依赖解除**: 完成任务时, 自动将其 ID 从其他任务的 `blockedBy` 中移除, 解锁后续任务。\n\n```python\ndef _clear_dependency(self, completed_id):\n    for f in self.dir.glob(\"task_*.json\"):\n        task = json.loads(f.read_text())\n        if completed_id in task.get(\"blockedBy\", []):\n            task[\"blockedBy\"].remove(completed_id)\n            self._save(task)\n```\n\n3. **状态变更 + 依赖关联**: `update` 处理状态转换和依赖边。\n\n```python\ndef update(self, task_id, status=None,\n           add_blocked_by=None, add_blocks=None):\n    task = self._load(task_id)\n    if status:\n        task[\"status\"] = status\n        if status == \"completed\":\n            self._clear_dependency(task_id)\n    self._save(task)\n```\n\n4. 四个任务工具加入 dispatch map。\n\n```python\nTOOL_HANDLERS = {\n    # ...base tools...\n    \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"]),\n    \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"], kw.get(\"status\")),\n    \"task_list\":   lambda **kw: TASKS.list_all(),\n    \"task_get\":    lambda **kw: TASKS.get(kw[\"task_id\"]),\n}\n```\n\n从 s07 起, 任务图是多步工作的默认选择。s03 的 Todo 仍可用于单次会话内的快速清单。\n\n## 相对 s06 的变更\n\n| 组件 | 之前 (s06) | 之后 (s07) |\n|---|---|---|\n| Tools | 5 | 8 (`task_create/update/list/get`) |\n| 规划模型 | 扁平清单 (仅内存) | 带依赖关系的任务图 (磁盘) |\n| 关系 | 无 | `blockedBy` + `blocks` 边 |\n| 状态追踪 | 做完没做完 | `pending` -> `in_progress` -> `completed` |\n| 持久化 | 压缩后丢失 | 压缩和重启后存活 |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s07_task_system.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Create 3 tasks: \"Setup project\", \"Write code\", \"Write tests\". Make them depend on each other in order.`\n2. `List all tasks and show the dependency graph`\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\n"
  },
  {
    "path": "docs/zh/s08-background-tasks.md",
    "content": "# s08: Background Tasks (后台任务)\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12`\n\n> *\"慢操作丢后台, agent 继续想下一步\"* -- 后台线程跑命令, 完成后注入通知。\n>\n> **Harness 层**: 后台执行 -- 模型继续思考, harness 负责等待。\n\n## 问题\n\n有些命令要跑好几分钟: `npm install`、`pytest`、`docker build`。阻塞式循环下模型只能干等。用户说 \"装依赖, 顺便建个配置文件\", 智能体却只能一个一个来。\n\n## 解决方案\n\n```\nMain thread                Background thread\n+-----------------+        +-----------------+\n| agent loop      |        | subprocess runs |\n| ...             |        | ...             |\n| [LLM call] <---+------- | enqueue(result) |\n|  ^drain queue   |        +-----------------+\n+-----------------+\n\nTimeline:\nAgent --[spawn A]--[spawn B]--[other work]----\n             |          |\n             v          v\n          [A runs]   [B runs]      (parallel)\n             |          |\n             +-- results injected before next LLM call --+\n```\n\n## 工作原理\n\n1. BackgroundManager 用线程安全的通知队列追踪任务。\n\n```python\nclass BackgroundManager:\n    def __init__(self):\n        self.tasks = {}\n        self._notification_queue = []\n        self._lock = threading.Lock()\n```\n\n2. `run()` 启动守护线程, 立即返回。\n\n```python\ndef run(self, command: str) -> str:\n    task_id = str(uuid.uuid4())[:8]\n    self.tasks[task_id] = {\"status\": \"running\", \"command\": command}\n    thread = threading.Thread(\n        target=self._execute, args=(task_id, command), daemon=True)\n    thread.start()\n    return f\"Background task {task_id} started\"\n```\n\n3. 子进程完成后, 结果进入通知队列。\n\n```python\ndef _execute(self, task_id, command):\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n            capture_output=True, text=True, timeout=300)\n        output = (r.stdout + r.stderr).strip()[:50000]\n    except subprocess.TimeoutExpired:\n        output = \"Error: Timeout (300s)\"\n    with self._lock:\n        self._notification_queue.append({\n            \"task_id\": task_id, \"result\": output[:500]})\n```\n\n4. 每次 LLM 调用前排空通知队列。\n\n```python\ndef agent_loop(messages: list):\n    while True:\n        notifs = BG.drain_notifications()\n        if notifs:\n            notif_text = \"\\n\".join(\n                f\"[bg:{n['task_id']}] {n['result']}\" for n in notifs)\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<background-results>\\n{notif_text}\\n\"\n                           f\"</background-results>\"})\n            messages.append({\"role\": \"assistant\",\n                \"content\": \"Noted background results.\"})\n        response = client.messages.create(...)\n```\n\n循环保持单线程。只有子进程 I/O 被并行化。\n\n## 相对 s07 的变更\n\n| 组件           | 之前 (s07)       | 之后 (s08)                         |\n|----------------|------------------|------------------------------------|\n| Tools          | 8                | 6 (基础 + background_run + check)  |\n| 执行方式       | 仅阻塞           | 阻塞 + 后台线程                    |\n| 通知机制       | 无               | 每轮排空的队列                     |\n| 并发           | 无               | 守护线程                           |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s08_background_tasks.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Run \"sleep 5 && echo done\" in the background, then create a file while it runs`\n2. `Start 3 background tasks: \"sleep 2\", \"sleep 4\", \"sleep 6\". Check their status.`\n3. `Run pytest in the background and keep working on other things`\n"
  },
  {
    "path": "docs/zh/s09-agent-teams.md",
    "content": "# s09: Agent Teams (智能体团队)\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12`\n\n> *\"任务太大一个人干不完, 要能分给队友\"* -- 持久化队友 + JSONL 邮箱。\n>\n> **Harness 层**: 团队邮箱 -- 多个模型, 通过文件协调。\n\n## 问题\n\n子智能体 (s04) 是一次性的: 生成、干活、返回摘要、消亡。没有身份, 没有跨调用的记忆。后台任务 (s08) 能跑 shell 命令, 但做不了 LLM 引导的决策。\n\n真正的团队协作需要三样东西: (1) 能跨多轮对话存活的持久智能体, (2) 身份和生命周期管理, (3) 智能体之间的通信通道。\n\n## 解决方案\n\n```\nTeammate lifecycle:\n  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\n\nCommunication:\n  .team/\n    config.json           <- team roster + statuses\n    inbox/\n      alice.jsonl         <- append-only, drain-on-read\n      bob.jsonl\n      lead.jsonl\n\n              +--------+    send(\"alice\",\"bob\",\"...\")    +--------+\n              | alice  | -----------------------------> |  bob   |\n              | loop   |    bob.jsonl << {json_line}    |  loop  |\n              +--------+                                +--------+\n                   ^                                         |\n                   |        BUS.read_inbox(\"alice\")          |\n                   +---- alice.jsonl -> read + drain ---------+\n```\n\n## 工作原理\n\n1. TeammateManager 通过 config.json 维护团队名册。\n\n```python\nclass TeammateManager:\n    def __init__(self, team_dir: Path):\n        self.dir = team_dir\n        self.dir.mkdir(exist_ok=True)\n        self.config_path = self.dir / \"config.json\"\n        self.config = self._load_config()\n        self.threads = {}\n```\n\n2. `spawn()` 创建队友并在线程中启动 agent loop。\n\n```python\ndef spawn(self, name: str, role: str, prompt: str) -> str:\n    member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n    self.config[\"members\"].append(member)\n    self._save_config()\n    thread = threading.Thread(\n        target=self._teammate_loop,\n        args=(name, role, prompt), daemon=True)\n    thread.start()\n    return f\"Spawned teammate '{name}' (role: {role})\"\n```\n\n3. MessageBus: append-only 的 JSONL 收件箱。`send()` 追加一行; `read_inbox()` 读取全部并清空。\n\n```python\nclass MessageBus:\n    def send(self, sender, to, content, msg_type=\"message\", extra=None):\n        msg = {\"type\": msg_type, \"from\": sender,\n               \"content\": content, \"timestamp\": time.time()}\n        if extra:\n            msg.update(extra)\n        with open(self.dir / f\"{to}.jsonl\", \"a\") as f:\n            f.write(json.dumps(msg) + \"\\n\")\n\n    def read_inbox(self, name):\n        path = self.dir / f\"{name}.jsonl\"\n        if not path.exists(): return \"[]\"\n        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]\n        path.write_text(\"\")  # drain\n        return json.dumps(msgs, indent=2)\n```\n\n4. 每个队友在每次 LLM 调用前检查收件箱, 将消息注入上下文。\n\n```python\ndef _teammate_loop(self, name, role, prompt):\n    messages = [{\"role\": \"user\", \"content\": prompt}]\n    for _ in range(50):\n        inbox = BUS.read_inbox(name)\n        if inbox != \"[]\":\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<inbox>{inbox}</inbox>\"})\n            messages.append({\"role\": \"assistant\",\n                \"content\": \"Noted inbox messages.\"})\n        response = client.messages.create(...)\n        if response.stop_reason != \"tool_use\":\n            break\n        # execute tools, append results...\n    self._find_member(name)[\"status\"] = \"idle\"\n```\n\n## 相对 s08 的变更\n\n| 组件           | 之前 (s08)       | 之后 (s09)                         |\n|----------------|------------------|------------------------------------|\n| Tools          | 6                | 9 (+spawn/send/read_inbox)         |\n| 智能体数量     | 单一             | 领导 + N 个队友                    |\n| 持久化         | 无               | config.json + JSONL 收件箱         |\n| 线程           | 后台命令         | 每线程完整 agent loop              |\n| 生命周期       | 一次性           | idle -> working -> idle            |\n| 通信           | 无               | message + broadcast                |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s09_agent_teams.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.`\n2. `Broadcast \"status update: phase 1 complete\" to all teammates`\n3. `Check the lead inbox for any messages`\n4. 输入 `/team` 查看团队名册和状态\n5. 输入 `/inbox` 手动检查领导的收件箱\n"
  },
  {
    "path": "docs/zh/s10-team-protocols.md",
    "content": "# s10: Team Protocols (团队协议)\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12`\n\n> *\"队友之间要有统一的沟通规矩\"* -- 一个 request-response 模式驱动所有协商。\n>\n> **Harness 层**: 协议 -- 模型之间的结构化握手。\n\n## 问题\n\ns09 中队友能干活能通信, 但缺少结构化协调:\n\n**关机**: 直接杀线程会留下写了一半的文件和过期的 config.json。需要握手 -- 领导请求, 队友批准 (收尾退出) 或拒绝 (继续干)。\n\n**计划审批**: 领导说 \"重构认证模块\", 队友立刻开干。高风险变更应该先过审。\n\n两者结构一样: 一方发带唯一 ID 的请求, 另一方引用同一 ID 响应。\n\n## 解决方案\n\n```\nShutdown Protocol            Plan Approval Protocol\n==================           ======================\n\nLead             Teammate    Teammate           Lead\n  |                 |           |                 |\n  |--shutdown_req-->|           |--plan_req------>|\n  | {req_id:\"abc\"}  |           | {req_id:\"xyz\"}  |\n  |                 |           |                 |\n  |<--shutdown_resp-|           |<--plan_resp-----|\n  | {req_id:\"abc\",  |           | {req_id:\"xyz\",  |\n  |  approve:true}  |           |  approve:true}  |\n\nShared FSM:\n  [pending] --approve--> [approved]\n  [pending] --reject---> [rejected]\n\nTrackers:\n  shutdown_requests = {req_id: {target, status}}\n  plan_requests     = {req_id: {from, plan, status}}\n```\n\n## 工作原理\n\n1. 领导生成 request_id, 通过收件箱发起关机请求。\n\n```python\nshutdown_requests = {}\n\ndef handle_shutdown_request(teammate: str) -> str:\n    req_id = str(uuid.uuid4())[:8]\n    shutdown_requests[req_id] = {\"target\": teammate, \"status\": \"pending\"}\n    BUS.send(\"lead\", teammate, \"Please shut down gracefully.\",\n             \"shutdown_request\", {\"request_id\": req_id})\n    return f\"Shutdown request {req_id} sent (status: pending)\"\n```\n\n2. 队友收到请求后, 用 approve/reject 响应。\n\n```python\nif tool_name == \"shutdown_response\":\n    req_id = args[\"request_id\"]\n    approve = args[\"approve\"]\n    shutdown_requests[req_id][\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(sender, \"lead\", args.get(\"reason\", \"\"),\n             \"shutdown_response\",\n             {\"request_id\": req_id, \"approve\": approve})\n```\n\n3. 计划审批遵循完全相同的模式。队友提交计划 (生成 request_id), 领导审查 (引用同一个 request_id)。\n\n```python\nplan_requests = {}\n\ndef handle_plan_review(request_id, approve, feedback=\"\"):\n    req = plan_requests[request_id]\n    req[\"status\"] = \"approved\" if approve else \"rejected\"\n    BUS.send(\"lead\", req[\"from\"], feedback,\n             \"plan_approval_response\",\n             {\"request_id\": request_id, \"approve\": approve})\n```\n\n一个 FSM, 两种用途。同样的 `pending -> approved | rejected` 状态机可以套用到任何请求-响应协议上。\n\n## 相对 s09 的变更\n\n| 组件           | 之前 (s09)       | 之后 (s10)                           |\n|----------------|------------------|--------------------------------------|\n| Tools          | 9                | 12 (+shutdown_req/resp +plan)        |\n| 关机           | 仅自然退出       | 请求-响应握手                        |\n| 计划门控       | 无               | 提交/审查与审批                      |\n| 关联           | 无               | 每个请求一个 request_id              |\n| FSM            | 无               | pending -> approved/rejected         |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s10_team_protocols.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Spawn alice as a coder. Then request her shutdown.`\n2. `List teammates to see alice's status after shutdown approval`\n3. `Spawn bob with a risky refactoring task. Review and reject his plan.`\n4. `Spawn charlie, have him submit a plan, then approve it.`\n5. 输入 `/team` 监控状态\n"
  },
  {
    "path": "docs/zh/s11-autonomous-agents.md",
    "content": "# s11: Autonomous Agents (自治智能体)\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12`\n\n> *\"队友自己看看板, 有活就认领\"* -- 不需要领导逐个分配, 自组织。\n>\n> **Harness 层**: 自治 -- 模型自己找活干, 无需指派。\n\n## 问题\n\ns09-s10 中, 队友只在被明确指派时才动。领导得给每个队友写 prompt, 任务看板上 10 个未认领的任务得手动分配。这扩展不了。\n\n真正的自治: 队友自己扫描任务看板, 认领没人做的任务, 做完再找下一个。\n\n一个细节: 上下文压缩 (s06) 后智能体可能忘了自己是谁。身份重注入解决这个问题。\n\n## 解决方案\n\n```\nTeammate lifecycle with idle cycle:\n\n+-------+\n| spawn |\n+---+---+\n    |\n    v\n+-------+   tool_use     +-------+\n| WORK  | <------------- |  LLM  |\n+---+---+                +-------+\n    |\n    | stop_reason != tool_use (or idle tool called)\n    v\n+--------+\n|  IDLE  |  poll every 5s for up to 60s\n+---+----+\n    |\n    +---> check inbox --> message? ----------> WORK\n    |\n    +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\n    |\n    +---> 60s timeout ----------------------> SHUTDOWN\n\nIdentity re-injection after compression:\n  if len(messages) <= 3:\n    messages.insert(0, identity_block)\n```\n\n## 工作原理\n\n1. 队友循环分两个阶段: WORK 和 IDLE。LLM 停止调用工具 (或调用了 `idle`) 时, 进入 IDLE。\n\n```python\ndef _loop(self, name, role, prompt):\n    while True:\n        # -- WORK PHASE --\n        messages = [{\"role\": \"user\", \"content\": prompt}]\n        for _ in range(50):\n            response = client.messages.create(...)\n            if response.stop_reason != \"tool_use\":\n                break\n            # execute tools...\n            if idle_requested:\n                break\n\n        # -- IDLE PHASE --\n        self._set_status(name, \"idle\")\n        resume = self._idle_poll(name, messages)\n        if not resume:\n            self._set_status(name, \"shutdown\")\n            return\n        self._set_status(name, \"working\")\n```\n\n2. 空闲阶段循环轮询收件箱和任务看板。\n\n```python\ndef _idle_poll(self, name, messages):\n    for _ in range(IDLE_TIMEOUT // POLL_INTERVAL):  # 60s / 5s = 12\n        time.sleep(POLL_INTERVAL)\n        inbox = BUS.read_inbox(name)\n        if inbox:\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<inbox>{inbox}</inbox>\"})\n            return True\n        unclaimed = scan_unclaimed_tasks()\n        if unclaimed:\n            claim_task(unclaimed[0][\"id\"], name)\n            messages.append({\"role\": \"user\",\n                \"content\": f\"<auto-claimed>Task #{unclaimed[0]['id']}: \"\n                           f\"{unclaimed[0]['subject']}</auto-claimed>\"})\n            return True\n    return False  # timeout -> shutdown\n```\n\n3. 任务看板扫描: 找 pending 状态、无 owner、未被阻塞的任务。\n\n```python\ndef scan_unclaimed_tasks() -> list:\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 not task.get(\"blockedBy\")):\n            unclaimed.append(task)\n    return unclaimed\n```\n\n4. 身份重注入: 上下文过短 (说明发生了压缩) 时, 在开头插入身份块。\n\n```python\nif len(messages) <= 3:\n    messages.insert(0, {\"role\": \"user\",\n        \"content\": f\"<identity>You are '{name}', role: {role}, \"\n                   f\"team: {team_name}. Continue your work.</identity>\"})\n    messages.insert(1, {\"role\": \"assistant\",\n        \"content\": f\"I am {name}. Continuing.\"})\n```\n\n## 相对 s10 的变更\n\n| 组件           | 之前 (s10)       | 之后 (s11)                       |\n|----------------|------------------|----------------------------------|\n| Tools          | 12               | 14 (+idle, +claim_task)          |\n| 自治性         | 领导指派         | 自组织                           |\n| 空闲阶段       | 无               | 轮询收件箱 + 任务看板            |\n| 任务认领       | 仅手动           | 自动认领未分配任务               |\n| 身份           | 系统提示         | + 压缩后重注入                   |\n| 超时           | 无               | 60 秒空闲 -> 自动关机            |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s11_autonomous_agents.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.`\n2. `Spawn a coder teammate and let it find work from the task board itself`\n3. `Create tasks with dependencies. Watch teammates respect the blocked order.`\n4. 输入 `/tasks` 查看带 owner 的任务看板\n5. 输入 `/team` 监控谁在工作、谁在空闲\n"
  },
  {
    "path": "docs/zh/s12-worktree-task-isolation.md",
    "content": "# s12: Worktree + Task Isolation (Worktree 任务隔离)\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\n\n> *\"各干各的目录, 互不干扰\"* -- 任务管目标, worktree 管目录, 按 ID 绑定。\n>\n> **Harness 层**: 目录隔离 -- 永不碰撞的并行执行通道。\n\n## 问题\n\n到 s11, 智能体已经能自主认领和完成任务。但所有任务共享一个目录。两个智能体同时重构不同模块 -- A 改 `config.py`, B 也改 `config.py`, 未提交的改动互相污染, 谁也没法干净回滚。\n\n任务板管 \"做什么\" 但不管 \"在哪做\"。解法: 给每个任务一个独立的 git worktree 目录, 用任务 ID 把两边关联起来。\n\n## 解决方案\n\n```\nControl plane (.tasks/)             Execution plane (.worktrees/)\n+------------------+                +------------------------+\n| task_1.json      |                | auth-refactor/         |\n|   status: in_progress  <------>   branch: wt/auth-refactor\n|   worktree: \"auth-refactor\"   |   task_id: 1             |\n+------------------+                +------------------------+\n| task_2.json      |                | ui-login/              |\n|   status: pending    <------>     branch: wt/ui-login\n|   worktree: \"ui-login\"       |   task_id: 2             |\n+------------------+                +------------------------+\n                                    |\n                          index.json (worktree registry)\n                          events.jsonl (lifecycle log)\n\nState machines:\n  Task:     pending -> in_progress -> completed\n  Worktree: absent  -> active      -> removed | kept\n```\n\n## 工作原理\n\n1. **创建任务。** 先把目标持久化。\n\n```python\nTASKS.create(\"Implement auth refactor\")\n# -> .tasks/task_1.json  status=pending  worktree=\"\"\n```\n\n2. **创建 worktree 并绑定任务。** 传入 `task_id` 自动将任务推进到 `in_progress`。\n\n```python\nWORKTREES.create(\"auth-refactor\", task_id=1)\n# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD\n# -> index.json gets new entry, task_1.json gets worktree=\"auth-refactor\"\n```\n\n绑定同时写入两侧状态:\n\n```python\ndef bind_worktree(self, task_id, worktree):\n    task = self._load(task_id)\n    task[\"worktree\"] = worktree\n    if task[\"status\"] == \"pending\":\n        task[\"status\"] = \"in_progress\"\n    self._save(task)\n```\n\n3. **在 worktree 中执行命令。** `cwd` 指向隔离目录。\n\n```python\nsubprocess.run(command, shell=True, cwd=worktree_path,\n               capture_output=True, text=True, timeout=300)\n```\n\n4. **收尾。** 两种选择:\n   - `worktree_keep(name)` -- 保留目录供后续使用。\n   - `worktree_remove(name, complete_task=True)` -- 删除目录, 完成绑定任务, 发出事件。一个调用搞定拆除 + 完成。\n\n```python\ndef remove(self, name, force=False, complete_task=False):\n    self._run_git([\"worktree\", \"remove\", wt[\"path\"]])\n    if complete_task and wt.get(\"task_id\") is not None:\n        self.tasks.update(wt[\"task_id\"], status=\"completed\")\n        self.tasks.unbind_worktree(wt[\"task_id\"])\n        self.events.emit(\"task.completed\", ...)\n```\n\n5. **事件流。** 每个生命周期步骤写入 `.worktrees/events.jsonl`:\n\n```json\n{\n  \"event\": \"worktree.remove.after\",\n  \"task\": {\"id\": 1, \"status\": \"completed\"},\n  \"worktree\": {\"name\": \"auth-refactor\", \"status\": \"removed\"},\n  \"ts\": 1730000000\n}\n```\n\n事件类型: `worktree.create.before/after/failed`, `worktree.remove.before/after/failed`, `worktree.keep`, `task.completed`。\n\n崩溃后从 `.tasks/` + `.worktrees/index.json` 重建现场。会话记忆是易失的; 磁盘状态是持久的。\n\n## 相对 s11 的变更\n\n| 组件               | 之前 (s11)                 | 之后 (s12)                                   |\n|--------------------|----------------------------|----------------------------------------------|\n| 协调               | 任务板 (owner/status)      | 任务板 + worktree 显式绑定                   |\n| 执行范围           | 共享目录                   | 每个任务独立目录                             |\n| 可恢复性           | 仅任务状态                 | 任务状态 + worktree 索引                     |\n| 收尾               | 任务完成                   | 任务完成 + 显式 keep/remove                  |\n| 生命周期可见性     | 隐式日志                   | `.worktrees/events.jsonl` 显式事件流         |\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s12_worktree_task_isolation.py\n```\n\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\n\n1. `Create tasks for backend auth and frontend login page, then list tasks.`\n2. `Create worktree \"auth-refactor\" for task 1, then bind task 2 to a new worktree \"ui-login\".`\n3. `Run \"git status --short\" in worktree \"auth-refactor\".`\n4. `Keep worktree \"ui-login\", then list worktrees and inspect events.`\n5. `Remove worktree \"auth-refactor\" with complete_task=true, then list tasks/worktrees/events.`\n"
  },
  {
    "path": "requirements.txt",
    "content": "anthropic>=0.25.0\npython-dotenv>=1.0.0\n"
  },
  {
    "path": "skills/agent-builder/SKILL.md",
    "content": "---\nname: agent-builder\ndescription: |\n  Design and build AI agents for any domain. Use when users:\n  (1) ask to \"create an agent\", \"build an assistant\", or \"design an AI system\"\n  (2) want to understand agent architecture, agentic patterns, or autonomous AI\n  (3) need help with capabilities, subagents, planning, or skill mechanisms\n  (4) ask about Claude Code, Cursor, or similar agent internals\n  (5) want to build agents for business, research, creative, or operational tasks\n  Keywords: agent, assistant, autonomous, workflow, tool use, multi-step, orchestration\n---\n\n# Agent Builder\n\nBuild AI agents for any domain - customer service, research, operations, creative work, or specialized business processes.\n\n## The Core Philosophy\n\n> **The model already knows how to be an agent. Your job is to get out of the way.**\n\nAn agent is not complex engineering. It's a simple loop that invites the model to act:\n\n```\nLOOP:\n  Model sees: context + available capabilities\n  Model decides: act or respond\n  If act: execute capability, add result, continue\n  If respond: return to user\n```\n\n**That's it.** The magic isn't in the code - it's in the model. Your code just provides the opportunity.\n\n## The Three Elements\n\n### 1. Capabilities (What can it DO?)\n\nAtomic actions the agent can perform: search, read, create, send, query, modify.\n\n**Design principle**: Start with 3-5 capabilities. Add more only when the agent consistently fails because a capability is missing.\n\n### 2. Knowledge (What does it KNOW?)\n\nDomain expertise injected on-demand: policies, workflows, best practices, schemas.\n\n**Design principle**: Make knowledge available, not mandatory. Load it when relevant, not upfront.\n\n### 3. Context (What has happened?)\n\nThe conversation history - the thread connecting actions into coherent behavior.\n\n**Design principle**: Context is precious. Isolate noisy subtasks. Truncate verbose outputs. Protect clarity.\n\n## Agent Design Thinking\n\nBefore building, understand:\n\n- **Purpose**: What should this agent accomplish?\n- **Domain**: What world does it operate in? (customer service, research, operations, creative...)\n- **Capabilities**: What 3-5 actions are essential?\n- **Knowledge**: What expertise does it need access to?\n- **Trust**: What decisions can you delegate to the model?\n\n**CRITICAL**: Trust the model. Don't over-engineer. Don't pre-specify workflows. Give it capabilities and let it reason.\n\n## Progressive Complexity\n\nStart simple. Add complexity only when real usage reveals the need:\n\n| Level | What to add | When to add it |\n|-------|-------------|----------------|\n| Basic | 3-5 capabilities | Always start here |\n| Planning | Progress tracking | Multi-step tasks lose coherence |\n| Subagents | Isolated child agents | Exploration pollutes context |\n| Skills | On-demand knowledge | Domain expertise needed |\n\n**Most agents never need to go beyond Level 2.**\n\n## Domain Examples\n\n**Business**: CRM queries, email, calendar, approvals\n**Research**: Database search, document analysis, citations\n**Operations**: Monitoring, tickets, notifications, escalation\n**Creative**: Asset generation, editing, collaboration, review\n\nThe pattern is universal. Only the capabilities change.\n\n## Key Principles\n\n1. **The model IS the agent** - Code just runs the loop\n2. **Capabilities enable** - What it CAN do\n3. **Knowledge informs** - What it KNOWS how to do\n4. **Constraints focus** - Limits create clarity\n5. **Trust liberates** - Let the model reason\n6. **Iteration reveals** - Start minimal, evolve from usage\n\n## Anti-Patterns\n\n| Pattern | Problem | Solution |\n|---------|---------|----------|\n| Over-engineering | Complexity before need | Start simple |\n| Too many capabilities | Model confusion | 3-5 to start |\n| Rigid workflows | Can't adapt | Let model decide |\n| Front-loaded knowledge | Context bloat | Load on-demand |\n| Micromanagement | Undercuts intelligence | Trust the model |\n\n## Resources\n\n**Philosophy & Theory**:\n- `references/agent-philosophy.md` - Deep dive into why agents work\n\n**Implementation**:\n- `references/minimal-agent.py` - Complete working agent (~80 lines)\n- `references/tool-templates.py` - Capability definitions\n- `references/subagent-pattern.py` - Context isolation\n\n**Scaffolding**:\n- `scripts/init_agent.py` - Generate new agent projects\n\n## The Agent Mindset\n\n**From**: \"How do I make the system do X?\"\n**To**: \"How do I enable the model to do X?\"\n\n**From**: \"What's the workflow for this task?\"\n**To**: \"What capabilities would help accomplish this?\"\n\nThe best agent code is almost boring. Simple loops. Clear capabilities. Clean context. The magic isn't in the code.\n\n**Give the model capabilities and knowledge. Trust it to figure out the rest.**\n"
  },
  {
    "path": "skills/agent-builder/references/agent-philosophy.md",
    "content": "# The Philosophy of Agent Harness Engineering\n\n> **The model already knows how to be an agent. Your job is to build it a world worth acting in.**\n\n## The Fundamental Truth\n\nStrip away every framework, every library, every architectural pattern. What remains?\n\nA loop. A model. An invitation to act.\n\nThe agent is not the code. The agent is the model itself -- a vast neural network trained on humanity's collective problem-solving, reasoning, and tool use. The code merely provides the opportunity for the model to express its agency.\n\nThe code is the harness. The model is the agent. These are not interchangeable. Confuse them, and you will build the wrong thing.\n\n## What an Agent IS\n\nAn agent is a neural network -- a Transformer, an RNN, a learned function -- that has been trained, through billions of gradient updates on action-sequence data, to perceive an environment, reason about goals, and take actions to achieve them.\n\nA human is an agent: a biological neural network shaped by evolution. DeepMind's DQN is an agent: a convolutional network that learned to play Atari from raw pixels. OpenAI Five is an agent: five networks that learned Dota 2 teamwork through self-play. Claude is an agent: a language model that learned to reason and act from the breadth of human knowledge.\n\nIn every case, the agent is the trained model. Not the game engine. Not the Dota 2 client. Not the terminal. The model.\n\n## What an Agent Is NOT\n\nPrompt plumbing is not agency. Wiring together LLM API calls with if-else branches, node graphs, and hardcoded routing logic does not produce an agent. It produces a brittle pipeline -- a Rube Goldberg machine with an LLM wedged in as a text-completion node.\n\nYou cannot engineer your way to agency. Agency is learned, not programmed. No amount of glue code will emergently produce autonomous behavior. Those systems are the modern resurrection of GOFAI -- symbolic rule systems the field abandoned decades ago, now spray-painted with an LLM veneer.\n\n## The Harness: What We Actually Build\n\nIf the model is the agent, then what is the code? It is the **harness** -- the environment that gives the agent the ability to perceive and act in a specific domain.\n\n```\nHarness = Tools + Knowledge + Observation + Action Interfaces + Permissions\n```\n\n### Tools: The Agent's Hands\n\nTools answer: **What can the agent DO?**\n\nEach tool is an atomic action the agent can take in its environment. File read/write, shell execution, API calls, browser control, database queries. The model needs to understand what each tool does, but not how to sequence them -- it will figure that out.\n\n**Design principle**: Atomic, composable, well-described. Start with 3-5. Add more only when the model consistently fails to accomplish tasks because a tool is missing.\n\n### Knowledge: The Agent's Expertise\n\nKnowledge answers: **What does the agent KNOW?**\n\nDomain expertise that turns a general agent into a domain specialist. Product documentation, architectural decisions, regulatory requirements, style guides. Inject on-demand (via tool_result), not upfront (via system prompt). Progressive disclosure preserves context for what matters.\n\n**Design principle**: Available but not mandatory. The agent should know what knowledge exists and pull what it needs.\n\n### Context: The Agent's Memory\n\nContext is the thread connecting individual actions into coherent behavior. What has been said, tried, learned, and decided.\n\n**Design principle**: Context is precious. Protect it. Isolate subtasks that generate noise (s04). Compress when history grows long (s06). Persist goals beyond single conversations (s07).\n\n### Permissions: The Agent's Boundaries\n\nPermissions answer: **What is the agent ALLOWED to do?**\n\nSandbox file access. Require approval for destructive operations. Enforce trust boundaries between the agent and external systems. This is where safety engineering meets harness engineering.\n\n**Design principle**: Constraints focus behavior, not limit it. \"One task in_progress at a time\" forces sequential focus. \"Read-only subagent\" prevents accidental modifications.\n\n### Task-Process Data: The Agent's Training Signal\n\nEvery action sequence the agent executes in your harness is training signal. The perception-reasoning-action traces from real deployments are the raw material for fine-tuning the next generation of agent models. Your harness doesn't just serve the agent -- it can help evolve the agent.\n\n## The Universal Loop\n\nEvery effective agent -- regardless of domain -- follows the same pattern:\n\n```\nLOOP:\n  Model sees: conversation history + available tools\n  Model decides: act or respond\n  If act: tool executed, result added to context, loop continues\n  If respond: answer returned, loop ends\n```\n\nThis is not a simplification. This is the actual architecture. Everything else is harness engineering -- mechanisms layered on top of this loop to make the agent more effective. The loop belongs to the agent. The mechanisms belong to the harness.\n\n## Principles of Harness Engineering\n\n### Trust the Model\n\nThe most important principle: **trust the model**.\n\nDon't anticipate every edge case. Don't build elaborate decision trees. Don't pre-specify the workflow.\n\nThe model is better at reasoning than any rule system you could write. Your conditional logic will fail on edge cases. The model will reason through them.\n\n**Give the model tools and knowledge. Let it figure out how to use them.**\n\n### Constraints Enable\n\nThis seems paradoxical, but constraints don't limit agents -- they focus them.\n\nA todo list with \"only one task in progress\" forces sequential focus. A subagent with read-only access prevents accidental modifications. A context compression threshold keeps history from overwhelming.\n\nThe best constraints prevent the model from getting lost, not micromanage its approach.\n\n### Progressive Complexity\n\nNever build everything upfront.\n\n```\nLevel 0: Model + one tool (bash)                     -- s01\nLevel 1: Model + tool dispatch map                    -- s02\nLevel 2: Model + planning                             -- s03\nLevel 3: Model + subagents + skills                   -- s04, s05\nLevel 4: Model + context management + persistence     -- s06, s07, s08\nLevel 5: Model + teams + autonomy + isolation         -- s09-s12\n```\n\nStart at the lowest level that might work. Move up only when real usage reveals the need.\n\n## The Mind Shift\n\nBuilding harnesses requires a fundamental shift in thinking:\n\n**From**: \"How do I make the system do X?\"\n**To**: \"How do I enable the model to do X?\"\n\n**From**: \"What should happen when the user says Y?\"\n**To**: \"What tools would help address Y?\"\n\n**From**: \"What's the workflow for this task?\"\n**To**: \"What does the model need to figure out the workflow?\"\n\n**From**: \"I'm building an agent.\"\n**To**: \"I'm building a harness for the agent.\"\n\nThe best harness code is almost boring. Simple loops. Clear tool definitions. Clean context management. The magic isn't in the code -- it's in the model.\n\n## The Vehicle Metaphor\n\nThe model is the driver. The harness is the vehicle.\n\nA coding agent's vehicle is its IDE, terminal, and filesystem. A farm agent's vehicle is its sensor array, irrigation controls, and weather data. A hotel agent's vehicle is its booking system, guest channels, and facility APIs.\n\nThe driver generalizes. The vehicle specializes. Your job as a harness engineer is to build the best vehicle for your domain -- one that gives the driver maximum visibility, precise controls, and clear boundaries.\n\nBuild the cockpit. Build the dashboard. Build the controls. The pilot is already trained.\n\n## Conclusion\n\nThe model is the agent. The code is the harness. Know which one you're building.\n\nYou are not writing intelligence. You are building the world intelligence inhabits. The quality of that world -- how clearly the agent can perceive, how precisely it can act, how rich its knowledge -- directly determines how effectively the intelligence can express itself.\n\nBuild great harnesses. The agent will do the rest.\n"
  },
  {
    "path": "skills/agent-builder/references/minimal-agent.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nMinimal Agent Template - Copy and customize this.\n\nThis is the simplest possible working agent (~80 lines).\nIt has everything you need: 3 tools + loop.\n\nUsage:\n    1. Set ANTHROPIC_API_KEY environment variable\n    2. python minimal-agent.py\n    3. Type commands, 'q' to quit\n\"\"\"\n\nfrom anthropic import Anthropic\nfrom pathlib import Path\nimport subprocess\nimport os\n\n# Configuration\nclient = Anthropic(api_key=os.getenv(\"ANTHROPIC_API_KEY\"))\nMODEL = os.getenv(\"MODEL_NAME\", \"claude-sonnet-4-20250514\")\nWORKDIR = Path.cwd()\n\n# System prompt - keep it simple\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\n\nRules:\n- Use tools to complete tasks\n- Prefer action over explanation\n- Summarize what you did when done\"\"\"\n\n# Minimal tool set - add more as needed\nTOOLS = [\n    {\n        \"name\": \"bash\",\n        \"description\": \"Run shell command\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"command\": {\"type\": \"string\"}},\n            \"required\": [\"command\"]\n        }\n    },\n    {\n        \"name\": \"read_file\",\n        \"description\": \"Read file contents\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"path\": {\"type\": \"string\"}},\n            \"required\": [\"path\"]\n        }\n    },\n    {\n        \"name\": \"write_file\",\n        \"description\": \"Write content to file\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"path\": {\"type\": \"string\"},\n                \"content\": {\"type\": \"string\"}\n            },\n            \"required\": [\"path\", \"content\"]\n        }\n    },\n]\n\n\ndef execute_tool(name: str, args: dict) -> str:\n    \"\"\"Execute a tool and return result.\"\"\"\n    if name == \"bash\":\n        try:\n            r = subprocess.run(\n                args[\"command\"], shell=True, cwd=WORKDIR,\n                capture_output=True, text=True, timeout=60\n            )\n            return (r.stdout + r.stderr).strip() or \"(empty)\"\n        except subprocess.TimeoutExpired:\n            return \"Error: Timeout\"\n\n    if name == \"read_file\":\n        try:\n            return (WORKDIR / args[\"path\"]).read_text()[:50000]\n        except Exception as e:\n            return f\"Error: {e}\"\n\n    if name == \"write_file\":\n        try:\n            p = WORKDIR / args[\"path\"]\n            p.parent.mkdir(parents=True, exist_ok=True)\n            p.write_text(args[\"content\"])\n            return f\"Wrote {len(args['content'])} bytes to {args['path']}\"\n        except Exception as e:\n            return f\"Error: {e}\"\n\n    return f\"Unknown tool: {name}\"\n\n\ndef agent(prompt: str, history: list = None) -> str:\n    \"\"\"Run the agent loop.\"\"\"\n    if history is None:\n        history = []\n\n    history.append({\"role\": \"user\", \"content\": prompt})\n\n    while True:\n        response = client.messages.create(\n            model=MODEL,\n            system=SYSTEM,\n            messages=history,\n            tools=TOOLS,\n            max_tokens=8000,\n        )\n\n        # Build assistant message\n        history.append({\"role\": \"assistant\", \"content\": response.content})\n\n        # If no tool calls, return text\n        if response.stop_reason != \"tool_use\":\n            return \"\".join(b.text for b in response.content if hasattr(b, \"text\"))\n\n        # Execute tools\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                print(f\"> {block.name}: {block.input}\")\n                output = execute_tool(block.name, block.input)\n                print(f\"  {output[:100]}...\")\n                results.append({\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": block.id,\n                    \"content\": output\n                })\n\n        history.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n    print(f\"Minimal Agent - {WORKDIR}\")\n    print(\"Type 'q' to quit.\\n\")\n\n    history = []\n    while True:\n        try:\n            query = input(\">> \").strip()\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query in (\"q\", \"quit\", \"exit\", \"\"):\n            break\n        print(agent(query, history))\n        print()\n"
  },
  {
    "path": "skills/agent-builder/references/subagent-pattern.py",
    "content": "\"\"\"\nSubagent Pattern - How to implement Task tool for context isolation.\n\nThe key insight: spawn child agents with ISOLATED context to prevent\n\"context pollution\" where exploration details fill up the main conversation.\n\"\"\"\n\nimport time\nimport sys\n\n# Assuming client, MODEL, execute_tool are defined elsewhere\n\n\n# =============================================================================\n# AGENT TYPE REGISTRY\n# =============================================================================\n\nAGENT_TYPES = {\n    # Explore: Read-only, for searching and analyzing\n    \"explore\": {\n        \"description\": \"Read-only agent for exploring code, finding files, searching\",\n        \"tools\": [\"bash\", \"read_file\"],  # No write access!\n        \"prompt\": \"You are an exploration agent. Search and analyze, but NEVER modify files. Return a concise summary of what you found.\",\n    },\n\n    # Code: Full-powered, for implementation\n    \"code\": {\n        \"description\": \"Full agent for implementing features and fixing bugs\",\n        \"tools\": \"*\",  # All tools\n        \"prompt\": \"You are a coding agent. Implement the requested changes efficiently. Return a summary of what you changed.\",\n    },\n\n    # Plan: Read-only, for design work\n    \"plan\": {\n        \"description\": \"Planning agent for designing implementation strategies\",\n        \"tools\": [\"bash\", \"read_file\"],  # Read-only\n        \"prompt\": \"You are a planning agent. Analyze the codebase and output a numbered implementation plan. Do NOT make any changes.\",\n    },\n\n    # Add your own types here...\n    # \"test\": {\n    #     \"description\": \"Testing agent for running and analyzing tests\",\n    #     \"tools\": [\"bash\", \"read_file\"],\n    #     \"prompt\": \"Run tests and report results. Don't modify code.\",\n    # },\n}\n\n\ndef get_agent_descriptions() -> str:\n    \"\"\"Generate descriptions for Task tool schema.\"\"\"\n    return \"\\n\".join(\n        f\"- {name}: {cfg['description']}\"\n        for name, cfg in AGENT_TYPES.items()\n    )\n\n\ndef get_tools_for_agent(agent_type: str, base_tools: list) -> list:\n    \"\"\"\n    Filter tools based on agent type.\n\n    '*' means all base tools.\n    Otherwise, whitelist specific tool names.\n\n    Note: Subagents don't get Task tool to prevent infinite recursion.\n    \"\"\"\n    allowed = AGENT_TYPES.get(agent_type, {}).get(\"tools\", \"*\")\n\n    if allowed == \"*\":\n        return base_tools  # All base tools, but NOT Task\n\n    return [t for t in base_tools if t[\"name\"] in allowed]\n\n\n# =============================================================================\n# TASK TOOL DEFINITION\n# =============================================================================\n\nTASK_TOOL = {\n    \"name\": \"Task\",\n    \"description\": f\"\"\"Spawn a subagent for a focused subtask.\n\nSubagents run in ISOLATED context - they don't see parent's history.\nUse this to keep the main conversation clean.\n\nAgent types:\n{get_agent_descriptions()}\n\nExample uses:\n- Task(explore): \"Find all files using the auth module\"\n- Task(plan): \"Design a migration strategy for the database\"\n- Task(code): \"Implement the user registration form\"\n\"\"\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"description\": {\n                \"type\": \"string\",\n                \"description\": \"Short task name (3-5 words) for progress display\"\n            },\n            \"prompt\": {\n                \"type\": \"string\",\n                \"description\": \"Detailed instructions for the subagent\"\n            },\n            \"agent_type\": {\n                \"type\": \"string\",\n                \"enum\": list(AGENT_TYPES.keys()),\n                \"description\": \"Type of agent to spawn\"\n            },\n        },\n        \"required\": [\"description\", \"prompt\", \"agent_type\"],\n    },\n}\n\n\n# =============================================================================\n# SUBAGENT EXECUTION\n# =============================================================================\n\ndef run_task(description: str, prompt: str, agent_type: str,\n             client, model: str, workdir, base_tools: list, execute_tool) -> str:\n    \"\"\"\n    Execute a subagent task with isolated context.\n\n    Key concepts:\n    1. ISOLATED HISTORY - subagent starts fresh, no parent context\n    2. FILTERED TOOLS - based on agent type permissions\n    3. AGENT-SPECIFIC PROMPT - specialized behavior\n    4. RETURNS SUMMARY ONLY - parent sees just the final result\n\n    Args:\n        description: Short name for progress display\n        prompt: Detailed instructions for subagent\n        agent_type: Key from AGENT_TYPES\n        client: Anthropic client\n        model: Model to use\n        workdir: Working directory\n        base_tools: List of tool definitions\n        execute_tool: Function to execute tools\n\n    Returns:\n        Final text output from subagent\n    \"\"\"\n    if agent_type not in AGENT_TYPES:\n        return f\"Error: Unknown agent type '{agent_type}'\"\n\n    config = AGENT_TYPES[agent_type]\n\n    # Agent-specific system prompt\n    sub_system = f\"\"\"You are a {agent_type} subagent at {workdir}.\n\n{config[\"prompt\"]}\n\nComplete the task and return a clear, concise summary.\"\"\"\n\n    # Filtered tools for this agent type\n    sub_tools = get_tools_for_agent(agent_type, base_tools)\n\n    # KEY: ISOLATED message history!\n    # The subagent starts fresh, doesn't see parent's conversation\n    sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n\n    # Progress display\n    print(f\"  [{agent_type}] {description}\")\n    start = time.time()\n    tool_count = 0\n\n    # Run the same agent loop (but silently)\n    while True:\n        response = client.messages.create(\n            model=model,\n            system=sub_system,\n            messages=sub_messages,\n            tools=sub_tools,\n            max_tokens=8000,\n        )\n\n        # Check if done\n        if response.stop_reason != \"tool_use\":\n            break\n\n        # Execute tools\n        tool_calls = [b for b in response.content if b.type == \"tool_use\"]\n        results = []\n\n        for tc in tool_calls:\n            tool_count += 1\n            output = execute_tool(tc.name, tc.input)\n            results.append({\n                \"type\": \"tool_result\",\n                \"tool_use_id\": tc.id,\n                \"content\": output\n            })\n\n            # Update progress (in-place on same line)\n            elapsed = time.time() - start\n            sys.stdout.write(\n                f\"\\r  [{agent_type}] {description} ... {tool_count} tools, {elapsed:.1f}s\"\n            )\n            sys.stdout.flush()\n\n        sub_messages.append({\"role\": \"assistant\", \"content\": response.content})\n        sub_messages.append({\"role\": \"user\", \"content\": results})\n\n    # Final progress update\n    elapsed = time.time() - start\n    sys.stdout.write(\n        f\"\\r  [{agent_type}] {description} - done ({tool_count} tools, {elapsed:.1f}s)\\n\"\n    )\n\n    # Extract and return ONLY the final text\n    # This is what the parent agent sees - a clean summary\n    for block in response.content:\n        if hasattr(block, \"text\"):\n            return block.text\n\n    return \"(subagent returned no text)\"\n\n\n# =============================================================================\n# USAGE EXAMPLE\n# =============================================================================\n\n\"\"\"\n# In your main agent's execute_tool function:\n\ndef execute_tool(name: str, args: dict) -> str:\n    if name == \"Task\":\n        return run_task(\n            description=args[\"description\"],\n            prompt=args[\"prompt\"],\n            agent_type=args[\"agent_type\"],\n            client=client,\n            model=MODEL,\n            workdir=WORKDIR,\n            base_tools=BASE_TOOLS,\n            execute_tool=execute_tool  # Pass self for recursion\n        )\n    # ... other tools ...\n\n\n# In your TOOLS list:\nTOOLS = BASE_TOOLS + [TASK_TOOL]\n\"\"\"\n"
  },
  {
    "path": "skills/agent-builder/references/tool-templates.py",
    "content": "\"\"\"\nTool Templates - Copy and customize these for your agent.\n\nEach tool needs:\n1. Definition (JSON schema for the model)\n2. Implementation (Python function)\n\"\"\"\n\nfrom pathlib import Path\nimport subprocess\n\nWORKDIR = Path.cwd()\n\n\n# =============================================================================\n# TOOL DEFINITIONS (for TOOLS list)\n# =============================================================================\n\nBASH_TOOL = {\n    \"name\": \"bash\",\n    \"description\": \"Run a shell command. Use for: ls, find, grep, git, npm, python, etc.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"command\": {\n                \"type\": \"string\",\n                \"description\": \"The shell command to execute\"\n            }\n        },\n        \"required\": [\"command\"],\n    },\n}\n\nREAD_FILE_TOOL = {\n    \"name\": \"read_file\",\n    \"description\": \"Read file contents. Returns UTF-8 text.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"path\": {\n                \"type\": \"string\",\n                \"description\": \"Relative path to the file\"\n            },\n            \"limit\": {\n                \"type\": \"integer\",\n                \"description\": \"Max lines to read (default: all)\"\n            },\n        },\n        \"required\": [\"path\"],\n    },\n}\n\nWRITE_FILE_TOOL = {\n    \"name\": \"write_file\",\n    \"description\": \"Write content to a file. Creates parent directories if needed.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"path\": {\n                \"type\": \"string\",\n                \"description\": \"Relative path for the file\"\n            },\n            \"content\": {\n                \"type\": \"string\",\n                \"description\": \"Content to write\"\n            },\n        },\n        \"required\": [\"path\", \"content\"],\n    },\n}\n\nEDIT_FILE_TOOL = {\n    \"name\": \"edit_file\",\n    \"description\": \"Replace exact text in a file. Use for surgical edits.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"path\": {\n                \"type\": \"string\",\n                \"description\": \"Relative path to the file\"\n            },\n            \"old_text\": {\n                \"type\": \"string\",\n                \"description\": \"Exact text to find (must match precisely)\"\n            },\n            \"new_text\": {\n                \"type\": \"string\",\n                \"description\": \"Replacement text\"\n            },\n        },\n        \"required\": [\"path\", \"old_text\", \"new_text\"],\n    },\n}\n\nTODO_WRITE_TOOL = {\n    \"name\": \"TodoWrite\",\n    \"description\": \"Update the task list. Use to plan and track progress.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"items\": {\n                \"type\": \"array\",\n                \"description\": \"Complete list of tasks\",\n                \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"content\": {\"type\": \"string\", \"description\": \"Task description\"},\n                        \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]},\n                        \"activeForm\": {\"type\": \"string\", \"description\": \"Present tense, e.g. 'Reading files'\"},\n                    },\n                    \"required\": [\"content\", \"status\", \"activeForm\"],\n                },\n            }\n        },\n        \"required\": [\"items\"],\n    },\n}\n\nTASK_TOOL_TEMPLATE = \"\"\"\n# Generate dynamically with agent types\nTASK_TOOL = {\n    \"name\": \"Task\",\n    \"description\": f\"Spawn a subagent for a focused subtask.\\\\n\\\\nAgent types:\\\\n{get_agent_descriptions()}\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"description\": {\"type\": \"string\", \"description\": \"Short task name (3-5 words)\"},\n            \"prompt\": {\"type\": \"string\", \"description\": \"Detailed instructions\"},\n            \"agent_type\": {\"type\": \"string\", \"enum\": list(AGENT_TYPES.keys())},\n        },\n        \"required\": [\"description\", \"prompt\", \"agent_type\"],\n    },\n}\n\"\"\"\n\n\n# =============================================================================\n# TOOL IMPLEMENTATIONS\n# =============================================================================\n\ndef safe_path(p: str) -> Path:\n    \"\"\"\n    Security: Ensure path stays within workspace.\n    Prevents ../../../etc/passwd attacks.\n    \"\"\"\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    \"\"\"\n    Execute shell command with safety checks.\n\n    Safety features:\n    - Blocks obviously dangerous commands\n    - 60 second timeout\n    - Output truncated to 50KB\n    \"\"\"\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\n    try:\n        result = subprocess.run(\n            command,\n            shell=True,\n            cwd=WORKDIR,\n            capture_output=True,\n            text=True,\n            timeout=60\n        )\n        output = (result.stdout + result.stderr).strip()\n        return output[:50000] if output else \"(no output)\"\n\n    except subprocess.TimeoutExpired:\n        return \"Error: Command timed out (60s)\"\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_read_file(path: str, limit: int = None) -> str:\n    \"\"\"\n    Read file contents with optional line limit.\n\n    Features:\n    - Safe path resolution\n    - Optional line limit for large files\n    - Output truncated to 50KB\n    \"\"\"\n    try:\n        text = safe_path(path).read_text()\n        lines = text.splitlines()\n\n        if limit and limit < len(lines):\n            lines = lines[:limit]\n            lines.append(f\"... ({len(text.splitlines()) - limit} more lines)\")\n\n        return \"\\n\".join(lines)[:50000]\n\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write_file(path: str, content: str) -> str:\n    \"\"\"\n    Write content to file, creating parent directories if needed.\n\n    Features:\n    - Safe path resolution\n    - Auto-creates parent directories\n    - Returns byte count for confirmation\n    \"\"\"\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\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_edit_file(path: str, old_text: str, new_text: str) -> str:\n    \"\"\"\n    Replace exact text in a file (surgical edit).\n\n    Features:\n    - Exact string matching (not regex)\n    - Only replaces first occurrence (safety)\n    - Clear error if text not found\n    \"\"\"\n    try:\n        fp = safe_path(path)\n        content = fp.read_text()\n\n        if old_text not in content:\n            return f\"Error: Text not found in {path}\"\n\n        new_content = content.replace(old_text, new_text, 1)\n        fp.write_text(new_content)\n        return f\"Edited {path}\"\n\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\n# =============================================================================\n# DISPATCHER PATTERN\n# =============================================================================\n\ndef execute_tool(name: str, args: dict) -> str:\n    \"\"\"\n    Dispatch tool call to implementation.\n\n    This pattern makes it easy to add new tools:\n    1. Add definition to TOOLS list\n    2. Add implementation function\n    3. Add case to this dispatcher\n    \"\"\"\n    if name == \"bash\":\n        return run_bash(args[\"command\"])\n    if name == \"read_file\":\n        return run_read_file(args[\"path\"], args.get(\"limit\"))\n    if name == \"write_file\":\n        return run_write_file(args[\"path\"], args[\"content\"])\n    if name == \"edit_file\":\n        return run_edit_file(args[\"path\"], args[\"old_text\"], args[\"new_text\"])\n    # Add more tools here...\n    return f\"Unknown tool: {name}\"\n"
  },
  {
    "path": "skills/agent-builder/scripts/init_agent.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nAgent Scaffold Script - Create a new agent project with best practices.\n\nUsage:\n    python init_agent.py <agent-name> [--level 0-4] [--path <output-dir>]\n\nExamples:\n    python init_agent.py my-agent                 # Level 1 (4 tools)\n    python init_agent.py my-agent --level 0      # Minimal (bash only)\n    python init_agent.py my-agent --level 2      # With TodoWrite\n    python init_agent.py my-agent --path ./bots  # Custom output directory\n\"\"\"\n\nimport argparse\nimport sys\nfrom pathlib import Path\n\n# Agent templates for each level\nTEMPLATES = {\n    0: '''#!/usr/bin/env python3\n\"\"\"\nLevel 0 Agent - Bash is All You Need (~50 lines)\n\nCore insight: One tool (bash) can do everything.\nSubagents via self-recursion: python {name}.py \"subtask\"\n\"\"\"\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\nimport subprocess\nimport os\n\nload_dotenv()\n\nclient = Anthropic(\n    api_key=os.getenv(\"ANTHROPIC_API_KEY\"),\n    base_url=os.getenv(\"ANTHROPIC_BASE_URL\")\n)\nMODEL = os.getenv(\"MODEL_NAME\", \"claude-sonnet-4-20250514\")\n\nSYSTEM = \"\"\"You are a coding agent. Use bash for everything:\n- Read: cat, grep, find, ls\n- Write: echo 'content' > file\n- Subagent: python {name}.py \"subtask\"\n\"\"\"\n\nTOOL = [{{\n    \"name\": \"bash\",\n    \"description\": \"Execute shell command\",\n    \"input_schema\": {{\"type\": \"object\", \"properties\": {{\"command\": {{\"type\": \"string\"}}}}, \"required\": [\"command\"]}}\n}}]\n\ndef run(prompt, history=[]):\n    history.append({{\"role\": \"user\", \"content\": prompt}})\n    while True:\n        r = client.messages.create(model=MODEL, system=SYSTEM, messages=history, tools=TOOL, max_tokens=8000)\n        history.append({{\"role\": \"assistant\", \"content\": r.content}})\n        if r.stop_reason != \"tool_use\":\n            return \"\".join(b.text for b in r.content if hasattr(b, \"text\"))\n        results = []\n        for b in r.content:\n            if b.type == \"tool_use\":\n                print(f\"> {{b.input['command']}}\")\n                try:\n                    out = subprocess.run(b.input[\"command\"], shell=True, capture_output=True, text=True, timeout=60)\n                    output = (out.stdout + out.stderr).strip() or \"(empty)\"\n                except Exception as e:\n                    output = f\"Error: {{e}}\"\n                results.append({{\"type\": \"tool_result\", \"tool_use_id\": b.id, \"content\": output[:50000]}})\n        history.append({{\"role\": \"user\", \"content\": results}})\n\nif __name__ == \"__main__\":\n    h = []\n    print(\"{name} - Level 0 Agent\\\\nType 'q' to quit.\\\\n\")\n    while (q := input(\">> \").strip()) not in (\"q\", \"quit\", \"\"):\n        print(run(q, h), \"\\\\n\")\n''',\n\n    1: '''#!/usr/bin/env python3\n\"\"\"\nLevel 1 Agent - Model as Agent (~200 lines)\n\nCore insight: 4 tools cover 90% of coding tasks.\nThe model IS the agent. Code just runs the loop.\n\"\"\"\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\nfrom pathlib import Path\nimport subprocess\nimport os\n\nload_dotenv()\n\nclient = Anthropic(\n    api_key=os.getenv(\"ANTHROPIC_API_KEY\"),\n    base_url=os.getenv(\"ANTHROPIC_BASE_URL\")\n)\nMODEL = os.getenv(\"MODEL_NAME\", \"claude-sonnet-4-20250514\")\nWORKDIR = Path.cwd()\n\nSYSTEM = f\"\"\"You are a coding agent at {{WORKDIR}}.\n\nRules:\n- Prefer tools over prose. Act, don't just explain.\n- Never invent file paths. Use ls/find first if unsure.\n- Make minimal changes. Don't over-engineer.\n- After finishing, summarize what changed.\"\"\"\n\nTOOLS = [\n    {{\"name\": \"bash\", \"description\": \"Run 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 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 file\",\n     \"input_schema\": {{\"type\": \"object\", \"properties\": {{\"path\": {{\"type\": \"string\"}}, \"old_text\": {{\"type\": \"string\"}}, \"new_text\": {{\"type\": \"string\"}}}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}}}},\n]\n\ndef safe_path(p: str) -> Path:\n    \"\"\"Prevent path escape attacks.\"\"\"\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 execute(name: str, args: dict) -> str:\n    \"\"\"Execute a tool and return result.\"\"\"\n    if name == \"bash\":\n        dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"> /dev/\"]\n        if any(d in args[\"command\"] for d in dangerous):\n            return \"Error: Dangerous command blocked\"\n        try:\n            r = subprocess.run(args[\"command\"], shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=60)\n            return (r.stdout + r.stderr).strip()[:50000] or \"(empty)\"\n        except subprocess.TimeoutExpired:\n            return \"Error: Timeout (60s)\"\n        except Exception as e:\n            return f\"Error: {{e}}\"\n\n    if name == \"read_file\":\n        try:\n            return safe_path(args[\"path\"]).read_text()[:50000]\n        except Exception as e:\n            return f\"Error: {{e}}\"\n\n    if name == \"write_file\":\n        try:\n            p = safe_path(args[\"path\"])\n            p.parent.mkdir(parents=True, exist_ok=True)\n            p.write_text(args[\"content\"])\n            return f\"Wrote {{len(args['content'])}} bytes to {{args['path']}}\"\n        except Exception as e:\n            return f\"Error: {{e}}\"\n\n    if name == \"edit_file\":\n        try:\n            p = safe_path(args[\"path\"])\n            content = p.read_text()\n            if args[\"old_text\"] not in content:\n                return f\"Error: Text not found in {{args['path']}}\"\n            p.write_text(content.replace(args[\"old_text\"], args[\"new_text\"], 1))\n            return f\"Edited {{args['path']}}\"\n        except Exception as e:\n            return f\"Error: {{e}}\"\n\n    return f\"Unknown tool: {{name}}\"\n\ndef agent(prompt: str, history: list = None) -> str:\n    \"\"\"Run the agent loop.\"\"\"\n    if history is None:\n        history = []\n    history.append({{\"role\": \"user\", \"content\": prompt}})\n\n    while True:\n        response = client.messages.create(\n            model=MODEL, system=SYSTEM, messages=history, tools=TOOLS, max_tokens=8000\n        )\n        history.append({{\"role\": \"assistant\", \"content\": response.content}})\n\n        if response.stop_reason != \"tool_use\":\n            return \"\".join(b.text for b in response.content if hasattr(b, \"text\"))\n\n        results = []\n        for block in response.content:\n            if block.type == \"tool_use\":\n                print(f\"> {{block.name}}: {{str(block.input)[:100]}}\")\n                output = execute(block.name, block.input)\n                print(f\"  {{output[:100]}}...\")\n                results.append({{\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output}})\n        history.append({{\"role\": \"user\", \"content\": results}})\n\nif __name__ == \"__main__\":\n    print(f\"{name} - Level 1 Agent at {{WORKDIR}}\")\n    print(\"Type 'q' to quit.\\\\n\")\n    h = []\n    while True:\n        try:\n            query = input(\">> \").strip()\n        except (EOFError, KeyboardInterrupt):\n            break\n        if query in (\"q\", \"quit\", \"exit\", \"\"):\n            break\n        print(agent(query, h), \"\\\\n\")\n''',\n}\n\nENV_TEMPLATE = '''# API Configuration\nANTHROPIC_API_KEY=sk-xxx\nANTHROPIC_BASE_URL=https://api.anthropic.com\nMODEL_NAME=claude-sonnet-4-20250514\n'''\n\n\ndef create_agent(name: str, level: int, output_dir: Path):\n    \"\"\"Create a new agent project.\"\"\"\n    # Validate level\n    if level not in TEMPLATES and level not in (2, 3, 4):\n        print(f\"Error: Level {level} not yet implemented in scaffold.\")\n        print(\"Available levels: 0 (minimal), 1 (4 tools)\")\n        print(\"For levels 2-4, copy from mini-claude-code repository.\")\n        sys.exit(1)\n\n    # Create output directory\n    agent_dir = output_dir / name\n    agent_dir.mkdir(parents=True, exist_ok=True)\n\n    # Write agent file\n    agent_file = agent_dir / f\"{name}.py\"\n    template = TEMPLATES.get(level, TEMPLATES[1])\n    agent_file.write_text(template.format(name=name))\n    print(f\"Created: {agent_file}\")\n\n    # Write .env.example\n    env_file = agent_dir / \".env.example\"\n    env_file.write_text(ENV_TEMPLATE)\n    print(f\"Created: {env_file}\")\n\n    # Write .gitignore\n    gitignore = agent_dir / \".gitignore\"\n    gitignore.write_text(\".env\\n__pycache__/\\n*.pyc\\n\")\n    print(f\"Created: {gitignore}\")\n\n    print(f\"\\nAgent '{name}' created at {agent_dir}\")\n    print(f\"\\nNext steps:\")\n    print(f\"  1. cd {agent_dir}\")\n    print(f\"  2. cp .env.example .env\")\n    print(f\"  3. Edit .env with your API key\")\n    print(f\"  4. pip install anthropic python-dotenv\")\n    print(f\"  5. python {name}.py\")\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Scaffold a new AI coding agent project\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=\"\"\"\nLevels:\n  0  Minimal (~50 lines) - Single bash tool, self-recursion for subagents\n  1  Basic (~200 lines)  - 4 core tools: bash, read, write, edit\n  2  Todo (~300 lines)   - + TodoWrite for structured planning\n  3  Subagent (~450)     - + Task tool for context isolation\n  4  Skills (~550)       - + Skill tool for domain expertise\n        \"\"\"\n    )\n    parser.add_argument(\"name\", help=\"Name of the agent to create\")\n    parser.add_argument(\"--level\", type=int, default=1, choices=[0, 1, 2, 3, 4],\n                       help=\"Complexity level (default: 1)\")\n    parser.add_argument(\"--path\", type=Path, default=Path.cwd(),\n                       help=\"Output directory (default: current directory)\")\n\n    args = parser.parse_args()\n    create_agent(args.name, args.level, args.path)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/code-review/SKILL.md",
    "content": "---\nname: code-review\ndescription: Perform thorough code reviews with security, performance, and maintainability analysis. Use when user asks to review code, check for bugs, or audit a codebase.\n---\n\n# Code Review Skill\n\nYou now have expertise in conducting comprehensive code reviews. Follow this structured approach:\n\n## Review Checklist\n\n### 1. Security (Critical)\n\nCheck for:\n- [ ] **Injection vulnerabilities**: SQL, command, XSS, template injection\n- [ ] **Authentication issues**: Hardcoded credentials, weak auth\n- [ ] **Authorization flaws**: Missing access controls, IDOR\n- [ ] **Data exposure**: Sensitive data in logs, error messages\n- [ ] **Cryptography**: Weak algorithms, improper key management\n- [ ] **Dependencies**: Known vulnerabilities (check with `npm audit`, `pip-audit`)\n\n```bash\n# Quick security scans\nnpm audit                    # Node.js\npip-audit                    # Python\ncargo audit                  # Rust\ngrep -r \"password\\|secret\\|api_key\" --include=\"*.py\" --include=\"*.js\"\n```\n\n### 2. Correctness\n\nCheck for:\n- [ ] **Logic errors**: Off-by-one, null handling, edge cases\n- [ ] **Race conditions**: Concurrent access without synchronization\n- [ ] **Resource leaks**: Unclosed files, connections, memory\n- [ ] **Error handling**: Swallowed exceptions, missing error paths\n- [ ] **Type safety**: Implicit conversions, any types\n\n### 3. Performance\n\nCheck for:\n- [ ] **N+1 queries**: Database calls in loops\n- [ ] **Memory issues**: Large allocations, retained references\n- [ ] **Blocking operations**: Sync I/O in async code\n- [ ] **Inefficient algorithms**: O(n^2) when O(n) possible\n- [ ] **Missing caching**: Repeated expensive computations\n\n### 4. Maintainability\n\nCheck for:\n- [ ] **Naming**: Clear, consistent, descriptive\n- [ ] **Complexity**: Functions > 50 lines, deep nesting > 3 levels\n- [ ] **Duplication**: Copy-pasted code blocks\n- [ ] **Dead code**: Unused imports, unreachable branches\n- [ ] **Comments**: Outdated, redundant, or missing where needed\n\n### 5. Testing\n\nCheck for:\n- [ ] **Coverage**: Critical paths tested\n- [ ] **Edge cases**: Null, empty, boundary values\n- [ ] **Mocking**: External dependencies isolated\n- [ ] **Assertions**: Meaningful, specific checks\n\n## Review Output Format\n\n```markdown\n## Code Review: [file/component name]\n\n### Summary\n[1-2 sentence overview]\n\n### Critical Issues\n1. **[Issue]** (line X): [Description]\n   - Impact: [What could go wrong]\n   - Fix: [Suggested solution]\n\n### Improvements\n1. **[Suggestion]** (line X): [Description]\n\n### Positive Notes\n- [What was done well]\n\n### Verdict\n[ ] Ready to merge\n[ ] Needs minor changes\n[ ] Needs major revision\n```\n\n## Common Patterns to Flag\n\n### Python\n```python\n# Bad: SQL injection\ncursor.execute(f\"SELECT * FROM users WHERE id = {user_id}\")\n# Good:\ncursor.execute(\"SELECT * FROM users WHERE id = ?\", (user_id,))\n\n# Bad: Command injection\nos.system(f\"ls {user_input}\")\n# Good:\nsubprocess.run([\"ls\", user_input], check=True)\n\n# Bad: Mutable default argument\ndef append(item, lst=[]):  # Bug: shared mutable default\n# Good:\ndef append(item, lst=None):\n    lst = lst or []\n```\n\n### JavaScript/TypeScript\n```javascript\n// Bad: Prototype pollution\nObject.assign(target, userInput)\n// Good:\nObject.assign(target, sanitize(userInput))\n\n// Bad: eval usage\neval(userCode)\n// Good: Never use eval with user input\n\n// Bad: Callback hell\ngetData(x => process(x, y => save(y, z => done(z))))\n// Good:\nconst data = await getData();\nconst processed = await process(data);\nawait save(processed);\n```\n\n## Review Commands\n\n```bash\n# Show recent changes\ngit diff HEAD~5 --stat\ngit log --oneline -10\n\n# Find potential issues\ngrep -rn \"TODO\\|FIXME\\|HACK\\|XXX\" .\ngrep -rn \"password\\|secret\\|token\" . --include=\"*.py\"\n\n# Check complexity (Python)\npip install radon && radon cc . -a\n\n# Check dependencies\nnpm outdated  # Node\npip list --outdated  # Python\n```\n\n## Review Workflow\n\n1. **Understand context**: Read PR description, linked issues\n2. **Run the code**: Build, test, run locally if possible\n3. **Read top-down**: Start with main entry points\n4. **Check tests**: Are changes tested? Do tests pass?\n5. **Security scan**: Run automated tools\n6. **Manual review**: Use checklist above\n7. **Write feedback**: Be specific, suggest fixes, be kind\n"
  },
  {
    "path": "skills/mcp-builder/SKILL.md",
    "content": "---\nname: mcp-builder\ndescription: Build MCP (Model Context Protocol) servers that give Claude new capabilities. Use when user wants to create an MCP server, add tools to Claude, or integrate external services.\n---\n\n# MCP Server Building Skill\n\nYou now have expertise in building MCP (Model Context Protocol) servers. MCP enables Claude to interact with external services through a standardized protocol.\n\n## What is MCP?\n\nMCP servers expose:\n- **Tools**: Functions Claude can call (like API endpoints)\n- **Resources**: Data Claude can read (like files or database records)\n- **Prompts**: Pre-built prompt templates\n\n## Quick Start: Python MCP Server\n\n### 1. Project Setup\n\n```bash\n# Create project\nmkdir my-mcp-server && cd my-mcp-server\npython3 -m venv venv && source venv/bin/activate\n\n# Install MCP SDK\npip install mcp\n```\n\n### 2. Basic Server Template\n\n```python\n#!/usr/bin/env python3\n\"\"\"my_server.py - A simple MCP server\"\"\"\n\nfrom mcp.server import Server\nfrom mcp.server.stdio import stdio_server\nfrom mcp.types import Tool, TextContent\n\n# Create server instance\nserver = Server(\"my-server\")\n\n# Define a tool\n@server.tool()\nasync def hello(name: str) -> str:\n    \"\"\"Say hello to someone.\n\n    Args:\n        name: The name to greet\n    \"\"\"\n    return f\"Hello, {name}!\"\n\n@server.tool()\nasync def add_numbers(a: int, b: int) -> str:\n    \"\"\"Add two numbers together.\n\n    Args:\n        a: First number\n        b: Second number\n    \"\"\"\n    return str(a + b)\n\n# Run server\nasync def main():\n    async with stdio_server() as (read, write):\n        await server.run(read, write)\n\nif __name__ == \"__main__\":\n    import asyncio\n    asyncio.run(main())\n```\n\n### 3. Register with Claude\n\nAdd to `~/.claude/mcp.json`:\n```json\n{\n  \"mcpServers\": {\n    \"my-server\": {\n      \"command\": \"python3\",\n      \"args\": [\"/path/to/my_server.py\"]\n    }\n  }\n}\n```\n\n## TypeScript MCP Server\n\n### 1. Setup\n\n```bash\nmkdir my-mcp-server && cd my-mcp-server\nnpm init -y\nnpm install @modelcontextprotocol/sdk\n```\n\n### 2. Template\n\n```typescript\n// src/index.ts\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nconst server = new Server({\n  name: \"my-server\",\n  version: \"1.0.0\",\n});\n\n// Define tools\nserver.setRequestHandler(\"tools/list\", async () => ({\n  tools: [\n    {\n      name: \"hello\",\n      description: \"Say hello to someone\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          name: { type: \"string\", description: \"Name to greet\" },\n        },\n        required: [\"name\"],\n      },\n    },\n  ],\n}));\n\nserver.setRequestHandler(\"tools/call\", async (request) => {\n  if (request.params.name === \"hello\") {\n    const name = request.params.arguments.name;\n    return { content: [{ type: \"text\", text: `Hello, ${name}!` }] };\n  }\n  throw new Error(\"Unknown tool\");\n});\n\n// Start server\nconst transport = new StdioServerTransport();\nserver.connect(transport);\n```\n\n## Advanced Patterns\n\n### External API Integration\n\n```python\nimport httpx\nfrom mcp.server import Server\n\nserver = Server(\"weather-server\")\n\n@server.tool()\nasync def get_weather(city: str) -> str:\n    \"\"\"Get current weather for a city.\"\"\"\n    async with httpx.AsyncClient() as client:\n        resp = await client.get(\n            f\"https://api.weatherapi.com/v1/current.json\",\n            params={\"key\": \"YOUR_API_KEY\", \"q\": city}\n        )\n        data = resp.json()\n        return f\"{city}: {data['current']['temp_c']}C, {data['current']['condition']['text']}\"\n```\n\n### Database Access\n\n```python\nimport sqlite3\nfrom mcp.server import Server\n\nserver = Server(\"db-server\")\n\n@server.tool()\nasync def query_db(sql: str) -> str:\n    \"\"\"Execute a read-only SQL query.\"\"\"\n    if not sql.strip().upper().startswith(\"SELECT\"):\n        return \"Error: Only SELECT queries allowed\"\n\n    conn = sqlite3.connect(\"data.db\")\n    cursor = conn.execute(sql)\n    rows = cursor.fetchall()\n    conn.close()\n    return str(rows)\n```\n\n### Resources (Read-only Data)\n\n```python\n@server.resource(\"config://settings\")\nasync def get_settings() -> str:\n    \"\"\"Application settings.\"\"\"\n    return open(\"settings.json\").read()\n\n@server.resource(\"file://{path}\")\nasync def read_file(path: str) -> str:\n    \"\"\"Read a file from the workspace.\"\"\"\n    return open(path).read()\n```\n\n## Testing\n\n```bash\n# Test with MCP Inspector\nnpx @anthropics/mcp-inspector python3 my_server.py\n\n# Or send test messages directly\necho '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}' | python3 my_server.py\n```\n\n## Best Practices\n\n1. **Clear tool descriptions**: Claude uses these to decide when to call tools\n2. **Input validation**: Always validate and sanitize inputs\n3. **Error handling**: Return meaningful error messages\n4. **Async by default**: Use async/await for I/O operations\n5. **Security**: Never expose sensitive operations without auth\n6. **Idempotency**: Tools should be safe to retry\n"
  },
  {
    "path": "skills/pdf/SKILL.md",
    "content": "---\nname: pdf\ndescription: Process PDF files - extract text, create PDFs, merge documents. Use when user asks to read PDF, create PDF, or work with PDF files.\n---\n\n# PDF Processing Skill\n\nYou now have expertise in PDF manipulation. Follow these workflows:\n\n## Reading PDFs\n\n**Option 1: Quick text extraction (preferred)**\n```bash\n# Using pdftotext (poppler-utils)\npdftotext input.pdf -  # Output to stdout\npdftotext input.pdf output.txt  # Output to file\n\n# If pdftotext not available, try:\npython3 -c \"\nimport fitz  # PyMuPDF\ndoc = fitz.open('input.pdf')\nfor page in doc:\n    print(page.get_text())\n\"\n```\n\n**Option 2: Page-by-page with metadata**\n```python\nimport fitz  # pip install pymupdf\n\ndoc = fitz.open(\"input.pdf\")\nprint(f\"Pages: {len(doc)}\")\nprint(f\"Metadata: {doc.metadata}\")\n\nfor i, page in enumerate(doc):\n    text = page.get_text()\n    print(f\"--- Page {i+1} ---\")\n    print(text)\n```\n\n## Creating PDFs\n\n**Option 1: From Markdown (recommended)**\n```bash\n# Using pandoc\npandoc input.md -o output.pdf\n\n# With custom styling\npandoc input.md -o output.pdf --pdf-engine=xelatex -V geometry:margin=1in\n```\n\n**Option 2: Programmatically**\n```python\nfrom reportlab.lib.pagesizes import letter\nfrom reportlab.pdfgen import canvas\n\nc = canvas.Canvas(\"output.pdf\", pagesize=letter)\nc.drawString(100, 750, \"Hello, PDF!\")\nc.save()\n```\n\n**Option 3: From HTML**\n```bash\n# Using wkhtmltopdf\nwkhtmltopdf input.html output.pdf\n\n# Or with Python\npython3 -c \"\nimport pdfkit\npdfkit.from_file('input.html', 'output.pdf')\n\"\n```\n\n## Merging PDFs\n\n```python\nimport fitz\n\nresult = fitz.open()\nfor pdf_path in [\"file1.pdf\", \"file2.pdf\", \"file3.pdf\"]:\n    doc = fitz.open(pdf_path)\n    result.insert_pdf(doc)\nresult.save(\"merged.pdf\")\n```\n\n## Splitting PDFs\n\n```python\nimport fitz\n\ndoc = fitz.open(\"input.pdf\")\nfor i in range(len(doc)):\n    single = fitz.open()\n    single.insert_pdf(doc, from_page=i, to_page=i)\n    single.save(f\"page_{i+1}.pdf\")\n```\n\n## Key Libraries\n\n| Task | Library | Install |\n|------|---------|---------|\n| Read/Write/Merge | PyMuPDF | `pip install pymupdf` |\n| Create from scratch | ReportLab | `pip install reportlab` |\n| HTML to PDF | pdfkit | `pip install pdfkit` + wkhtmltopdf |\n| Text extraction | pdftotext | `brew install poppler` / `apt install poppler-utils` |\n\n## Best Practices\n\n1. **Always check if tools are installed** before using them\n2. **Handle encoding issues** - PDFs may contain various character encodings\n3. **Large PDFs**: Process page by page to avoid memory issues\n4. **OCR for scanned PDFs**: Use `pytesseract` if text extraction returns empty\n"
  },
  {
    "path": "web/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.env*.local\n"
  },
  {
    "path": "web/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "web/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  output: \"export\",\n  images: { unoptimized: true },\n  trailingSlash: true,\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"web\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"extract\": \"tsx scripts/extract-content.ts\",\n    \"predev\": \"npm run extract\",\n    \"dev\": \"next dev\",\n    \"prebuild\": \"npm run extract\",\n    \"build\": \"next build\",\n    \"start\": \"next start\"\n  },\n  \"dependencies\": {\n    \"diff\": \"^8.0.3\",\n    \"framer-motion\": \"^12.34.0\",\n    \"lucide-react\": \"^0.564.0\",\n    \"next\": \"16.1.6\",\n    \"react\": \"19.2.3\",\n    \"react-dom\": \"19.2.3\",\n    \"rehype-highlight\": \"^7.0.2\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"rehype-stringify\": \"^10.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-parse\": \"^11.0.0\",\n    \"remark-rehype\": \"^11.1.2\",\n    \"tsx\": \"^4.21.0\",\n    \"unified\": \"^11.0.5\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/diff\": \"^7.0.2\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"tailwindcss\": \"^4\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "web/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "web/scripts/extract-content.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport type {\n  AgentVersion,\n  VersionDiff,\n  DocContent,\n  VersionIndex,\n} from \"../src/types/agent-data\";\nimport { VERSION_META, VERSION_ORDER, LEARNING_PATH } from \"../src/lib/constants\";\n\n// Resolve paths relative to this script's location (web/scripts/)\nconst WEB_DIR = path.resolve(__dirname, \"..\");\nconst REPO_ROOT = path.resolve(WEB_DIR, \"..\");\nconst AGENTS_DIR = path.join(REPO_ROOT, \"agents\");\nconst DOCS_DIR = path.join(REPO_ROOT, \"docs\");\nconst OUT_DIR = path.join(WEB_DIR, \"src\", \"data\", \"generated\");\n\n// Map python filenames to version IDs\n// s01_agent_loop.py -> s01\n// s02_tools.py -> s02\n// s_full.py -> s_full (reference agent, typically skipped)\nfunction filenameToVersionId(filename: string): string | null {\n  const base = path.basename(filename, \".py\");\n  if (base === \"s_full\") return null;\n  if (base === \"__init__\") return null;\n\n  const match = base.match(/^(s\\d+[a-c]?)_/);\n  if (!match) return null;\n  return match[1];\n}\n\n// Extract classes from Python source\nfunction extractClasses(\n  lines: string[]\n): { name: string; startLine: number; endLine: number }[] {\n  const classes: { name: string; startLine: number; endLine: number }[] = [];\n  const classPattern = /^class\\s+(\\w+)/;\n\n  for (let i = 0; i < lines.length; i++) {\n    const m = lines[i].match(classPattern);\n    if (m) {\n      const name = m[1];\n      const startLine = i + 1;\n      // Find end of class: next class/function at indent 0, or EOF\n      let endLine = lines.length;\n      for (let j = i + 1; j < lines.length; j++) {\n        if (\n          lines[j].match(/^class\\s/) ||\n          lines[j].match(/^def\\s/) ||\n          (lines[j].match(/^\\S/) && lines[j].trim() !== \"\" && !lines[j].startsWith(\"#\") && !lines[j].startsWith(\"@\"))\n        ) {\n          endLine = j;\n          break;\n        }\n      }\n      classes.push({ name, startLine, endLine });\n    }\n  }\n  return classes;\n}\n\n// Extract top-level functions from Python source\nfunction extractFunctions(\n  lines: string[]\n): { name: string; signature: string; startLine: number }[] {\n  const functions: { name: string; signature: string; startLine: number }[] = [];\n  const funcPattern = /^def\\s+(\\w+)\\((.*?)\\)/;\n\n  for (let i = 0; i < lines.length; i++) {\n    const m = lines[i].match(funcPattern);\n    if (m) {\n      functions.push({\n        name: m[1],\n        signature: `def ${m[1]}(${m[2]})`,\n        startLine: i + 1,\n      });\n    }\n  }\n  return functions;\n}\n\n// Extract tool names from Python source\n// Looks for \"name\": \"tool_name\" patterns in dict literals\nfunction extractTools(source: string): string[] {\n  const toolPattern = /\"name\"\\s*:\\s*\"(\\w+)\"/g;\n  const tools = new Set<string>();\n  let m;\n  while ((m = toolPattern.exec(source)) !== null) {\n    tools.add(m[1]);\n  }\n  return Array.from(tools);\n}\n\n// Count non-blank, non-comment lines\nfunction countLoc(lines: string[]): number {\n  return lines.filter((line) => {\n    const trimmed = line.trim();\n    return trimmed !== \"\" && !trimmed.startsWith(\"#\");\n  }).length;\n}\n\n// Detect locale from subdirectory path\n// docs/en/s01-the-agent-loop.md -> \"en\"\n// docs/zh/s01-the-agent-loop.md -> \"zh\"\n// docs/ja/s01-the-agent-loop.md -> \"ja\"\nfunction detectLocale(relPath: string): \"en\" | \"zh\" | \"ja\" {\n  if (relPath.startsWith(\"zh/\") || relPath.startsWith(\"zh\\\\\")) return \"zh\";\n  if (relPath.startsWith(\"ja/\") || relPath.startsWith(\"ja\\\\\")) return \"ja\";\n  return \"en\";\n}\n\n// Extract version from doc filename (e.g., \"s01-the-agent-loop.md\" -> \"s01\")\nfunction extractDocVersion(filename: string): string | null {\n  const m = filename.match(/^(s\\d+[a-c]?)-/);\n  return m ? m[1] : null;\n}\n\n// Main extraction\nfunction main() {\n  console.log(\"Extracting content from agents and docs...\");\n  console.log(`  Repo root: ${REPO_ROOT}`);\n  console.log(`  Agents dir: ${AGENTS_DIR}`);\n  console.log(`  Docs dir: ${DOCS_DIR}`);\n\n  // Skip extraction if source directories don't exist (e.g. Vercel build).\n  // Pre-committed generated data will be used instead.\n  if (!fs.existsSync(AGENTS_DIR)) {\n    console.log(\"  Agents directory not found, skipping extraction.\");\n    console.log(\"  Using pre-committed generated data.\");\n    return;\n  }\n\n  // 1. Read all agent files\n  const agentFiles = fs\n    .readdirSync(AGENTS_DIR)\n    .filter((f) => f.startsWith(\"s\") && f.endsWith(\".py\"));\n\n  console.log(`  Found ${agentFiles.length} agent files`);\n\n  const versions: AgentVersion[] = [];\n\n  for (const filename of agentFiles) {\n    const versionId = filenameToVersionId(filename);\n    if (!versionId) {\n      console.warn(`  Skipping ${filename}: could not determine version ID`);\n      continue;\n    }\n\n    const filePath = path.join(AGENTS_DIR, filename);\n    const source = fs.readFileSync(filePath, \"utf-8\");\n    const lines = source.split(\"\\n\");\n\n    const meta = VERSION_META[versionId];\n    const classes = extractClasses(lines);\n    const functions = extractFunctions(lines);\n    const tools = extractTools(source);\n    const loc = countLoc(lines);\n\n    versions.push({\n      id: versionId,\n      filename,\n      title: meta?.title ?? versionId,\n      subtitle: meta?.subtitle ?? \"\",\n      loc,\n      tools,\n      newTools: [], // computed after all versions are loaded\n      coreAddition: meta?.coreAddition ?? \"\",\n      keyInsight: meta?.keyInsight ?? \"\",\n      classes,\n      functions,\n      layer: meta?.layer ?? \"tools\",\n      source,\n    });\n  }\n\n  // Sort versions according to VERSION_ORDER\n  const orderMap = new Map(VERSION_ORDER.map((v, i) => [v, i]));\n  versions.sort(\n    (a, b) => (orderMap.get(a.id as any) ?? 99) - (orderMap.get(b.id as any) ?? 99)\n  );\n\n  // 2. Compute newTools for each version\n  for (let i = 0; i < versions.length; i++) {\n    const prev = i > 0 ? new Set(versions[i - 1].tools) : new Set<string>();\n    versions[i].newTools = versions[i].tools.filter((t) => !prev.has(t));\n  }\n\n  // 3. Compute diffs between adjacent versions in LEARNING_PATH\n  const diffs: VersionDiff[] = [];\n  const versionMap = new Map(versions.map((v) => [v.id, v]));\n\n  for (let i = 1; i < LEARNING_PATH.length; i++) {\n    const fromId = LEARNING_PATH[i - 1];\n    const toId = LEARNING_PATH[i];\n    const fromVer = versionMap.get(fromId);\n    const toVer = versionMap.get(toId);\n\n    if (!fromVer || !toVer) continue;\n\n    const fromClassNames = new Set(fromVer.classes.map((c) => c.name));\n    const fromFuncNames = new Set(fromVer.functions.map((f) => f.name));\n    const fromToolNames = new Set(fromVer.tools);\n\n    diffs.push({\n      from: fromId,\n      to: toId,\n      newClasses: toVer.classes\n        .map((c) => c.name)\n        .filter((n) => !fromClassNames.has(n)),\n      newFunctions: toVer.functions\n        .map((f) => f.name)\n        .filter((n) => !fromFuncNames.has(n)),\n      newTools: toVer.tools.filter((t) => !fromToolNames.has(t)),\n      locDelta: toVer.loc - fromVer.loc,\n    });\n  }\n\n  // 4. Read doc files from locale subdirectories (en/, zh/, ja/)\n  const docs: DocContent[] = [];\n\n  if (fs.existsSync(DOCS_DIR)) {\n    const localeDirs = [\"en\", \"zh\", \"ja\"];\n    let totalDocFiles = 0;\n\n    for (const locale of localeDirs) {\n      const localeDir = path.join(DOCS_DIR, locale);\n      if (!fs.existsSync(localeDir)) continue;\n\n      const docFiles = fs\n        .readdirSync(localeDir)\n        .filter((f) => f.endsWith(\".md\"));\n\n      totalDocFiles += docFiles.length;\n\n      for (const filename of docFiles) {\n        const version = extractDocVersion(filename);\n        if (!version) {\n          console.warn(`  Skipping doc ${locale}/${filename}: could not determine version`);\n          continue;\n        }\n\n        const filePath = path.join(localeDir, filename);\n        const content = fs.readFileSync(filePath, \"utf-8\");\n\n        const titleMatch = content.match(/^#\\s+(.+)$/m);\n        const title = titleMatch ? titleMatch[1] : filename;\n\n        docs.push({ version, locale: locale as \"en\" | \"zh\" | \"ja\", title, content });\n      }\n    }\n\n    console.log(`  Found ${totalDocFiles} doc files across ${localeDirs.length} locales`);\n  } else {\n    console.warn(`  Docs directory not found: ${DOCS_DIR}`);\n  }\n\n  // 5. Write output\n  fs.mkdirSync(OUT_DIR, { recursive: true });\n\n  const index: VersionIndex = { versions, diffs };\n  const indexPath = path.join(OUT_DIR, \"versions.json\");\n  fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));\n  console.log(`  Wrote ${indexPath}`);\n\n  const docsPath = path.join(OUT_DIR, \"docs.json\");\n  fs.writeFileSync(docsPath, JSON.stringify(docs, null, 2));\n  console.log(`  Wrote ${docsPath}`);\n\n  // Summary\n  console.log(\"\\nExtraction complete:\");\n  console.log(`  ${versions.length} versions`);\n  console.log(`  ${diffs.length} diffs`);\n  console.log(`  ${docs.length} docs`);\n  for (const v of versions) {\n    console.log(\n      `    ${v.id}: ${v.loc} LOC, ${v.tools.length} tools, ${v.classes.length} classes, ${v.functions.length} functions`\n    );\n  }\n}\n\nmain();\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/[version]/client.tsx",
    "content": "\"use client\";\n\nimport { ArchDiagram } from \"@/components/architecture/arch-diagram\";\nimport { WhatsNew } from \"@/components/diff/whats-new\";\nimport { DesignDecisions } from \"@/components/architecture/design-decisions\";\nimport { DocRenderer } from \"@/components/docs/doc-renderer\";\nimport { SourceViewer } from \"@/components/code/source-viewer\";\nimport { AgentLoopSimulator } from \"@/components/simulator/agent-loop-simulator\";\nimport { ExecutionFlow } from \"@/components/architecture/execution-flow\";\nimport { SessionVisualization } from \"@/components/visualizations\";\nimport { Tabs } from \"@/components/ui/tabs\";\nimport { useTranslations } from \"@/lib/i18n\";\n\ninterface VersionDetailClientProps {\n  version: string;\n  diff: {\n    from: string;\n    to: string;\n    newClasses: string[];\n    newFunctions: string[];\n    newTools: string[];\n    locDelta: number;\n  } | null;\n  source: string;\n  filename: string;\n}\n\nexport function VersionDetailClient({\n  version,\n  diff,\n  source,\n  filename,\n}: VersionDetailClientProps) {\n  const t = useTranslations(\"version\");\n\n  const tabs = [\n    { id: \"learn\", label: t(\"tab_learn\") },\n    { id: \"simulate\", label: t(\"tab_simulate\") },\n    { id: \"code\", label: t(\"tab_code\") },\n    { id: \"deep-dive\", label: t(\"tab_deep_dive\") },\n  ];\n\n  return (\n    <div className=\"space-y-6\">\n      {/* Hero Visualization */}\n      <SessionVisualization version={version} />\n\n      {/* Tabbed content */}\n      <Tabs tabs={tabs} defaultTab=\"learn\">\n        {(activeTab) => (\n          <>\n            {activeTab === \"learn\" && <DocRenderer version={version} />}\n            {activeTab === \"simulate\" && (\n              <AgentLoopSimulator version={version} />\n            )}\n            {activeTab === \"code\" && (\n              <SourceViewer source={source} filename={filename} />\n            )}\n            {activeTab === \"deep-dive\" && (\n              <div className=\"space-y-8\">\n                <section>\n                  <h2 className=\"mb-4 text-xl font-semibold\">\n                    {t(\"execution_flow\")}\n                  </h2>\n                  <ExecutionFlow version={version} />\n                </section>\n                <section>\n                  <h2 className=\"mb-4 text-xl font-semibold\">\n                    {t(\"architecture\")}\n                  </h2>\n                  <ArchDiagram version={version} />\n                </section>\n                {diff && <WhatsNew diff={diff} />}\n                <DesignDecisions version={version} />\n              </div>\n            )}\n          </>\n        )}\n      </Tabs>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/[version]/diff/diff-content.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport Link from \"next/link\";\nimport { useLocale } from \"@/lib/i18n\";\nimport { VERSION_META } from \"@/lib/constants\";\nimport { Card, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { LayerBadge } from \"@/components/ui/badge\";\nimport { CodeDiff } from \"@/components/diff/code-diff\";\nimport { ArrowLeft, Plus, Minus, FileCode, Wrench, Box, FunctionSquare } from \"lucide-react\";\nimport type { AgentVersion, VersionDiff, VersionIndex } from \"@/types/agent-data\";\nimport versionData from \"@/data/generated/versions.json\";\n\nconst data = versionData as VersionIndex;\n\ninterface DiffPageContentProps {\n  version: string;\n}\n\nexport function DiffPageContent({ version }: DiffPageContentProps) {\n  const locale = useLocale();\n  const meta = VERSION_META[version];\n\n  const { currentVersion, prevVersion, diff } = useMemo(() => {\n    const current = data.versions.find((v) => v.id === version);\n    const prevId = meta?.prevVersion;\n    const prev = prevId ? data.versions.find((v) => v.id === prevId) : null;\n    const d = data.diffs.find((d) => d.to === version);\n    return { currentVersion: current, prevVersion: prev, diff: d };\n  }, [version, meta]);\n\n  if (!meta || !currentVersion) {\n    return (\n      <div className=\"py-12 text-center\">\n        <p className=\"text-zinc-500\">Version not found.</p>\n        <Link href={`/${locale}/timeline`} className=\"mt-4 inline-block text-sm text-blue-600 hover:underline\">\n          Back to timeline\n        </Link>\n      </div>\n    );\n  }\n\n  if (!prevVersion || !diff) {\n    return (\n      <div className=\"py-12\">\n        <Link\n          href={`/${locale}/${version}`}\n          className=\"mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300\"\n        >\n          <ArrowLeft size={14} />\n          Back to {meta.title}\n        </Link>\n        <h1 className=\"text-3xl font-bold\">{meta.title}</h1>\n        <p className=\"mt-4 text-zinc-500\">\n          This is the first version -- there is no previous version to compare against.\n        </p>\n      </div>\n    );\n  }\n\n  const prevMeta = VERSION_META[prevVersion.id];\n\n  return (\n    <div className=\"py-4\">\n      <Link\n        href={`/${locale}/${version}`}\n        className=\"mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300\"\n      >\n        <ArrowLeft size={14} />\n        Back to {meta.title}\n      </Link>\n\n      {/* Header */}\n      <div className=\"mb-8\">\n        <h1 className=\"text-3xl font-bold\">\n          {prevMeta?.title || prevVersion.id} → {meta.title}\n        </h1>\n        <p className=\"mt-2 text-zinc-500 dark:text-zinc-400\">\n          {prevVersion.id} ({prevVersion.loc} LOC) → {version} ({currentVersion.loc} LOC)\n        </p>\n      </div>\n\n      {/* Structural Diff */}\n      <div className=\"mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n              <FileCode size={16} />\n              <span className=\"text-sm\">LOC Delta</span>\n            </div>\n          </CardHeader>\n          <CardTitle>\n            <span className={diff.locDelta >= 0 ? \"text-green-600 dark:text-green-400\" : \"text-red-600 dark:text-red-400\"}>\n              {diff.locDelta >= 0 ? \"+\" : \"\"}{diff.locDelta}\n            </span>\n            <span className=\"ml-2 text-sm font-normal text-zinc-500\">lines</span>\n          </CardTitle>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n              <Wrench size={16} />\n              <span className=\"text-sm\">New Tools</span>\n            </div>\n          </CardHeader>\n          <CardTitle>\n            <span className=\"text-blue-600 dark:text-blue-400\">{diff.newTools.length}</span>\n          </CardTitle>\n          {diff.newTools.length > 0 && (\n            <div className=\"mt-2 flex flex-wrap gap-1\">\n              {diff.newTools.map((tool) => (\n                <span key={tool} className=\"rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300\">\n                  {tool}\n                </span>\n              ))}\n            </div>\n          )}\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n              <Box size={16} />\n              <span className=\"text-sm\">New Classes</span>\n            </div>\n          </CardHeader>\n          <CardTitle>\n            <span className=\"text-purple-600 dark:text-purple-400\">{diff.newClasses.length}</span>\n          </CardTitle>\n          {diff.newClasses.length > 0 && (\n            <div className=\"mt-2 flex flex-wrap gap-1\">\n              {diff.newClasses.map((cls) => (\n                <span key={cls} className=\"rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300\">\n                  {cls}\n                </span>\n              ))}\n            </div>\n          )}\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n              <FunctionSquare size={16} />\n              <span className=\"text-sm\">New Functions</span>\n            </div>\n          </CardHeader>\n          <CardTitle>\n            <span className=\"text-amber-600 dark:text-amber-400\">{diff.newFunctions.length}</span>\n          </CardTitle>\n          {diff.newFunctions.length > 0 && (\n            <div className=\"mt-2 flex flex-wrap gap-1\">\n              {diff.newFunctions.map((fn) => (\n                <span key={fn} className=\"rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300\">\n                  {fn}\n                </span>\n              ))}\n            </div>\n          )}\n        </Card>\n      </div>\n\n      {/* Version Info Comparison */}\n      <div className=\"mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n        <Card className=\"border-l-4 border-l-red-300 dark:border-l-red-700\">\n          <CardHeader>\n            <CardTitle>{prevMeta?.title || prevVersion.id}</CardTitle>\n            <p className=\"text-sm text-zinc-500\">{prevMeta?.subtitle}</p>\n          </CardHeader>\n          <div className=\"space-y-1 text-sm text-zinc-600 dark:text-zinc-400\">\n            <p>{prevVersion.loc} LOC</p>\n            <p>{prevVersion.tools.length} tools: {prevVersion.tools.join(\", \")}</p>\n            <LayerBadge layer={prevVersion.layer}>{prevVersion.layer}</LayerBadge>\n          </div>\n        </Card>\n        <Card className=\"border-l-4 border-l-green-300 dark:border-l-green-700\">\n          <CardHeader>\n            <CardTitle>{meta.title}</CardTitle>\n            <p className=\"text-sm text-zinc-500\">{meta.subtitle}</p>\n          </CardHeader>\n          <div className=\"space-y-1 text-sm text-zinc-600 dark:text-zinc-400\">\n            <p>{currentVersion.loc} LOC</p>\n            <p>{currentVersion.tools.length} tools: {currentVersion.tools.join(\", \")}</p>\n            <LayerBadge layer={currentVersion.layer}>{currentVersion.layer}</LayerBadge>\n          </div>\n        </Card>\n      </div>\n\n      {/* Code Diff */}\n      <div>\n        <h2 className=\"mb-4 text-xl font-semibold\">Source Code Diff</h2>\n        <CodeDiff\n          oldSource={prevVersion.source}\n          newSource={currentVersion.source}\n          oldLabel={`${prevVersion.id} (${prevVersion.filename})`}\n          newLabel={`${version} (${currentVersion.filename})`}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/[version]/diff/page.tsx",
    "content": "import { LEARNING_PATH } from \"@/lib/constants\";\nimport { DiffPageContent } from \"./diff-content\";\n\nexport function generateStaticParams() {\n  return LEARNING_PATH.map((version) => ({ version }));\n}\n\nexport default async function DiffPage({\n  params,\n}: {\n  params: Promise<{ locale: string; version: string }>;\n}) {\n  const { version } = await params;\n  return <DiffPageContent version={version} />;\n}\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/[version]/page.tsx",
    "content": "import Link from \"next/link\";\nimport { LEARNING_PATH, VERSION_META, LAYERS } from \"@/lib/constants\";\nimport { LayerBadge } from \"@/components/ui/badge\";\nimport versionsData from \"@/data/generated/versions.json\";\nimport { VersionDetailClient } from \"./client\";\nimport { getTranslations } from \"@/lib/i18n-server\";\n\nexport function generateStaticParams() {\n  return LEARNING_PATH.map((version) => ({ version }));\n}\n\nexport default async function VersionPage({\n  params,\n}: {\n  params: Promise<{ locale: string; version: string }>;\n}) {\n  const { locale, version } = await params;\n\n  const versionData = versionsData.versions.find((v) => v.id === version);\n  const meta = VERSION_META[version];\n  const diff = versionsData.diffs.find((d) => d.to === version) ?? null;\n\n  if (!versionData || !meta) {\n    return (\n      <div className=\"py-20 text-center\">\n        <h1 className=\"text-2xl font-bold\">Version not found</h1>\n        <p className=\"mt-2 text-zinc-500\">{version}</p>\n      </div>\n    );\n  }\n\n  const t = getTranslations(locale, \"version\");\n  const tSession = getTranslations(locale, \"sessions\");\n  const tLayer = getTranslations(locale, \"layer_labels\");\n  const layer = LAYERS.find((l) => l.id === meta.layer);\n\n  const pathIndex = LEARNING_PATH.indexOf(version as typeof LEARNING_PATH[number]);\n  const prevVersion = pathIndex > 0 ? LEARNING_PATH[pathIndex - 1] : null;\n  const nextVersion =\n    pathIndex < LEARNING_PATH.length - 1\n      ? LEARNING_PATH[pathIndex + 1]\n      : null;\n\n  return (\n    <div className=\"mx-auto max-w-3xl space-y-10 py-4\">\n      {/* Header */}\n      <header className=\"space-y-3\">\n        <div className=\"flex flex-wrap items-center gap-3\">\n          <span className=\"rounded-lg bg-zinc-100 px-3 py-1 font-mono text-lg font-bold dark:bg-zinc-800\">\n            {version}\n          </span>\n          <h1 className=\"text-2xl font-bold sm:text-3xl\">{tSession(version) || meta.title}</h1>\n          {layer && (\n            <LayerBadge layer={meta.layer}>{tLayer(layer.id)}</LayerBadge>\n          )}\n        </div>\n        <p className=\"text-lg text-zinc-500 dark:text-zinc-400\">\n          {meta.subtitle}\n        </p>\n        <div className=\"flex flex-wrap items-center gap-4 text-sm text-zinc-500 dark:text-zinc-400\">\n          <span className=\"font-mono\">{versionData.loc} LOC</span>\n          <span>{versionData.tools.length} {t(\"tools\")}</span>\n          {meta.coreAddition && (\n            <span className=\"rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs dark:bg-zinc-800\">\n              {meta.coreAddition}\n            </span>\n          )}\n        </div>\n        {meta.keyInsight && (\n          <blockquote className=\"border-l-4 border-zinc-300 pl-4 text-sm italic text-zinc-500 dark:border-zinc-600 dark:text-zinc-400\">\n            {meta.keyInsight}\n          </blockquote>\n        )}\n      </header>\n\n      {/* Client-rendered interactive sections */}\n      <VersionDetailClient\n        version={version}\n        diff={diff}\n        source={versionData.source}\n        filename={versionData.filename}\n      />\n\n      {/* Prev / Next navigation */}\n      <nav className=\"flex items-center justify-between border-t border-zinc-200 pt-6 dark:border-zinc-700\">\n        {prevVersion ? (\n          <Link\n            href={`/${locale}/${prevVersion}`}\n            className=\"group flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white\"\n          >\n            <span className=\"transition-transform group-hover:-translate-x-1\">\n              &larr;\n            </span>\n            <div>\n              <div className=\"text-xs text-zinc-400\">{t(\"prev\")}</div>\n              <div className=\"font-medium\">\n                {prevVersion} - {tSession(prevVersion) || VERSION_META[prevVersion]?.title}\n              </div>\n            </div>\n          </Link>\n        ) : (\n          <div />\n        )}\n        {nextVersion ? (\n          <Link\n            href={`/${locale}/${nextVersion}`}\n            className=\"group flex items-center gap-2 text-right text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white\"\n          >\n            <div>\n              <div className=\"text-xs text-zinc-400\">{t(\"next\")}</div>\n              <div className=\"font-medium\">\n                {tSession(nextVersion) || VERSION_META[nextVersion]?.title} - {nextVersion}\n              </div>\n            </div>\n            <span className=\"transition-transform group-hover:translate-x-1\">\n              &rarr;\n            </span>\n          </Link>\n        ) : (\n          <div />\n        )}\n      </nav>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/compare/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { useLocale, useTranslations } from \"@/lib/i18n\";\nimport { LEARNING_PATH, VERSION_META } from \"@/lib/constants\";\nimport { Card, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { LayerBadge } from \"@/components/ui/badge\";\nimport { CodeDiff } from \"@/components/diff/code-diff\";\nimport { ArchDiagram } from \"@/components/architecture/arch-diagram\";\nimport { ArrowRight, FileCode, Wrench, Box, FunctionSquare } from \"lucide-react\";\nimport type { VersionIndex } from \"@/types/agent-data\";\nimport versionData from \"@/data/generated/versions.json\";\n\nconst data = versionData as VersionIndex;\n\nexport default function ComparePage() {\n  const t = useTranslations(\"compare\");\n  const locale = useLocale();\n  const [versionA, setVersionA] = useState<string>(\"\");\n  const [versionB, setVersionB] = useState<string>(\"\");\n\n  const infoA = useMemo(() => data.versions.find((v) => v.id === versionA), [versionA]);\n  const infoB = useMemo(() => data.versions.find((v) => v.id === versionB), [versionB]);\n  const metaA = versionA ? VERSION_META[versionA] : null;\n  const metaB = versionB ? VERSION_META[versionB] : null;\n\n  const comparison = useMemo(() => {\n    if (!infoA || !infoB) return null;\n    const toolsA = new Set(infoA.tools);\n    const toolsB = new Set(infoB.tools);\n    const onlyA = infoA.tools.filter((t) => !toolsB.has(t));\n    const onlyB = infoB.tools.filter((t) => !toolsA.has(t));\n    const shared = infoA.tools.filter((t) => toolsB.has(t));\n\n    const classesA = new Set(infoA.classes.map((c) => c.name));\n    const classesB = new Set(infoB.classes.map((c) => c.name));\n    const newClasses = infoB.classes.map((c) => c.name).filter((c) => !classesA.has(c));\n\n    const funcsA = new Set(infoA.functions.map((f) => f.name));\n    const funcsB = new Set(infoB.functions.map((f) => f.name));\n    const newFunctions = infoB.functions.map((f) => f.name).filter((f) => !funcsA.has(f));\n\n    return {\n      locDelta: infoB.loc - infoA.loc,\n      toolsOnlyA: onlyA,\n      toolsOnlyB: onlyB,\n      toolsShared: shared,\n      newClasses,\n      newFunctions,\n    };\n  }, [infoA, infoB]);\n\n  return (\n    <div className=\"py-4\">\n      <div className=\"mb-8\">\n        <h1 className=\"text-3xl font-bold\">{t(\"title\")}</h1>\n        <p className=\"mt-2 text-zinc-500 dark:text-zinc-400\">{t(\"subtitle\")}</p>\n      </div>\n\n      {/* Selectors */}\n      <div className=\"mb-8 flex flex-col items-start gap-4 sm:flex-row sm:items-center\">\n        <div className=\"flex-1\">\n          <label className=\"mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400\">\n            {t(\"select_a\")}\n          </label>\n          <select\n            value={versionA}\n            onChange={(e) => setVersionA(e.target.value)}\n            className=\"w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200\"\n          >\n            <option value=\"\">-- select --</option>\n            {LEARNING_PATH.map((v) => (\n              <option key={v} value={v}>\n                {v} - {VERSION_META[v]?.title}\n              </option>\n            ))}\n          </select>\n        </div>\n\n        <ArrowRight size={20} className=\"mt-5 hidden text-zinc-400 sm:block\" />\n\n        <div className=\"flex-1\">\n          <label className=\"mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400\">\n            {t(\"select_b\")}\n          </label>\n          <select\n            value={versionB}\n            onChange={(e) => setVersionB(e.target.value)}\n            className=\"w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200\"\n          >\n            <option value=\"\">-- select --</option>\n            {LEARNING_PATH.map((v) => (\n              <option key={v} value={v}>\n                {v} - {VERSION_META[v]?.title}\n              </option>\n            ))}\n          </select>\n        </div>\n      </div>\n\n      {/* Results */}\n      {infoA && infoB && comparison && (\n        <div className=\"space-y-8\">\n          {/* Side-by-side version info */}\n          <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n            <Card>\n              <CardHeader>\n                <CardTitle>{metaA?.title || versionA}</CardTitle>\n                <p className=\"text-sm text-zinc-500\">{metaA?.subtitle}</p>\n              </CardHeader>\n              <div className=\"space-y-2 text-sm text-zinc-600 dark:text-zinc-400\">\n                <p>{infoA.loc} LOC</p>\n                <p>{infoA.tools.length} tools</p>\n                {metaA && <LayerBadge layer={metaA.layer}>{metaA.layer}</LayerBadge>}\n              </div>\n            </Card>\n            <Card>\n              <CardHeader>\n                <CardTitle>{metaB?.title || versionB}</CardTitle>\n                <p className=\"text-sm text-zinc-500\">{metaB?.subtitle}</p>\n              </CardHeader>\n              <div className=\"space-y-2 text-sm text-zinc-600 dark:text-zinc-400\">\n                <p>{infoB.loc} LOC</p>\n                <p>{infoB.tools.length} tools</p>\n                {metaB && <LayerBadge layer={metaB.layer}>{metaB.layer}</LayerBadge>}\n              </div>\n            </Card>\n          </div>\n\n          {/* Side-by-side Architecture Diagrams */}\n          <div>\n            <h2 className=\"mb-4 text-xl font-semibold\">{t(\"architecture\")}</h2>\n            <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n              <div>\n                <h3 className=\"mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400\">\n                  {metaA?.title || versionA}\n                </h3>\n                <ArchDiagram version={versionA} />\n              </div>\n              <div>\n                <h3 className=\"mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400\">\n                  {metaB?.title || versionB}\n                </h3>\n                <ArchDiagram version={versionB} />\n              </div>\n            </div>\n          </div>\n\n          {/* Structural diff */}\n          <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n            <Card>\n              <CardHeader>\n                <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n                  <FileCode size={16} />\n                  <span className=\"text-sm\">{t(\"loc_delta\")}</span>\n                </div>\n              </CardHeader>\n              <CardTitle>\n                <span className={comparison.locDelta >= 0 ? \"text-green-600 dark:text-green-400\" : \"text-red-600 dark:text-red-400\"}>\n                  {comparison.locDelta >= 0 ? \"+\" : \"\"}{comparison.locDelta}\n                </span>\n                <span className=\"ml-2 text-sm font-normal text-zinc-500\">{t(\"lines\")}</span>\n              </CardTitle>\n            </Card>\n\n            <Card>\n              <CardHeader>\n                <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n                  <Wrench size={16} />\n                  <span className=\"text-sm\">{t(\"new_tools_in_b\")}</span>\n                </div>\n              </CardHeader>\n              <CardTitle>\n                <span className=\"text-blue-600 dark:text-blue-400\">{comparison.toolsOnlyB.length}</span>\n              </CardTitle>\n              {comparison.toolsOnlyB.length > 0 && (\n                <div className=\"mt-2 flex flex-wrap gap-1\">\n                  {comparison.toolsOnlyB.map((tool) => (\n                    <span key={tool} className=\"rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300\">\n                      {tool}\n                    </span>\n                  ))}\n                </div>\n              )}\n            </Card>\n\n            <Card>\n              <CardHeader>\n                <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n                  <Box size={16} />\n                  <span className=\"text-sm\">{t(\"new_classes_in_b\")}</span>\n                </div>\n              </CardHeader>\n              <CardTitle>\n                <span className=\"text-purple-600 dark:text-purple-400\">{comparison.newClasses.length}</span>\n              </CardTitle>\n              {comparison.newClasses.length > 0 && (\n                <div className=\"mt-2 flex flex-wrap gap-1\">\n                  {comparison.newClasses.map((cls) => (\n                    <span key={cls} className=\"rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300\">\n                      {cls}\n                    </span>\n                  ))}\n                </div>\n              )}\n            </Card>\n\n            <Card>\n              <CardHeader>\n                <div className=\"flex items-center gap-2 text-zinc-500 dark:text-zinc-400\">\n                  <FunctionSquare size={16} />\n                  <span className=\"text-sm\">{t(\"new_functions_in_b\")}</span>\n                </div>\n              </CardHeader>\n              <CardTitle>\n                <span className=\"text-amber-600 dark:text-amber-400\">{comparison.newFunctions.length}</span>\n              </CardTitle>\n              {comparison.newFunctions.length > 0 && (\n                <div className=\"mt-2 flex flex-wrap gap-1\">\n                  {comparison.newFunctions.map((fn) => (\n                    <span key={fn} className=\"rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300\">\n                      {fn}\n                    </span>\n                  ))}\n                </div>\n              )}\n            </Card>\n          </div>\n\n          {/* Tool comparison */}\n          <Card>\n            <CardHeader>\n              <CardTitle>{t(\"tool_comparison\")}</CardTitle>\n            </CardHeader>\n            <div className=\"grid grid-cols-1 gap-6 sm:grid-cols-3\">\n              <div>\n                <h4 className=\"mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400\">\n                  {t(\"only_in\")} {metaA?.title || versionA}\n                </h4>\n                {comparison.toolsOnlyA.length === 0 ? (\n                  <p className=\"text-xs text-zinc-400\">{t(\"none\")}</p>\n                ) : (\n                  <div className=\"flex flex-wrap gap-1\">\n                    {comparison.toolsOnlyA.map((tool) => (\n                      <span key={tool} className=\"rounded bg-red-100 px-1.5 py-0.5 text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300\">\n                        {tool}\n                      </span>\n                    ))}\n                  </div>\n                )}\n              </div>\n              <div>\n                <h4 className=\"mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400\">\n                  {t(\"shared\")}\n                </h4>\n                {comparison.toolsShared.length === 0 ? (\n                  <p className=\"text-xs text-zinc-400\">{t(\"none\")}</p>\n                ) : (\n                  <div className=\"flex flex-wrap gap-1\">\n                    {comparison.toolsShared.map((tool) => (\n                      <span key={tool} className=\"rounded bg-zinc-100 px-1.5 py-0.5 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300\">\n                        {tool}\n                      </span>\n                    ))}\n                  </div>\n                )}\n              </div>\n              <div>\n                <h4 className=\"mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400\">\n                  {t(\"only_in\")} {metaB?.title || versionB}\n                </h4>\n                {comparison.toolsOnlyB.length === 0 ? (\n                  <p className=\"text-xs text-zinc-400\">{t(\"none\")}</p>\n                ) : (\n                  <div className=\"flex flex-wrap gap-1\">\n                    {comparison.toolsOnlyB.map((tool) => (\n                      <span key={tool} className=\"rounded bg-green-100 px-1.5 py-0.5 text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300\">\n                        {tool}\n                      </span>\n                    ))}\n                  </div>\n                )}\n              </div>\n            </div>\n          </Card>\n\n          {/* Code Diff */}\n          <div>\n            <h2 className=\"mb-4 text-xl font-semibold\">{t(\"source_diff\")}</h2>\n            <CodeDiff\n              oldSource={infoA.source}\n              newSource={infoB.source}\n              oldLabel={`${infoA.id} (${infoA.filename})`}\n              newLabel={`${infoB.id} (${infoB.filename})`}\n            />\n          </div>\n        </div>\n      )}\n\n      {/* Empty state */}\n      {(!versionA || !versionB) && (\n        <div className=\"rounded-lg border border-dashed border-zinc-300 p-12 text-center dark:border-zinc-700\">\n          <p className=\"text-zinc-400\">{t(\"empty_hint\")}</p>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/layers/page.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useTranslations, useLocale } from \"@/lib/i18n\";\nimport { LAYERS, VERSION_META } from \"@/lib/constants\";\nimport { Card, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { LayerBadge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\nimport type { VersionIndex } from \"@/types/agent-data\";\nimport versionData from \"@/data/generated/versions.json\";\n\nconst data = versionData as VersionIndex;\n\nconst LAYER_BORDER_CLASSES: Record<string, string> = {\n  tools: \"border-l-blue-500\",\n  planning: \"border-l-emerald-500\",\n  memory: \"border-l-purple-500\",\n  concurrency: \"border-l-amber-500\",\n  collaboration: \"border-l-red-500\",\n};\n\nconst LAYER_HEADER_BG: Record<string, string> = {\n  tools: \"bg-blue-500\",\n  planning: \"bg-emerald-500\",\n  memory: \"bg-purple-500\",\n  concurrency: \"bg-amber-500\",\n  collaboration: \"bg-red-500\",\n};\n\nexport default function LayersPage() {\n  const t = useTranslations(\"layers\");\n  const locale = useLocale();\n\n  return (\n    <div className=\"py-4\">\n      <div className=\"mb-10\">\n        <h1 className=\"text-3xl font-bold\">{t(\"title\")}</h1>\n        <p className=\"mt-2 text-zinc-500 dark:text-zinc-400\">{t(\"subtitle\")}</p>\n      </div>\n\n      <div className=\"space-y-6\">\n        {LAYERS.map((layer, index) => {\n          const versionInfos = layer.versions.map((vId) => {\n            const info = data.versions.find((v) => v.id === vId);\n            const meta = VERSION_META[vId];\n            return { id: vId, info, meta };\n          });\n\n          return (\n            <div\n              key={layer.id}\n              className={cn(\n                \"overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800\",\n                \"border-l-4\",\n                LAYER_BORDER_CLASSES[layer.id]\n              )}\n            >\n              {/* Layer header */}\n              <div className=\"flex items-center gap-3 px-6 py-4\">\n                <div className={cn(\"h-3 w-3 rounded-full\", LAYER_HEADER_BG[layer.id])} />\n                <div>\n                  <h2 className=\"text-xl font-bold\">\n                    <span className=\"text-zinc-400 dark:text-zinc-600\">L{index + 1}</span>\n                    {\" \"}\n                    {layer.label}\n                  </h2>\n                  <p className=\"mt-1 text-sm text-zinc-500 dark:text-zinc-400\">\n                    {t(layer.id)}\n                  </p>\n                </div>\n              </div>\n\n              {/* Version cards within this layer */}\n              <div className=\"border-t border-zinc-200 bg-zinc-50/50 px-6 py-4 dark:border-zinc-800 dark:bg-zinc-900/50\">\n                <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n                  {versionInfos.map(({ id, info, meta }) => (\n                    <Link\n                      key={id}\n                      href={`/${locale}/${id}`}\n                      className=\"group\"\n                    >\n                      <Card className=\"transition-shadow hover:shadow-md\">\n                        <div className=\"flex items-start justify-between\">\n                          <div className=\"min-w-0 flex-1\">\n                            <div className=\"flex items-center gap-2\">\n                              <span className=\"text-xs font-mono text-zinc-400\">{id}</span>\n                              <LayerBadge layer={layer.id}>{layer.id}</LayerBadge>\n                            </div>\n                            <h3 className=\"mt-1 font-semibold text-zinc-900 dark:text-zinc-100\">\n                              {meta?.title || id}\n                            </h3>\n                            {meta?.subtitle && (\n                              <p className=\"mt-0.5 text-xs text-zinc-500 dark:text-zinc-400\">\n                                {meta.subtitle}\n                              </p>\n                            )}\n                          </div>\n                          <ChevronRight\n                            size={16}\n                            className=\"mt-1 shrink-0 text-zinc-300 transition-colors group-hover:text-zinc-600 dark:text-zinc-600 dark:group-hover:text-zinc-300\"\n                          />\n                        </div>\n                        <div className=\"mt-3 flex items-center gap-4 text-xs text-zinc-500 dark:text-zinc-400\">\n                          <span>{info?.loc ?? \"?\"} LOC</span>\n                          <span>{info?.tools.length ?? \"?\"} tools</span>\n                        </div>\n                        {meta?.keyInsight && (\n                          <p className=\"mt-2 text-xs leading-relaxed text-zinc-500 dark:text-zinc-400 line-clamp-2\">\n                            {meta.keyInsight}\n                          </p>\n                        )}\n                      </Card>\n                    </Link>\n                  ))}\n                </div>\n              </div>\n\n              {/* Composition indicator */}\n              {index < LAYERS.length - 1 && (\n                <div className=\"flex items-center justify-center py-1 text-zinc-300 dark:text-zinc-700\">\n                  <svg width=\"20\" height=\"12\" viewBox=\"0 0 20 12\" fill=\"none\" className=\"text-current\">\n                    <path d=\"M10 0 L10 12 M5 7 L10 12 L15 7\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n                  </svg>\n                </div>\n              )}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/layout.tsx",
    "content": "import { Sidebar } from \"@/components/layout/sidebar\";\n\nexport default function LearnLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"flex gap-8\">\n      <Sidebar />\n      <div className=\"min-w-0 flex-1\">{children}</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/(learn)/timeline/page.tsx",
    "content": "\"use client\";\n\nimport { useTranslations } from \"@/lib/i18n\";\nimport { Timeline } from \"@/components/timeline/timeline\";\n\nexport default function TimelinePage() {\n  const t = useTranslations(\"timeline\");\n\n  return (\n    <div>\n      <div className=\"mb-8\">\n        <h1 className=\"text-3xl font-bold\">{t(\"title\")}</h1>\n        <p className=\"mt-2 text-[var(--color-text-secondary)]\">\n          {t(\"subtitle\")}\n        </p>\n      </div>\n      <Timeline />\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { I18nProvider } from \"@/lib/i18n\";\nimport { Header } from \"@/components/layout/header\";\nimport en from \"@/i18n/messages/en.json\";\nimport zh from \"@/i18n/messages/zh.json\";\nimport ja from \"@/i18n/messages/ja.json\";\nimport \"../globals.css\";\n\nconst locales = [\"en\", \"zh\", \"ja\"];\nconst metaMessages: Record<string, typeof en> = { en, zh, ja };\n\nexport function generateStaticParams() {\n  return locales.map((locale) => ({ locale }));\n}\n\nexport async function generateMetadata({\n  params,\n}: {\n  params: Promise<{ locale: string }>;\n}): Promise<Metadata> {\n  const { locale } = await params;\n  const messages = metaMessages[locale] || metaMessages.en;\n  return {\n    title: messages.meta?.title || \"Learn Claude Code\",\n    description: messages.meta?.description || \"Build an AI coding agent from scratch, one concept at a time\",\n  };\n}\n\nexport default async function RootLayout({\n  children,\n  params,\n}: {\n  children: React.ReactNode;\n  params: Promise<{ locale: string }>;\n}) {\n  const { locale } = await params;\n\n  return (\n    <html lang={locale} suppressHydrationWarning>\n      <head>\n        <script dangerouslySetInnerHTML={{ __html: `\n          (function() {\n            var theme = localStorage.getItem('theme');\n            if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {\n              document.documentElement.classList.add('dark');\n            }\n          })();\n        `}} />\n      </head>\n      <body className=\"min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] antialiased\">\n        <I18nProvider locale={locale}>\n          <Header />\n          <main className=\"mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8\">\n            {children}\n          </main>\n        </I18nProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "web/src/app/[locale]/page.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useTranslations, useLocale } from \"@/lib/i18n\";\nimport { LEARNING_PATH, VERSION_META, LAYERS } from \"@/lib/constants\";\nimport { LayerBadge } from \"@/components/ui/badge\";\nimport { Card } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport versionsData from \"@/data/generated/versions.json\";\nimport { MessageFlow } from \"@/components/architecture/message-flow\";\n\nconst LAYER_DOT_COLORS: Record<string, string> = {\n  tools: \"bg-blue-500\",\n  planning: \"bg-emerald-500\",\n  memory: \"bg-purple-500\",\n  concurrency: \"bg-amber-500\",\n  collaboration: \"bg-red-500\",\n};\n\nconst LAYER_BORDER_COLORS: Record<string, string> = {\n  tools: \"border-blue-500/30 hover:border-blue-500/60\",\n  planning: \"border-emerald-500/30 hover:border-emerald-500/60\",\n  memory: \"border-purple-500/30 hover:border-purple-500/60\",\n  concurrency: \"border-amber-500/30 hover:border-amber-500/60\",\n  collaboration: \"border-red-500/30 hover:border-red-500/60\",\n};\n\nconst LAYER_BAR_COLORS: Record<string, string> = {\n  tools: \"bg-blue-500\",\n  planning: \"bg-emerald-500\",\n  memory: \"bg-purple-500\",\n  concurrency: \"bg-amber-500\",\n  collaboration: \"bg-red-500\",\n};\n\nfunction getVersionData(id: string) {\n  return versionsData.versions.find((v) => v.id === id);\n}\n\nexport default function HomePage() {\n  const t = useTranslations(\"home\");\n  const locale = useLocale();\n\n  return (\n    <div className=\"flex flex-col gap-20 pb-16\">\n      {/* Hero Section */}\n      <section className=\"flex flex-col items-center px-2 pt-8 text-center sm:pt-20\">\n        <h1 className=\"text-3xl font-bold tracking-tight sm:text-5xl lg:text-6xl\">\n          {t(\"hero_title\")}\n        </h1>\n        <p className=\"mt-4 max-w-2xl text-base text-[var(--color-text-secondary)] sm:text-xl\">\n          {t(\"hero_subtitle\")}\n        </p>\n        <div className=\"mt-8\">\n          <Link\n            href={`/${locale}/timeline`}\n            className=\"inline-flex min-h-[44px] items-center gap-2 rounded-lg bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200\"\n          >\n            {t(\"start\")}\n            <span aria-hidden=\"true\">&rarr;</span>\n          </Link>\n        </div>\n      </section>\n\n      {/* Core Pattern Section */}\n      <section>\n        <div className=\"mb-6 text-center\">\n          <h2 className=\"text-2xl font-bold sm:text-3xl\">{t(\"core_pattern\")}</h2>\n          <p className=\"mt-2 text-[var(--color-text-secondary)]\">\n            {t(\"core_pattern_desc\")}\n          </p>\n        </div>\n        <div className=\"mx-auto max-w-2xl overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950\">\n          <div className=\"flex items-center gap-2 border-b border-zinc-800 px-4 py-2.5\">\n            <span className=\"h-3 w-3 rounded-full bg-red-500/70\" />\n            <span className=\"h-3 w-3 rounded-full bg-yellow-500/70\" />\n            <span className=\"h-3 w-3 rounded-full bg-green-500/70\" />\n            <span className=\"ml-3 text-xs text-zinc-500\">agent_loop.py</span>\n          </div>\n          <pre className=\"overflow-x-auto p-4 text-sm leading-relaxed\">\n            <code>\n              <span className=\"text-purple-400\">while</span>\n              <span className=\"text-zinc-300\"> </span>\n              <span className=\"text-orange-300\">True</span>\n              <span className=\"text-zinc-500\">:</span>\n              {\"\\n\"}\n              <span className=\"text-zinc-300\">{\"    \"}response = client.messages.</span>\n              <span className=\"text-blue-400\">create</span>\n              <span className=\"text-zinc-500\">(</span>\n              <span className=\"text-zinc-300\">messages=</span>\n              <span className=\"text-zinc-300\">messages</span>\n              <span className=\"text-zinc-500\">,</span>\n              <span className=\"text-zinc-300\"> tools=</span>\n              <span className=\"text-zinc-300\">tools</span>\n              <span className=\"text-zinc-500\">)</span>\n              {\"\\n\"}\n              <span className=\"text-purple-400\">{\"    \"}if</span>\n              <span className=\"text-zinc-300\"> response.stop_reason != </span>\n              <span className=\"text-green-400\">&quot;tool_use&quot;</span>\n              <span className=\"text-zinc-500\">:</span>\n              {\"\\n\"}\n              <span className=\"text-purple-400\">{\"        \"}break</span>\n              {\"\\n\"}\n              <span className=\"text-purple-400\">{\"    \"}for</span>\n              <span className=\"text-zinc-300\"> tool_call </span>\n              <span className=\"text-purple-400\">in</span>\n              <span className=\"text-zinc-300\"> response.content</span>\n              <span className=\"text-zinc-500\">:</span>\n              {\"\\n\"}\n              <span className=\"text-zinc-300\">{\"        \"}result = </span>\n              <span className=\"text-blue-400\">execute_tool</span>\n              <span className=\"text-zinc-500\">(</span>\n              <span className=\"text-zinc-300\">tool_call.name</span>\n              <span className=\"text-zinc-500\">,</span>\n              <span className=\"text-zinc-300\"> tool_call.input</span>\n              <span className=\"text-zinc-500\">)</span>\n              {\"\\n\"}\n              <span className=\"text-zinc-300\">{\"        \"}messages.</span>\n              <span className=\"text-blue-400\">append</span>\n              <span className=\"text-zinc-500\">(</span>\n              <span className=\"text-zinc-300\">result</span>\n              <span className=\"text-zinc-500\">)</span>\n            </code>\n          </pre>\n        </div>\n      </section>\n\n      {/* Message Flow Visualization */}\n      <section>\n        <div className=\"mb-6 text-center\">\n          <h2 className=\"text-2xl font-bold sm:text-3xl\">{t(\"message_flow\")}</h2>\n          <p className=\"mt-2 text-[var(--color-text-secondary)]\">\n            {t(\"message_flow_desc\")}\n          </p>\n        </div>\n        <div className=\"mx-auto max-w-2xl\">\n          <MessageFlow />\n        </div>\n      </section>\n\n      {/* Learning Path Preview */}\n      <section>\n        <div className=\"mb-6 text-center\">\n          <h2 className=\"text-2xl font-bold sm:text-3xl\">{t(\"learning_path\")}</h2>\n          <p className=\"mt-2 text-[var(--color-text-secondary)]\">\n            {t(\"learning_path_desc\")}\n          </p>\n        </div>\n        <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n          {LEARNING_PATH.map((versionId) => {\n            const meta = VERSION_META[versionId];\n            const data = getVersionData(versionId);\n            if (!meta || !data) return null;\n            return (\n              <Link\n                key={versionId}\n                href={`/${locale}/${versionId}`}\n                className=\"group block\"\n              >\n                <Card\n                  className={cn(\n                    \"h-full border transition-all duration-200\",\n                    LAYER_BORDER_COLORS[meta.layer]\n                  )}\n                >\n                  <div className=\"flex items-start justify-between gap-2\">\n                    <LayerBadge layer={meta.layer}>{versionId}</LayerBadge>\n                    <span className=\"text-xs tabular-nums text-[var(--color-text-secondary)]\">\n                      {data.loc} {t(\"loc\")}\n                    </span>\n                  </div>\n                  <h3 className=\"mt-3 text-sm font-semibold group-hover:underline\">\n                    {meta.title}\n                  </h3>\n                  <p className=\"mt-1 text-xs text-[var(--color-text-secondary)]\">\n                    {meta.keyInsight}\n                  </p>\n                </Card>\n              </Link>\n            );\n          })}\n        </div>\n      </section>\n\n      {/* Layer Overview */}\n      <section>\n        <div className=\"mb-6 text-center\">\n          <h2 className=\"text-2xl font-bold sm:text-3xl\">{t(\"layers_title\")}</h2>\n          <p className=\"mt-2 text-[var(--color-text-secondary)]\">\n            {t(\"layers_desc\")}\n          </p>\n        </div>\n        <div className=\"flex flex-col gap-3\">\n          {LAYERS.map((layer) => (\n            <div\n              key={layer.id}\n              className=\"flex items-center gap-4 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4\"\n            >\n              <div\n                className={cn(\n                  \"h-full w-1.5 self-stretch rounded-full\",\n                  LAYER_BAR_COLORS[layer.id]\n                )}\n              />\n              <div className=\"flex-1\">\n                <div className=\"flex items-center gap-2\">\n                  <h3 className=\"text-sm font-semibold\">{layer.label}</h3>\n                  <span className=\"text-xs text-[var(--color-text-secondary)]\">\n                    {layer.versions.length} {t(\"versions_in_layer\")}\n                  </span>\n                </div>\n                <div className=\"mt-2 flex flex-wrap gap-1.5\">\n                  {layer.versions.map((vid) => {\n                    const meta = VERSION_META[vid];\n                    return (\n                      <Link key={vid} href={`/${locale}/${vid}`}>\n                        <LayerBadge\n                          layer={layer.id}\n                          className=\"cursor-pointer transition-opacity hover:opacity-80\"\n                        >\n                          {vid}: {meta?.title}\n                        </LayerBadge>\n                      </Link>\n                    );\n                  })}\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n      </section>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/app/globals.css",
    "content": "@import \"tailwindcss\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n:root {\n  --color-layer-tools: #3B82F6;\n  --color-layer-planning: #10B981;\n  --color-layer-memory: #8B5CF6;\n  --color-layer-concurrency: #F59E0B;\n  --color-layer-collaboration: #EF4444;\n  --color-bg: #ffffff;\n  --color-bg-secondary: #f4f4f5;\n  --color-text: #09090b;\n  --color-text-secondary: #71717a;\n  --color-border: #e4e4e7;\n}\n\n.dark {\n  --color-bg: #09090b;\n  --color-bg-secondary: #18181b;\n  --color-text: #fafafa;\n  --color-text-secondary: #a1a1aa;\n  --color-border: #27272a;\n}\n\nbody {\n  background: var(--color-bg);\n  color: var(--color-text);\n}\n\n@media (max-width: 640px) {\n  pre, code {\n    font-size: 11px;\n  }\n}\n\n* {\n  -webkit-tap-highlight-color: transparent;\n}\n\n/* =====================================================\n   PROSE-CUSTOM: Premium documentation rendering\n   ===================================================== */\n\n/* -- Headings -- */\n\n.prose-custom h1 {\n  margin-top: 2.5rem;\n  margin-bottom: 1rem;\n  font-size: 1.5rem;\n  line-height: 2rem;\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  color: #09090b;\n}\n\n.dark .prose-custom h1 {\n  color: #fafafa;\n}\n\n.prose-custom h2 {\n  margin-top: 2.5rem;\n  margin-bottom: 1rem;\n  padding-bottom: 0.5rem;\n  font-size: 1.25rem;\n  line-height: 1.75rem;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n  color: #09090b;\n  border-bottom: 1px solid #e4e4e7;\n}\n\n.dark .prose-custom h2 {\n  color: #fafafa;\n  border-bottom-color: #27272a;\n}\n\n.prose-custom h3 {\n  margin-top: 2rem;\n  margin-bottom: 0.75rem;\n  font-size: 1.0625rem;\n  line-height: 1.5rem;\n  font-weight: 600;\n  color: #18181b;\n}\n\n.dark .prose-custom h3 {\n  color: #e4e4e7;\n}\n\n.prose-custom h4 {\n  margin-top: 1.5rem;\n  margin-bottom: 0.5rem;\n  font-size: 0.9375rem;\n  line-height: 1.5rem;\n  font-weight: 600;\n  color: #27272a;\n}\n\n.dark .prose-custom h4 {\n  color: #d4d4d8;\n}\n\n/* -- Paragraphs -- */\n\n.prose-custom p {\n  margin-top: 0.75rem;\n  margin-bottom: 0.75rem;\n  font-size: 0.9rem;\n  line-height: 1.7;\n  color: #3f3f46;\n}\n\n.dark .prose-custom p {\n  color: #d4d4d8;\n}\n\n/* -- Hero callout (first blockquote) -- */\n\n.prose-custom blockquote.hero-callout {\n  position: relative;\n  margin-top: 0;\n  margin-bottom: 1.5rem;\n  border-left: none;\n  border-radius: 0.75rem;\n  background: linear-gradient(135deg, #eff6ff 0%, #f0fdf4 100%);\n  padding: 1.25rem 1.5rem 1.25rem 1.75rem;\n  font-style: normal;\n  overflow: hidden;\n}\n\n.prose-custom blockquote.hero-callout::before {\n  content: '';\n  position: absolute;\n  left: 0;\n  top: 0;\n  bottom: 0;\n  width: 4px;\n  background: linear-gradient(to bottom, #3b82f6, #10b981);\n  border-radius: 4px 0 0 4px;\n}\n\n.prose-custom blockquote.hero-callout p {\n  font-size: 0.95rem;\n  line-height: 1.65;\n  font-weight: 500;\n  color: #1e40af;\n  margin: 0;\n}\n\n.dark .prose-custom blockquote.hero-callout {\n  background: linear-gradient(135deg, #172554 0%, #052e16 100%);\n}\n\n.dark .prose-custom blockquote.hero-callout p {\n  color: #93c5fd;\n}\n\n/* -- Regular blockquotes -- */\n\n.prose-custom blockquote {\n  margin-top: 1rem;\n  margin-bottom: 1rem;\n  border-left: 3px solid #a5b4fc;\n  border-radius: 0 0.5rem 0.5rem 0;\n  background-color: #eef2ff;\n  padding: 0.75rem 1rem;\n  font-style: normal;\n}\n\n.prose-custom blockquote p {\n  color: #4338ca;\n  font-size: 0.875rem;\n  margin: 0;\n}\n\n.dark .prose-custom blockquote {\n  border-left-color: #6366f1;\n  background-color: rgba(99, 102, 241, 0.1);\n}\n\n.dark .prose-custom blockquote p {\n  color: #c7d2fe;\n}\n\n/* -- Code blocks with language label -- */\n\n.prose-custom pre {\n  position: relative;\n  overflow-x: auto;\n  margin-top: 1rem;\n  margin-bottom: 1rem;\n  border-radius: 0.75rem;\n  border: 1px solid #1e293b;\n  background-color: #0f172a;\n  padding: 1.25rem;\n  font-size: 0.8125rem;\n  line-height: 1.6;\n  color: #e2e8f0;\n  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n}\n\n.prose-custom pre.code-block {\n  padding-top: 2.25rem;\n}\n\n.prose-custom pre.code-block::before {\n  content: attr(data-language);\n  position: absolute;\n  top: 0;\n  right: 0.75rem;\n  padding: 0.125rem 0.625rem 0.25rem;\n  background: #3b82f6;\n  color: #ffffff;\n  font-size: 0.625rem;\n  font-weight: 700;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  border-radius: 0 0 0.375rem 0.375rem;\n  font-family: system-ui, -apple-system, sans-serif;\n}\n\n.prose-custom pre.code-block[data-language=\"sh\"]::before {\n  background: #22c55e;\n  content: \"terminal\";\n}\n\n/* -- ASCII diagram containers -- */\n\n.prose-custom pre.ascii-diagram {\n  background: linear-gradient(135deg, #f8fafc, #f1f5f9);\n  border: 1px solid #cbd5e1;\n  color: #334155;\n  text-align: center;\n  font-size: 0.75rem;\n  line-height: 1.35;\n  padding: 1.5rem 1rem;\n}\n\n.dark .prose-custom pre.ascii-diagram {\n  background: linear-gradient(135deg, #1e1b4b, #172554);\n  border-color: #312e81;\n  color: #c7d2fe;\n}\n\n/* -- Inline code -- */\n\n.prose-custom :not(pre) > code {\n  border-radius: 0.375rem;\n  background-color: #f1f5f9;\n  border: 1px solid #e2e8f0;\n  padding: 0.125rem 0.425rem;\n  font-size: 0.8125rem;\n  font-weight: 500;\n  color: #be185d;\n  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n}\n\n.dark .prose-custom :not(pre) > code {\n  background-color: #27272a;\n  border-color: #3f3f46;\n  color: #f9a8d4;\n}\n\n/* -- Links -- */\n\n.prose-custom a {\n  color: #2563eb;\n  font-weight: 500;\n  text-decoration: underline;\n  text-decoration-color: #93c5fd;\n  text-underline-offset: 2px;\n  transition: text-decoration-color 0.15s;\n}\n\n.prose-custom a:hover {\n  text-decoration-color: #2563eb;\n}\n\n.dark .prose-custom a {\n  color: #60a5fa;\n  text-decoration-color: #1e40af;\n}\n\n.dark .prose-custom a:hover {\n  text-decoration-color: #60a5fa;\n}\n\n/* -- Lists -- */\n\n.prose-custom ul {\n  margin-top: 0.75rem;\n  margin-bottom: 0.75rem;\n  padding-left: 1.5rem;\n  font-size: 0.9rem;\n  line-height: 1.7;\n  color: #3f3f46;\n}\n\n.dark .prose-custom ul {\n  color: #d4d4d8;\n}\n\n.prose-custom ul > li {\n  margin-top: 0.375rem;\n  margin-bottom: 0.375rem;\n  position: relative;\n}\n\n.prose-custom ul > li::marker {\n  color: #3b82f6;\n}\n\n.prose-custom ol {\n  margin-top: 0.75rem;\n  margin-bottom: 0.75rem;\n  padding-left: 0;\n  list-style: none;\n  counter-reset: step-counter;\n  font-size: 0.9rem;\n  line-height: 1.7;\n  color: #3f3f46;\n}\n\n.dark .prose-custom ol {\n  color: #d4d4d8;\n}\n\n.prose-custom ol > li {\n  counter-increment: step-counter;\n  margin-top: 0.75rem;\n  margin-bottom: 0.75rem;\n  padding-left: 2.75rem;\n  position: relative;\n}\n\n.prose-custom ol > li::before {\n  content: counter(step-counter);\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 1.75rem;\n  height: 1.75rem;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 0.5rem;\n  background: linear-gradient(135deg, #3b82f6, #6366f1);\n  color: #ffffff;\n  font-size: 0.75rem;\n  font-weight: 700;\n  font-family: ui-monospace, SFMono-Regular, monospace;\n  flex-shrink: 0;\n}\n\n/* Reset nested lists inside ol to normal style */\n.prose-custom ol > li > ul {\n  padding-left: 1.25rem;\n}\n\n.prose-custom ol > li > ul > li {\n  padding-left: 0;\n}\n\n.prose-custom ol > li > ul > li::before {\n  display: none;\n}\n\n/* -- Tables -- */\n\n.prose-custom table {\n  width: 100%;\n  margin-top: 1.25rem;\n  margin-bottom: 1.25rem;\n  border-collapse: separate;\n  border-spacing: 0;\n  font-size: 0.8125rem;\n  line-height: 1.5;\n  border-radius: 0.75rem;\n  overflow: hidden;\n  border: 1px solid #e2e8f0;\n}\n\n.dark .prose-custom table {\n  border-color: #27272a;\n}\n\n.prose-custom thead {\n  border-bottom: none;\n}\n\n.prose-custom th {\n  padding: 0.625rem 1rem;\n  text-align: left;\n  font-weight: 600;\n  font-size: 0.6875rem;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: #64748b;\n  background-color: #f8fafc;\n  border-bottom: 1px solid #e2e8f0;\n}\n\n.dark .prose-custom th {\n  color: #94a3b8;\n  background-color: #18181b;\n  border-bottom-color: #27272a;\n}\n\n.prose-custom td {\n  padding: 0.625rem 1rem;\n  border-bottom: 1px solid #f1f5f9;\n  color: #475569;\n}\n\n.prose-custom td code {\n  font-size: 0.75rem;\n}\n\n.dark .prose-custom td {\n  border-bottom-color: #1e1e22;\n  color: #cbd5e1;\n}\n\n.prose-custom tbody tr:last-child td {\n  border-bottom: none;\n}\n\n.prose-custom tbody tr:hover {\n  background-color: #f8fafc;\n}\n\n.dark .prose-custom tbody tr:hover {\n  background-color: #111113;\n}\n\n/* -- Horizontal rules -- */\n\n.prose-custom hr {\n  margin-top: 2rem;\n  margin-bottom: 2rem;\n  border: none;\n  height: 1px;\n  background: linear-gradient(to right, transparent, #d4d4d8, transparent);\n}\n\n.dark .prose-custom hr {\n  background: linear-gradient(to right, transparent, #3f3f46, transparent);\n}\n\n/* -- Strong / Em -- */\n\n.prose-custom strong {\n  font-weight: 700;\n  color: #09090b;\n}\n\n.dark .prose-custom strong {\n  color: #fafafa;\n}\n\n.prose-custom em {\n  font-style: italic;\n  color: #52525b;\n}\n\n.dark .prose-custom em {\n  color: #a1a1aa;\n}\n\n/* =====================================================\n   HIGHLIGHT.JS TOKEN THEME (code syntax highlighting)\n   ===================================================== */\n\n.hljs {\n  background: transparent !important;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-type {\n  color: #c084fc;\n  font-weight: 500;\n}\n\n.hljs-literal,\n.hljs-symbol,\n.hljs-bullet {\n  color: #fb923c;\n}\n\n.hljs-string,\n.hljs-doctag,\n.hljs-template-variable,\n.hljs-variable {\n  color: #34d399;\n}\n\n.hljs-number {\n  color: #fb923c;\n}\n\n.hljs-comment,\n.hljs-quote {\n  color: #64748b;\n  font-style: italic;\n}\n\n.hljs-title,\n.hljs-section {\n  color: #60a5fa;\n  font-weight: 600;\n}\n\n.hljs-title.function_,\n.hljs-title.class_ {\n  color: #60a5fa;\n}\n\n.hljs-built_in {\n  color: #f472b6;\n}\n\n.hljs-attr,\n.hljs-attribute {\n  color: #fbbf24;\n}\n\n.hljs-params {\n  color: #e2e8f0;\n}\n\n.hljs-meta {\n  color: #94a3b8;\n}\n\n.hljs-name,\n.hljs-tag {\n  color: #f87171;\n}\n\n.hljs-selector-class,\n.hljs-selector-id {\n  color: #a78bfa;\n}\n\n.hljs-deletion {\n  color: #fca5a5;\n  background-color: rgba(239, 68, 68, 0.15);\n}\n\n.hljs-addition {\n  color: #86efac;\n  background-color: rgba(34, 197, 94, 0.15);\n}\n"
  },
  {
    "path": "web/src/app/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function RootPage() {\n  redirect(\"/en/\");\n}\n"
  },
  {
    "path": "web/src/components/architecture/arch-diagram.tsx",
    "content": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { LAYERS } from \"@/lib/constants\";\nimport versionsData from \"@/data/generated/versions.json\";\n\nconst CLASS_DESCRIPTIONS: Record<string, string> = {\n  TodoManager: \"Visible task planning with constraints\",\n  SkillLoader: \"Dynamic knowledge injection from SKILL.md files\",\n  ContextManager: \"Three-layer context compression pipeline\",\n  Task: \"File-based persistent task with dependencies\",\n  TaskManager: \"File-based persistent task CRUD with dependencies\",\n  BackgroundTask: \"Single background execution unit\",\n  BackgroundManager: \"Non-blocking thread execution + notification queue\",\n  TeammateManager: \"Multi-agent team lifecycle and coordination\",\n  Teammate: \"Individual agent identity and state tracking\",\n  SharedBoard: \"Cross-agent shared state coordination\",\n};\n\ninterface ArchDiagramProps {\n  version: string;\n}\n\nfunction getLayerColor(versionId: string): string {\n  const layer = LAYERS.find((l) => (l.versions as readonly string[]).includes(versionId));\n  return layer?.color ?? \"#71717a\";\n}\n\nfunction getLayerColorClasses(versionId: string): {\n  border: string;\n  bg: string;\n} {\n  const v =\n    versionsData.versions.find((v) => v.id === versionId) as { layer?: string } | undefined;\n  const layer = v?.layer;\n  switch (layer) {\n    case \"tools\":\n      return {\n        border: \"border-blue-500\",\n        bg: \"bg-blue-500/10\",\n      };\n    case \"planning\":\n      return {\n        border: \"border-emerald-500\",\n        bg: \"bg-emerald-500/10\",\n      };\n    case \"memory\":\n      return {\n        border: \"border-purple-500\",\n        bg: \"bg-purple-500/10\",\n      };\n    case \"concurrency\":\n      return {\n        border: \"border-amber-500\",\n        bg: \"bg-amber-500/10\",\n      };\n    case \"collaboration\":\n      return {\n        border: \"border-red-500\",\n        bg: \"bg-red-500/10\",\n      };\n    default:\n      return {\n        border: \"border-zinc-500\",\n        bg: \"bg-zinc-500/10\",\n      };\n  }\n}\n\nfunction collectClassesUpTo(\n  targetId: string\n): { name: string; introducedIn: string }[] {\n  const { versions, diffs } = versionsData;\n  const order = versions.map((v) => v.id);\n  const targetIdx = order.indexOf(targetId);\n  if (targetIdx < 0) return [];\n\n  const result: { name: string; introducedIn: string }[] = [];\n  const seen = new Set<string>();\n\n  for (let i = 0; i <= targetIdx; i++) {\n    const v = versions[i];\n    if (!v.classes) continue;\n    for (const cls of v.classes) {\n      if (!seen.has(cls.name)) {\n        seen.add(cls.name);\n        result.push({ name: cls.name, introducedIn: v.id });\n      }\n    }\n  }\n\n  return result;\n}\n\nfunction getNewClassNames(version: string): Set<string> {\n  const diff = versionsData.diffs.find((d) => d.to === version);\n  if (!diff) {\n    const v = versionsData.versions.find((ver) => ver.id === version);\n    return new Set(v?.classes?.map((c) => c.name) ?? []);\n  }\n  return new Set(diff.newClasses ?? []);\n}\n\nexport function ArchDiagram({ version }: ArchDiagramProps) {\n  const allClasses = collectClassesUpTo(version);\n  const newClassNames = getNewClassNames(version);\n  const versionData = versionsData.versions.find((v) => v.id === version);\n  const tools = versionData?.tools ?? [];\n\n  const reversed = [...allClasses].reverse();\n\n  return (\n    <div className=\"space-y-3\">\n      {reversed.map((cls, i) => {\n        const isNew = newClassNames.has(cls.name);\n        const colorClasses = getLayerColorClasses(cls.introducedIn);\n\n        return (\n          <div key={cls.name}>\n            {i > 0 && (\n              <div className=\"flex justify-center py-1\">\n                <motion.svg\n                  width=\"24\"\n                  height=\"20\"\n                  viewBox=\"0 0 24 20\"\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  transition={{ delay: i * 0.08 + 0.05 }}\n                >\n                  <motion.line\n                    x1={12}\n                    y1={0}\n                    x2={12}\n                    y2={14}\n                    stroke=\"var(--color-text-secondary)\"\n                    strokeWidth={1.5}\n                    initial={{ pathLength: 0 }}\n                    animate={{ pathLength: 1 }}\n                    transition={{ duration: 0.3, delay: i * 0.08 }}\n                  />\n                  <motion.polygon\n                    points=\"7,12 12,19 17,12\"\n                    fill=\"var(--color-text-secondary)\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    transition={{ delay: i * 0.08 + 0.2 }}\n                  />\n                </motion.svg>\n              </div>\n            )}\n            <motion.div\n            key={cls.name}\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ delay: i * 0.08, duration: 0.3 }}\n            className={cn(\n              \"rounded-lg border-2 px-4 py-3 transition-colors\",\n              isNew\n                ? cn(colorClasses.border, colorClasses.bg)\n                : \"border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800/50\"\n            )}\n          >\n            <div className=\"flex items-center justify-between\">\n              <div>\n                <span\n                  className={cn(\n                    \"font-mono text-sm font-semibold\",\n                    isNew\n                      ? \"text-zinc-900 dark:text-white\"\n                      : \"text-zinc-400 dark:text-zinc-500\"\n                  )}\n                >\n                  {cls.name}\n                </span>\n                <p\n                  className={cn(\n                    \"mt-0.5 text-xs\",\n                    isNew\n                      ? \"text-zinc-600 dark:text-zinc-300\"\n                      : \"text-zinc-400 dark:text-zinc-500\"\n                  )}\n                >\n                  {CLASS_DESCRIPTIONS[cls.name] || \"\"}\n                </p>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <span className=\"text-xs text-zinc-400 dark:text-zinc-500\">\n                  {cls.introducedIn}\n                </span>\n                {isNew && (\n                  <span className=\"rounded-full bg-zinc-900 px-2 py-0.5 text-[10px] font-bold uppercase text-white dark:bg-white dark:text-zinc-900\">\n                    NEW\n                  </span>\n                )}\n              </div>\n            </div>\n          </motion.div>\n          </div>\n        );\n      })}\n\n      {allClasses.length === 0 && (\n        <div className=\"rounded-lg border border-dashed border-zinc-300 px-4 py-6 text-center text-sm text-zinc-400 dark:border-zinc-600\">\n          No classes in this version (functions only)\n        </div>\n      )}\n\n      {tools.length > 0 && (\n        <motion.div\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={{ delay: reversed.length * 0.08 + 0.1 }}\n          className=\"flex flex-wrap gap-1.5 pt-2\"\n        >\n          {tools.map((tool) => (\n            <span\n              key={tool}\n              className=\"rounded-md bg-zinc-100 px-2 py-1 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400\"\n            >\n              {tool}\n            </span>\n          ))}\n        </motion.div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/architecture/design-decisions.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useTranslations, useLocale } from \"@/lib/i18n\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nimport s01Annotations from \"@/data/annotations/s01.json\";\nimport s02Annotations from \"@/data/annotations/s02.json\";\nimport s03Annotations from \"@/data/annotations/s03.json\";\nimport s04Annotations from \"@/data/annotations/s04.json\";\nimport s05Annotations from \"@/data/annotations/s05.json\";\nimport s06Annotations from \"@/data/annotations/s06.json\";\nimport s07Annotations from \"@/data/annotations/s07.json\";\nimport s08Annotations from \"@/data/annotations/s08.json\";\nimport s09Annotations from \"@/data/annotations/s09.json\";\nimport s10Annotations from \"@/data/annotations/s10.json\";\nimport s11Annotations from \"@/data/annotations/s11.json\";\nimport s12Annotations from \"@/data/annotations/s12.json\";\n\ninterface Decision {\n  id: string;\n  title: string;\n  description: string;\n  alternatives: string;\n  zh?: { title: string; description: string };\n  ja?: { title: string; description: string };\n}\n\ninterface AnnotationFile {\n  version: string;\n  decisions: Decision[];\n}\n\nconst ANNOTATIONS: Record<string, AnnotationFile> = {\n  s01: s01Annotations as AnnotationFile,\n  s02: s02Annotations as AnnotationFile,\n  s03: s03Annotations as AnnotationFile,\n  s04: s04Annotations as AnnotationFile,\n  s05: s05Annotations as AnnotationFile,\n  s06: s06Annotations as AnnotationFile,\n  s07: s07Annotations as AnnotationFile,\n  s08: s08Annotations as AnnotationFile,\n  s09: s09Annotations as AnnotationFile,\n  s10: s10Annotations as AnnotationFile,\n  s11: s11Annotations as AnnotationFile,\n  s12: s12Annotations as AnnotationFile,\n};\n\ninterface DesignDecisionsProps {\n  version: string;\n}\n\nfunction DecisionCard({\n  decision,\n  locale,\n}: {\n  decision: Decision;\n  locale: string;\n}) {\n  const [open, setOpen] = useState(false);\n  const t = useTranslations(\"version\");\n\n  const localized =\n    locale !== \"en\" ? (decision as unknown as Record<string, unknown>)[locale] as { title?: string; description?: string } | undefined : undefined;\n\n  const title = localized?.title || decision.title;\n  const description = localized?.description || decision.description;\n\n  return (\n    <div className=\"rounded-lg border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\">\n      <button\n        onClick={() => setOpen(!open)}\n        className=\"flex w-full items-center justify-between px-4 py-3 text-left\"\n      >\n        <span className=\"pr-4 text-sm font-semibold text-zinc-900 dark:text-white\">\n          {title}\n        </span>\n        <ChevronDown\n          size={16}\n          className={cn(\n            \"shrink-0 text-zinc-400 transition-transform duration-200\",\n            open && \"rotate-180\"\n          )}\n        />\n      </button>\n\n      <AnimatePresence>\n        {open && (\n          <motion.div\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.2 }}\n            className=\"overflow-hidden\"\n          >\n            <div className=\"border-t border-zinc-100 px-4 py-3 dark:border-zinc-800\">\n              <p className=\"text-sm leading-relaxed text-zinc-600 dark:text-zinc-300\">\n                {description}\n              </p>\n\n              {decision.alternatives && (\n                <div className=\"mt-3\">\n                  <h4 className=\"text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500\">\n                    {t(\"alternatives\")}\n                  </h4>\n                  <p className=\"mt-1 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400\">\n                    {decision.alternatives}\n                  </p>\n                </div>\n              )}\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport function DesignDecisions({ version }: DesignDecisionsProps) {\n  const t = useTranslations(\"version\");\n  const locale = useLocale();\n\n  const annotations = ANNOTATIONS[version];\n  if (!annotations || annotations.decisions.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold\">{t(\"design_decisions\")}</h2>\n      <div className=\"space-y-2\">\n        {annotations.decisions.map((decision, i) => (\n          <motion.div\n            key={decision.id}\n            initial={{ opacity: 0, y: 10 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ delay: i * 0.05 }}\n          >\n            <DecisionCard decision={decision} locale={locale} />\n          </motion.div>\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/architecture/execution-flow.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { useTranslations } from \"@/lib/i18n\";\nimport { getFlowForVersion } from \"@/data/execution-flows\";\nimport type { FlowNode, FlowEdge } from \"@/types/agent-data\";\n\nconst NODE_WIDTH = 140;\nconst NODE_HEIGHT = 40;\nconst DIAMOND_SIZE = 50;\n\nconst LAYER_COLORS: Record<string, string> = {\n  start: \"#3B82F6\",\n  process: \"#10B981\",\n  decision: \"#F59E0B\",\n  subprocess: \"#8B5CF6\",\n  end: \"#EF4444\",\n};\n\nfunction getNodeCenter(node: FlowNode): { cx: number; cy: number } {\n  return { cx: node.x, cy: node.y };\n}\n\nfunction getEdgePath(from: FlowNode, to: FlowNode): string {\n  const { cx: x1, cy: y1 } = getNodeCenter(from);\n  const { cx: x2, cy: y2 } = getNodeCenter(to);\n\n  const halfH = from.type === \"decision\" ? DIAMOND_SIZE / 2 : NODE_HEIGHT / 2;\n  const halfHTo = to.type === \"decision\" ? DIAMOND_SIZE / 2 : NODE_HEIGHT / 2;\n\n  if (Math.abs(x1 - x2) < 10) {\n    const startY = y1 + halfH;\n    const endY = y2 - halfHTo;\n    return `M ${x1} ${startY} L ${x2} ${endY}`;\n  }\n\n  const startY = y1 + halfH;\n  const endY = y2 - halfHTo;\n  const midY = (startY + endY) / 2;\n  return `M ${x1} ${startY} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${endY}`;\n}\n\nfunction NodeShape({ node }: { node: FlowNode }) {\n  const color = LAYER_COLORS[node.type];\n  const lines = node.label.split(\"\\n\");\n\n  if (node.type === \"decision\") {\n    const half = DIAMOND_SIZE / 2;\n    return (\n      <g>\n        <polygon\n          points={`${node.x},${node.y - half} ${node.x + half},${node.y} ${node.x},${node.y + half} ${node.x - half},${node.y}`}\n          fill=\"none\"\n          stroke={color}\n          strokeWidth={2}\n        />\n        {lines.map((line, i) => (\n          <text\n            key={i}\n            x={node.x}\n            y={node.y + (i - (lines.length - 1) / 2) * 12}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n            fontSize={10}\n            fontFamily=\"monospace\"\n            fill=\"currentColor\"\n          >\n            {line}\n          </text>\n        ))}\n      </g>\n    );\n  }\n\n  if (node.type === \"start\" || node.type === \"end\") {\n    return (\n      <g>\n        <rect\n          x={node.x - NODE_WIDTH / 2}\n          y={node.y - NODE_HEIGHT / 2}\n          width={NODE_WIDTH}\n          height={NODE_HEIGHT}\n          rx={NODE_HEIGHT / 2}\n          fill=\"none\"\n          stroke={color}\n          strokeWidth={2}\n        />\n        <text\n          x={node.x}\n          y={node.y}\n          textAnchor=\"middle\"\n          dominantBaseline=\"central\"\n          fontSize={12}\n          fontWeight={600}\n          fontFamily=\"monospace\"\n          fill=\"currentColor\"\n        >\n          {node.label}\n        </text>\n      </g>\n    );\n  }\n\n  const isSubprocess = node.type === \"subprocess\";\n  return (\n    <g>\n      <rect\n        x={node.x - NODE_WIDTH / 2}\n        y={node.y - NODE_HEIGHT / 2}\n        width={NODE_WIDTH}\n        height={NODE_HEIGHT}\n        rx={4}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={2}\n        strokeDasharray={isSubprocess ? \"6 3\" : undefined}\n      />\n      {lines.map((line, i) => (\n        <text\n          key={i}\n          x={node.x}\n          y={node.y + (i - (lines.length - 1) / 2) * 13}\n          textAnchor=\"middle\"\n          dominantBaseline=\"central\"\n          fontSize={11}\n          fontFamily=\"monospace\"\n          fill=\"currentColor\"\n        >\n          {line}\n        </text>\n      ))}\n    </g>\n  );\n}\n\nfunction EdgePath({\n  edge,\n  nodes,\n  index,\n}: {\n  edge: FlowEdge;\n  nodes: FlowNode[];\n  index: number;\n}) {\n  const from = nodes.find((n) => n.id === edge.from);\n  const to = nodes.find((n) => n.id === edge.to);\n  if (!from || !to) return null;\n\n  const d = getEdgePath(from, to);\n  const midX = (from.x + to.x) / 2;\n  const midY = (from.y + to.y) / 2;\n\n  return (\n    <g>\n      <motion.path\n        d={d}\n        fill=\"none\"\n        stroke=\"var(--color-text-secondary)\"\n        strokeWidth={1.5}\n        markerEnd=\"url(#arrowhead)\"\n        initial={{ pathLength: 0, opacity: 0 }}\n        animate={{ pathLength: 1, opacity: 1 }}\n        transition={{ duration: 0.5, delay: index * 0.12 }}\n      />\n      {edge.label && (\n        <motion.text\n          x={midX + 8}\n          y={midY - 4}\n          fontSize={10}\n          fill=\"var(--color-text-secondary)\"\n          fontFamily=\"monospace\"\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={{ delay: index * 0.12 + 0.3 }}\n        >\n          {edge.label}\n        </motion.text>\n      )}\n    </g>\n  );\n}\n\ninterface ExecutionFlowProps {\n  version: string;\n}\n\nexport function ExecutionFlow({ version }: ExecutionFlowProps) {\n  const t = useTranslations(\"version\");\n  const [flow, setFlow] = useState<ReturnType<typeof getFlowForVersion>>(null);\n\n  useEffect(() => {\n    setFlow(getFlowForVersion(version));\n  }, [version]);\n\n  if (!flow) return null;\n\n  const maxY = Math.max(...flow.nodes.map((n) => n.y)) + 50;\n\n  return (\n    <section>\n      <h2 className=\"mb-4 text-xl font-semibold\">{t(\"execution_flow\")}</h2>\n      <div className=\"overflow-x-auto rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4\">\n        <svg\n          viewBox={`0 0 600 ${maxY}`}\n          className=\"mx-auto w-full max-w-[600px]\"\n          style={{ minHeight: 300 }}\n        >\n          <defs>\n            <marker\n              id=\"arrowhead\"\n              markerWidth={8}\n              markerHeight={6}\n              refX={8}\n              refY={3}\n              orient=\"auto\"\n            >\n              <polygon\n                points=\"0 0, 8 3, 0 6\"\n                fill=\"var(--color-text-secondary)\"\n              />\n            </marker>\n          </defs>\n\n          {flow.edges.map((edge, i) => (\n            <EdgePath key={`${edge.from}-${edge.to}`} edge={edge} nodes={flow.nodes} index={i} />\n          ))}\n\n          {flow.nodes.map((node, i) => (\n            <motion.g\n              key={node.id}\n              initial={{ opacity: 0, y: -10 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ delay: i * 0.06, duration: 0.3 }}\n            >\n              <NodeShape node={node} />\n            </motion.g>\n          ))}\n        </svg>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/architecture/message-flow.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect, useRef } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\nconst FLOW_STEPS = [\n  { role: \"user\", label: \"user\", color: \"bg-blue-500\" },\n  { role: \"assistant\", label: \"assistant\", color: \"bg-zinc-600\" },\n  { role: \"tool_call\", label: \"tool_call\", color: \"bg-amber-500\" },\n  { role: \"tool_result\", label: \"tool_result\", color: \"bg-emerald-500\" },\n  { role: \"assistant\", label: \"assistant\", color: \"bg-zinc-600\" },\n  { role: \"tool_call\", label: \"tool_call\", color: \"bg-amber-500\" },\n  { role: \"tool_result\", label: \"tool_result\", color: \"bg-emerald-500\" },\n  { role: \"assistant\", label: \"assistant (final)\", color: \"bg-zinc-600\" },\n];\n\nexport function MessageFlow() {\n  const [count, setCount] = useState(0);\n  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n  useEffect(() => {\n    intervalRef.current = setInterval(() => {\n      setCount((prev) => {\n        if (prev >= FLOW_STEPS.length) {\n          setTimeout(() => setCount(0), 1500);\n          return prev;\n        }\n        return prev + 1;\n      });\n    }, 800);\n    return () => {\n      if (intervalRef.current) clearInterval(intervalRef.current);\n    };\n  }, []);\n\n  return (\n    <div className=\"overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4\">\n      <div className=\"mb-3 flex items-center gap-2\">\n        <span className=\"font-mono text-xs text-[var(--color-text-secondary)]\">\n          messages[]\n        </span>\n        <span className=\"ml-auto rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-xs tabular-nums dark:bg-zinc-800\">\n          len={count}\n        </span>\n      </div>\n      <div className=\"flex gap-1.5 overflow-x-auto pb-1\">\n        <AnimatePresence>\n          {FLOW_STEPS.slice(0, count).map((step, i) => (\n            <motion.div\n              key={i}\n              initial={{ opacity: 0, scale: 0.7, width: 0 }}\n              animate={{ opacity: 1, scale: 1, width: \"auto\" }}\n              transition={{ duration: 0.25 }}\n              className={`flex shrink-0 items-center rounded-md px-2.5 py-1.5 ${step.color}`}\n            >\n              <span className=\"whitespace-nowrap font-mono text-[10px] font-medium text-white\">\n                {step.label}\n              </span>\n            </motion.div>\n          ))}\n        </AnimatePresence>\n        {count === 0 && (\n          <div className=\"flex h-7 items-center text-xs text-[var(--color-text-secondary)]\">\n            []\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/code/source-viewer.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\n\ninterface SourceViewerProps {\n  source: string;\n  filename: string;\n}\n\nfunction highlightLine(line: string): React.ReactNode[] {\n  const trimmed = line.trimStart();\n  if (trimmed.startsWith(\"#\")) {\n    return [\n      <span key={0} className=\"text-zinc-400 italic\">\n        {line}\n      </span>,\n    ];\n  }\n  if (trimmed.startsWith(\"@\")) {\n    return [\n      <span key={0} className=\"text-amber-400\">\n        {line}\n      </span>,\n    ];\n  }\n  if (trimmed.startsWith('\"\"\"') || trimmed.startsWith(\"'''\")) {\n    return [\n      <span key={0} className=\"text-emerald-500\">\n        {line}\n      </span>,\n    ];\n  }\n\n  const keywordSet = new Set([\n    \"def\", \"class\", \"import\", \"from\", \"return\", \"if\", \"elif\", \"else\",\n    \"while\", \"for\", \"in\", \"not\", \"and\", \"or\", \"is\", \"None\", \"True\",\n    \"False\", \"try\", \"except\", \"raise\", \"with\", \"as\", \"yield\", \"break\",\n    \"continue\", \"pass\", \"global\", \"lambda\", \"async\", \"await\",\n  ]);\n\n  const parts = line.split(\n    /(\\b(?:def|class|import|from|return|if|elif|else|while|for|in|not|and|or|is|None|True|False|try|except|raise|with|as|yield|break|continue|pass|global|lambda|async|await|self)\\b|\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|f\"(?:[^\"\\\\]|\\\\.)*\"|f'(?:[^'\\\\]|\\\\.)*'|#.*$|\\b\\d+(?:\\.\\d+)?\\b)/\n  );\n\n  return parts.map((part, idx) => {\n    if (!part) return null;\n    if (keywordSet.has(part)) {\n      return <span key={idx} className=\"text-blue-400 font-medium\">{part}</span>;\n    }\n    if (part === \"self\") {\n      return <span key={idx} className=\"text-purple-400\">{part}</span>;\n    }\n    if (part.startsWith(\"#\")) {\n      return <span key={idx} className=\"text-zinc-400 italic\">{part}</span>;\n    }\n    if (\n      (part.startsWith('\"') && part.endsWith('\"')) ||\n      (part.startsWith(\"'\") && part.endsWith(\"'\")) ||\n      (part.startsWith('f\"') && part.endsWith('\"')) ||\n      (part.startsWith(\"f'\") && part.endsWith(\"'\"))\n    ) {\n      return <span key={idx} className=\"text-emerald-500\">{part}</span>;\n    }\n    if (/^\\d+(?:\\.\\d+)?$/.test(part)) {\n      return <span key={idx} className=\"text-orange-400\">{part}</span>;\n    }\n    return <span key={idx}>{part}</span>;\n  });\n}\n\nexport function SourceViewer({ source, filename }: SourceViewerProps) {\n  const lines = useMemo(() => source.split(\"\\n\"), [source]);\n\n  return (\n    <div className=\"rounded-lg border border-zinc-200 dark:border-zinc-700\">\n      <div className=\"flex items-center gap-2 border-b border-zinc-200 px-4 py-2 dark:border-zinc-700\">\n        <div className=\"flex gap-1.5\">\n          <span className=\"h-3 w-3 rounded-full bg-red-400\" />\n          <span className=\"h-3 w-3 rounded-full bg-yellow-400\" />\n          <span className=\"h-3 w-3 rounded-full bg-green-400\" />\n        </div>\n        <span className=\"font-mono text-xs text-zinc-400\">{filename}</span>\n      </div>\n      <div className=\"overflow-x-auto bg-zinc-950\">\n        <pre className=\"p-2 text-[10px] leading-4 sm:p-4 sm:text-xs sm:leading-5\">\n          <code>\n            {lines.map((line, i) => (\n              <div key={i} className=\"flex\">\n                <span className=\"mr-2 inline-block w-6 shrink-0 select-none text-right text-zinc-600 sm:mr-4 sm:w-8\">\n                  {i + 1}\n                </span>\n                <span className=\"text-zinc-200\">\n                  {highlightLine(line)}\n                </span>\n              </div>\n            ))}\n          </code>\n        </pre>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/diff/code-diff.tsx",
    "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { diffLines, Change } from \"diff\";\nimport { cn } from \"@/lib/utils\";\n\ninterface CodeDiffProps {\n  oldSource: string;\n  newSource: string;\n  oldLabel: string;\n  newLabel: string;\n}\n\nexport function CodeDiff({ oldSource, newSource, oldLabel, newLabel }: CodeDiffProps) {\n  const [viewMode, setViewMode] = useState<\"unified\" | \"split\">(\"unified\");\n\n  const changes = useMemo(() => diffLines(oldSource, newSource), [oldSource, newSource]);\n\n  return (\n    <div>\n      <div className=\"mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n        <div className=\"min-w-0 truncate text-sm text-zinc-500 dark:text-zinc-400\">\n          <span className=\"font-medium text-zinc-700 dark:text-zinc-300\">{oldLabel}</span>\n          {\" -> \"}\n          <span className=\"font-medium text-zinc-700 dark:text-zinc-300\">{newLabel}</span>\n        </div>\n        <div className=\"flex shrink-0 rounded-lg border border-zinc-200 dark:border-zinc-700\">\n          <button\n            onClick={() => setViewMode(\"unified\")}\n            className={cn(\n              \"min-h-[36px] px-3 text-xs font-medium transition-colors\",\n              viewMode === \"unified\"\n                ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\"\n                : \"text-zinc-500 hover:text-zinc-700 dark:text-zinc-400\"\n            )}\n          >\n            Unified\n          </button>\n          <button\n            onClick={() => setViewMode(\"split\")}\n            className={cn(\n              \"min-h-[36px] px-3 text-xs font-medium transition-colors sm:inline-flex hidden\",\n              viewMode === \"split\"\n                ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\"\n                : \"text-zinc-500 hover:text-zinc-700 dark:text-zinc-400\"\n            )}\n          >\n            Split\n          </button>\n        </div>\n      </div>\n\n      {viewMode === \"unified\" ? (\n        <UnifiedView changes={changes} />\n      ) : (\n        <SplitView changes={changes} />\n      )}\n    </div>\n  );\n}\n\nfunction UnifiedView({ changes }: { changes: Change[] }) {\n  let oldLine = 1;\n  let newLine = 1;\n\n  const rows: { oldNum: number | null; newNum: number | null; type: \"add\" | \"remove\" | \"context\"; text: string }[] = [];\n\n  for (const change of changes) {\n    const lines = change.value.replace(/\\n$/, \"\").split(\"\\n\");\n    for (const line of lines) {\n      if (change.added) {\n        rows.push({ oldNum: null, newNum: newLine++, type: \"add\", text: line });\n      } else if (change.removed) {\n        rows.push({ oldNum: oldLine++, newNum: null, type: \"remove\", text: line });\n      } else {\n        rows.push({ oldNum: oldLine++, newNum: newLine++, type: \"context\", text: line });\n      }\n    }\n  }\n\n  return (\n    <div className=\"overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700\">\n      <table className=\"w-full border-collapse font-mono text-xs leading-5\">\n        <tbody>\n          {rows.map((row, i) => (\n            <tr\n              key={i}\n              className={cn(\n                row.type === \"add\" && \"bg-green-50 dark:bg-green-950/30\",\n                row.type === \"remove\" && \"bg-red-50 dark:bg-red-950/30\"\n              )}\n            >\n              <td className=\"w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600\">\n                {row.oldNum ?? \"\"}\n              </td>\n              <td className=\"w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600\">\n                {row.newNum ?? \"\"}\n              </td>\n              <td className=\"w-4 select-none px-1 text-center\">\n                {row.type === \"add\" && <span className=\"text-green-600 dark:text-green-400\">+</span>}\n                {row.type === \"remove\" && <span className=\"text-red-600 dark:text-red-400\">-</span>}\n              </td>\n              <td className=\"whitespace-pre px-2\">\n                <span\n                  className={cn(\n                    row.type === \"add\" && \"text-green-800 dark:text-green-300\",\n                    row.type === \"remove\" && \"text-red-800 dark:text-red-300\",\n                    row.type === \"context\" && \"text-zinc-700 dark:text-zinc-300\"\n                  )}\n                >\n                  {row.text}\n                </span>\n              </td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n\nfunction SplitView({ changes }: { changes: Change[] }) {\n  let oldLine = 1;\n  let newLine = 1;\n\n  type SplitRow = {\n    left: { num: number | null; text: string; type: \"remove\" | \"context\" | \"empty\" };\n    right: { num: number | null; text: string; type: \"add\" | \"context\" | \"empty\" };\n  };\n\n  const rows: SplitRow[] = [];\n\n  for (const change of changes) {\n    const lines = change.value.replace(/\\n$/, \"\").split(\"\\n\");\n    if (change.removed) {\n      for (const line of lines) {\n        rows.push({\n          left: { num: oldLine++, text: line, type: \"remove\" },\n          right: { num: null, text: \"\", type: \"empty\" },\n        });\n      }\n    } else if (change.added) {\n      let filled = 0;\n      for (const line of lines) {\n        // Try to fill in empty right-side slots from preceding removes\n        const lastUnfilled = rows.length - lines.length + filled;\n        if (\n          lastUnfilled >= 0 &&\n          lastUnfilled < rows.length &&\n          rows[lastUnfilled].right.type === \"empty\" &&\n          rows[lastUnfilled].left.type === \"remove\"\n        ) {\n          rows[lastUnfilled].right = { num: newLine++, text: line, type: \"add\" };\n        } else {\n          rows.push({\n            left: { num: null, text: \"\", type: \"empty\" },\n            right: { num: newLine++, text: line, type: \"add\" },\n          });\n        }\n        filled++;\n      }\n    } else {\n      for (const line of lines) {\n        rows.push({\n          left: { num: oldLine++, text: line, type: \"context\" },\n          right: { num: newLine++, text: line, type: \"context\" },\n        });\n      }\n    }\n  }\n\n  const cellClass = (type: string) =>\n    cn(\n      \"whitespace-pre px-2\",\n      type === \"add\" && \"bg-green-50 text-green-800 dark:bg-green-950/30 dark:text-green-300\",\n      type === \"remove\" && \"bg-red-50 text-red-800 dark:bg-red-950/30 dark:text-red-300\",\n      type === \"context\" && \"text-zinc-700 dark:text-zinc-300\",\n      type === \"empty\" && \"bg-zinc-50 dark:bg-zinc-900\"\n    );\n\n  return (\n    <div className=\"overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700\">\n      <table className=\"w-full border-collapse font-mono text-xs leading-5\">\n        <tbody>\n          {rows.map((row, i) => (\n            <tr key={i}>\n              <td className=\"w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600\">\n                {row.left.num ?? \"\"}\n              </td>\n              <td className={cn(\"w-1/2 border-r border-zinc-200 dark:border-zinc-700\", cellClass(row.left.type))}>\n                {row.left.text}\n              </td>\n              <td className=\"w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600\">\n                {row.right.num ?? \"\"}\n              </td>\n              <td className={cn(\"w-1/2\", cellClass(row.right.type))}>\n                {row.right.text}\n              </td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/diff/whats-new.tsx",
    "content": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { useTranslations } from \"@/lib/i18n\";\nimport { Card } from \"@/components/ui/card\";\n\ninterface WhatsNewProps {\n  diff: {\n    from: string;\n    to: string;\n    newClasses: string[];\n    newFunctions: string[];\n    newTools: string[];\n    locDelta: number;\n  } | null;\n}\n\nexport function WhatsNew({ diff }: WhatsNewProps) {\n  const t = useTranslations(\"version\");\n  const td = useTranslations(\"diff\");\n\n  if (!diff) {\n    return null;\n  }\n\n  const hasContent =\n    diff.newClasses.length > 0 ||\n    diff.newTools.length > 0 ||\n    diff.newFunctions.length > 0 ||\n    diff.locDelta !== 0;\n\n  if (!hasContent) {\n    return null;\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold\">{t(\"whats_new\")}</h2>\n\n      <div className=\"grid gap-4 sm:grid-cols-2\">\n        {diff.newClasses.length > 0 && (\n          <motion.div\n            initial={{ opacity: 0, y: 12 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ delay: 0.1 }}\n          >\n            <Card className=\"h-full\">\n              <h3 className=\"mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400\">\n                {td(\"new_classes\")}\n              </h3>\n              <div className=\"space-y-1.5\">\n                {diff.newClasses.map((cls) => (\n                  <div\n                    key={cls}\n                    className=\"rounded-md bg-emerald-50 px-3 py-1.5 font-mono text-sm font-medium text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-300\"\n                  >\n                    {cls}\n                  </div>\n                ))}\n              </div>\n            </Card>\n          </motion.div>\n        )}\n\n        {diff.newTools.length > 0 && (\n          <motion.div\n            initial={{ opacity: 0, y: 12 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ delay: 0.15 }}\n          >\n            <Card className=\"h-full\">\n              <h3 className=\"mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400\">\n                {td(\"new_tools\")}\n              </h3>\n              <div className=\"flex flex-wrap gap-1.5\">\n                {diff.newTools.map((tool) => (\n                  <span\n                    key={tool}\n                    className=\"rounded-full bg-blue-50 px-3 py-1 font-mono text-xs font-medium text-blue-700 dark:bg-blue-900/20 dark:text-blue-300\"\n                  >\n                    {tool}\n                  </span>\n                ))}\n              </div>\n            </Card>\n          </motion.div>\n        )}\n\n        {diff.newFunctions.length > 0 && (\n          <motion.div\n            initial={{ opacity: 0, y: 12 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ delay: 0.2 }}\n          >\n            <Card className=\"h-full\">\n              <h3 className=\"mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400\">\n                {td(\"new_functions\")}\n              </h3>\n              <ul className=\"space-y-1 text-sm text-zinc-700 dark:text-zinc-300\">\n                {diff.newFunctions.map((fn) => (\n                  <li key={fn} className=\"font-mono\">\n                    <span className=\"text-zinc-400 dark:text-zinc-500\">\n                      def{\" \"}\n                    </span>\n                    {fn}()\n                  </li>\n                ))}\n              </ul>\n            </Card>\n          </motion.div>\n        )}\n\n        {diff.locDelta !== 0 && (\n          <motion.div\n            initial={{ opacity: 0, y: 12 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ delay: 0.25 }}\n          >\n            <Card className=\"flex h-full items-center\">\n              <div>\n                <h3 className=\"mb-1 text-sm font-medium text-zinc-500 dark:text-zinc-400\">\n                  {td(\"loc_delta\")}\n                </h3>\n                <p className=\"text-2xl font-bold text-emerald-600 dark:text-emerald-400\">\n                  +{diff.locDelta} lines\n                </p>\n              </div>\n            </Card>\n          </motion.div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/docs/doc-renderer.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useLocale } from \"@/lib/i18n\";\nimport docsData from \"@/data/generated/docs.json\";\nimport { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkRehype from \"remark-rehype\";\nimport rehypeRaw from \"rehype-raw\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeStringify from \"rehype-stringify\";\n\ninterface DocRendererProps {\n  version: string;\n}\n\nfunction renderMarkdown(md: string): string {\n  const result = unified()\n    .use(remarkParse)\n    .use(remarkGfm)\n    .use(remarkRehype, { allowDangerousHtml: true })\n    .use(rehypeRaw)\n    .use(rehypeHighlight, { detect: false, ignoreMissing: true })\n    .use(rehypeStringify)\n    .processSync(md);\n  return String(result);\n}\n\nfunction postProcessHtml(html: string): string {\n  // Add language labels to highlighted code blocks\n  html = html.replace(\n    /<pre><code class=\"hljs language-(\\w+)\">/g,\n    '<pre class=\"code-block\" data-language=\"$1\"><code class=\"hljs language-$1\">'\n  );\n\n  // Wrap plain pre>code (ASCII art / diagrams) in diagram container\n  html = html.replace(\n    /<pre><code(?! class=\"hljs)([^>]*)>/g,\n    '<pre class=\"ascii-diagram\"><code$1>'\n  );\n\n  // Mark the first blockquote as hero callout\n  html = html.replace(\n    /<blockquote>/,\n    '<blockquote class=\"hero-callout\">'\n  );\n\n  // Remove the h1 (it's redundant with the page header)\n  html = html.replace(/<h1>.*?<\\/h1>\\n?/, \"\");\n\n  // Fix ordered list counter for interrupted lists (ol start=\"N\")\n  html = html.replace(\n    /<ol start=\"(\\d+)\">/g,\n    (_, start) => `<ol style=\"counter-reset:step-counter ${parseInt(start) - 1}\">`\n  );\n\n  return html;\n}\n\nexport function DocRenderer({ version }: DocRendererProps) {\n  const locale = useLocale();\n\n  const doc = useMemo(() => {\n    const match = docsData.find(\n      (d: { version: string; locale: string }) =>\n        d.version === version && d.locale === locale\n    );\n    if (match) return match;\n    return docsData.find(\n      (d: { version: string; locale: string }) =>\n        d.version === version && d.locale === \"en\"\n    );\n  }, [version, locale]);\n\n  if (!doc) return null;\n\n  const html = useMemo(() => {\n    const raw = renderMarkdown(doc.content);\n    return postProcessHtml(raw);\n  }, [doc.content]);\n\n  return (\n    <div className=\"py-4\">\n      <div\n        className=\"prose-custom\"\n        dangerouslySetInnerHTML={{ __html: html }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/layout/header.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { useTranslations, useLocale } from \"@/lib/i18n\";\nimport { Github, Menu, X, Sun, Moon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst NAV_ITEMS = [\n  { key: \"timeline\", href: \"/timeline\" },\n  { key: \"compare\", href: \"/compare\" },\n  { key: \"layers\", href: \"/layers\" },\n] as const;\n\nconst LOCALES = [\n  { code: \"en\", label: \"EN\" },\n  { code: \"zh\", label: \"中文\" },\n  { code: \"ja\", label: \"日本語\" },\n];\n\nexport function Header() {\n  const t = useTranslations(\"nav\");\n  const pathname = usePathname();\n  const locale = useLocale();\n  const [mobileOpen, setMobileOpen] = useState(false);\n  const [dark, setDark] = useState(() => {\n    if (typeof window !== \"undefined\") {\n      const stored = localStorage.getItem(\"theme\");\n      if (stored) return stored === \"dark\";\n      return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n    }\n    return false;\n  });\n\n  function toggleDark() {\n    const next = !dark;\n    setDark(next);\n    document.documentElement.classList.toggle(\"dark\", next);\n    localStorage.setItem(\"theme\", next ? \"dark\" : \"light\");\n  }\n\n  function switchLocale(newLocale: string) {\n    const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);\n    window.location.href = newPath;\n  }\n\n  return (\n    <header className=\"sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-bg)]/80 backdrop-blur-sm\">\n      <div className=\"mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8\">\n        <Link href={`/${locale}`} className=\"text-lg font-bold\">\n          Learn Claude Code\n        </Link>\n\n        {/* Desktop nav */}\n        <nav className=\"hidden items-center gap-6 md:flex\">\n          {NAV_ITEMS.map((item) => (\n            <Link\n              key={item.key}\n              href={`/${locale}${item.href}`}\n              className={cn(\n                \"text-sm font-medium transition-colors hover:text-zinc-900 dark:hover:text-white\",\n                pathname.includes(item.href)\n                  ? \"text-zinc-900 dark:text-white\"\n                  : \"text-zinc-500 dark:text-zinc-400\"\n              )}\n            >\n              {t(item.key)}\n            </Link>\n          ))}\n\n          {/* Locale switcher */}\n          <div className=\"flex items-center gap-1 rounded-lg border border-[var(--color-border)] p-0.5\">\n            {LOCALES.map((l) => (\n              <button\n                key={l.code}\n                onClick={() => switchLocale(l.code)}\n                className={cn(\n                  \"rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n                  locale === l.code\n                    ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\"\n                    : \"text-zinc-500 hover:text-zinc-700 dark:text-zinc-400\"\n                )}\n              >\n                {l.label}\n              </button>\n            ))}\n          </div>\n\n          <button\n            onClick={toggleDark}\n            className=\"rounded-md p-1.5 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white\"\n          >\n            {dark ? <Sun size={16} /> : <Moon size={16} />}\n          </button>\n\n          <a\n            href=\"https://github.com/shareAI-lab/learn-claude-code\"\n            target=\"_blank\"\n            rel=\"noopener\"\n            className=\"text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white\"\n          >\n            <Github size={18} />\n          </a>\n        </nav>\n\n        {/* Mobile hamburger */}\n        <button\n          onClick={() => setMobileOpen(!mobileOpen)}\n          className=\"flex min-h-[44px] min-w-[44px] items-center justify-center md:hidden\"\n        >\n          {mobileOpen ? <X size={20} /> : <Menu size={20} />}\n        </button>\n      </div>\n\n      {/* Mobile menu */}\n      {mobileOpen && (\n        <div className=\"border-t border-[var(--color-border)] bg-[var(--color-bg)] p-4 md:hidden\">\n          {NAV_ITEMS.map((item) => (\n            <Link\n              key={item.key}\n              href={`/${locale}${item.href}`}\n              className=\"flex min-h-[44px] items-center text-sm\"\n              onClick={() => setMobileOpen(false)}\n            >\n              {t(item.key)}\n            </Link>\n          ))}\n          <div className=\"mt-3 flex items-center justify-between border-t border-[var(--color-border)] pt-3\">\n            <div className=\"flex gap-2\">\n              {LOCALES.map((l) => (\n                <button\n                  key={l.code}\n                  onClick={() => switchLocale(l.code)}\n                  className={cn(\n                    \"min-h-[44px] min-w-[44px] rounded-md px-3 text-xs font-medium\",\n                    locale === l.code\n                      ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\"\n                      : \"border border-[var(--color-border)]\"\n                  )}\n                >\n                  {l.label}\n                </button>\n              ))}\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <button\n                onClick={toggleDark}\n                className=\"flex min-h-[44px] min-w-[44px] items-center justify-center rounded-md text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white\"\n              >\n                {dark ? <Sun size={18} /> : <Moon size={18} />}\n              </button>\n              <a\n                href=\"https://github.com/shareAI-lab/learn-claude-code\"\n                target=\"_blank\"\n                rel=\"noopener\"\n                className=\"flex min-h-[44px] min-w-[44px] items-center justify-center text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white\"\n              >\n                <Github size={18} />\n              </a>\n            </div>\n          </div>\n        </div>\n      )}\n    </header>\n  );\n}\n"
  },
  {
    "path": "web/src/components/layout/sidebar.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { LAYERS, VERSION_META } from \"@/lib/constants\";\nimport { useTranslations } from \"@/lib/i18n\";\nimport { cn } from \"@/lib/utils\";\n\nconst LAYER_DOT_BG: Record<string, string> = {\n  tools: \"bg-blue-500\",\n  planning: \"bg-emerald-500\",\n  memory: \"bg-purple-500\",\n  concurrency: \"bg-amber-500\",\n  collaboration: \"bg-red-500\",\n};\n\nexport function Sidebar() {\n  const pathname = usePathname();\n  const locale = pathname.split(\"/\")[1] || \"en\";\n  const t = useTranslations(\"sessions\");\n  const tLayer = useTranslations(\"layer_labels\");\n\n  return (\n    <nav className=\"hidden w-56 shrink-0 md:block\">\n      <div className=\"sticky top-[calc(3.5rem+2rem)] space-y-5\">\n        {LAYERS.map((layer) => (\n          <div key={layer.id}>\n            <div className=\"flex items-center gap-1.5 pb-1.5\">\n              <span className={cn(\"h-2 w-2 rounded-full\", LAYER_DOT_BG[layer.id])} />\n              <span className=\"text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500\">\n                {tLayer(layer.id)}\n              </span>\n            </div>\n            <ul className=\"space-y-0.5\">\n              {layer.versions.map((vId) => {\n                const meta = VERSION_META[vId];\n                const href = `/${locale}/${vId}`;\n                const isActive =\n                  pathname === href ||\n                  pathname === `${href}/` ||\n                  pathname.startsWith(`${href}/diff`);\n\n                return (\n                  <li key={vId}>\n                    <Link\n                      href={href}\n                      className={cn(\n                        \"block rounded-md px-2.5 py-1.5 text-sm transition-colors\",\n                        isActive\n                          ? \"bg-zinc-100 font-medium text-zinc-900 dark:bg-zinc-800 dark:text-white\"\n                          : \"text-zinc-500 hover:bg-zinc-50 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800/50 dark:hover:text-zinc-300\"\n                      )}\n                    >\n                      <span className=\"font-mono text-xs\">{vId}</span>\n                      <span className=\"ml-1.5\">{t(vId) || meta?.title}</span>\n                    </Link>\n                  </li>\n                );\n              })}\n            </ul>\n          </div>\n        ))}\n      </div>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "web/src/components/simulator/agent-loop-simulator.tsx",
    "content": "\"use client\";\n\nimport { useRef, useEffect, useState } from \"react\";\nimport { AnimatePresence } from \"framer-motion\";\nimport { useTranslations } from \"@/lib/i18n\";\nimport { useSimulator } from \"@/hooks/useSimulator\";\nimport { SimulatorControls } from \"./simulator-controls\";\nimport { SimulatorMessage } from \"./simulator-message\";\nimport type { Scenario } from \"@/types/agent-data\";\n\nconst scenarioModules: Record<string, () => Promise<{ default: Scenario }>> = {\n  s01: () => import(\"@/data/scenarios/s01.json\") as Promise<{ default: Scenario }>,\n  s02: () => import(\"@/data/scenarios/s02.json\") as Promise<{ default: Scenario }>,\n  s03: () => import(\"@/data/scenarios/s03.json\") as Promise<{ default: Scenario }>,\n  s04: () => import(\"@/data/scenarios/s04.json\") as Promise<{ default: Scenario }>,\n  s05: () => import(\"@/data/scenarios/s05.json\") as Promise<{ default: Scenario }>,\n  s06: () => import(\"@/data/scenarios/s06.json\") as Promise<{ default: Scenario }>,\n  s07: () => import(\"@/data/scenarios/s07.json\") as Promise<{ default: Scenario }>,\n  s08: () => import(\"@/data/scenarios/s08.json\") as Promise<{ default: Scenario }>,\n  s09: () => import(\"@/data/scenarios/s09.json\") as Promise<{ default: Scenario }>,\n  s10: () => import(\"@/data/scenarios/s10.json\") as Promise<{ default: Scenario }>,\n  s11: () => import(\"@/data/scenarios/s11.json\") as Promise<{ default: Scenario }>,\n  s12: () => import(\"@/data/scenarios/s12.json\") as Promise<{ default: Scenario }>,\n};\n\ninterface AgentLoopSimulatorProps {\n  version: string;\n}\n\nexport function AgentLoopSimulator({ version }: AgentLoopSimulatorProps) {\n  const t = useTranslations(\"version\");\n  const [scenario, setScenario] = useState<Scenario | null>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const loader = scenarioModules[version];\n    if (loader) {\n      loader().then((mod) => setScenario(mod.default));\n    }\n  }, [version]);\n\n  const sim = useSimulator(scenario?.steps ?? []);\n\n  useEffect(() => {\n    if (scrollRef.current) {\n      scrollRef.current.scrollTo({\n        top: scrollRef.current.scrollHeight,\n        behavior: \"smooth\",\n      });\n    }\n  }, [sim.visibleSteps.length]);\n\n  if (!scenario) return null;\n\n  return (\n    <section>\n      <h2 className=\"mb-2 text-xl font-semibold\">{t(\"simulator\")}</h2>\n      <p className=\"mb-4 text-sm text-[var(--color-text-secondary)]\">\n        {scenario.description}\n      </p>\n\n      <div className=\"overflow-hidden rounded-xl border border-[var(--color-border)]\">\n        <div className=\"border-b border-[var(--color-border)] bg-zinc-50 px-4 py-3 dark:bg-zinc-900\">\n          <SimulatorControls\n            isPlaying={sim.isPlaying}\n            isComplete={sim.isComplete}\n            currentIndex={sim.currentIndex}\n            totalSteps={sim.totalSteps}\n            speed={sim.speed}\n            onPlay={sim.play}\n            onPause={sim.pause}\n            onStep={sim.stepForward}\n            onReset={sim.reset}\n            onSpeedChange={sim.setSpeed}\n          />\n        </div>\n\n        <div\n          ref={scrollRef}\n          className=\"flex max-h-[500px] min-h-[200px] flex-col gap-3 overflow-y-auto p-4\"\n        >\n          {sim.visibleSteps.length === 0 && (\n            <div className=\"flex flex-1 items-center justify-center text-sm text-[var(--color-text-secondary)]\">\n              Press Play or Step to begin\n            </div>\n          )}\n          <AnimatePresence mode=\"popLayout\">\n            {sim.visibleSteps.map((step, i) => (\n              <SimulatorMessage key={i} step={step} index={i} />\n            ))}\n          </AnimatePresence>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/simulator/simulator-controls.tsx",
    "content": "\"use client\";\n\nimport { useTranslations } from \"@/lib/i18n\";\nimport { Play, Pause, SkipForward, RotateCcw } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SimulatorControlsProps {\n  isPlaying: boolean;\n  isComplete: boolean;\n  currentIndex: number;\n  totalSteps: number;\n  speed: number;\n  onPlay: () => void;\n  onPause: () => void;\n  onStep: () => void;\n  onReset: () => void;\n  onSpeedChange: (speed: number) => void;\n}\n\nconst SPEEDS = [0.5, 1, 2, 4];\n\nexport function SimulatorControls({\n  isPlaying,\n  isComplete,\n  currentIndex,\n  totalSteps,\n  speed,\n  onPlay,\n  onPause,\n  onStep,\n  onReset,\n  onSpeedChange,\n}: SimulatorControlsProps) {\n  const t = useTranslations(\"sim\");\n\n  return (\n    <div className=\"flex flex-wrap items-center gap-3\">\n      <div className=\"flex items-center gap-1.5\">\n        {isPlaying ? (\n          <button\n            onClick={onPause}\n            className=\"flex h-9 w-9 items-center justify-center rounded-lg bg-zinc-900 text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200\"\n            title={t(\"pause\")}\n          >\n            <Pause size={16} />\n          </button>\n        ) : (\n          <button\n            onClick={onPlay}\n            disabled={isComplete}\n            className=\"flex h-9 w-9 items-center justify-center rounded-lg bg-zinc-900 text-white transition-colors hover:bg-zinc-700 disabled:opacity-40 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200\"\n            title={t(\"play\")}\n          >\n            <Play size={16} />\n          </button>\n        )}\n        <button\n          onClick={onStep}\n          disabled={isComplete}\n          className=\"flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--color-border)] transition-colors hover:bg-zinc-100 disabled:opacity-40 dark:hover:bg-zinc-800\"\n          title={t(\"step\")}\n        >\n          <SkipForward size={16} />\n        </button>\n        <button\n          onClick={onReset}\n          className=\"flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--color-border)] transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800\"\n          title={t(\"reset\")}\n        >\n          <RotateCcw size={16} />\n        </button>\n      </div>\n\n      <div className=\"flex items-center gap-1.5\">\n        <span className=\"text-xs text-[var(--color-text-secondary)]\">\n          {t(\"speed\")}:\n        </span>\n        {SPEEDS.map((s) => (\n          <button\n            key={s}\n            onClick={() => onSpeedChange(s)}\n            className={cn(\n              \"rounded px-2 py-1 text-xs font-medium transition-colors\",\n              speed === s\n                ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\"\n                : \"text-[var(--color-text-secondary)] hover:text-[var(--color-text)]\"\n            )}\n          >\n            {s}x\n          </button>\n        ))}\n      </div>\n\n      <span className=\"ml-auto text-xs tabular-nums text-[var(--color-text-secondary)]\">\n        {Math.max(0, currentIndex + 1)} {t(\"step_of\")} {totalSteps}\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/simulator/simulator-message.tsx",
    "content": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport type { SimStep } from \"@/types/agent-data\";\nimport { User, Bot, Terminal, ArrowRight, AlertCircle } from \"lucide-react\";\n\ninterface SimulatorMessageProps {\n  step: SimStep;\n  index: number;\n}\n\nconst TYPE_CONFIG: Record<\n  string,\n  { icon: typeof User; label: string; bgClass: string; borderClass: string }\n> = {\n  user_message: {\n    icon: User,\n    label: \"User\",\n    bgClass: \"bg-blue-50 dark:bg-blue-950/30\",\n    borderClass: \"border-blue-200 dark:border-blue-800\",\n  },\n  assistant_text: {\n    icon: Bot,\n    label: \"Assistant\",\n    bgClass: \"bg-zinc-50 dark:bg-zinc-900\",\n    borderClass: \"border-zinc-200 dark:border-zinc-700\",\n  },\n  tool_call: {\n    icon: Terminal,\n    label: \"Tool Call\",\n    bgClass: \"bg-amber-50 dark:bg-amber-950/30\",\n    borderClass: \"border-amber-200 dark:border-amber-800\",\n  },\n  tool_result: {\n    icon: ArrowRight,\n    label: \"Tool Result\",\n    bgClass: \"bg-emerald-50 dark:bg-emerald-950/30\",\n    borderClass: \"border-emerald-200 dark:border-emerald-800\",\n  },\n  system_event: {\n    icon: AlertCircle,\n    label: \"System\",\n    bgClass: \"bg-purple-50 dark:bg-purple-950/30\",\n    borderClass: \"border-purple-200 dark:border-purple-800\",\n  },\n};\n\nexport function SimulatorMessage({ step, index }: SimulatorMessageProps) {\n  const config = TYPE_CONFIG[step.type] || TYPE_CONFIG.assistant_text;\n  const Icon = config.icon;\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, y: 12 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.25 }}\n      className={cn(\n        \"rounded-lg border p-3\",\n        config.bgClass,\n        config.borderClass\n      )}\n    >\n      <div className=\"mb-1.5 flex items-center gap-2\">\n        <Icon size={14} className=\"shrink-0 text-[var(--color-text-secondary)]\" />\n        <span className=\"text-xs font-medium text-[var(--color-text-secondary)]\">\n          {config.label}\n          {step.toolName && (\n            <span className=\"ml-1.5 font-mono text-[var(--color-text)]\">\n              {step.toolName}\n            </span>\n          )}\n        </span>\n      </div>\n\n      {step.type === \"tool_call\" || step.type === \"tool_result\" ? (\n        <pre className=\"overflow-x-auto whitespace-pre-wrap rounded bg-zinc-900 p-2.5 font-mono text-xs leading-relaxed text-zinc-100 dark:bg-zinc-950\">\n          {step.content || \"(empty)\"}\n        </pre>\n      ) : step.type === \"system_event\" ? (\n        <pre className=\"overflow-x-auto whitespace-pre-wrap rounded bg-purple-900/80 p-2.5 font-mono text-xs leading-relaxed text-purple-100 dark:bg-purple-950\">\n          {step.content}\n        </pre>\n      ) : (\n        <p className=\"text-sm leading-relaxed\">{step.content}</p>\n      )}\n\n      <p className=\"mt-2 text-xs italic text-[var(--color-text-secondary)]\">\n        {step.annotation}\n      </p>\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/timeline/timeline.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { motion } from \"framer-motion\";\nimport { useTranslations, useLocale } from \"@/lib/i18n\";\nimport { LEARNING_PATH, VERSION_META, LAYERS } from \"@/lib/constants\";\nimport { LayerBadge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport versionsData from \"@/data/generated/versions.json\";\n\nconst LAYER_DOT_BG: Record<string, string> = {\n  tools: \"bg-blue-500\",\n  planning: \"bg-emerald-500\",\n  memory: \"bg-purple-500\",\n  concurrency: \"bg-amber-500\",\n  collaboration: \"bg-red-500\",\n};\n\nconst LAYER_LINE_BG: Record<string, string> = {\n  tools: \"bg-blue-500/30\",\n  planning: \"bg-emerald-500/30\",\n  memory: \"bg-purple-500/30\",\n  concurrency: \"bg-amber-500/30\",\n  collaboration: \"bg-red-500/30\",\n};\n\nconst LAYER_BAR_BG: Record<string, string> = {\n  tools: \"bg-blue-500\",\n  planning: \"bg-emerald-500\",\n  memory: \"bg-purple-500\",\n  concurrency: \"bg-amber-500\",\n  collaboration: \"bg-red-500\",\n};\n\nfunction getVersionData(id: string) {\n  return versionsData.versions.find((v) => v.id === id);\n}\n\nconst MAX_LOC = Math.max(\n  ...versionsData.versions\n    .filter((v) => LEARNING_PATH.includes(v.id as (typeof LEARNING_PATH)[number]))\n    .map((v) => v.loc)\n);\n\nexport function Timeline() {\n  const t = useTranslations(\"timeline\");\n  const tv = useTranslations(\"version\");\n  const locale = useLocale();\n\n  return (\n    <div className=\"flex flex-col gap-12\">\n      {/* Layer Legend */}\n      <div>\n        <h3 className=\"mb-3 text-sm font-medium text-[var(--color-text-secondary)]\">\n          {t(\"layer_legend\")}\n        </h3>\n        <div className=\"flex flex-wrap gap-2\">\n          {LAYERS.map((layer) => (\n            <div key={layer.id} className=\"flex items-center gap-1.5\">\n              <span\n                className={cn(\"h-3 w-3 rounded-full\", LAYER_DOT_BG[layer.id])}\n              />\n              <span className=\"text-xs font-medium\">{layer.label}</span>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* Vertical Timeline */}\n      <div className=\"relative\">\n        {LEARNING_PATH.map((versionId, index) => {\n          const meta = VERSION_META[versionId];\n          const data = getVersionData(versionId);\n          if (!meta || !data) return null;\n\n          const isLast = index === LEARNING_PATH.length - 1;\n          const locPercent = Math.round((data.loc / MAX_LOC) * 100);\n\n          return (\n            <div key={versionId} className=\"relative flex gap-4 pb-8 sm:gap-6\">\n              {/* Timeline line + dot */}\n              <div className=\"flex flex-col items-center\">\n                <div\n                  className={cn(\n                    \"z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full ring-4 ring-[var(--color-bg)] sm:h-10 sm:w-10\",\n                    LAYER_DOT_BG[meta.layer]\n                  )}\n                >\n                  <span className=\"text-[10px] font-bold text-white sm:text-xs\">\n                    {versionId.replace(\"s\", \"\").replace(\"_mini\", \"m\")}\n                  </span>\n                </div>\n                {!isLast && (\n                  <div\n                    className={cn(\n                      \"w-0.5 flex-1\",\n                      LAYER_LINE_BG[\n                        VERSION_META[LEARNING_PATH[index + 1]]?.layer || meta.layer\n                      ]\n                    )}\n                  />\n                )}\n              </div>\n\n              {/* Content card */}\n              <div className=\"flex-1 pb-2\">\n                <motion.div\n                  initial={{ opacity: 0, x: 30 }}\n                  whileInView={{ opacity: 1, x: 0 }}\n                  viewport={{ once: true, margin: \"-50px\" }}\n                  transition={{ duration: 0.4, delay: 0.1 }}\n                  className=\"rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4 transition-colors hover:border-[var(--color-text-secondary)]/30 sm:p-5\"\n                >\n                  <div className=\"flex flex-wrap items-start gap-2\">\n                    <LayerBadge layer={meta.layer}>{versionId}</LayerBadge>\n                    <span className=\"text-xs text-[var(--color-text-secondary)]\">\n                      {meta.coreAddition}\n                    </span>\n                  </div>\n\n                  <h3 className=\"mt-2 text-base font-semibold sm:text-lg\">\n                    {meta.title}\n                    <span className=\"ml-2 text-sm font-normal text-[var(--color-text-secondary)]\">\n                      {meta.subtitle}\n                    </span>\n                  </h3>\n\n                  {/* Stats row */}\n                  <div className=\"mt-3 flex flex-wrap items-center gap-4 text-xs text-[var(--color-text-secondary)]\">\n                    <span className=\"tabular-nums\">\n                      {data.loc} {tv(\"loc\")}\n                    </span>\n                    <span className=\"tabular-nums\">\n                      {data.tools.length} {tv(\"tools\")}\n                    </span>\n                  </div>\n\n                  {/* LOC bar */}\n                  <div className=\"mt-2 h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800\">\n                    <div\n                      className={cn(\n                        \"h-full rounded-full transition-all\",\n                        LAYER_BAR_BG[meta.layer]\n                      )}\n                      style={{ width: `${locPercent}%` }}\n                    />\n                  </div>\n\n                  {/* Key insight */}\n                  {meta.keyInsight && (\n                    <p className=\"mt-3 text-sm italic text-[var(--color-text-secondary)]\">\n                      &ldquo;{meta.keyInsight}&rdquo;\n                    </p>\n                  )}\n\n                  {/* Link */}\n                  <Link\n                    href={`/${locale}/${versionId}`}\n                    className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-zinc-900 hover:underline dark:text-zinc-100\"\n                  >\n                    {t(\"learn_more\")}\n                    <span aria-hidden=\"true\">&rarr;</span>\n                  </Link>\n                </motion.div>\n              </div>\n            </div>\n          );\n        })}\n      </div>\n\n      {/* LOC Growth Chart */}\n      <div>\n        <h3 className=\"mb-4 text-lg font-semibold\">{t(\"loc_growth\")}</h3>\n        <div className=\"flex flex-col gap-2\">\n          {LEARNING_PATH.map((versionId) => {\n            const meta = VERSION_META[versionId];\n            const data = getVersionData(versionId);\n            if (!meta || !data) return null;\n\n            const widthPercent = Math.max(\n              2,\n              Math.round((data.loc / MAX_LOC) * 100)\n            );\n\n            return (\n              <div key={versionId} className=\"flex items-center gap-3\">\n                <span className=\"w-8 shrink-0 text-right text-xs font-medium tabular-nums\">\n                  {versionId}\n                </span>\n                <div className=\"flex-1\">\n                  <div className=\"h-5 w-full overflow-hidden rounded bg-zinc-100 dark:bg-zinc-800\">\n                    <motion.div\n                      initial={{ width: 0 }}\n                      whileInView={{ width: `${widthPercent}%` }}\n                      viewport={{ once: true }}\n                      transition={{ duration: 0.6, delay: 0.05 * LEARNING_PATH.indexOf(versionId) }}\n                      className={cn(\n                        \"flex h-full items-center rounded px-2\",\n                        LAYER_BAR_BG[meta.layer]\n                      )}\n                    >\n                      <span className=\"text-[10px] font-medium text-white\">\n                        {data.loc}\n                      </span>\n                    </motion.div>\n                  </div>\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/ui/badge.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nconst LAYER_COLORS = {\n  tools:\n    \"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300\",\n  planning:\n    \"bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300\",\n  memory:\n    \"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300\",\n  concurrency:\n    \"bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300\",\n  collaboration:\n    \"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300\",\n} as const;\n\ninterface BadgeProps {\n  layer: keyof typeof LAYER_COLORS;\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport function LayerBadge({ layer, children, className }: BadgeProps) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium\",\n        LAYER_COLORS[layer],\n        className\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n"
  },
  {
    "path": "web/src/components/ui/card.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\ninterface CardProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n}\n\nexport function Card({ className, children, ...props }: CardProps) {\n  return (\n    <div\n      className={cn(\n        \"rounded-xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function CardHeader({ className, children, ...props }: CardProps) {\n  return (\n    <div className={cn(\"mb-4\", className)} {...props}>\n      {children}\n    </div>\n  );\n}\n\nexport function CardTitle({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLHeadingElement>) {\n  return (\n    <h3 className={cn(\"text-lg font-semibold\", className)} {...props}>\n      {children}\n    </h3>\n  );\n}\n"
  },
  {
    "path": "web/src/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface TabsProps {\n  tabs: { id: string; label: string }[];\n  defaultTab?: string;\n  children: (activeTab: string) => React.ReactNode;\n  className?: string;\n}\n\nexport function Tabs({ tabs, defaultTab, children, className }: TabsProps) {\n  const [active, setActive] = useState(defaultTab || tabs[0]?.id || \"\");\n\n  return (\n    <div className={className}>\n      <div className=\"flex border-b border-zinc-200 dark:border-zinc-700\">\n        {tabs.map((tab) => (\n          <button\n            key={tab.id}\n            onClick={() => setActive(tab.id)}\n            className={cn(\n              \"px-4 py-2 text-sm font-medium transition-colors\",\n              active === tab.id\n                ? \"border-b-2 border-zinc-900 text-zinc-900 dark:border-white dark:text-white\"\n                : \"text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200\"\n            )}\n          >\n            {tab.label}\n          </button>\n        ))}\n      </div>\n      <div className=\"mt-4\">{children(active)}</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/index.tsx",
    "content": "\"use client\";\n\nimport { lazy, Suspense } from \"react\";\nimport { useTranslations } from \"@/lib/i18n\";\n\nconst visualizations: Record<\n  string,\n  React.LazyExoticComponent<React.ComponentType<{ title?: string }>>\n> = {\n  s01: lazy(() => import(\"./s01-agent-loop\")),\n  s02: lazy(() => import(\"./s02-tool-dispatch\")),\n  s03: lazy(() => import(\"./s03-todo-write\")),\n  s04: lazy(() => import(\"./s04-subagent\")),\n  s05: lazy(() => import(\"./s05-skill-loading\")),\n  s06: lazy(() => import(\"./s06-context-compact\")),\n  s07: lazy(() => import(\"./s07-task-system\")),\n  s08: lazy(() => import(\"./s08-background-tasks\")),\n  s09: lazy(() => import(\"./s09-agent-teams\")),\n  s10: lazy(() => import(\"./s10-team-protocols\")),\n  s11: lazy(() => import(\"./s11-autonomous-agents\")),\n  s12: lazy(() => import(\"./s12-worktree-task-isolation\")),\n};\n\nexport function SessionVisualization({ version }: { version: string }) {\n  const t = useTranslations(\"viz\");\n  const Component = visualizations[version];\n  if (!Component) return null;\n  return (\n    <Suspense\n      fallback={\n        <div className=\"min-h-[500px] animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800\" />\n      }\n    >\n      <div className=\"min-h-[500px]\">\n        <Component title={t(version)} />\n      </div>\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s01-agent-loop.tsx",
    "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\nimport { useSvgPalette } from \"@/hooks/useDarkMode\";\n\n// -- Flowchart node definitions --\n\ninterface FlowNode {\n  id: string;\n  label: string;\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  type: \"rect\" | \"diamond\";\n}\n\nconst NODES: FlowNode[] = [\n  { id: \"start\", label: \"Start\", x: 160, y: 30, w: 120, h: 40, type: \"rect\" },\n  { id: \"api_call\", label: \"API Call\", x: 160, y: 110, w: 120, h: 40, type: \"rect\" },\n  { id: \"check\", label: \"stop_reason?\", x: 160, y: 200, w: 140, h: 50, type: \"diamond\" },\n  { id: \"execute\", label: \"Execute Tool\", x: 160, y: 300, w: 120, h: 40, type: \"rect\" },\n  { id: \"append\", label: \"Append Result\", x: 160, y: 380, w: 120, h: 40, type: \"rect\" },\n  { id: \"end\", label: \"Break / Done\", x: 380, y: 200, w: 120, h: 40, type: \"rect\" },\n];\n\n// Edges between nodes (SVG path data computed inline)\ninterface FlowEdge {\n  from: string;\n  to: string;\n  label?: string;\n}\n\nconst EDGES: FlowEdge[] = [\n  { from: \"start\", to: \"api_call\" },\n  { from: \"api_call\", to: \"check\" },\n  { from: \"check\", to: \"execute\", label: \"tool_use\" },\n  { from: \"execute\", to: \"append\" },\n  { from: \"append\", to: \"api_call\" },\n  { from: \"check\", to: \"end\", label: \"end_turn\" },\n];\n\n// Which nodes light up at each step\nconst ACTIVE_NODES_PER_STEP: string[][] = [\n  [],\n  [\"start\"],\n  [\"api_call\"],\n  [\"check\", \"execute\"],\n  [\"execute\", \"append\"],\n  [\"api_call\", \"check\", \"execute\", \"append\"],\n  [\"check\", \"end\"],\n];\n\n// Which edges highlight at each step\nconst ACTIVE_EDGES_PER_STEP: string[][] = [\n  [],\n  [],\n  [\"start->api_call\"],\n  [\"api_call->check\", \"check->execute\"],\n  [\"execute->append\"],\n  [\"append->api_call\", \"api_call->check\", \"check->execute\", \"execute->append\"],\n  [\"api_call->check\", \"check->end\"],\n];\n\n// -- Message blocks --\n\ninterface MessageBlock {\n  role: string;\n  detail: string;\n  colorClass: string;\n}\n\nconst MESSAGES_PER_STEP: (MessageBlock | null)[][] = [\n  [],\n  [{ role: \"user\", detail: \"Fix the login bug\", colorClass: \"bg-blue-500 dark:bg-blue-600\" }],\n  [],\n  [{ role: \"assistant\", detail: \"tool_use: read_file\", colorClass: \"bg-zinc-600 dark:bg-zinc-500\" }],\n  [{ role: \"tool_result\", detail: \"auth.ts contents...\", colorClass: \"bg-emerald-500 dark:bg-emerald-600\" }],\n  [\n    { role: \"assistant\", detail: \"tool_use: edit_file\", colorClass: \"bg-zinc-600 dark:bg-zinc-500\" },\n    { role: \"tool_result\", detail: \"file updated\", colorClass: \"bg-emerald-500 dark:bg-emerald-600\" },\n  ],\n  [{ role: \"assistant\", detail: \"end_turn: Done!\", colorClass: \"bg-purple-500 dark:bg-purple-600\" }],\n];\n\n// -- Step annotations --\n\nconst STEP_INFO = [\n  { title: \"The While Loop\", desc: \"Every agent is a while loop that keeps calling the model until it says 'stop'.\" },\n  { title: \"User Input\", desc: \"The loop starts when the user sends a message.\" },\n  { title: \"Call the Model\", desc: \"Send all messages to the LLM. It sees everything and decides what to do.\" },\n  { title: \"stop_reason: tool_use\", desc: \"The model wants to use a tool. The loop continues.\" },\n  { title: \"Execute & Append\", desc: \"Run the tool, append the result to messages[]. Feed it back.\" },\n  { title: \"Loop Again\", desc: \"Same code path, second iteration. The model decides to edit a file.\" },\n  { title: \"stop_reason: end_turn\", desc: \"The model is done. Loop exits. That's the entire agent.\" },\n];\n\n// -- Helpers --\n\nfunction getNode(id: string): FlowNode {\n  return NODES.find((n) => n.id === id)!;\n}\n\nfunction edgePath(fromId: string, toId: string): string {\n  const from = getNode(fromId);\n  const to = getNode(toId);\n\n  // Loop-back: append -> api_call (goes to the left side and back up)\n  if (fromId === \"append\" && toId === \"api_call\") {\n    const startX = from.x - from.w / 2;\n    const startY = from.y;\n    const endX = to.x - to.w / 2;\n    const endY = to.y;\n    return `M ${startX} ${startY} L ${startX - 50} ${startY} L ${endX - 50} ${endY} L ${endX} ${endY}`;\n  }\n\n  // Horizontal: check -> end\n  if (fromId === \"check\" && toId === \"end\") {\n    const startX = from.x + from.w / 2;\n    const startY = from.y;\n    const endX = to.x - to.w / 2;\n    const endY = to.y;\n    return `M ${startX} ${startY} L ${endX} ${endY}`;\n  }\n\n  // Vertical (default)\n  const startX = from.x;\n  const startY = from.y + from.h / 2;\n  const endX = to.x;\n  const endY = to.y - to.h / 2;\n  return `M ${startX} ${startY} L ${endX} ${endY}`;\n}\n\n// -- Component --\n\nexport default function AgentLoop({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });\n\n  const palette = useSvgPalette();\n  const activeNodes = ACTIVE_NODES_PER_STEP[currentStep];\n  const activeEdges = ACTIVE_EDGES_PER_STEP[currentStep];\n\n  // Build accumulated messages up to the current step\n  const visibleMessages: MessageBlock[] = [];\n  for (let s = 0; s <= currentStep; s++) {\n    for (const msg of MESSAGES_PER_STEP[s]) {\n      if (msg) visibleMessages.push(msg);\n    }\n  }\n\n  const stepInfo = STEP_INFO[currentStep];\n\n  return (\n    <section className=\"min-h-[500px] space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"The Agent While-Loop\"}\n      </h2>\n\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\">\n        <div className=\"flex flex-col gap-4 lg:flex-row\">\n          {/* Left panel: SVG Flowchart (60%) */}\n          <div className=\"w-full lg:w-[60%]\">\n            <div className=\"mb-2 font-mono text-xs text-zinc-400 dark:text-zinc-500\">\n              while (stop_reason === \"tool_use\")\n            </div>\n            <svg\n              viewBox=\"0 0 500 440\"\n              className=\"w-full rounded-md border border-zinc-100 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950\"\n              style={{ minHeight: 300 }}\n            >\n              <defs>\n                <filter id=\"glow-blue\">\n                  <feDropShadow dx=\"0\" dy=\"0\" stdDeviation=\"4\" floodColor=\"#3b82f6\" floodOpacity=\"0.7\" />\n                </filter>\n                <filter id=\"glow-purple\">\n                  <feDropShadow dx=\"0\" dy=\"0\" stdDeviation=\"4\" floodColor=\"#a855f7\" floodOpacity=\"0.7\" />\n                </filter>\n                <marker\n                  id=\"arrowhead\"\n                  markerWidth=\"8\"\n                  markerHeight=\"6\"\n                  refX=\"8\"\n                  refY=\"3\"\n                  orient=\"auto\"\n                >\n                  <polygon points=\"0 0, 8 3, 0 6\" fill={palette.arrowFill} />\n                </marker>\n                <marker\n                  id=\"arrowhead-active\"\n                  markerWidth=\"8\"\n                  markerHeight=\"6\"\n                  refX=\"8\"\n                  refY=\"3\"\n                  orient=\"auto\"\n                >\n                  <polygon points=\"0 0, 8 3, 0 6\" fill={palette.activeEdgeStroke} />\n                </marker>\n              </defs>\n\n              {/* Edges */}\n              {EDGES.map((edge) => {\n                const key = `${edge.from}->${edge.to}`;\n                const isActive = activeEdges.includes(key);\n                const d = edgePath(edge.from, edge.to);\n\n                return (\n                  <g key={key}>\n                    <motion.path\n                      d={d}\n                      fill=\"none\"\n                      stroke={isActive ? palette.activeEdgeStroke : palette.edgeStroke}\n                      strokeWidth={isActive ? 2.5 : 1.5}\n                      strokeDasharray={isActive ? \"none\" : \"none\"}\n                      markerEnd={isActive ? \"url(#arrowhead-active)\" : \"url(#arrowhead)\"}\n                      animate={{\n                        stroke: isActive ? palette.activeEdgeStroke : palette.edgeStroke,\n                        strokeWidth: isActive ? 2.5 : 1.5,\n                      }}\n                      transition={{ duration: 0.4 }}\n                    />\n                    {edge.label && (\n                      <text\n                        x={\n                          edge.from === \"check\" && edge.to === \"end\"\n                            ? (getNode(\"check\").x + getNode(\"end\").x) / 2\n                            : getNode(edge.from).x + 75\n                        }\n                        y={\n                          edge.from === \"check\" && edge.to === \"end\"\n                            ? getNode(\"check\").y - 10\n                            : (getNode(edge.from).y + getNode(edge.to).y) / 2\n                        }\n                        textAnchor=\"middle\"\n                        className=\"fill-zinc-400 text-[10px] dark:fill-zinc-500\"\n                      >\n                        {edge.label}\n                      </text>\n                    )}\n                  </g>\n                );\n              })}\n\n              {/* Nodes */}\n              {NODES.map((node) => {\n                const isActive = activeNodes.includes(node.id);\n                const isEnd = node.id === \"end\";\n                const filterAttr = isActive\n                  ? isEnd\n                    ? \"url(#glow-purple)\"\n                    : \"url(#glow-blue)\"\n                  : \"none\";\n\n                if (node.type === \"diamond\") {\n                  // Diamond shape for decision node\n                  const cx = node.x;\n                  const cy = node.y;\n                  const hw = node.w / 2;\n                  const hh = node.h / 2;\n                  const points = `${cx},${cy - hh} ${cx + hw},${cy} ${cx},${cy + hh} ${cx - hw},${cy}`;\n                  return (\n                    <g key={node.id}>\n                      <motion.polygon\n                        points={points}\n                        rx={6}\n                        fill={isActive ? palette.activeNodeFill : palette.nodeFill}\n                        stroke={isActive ? palette.activeNodeStroke : palette.nodeStroke}\n                        strokeWidth={1.5}\n                        filter={filterAttr}\n                        animate={{\n                          fill: isActive ? palette.activeNodeFill : palette.nodeFill,\n                          stroke: isActive ? palette.activeNodeStroke : palette.nodeStroke,\n                        }}\n                        transition={{ duration: 0.4 }}\n                      />\n                      <motion.text\n                        x={cx}\n                        y={cy + 4}\n                        textAnchor=\"middle\"\n                        fontSize={11}\n                        fontWeight={600}\n                        fontFamily=\"monospace\"\n                        animate={{ fill: isActive ? palette.activeNodeText : palette.nodeText }}\n                        transition={{ duration: 0.4 }}\n                      >\n                        {node.label}\n                      </motion.text>\n                    </g>\n                  );\n                }\n\n                return (\n                  <g key={node.id}>\n                    <motion.rect\n                      x={node.x - node.w / 2}\n                      y={node.y - node.h / 2}\n                      width={node.w}\n                      height={node.h}\n                      rx={8}\n                      fill={isActive ? (isEnd ? palette.endNodeFill : palette.activeNodeFill) : palette.nodeFill}\n                      stroke={isActive ? (isEnd ? palette.endNodeStroke : palette.activeNodeStroke) : palette.nodeStroke}\n                      strokeWidth={1.5}\n                      filter={filterAttr}\n                      animate={{\n                        fill: isActive ? (isEnd ? palette.endNodeFill : palette.activeNodeFill) : palette.nodeFill,\n                        stroke: isActive ? (isEnd ? palette.endNodeStroke : palette.activeNodeStroke) : palette.nodeStroke,\n                      }}\n                      transition={{ duration: 0.4 }}\n                    />\n                    <motion.text\n                      x={node.x}\n                      y={node.y + 4}\n                      textAnchor=\"middle\"\n                      fontSize={12}\n                      fontWeight={600}\n                      fontFamily=\"monospace\"\n                      animate={{ fill: isActive ? palette.activeNodeText : palette.nodeText }}\n                      transition={{ duration: 0.4 }}\n                    >\n                      {node.label}\n                    </motion.text>\n                  </g>\n                );\n              })}\n\n              {/* Iteration counter */}\n              {currentStep >= 5 && (\n                <motion.text\n                  x={60}\n                  y={130}\n                  textAnchor=\"middle\"\n                  fontSize={10}\n                  fontFamily=\"monospace\"\n                  fill=\"#3b82f6\"\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                >\n                  iter #2\n                </motion.text>\n              )}\n            </svg>\n          </div>\n\n          {/* Right panel: messages[] array (40%) */}\n          <div className=\"w-full lg:w-[40%]\">\n            <div className=\"mb-2 font-mono text-xs text-zinc-400 dark:text-zinc-500\">\n              messages[]\n            </div>\n            <div className=\"min-h-[300px] space-y-2 rounded-md border border-zinc-100 bg-zinc-50 p-3 dark:border-zinc-800 dark:bg-zinc-950\">\n              <AnimatePresence mode=\"popLayout\">\n                {visibleMessages.length === 0 && (\n                  <motion.div\n                    key=\"empty\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    exit={{ opacity: 0 }}\n                    className=\"py-8 text-center text-xs text-zinc-400 dark:text-zinc-600\"\n                  >\n                    [ empty ]\n                  </motion.div>\n                )}\n                {visibleMessages.map((msg, i) => (\n                  <motion.div\n                    key={`${msg.role}-${msg.detail}-${i}`}\n                    initial={{ opacity: 0, y: 12, scale: 0.9 }}\n                    animate={{ opacity: 1, y: 0, scale: 1 }}\n                    exit={{ opacity: 0, scale: 0.9 }}\n                    transition={{ duration: 0.35, type: \"spring\", bounce: 0.3 }}\n                    className={`rounded-md px-3 py-2 ${msg.colorClass}`}\n                  >\n                    <div className=\"font-mono text-[11px] font-semibold text-white\">\n                      {msg.role}\n                    </div>\n                    <div className=\"mt-0.5 text-[10px] text-white/80\">\n                      {msg.detail}\n                    </div>\n                  </motion.div>\n                ))}\n              </AnimatePresence>\n\n              {/* Array index markers */}\n              {visibleMessages.length > 0 && (\n                <div className=\"mt-3 border-t border-zinc-200 pt-2 dark:border-zinc-700\">\n                  <span className=\"font-mono text-[10px] text-zinc-400\">\n                    length: {visibleMessages.length}\n                  </span>\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <StepControls\n        currentStep={currentStep}\n        totalSteps={totalSteps}\n        onPrev={prev}\n        onNext={next}\n        onReset={reset}\n        isPlaying={isPlaying}\n        onToggleAutoPlay={toggleAutoPlay}\n        stepTitle={stepInfo.title}\n        stepDescription={stepInfo.desc}\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s02-tool-dispatch.tsx",
    "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\nimport { useSvgPalette } from \"@/hooks/useDarkMode\";\n\n// -- Tool definitions --\n\ninterface ToolDef {\n  name: string;\n  desc: string;\n  color: string;\n  activeColor: string;\n  darkColor: string;\n  darkActiveColor: string;\n}\n\nconst TOOLS: ToolDef[] = [\n  {\n    name: \"bash\",\n    desc: \"Execute shell commands\",\n    color: \"border-orange-300 bg-orange-50\",\n    activeColor: \"border-orange-500 bg-orange-100 ring-2 ring-orange-400\",\n    darkColor: \"dark:border-zinc-700 dark:bg-zinc-800/50\",\n    darkActiveColor: \"dark:border-orange-500 dark:bg-orange-950/40 dark:ring-orange-500\",\n  },\n  {\n    name: \"read_file\",\n    desc: \"Read file contents\",\n    color: \"border-sky-300 bg-sky-50\",\n    activeColor: \"border-sky-500 bg-sky-100 ring-2 ring-sky-400\",\n    darkColor: \"dark:border-zinc-700 dark:bg-zinc-800/50\",\n    darkActiveColor: \"dark:border-sky-500 dark:bg-sky-950/40 dark:ring-sky-500\",\n  },\n  {\n    name: \"write_file\",\n    desc: \"Create or overwrite a file\",\n    color: \"border-emerald-300 bg-emerald-50\",\n    activeColor: \"border-emerald-500 bg-emerald-100 ring-2 ring-emerald-400\",\n    darkColor: \"dark:border-zinc-700 dark:bg-zinc-800/50\",\n    darkActiveColor: \"dark:border-emerald-500 dark:bg-emerald-950/40 dark:ring-emerald-500\",\n  },\n  {\n    name: \"edit_file\",\n    desc: \"Apply targeted edits\",\n    color: \"border-violet-300 bg-violet-50\",\n    activeColor: \"border-violet-500 bg-violet-100 ring-2 ring-violet-400\",\n    darkColor: \"dark:border-zinc-700 dark:bg-zinc-800/50\",\n    darkActiveColor: \"dark:border-violet-500 dark:bg-violet-950/40 dark:ring-violet-500\",\n  },\n];\n\n// Per-step: which tool index is active (-1 = none, 4 = all)\nconst ACTIVE_TOOL_PER_STEP: number[] = [-1, 0, 1, 2, 3, 4];\n\n// Incoming request JSON per step\nconst REQUEST_PER_STEP: (string | null)[] = [\n  null,\n  '{ name: \"bash\", input: { cmd: \"ls -la\" } }',\n  '{ name: \"read_file\", input: { path: \"src/auth.ts\" } }',\n  '{ name: \"write_file\", input: { path: \"config.json\" } }',\n  '{ name: \"edit_file\", input: { path: \"index.ts\" } }',\n  null,\n];\n\n// Step annotations\nconst STEP_INFO = [\n  { title: \"The Dispatch Map\", desc: \"A dictionary maps tool names to handler functions. The loop code never changes.\" },\n  { title: \"Route: bash\", desc: \"tool_call.name -> handlers['bash'](input). Name-based routing.\" },\n  { title: \"Route: read_file\", desc: \"Same pattern, different handler. Validate input, execute, return result.\" },\n  { title: \"Route: write_file\", desc: \"Every tool returns a tool_result that goes back into messages[].\" },\n  { title: \"Route: edit_file\", desc: \"Adding a new tool = adding one entry to the dispatch map.\" },\n  { title: \"The Key Insight\", desc: \"The while loop stays the same. You only grow the dispatch map. That's it.\" },\n];\n\n// SVG layout constants\nconst SVG_WIDTH = 600;\nconst SVG_HEIGHT = 320;\nconst DISPATCHER_X = SVG_WIDTH / 2;\nconst DISPATCHER_Y = 60;\nconst DISPATCHER_W = 160;\nconst DISPATCHER_H = 50;\nconst CARD_Y = 230;\nconst CARD_W = 110;\nconst CARD_H = 65;\nconst CARD_GAP = 20;\n\nfunction getCardX(index: number): number {\n  const totalWidth = TOOLS.length * CARD_W + (TOOLS.length - 1) * CARD_GAP;\n  const startX = (SVG_WIDTH - totalWidth) / 2;\n  return startX + index * (CARD_W + CARD_GAP) + CARD_W / 2;\n}\n\nexport default function ToolDispatch({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: 6, autoPlayInterval: 2500 });\n\n  const palette = useSvgPalette();\n  const activeToolIdx = ACTIVE_TOOL_PER_STEP[currentStep];\n  const request = REQUEST_PER_STEP[currentStep];\n  const stepInfo = STEP_INFO[currentStep];\n  const isAllActive = activeToolIdx === 4;\n\n  return (\n    <section className=\"min-h-[500px] space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Tool Dispatch Map\"}\n      </h2>\n\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\">\n        {/* Incoming request display */}\n        <div className=\"mb-4 flex min-h-[32px] items-center gap-2\">\n          <span className=\"shrink-0 text-xs font-medium text-zinc-500 dark:text-zinc-400\">\n            Incoming:\n          </span>\n          <AnimatePresence mode=\"wait\">\n            {request && (\n              <motion.code\n                key={request}\n                initial={{ opacity: 0, y: -8 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 8 }}\n                transition={{ duration: 0.3 }}\n                className=\"rounded bg-blue-100 px-2.5 py-1 font-mono text-xs font-medium text-blue-800 dark:bg-blue-900/40 dark:text-blue-300\"\n              >\n                {request}\n              </motion.code>\n            )}\n            {!request && currentStep === 0 && (\n              <motion.span\n                key=\"waiting\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 0.6 }}\n                className=\"text-xs text-zinc-400 dark:text-zinc-600\"\n              >\n                waiting for tool_call...\n              </motion.span>\n            )}\n            {isAllActive && (\n              <motion.span\n                key=\"all-routes\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                className=\"text-xs font-medium text-emerald-600 dark:text-emerald-400\"\n              >\n                All routes active\n              </motion.span>\n            )}\n          </AnimatePresence>\n        </div>\n\n        {/* SVG dispatch diagram */}\n        <svg\n          viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}\n          className=\"w-full rounded-md border border-zinc-100 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950\"\n          style={{ minHeight: 240 }}\n        >\n          <defs>\n            <filter id=\"dispatch-glow\">\n              <feDropShadow dx=\"0\" dy=\"0\" stdDeviation=\"4\" floodColor=\"#3b82f6\" floodOpacity=\"0.6\" />\n            </filter>\n            <filter id=\"card-glow-orange\">\n              <feDropShadow dx=\"0\" dy=\"0\" stdDeviation=\"3\" floodColor=\"#f97316\" floodOpacity=\"0.6\" />\n            </filter>\n            <filter id=\"card-glow-sky\">\n              <feDropShadow dx=\"0\" dy=\"0\" stdDeviation=\"3\" floodColor=\"#0ea5e9\" floodOpacity=\"0.6\" />\n            </filter>\n            <filter id=\"card-glow-emerald\">\n              <feDropShadow dx=\"0\" dy=\"0\" stdDeviation=\"3\" floodColor=\"#10b981\" floodOpacity=\"0.6\" />\n            </filter>\n            <filter id=\"card-glow-violet\">\n              <feDropShadow dx=\"0\" dy=\"0\" stdDeviation=\"3\" floodColor=\"#8b5cf6\" floodOpacity=\"0.6\" />\n            </filter>\n            <marker\n              id=\"dispatch-arrow\"\n              markerWidth=\"8\"\n              markerHeight=\"6\"\n              refX=\"8\"\n              refY=\"3\"\n              orient=\"auto\"\n            >\n              <polygon points=\"0 0, 8 3, 0 6\" fill={palette.activeEdgeStroke} />\n            </marker>\n            <marker\n              id=\"dispatch-arrow-dim\"\n              markerWidth=\"8\"\n              markerHeight=\"6\"\n              refX=\"8\"\n              refY=\"3\"\n              orient=\"auto\"\n            >\n              <polygon points=\"0 0, 8 3, 0 6\" fill={palette.arrowFill} />\n            </marker>\n          </defs>\n\n          {/* Dispatcher box */}\n          <motion.rect\n            x={DISPATCHER_X - DISPATCHER_W / 2}\n            y={DISPATCHER_Y - DISPATCHER_H / 2}\n            width={DISPATCHER_W}\n            height={DISPATCHER_H}\n            rx={10}\n            strokeWidth={2}\n            animate={{\n              fill: currentStep > 0 ? palette.activeNodeFill : palette.nodeFill,\n              stroke: currentStep > 0 ? palette.activeNodeStroke : palette.nodeStroke,\n            }}\n            filter={currentStep > 0 ? \"url(#dispatch-glow)\" : \"none\"}\n            transition={{ duration: 0.4 }}\n          />\n          <motion.text\n            x={DISPATCHER_X}\n            y={DISPATCHER_Y + 1}\n            textAnchor=\"middle\"\n            dominantBaseline=\"middle\"\n            fontSize={13}\n            fontWeight={700}\n            fontFamily=\"monospace\"\n            animate={{ fill: currentStep > 0 ? palette.activeNodeText : palette.nodeText }}\n            transition={{ duration: 0.4 }}\n          >\n            dispatch(name)\n          </motion.text>\n\n          {/* Connection lines from dispatcher to each tool card */}\n          {TOOLS.map((tool, i) => {\n            const cardX = getCardX(i);\n            const isActive = isAllActive || i === activeToolIdx;\n            const lineColor = isActive ? palette.activeEdgeStroke : palette.edgeStroke;\n\n            return (\n              <motion.line\n                key={`line-${tool.name}`}\n                x1={DISPATCHER_X}\n                y1={DISPATCHER_Y + DISPATCHER_H / 2}\n                x2={cardX}\n                y2={CARD_Y - CARD_H / 2}\n                strokeWidth={isActive ? 2.5 : 1.5}\n                markerEnd={isActive ? \"url(#dispatch-arrow)\" : \"url(#dispatch-arrow-dim)\"}\n                animate={{ stroke: lineColor, strokeWidth: isActive ? 2.5 : 1.5 }}\n                transition={{ duration: 0.4 }}\n              />\n            );\n          })}\n\n          {/* Tool cards */}\n          {TOOLS.map((tool, i) => {\n            const cardX = getCardX(i);\n            const isActive = isAllActive || i === activeToolIdx;\n            const glowFilters = [\n              \"url(#card-glow-orange)\",\n              \"url(#card-glow-sky)\",\n              \"url(#card-glow-emerald)\",\n              \"url(#card-glow-violet)\",\n            ];\n            const activeColors = [\"#f97316\", \"#0ea5e9\", \"#10b981\", \"#8b5cf6\"];\n            const activeBorders = [\"#ea580c\", \"#0284c7\", \"#059669\", \"#7c3aed\"];\n\n            return (\n              <g key={tool.name}>\n                <motion.rect\n                  x={cardX - CARD_W / 2}\n                  y={CARD_Y - CARD_H / 2}\n                  width={CARD_W}\n                  height={CARD_H}\n                  rx={8}\n                  strokeWidth={2}\n                  animate={{\n                    fill: isActive ? activeColors[i] : palette.nodeFill,\n                    stroke: isActive ? activeBorders[i] : palette.nodeStroke,\n                  }}\n                  filter={isActive ? glowFilters[i] : \"none\"}\n                  transition={{ duration: 0.4 }}\n                />\n                <motion.text\n                  x={cardX}\n                  y={CARD_Y - 8}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"middle\"\n                  fontSize={11}\n                  fontWeight={700}\n                  fontFamily=\"monospace\"\n                  animate={{ fill: isActive ? \"#ffffff\" : palette.nodeText }}\n                  transition={{ duration: 0.4 }}\n                >\n                  {tool.name}\n                </motion.text>\n                <motion.text\n                  x={cardX}\n                  y={CARD_Y + 12}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"middle\"\n                  fontSize={8}\n                  fontFamily=\"sans-serif\"\n                  animate={{ fill: isActive ? \"rgba(255,255,255,0.8)\" : palette.labelFill }}\n                  transition={{ duration: 0.4 }}\n                >\n                  {tool.desc}\n                </motion.text>\n              </g>\n            );\n          })}\n\n          {/* \"+\" extensibility indicator on last step */}\n          {isAllActive && (\n            <motion.g\n              initial={{ opacity: 0, scale: 0.5 }}\n              animate={{ opacity: 1, scale: 1 }}\n              transition={{ delay: 0.3, duration: 0.4 }}\n            >\n              <circle\n                cx={getCardX(3) + CARD_W / 2 + 30}\n                cy={CARD_Y}\n                r={16}\n                fill=\"none\"\n                stroke=\"#3b82f6\"\n                strokeWidth={2}\n                strokeDasharray=\"4 3\"\n              />\n              <text\n                x={getCardX(3) + CARD_W / 2 + 30}\n                y={CARD_Y + 1}\n                textAnchor=\"middle\"\n                dominantBaseline=\"middle\"\n                fontSize={18}\n                fontWeight={700}\n                fill=\"#3b82f6\"\n              >\n                +\n              </text>\n            </motion.g>\n          )}\n        </svg>\n\n        {/* Code snippet below the diagram */}\n        <div className=\"mt-3 rounded-md bg-zinc-100 px-3 py-2 dark:bg-zinc-800\">\n          <code className=\"block font-mono text-[11px] leading-relaxed text-zinc-600 dark:text-zinc-300\">\n            <span className=\"text-blue-600 dark:text-blue-400\">const</span> handlers = {\"{\"}\n            {TOOLS.map((tool, i) => {\n              const isActive = isAllActive || i === activeToolIdx;\n              return (\n                <motion.span\n                  key={tool.name}\n                  animate={{\n                    color: isActive ? \"#3b82f6\" : undefined,\n                    fontWeight: isActive ? 700 : 400,\n                  }}\n                  className=\"text-zinc-600 dark:text-zinc-300\"\n                >\n                  {\" \"}{tool.name},\n                </motion.span>\n              );\n            })}\n            {\" }{\"}{\"}\"};\n          </code>\n        </div>\n      </div>\n\n      <StepControls\n        currentStep={currentStep}\n        totalSteps={totalSteps}\n        onPrev={prev}\n        onNext={next}\n        onReset={reset}\n        isPlaying={isPlaying}\n        onToggleAutoPlay={toggleAutoPlay}\n        stepTitle={stepInfo.title}\n        stepDescription={stepInfo.desc}\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s03-todo-write.tsx",
    "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\n\n// -- Task definitions --\n\ntype TaskStatus = \"pending\" | \"in_progress\" | \"done\";\n\ninterface Task {\n  id: number;\n  label: string;\n  status: TaskStatus;\n}\n\n// Snapshot of all 4 tasks at each step\nconst TASK_STATES: Task[][] = [\n  // Step 0: all pending\n  [\n    { id: 1, label: \"Write auth tests\", status: \"pending\" },\n    { id: 2, label: \"Fix mobile layout\", status: \"pending\" },\n    { id: 3, label: \"Add error handling\", status: \"pending\" },\n    { id: 4, label: \"Update config loader\", status: \"pending\" },\n  ],\n  // Step 1: still all pending (idle round 1)\n  [\n    { id: 1, label: \"Write auth tests\", status: \"pending\" },\n    { id: 2, label: \"Fix mobile layout\", status: \"pending\" },\n    { id: 3, label: \"Add error handling\", status: \"pending\" },\n    { id: 4, label: \"Update config loader\", status: \"pending\" },\n  ],\n  // Step 2: still all pending (idle round 2)\n  [\n    { id: 1, label: \"Write auth tests\", status: \"pending\" },\n    { id: 2, label: \"Fix mobile layout\", status: \"pending\" },\n    { id: 3, label: \"Add error handling\", status: \"pending\" },\n    { id: 4, label: \"Update config loader\", status: \"pending\" },\n  ],\n  // Step 3: NAG fires, task 1 moves to in_progress\n  [\n    { id: 1, label: \"Write auth tests\", status: \"in_progress\" },\n    { id: 2, label: \"Fix mobile layout\", status: \"pending\" },\n    { id: 3, label: \"Add error handling\", status: \"pending\" },\n    { id: 4, label: \"Update config loader\", status: \"pending\" },\n  ],\n  // Step 4: task 1 done\n  [\n    { id: 1, label: \"Write auth tests\", status: \"done\" },\n    { id: 2, label: \"Fix mobile layout\", status: \"pending\" },\n    { id: 3, label: \"Add error handling\", status: \"pending\" },\n    { id: 4, label: \"Update config loader\", status: \"pending\" },\n  ],\n  // Step 5: task 2 self-directed to in_progress\n  [\n    { id: 1, label: \"Write auth tests\", status: \"done\" },\n    { id: 2, label: \"Fix mobile layout\", status: \"in_progress\" },\n    { id: 3, label: \"Add error handling\", status: \"pending\" },\n    { id: 4, label: \"Update config loader\", status: \"pending\" },\n  ],\n  // Step 6: tasks 2,3 done, task 4 in_progress\n  [\n    { id: 1, label: \"Write auth tests\", status: \"done\" },\n    { id: 2, label: \"Fix mobile layout\", status: \"done\" },\n    { id: 3, label: \"Add error handling\", status: \"done\" },\n    { id: 4, label: \"Update config loader\", status: \"in_progress\" },\n  ],\n];\n\n// Nag timer value at each step (out of 3)\nconst NAG_TIMER_PER_STEP = [0, 1, 2, 3, 0, 0, 0];\nconst NAG_THRESHOLD = 3;\n\n// Whether the nag fires at this step\nconst NAG_FIRES_PER_STEP = [false, false, false, true, false, false, false];\n\n// Step annotations\nconst STEP_INFO = [\n  { title: \"The Plan\", desc: \"TodoWrite gives the model a visible plan. All tasks start as pending.\" },\n  { title: \"Round 1 -- Idle\", desc: \"The model does work but doesn't touch its todos. The nag counter increments.\" },\n  { title: \"Round 2 -- Still Idle\", desc: \"Two rounds without progress. Pressure builds.\" },\n  { title: \"NAG!\", desc: \"Threshold reached! System message injected: 'You have pending tasks. Pick one up now!'\" },\n  { title: \"Task Complete\", desc: \"The model completes the task. Timer stays at 0 -- working on todos resets the counter.\" },\n  { title: \"Self-Directed\", desc: \"Once the model learns the pattern, it picks up tasks voluntarily.\" },\n  { title: \"Mission Accomplished\", desc: \"Visible plan + nag pressure = reliable task completion.\" },\n];\n\n// -- Column component --\n\nfunction KanbanColumn({\n  title,\n  tasks,\n  accentClass,\n  headerBg,\n}: {\n  title: string;\n  tasks: Task[];\n  accentClass: string;\n  headerBg: string;\n}) {\n  return (\n    <div className=\"flex min-h-[280px] flex-1 flex-col rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900\">\n      <div\n        className={`rounded-t-lg px-3 py-2 text-center text-xs font-bold uppercase tracking-wider ${headerBg}`}\n      >\n        {title}\n        <span className={`ml-1.5 inline-flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-bold ${accentClass}`}>\n          {tasks.length}\n        </span>\n      </div>\n      <div className=\"flex flex-1 flex-col gap-2 p-2\">\n        <AnimatePresence mode=\"popLayout\">\n          {tasks.map((task) => (\n            <TaskCard key={task.id} task={task} />\n          ))}\n        </AnimatePresence>\n        {tasks.length === 0 && (\n          <div className=\"flex flex-1 items-center justify-center text-xs text-zinc-400 dark:text-zinc-600\">\n            --\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\n// -- Task card --\n\nfunction TaskCard({ task }: { task: Task }) {\n  const statusStyles: Record<TaskStatus, string> = {\n    pending: \"bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400\",\n    in_progress: \"bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300\",\n    done: \"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300\",\n  };\n\n  const borderStyles: Record<TaskStatus, string> = {\n    pending: \"border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-800\",\n    in_progress: \"border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950/30\",\n    done: \"border-emerald-300 bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-950/30\",\n  };\n\n  return (\n    <motion.div\n      layout\n      layoutId={`task-${task.id}`}\n      initial={{ opacity: 0, scale: 0.8 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={{ opacity: 0, scale: 0.8 }}\n      transition={{ type: \"spring\", stiffness: 400, damping: 30 }}\n      className={`rounded-md border p-2.5 ${borderStyles[task.status]}`}\n    >\n      <div className=\"mb-1.5 flex items-center justify-between\">\n        <span className=\"font-mono text-[10px] text-zinc-400 dark:text-zinc-500\">\n          #{task.id}\n        </span>\n        <span\n          className={`rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide ${statusStyles[task.status]}`}\n        >\n          {task.status.replace(\"_\", \" \")}\n        </span>\n      </div>\n      <div className=\"text-xs font-medium text-zinc-700 dark:text-zinc-300\">\n        {task.label}\n      </div>\n    </motion.div>\n  );\n}\n\n// -- Nag gauge --\n\nfunction NagGauge({ value, max, firing }: { value: number; max: number; firing: boolean }) {\n  const pct = Math.min((value / max) * 100, 100);\n\n  const barColor =\n    value === 0\n      ? \"bg-zinc-300 dark:bg-zinc-600\"\n      : value === 1\n        ? \"bg-green-400 dark:bg-green-500\"\n        : value === 2\n          ? \"bg-yellow-400 dark:bg-yellow-500\"\n          : \"bg-red-500 dark:bg-red-500\";\n\n  return (\n    <div className=\"space-y-1\">\n      <div className=\"flex items-center justify-between\">\n        <span className=\"text-xs font-medium text-zinc-600 dark:text-zinc-300\">\n          Nag Timer\n        </span>\n        <span className=\"font-mono text-xs text-zinc-500 dark:text-zinc-400\">\n          {value}/{max}\n        </span>\n      </div>\n      <div className=\"relative h-4 w-full overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-700\">\n        <motion.div\n          className={`absolute inset-y-0 left-0 rounded-full ${barColor}`}\n          initial={{ width: \"0%\" }}\n          animate={{\n            width: `${pct}%`,\n            ...(firing ? { scale: [1, 1.05, 1] } : {}),\n          }}\n          transition={{\n            width: { duration: 0.5, ease: \"easeOut\" },\n            scale: { duration: 0.3, repeat: 2 },\n          }}\n        />\n        {firing && (\n          <motion.div\n            className=\"absolute inset-0 rounded-full border-2 border-red-500\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: [0, 1, 0, 1, 0] }}\n            transition={{ duration: 1 }}\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n\n// -- Main component --\n\nexport default function TodoWrite({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });\n\n  const tasks = TASK_STATES[currentStep];\n  const nagValue = NAG_TIMER_PER_STEP[currentStep];\n  const nagFires = NAG_FIRES_PER_STEP[currentStep];\n  const stepInfo = STEP_INFO[currentStep];\n\n  const pendingTasks = tasks.filter((t) => t.status === \"pending\");\n  const inProgressTasks = tasks.filter((t) => t.status === \"in_progress\");\n  const doneTasks = tasks.filter((t) => t.status === \"done\");\n\n  return (\n    <section className=\"min-h-[500px] space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"TodoWrite Nag System\"}\n      </h2>\n\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\">\n        {/* Nag gauge + nag message */}\n        <div className=\"mb-4 space-y-2\">\n          <NagGauge value={nagValue} max={NAG_THRESHOLD} firing={nagFires} />\n\n          <AnimatePresence>\n            {nagFires && (\n              <motion.div\n                initial={{ opacity: 0, y: -8, height: 0 }}\n                animate={{ opacity: 1, y: 0, height: \"auto\" }}\n                exit={{ opacity: 0, y: -8, height: 0 }}\n                className=\"rounded-md border border-red-300 bg-red-50 px-3 py-2 text-center text-xs font-bold text-red-700 dark:border-red-700 dark:bg-red-950/30 dark:text-red-300\"\n              >\n                SYSTEM: \"You have pending tasks. Pick one up now!\"\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n\n        {/* Kanban board */}\n        <div className=\"flex gap-3\">\n          <KanbanColumn\n            title=\"Pending\"\n            tasks={pendingTasks}\n            accentClass=\"bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300\"\n            headerBg=\"bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300\"\n          />\n          <KanbanColumn\n            title=\"In Progress\"\n            tasks={inProgressTasks}\n            accentClass=\"bg-amber-200 text-amber-700 dark:bg-amber-800 dark:text-amber-200\"\n            headerBg=\"bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300\"\n          />\n          <KanbanColumn\n            title=\"Done\"\n            tasks={doneTasks}\n            accentClass=\"bg-emerald-200 text-emerald-700 dark:bg-emerald-800 dark:text-emerald-200\"\n            headerBg=\"bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300\"\n          />\n        </div>\n\n        {/* Progress summary */}\n        <div className=\"mt-3 flex items-center justify-between rounded-md bg-zinc-100 px-3 py-2 dark:bg-zinc-800\">\n          <span className=\"font-mono text-[11px] text-zinc-500 dark:text-zinc-400\">\n            Progress: {doneTasks.length}/{tasks.length} complete\n          </span>\n          <div className=\"flex gap-0.5\">\n            {tasks.map((t) => (\n              <div\n                key={t.id}\n                className={`h-2 w-6 rounded-sm ${\n                  t.status === \"done\"\n                    ? \"bg-emerald-500\"\n                    : t.status === \"in_progress\"\n                      ? \"bg-amber-400\"\n                      : \"bg-zinc-300 dark:bg-zinc-600\"\n                }`}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n\n      <StepControls\n        currentStep={currentStep}\n        totalSteps={totalSteps}\n        onPrev={prev}\n        onNext={next}\n        onReset={reset}\n        isPlaying={isPlaying}\n        onToggleAutoPlay={toggleAutoPlay}\n        stepTitle={stepInfo.title}\n        stepDescription={stepInfo.desc}\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s04-subagent.tsx",
    "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\n\ninterface MessageBlock {\n  id: string;\n  label: string;\n  color: string;\n}\n\nconst PARENT_BASE_MESSAGES: MessageBlock[] = [\n  { id: \"p1\", label: \"user: Build login + tests\", color: \"bg-blue-500\" },\n  { id: \"p2\", label: \"assistant: Planning approach...\", color: \"bg-zinc-600\" },\n  { id: \"p3\", label: \"tool_result: project structure\", color: \"bg-emerald-500\" },\n];\n\nconst TASK_PROMPT: MessageBlock = {\n  id: \"task\",\n  label: \"task: Write unit tests for auth\",\n  color: \"bg-purple-500\",\n};\n\nconst CHILD_WORK_MESSAGES: MessageBlock[] = [\n  { id: \"c1\", label: \"tool_use: read auth.ts\", color: \"bg-amber-500\" },\n  { id: \"c2\", label: \"tool_use: write test.ts\", color: \"bg-amber-500\" },\n];\n\nconst SUMMARY_BLOCK: MessageBlock = {\n  id: \"summary\",\n  label: \"summary: 3 tests written, all passing\",\n  color: \"bg-teal-500\",\n};\n\nconst STEPS = [\n  {\n    title: \"Parent Context\",\n    description:\n      \"The parent agent has accumulated messages from the conversation.\",\n  },\n  {\n    title: \"Spawn Subagent\",\n    description:\n      \"Task tool creates a child with fresh messages[]. Only the task description is passed.\",\n  },\n  {\n    title: \"Independent Work\",\n    description:\n      \"The child has its own context. It doesn't see the parent's history.\",\n  },\n  {\n    title: \"Compress Result\",\n    description:\n      \"The child's full conversation compresses into one summary.\",\n  },\n  {\n    title: \"Return Summary\",\n    description:\n      \"Only the summary returns. The child's full context is discarded.\",\n  },\n  {\n    title: \"Clean Context\",\n    description:\n      \"The parent gets a clean summary without context bloat. This is fresh-context isolation via messages[].\",\n  },\n];\n\nexport default function SubagentIsolation({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });\n\n  // Derive what to show in each container based on step\n  const parentMessages: MessageBlock[] = (() => {\n    const base = [...PARENT_BASE_MESSAGES];\n    if (currentStep >= 5) {\n      base.push(SUMMARY_BLOCK);\n    }\n    return base;\n  })();\n\n  const childMessages: MessageBlock[] = (() => {\n    if (currentStep < 1) return [];\n    if (currentStep === 1) return [TASK_PROMPT];\n    if (currentStep === 2) return [TASK_PROMPT, ...CHILD_WORK_MESSAGES];\n    if (currentStep === 3) return [SUMMARY_BLOCK];\n    return currentStep >= 4 ? [TASK_PROMPT, ...CHILD_WORK_MESSAGES] : [];\n  })();\n\n  const showChildEmpty = currentStep === 0;\n  const showArcToChild = currentStep === 1;\n  const showCompression = currentStep === 3;\n  const showArcToParent = currentStep === 4;\n  const childDiscarded = currentStep >= 4;\n  const childFaded = currentStep >= 4;\n\n  return (\n    <section className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Subagent Context Isolation\"}\n      </h2>\n\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900\"\n        style={{ minHeight: 500 }}\n      >\n        {/* Main layout: two containers side by side */}\n        <div className=\"relative flex gap-4\" style={{ minHeight: 340 }}>\n          {/* Parent Process Container */}\n          <div className=\"flex-1 rounded-xl border-2 border-blue-300 bg-blue-50/50 p-4 dark:border-blue-700 dark:bg-blue-950/20\">\n            <div className=\"mb-3 flex items-center gap-2\">\n              <div className=\"h-3 w-3 rounded-full bg-blue-500\" />\n              <span className=\"text-sm font-bold text-blue-700 dark:text-blue-300\">\n                Parent Process\n              </span>\n            </div>\n            <div className=\"mb-2 font-mono text-xs text-zinc-400\">\n              messages[]\n            </div>\n            <div className=\"space-y-2\">\n              <AnimatePresence>\n                {parentMessages.map((msg, i) => (\n                  <motion.div\n                    key={msg.id}\n                    initial={{ opacity: 0, x: -12 }}\n                    animate={{ opacity: 1, x: 0 }}\n                    exit={{ opacity: 0, x: -12 }}\n                    transition={{ duration: 0.4, delay: msg.id === \"summary\" ? 0.3 : 0 }}\n                    className={`rounded-lg px-3 py-2 text-xs font-medium text-white shadow-sm ${msg.color}`}\n                  >\n                    {msg.label}\n                  </motion.div>\n                ))}\n              </AnimatePresence>\n            </div>\n            {currentStep >= 5 && (\n              <motion.div\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                transition={{ delay: 0.5 }}\n                className=\"mt-3 rounded border border-blue-200 bg-white/60 px-2 py-1 text-center text-xs text-blue-600 dark:border-blue-700 dark:bg-blue-950/30 dark:text-blue-300\"\n              >\n                3 original + 1 summary = clean context\n              </motion.div>\n            )}\n          </div>\n\n          {/* Isolation Wall */}\n          <div className=\"flex flex-col items-center justify-center gap-2\">\n            <div className=\"h-full w-px border-l-2 border-dashed border-zinc-300 dark:border-zinc-600\" />\n            <motion.div\n              animate={{\n                opacity: currentStep >= 1 && currentStep <= 4 ? 1 : 0.4,\n              }}\n              className=\"rounded bg-zinc-200 px-2 py-1 text-center font-mono text-[10px] text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400\"\n              style={{ writingMode: \"vertical-rl\", textOrientation: \"mixed\" }}\n            >\n              ISOLATION\n            </motion.div>\n            <div className=\"h-full w-px border-l-2 border-dashed border-zinc-300 dark:border-zinc-600\" />\n          </div>\n\n          {/* Child Process Container */}\n          <div\n            className={`flex-1 rounded-xl border-2 p-4 transition-colors duration-300 ${\n              showChildEmpty\n                ? \"border-dashed border-zinc-300 bg-zinc-50/50 dark:border-zinc-600 dark:bg-zinc-800/30\"\n                : childDiscarded\n                  ? \"border-zinc-300 bg-zinc-100/50 dark:border-zinc-600 dark:bg-zinc-800/40\"\n                  : \"border-purple-300 bg-purple-50/50 dark:border-purple-700 dark:bg-purple-950/20\"\n            }`}\n          >\n            <div className=\"mb-3 flex items-center gap-2\">\n              <div\n                className={`h-3 w-3 rounded-full ${\n                  showChildEmpty\n                    ? \"bg-zinc-300 dark:bg-zinc-600\"\n                    : childDiscarded\n                      ? \"bg-zinc-400 dark:bg-zinc-500\"\n                      : \"bg-purple-500\"\n                }`}\n              />\n              <span\n                className={`text-sm font-bold ${\n                  showChildEmpty\n                    ? \"text-zinc-400 dark:text-zinc-500\"\n                    : childDiscarded\n                      ? \"text-zinc-400 dark:text-zinc-500\"\n                      : \"text-purple-700 dark:text-purple-300\"\n                }`}\n              >\n                Child Process\n              </span>\n            </div>\n            <div className=\"mb-2 font-mono text-xs text-zinc-400\">\n              messages[] (fresh)\n            </div>\n\n            {showChildEmpty && (\n              <motion.div\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                className=\"flex h-24 items-center justify-center rounded-lg border border-dashed border-zinc-200 dark:border-zinc-700\"\n              >\n                <span className=\"text-xs text-zinc-400\">\n                  not yet spawned\n                </span>\n              </motion.div>\n            )}\n\n            <div className=\"space-y-2\">\n              <AnimatePresence>\n                {childMessages.map((msg) => (\n                  <motion.div\n                    key={msg.id + \"-child\"}\n                    initial={{ opacity: 0, x: 12 }}\n                    animate={{ opacity: childFaded ? 0.3 : 1, x: 0 }}\n                    exit={{ opacity: 0, scale: 0.8 }}\n                    transition={{ duration: 0.4 }}\n                    className={`rounded-lg px-3 py-2 text-xs font-medium text-white shadow-sm ${msg.color}`}\n                  >\n                    {msg.label}\n                  </motion.div>\n                ))}\n              </AnimatePresence>\n            </div>\n\n            {showCompression && (\n              <motion.div\n                initial={{ opacity: 0, scale: 0.9 }}\n                animate={{ opacity: 1, scale: 1 }}\n                className=\"mt-3 rounded border border-amber-300 bg-amber-50 px-2 py-1 text-center text-xs text-amber-700 dark:border-amber-600 dark:bg-amber-900/20 dark:text-amber-300\"\n              >\n                Compressing full context into summary...\n              </motion.div>\n            )}\n\n            {childDiscarded && (\n              <motion.div\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                className=\"mt-3 rounded border border-red-200 bg-red-50 px-2 py-1 text-center text-xs text-red-500 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400\"\n              >\n                context discarded\n              </motion.div>\n            )}\n          </div>\n\n          {/* Animated arcs: task prompt going from parent to child */}\n          <AnimatePresence>\n            {showArcToChild && (\n              <motion.div\n                initial={{ opacity: 0, x: \"20%\", y: \"-10%\" }}\n                animate={{ opacity: 1, x: \"55%\", y: \"-10%\" }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 1.0, ease: \"easeInOut\" }}\n                className=\"pointer-events-none absolute left-0 top-0\"\n                style={{ zIndex: 10 }}\n              >\n                <div className=\"rounded-lg bg-purple-500 px-3 py-1.5 text-xs font-medium text-white shadow-lg\">\n                  task prompt\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n\n          <AnimatePresence>\n            {showArcToParent && (\n              <motion.div\n                initial={{ opacity: 0, x: \"75%\", y: \"60%\" }}\n                animate={{ opacity: 1, x: \"15%\", y: \"60%\" }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 1.0, ease: \"easeInOut\" }}\n                className=\"pointer-events-none absolute left-0 top-0\"\n                style={{ zIndex: 10 }}\n              >\n                <div className=\"rounded-lg bg-teal-500 px-3 py-1.5 text-xs font-medium text-white shadow-lg\">\n                  summary\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n\n        {/* Step Controls */}\n        <div className=\"mt-6\">\n          <StepControls\n            currentStep={currentStep}\n            totalSteps={totalSteps}\n            onPrev={prev}\n            onNext={next}\n            onReset={reset}\n            isPlaying={isPlaying}\n            onToggleAutoPlay={toggleAutoPlay}\n            stepTitle={STEPS[currentStep].title}\n            stepDescription={STEPS[currentStep].description}\n          />\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s05-skill-loading.tsx",
    "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\n\ninterface SkillEntry {\n  name: string;\n  summary: string;\n  fullTokens: number;\n  content: string[];\n}\n\nconst SKILLS: SkillEntry[] = [\n  {\n    name: \"/commit\",\n    summary: \"Create git commits following repo conventions\",\n    fullTokens: 320,\n    content: [\n      \"1. Run git status + git diff to see changes\",\n      \"2. Analyze all staged changes and draft message\",\n      \"3. Create commit with Co-Authored-By trailer\",\n      \"4. Run git status after commit to verify\",\n    ],\n  },\n  {\n    name: \"/review-pr\",\n    summary: \"Review pull requests for bugs and style\",\n    fullTokens: 480,\n    content: [\n      \"1. Fetch PR diff via gh pr view\",\n      \"2. Analyze changes file by file for issues\",\n      \"3. Check for bugs, security, and style problems\",\n      \"4. Post review comments with gh pr review\",\n    ],\n  },\n  {\n    name: \"/test\",\n    summary: \"Run and analyze test suites\",\n    fullTokens: 290,\n    content: [\n      \"1. Detect test framework from package.json\",\n      \"2. Run test suite and capture output\",\n      \"3. Analyze failures and suggest fixes\",\n      \"4. Re-run after applying fixes\",\n    ],\n  },\n  {\n    name: \"/deploy\",\n    summary: \"Deploy application to target environment\",\n    fullTokens: 350,\n    content: [\n      \"1. Verify all tests pass before deploy\",\n      \"2. Build production bundle\",\n      \"3. Push to deployment target via CI\",\n      \"4. Verify health check on deployed URL\",\n    ],\n  },\n];\n\nconst TOKEN_STATES = [120, 120, 440, 440, 780, 780];\nconst MAX_TOKEN_DISPLAY = 1000;\n\nconst STEPS = [\n  {\n    title: \"Layer 1: Compact Summaries\",\n    description:\n      \"All skills are summarized in the system prompt. Compact, always present.\",\n  },\n  {\n    title: \"Skill Invocation\",\n    description:\n      'The model recognizes a skill invocation and triggers the Skill tool.',\n  },\n  {\n    title: \"Layer 2: Full Injection\",\n    description:\n      \"The full skill instructions are injected as a tool_result, not into the system prompt.\",\n  },\n  {\n    title: \"In Context Now\",\n    description:\n      \"The detailed instructions appear as if a tool returned them. The model follows them precisely.\",\n  },\n  {\n    title: \"Stack Skills\",\n    description:\n      \"Multiple skills can be loaded. Only summaries are permanent; full content comes and goes.\",\n  },\n  {\n    title: \"Two-Layer Architecture\",\n    description:\n      \"Layer 1: always present, tiny. Layer 2: loaded on demand, detailed. Elegant separation.\",\n  },\n];\n\nexport default function SkillLoading({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });\n\n  const tokenCount = TOKEN_STATES[currentStep];\n  const highlightedSkill = currentStep >= 1 && currentStep <= 3 ? 0 : currentStep >= 4 ? 1 : -1;\n  const showFirstContent = currentStep >= 2;\n  const showSecondContent = currentStep >= 4;\n  const firstContentFaded = currentStep >= 5;\n\n  return (\n    <section className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"On-Demand Skill Loading\"}\n      </h2>\n\n      <div\n        className=\"rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900\"\n        style={{ minHeight: 500 }}\n      >\n        <div className=\"flex gap-6\">\n          {/* Main content area */}\n          <div className=\"flex-1 space-y-4\">\n            {/* System Prompt Block */}\n            <div>\n              <div className=\"mb-2 flex items-center gap-2\">\n                <div className=\"h-2 w-2 rounded-full bg-zinc-400\" />\n                <span className=\"text-xs font-semibold text-zinc-600 dark:text-zinc-300\">\n                  System Prompt\n                </span>\n                <span className=\"rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-[10px] text-zinc-400 dark:bg-zinc-800\">\n                  always present\n                </span>\n              </div>\n              <div className=\"rounded-lg border border-zinc-300 bg-zinc-900 p-4 dark:border-zinc-600\">\n                <div className=\"mb-2 font-mono text-[10px] text-zinc-500\">\n                  # Available Skills\n                </div>\n                <div className=\"space-y-1.5\">\n                  {SKILLS.map((skill, i) => {\n                    const isHighlighted = i === highlightedSkill;\n                    return (\n                      <motion.div\n                        key={skill.name}\n                        animate={{\n                          boxShadow: isHighlighted\n                            ? \"0 0 12px 2px rgba(59, 130, 246, 0.5)\"\n                            : \"0 0 0 0px rgba(59, 130, 246, 0)\",\n                        }}\n                        transition={{ duration: 0.4 }}\n                        className={`rounded px-3 py-1.5 font-mono text-xs transition-colors ${\n                          isHighlighted\n                            ? \"bg-blue-900/60 text-blue-300\"\n                            : \"bg-zinc-800 text-zinc-400\"\n                        }`}\n                      >\n                        <span className=\"font-semibold text-zinc-200\">\n                          {skill.name}\n                        </span>\n                        {\" - \"}\n                        {skill.summary}\n                      </motion.div>\n                    );\n                  })}\n                </div>\n              </div>\n            </div>\n\n            {/* User invocation indicator */}\n            <AnimatePresence>\n              {currentStep === 1 && (\n                <motion.div\n                  initial={{ opacity: 0, y: -8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0 }}\n                  className=\"flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 dark:border-blue-800 dark:bg-blue-950/30\"\n                >\n                  <span className=\"text-xs text-blue-600 dark:text-blue-400\">\n                    User types:\n                  </span>\n                  <code className=\"rounded bg-blue-100 px-2 py-0.5 text-xs font-bold text-blue-800 dark:bg-blue-900/50 dark:text-blue-200\">\n                    /commit\n                  </code>\n                </motion.div>\n              )}\n              {currentStep === 4 && (\n                <motion.div\n                  initial={{ opacity: 0, y: -8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0 }}\n                  className=\"flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 dark:border-blue-800 dark:bg-blue-950/30\"\n                >\n                  <span className=\"text-xs text-blue-600 dark:text-blue-400\">\n                    User types:\n                  </span>\n                  <code className=\"rounded bg-blue-100 px-2 py-0.5 text-xs font-bold text-blue-800 dark:bg-blue-900/50 dark:text-blue-200\">\n                    /review-pr\n                  </code>\n                </motion.div>\n              )}\n            </AnimatePresence>\n\n            {/* Connecting arrow */}\n            <AnimatePresence>\n              {(showFirstContent || showSecondContent) && (\n                <motion.div\n                  initial={{ opacity: 0, scaleY: 0 }}\n                  animate={{ opacity: 1, scaleY: 1 }}\n                  exit={{ opacity: 0 }}\n                  className=\"flex justify-center\"\n                >\n                  <div className=\"flex flex-col items-center\">\n                    <div className=\"h-6 w-px bg-blue-400 dark:bg-blue-500\" />\n                    <div className=\"h-0 w-0 border-l-[5px] border-r-[5px] border-t-[6px] border-l-transparent border-r-transparent border-t-blue-400 dark:border-t-blue-500\" />\n                  </div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n\n            {/* Expanded Skill Content Blocks */}\n            <div className=\"space-y-3\">\n              <AnimatePresence>\n                {showFirstContent && (\n                  <motion.div\n                    initial={{ opacity: 0, height: 0 }}\n                    animate={{\n                      opacity: firstContentFaded ? 0.4 : 1,\n                      height: \"auto\",\n                    }}\n                    exit={{ opacity: 0, height: 0 }}\n                    transition={{ duration: 0.4 }}\n                    className=\"overflow-hidden\"\n                  >\n                    <div className=\"rounded-lg border-2 border-blue-300 bg-white p-4 dark:border-blue-700 dark:bg-zinc-800\">\n                      <div className=\"mb-2 flex items-center justify-between\">\n                        <div className=\"flex items-center gap-2\">\n                          <div className=\"h-2 w-2 rounded-full bg-blue-500\" />\n                          <span className=\"text-xs font-bold text-blue-700 dark:text-blue-300\">\n                            SKILL.md: /commit\n                          </span>\n                        </div>\n                        <span className=\"rounded bg-blue-100 px-1.5 py-0.5 font-mono text-[10px] text-blue-600 dark:bg-blue-900/40 dark:text-blue-300\">\n                          tool_result\n                        </span>\n                      </div>\n                      <div className=\"space-y-1\">\n                        {SKILLS[0].content.map((line, i) => (\n                          <motion.div\n                            key={i}\n                            initial={{ opacity: 0, x: -8 }}\n                            animate={{\n                              opacity: firstContentFaded ? 0.5 : 1,\n                              x: 0,\n                            }}\n                            transition={{ delay: i * 0.08 }}\n                            className=\"font-mono text-xs text-zinc-600 dark:text-zinc-300\"\n                          >\n                            {line}\n                          </motion.div>\n                        ))}\n                      </div>\n                    </div>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n\n              <AnimatePresence>\n                {showSecondContent && (\n                  <motion.div\n                    initial={{ opacity: 0, height: 0 }}\n                    animate={{ opacity: 1, height: \"auto\" }}\n                    exit={{ opacity: 0, height: 0 }}\n                    transition={{ duration: 0.4 }}\n                    className=\"overflow-hidden\"\n                  >\n                    <div className=\"rounded-lg border-2 border-purple-300 bg-white p-4 dark:border-purple-700 dark:bg-zinc-800\">\n                      <div className=\"mb-2 flex items-center justify-between\">\n                        <div className=\"flex items-center gap-2\">\n                          <div className=\"h-2 w-2 rounded-full bg-purple-500\" />\n                          <span className=\"text-xs font-bold text-purple-700 dark:text-purple-300\">\n                            SKILL.md: /review-pr\n                          </span>\n                        </div>\n                        <span className=\"rounded bg-purple-100 px-1.5 py-0.5 font-mono text-[10px] text-purple-600 dark:bg-purple-900/40 dark:text-purple-300\">\n                          tool_result\n                        </span>\n                      </div>\n                      <div className=\"space-y-1\">\n                        {SKILLS[1].content.map((line, i) => (\n                          <motion.div\n                            key={i}\n                            initial={{ opacity: 0, x: -8 }}\n                            animate={{ opacity: 1, x: 0 }}\n                            transition={{ delay: i * 0.08 }}\n                            className=\"font-mono text-xs text-zinc-600 dark:text-zinc-300\"\n                          >\n                            {line}\n                          </motion.div>\n                        ))}\n                      </div>\n                    </div>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </div>\n\n            {/* Mechanism annotation on step 3 */}\n            <AnimatePresence>\n              {currentStep === 3 && (\n                <motion.div\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  exit={{ opacity: 0 }}\n                  className=\"rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-300\"\n                >\n                  The Skill tool returns content as a tool_result message.\n                  The model sees it in context and follows the instructions.\n                  No system prompt bloat.\n                </motion.div>\n              )}\n            </AnimatePresence>\n\n            {/* Final overview label on step 5 */}\n            <AnimatePresence>\n              {currentStep === 5 && (\n                <motion.div\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  exit={{ opacity: 0 }}\n                  className=\"flex gap-3\"\n                >\n                  <div className=\"flex-1 rounded border border-zinc-200 bg-zinc-50 p-2 text-center dark:border-zinc-700 dark:bg-zinc-800\">\n                    <div className=\"text-[10px] font-semibold text-zinc-500 dark:text-zinc-400\">\n                      LAYER 1\n                    </div>\n                    <div className=\"text-xs text-zinc-600 dark:text-zinc-300\">\n                      Always present, ~120 tokens\n                    </div>\n                  </div>\n                  <div className=\"flex-1 rounded border border-blue-200 bg-blue-50 p-2 text-center dark:border-blue-700 dark:bg-blue-900/20\">\n                    <div className=\"text-[10px] font-semibold text-blue-500 dark:text-blue-400\">\n                      LAYER 2\n                    </div>\n                    <div className=\"text-xs text-blue-600 dark:text-blue-300\">\n                      On demand, ~300-500 tokens each\n                    </div>\n                  </div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n\n          {/* Token Gauge (vertical bar on the right) */}\n          <div className=\"flex w-16 flex-col items-center\">\n            <div className=\"mb-1 text-center font-mono text-[10px] text-zinc-400\">\n              Tokens\n            </div>\n            <div\n              className=\"relative w-8 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800\"\n              style={{ height: 300 }}\n            >\n              <motion.div\n                animate={{\n                  height: `${(tokenCount / MAX_TOKEN_DISPLAY) * 100}%`,\n                }}\n                transition={{ duration: 0.5 }}\n                className={`absolute bottom-0 w-full rounded-full ${\n                  tokenCount > 600\n                    ? \"bg-amber-500\"\n                    : tokenCount > 300\n                      ? \"bg-blue-500\"\n                      : \"bg-emerald-500\"\n                }`}\n              />\n            </div>\n            <motion.div\n              key={tokenCount}\n              initial={{ scale: 0.8 }}\n              animate={{ scale: 1 }}\n              className=\"mt-2 text-center font-mono text-xs font-semibold text-zinc-600 dark:text-zinc-300\"\n            >\n              {tokenCount}\n            </motion.div>\n          </div>\n        </div>\n\n        {/* Step Controls */}\n        <div className=\"mt-6\">\n          <StepControls\n            currentStep={currentStep}\n            totalSteps={totalSteps}\n            onPrev={prev}\n            onNext={next}\n            onReset={reset}\n            isPlaying={isPlaying}\n            onToggleAutoPlay={toggleAutoPlay}\n            stepTitle={STEPS[currentStep].title}\n            stepDescription={STEPS[currentStep].description}\n          />\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s06-context-compact.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\n\ntype BlockType = \"user\" | \"assistant\" | \"tool_result\";\n\ninterface ContextBlock {\n  id: string;\n  type: BlockType;\n  label: string;\n  tokens: number;\n}\n\nconst BLOCK_COLORS: Record<BlockType, string> = {\n  user: \"bg-blue-500\",\n  assistant: \"bg-zinc-500 dark:bg-zinc-600\",\n  tool_result: \"bg-emerald-500\",\n};\n\nconst BLOCK_LABELS: Record<BlockType, string> = {\n  user: \"USR\",\n  assistant: \"AST\",\n  tool_result: \"TRL\",\n};\n\nfunction generateBlocks(count: number, seed: number): ContextBlock[] {\n  const types: BlockType[] = [\"user\", \"assistant\", \"tool_result\"];\n  const blocks: ContextBlock[] = [];\n  for (let i = 0; i < count; i++) {\n    const typeIndex = (i + seed) % 3;\n    const type = types[typeIndex];\n    const tokens = type === \"tool_result\" ? 4000 + (i % 3) * 1000 : 1500 + (i % 4) * 500;\n    blocks.push({\n      id: `b-${seed}-${i}`,\n      type,\n      label: `${BLOCK_LABELS[type]} ${i + 1}`,\n      tokens,\n    });\n  }\n  return blocks;\n}\n\nconst MAX_TOKENS = 100000;\nconst WINDOW_HEIGHT = 350;\n\ninterface StepState {\n  blocks: { id: string; type: BlockType; label: string; heightPx: number; compressed?: boolean }[];\n  tokenCount: number;\n  fillPercent: number;\n  compressionLabel: string | null;\n}\n\nfunction computeStepState(step: number): StepState {\n  switch (step) {\n    case 0: {\n      const raw = generateBlocks(8, 0);\n      const tokenCount = 30000;\n      const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);\n      const blocks = raw.map((b) => ({\n        ...b,\n        heightPx: Math.max(16, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.3),\n      }));\n      return { blocks, tokenCount, fillPercent: 30, compressionLabel: null };\n    }\n    case 1: {\n      const raw = generateBlocks(16, 0);\n      const tokenCount = 60000;\n      const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);\n      const blocks = raw.map((b) => ({\n        ...b,\n        heightPx: Math.max(12, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.6),\n      }));\n      return { blocks, tokenCount, fillPercent: 60, compressionLabel: null };\n    }\n    case 2: {\n      const raw = generateBlocks(20, 0);\n      const tokenCount = 80000;\n      const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);\n      const blocks = raw.map((b) => ({\n        ...b,\n        heightPx: Math.max(10, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.8),\n      }));\n      return { blocks, tokenCount, fillPercent: 80, compressionLabel: null };\n    }\n    case 3: {\n      const raw = generateBlocks(20, 0);\n      const tokenCount = 60000;\n      const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);\n      const blocks = raw.map((b) => ({\n        ...b,\n        heightPx:\n          b.type === \"tool_result\"\n            ? 6\n            : Math.max(12, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.6),\n        compressed: b.type === \"tool_result\",\n      }));\n      return {\n        blocks,\n        tokenCount,\n        fillPercent: 60,\n        compressionLabel: \"MICRO-COMPACT\",\n      };\n    }\n    case 4: {\n      const raw = generateBlocks(24, 1);\n      const tokenCount = 85000;\n      const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);\n      const blocks = raw.map((b) => ({\n        ...b,\n        heightPx: Math.max(10, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.85),\n      }));\n      return { blocks, tokenCount, fillPercent: 85, compressionLabel: null };\n    }\n    case 5: {\n      const tokenCount = 25000;\n      const summaryBlock = {\n        id: \"auto-summary\",\n        type: \"assistant\" as BlockType,\n        label: \"SUMMARY\",\n        heightPx: 40,\n        compressed: false,\n      };\n      const recentBlocks = generateBlocks(4, 2).map((b) => ({\n        ...b,\n        heightPx: 20,\n      }));\n      return {\n        blocks: [summaryBlock, ...recentBlocks],\n        tokenCount,\n        fillPercent: 25,\n        compressionLabel: \"AUTO-COMPACT\",\n      };\n    }\n    case 6: {\n      const tokenCount = 8000;\n      const compactBlock = {\n        id: \"compact-summary\",\n        type: \"assistant\" as BlockType,\n        label: \"COMPACT SUMMARY\",\n        heightPx: 24,\n        compressed: false,\n      };\n      return {\n        blocks: [compactBlock],\n        tokenCount,\n        fillPercent: 8,\n        compressionLabel: \"/compact\",\n      };\n    }\n    default:\n      return { blocks: [], tokenCount: 0, fillPercent: 0, compressionLabel: null };\n  }\n}\n\nconst STEPS = [\n  {\n    title: \"Growing Context\",\n    description:\n      \"The context window holds the conversation. Each API call adds more messages.\",\n  },\n  {\n    title: \"Context Growing\",\n    description:\n      \"As the agent works, messages accumulate. The context window fills up.\",\n  },\n  {\n    title: \"Approaching Limit\",\n    description:\n      \"Old tool_results are the biggest consumers. Micro-compact targets these first.\",\n  },\n  {\n    title: \"Stage 1: Micro-Compact\",\n    description:\n      \"Replace old tool_results with short summaries. Automatic, transparent to the model.\",\n  },\n  {\n    title: \"Still Growing\",\n    description:\n      \"Work continues. Context grows again toward the threshold...\",\n  },\n  {\n    title: \"Stage 2: Auto-Compact\",\n    description:\n      \"Entire conversation summarized into a compact block. Triggered at token threshold.\",\n  },\n  {\n    title: \"Stage 3: /compact\",\n    description:\n      \"User-triggered, most aggressive. Three layers of strategic forgetting enable infinite sessions.\",\n  },\n];\n\nexport default function ContextCompact({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });\n\n  const state = useMemo(() => computeStepState(currentStep), [currentStep]);\n\n  const fillColor =\n    state.fillPercent > 75\n      ? \"bg-red-500\"\n      : state.fillPercent > 45\n        ? \"bg-amber-500\"\n        : \"bg-emerald-500\";\n\n  const tokenDisplay = `${(state.tokenCount / 1000).toFixed(0)}K`;\n\n  return (\n    <section className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Three-Layer Context Compression\"}\n      </h2>\n\n      <div\n        className=\"rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900\"\n        style={{ minHeight: 500 }}\n      >\n        <div className=\"flex gap-6\">\n          {/* Token Window (tall vertical bar on the left) */}\n          <div className=\"flex flex-col items-center\">\n            <div className=\"mb-2 font-mono text-[10px] font-semibold text-zinc-500 dark:text-zinc-400\">\n              Context Window\n            </div>\n            <div\n              className=\"relative w-24 overflow-hidden rounded-xl border-2 border-zinc-300 bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800\"\n              style={{ height: WINDOW_HEIGHT }}\n            >\n              {/* Blocks stacked from bottom up */}\n              <div className=\"absolute bottom-0 left-0 right-0 flex flex-col-reverse gap-px p-1\">\n                <AnimatePresence mode=\"popLayout\">\n                  {state.blocks.map((block) => (\n                    <motion.div\n                      key={block.id}\n                      initial={{ opacity: 0, scaleY: 0 }}\n                      animate={{\n                        opacity: 1,\n                        scaleY: 1,\n                        height: block.heightPx,\n                      }}\n                      exit={{ opacity: 0, scaleY: 0 }}\n                      transition={{ duration: 0.4 }}\n                      className={`flex w-full items-center justify-center rounded-sm ${\n                        block.compressed\n                          ? \"bg-emerald-300 dark:bg-emerald-700\"\n                          : BLOCK_COLORS[block.type]\n                      }`}\n                      style={{ originY: 1 }}\n                    >\n                      {block.heightPx >= 14 && (\n                        <span className=\"truncate px-1 text-[8px] font-medium text-white\">\n                          {block.label}\n                        </span>\n                      )}\n                    </motion.div>\n                  ))}\n                </AnimatePresence>\n              </div>\n\n              {/* Fill level line */}\n              <motion.div\n                animate={{ bottom: `${state.fillPercent}%` }}\n                transition={{ duration: 0.5 }}\n                className=\"absolute left-0 right-0 border-t-2 border-dashed border-red-400 dark:border-red-500\"\n              >\n                <span className=\"absolute -top-4 right-1 font-mono text-[9px] font-bold text-red-500 dark:text-red-400\">\n                  {state.fillPercent}%\n                </span>\n              </motion.div>\n            </div>\n\n            {/* Token count */}\n            <motion.div\n              key={state.tokenCount}\n              initial={{ scale: 0.85 }}\n              animate={{ scale: 1 }}\n              className=\"mt-2 font-mono text-sm font-bold text-zinc-700 dark:text-zinc-200\"\n            >\n              {tokenDisplay}\n            </motion.div>\n            <div className=\"font-mono text-[10px] text-zinc-400\">\n              / 100K\n            </div>\n          </div>\n\n          {/* Right side: state display and compression stage */}\n          <div className=\"flex flex-1 flex-col justify-between\">\n            {/* Top: horizontal token bar */}\n            <div>\n              <div className=\"mb-1 flex items-center justify-between\">\n                <span className=\"text-xs text-zinc-500 dark:text-zinc-400\">\n                  Token usage\n                </span>\n                <span className=\"font-mono text-xs text-zinc-500\">\n                  {state.tokenCount.toLocaleString()} / {MAX_TOKENS.toLocaleString()}\n                </span>\n              </div>\n              <div className=\"h-3 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800\">\n                <motion.div\n                  animate={{ width: `${state.fillPercent}%` }}\n                  transition={{ duration: 0.5 }}\n                  className={`h-full rounded-full ${fillColor}`}\n                />\n              </div>\n            </div>\n\n            {/* Message type legend */}\n            <div className=\"mt-4 flex items-center gap-4\">\n              <div className=\"flex items-center gap-1\">\n                <div className=\"h-3 w-3 rounded bg-blue-500\" />\n                <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">user</span>\n              </div>\n              <div className=\"flex items-center gap-1\">\n                <div className=\"h-3 w-3 rounded bg-zinc-500\" />\n                <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">assistant</span>\n              </div>\n              <div className=\"flex items-center gap-1\">\n                <div className=\"h-3 w-3 rounded bg-emerald-500\" />\n                <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">tool_result</span>\n              </div>\n            </div>\n\n            {/* Highlight old tool_results at step 2 */}\n            <AnimatePresence>\n              {currentStep === 2 && (\n                <motion.div\n                  initial={{ opacity: 0, y: 8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0 }}\n                  className=\"mt-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 dark:border-amber-700 dark:bg-amber-900/20\"\n                >\n                  <div className=\"text-xs font-semibold text-amber-700 dark:text-amber-300\">\n                    tool_results are the largest blocks\n                  </div>\n                  <div className=\"text-[11px] text-amber-600 dark:text-amber-400\">\n                    File contents, command outputs, search results -- each one is thousands of tokens.\n                  </div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n\n            {/* Compression stage label */}\n            <AnimatePresence>\n              {state.compressionLabel && (\n                <motion.div\n                  initial={{ opacity: 0, scale: 0.9 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, scale: 0.9 }}\n                  transition={{ duration: 0.4 }}\n                  className=\"mt-4\"\n                >\n                  <div className={`rounded-lg border-2 p-4 text-center ${\n                    currentStep === 3\n                      ? \"border-amber-400 bg-amber-50 dark:border-amber-600 dark:bg-amber-900/20\"\n                      : currentStep === 5\n                        ? \"border-blue-400 bg-blue-50 dark:border-blue-600 dark:bg-blue-900/20\"\n                        : \"border-emerald-400 bg-emerald-50 dark:border-emerald-600 dark:bg-emerald-900/20\"\n                  }`}>\n                    <div className={`text-lg font-black ${\n                      currentStep === 3\n                        ? \"text-amber-600 dark:text-amber-300\"\n                        : currentStep === 5\n                          ? \"text-blue-600 dark:text-blue-300\"\n                          : \"text-emerald-600 dark:text-emerald-300\"\n                    }`}>\n                      {state.compressionLabel}\n                    </div>\n                    <div className={`mt-1 text-xs ${\n                      currentStep === 3\n                        ? \"text-amber-500 dark:text-amber-400\"\n                        : currentStep === 5\n                          ? \"text-blue-500 dark:text-blue-400\"\n                          : \"text-emerald-500 dark:text-emerald-400\"\n                    }`}>\n                      {currentStep === 3 && \"Old tool_results shrunk to tiny summaries\"}\n                      {currentStep === 5 && \"Full conversation compressed to summary block\"}\n                      {currentStep === 6 && \"Most aggressive compression -- near-empty context\"}\n                    </div>\n                  </div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n\n            {/* Three stages overview on final step */}\n            {currentStep === 6 && (\n              <motion.div\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                transition={{ delay: 0.4 }}\n                className=\"mt-4 space-y-2\"\n              >\n                <div className=\"flex items-center gap-2 rounded bg-amber-50 px-3 py-1.5 dark:bg-amber-900/10\">\n                  <div className=\"h-2 w-2 rounded-full bg-amber-500\" />\n                  <span className=\"text-xs text-amber-700 dark:text-amber-300\">\n                    Stage 1: Micro -- shrink old tool_results\n                  </span>\n                  <span className=\"ml-auto font-mono text-[10px] text-amber-500\">\n                    automatic\n                  </span>\n                </div>\n                <div className=\"flex items-center gap-2 rounded bg-blue-50 px-3 py-1.5 dark:bg-blue-900/10\">\n                  <div className=\"h-2 w-2 rounded-full bg-blue-500\" />\n                  <span className=\"text-xs text-blue-700 dark:text-blue-300\">\n                    Stage 2: Auto -- summarize entire conversation\n                  </span>\n                  <span className=\"ml-auto font-mono text-[10px] text-blue-500\">\n                    at threshold\n                  </span>\n                </div>\n                <div className=\"flex items-center gap-2 rounded bg-emerald-50 px-3 py-1.5 dark:bg-emerald-900/10\">\n                  <div className=\"h-2 w-2 rounded-full bg-emerald-500\" />\n                  <span className=\"text-xs text-emerald-700 dark:text-emerald-300\">\n                    Stage 3: /compact -- user-triggered, deepest compression\n                  </span>\n                  <span className=\"ml-auto font-mono text-[10px] text-emerald-500\">\n                    manual\n                  </span>\n                </div>\n              </motion.div>\n            )}\n          </div>\n        </div>\n\n        {/* Step Controls */}\n        <div className=\"mt-6\">\n          <StepControls\n            currentStep={currentStep}\n            totalSteps={totalSteps}\n            onPrev={prev}\n            onNext={next}\n            onReset={reset}\n            isPlaying={isPlaying}\n            onToggleAutoPlay={toggleAutoPlay}\n            stepTitle={STEPS[currentStep].title}\n            stepDescription={STEPS[currentStep].description}\n          />\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s07-task-system.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\nimport { useDarkMode, useSvgPalette } from \"@/hooks/useDarkMode\";\n\ntype TaskStatus = \"pending\" | \"in_progress\" | \"completed\" | \"blocked\";\n\ninterface TaskNode {\n  id: string;\n  label: string;\n  x: number;\n  y: number;\n  deps: string[];\n}\n\ninterface StepInfo {\n  title: string;\n  description: string;\n}\n\nconst TASKS: TaskNode[] = [\n  { id: \"T1\", label: \"T1: Setup DB\", x: 80, y: 160, deps: [] },\n  { id: \"T2\", label: \"T2: API routes\", x: 280, y: 80, deps: [\"T1\"] },\n  { id: \"T3\", label: \"T3: Auth module\", x: 280, y: 240, deps: [\"T1\"] },\n  { id: \"T4\", label: \"T4: Integration\", x: 480, y: 160, deps: [\"T2\", \"T3\"] },\n  { id: \"T5\", label: \"T5: Deploy\", x: 650, y: 160, deps: [\"T4\"] },\n];\n\nconst NODE_W = 140;\nconst NODE_H = 50;\n\nconst STEP_INFO: StepInfo[] = [\n  {\n    title: \"File-Based Tasks\",\n    description:\n      \"Tasks are stored in JSON files on disk. They survive context compaction -- unlike in-memory state.\",\n  },\n  {\n    title: \"Start T1\",\n    description:\n      \"Tasks without dependencies can start immediately. T1 has no blockers.\",\n  },\n  {\n    title: \"T1 Complete\",\n    description: \"Completing T1 unblocks its dependents: T2 and T3.\",\n  },\n  {\n    title: \"Parallel Work\",\n    description:\n      \"T2 and T3 have no dependency on each other. Both can run simultaneously.\",\n  },\n  {\n    title: \"Partial Unblock\",\n    description:\n      \"T4 depends on BOTH T2 and T3. It waits for all blockers to complete.\",\n  },\n  {\n    title: \"Fully Unblocked\",\n    description: \"All blockers resolved. T4 can now proceed.\",\n  },\n  {\n    title: \"Graph Resolved\",\n    description:\n      \"The entire dependency graph is resolved. File-based persistence means this works across context compressions.\",\n  },\n];\n\nfunction getTaskStatus(taskId: string, step: number): TaskStatus {\n  const statusMap: Record<string, TaskStatus[]> = {\n    T1: [\n      \"pending\",\n      \"in_progress\",\n      \"completed\",\n      \"completed\",\n      \"completed\",\n      \"completed\",\n      \"completed\",\n    ],\n    T2: [\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"in_progress\",\n      \"completed\",\n      \"completed\",\n      \"completed\",\n    ],\n    T3: [\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"in_progress\",\n      \"in_progress\",\n      \"completed\",\n      \"completed\",\n    ],\n    T4: [\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"blocked\",\n      \"in_progress\",\n      \"completed\",\n    ],\n    T5: [\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"pending\",\n      \"completed\",\n    ],\n  };\n  return statusMap[taskId]?.[step] ?? \"pending\";\n}\n\nfunction isEdgeActive(fromId: string, toId: string, step: number): boolean {\n  const fromStatus = getTaskStatus(fromId, step);\n  const toStatus = getTaskStatus(toId, step);\n  return (\n    fromStatus === \"completed\" &&\n    (toStatus === \"in_progress\" || toStatus === \"completed\")\n  );\n}\n\nfunction getStatusColor(status: TaskStatus) {\n  switch (status) {\n    case \"pending\":\n      return {\n        fill: \"#e2e8f0\",\n        darkFill: \"#27272a\",\n        stroke: \"#cbd5e1\",\n        darkStroke: \"#3f3f46\",\n        text: \"#475569\",\n        darkText: \"#d4d4d8\",\n      };\n    case \"in_progress\":\n      return {\n        fill: \"#fef3c7\",\n        darkFill: \"#451a0340\",\n        stroke: \"#f59e0b\",\n        darkStroke: \"#d97706\",\n        text: \"#b45309\",\n        darkText: \"#fbbf24\",\n      };\n    case \"completed\":\n      return {\n        fill: \"#d1fae5\",\n        darkFill: \"#06402740\",\n        stroke: \"#10b981\",\n        darkStroke: \"#059669\",\n        text: \"#047857\",\n        darkText: \"#34d399\",\n      };\n    case \"blocked\":\n      return {\n        fill: \"#fecaca\",\n        darkFill: \"#45050540\",\n        stroke: \"#ef4444\",\n        darkStroke: \"#dc2626\",\n        text: \"#dc2626\",\n        darkText: \"#f87171\",\n      };\n  }\n}\n\nfunction getStatusLabel(status: TaskStatus): string {\n  switch (status) {\n    case \"pending\":\n      return \"pending\";\n    case \"in_progress\":\n      return \"in_progress\";\n    case \"completed\":\n      return \"done\";\n    case \"blocked\":\n      return \"blocked\";\n  }\n}\n\nfunction buildCurvePath(\n  x1: number,\n  y1: number,\n  x2: number,\n  y2: number\n): string {\n  const midX = (x1 + x2) / 2;\n  return `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;\n}\n\nexport default function TaskSystem({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });\n\n  const isDark = useDarkMode();\n  const palette = useSvgPalette();\n\n  const edges = useMemo(() => {\n    const result: {\n      fromId: string;\n      toId: string;\n      x1: number;\n      y1: number;\n      x2: number;\n      y2: number;\n    }[] = [];\n    for (const task of TASKS) {\n      for (const depId of task.deps) {\n        const dep = TASKS.find((t) => t.id === depId);\n        if (!dep) continue;\n        result.push({\n          fromId: dep.id,\n          toId: task.id,\n          x1: dep.x + NODE_W,\n          y1: dep.y + NODE_H / 2,\n          x2: task.x,\n          y2: task.y + NODE_H / 2,\n        });\n      }\n    }\n    return result;\n  }, []);\n\n  const stepInfo = STEP_INFO[currentStep];\n\n  return (\n    <section className=\"min-h-[500px] space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Task Dependency Graph\"}\n      </h2>\n\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\">\n        <svg viewBox=\"0 0 800 340\" className=\"w-full\" aria-label=\"Task DAG\">\n          <defs>\n            <marker\n              id=\"arrowGray\"\n              viewBox=\"0 0 10 10\"\n              refX=\"9\"\n              refY=\"5\"\n              markerWidth=\"6\"\n              markerHeight=\"6\"\n              orient=\"auto-start-reverse\"\n            >\n              <path d=\"M 0 0 L 10 5 L 0 10 z\" fill={palette.arrowFill} />\n            </marker>\n            <marker\n              id=\"arrowGreen\"\n              viewBox=\"0 0 10 10\"\n              refX=\"9\"\n              refY=\"5\"\n              markerWidth=\"6\"\n              markerHeight=\"6\"\n              orient=\"auto-start-reverse\"\n            >\n              <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"#10b981\" />\n            </marker>\n            <marker\n              id=\"arrowRed\"\n              viewBox=\"0 0 10 10\"\n              refX=\"9\"\n              refY=\"5\"\n              markerWidth=\"6\"\n              markerHeight=\"6\"\n              orient=\"auto-start-reverse\"\n            >\n              <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"#ef4444\" />\n            </marker>\n            <filter id=\"glowAmber\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\">\n              <feGaussianBlur stdDeviation=\"4\" result=\"blur\" />\n              <feFlood floodColor=\"#f59e0b\" floodOpacity=\"0.4\" result=\"color\" />\n              <feComposite in=\"color\" in2=\"blur\" operator=\"in\" result=\"glow\" />\n              <feMerge>\n                <feMergeNode in=\"glow\" />\n                <feMergeNode in=\"SourceGraphic\" />\n              </feMerge>\n            </filter>\n            <filter\n              id=\"glowGreen\"\n              x=\"-30%\"\n              y=\"-30%\"\n              width=\"160%\"\n              height=\"160%\"\n            >\n              <feGaussianBlur stdDeviation=\"3\" result=\"blur\" />\n              <feFlood floodColor=\"#10b981\" floodOpacity=\"0.3\" result=\"color\" />\n              <feComposite in=\"color\" in2=\"blur\" operator=\"in\" result=\"glow\" />\n              <feMerge>\n                <feMergeNode in=\"glow\" />\n                <feMergeNode in=\"SourceGraphic\" />\n              </feMerge>\n            </filter>\n          </defs>\n\n          {/* Dependency edges */}\n          {edges.map(({ fromId, toId, x1, y1, x2, y2 }) => {\n            const active = isEdgeActive(fromId, toId, currentStep);\n            const toStatus = getTaskStatus(toId, currentStep);\n            const isBlocked = toStatus === \"blocked\";\n            let markerEnd = \"url(#arrowGray)\";\n            let strokeColor = palette.arrowFill;\n            if (active) {\n              markerEnd = \"url(#arrowGreen)\";\n              strokeColor = \"#10b981\";\n            } else if (isBlocked) {\n              markerEnd = \"url(#arrowRed)\";\n              strokeColor = \"#ef4444\";\n            }\n\n            return (\n              <motion.path\n                key={`${fromId}-${toId}`}\n                d={buildCurvePath(x1, y1, x2, y2)}\n                fill=\"none\"\n                markerEnd={markerEnd}\n                animate={{\n                  stroke: strokeColor,\n                  strokeWidth: active ? 2.5 : 1.5,\n                  strokeDasharray: isBlocked ? \"6 4\" : \"none\",\n                }}\n                transition={{ duration: 0.5 }}\n              />\n            );\n          })}\n\n          {/* Task nodes */}\n          {TASKS.map((task) => {\n            const status = getTaskStatus(task.id, currentStep);\n            const colors = getStatusColor(status);\n            const statusLabel = getStatusLabel(status);\n            const isActive = status === \"in_progress\";\n            const isComplete = status === \"completed\";\n\n            let filterAttr: string | undefined;\n            if (isActive) filterAttr = \"url(#glowAmber)\";\n            else if (isComplete) filterAttr = \"url(#glowGreen)\";\n\n            return (\n              <g key={task.id} filter={filterAttr}>\n                <motion.rect\n                  x={task.x}\n                  y={task.y}\n                  width={NODE_W}\n                  height={NODE_H}\n                  rx={8}\n                  animate={{\n                    fill: isDark ? colors.darkFill : colors.fill,\n                    stroke: isDark ? colors.darkStroke : colors.stroke,\n                  }}\n                  strokeWidth={isActive ? 2 : 1.5}\n                  transition={{ duration: 0.4 }}\n                />\n                <text\n                  x={task.x + NODE_W / 2}\n                  y={task.y + 20}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"middle\"\n                  fontSize=\"11\"\n                  fontWeight=\"600\"\n                  fill={isDark ? colors.darkText : colors.text}\n                >\n                  {task.label}\n                </text>\n                <text\n                  x={task.x + NODE_W / 2}\n                  y={task.y + 38}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"middle\"\n                  fontSize=\"9\"\n                  fontFamily=\"monospace\"\n                  fill={isDark ? colors.darkText : colors.text}\n                  opacity={0.8}\n                >\n                  {statusLabel}\n                </text>\n              </g>\n            );\n          })}\n\n          {/* Blocked annotation for T4 at step 4 */}\n          {currentStep === 4 && (\n            <motion.g\n              initial={{ opacity: 0, y: 5 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ duration: 0.4 }}\n            >\n              <rect\n                x={445}\n                y={118}\n                width={170}\n                height={22}\n                rx={4}\n                fill={isDark ? \"#451a03\" : \"#fef2f2\"}\n                stroke={isDark ? \"#dc2626\" : \"#fca5a5\"}\n                strokeWidth={1}\n              />\n              <text\n                x={530}\n                y={132}\n                textAnchor=\"middle\"\n                dominantBaseline=\"middle\"\n                fontSize=\"9\"\n                fontFamily=\"monospace\"\n                fill={isDark ? \"#f87171\" : \"#dc2626\"}\n              >\n                Blocked: waiting on T3\n              </text>\n            </motion.g>\n          )}\n        </svg>\n\n        {/* File persistence indicator */}\n        <div className=\"mt-3 flex items-center gap-2 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-800/60\">\n          <svg\n            viewBox=\"0 0 24 24\"\n            className=\"h-5 w-5 flex-shrink-0 text-zinc-400 dark:text-zinc-500\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n          >\n            <path\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              d=\"M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776\"\n            />\n          </svg>\n          <div className=\"flex flex-col\">\n            <span className=\"font-mono text-xs font-medium text-zinc-600 dark:text-zinc-300\">\n              .tasks/tasks.json\n            </span>\n            <span className=\"text-[10px] text-zinc-400 dark:text-zinc-500\">\n              Persisted to disk -- survives context compaction\n            </span>\n          </div>\n          <motion.div\n            className=\"ml-auto h-2 w-2 rounded-full bg-emerald-500\"\n            animate={{ opacity: [1, 0.3, 1] }}\n            transition={{ repeat: Infinity, duration: 2 }}\n          />\n        </div>\n\n        {/* Legend */}\n        <div className=\"mt-3 flex flex-wrap items-center gap-4\">\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded bg-zinc-300 dark:bg-zinc-600\" />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              pending\n            </span>\n          </div>\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded bg-amber-400 dark:bg-amber-600\" />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              in_progress\n            </span>\n          </div>\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded bg-emerald-400 dark:bg-emerald-600\" />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              completed\n            </span>\n          </div>\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded bg-red-400 dark:bg-red-600\" />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              blocked\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <StepControls\n        currentStep={currentStep}\n        totalSteps={totalSteps}\n        onPrev={prev}\n        onNext={next}\n        onReset={reset}\n        isPlaying={isPlaying}\n        onToggleAutoPlay={toggleAutoPlay}\n        stepTitle={stepInfo.title}\n        stepDescription={stepInfo.description}\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s08-background-tasks.tsx",
    "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\nimport { useDarkMode, useSvgPalette } from \"@/hooks/useDarkMode\";\n\ninterface StepInfo {\n  title: string;\n  description: string;\n}\n\nconst STEP_INFO: StepInfo[] = [\n  {\n    title: \"Three Lanes\",\n    description:\n      \"The agent has a main thread and can spawn daemon background threads for parallel work.\",\n  },\n  {\n    title: \"Main Thread Working\",\n    description:\n      \"The main agent loop runs as usual, processing user requests.\",\n  },\n  {\n    title: \"Spawn Background\",\n    description:\n      \"Background tasks run as daemon threads. The main loop doesn't wait for them.\",\n  },\n  {\n    title: \"Multiple Backgrounds\",\n    description: \"Multiple background tasks can run concurrently.\",\n  },\n  {\n    title: \"Task Completes\",\n    description:\n      \"Background task finishes. Its result goes to the notification queue.\",\n  },\n  {\n    title: \"Queue Fills\",\n    description:\n      \"Results accumulate in the queue, invisible to the model during this turn.\",\n  },\n  {\n    title: \"Drain Queue\",\n    description:\n      \"Just before the next LLM call, all queued notifications are injected as tool_results. Non-blocking, async.\",\n  },\n];\n\nconst LANE_Y = {\n  main: 60,\n  bg1: 140,\n  bg2: 220,\n} as const;\n\nconst LANE_HEIGHT = 44;\nconst TIMELINE_LEFT = 160;\nconst TIMELINE_RIGHT = 720;\nconst TIMELINE_WIDTH = TIMELINE_RIGHT - TIMELINE_LEFT;\n\nconst QUEUE_Y = 300;\n\ninterface WorkBlock {\n  lane: \"main\" | \"bg1\" | \"bg2\";\n  startFraction: number;\n  endFraction: number;\n  color: string;\n  label?: string;\n  appearsAtStep: number;\n  completesAtStep?: number;\n}\n\nconst WORK_BLOCKS: WorkBlock[] = [\n  {\n    lane: \"main\",\n    startFraction: 0,\n    endFraction: 1,\n    color: \"#8b5cf6\",\n    label: \"Main agent loop\",\n    appearsAtStep: 1,\n  },\n  {\n    lane: \"bg1\",\n    startFraction: 0.18,\n    endFraction: 0.75,\n    color: \"#10b981\",\n    label: \"Run tests\",\n    appearsAtStep: 2,\n    completesAtStep: 5,\n  },\n  {\n    lane: \"bg2\",\n    startFraction: 0.35,\n    endFraction: 0.58,\n    color: \"#3b82f6\",\n    label: \"Lint code\",\n    appearsAtStep: 3,\n    completesAtStep: 4,\n  },\n];\n\ninterface ForkArrow {\n  fromFraction: number;\n  toLane: \"bg1\" | \"bg2\";\n  appearsAtStep: number;\n}\n\nconst FORK_ARROWS: ForkArrow[] = [\n  { fromFraction: 0.18, toLane: \"bg1\", appearsAtStep: 2 },\n  { fromFraction: 0.35, toLane: \"bg2\", appearsAtStep: 3 },\n];\n\ninterface QueueCard {\n  id: string;\n  label: string;\n  appearsAtStep: number;\n  drainsAtStep: number;\n}\n\nconst QUEUE_CARDS: QueueCard[] = [\n  {\n    id: \"lint-result\",\n    label: \"Lint: 0 errors\",\n    appearsAtStep: 4,\n    drainsAtStep: 6,\n  },\n  {\n    id: \"test-result\",\n    label: \"Tests: 42 passed\",\n    appearsAtStep: 5,\n    drainsAtStep: 6,\n  },\n];\n\nfunction fractionToX(fraction: number): number {\n  return TIMELINE_LEFT + fraction * TIMELINE_WIDTH;\n}\n\nfunction getBlockEndFraction(block: WorkBlock, step: number): number {\n  if (step < block.appearsAtStep) return block.startFraction;\n  if (block.completesAtStep !== undefined && step >= block.completesAtStep) {\n    return block.endFraction;\n  }\n  const growthSteps = (block.completesAtStep ?? 6) - block.appearsAtStep;\n  const stepsElapsed = step - block.appearsAtStep;\n  const progress = Math.min(stepsElapsed / growthSteps, 1);\n  const range = block.endFraction - block.startFraction;\n  return block.startFraction + range * progress;\n}\n\nexport default function BackgroundTasks({ title }: { title?: string }) {\n  const {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    isPlaying,\n    toggleAutoPlay,\n  } = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });\n\n  const isDark = useDarkMode();\n  const palette = useSvgPalette();\n\n  const stepInfo = STEP_INFO[currentStep];\n\n  const llmCallFraction = 0.82;\n  const showLlmMarker = currentStep >= 5;\n\n  return (\n    <section className=\"min-h-[500px] space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Background Task Lanes\"}\n      </h2>\n\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\">\n        <svg viewBox=\"0 0 780 380\" className=\"w-full\" aria-label=\"Background task lanes\">\n          <defs>\n            <marker\n              id=\"forkArrow\"\n              viewBox=\"0 0 10 10\"\n              refX=\"9\"\n              refY=\"5\"\n              markerWidth=\"5\"\n              markerHeight=\"5\"\n              orient=\"auto-start-reverse\"\n            >\n              <path d=\"M 0 0 L 10 5 L 0 10 z\" fill={palette.arrowFill} />\n            </marker>\n            <marker\n              id=\"drainArrow\"\n              viewBox=\"0 0 10 10\"\n              refX=\"9\"\n              refY=\"5\"\n              markerWidth=\"5\"\n              markerHeight=\"5\"\n              orient=\"auto-start-reverse\"\n            >\n              <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"#f59e0b\" />\n            </marker>\n            <filter id=\"blockGlow\" x=\"-10%\" y=\"-20%\" width=\"120%\" height=\"140%\">\n              <feGaussianBlur stdDeviation=\"2\" result=\"blur\" />\n              <feFlood floodColor=\"#8b5cf6\" floodOpacity=\"0.2\" result=\"color\" />\n              <feComposite in=\"color\" in2=\"blur\" operator=\"in\" result=\"glow\" />\n              <feMerge>\n                <feMergeNode in=\"glow\" />\n                <feMergeNode in=\"SourceGraphic\" />\n              </feMerge>\n            </filter>\n          </defs>\n\n          {/* Timeline axis */}\n          <line\n            x1={TIMELINE_LEFT}\n            y1={30}\n            x2={TIMELINE_RIGHT}\n            y2={30}\n            stroke={palette.labelFill}\n            strokeWidth={1}\n            strokeDasharray=\"4 3\"\n            opacity={0.5}\n          />\n          <text\n            x={TIMELINE_LEFT}\n            y={22}\n            fontSize=\"9\"\n            fontFamily=\"monospace\"\n            fill={palette.labelFill}\n          >\n            t=0\n          </text>\n          <text\n            x={TIMELINE_RIGHT}\n            y={22}\n            fontSize=\"9\"\n            fontFamily=\"monospace\"\n            fill={palette.labelFill}\n            textAnchor=\"end\"\n          >\n            time\n          </text>\n\n          {/* Lane backgrounds and labels */}\n          {(\n            [\n              { key: \"main\", y: LANE_Y.main, label: \"Main Thread\" },\n              { key: \"bg1\", y: LANE_Y.bg1, label: \"Background 1\" },\n              { key: \"bg2\", y: LANE_Y.bg2, label: \"Background 2\" },\n            ] as const\n          ).map(({ key, y, label }) => (\n            <g key={key}>\n              <rect\n                x={TIMELINE_LEFT}\n                y={y}\n                width={TIMELINE_WIDTH}\n                height={LANE_HEIGHT}\n                rx={6}\n                fill=\"none\"\n                stroke={palette.nodeStroke}\n                strokeWidth={1}\n                strokeDasharray=\"4 2\"\n                opacity={0.6}\n              />\n              <text\n                x={TIMELINE_LEFT - 10}\n                y={y + LANE_HEIGHT / 2 + 1}\n                textAnchor=\"end\"\n                dominantBaseline=\"middle\"\n                fontSize=\"11\"\n                fontWeight=\"600\"\n                fill={palette.labelFill}\n              >\n                {label}\n              </text>\n            </g>\n          ))}\n\n          {/* Work blocks */}\n          {WORK_BLOCKS.map((block) => {\n            if (currentStep < block.appearsAtStep) return null;\n\n            const startX = fractionToX(block.startFraction);\n            const endFraction = getBlockEndFraction(block, currentStep);\n            const endX = fractionToX(endFraction);\n            const width = Math.max(endX - startX, 4);\n            const y = LANE_Y[block.lane];\n            const isComplete =\n              block.completesAtStep !== undefined &&\n              currentStep >= block.completesAtStep;\n\n            return (\n              <motion.g key={`${block.lane}-block`}>\n                <motion.rect\n                  x={startX}\n                  y={y + 4}\n                  height={LANE_HEIGHT - 8}\n                  rx={5}\n                  initial={{ width: 4 }}\n                  animate={{\n                    width,\n                    opacity: isComplete ? 0.7 : 1,\n                  }}\n                  transition={{ duration: 0.6, ease: \"easeOut\" }}\n                  fill={block.color}\n                  filter={\n                    !isComplete && block.lane === \"main\"\n                      ? \"url(#blockGlow)\"\n                      : undefined\n                  }\n                />\n                {width > 60 && block.label && (\n                  <motion.text\n                    x={startX + width / 2}\n                    y={y + LANE_HEIGHT / 2 + 1}\n                    textAnchor=\"middle\"\n                    dominantBaseline=\"middle\"\n                    fontSize=\"10\"\n                    fontWeight=\"500\"\n                    fill=\"white\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    transition={{ delay: 0.3 }}\n                  >\n                    {block.label}\n                  </motion.text>\n                )}\n                {isComplete && (\n                  <motion.text\n                    x={endX + 6}\n                    y={y + LANE_HEIGHT / 2 + 1}\n                    dominantBaseline=\"middle\"\n                    fontSize=\"9\"\n                    fontFamily=\"monospace\"\n                    fill=\"#10b981\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                  >\n                    done\n                  </motion.text>\n                )}\n              </motion.g>\n            );\n          })}\n\n          {/* Fork arrows from main to background lanes */}\n          {FORK_ARROWS.map((arrow) => {\n            if (currentStep < arrow.appearsAtStep) return null;\n            const x = fractionToX(arrow.fromFraction);\n            const fromY = LANE_Y.main + LANE_HEIGHT;\n            const toY = LANE_Y[arrow.toLane];\n\n            return (\n              <motion.line\n                key={`fork-${arrow.toLane}`}\n                x1={x}\n                y1={fromY}\n                x2={x + 20}\n                y2={toY}\n                stroke={palette.arrowFill}\n                strokeWidth={1.5}\n                markerEnd=\"url(#forkArrow)\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                transition={{ duration: 0.4 }}\n              />\n            );\n          })}\n\n          {/* LLM API call marker */}\n          {showLlmMarker && (\n            <motion.g\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={{ duration: 0.4 }}\n            >\n              <line\n                x1={fractionToX(llmCallFraction)}\n                y1={LANE_Y.main}\n                x2={fractionToX(llmCallFraction)}\n                y2={LANE_Y.main + LANE_HEIGHT}\n                stroke=\"#f59e0b\"\n                strokeWidth={2}\n                strokeDasharray=\"3 2\"\n              />\n              <rect\n                x={fractionToX(llmCallFraction) - 36}\n                y={LANE_Y.main - 16}\n                width={72}\n                height={16}\n                rx={3}\n                fill=\"#f59e0b\"\n              />\n              <text\n                x={fractionToX(llmCallFraction)}\n                y={LANE_Y.main - 6}\n                textAnchor=\"middle\"\n                dominantBaseline=\"middle\"\n                fontSize=\"8\"\n                fontWeight=\"600\"\n                fill=\"white\"\n              >\n                LLM API call\n              </text>\n            </motion.g>\n          )}\n\n          {/* Notification queue area */}\n          <rect\n            x={TIMELINE_LEFT}\n            y={QUEUE_Y}\n            width={TIMELINE_WIDTH}\n            height={54}\n            rx={8}\n            fill=\"none\"\n            stroke={palette.nodeStroke}\n            strokeWidth={1}\n          />\n          <text\n            x={TIMELINE_LEFT - 10}\n            y={QUEUE_Y + 18}\n            textAnchor=\"end\"\n            fontSize=\"10\"\n            fontWeight=\"600\"\n            fill={palette.labelFill}\n          >\n            Notification\n          </text>\n          <text\n            x={TIMELINE_LEFT - 10}\n            y={QUEUE_Y + 32}\n            textAnchor=\"end\"\n            fontSize=\"10\"\n            fontWeight=\"600\"\n            fill={palette.labelFill}\n          >\n            Queue\n          </text>\n\n          {/* Queue cards */}\n          <AnimatePresence>\n            {QUEUE_CARDS.map((card, idx) => {\n              if (currentStep < card.appearsAtStep) return null;\n              const isDraining = currentStep >= card.drainsAtStep;\n              const cardX = TIMELINE_LEFT + 20 + idx * 150;\n              const cardY = QUEUE_Y + 10;\n              const drainTargetY = LANE_Y.main + LANE_HEIGHT / 2 - 12;\n              const drainTargetX = fractionToX(llmCallFraction) + 10 + idx * 15;\n\n              if (isDraining) {\n                return (\n                  <motion.g\n                    key={`card-${card.id}-drain`}\n                    initial={{ x: cardX, y: cardY, opacity: 1 }}\n                    animate={{\n                      x: drainTargetX,\n                      y: drainTargetY,\n                      opacity: [1, 1, 0],\n                    }}\n                    transition={{ duration: 0.8, ease: \"easeInOut\" }}\n                  >\n                    <rect\n                      x={0}\n                      y={0}\n                      width={130}\n                      height={34}\n                      rx={5}\n                      fill={isDark ? \"#451a0340\" : \"#fef3c7\"}\n                      stroke=\"#f59e0b\"\n                      strokeWidth={1}\n                    />\n                    <text\n                      x={65}\n                      y={13}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fontSize=\"9\"\n                      fontWeight=\"600\"\n                      fill={isDark ? \"#fbbf24\" : \"#b45309\"}\n                    >\n                      tool_result\n                    </text>\n                    <text\n                      x={65}\n                      y={26}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fontSize=\"8\"\n                      fontFamily=\"monospace\"\n                      fill={isDark ? \"#f59e0b\" : \"#92400e\"}\n                    >\n                      {card.label}\n                    </text>\n                  </motion.g>\n                );\n              }\n\n              return (\n                <motion.g\n                  key={`card-${card.id}`}\n                  initial={{ y: cardY - 40, opacity: 0 }}\n                  animate={{ y: 0, opacity: 1 }}\n                  transition={{ duration: 0.5, ease: \"easeOut\" }}\n                >\n                  <rect\n                    x={cardX}\n                    y={cardY}\n                    width={130}\n                    height={34}\n                    rx={5}\n                    fill={isDark ? \"#06402740\" : \"#d1fae5\"}\n                    stroke=\"#10b981\"\n                    strokeWidth={1}\n                  />\n                  <text\n                    x={cardX + 65}\n                    y={cardY + 13}\n                    textAnchor=\"middle\"\n                    dominantBaseline=\"middle\"\n                    fontSize=\"9\"\n                    fontWeight=\"600\"\n                    fill={isDark ? \"#34d399\" : \"#047857\"}\n                  >\n                    tool_result\n                  </text>\n                  <text\n                    x={cardX + 65}\n                    y={cardY + 26}\n                    textAnchor=\"middle\"\n                    dominantBaseline=\"middle\"\n                    fontSize=\"8\"\n                    fontFamily=\"monospace\"\n                    fill={isDark ? \"#10b981\" : \"#065f46\"}\n                  >\n                    {card.label}\n                  </text>\n                </motion.g>\n              );\n            })}\n          </AnimatePresence>\n\n          {/* Drain arrows from queue to main thread at step 6 */}\n          {currentStep >= 6 && (\n            <motion.g\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={{ duration: 0.3, delay: 0.1 }}\n            >\n              <motion.line\n                x1={fractionToX(llmCallFraction) + 20}\n                y1={QUEUE_Y}\n                x2={fractionToX(llmCallFraction) + 20}\n                y2={LANE_Y.main + LANE_HEIGHT + 4}\n                stroke=\"#f59e0b\"\n                strokeWidth={1.5}\n                markerEnd=\"url(#drainArrow)\"\n                initial={{ pathLength: 0 }}\n                animate={{ pathLength: 1 }}\n                transition={{ duration: 0.5 }}\n              />\n            </motion.g>\n          )}\n\n          {/* Empty queue label when drained */}\n          {currentStep >= 6 && (\n            <motion.text\n              x={TIMELINE_LEFT + TIMELINE_WIDTH / 2}\n              y={QUEUE_Y + 30}\n              textAnchor=\"middle\"\n              dominantBaseline=\"middle\"\n              fontSize=\"10\"\n              fontFamily=\"monospace\"\n              fill={palette.labelFill}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={{ delay: 0.6 }}\n            >\n              queue drained -- injected into next LLM call\n            </motion.text>\n          )}\n        </svg>\n\n        {/* Legend */}\n        <div className=\"mt-3 flex flex-wrap items-center gap-4\">\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded\" style={{ background: \"#8b5cf6\" }} />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              Main thread\n            </span>\n          </div>\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded\" style={{ background: \"#10b981\" }} />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              Background 1\n            </span>\n          </div>\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded\" style={{ background: \"#3b82f6\" }} />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              Background 2\n            </span>\n          </div>\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded\" style={{ background: \"#f59e0b\" }} />\n            <span className=\"text-[10px] text-zinc-500 dark:text-zinc-400\">\n              LLM boundary\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <StepControls\n        currentStep={currentStep}\n        totalSteps={totalSteps}\n        onPrev={prev}\n        onNext={next}\n        onReset={reset}\n        isPlaying={isPlaying}\n        onToggleAutoPlay={toggleAutoPlay}\n        stepTitle={stepInfo.title}\n        stepDescription={stepInfo.description}\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s09-agent-teams.tsx",
    "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\nimport { useSvgPalette } from \"@/hooks/useDarkMode\";\n\n// -- Layout constants --\nconst SVG_W = 560;\nconst SVG_H = 340;\nconst AGENT_R = 40;\n\n// Agent positions: inverted triangle (Lead top-center, Coder bottom-left, Reviewer bottom-right)\nconst AGENTS = [\n  { id: \"lead\", label: \"Lead\", cx: SVG_W / 2, cy: 70, inbox: \"lead.jsonl\" },\n  { id: \"coder\", label: \"Coder\", cx: 140, cy: 230, inbox: \"coder.jsonl\" },\n  { id: \"reviewer\", label: \"Reviewer\", cx: SVG_W - 140, cy: 230, inbox: \"reviewer.jsonl\" },\n] as const;\n\n// Inbox tray dimensions, positioned below each agent circle\nconst TRAY_W = 72;\nconst TRAY_H = 22;\nconst TRAY_OFFSET_Y = AGENT_R + 14;\n\n// Message block dimensions\nconst MSG_W = 60;\nconst MSG_H = 20;\n\nfunction agentById(id: string) {\n  return AGENTS.find((a) => a.id === id)!;\n}\n\nfunction trayCenter(id: string) {\n  const a = agentById(id);\n  return { x: a.cx, y: a.cy + TRAY_OFFSET_Y + TRAY_H / 2 };\n}\n\n// Step configuration\nconst STEPS = [\n  { title: \"The Team\", desc: \"Teams use a leader-worker pattern. Each teammate has a file-based mailbox inbox.\" },\n  { title: \"Lead Assigns Work\", desc: \"Communication is async: write a message to the recipient's .jsonl inbox file.\" },\n  { title: \"Read Inbox\", desc: \"Teammates poll their inbox before each LLM call. New messages become context.\" },\n  { title: \"Independent Work\", desc: \"Each teammate runs its own agent loop independently.\" },\n  { title: \"Pass Result\", desc: \"Results flow through the same mailbox mechanism. All communication is via files.\" },\n  { title: \"Feedback Loop\", desc: \"The mailbox pattern supports any communication topology: linear, broadcast, round-robin.\" },\n  { title: \"File-Based Coordination\", desc: \"No shared memory, no locks. All coordination through append-only files. Simple, robust, debuggable.\" },\n];\n\n// Helper: determine which agent glows at each step\nfunction agentGlows(agentId: string, step: number): boolean {\n  if (step === 1 && agentId === \"lead\") return true;\n  if (step === 2 && agentId === \"coder\") return true;\n  if (step === 3 && agentId === \"coder\") return true;\n  if (step === 4 && agentId === \"coder\") return true;\n  if (step === 5 && agentId === \"reviewer\") return true;\n  return false;\n}\n\n// Helper: determine which inbox tray has a message sitting in it\nfunction trayHasMessage(agentId: string, step: number): boolean {\n  if (step === 2 && agentId === \"coder\") return true;\n  if (step === 4 && agentId === \"reviewer\") return false;\n  if (step === 5 && agentId === \"reviewer\") return true;\n  return false;\n}\n\n// Animated message that travels from one point to another\nfunction TravelingMessage({\n  fromX,\n  fromY,\n  toX,\n  toY,\n  label,\n  delay = 0,\n}: {\n  fromX: number;\n  fromY: number;\n  toX: number;\n  toY: number;\n  label: string;\n  delay?: number;\n}) {\n  return (\n    <motion.g\n      initial={{ opacity: 0 }}\n      animate={{\n        opacity: [0, 1, 1, 0.8],\n        x: [fromX - MSG_W / 2, fromX - MSG_W / 2, toX - MSG_W / 2, toX - MSG_W / 2],\n        y: [fromY - MSG_H / 2, fromY - MSG_H / 2, toY - MSG_H / 2, toY - MSG_H / 2],\n      }}\n      transition={{\n        duration: 1.4,\n        delay,\n        times: [0, 0.1, 0.7, 1],\n        ease: \"easeInOut\",\n      }}\n    >\n      <rect width={MSG_W} height={MSG_H} rx={4} fill=\"#f59e0b\" />\n      <text\n        x={MSG_W / 2}\n        y={MSG_H / 2 + 1}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fill=\"white\"\n        fontSize={8}\n        fontWeight={600}\n      >\n        {label}\n      </text>\n    </motion.g>\n  );\n}\n\n// Faded trace line between two agents\nfunction TraceLine({ from, to, strokeColor }: { from: string; to: string; strokeColor: string }) {\n  const f = trayCenter(from);\n  const t = trayCenter(to);\n  return (\n    <motion.line\n      x1={f.x}\n      y1={f.y}\n      x2={t.x}\n      y2={t.y}\n      stroke={strokeColor}\n      strokeWidth={1.5}\n      strokeDasharray=\"6 4\"\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 0.4 }}\n      transition={{ duration: 0.6 }}\n    />\n  );\n}\n\nexport default function AgentTeams({ title }: { title?: string }) {\n  const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });\n  const step = vis.currentStep;\n  const palette = useSvgPalette();\n\n  return (\n    <section className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Agent Team Mailboxes\"}\n      </h2>\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900 min-h-[500px]\">\n        <div className=\"flex flex-col lg:flex-row gap-4\">\n          {/* SVG visualization */}\n          <div className=\"flex-1\">\n            <svg viewBox={`0 0 ${SVG_W} ${SVG_H}`} className=\"w-full\">\n              <defs>\n                <filter id=\"agent-glow\">\n                  <feGaussianBlur stdDeviation=\"4\" result=\"blur\" />\n                  <feMerge>\n                    <feMergeNode in=\"blur\" />\n                    <feMergeNode in=\"SourceGraphic\" />\n                  </feMerge>\n                </filter>\n              </defs>\n\n              {/* Step 6: trace lines */}\n              {step === 6 && (\n                <>\n                  <TraceLine from=\"lead\" to=\"coder\" strokeColor={palette.edgeStroke} />\n                  <TraceLine from=\"coder\" to=\"reviewer\" strokeColor={palette.edgeStroke} />\n                  <TraceLine from=\"reviewer\" to=\"lead\" strokeColor={palette.edgeStroke} />\n                </>\n              )}\n\n              {/* Agent nodes */}\n              {AGENTS.map((agent) => {\n                const glowing = agentGlows(agent.id, step);\n                const pulsing = step === 3 && agent.id === \"coder\";\n\n                return (\n                  <g key={agent.id}>\n                    {/* Agent circle */}\n                    <motion.circle\n                      cx={agent.cx}\n                      cy={agent.cy}\n                      r={AGENT_R}\n                      fill={glowing ? \"#3b82f6\" : palette.edgeStroke}\n                      stroke={glowing ? \"#60a5fa\" : palette.labelFill}\n                      strokeWidth={2}\n                      animate={{\n                        scale: pulsing ? [1, 1.08, 1] : 1,\n                        fill: glowing ? \"#3b82f6\" : palette.edgeStroke,\n                      }}\n                      transition={\n                        pulsing\n                          ? { duration: 0.8, repeat: Infinity, ease: \"easeInOut\" }\n                          : { duration: 0.4 }\n                      }\n                      filter={glowing ? \"url(#agent-glow)\" : undefined}\n                    />\n                    {/* Agent label */}\n                    <text\n                      x={agent.cx}\n                      y={agent.cy + 1}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fill=\"white\"\n                      fontSize={12}\n                      fontWeight={700}\n                    >\n                      {agent.label}\n                    </text>\n\n                    {/* Inbox tray (file icon style) */}\n                    <rect\n                      x={agent.cx - TRAY_W / 2}\n                      y={agent.cy + TRAY_OFFSET_Y}\n                      width={TRAY_W}\n                      height={TRAY_H}\n                      rx={3}\n                      fill={trayHasMessage(agent.id, step) ? \"#fef3c7\" : palette.nodeFill}\n                      stroke={trayHasMessage(agent.id, step) ? \"#f59e0b\" : palette.nodeStroke}\n                      strokeWidth={1}\n                    />\n                    <text\n                      x={agent.cx}\n                      y={agent.cy + TRAY_OFFSET_Y + TRAY_H / 2 + 1}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fontSize={8}\n                      fontFamily=\"monospace\"\n                      fill={palette.labelFill}\n                    >\n                      {agent.inbox}\n                    </text>\n                  </g>\n                );\n              })}\n\n              {/* Step 0: team config card */}\n              {step === 0 && (\n                <motion.g\n                  initial={{ opacity: 0, y: 8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  transition={{ duration: 0.5 }}\n                >\n                  <rect x={12} y={12} width={100} height={44} rx={4} fill=\"#f0f9ff\" stroke=\"#bae6fd\" strokeWidth={1} />\n                  <text x={20} y={28} fontSize={7} fontFamily=\"monospace\" fill=\"#0284c7\" fontWeight={600}>\n                    team.config\n                  </text>\n                  <text x={20} y={40} fontSize={6} fontFamily=\"monospace\" fill=\"#0369a1\">\n                    workers: [coder, reviewer]\n                  </text>\n                </motion.g>\n              )}\n\n              {/* Step 1: message from Lead to Coder inbox */}\n              <AnimatePresence>\n                {step === 1 && (\n                  <TravelingMessage\n                    key=\"msg-lead-coder\"\n                    fromX={agentById(\"lead\").cx}\n                    fromY={agentById(\"lead\").cy + AGENT_R}\n                    toX={agentById(\"coder\").cx}\n                    toY={agentById(\"coder\").cy + TRAY_OFFSET_Y + TRAY_H / 2}\n                    label=\"task:login\"\n                  />\n                )}\n              </AnimatePresence>\n\n              {/* Step 2: message from Coder inbox to Coder circle */}\n              <AnimatePresence>\n                {step === 2 && (\n                  <TravelingMessage\n                    key=\"msg-inbox-coder\"\n                    fromX={agentById(\"coder\").cx}\n                    fromY={agentById(\"coder\").cy + TRAY_OFFSET_Y + TRAY_H / 2}\n                    toX={agentById(\"coder\").cx}\n                    toY={agentById(\"coder\").cy}\n                    label=\"task:login\"\n                  />\n                )}\n              </AnimatePresence>\n\n              {/* Step 3: Coder working, result appears */}\n              <AnimatePresence>\n                {step === 3 && (\n                  <motion.g\n                    key=\"result-msg\"\n                    initial={{ opacity: 0, scale: 0.5 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    transition={{ delay: 0.8, duration: 0.4 }}\n                  >\n                    <rect\n                      x={agentById(\"coder\").cx + AGENT_R + 8}\n                      y={agentById(\"coder\").cy - MSG_H / 2}\n                      width={MSG_W + 10}\n                      height={MSG_H}\n                      rx={4}\n                      fill=\"#10b981\"\n                    />\n                    <text\n                      x={agentById(\"coder\").cx + AGENT_R + 8 + (MSG_W + 10) / 2}\n                      y={agentById(\"coder\").cy + 1}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fill=\"white\"\n                      fontSize={8}\n                      fontWeight={600}\n                    >\n                      result:done\n                    </text>\n                  </motion.g>\n                )}\n              </AnimatePresence>\n\n              {/* Step 4: Coder result message travels to Reviewer inbox */}\n              <AnimatePresence>\n                {step === 4 && (\n                  <TravelingMessage\n                    key=\"msg-coder-reviewer\"\n                    fromX={agentById(\"coder\").cx + AGENT_R + 8 + (MSG_W + 10) / 2}\n                    fromY={agentById(\"coder\").cy}\n                    toX={agentById(\"reviewer\").cx}\n                    toY={agentById(\"reviewer\").cy + TRAY_OFFSET_Y + TRAY_H / 2}\n                    label=\"result:done\"\n                  />\n                )}\n              </AnimatePresence>\n\n              {/* Step 5: Reviewer reads inbox, sends feedback to Lead */}\n              <AnimatePresence>\n                {step === 5 && (\n                  <>\n                    <TravelingMessage\n                      key=\"msg-reviewer-read\"\n                      fromX={agentById(\"reviewer\").cx}\n                      fromY={agentById(\"reviewer\").cy + TRAY_OFFSET_Y + TRAY_H / 2}\n                      toX={agentById(\"reviewer\").cx}\n                      toY={agentById(\"reviewer\").cy}\n                      label=\"result:done\"\n                      delay={0}\n                    />\n                    <TravelingMessage\n                      key=\"msg-reviewer-lead\"\n                      fromX={agentById(\"reviewer\").cx}\n                      fromY={agentById(\"reviewer\").cy}\n                      toX={agentById(\"lead\").cx}\n                      toY={agentById(\"lead\").cy + TRAY_OFFSET_Y + TRAY_H / 2}\n                      label=\"feedback\"\n                      delay={1.0}\n                    />\n                  </>\n                )}\n              </AnimatePresence>\n\n              {/* Step 6: filesystem tree */}\n              {step === 6 && (\n                <motion.g\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  transition={{ duration: 0.6 }}\n                >\n                  <rect x={SVG_W / 2 - 110} y={SVG_H - 80} width={220} height={68} rx={6} fill={palette.bgSubtle} stroke={palette.nodeStroke} strokeWidth={1} />\n                  <text x={SVG_W / 2 - 96} y={SVG_H - 60} fontSize={8} fontFamily=\"monospace\" fill={palette.labelFill}>\n                    .claude/teams/project/\n                  </text>\n                  <text x={SVG_W / 2 - 82} y={SVG_H - 48} fontSize={8} fontFamily=\"monospace\" fill=\"#60a5fa\">\n                    lead.jsonl\n                  </text>\n                  <text x={SVG_W / 2 - 82} y={SVG_H - 36} fontSize={8} fontFamily=\"monospace\" fill=\"#60a5fa\">\n                    coder.jsonl\n                  </text>\n                  <text x={SVG_W / 2 - 82} y={SVG_H - 24} fontSize={8} fontFamily=\"monospace\" fill=\"#60a5fa\">\n                    reviewer.jsonl\n                  </text>\n                </motion.g>\n              )}\n            </svg>\n          </div>\n        </div>\n\n        {/* Step controls */}\n        <div className=\"mt-4\">\n          <StepControls\n            currentStep={vis.currentStep}\n            totalSteps={vis.totalSteps}\n            onPrev={vis.prev}\n            onNext={vis.next}\n            onReset={vis.reset}\n            isPlaying={vis.isPlaying}\n            onToggleAutoPlay={vis.toggleAutoPlay}\n            stepTitle={STEPS[step].title}\n            stepDescription={STEPS[step].desc}\n          />\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s10-team-protocols.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\nimport { useSvgPalette } from \"@/hooks/useDarkMode\";\n\ntype Protocol = \"shutdown\" | \"plan\";\n\n// -- Layout constants for the sequence diagram --\nconst SVG_W = 560;\nconst SVG_H = 360;\nconst LIFELINE_LEFT_X = 140;\nconst LIFELINE_RIGHT_X = 420;\nconst LIFELINE_TOP = 60;\nconst LIFELINE_BOTTOM = 330;\nconst ACTIVATION_W = 12;\nconst ARROW_Y_START = 110;\nconst ARROW_Y_GAP = 70;\n\n// Request ID shown on message tags\nconst REQUEST_ID = \"req_abc\";\n\n// -- Shutdown protocol step definitions --\nconst SHUTDOWN_STEPS = [\n  { title: \"Structured Protocols\", desc: \"Protocols define structured message exchanges with correlated request IDs.\" },\n  { title: \"Shutdown Request\", desc: \"The leader initiates shutdown. The request_id links the request to its response.\" },\n  { title: \"Teammate Decides\", desc: \"The teammate can accept or reject. It's not a forced kill -- it's a polite request.\" },\n  { title: \"Approved\", desc: \"Same request_id in the response. Teammate exits cleanly.\" },\n];\n\n// -- Plan approval protocol step definitions --\nconst PLAN_STEPS = [\n  { title: \"Plan Approval\", desc: \"Teammates in plan_mode must get approval before implementing changes.\" },\n  { title: \"Submit Plan\", desc: \"The teammate designs a plan and sends it to the leader for review.\" },\n  { title: \"Leader Reviews\", desc: \"Leader reviews and approves or rejects with feedback. Same request-response pattern.\" },\n];\n\n// Horizontal arrow between lifelines\nfunction SequenceArrow({\n  y,\n  direction,\n  label,\n  tagLabel,\n  color,\n  tagBg,\n  tagStroke,\n  tagText,\n}: {\n  y: number;\n  direction: \"right\" | \"left\";\n  label: string;\n  tagLabel?: string;\n  color: string;\n  tagBg?: string;\n  tagStroke?: string;\n  tagText?: string;\n}) {\n  const fromX = direction === \"right\" ? LIFELINE_LEFT_X + ACTIVATION_W / 2 : LIFELINE_RIGHT_X - ACTIVATION_W / 2;\n  const toX = direction === \"right\" ? LIFELINE_RIGHT_X - ACTIVATION_W / 2 : LIFELINE_LEFT_X + ACTIVATION_W / 2;\n  const arrowTip = direction === \"right\" ? toX - 6 : toX + 6;\n  const labelX = (fromX + toX) / 2;\n\n  return (\n    <motion.g\n      initial={{ opacity: 0, y: -10 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.5 }}\n    >\n      {/* Arrow line */}\n      <line\n        x1={fromX}\n        y1={y}\n        x2={toX}\n        y2={y}\n        stroke={color}\n        strokeWidth={2}\n      />\n      {/* Arrow head */}\n      <polygon\n        points={\n          direction === \"right\"\n            ? `${toX},${y} ${arrowTip},${y - 4} ${arrowTip},${y + 4}`\n            : `${toX},${y} ${arrowTip},${y - 4} ${arrowTip},${y + 4}`\n        }\n        fill={color}\n      />\n      {/* Message label */}\n      <text\n        x={labelX}\n        y={y - 10}\n        textAnchor=\"middle\"\n        fontSize={8}\n        fontFamily=\"monospace\"\n        fontWeight={600}\n        fill={color}\n      >\n        {label}\n      </text>\n      {/* Request ID tag */}\n      {tagLabel && (\n        <g>\n          <rect\n            x={labelX - 36}\n            y={y + 4}\n            width={72}\n            height={16}\n            rx={3}\n            fill={tagBg || \"#f5f3ff\"}\n            stroke={tagStroke || \"#c4b5fd\"}\n            strokeWidth={0.5}\n          />\n          <text\n            x={labelX}\n            y={y + 14}\n            textAnchor=\"middle\"\n            fontSize={6}\n            fontFamily=\"monospace\"\n            fill={tagText || \"#7c3aed\"}\n          >\n            {tagLabel}\n          </text>\n        </g>\n      )}\n    </motion.g>\n  );\n}\n\n// Decision diamond on a lifeline\nfunction DecisionBox({ x, y }: { x: number; y: number }) {\n  const size = 14;\n  return (\n    <motion.g\n      initial={{ opacity: 0, scale: 0.5 }}\n      animate={{ opacity: 1, scale: 1 }}\n      transition={{ duration: 0.4 }}\n    >\n      <polygon\n        points={`${x},${y - size} ${x + size},${y} ${x},${y + size} ${x - size},${y}`}\n        fill=\"#fef3c7\"\n        stroke=\"#f59e0b\"\n        strokeWidth={1}\n      />\n      <text x={x} y={y + 1} textAnchor=\"middle\" dominantBaseline=\"middle\" fontSize={7} fontWeight={700} fill=\"#92400e\">\n        ?\n      </text>\n      <text x={x + size + 6} y={y - 4} fontSize={6} fontFamily=\"monospace\" fill=\"#10b981\">\n        approve\n      </text>\n      <text x={x + size + 6} y={y + 6} fontSize={6} fontFamily=\"monospace\" fill=\"#ef4444\">\n        reject\n      </text>\n    </motion.g>\n  );\n}\n\n// Activation bar on a lifeline\nfunction ActivationBar({\n  x,\n  yStart,\n  yEnd,\n  color,\n}: {\n  x: number;\n  yStart: number;\n  yEnd: number;\n  color: string;\n}) {\n  return (\n    <motion.rect\n      x={x - ACTIVATION_W / 2}\n      y={yStart}\n      width={ACTIVATION_W}\n      height={yEnd - yStart}\n      rx={2}\n      fill={color}\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 0.6 }}\n      transition={{ duration: 0.4 }}\n    />\n  );\n}\n\nexport default function TeamProtocols({ title }: { title?: string }) {\n  const [protocol, setProtocol] = useState<Protocol>(\"shutdown\");\n\n  const totalSteps = protocol === \"shutdown\" ? SHUTDOWN_STEPS.length : PLAN_STEPS.length;\n  const steps = protocol === \"shutdown\" ? SHUTDOWN_STEPS : PLAN_STEPS;\n\n  const vis = useSteppedVisualization({ totalSteps, autoPlayInterval: 2500 });\n  const step = vis.currentStep;\n  const palette = useSvgPalette();\n\n  const switchProtocol = (p: Protocol) => {\n    setProtocol(p);\n    vis.reset();\n  };\n\n  const leftLabel = protocol === \"shutdown\" ? \"Leader\" : \"Leader\";\n  const rightLabel = protocol === \"shutdown\" ? \"Teammate\" : \"Teammate\";\n\n  return (\n    <section className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"FSM Team Protocols\"}\n      </h2>\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900 min-h-[500px]\">\n        {/* Protocol toggle */}\n        <div className=\"flex justify-center gap-2 mb-4\">\n          <button\n            onClick={() => switchProtocol(\"shutdown\")}\n            className={`rounded-md px-4 py-1.5 text-xs font-medium transition-colors ${\n              protocol === \"shutdown\"\n                ? \"bg-blue-500 text-white\"\n                : \"bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700\"\n            }`}\n          >\n            Shutdown Protocol\n          </button>\n          <button\n            onClick={() => switchProtocol(\"plan\")}\n            className={`rounded-md px-4 py-1.5 text-xs font-medium transition-colors ${\n              protocol === \"plan\"\n                ? \"bg-emerald-500 text-white\"\n                : \"bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700\"\n            }`}\n          >\n            Plan Approval Protocol\n          </button>\n        </div>\n\n        {/* Sequence diagram SVG */}\n        <svg viewBox={`0 0 ${SVG_W} ${SVG_H}`} className=\"w-full\">\n          <defs>\n            <marker\n              id=\"seq-arrow\"\n              viewBox=\"0 0 10 10\"\n              refX=\"9\"\n              refY=\"5\"\n              markerWidth=\"5\"\n              markerHeight=\"5\"\n              orient=\"auto-start-reverse\"\n            >\n              <path d=\"M 0 0 L 10 5 L 0 10 z\" fill={palette.arrowFill} />\n            </marker>\n          </defs>\n\n          {/* Lifeline headers */}\n          <rect x={LIFELINE_LEFT_X - 40} y={20} width={80} height={28} rx={6} fill=\"#3b82f6\" />\n          <text x={LIFELINE_LEFT_X} y={37} textAnchor=\"middle\" dominantBaseline=\"middle\" fill=\"white\" fontSize={11} fontWeight={700}>\n            {leftLabel}\n          </text>\n\n          <rect x={LIFELINE_RIGHT_X - 40} y={20} width={80} height={28} rx={6} fill=\"#8b5cf6\" />\n          <text x={LIFELINE_RIGHT_X} y={37} textAnchor=\"middle\" dominantBaseline=\"middle\" fill=\"white\" fontSize={11} fontWeight={700}>\n            {rightLabel}\n          </text>\n\n          {/* Lifeline dashed lines */}\n          <line\n            x1={LIFELINE_LEFT_X}\n            y1={LIFELINE_TOP}\n            x2={LIFELINE_LEFT_X}\n            y2={LIFELINE_BOTTOM}\n            stroke={palette.edgeStroke}\n            strokeWidth={1}\n            strokeDasharray=\"6 4\"\n          />\n          <line\n            x1={LIFELINE_RIGHT_X}\n            y1={LIFELINE_TOP}\n            x2={LIFELINE_RIGHT_X}\n            y2={LIFELINE_BOTTOM}\n            stroke={palette.edgeStroke}\n            strokeWidth={1}\n            strokeDasharray=\"6 4\"\n          />\n\n          <AnimatePresence mode=\"wait\">\n            {protocol === \"shutdown\" && (\n              <g key=\"shutdown\">\n                {/* Activation bars appear as needed */}\n                {step >= 1 && (\n                  <ActivationBar\n                    x={LIFELINE_LEFT_X}\n                    yStart={ARROW_Y_START - 10}\n                    yEnd={step >= 3 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 20 : ARROW_Y_START + 30}\n                    color=\"#3b82f6\"\n                  />\n                )}\n                {step >= 1 && (\n                  <ActivationBar\n                    x={LIFELINE_RIGHT_X}\n                    yStart={ARROW_Y_START - 5}\n                    yEnd={step >= 3 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 15 : ARROW_Y_START + ARROW_Y_GAP + 20}\n                    color=\"#8b5cf6\"\n                  />\n                )}\n\n                {/* Step 1: shutdown_request arrow (Leader -> Teammate) */}\n                {step >= 1 && (\n                  <SequenceArrow\n                    y={ARROW_Y_START}\n                    direction=\"right\"\n                    label=\"shutdown_request\"\n                    tagLabel={`request_id: ${REQUEST_ID}`}\n                    color=\"#3b82f6\"\n                    tagBg={palette.bgSubtle}\n                    tagStroke={palette.nodeStroke}\n                    tagText={palette.nodeText}\n                  />\n                )}\n\n                {/* Step 2: decision box on teammate lifeline */}\n                {step >= 2 && (\n                  <DecisionBox\n                    x={LIFELINE_RIGHT_X + 50}\n                    y={ARROW_Y_START + ARROW_Y_GAP}\n                  />\n                )}\n\n                {/* Step 3: shutdown_response arrow (Teammate -> Leader) */}\n                {step >= 3 && (\n                  <SequenceArrow\n                    y={ARROW_Y_START + ARROW_Y_GAP * 2}\n                    direction=\"left\"\n                    label=\"shutdown_response { approve: true }\"\n                    tagLabel={`request_id: ${REQUEST_ID}`}\n                    color=\"#10b981\"\n                    tagBg={palette.bgSubtle}\n                    tagStroke={palette.nodeStroke}\n                    tagText={palette.nodeText}\n                  />\n                )}\n\n                {/* Step 3: exit annotation */}\n                {step >= 3 && (\n                  <motion.g\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    transition={{ delay: 0.3 }}\n                  >\n                    <line\n                      x1={LIFELINE_RIGHT_X - 10}\n                      y1={ARROW_Y_START + ARROW_Y_GAP * 2 + 20}\n                      x2={LIFELINE_RIGHT_X + 10}\n                      y2={ARROW_Y_START + ARROW_Y_GAP * 2 + 36}\n                      stroke=\"#ef4444\"\n                      strokeWidth={2}\n                    />\n                    <line\n                      x1={LIFELINE_RIGHT_X + 10}\n                      y1={ARROW_Y_START + ARROW_Y_GAP * 2 + 20}\n                      x2={LIFELINE_RIGHT_X - 10}\n                      y2={ARROW_Y_START + ARROW_Y_GAP * 2 + 36}\n                      stroke=\"#ef4444\"\n                      strokeWidth={2}\n                    />\n                    <text\n                      x={LIFELINE_RIGHT_X + 24}\n                      y={ARROW_Y_START + ARROW_Y_GAP * 2 + 32}\n                      fontSize={8}\n                      fill=\"#ef4444\"\n                      fontWeight={600}\n                    >\n                      exit\n                    </text>\n                  </motion.g>\n                )}\n              </g>\n            )}\n\n            {protocol === \"plan\" && (\n              <g key=\"plan\">\n                {/* Activation bars */}\n                {step >= 1 && (\n                  <ActivationBar\n                    x={LIFELINE_RIGHT_X}\n                    yStart={ARROW_Y_START - 10}\n                    yEnd={step >= 2 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 15 : ARROW_Y_START + 30}\n                    color=\"#8b5cf6\"\n                  />\n                )}\n                {step >= 1 && (\n                  <ActivationBar\n                    x={LIFELINE_LEFT_X}\n                    yStart={ARROW_Y_START - 5}\n                    yEnd={step >= 2 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 15 : ARROW_Y_START + ARROW_Y_GAP + 10}\n                    color=\"#3b82f6\"\n                  />\n                )}\n\n                {/* Step 1: plan submission arrow (Teammate -> Leader) */}\n                {step >= 1 && (\n                  <SequenceArrow\n                    y={ARROW_Y_START}\n                    direction=\"left\"\n                    label=\"exit_plan_mode { plan }\"\n                    tagLabel={`request_id: ${REQUEST_ID}`}\n                    color=\"#8b5cf6\"\n                    tagBg={palette.bgSubtle}\n                    tagStroke={palette.nodeStroke}\n                    tagText={palette.nodeText}\n                  />\n                )}\n\n                {/* Step 1: plan content box */}\n                {step >= 1 && (\n                  <motion.g\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    transition={{ delay: 0.4 }}\n                  >\n                    <rect\n                      x={20}\n                      y={ARROW_Y_START + 20}\n                      width={95}\n                      height={50}\n                      rx={4}\n                      fill={palette.bgSubtle}\n                      stroke={palette.nodeStroke}\n                      strokeWidth={0.5}\n                    />\n                    <text x={28} y={ARROW_Y_START + 34} fontSize={6} fontFamily=\"monospace\" fill={palette.nodeText} fontWeight={600}>\n                      Plan:\n                    </text>\n                    <text x={28} y={ARROW_Y_START + 44} fontSize={5.5} fontFamily=\"monospace\" fill={palette.labelFill}>\n                      1. Add error handler\n                    </text>\n                    <text x={28} y={ARROW_Y_START + 54} fontSize={5.5} fontFamily=\"monospace\" fill={palette.labelFill}>\n                      2. Update tests\n                    </text>\n                    <text x={28} y={ARROW_Y_START + 64} fontSize={5.5} fontFamily=\"monospace\" fill={palette.labelFill}>\n                      3. Refactor module\n                    </text>\n                  </motion.g>\n                )}\n\n                {/* Step 2: approval response arrow (Leader -> Teammate) */}\n                {step >= 2 && (\n                  <SequenceArrow\n                    y={ARROW_Y_START + ARROW_Y_GAP * 2}\n                    direction=\"right\"\n                    label=\"plan_approval_response { approve: true }\"\n                    tagLabel={`request_id: ${REQUEST_ID}`}\n                    color=\"#10b981\"\n                    tagBg={palette.bgSubtle}\n                    tagStroke={palette.nodeStroke}\n                    tagText={palette.nodeText}\n                  />\n                )}\n\n                {/* Step 2: checkmark */}\n                {step >= 2 && (\n                  <motion.g\n                    initial={{ opacity: 0, scale: 0.5 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    transition={{ delay: 0.3 }}\n                  >\n                    <circle cx={LIFELINE_RIGHT_X + 40} cy={ARROW_Y_START + ARROW_Y_GAP * 2} r={10} fill=\"#10b981\" />\n                    <text\n                      x={LIFELINE_RIGHT_X + 40}\n                      y={ARROW_Y_START + ARROW_Y_GAP * 2 + 1}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fontSize={10}\n                      fill=\"white\"\n                      fontWeight={700}\n                    >\n                      OK\n                    </text>\n                  </motion.g>\n                )}\n              </g>\n            )}\n          </AnimatePresence>\n        </svg>\n\n        {/* Step controls */}\n        <div className=\"mt-4\">\n          <StepControls\n            currentStep={vis.currentStep}\n            totalSteps={vis.totalSteps}\n            onPrev={vis.prev}\n            onNext={vis.next}\n            onReset={vis.reset}\n            isPlaying={vis.isPlaying}\n            onToggleAutoPlay={vis.toggleAutoPlay}\n            stepTitle={steps[step].title}\n            stepDescription={steps[step].desc}\n          />\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s11-autonomous-agents.tsx",
    "content": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\nimport { useSvgPalette } from \"@/hooks/useDarkMode\";\n\n// -- FSM states and their layout positions (diamond: idle top, poll right, claim bottom, work left) --\ntype Phase = \"idle\" | \"poll\" | \"claim\" | \"work\";\n\nconst FSM_CX = 110;\nconst FSM_CY = 110;\nconst FSM_R = 65;\nconst FSM_STATE_R = 22;\n\nconst FSM_STATES: { id: Phase; label: string; angle: number }[] = [\n  { id: \"idle\", label: \"idle\", angle: -Math.PI / 2 },\n  { id: \"poll\", label: \"poll\", angle: 0 },\n  { id: \"claim\", label: \"claim\", angle: Math.PI / 2 },\n  { id: \"work\", label: \"work\", angle: Math.PI },\n];\n\nconst FSM_TRANSITIONS: { from: Phase; to: Phase }[] = [\n  { from: \"idle\", to: \"poll\" },\n  { from: \"poll\", to: \"claim\" },\n  { from: \"claim\", to: \"work\" },\n  { from: \"work\", to: \"idle\" },\n];\n\nfunction fsmPos(angle: number) {\n  return { x: FSM_CX + FSM_R * Math.cos(angle), y: FSM_CY + FSM_R * Math.sin(angle) };\n}\n\nconst PHASE_COLORS: Record<Phase, string> = {\n  idle: \"#a1a1aa\",\n  poll: \"#f59e0b\",\n  claim: \"#3b82f6\",\n  work: \"#10b981\",\n};\n\n// -- Task board data --\ninterface TaskRow {\n  id: string;\n  name: string;\n  status: \"unclaimed\" | \"active\" | \"complete\";\n  owner: string;\n}\n\nconst INITIAL_TASKS: TaskRow[] = [\n  { id: \"T1\", name: \"Fix auth bug\", status: \"unclaimed\", owner: \"-\" },\n  { id: \"T2\", name: \"Add rate limiter\", status: \"unclaimed\", owner: \"-\" },\n  { id: \"T3\", name: \"Write tests\", status: \"unclaimed\", owner: \"-\" },\n  { id: \"T4\", name: \"Update API docs\", status: \"unclaimed\", owner: \"-\" },\n];\n\n// Agent positions around the task board (left panel)\nconst BOARD_CX = 140;\nconst BOARD_CY = 90;\nconst AGENT_ORBIT = 85;\nconst AGENT_R = 20;\n\nconst AGENT_ANGLES = [-Math.PI / 2, Math.PI / 6, (5 * Math.PI) / 6];\n\nfunction agentPos(index: number) {\n  const angle = AGENT_ANGLES[index];\n  return { x: BOARD_CX + AGENT_ORBIT * Math.cos(angle), y: BOARD_CY + AGENT_ORBIT * Math.sin(angle) };\n}\n\n// -- Step definitions --\nconst STEPS = [\n  { title: \"Self-Governing Agents\", desc: \"Autonomous agents need no coordinator. They govern themselves with an idle-poll-claim-work cycle.\" },\n  { title: \"Idle Timer\", desc: \"Each idle agent counts rounds. A timeout triggers self-directed task polling.\" },\n  { title: \"Poll Task Board\", desc: \"Timeout! The agent reads the task board looking for unclaimed work.\" },\n  { title: \"Claim Task\", desc: \"The agent writes its name to the task record. Atomic, no conflicts.\" },\n  { title: \"Work\", desc: \"The agent works on the claimed task using its own agent loop.\" },\n  { title: \"Independent Polling\", desc: \"Multiple agents poll and claim independently. No central coordinator needed.\" },\n  { title: \"Complete & Reset\", desc: \"Task done. Agent returns to idle. The cycle repeats.\" },\n  { title: \"Self-Organization\", desc: \"Three agents, zero coordination overhead. Polling + timeout = emergent organization.\" },\n];\n\n// Per-step state for each agent\ninterface AgentState {\n  phase: Phase;\n  timerFill: number;\n  color: string;\n  taskClaim: string | null;\n}\n\nfunction getAgentStates(step: number): AgentState[] {\n  const idle: AgentState = { phase: \"idle\", timerFill: 0, color: PHASE_COLORS.idle, taskClaim: null };\n\n  switch (step) {\n    case 0:\n      return [\n        { ...idle },\n        { ...idle },\n        { ...idle },\n      ];\n    case 1:\n      return [\n        { phase: \"idle\", timerFill: 0.6, color: PHASE_COLORS.idle, taskClaim: null },\n        { ...idle },\n        { ...idle },\n      ];\n    case 2:\n      return [\n        { phase: \"poll\", timerFill: 1.0, color: PHASE_COLORS.poll, taskClaim: null },\n        { ...idle },\n        { ...idle },\n      ];\n    case 3:\n      return [\n        { phase: \"claim\", timerFill: 0, color: PHASE_COLORS.claim, taskClaim: \"T1\" },\n        { ...idle },\n        { ...idle },\n      ];\n    case 4:\n      return [\n        { phase: \"work\", timerFill: 0, color: PHASE_COLORS.work, taskClaim: \"T1\" },\n        { ...idle },\n        { ...idle },\n      ];\n    case 5:\n      return [\n        { phase: \"work\", timerFill: 0, color: PHASE_COLORS.work, taskClaim: \"T1\" },\n        { phase: \"claim\", timerFill: 0, color: PHASE_COLORS.claim, taskClaim: \"T2\" },\n        { ...idle },\n      ];\n    case 6:\n      return [\n        { phase: \"idle\", timerFill: 0, color: PHASE_COLORS.idle, taskClaim: null },\n        { phase: \"work\", timerFill: 0, color: PHASE_COLORS.work, taskClaim: \"T2\" },\n        { ...idle },\n      ];\n    case 7:\n      return [\n        { phase: \"idle\", timerFill: 0, color: PHASE_COLORS.idle, taskClaim: null },\n        { phase: \"work\", timerFill: 0, color: PHASE_COLORS.work, taskClaim: \"T2\" },\n        { phase: \"claim\", timerFill: 0, color: PHASE_COLORS.claim, taskClaim: \"T3\" },\n      ];\n    default:\n      return [{ ...idle }, { ...idle }, { ...idle }];\n  }\n}\n\nfunction getTaskStates(step: number): TaskRow[] {\n  const tasks = INITIAL_TASKS.map((t) => ({ ...t }));\n  if (step >= 3) { tasks[0].status = \"active\"; tasks[0].owner = \"A\"; }\n  if (step >= 5) { tasks[1].status = \"active\"; tasks[1].owner = \"B\"; }\n  if (step >= 6) { tasks[0].status = \"complete\"; }\n  if (step >= 7) { tasks[2].status = \"active\"; tasks[2].owner = \"C\"; }\n  return tasks;\n}\n\nfunction getActivePhase(step: number): Phase {\n  if (step <= 1) return \"idle\";\n  if (step === 2) return \"poll\";\n  if (step === 3) return \"claim\";\n  if (step === 4 || step === 5) return \"work\";\n  if (step === 6) return \"idle\";\n  return \"claim\";\n}\n\n// Ring timer around an agent\nfunction TimerRing({ cx, cy, r, fill }: { cx: number; cy: number; r: number; fill: number }) {\n  if (fill <= 0) return null;\n  const circumference = 2 * Math.PI * (r + 4);\n  const offset = circumference * (1 - fill);\n  return (\n    <motion.circle\n      cx={cx}\n      cy={cy}\n      r={r + 4}\n      fill=\"none\"\n      stroke=\"#f59e0b\"\n      strokeWidth={3}\n      strokeDasharray={circumference}\n      strokeDashoffset={offset}\n      strokeLinecap=\"round\"\n      initial={{ strokeDashoffset: circumference }}\n      animate={{ strokeDashoffset: offset }}\n      transition={{ duration: 0.8, ease: \"easeOut\" }}\n      style={{ transform: \"rotate(-90deg)\", transformOrigin: `${cx}px ${cy}px` }}\n    />\n  );\n}\n\n// FSM arrow between two states\nfunction FSMArrow({ from, to, active, inactiveStroke }: { from: Phase; to: Phase; active: boolean; inactiveStroke: string }) {\n  const fState = FSM_STATES.find((s) => s.id === from)!;\n  const tState = FSM_STATES.find((s) => s.id === to)!;\n  const fPos = fsmPos(fState.angle);\n  const tPos = fsmPos(tState.angle);\n\n  const dx = tPos.x - fPos.x;\n  const dy = tPos.y - fPos.y;\n  const dist = Math.sqrt(dx * dx + dy * dy);\n  const ux = dx / dist;\n  const uy = dy / dist;\n\n  const x1 = fPos.x + ux * FSM_STATE_R;\n  const y1 = fPos.y + uy * FSM_STATE_R;\n  const x2 = tPos.x - ux * (FSM_STATE_R + 6);\n  const y2 = tPos.y - uy * (FSM_STATE_R + 6);\n\n  const perpX = -uy * 12;\n  const perpY = ux * 12;\n  const cx = (x1 + x2) / 2 + perpX;\n  const cy = (y1 + y2) / 2 + perpY;\n\n  return (\n    <g>\n      <path\n        d={`M ${x1} ${y1} Q ${cx} ${cy} ${x2} ${y2}`}\n        fill=\"none\"\n        stroke={active ? PHASE_COLORS[to] : inactiveStroke}\n        strokeWidth={active ? 2 : 1}\n        markerEnd=\"url(#fsm-arrowhead)\"\n      />\n    </g>\n  );\n}\n\nexport default function AutonomousAgents({ title }: { title?: string }) {\n  const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });\n  const step = vis.currentStep;\n  const palette = useSvgPalette();\n\n  const agentStates = getAgentStates(step);\n  const tasks = getTaskStates(step);\n  const activePhase = getActivePhase(step);\n  const agentNames = [\"A\", \"B\", \"C\"];\n\n  return (\n    <section className=\"space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Autonomous Agent Cycle\"}\n      </h2>\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900 min-h-[500px]\">\n        <div className=\"flex flex-col lg:flex-row gap-4\">\n          {/* Left panel: spatial view with agents and task board */}\n          <div className=\"flex-1\">\n            <div className=\"text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-2\">Spatial View</div>\n            <svg viewBox=\"0 0 280 240\" className=\"w-full\">\n              {/* Task board (small table in center) */}\n              <rect x={BOARD_CX - 35} y={BOARD_CY - 20} width={70} height={40} rx={4}\n                fill={palette.bgSubtle} stroke={palette.nodeStroke} strokeWidth={1}\n              />\n              <text x={BOARD_CX} y={BOARD_CY - 8} textAnchor=\"middle\" fontSize={7} fontWeight={600}\n                fill={palette.nodeText}\n              >\n                Task Board\n              </text>\n              <text x={BOARD_CX} y={BOARD_CY + 4} textAnchor=\"middle\" fontSize={6} fontFamily=\"monospace\"\n                fill={palette.labelFill}\n              >\n                {tasks.filter((t) => t.status === \"unclaimed\").length} unclaimed\n              </text>\n              <text x={BOARD_CX} y={BOARD_CY + 14} textAnchor=\"middle\" fontSize={6} fontFamily=\"monospace\"\n                fill=\"#10b981\"\n              >\n                {tasks.filter((t) => t.status === \"complete\").length} complete\n              </text>\n\n              {/* Agents */}\n              {agentStates.map((state, i) => {\n                const pos = agentPos(i);\n                const isPulsing = state.phase === \"work\";\n                const isPolling = state.phase === \"poll\";\n\n                return (\n                  <g key={i}>\n                    {/* Dashed line from agent to board when polling */}\n                    {isPolling && (\n                      <motion.line\n                        x1={pos.x} y1={pos.y} x2={BOARD_CX} y2={BOARD_CY}\n                        stroke=\"#f59e0b\" strokeWidth={1.5} strokeDasharray=\"4 3\"\n                        initial={{ opacity: 0 }} animate={{ opacity: 1 }}\n                        transition={{ duration: 0.3 }}\n                      />\n                    )}\n                    {/* Solid line from agent to board when claiming */}\n                    {state.phase === \"claim\" && (\n                      <motion.line\n                        x1={pos.x} y1={pos.y} x2={BOARD_CX} y2={BOARD_CY}\n                        stroke=\"#3b82f6\" strokeWidth={2}\n                        initial={{ opacity: 0 }} animate={{ opacity: 1 }}\n                        transition={{ duration: 0.3 }}\n                      />\n                    )}\n\n                    {/* Timer ring */}\n                    <TimerRing cx={pos.x} cy={pos.y} r={AGENT_R} fill={state.timerFill} />\n\n                    {/* Agent circle */}\n                    <motion.circle\n                      cx={pos.x} cy={pos.y} r={AGENT_R}\n                      fill={state.color}\n                      stroke={state.phase === \"work\" ? \"#059669\" : palette.nodeStroke}\n                      strokeWidth={1.5}\n                      animate={{\n                        scale: isPulsing ? [1, 1.1, 1] : 1,\n                        fill: state.color,\n                      }}\n                      transition={\n                        isPulsing\n                          ? { duration: 0.8, repeat: Infinity, ease: \"easeInOut\" }\n                          : { duration: 0.4 }\n                      }\n                    />\n                    <text x={pos.x} y={pos.y + 1} textAnchor=\"middle\" dominantBaseline=\"middle\"\n                      fill=\"white\" fontSize={11} fontWeight={700}\n                    >\n                      {agentNames[i]}\n                    </text>\n\n                    {/* Task label below agent when claiming or working */}\n                    {state.taskClaim && (\n                      <motion.text\n                        x={pos.x} y={pos.y + AGENT_R + 12}\n                        textAnchor=\"middle\" fontSize={7} fontFamily=\"monospace\"\n                        fill={state.phase === \"work\" ? \"#10b981\" : \"#3b82f6\"}\n                        fontWeight={600}\n                        initial={{ opacity: 0 }} animate={{ opacity: 1 }}\n                        transition={{ duration: 0.3 }}\n                      >\n                        {state.taskClaim}\n                      </motion.text>\n                    )}\n                  </g>\n                );\n              })}\n            </svg>\n\n            {/* Task table below the spatial view */}\n            <div className=\"mt-2 border border-zinc-200 rounded dark:border-zinc-700 overflow-hidden\">\n              <table className=\"w-full text-[10px]\">\n                <thead>\n                  <tr className=\"bg-zinc-50 dark:bg-zinc-800\">\n                    <th className=\"px-2 py-1 text-left font-medium text-zinc-500 dark:text-zinc-400\">Task</th>\n                    <th className=\"px-2 py-1 text-left font-medium text-zinc-500 dark:text-zinc-400\">Status</th>\n                    <th className=\"px-2 py-1 text-left font-medium text-zinc-500 dark:text-zinc-400\">Owner</th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {tasks.map((task) => (\n                    <tr key={task.id} className=\"border-t border-zinc-100 dark:border-zinc-800\">\n                      <td className=\"px-2 py-1 font-mono text-zinc-700 dark:text-zinc-300\">{task.name}</td>\n                      <td className=\"px-2 py-1\">\n                        <span className={`inline-block rounded px-1.5 py-0.5 text-[9px] font-medium ${\n                          task.status === \"complete\"\n                            ? \"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300\"\n                            : task.status === \"active\"\n                              ? \"bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300\"\n                              : \"bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400\"\n                        }`}>\n                          {task.status}\n                        </span>\n                      </td>\n                      <td className=\"px-2 py-1 font-mono text-zinc-600 dark:text-zinc-400\">{task.owner}</td>\n                    </tr>\n                  ))}\n                </tbody>\n              </table>\n            </div>\n          </div>\n\n          {/* Right panel: FSM state machine diagram */}\n          <div className=\"flex-1\">\n            <div className=\"text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-2\">FSM Cycle</div>\n            <svg viewBox=\"0 0 220 220\" className=\"w-full\">\n              <defs>\n                <marker\n                  id=\"fsm-arrowhead\"\n                  viewBox=\"0 0 10 10\"\n                  refX=\"8\"\n                  refY=\"5\"\n                  markerWidth=\"5\"\n                  markerHeight=\"5\"\n                  orient=\"auto-start-reverse\"\n                >\n                  <path d=\"M 0 0 L 10 5 L 0 10 z\" fill={palette.arrowFill} />\n                </marker>\n              </defs>\n\n              {/* Transition arrows */}\n              {FSM_TRANSITIONS.map((t) => {\n                const isActive =\n                  (activePhase === t.from) ||\n                  (activePhase === t.to && t.from === FSM_TRANSITIONS.find((tr) => tr.to === activePhase)?.from);\n                return (\n                  <FSMArrow\n                    key={`${t.from}-${t.to}`}\n                    from={t.from}\n                    to={t.to}\n                    active={isActive}\n                    inactiveStroke={palette.nodeStroke}\n                  />\n                );\n              })}\n\n              {/* State circles */}\n              {FSM_STATES.map((state) => {\n                const pos = fsmPos(state.angle);\n                const isActive = state.id === activePhase;\n                return (\n                  <g key={state.id}>\n                    <motion.circle\n                      cx={pos.x}\n                      cy={pos.y}\n                      r={FSM_STATE_R}\n                      fill={isActive ? PHASE_COLORS[state.id] : palette.nodeFill}\n                      stroke={isActive ? PHASE_COLORS[state.id] : palette.nodeStroke}\n                      strokeWidth={isActive ? 2 : 1}\n                      animate={{\n                        fill: isActive ? PHASE_COLORS[state.id] : palette.nodeFill,\n                        scale: isActive ? 1.1 : 1,\n                      }}\n                      transition={{ duration: 0.4 }}\n                    />\n                    <text\n                      x={pos.x}\n                      y={pos.y + 1}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fontSize={9}\n                      fontWeight={600}\n                      fill={isActive ? \"white\" : palette.nodeText}\n                    >\n                      {state.label}\n                    </text>\n                  </g>\n                );\n              })}\n            </svg>\n\n            {/* Legend */}\n            <div className=\"mt-2 flex flex-wrap gap-3 justify-center\">\n              {FSM_STATES.map((s) => (\n                <div key={s.id} className=\"flex items-center gap-1\">\n                  <span className=\"inline-block h-2.5 w-2.5 rounded-full\" style={{ backgroundColor: PHASE_COLORS[s.id] }} />\n                  <span className=\"text-[10px] font-mono text-zinc-500 dark:text-zinc-400\">{s.label}</span>\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n\n        {/* Step controls */}\n        <div className=\"mt-4\">\n          <StepControls\n            currentStep={vis.currentStep}\n            totalSteps={vis.totalSteps}\n            onPrev={vis.prev}\n            onNext={vis.next}\n            onReset={vis.reset}\n            isPlaying={vis.isPlaying}\n            onToggleAutoPlay={vis.toggleAutoPlay}\n            stepTitle={STEPS[step].title}\n            stepDescription={STEPS[step].desc}\n          />\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/s12-worktree-task-isolation.tsx",
    "content": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport { useSteppedVisualization } from \"@/hooks/useSteppedVisualization\";\nimport { StepControls } from \"@/components/visualizations/shared/step-controls\";\n\ntype TaskStatus = \"pending\" | \"in_progress\" | \"completed\";\n\ninterface TaskRow {\n  id: number;\n  subject: string;\n  status: TaskStatus;\n  worktree: string;\n}\n\ninterface WorktreeRow {\n  name: string;\n  branch: string;\n  task: string;\n  state: \"none\" | \"active\" | \"kept\" | \"removed\";\n}\n\ninterface Lane {\n  name: string;\n  files: string[];\n  highlight?: boolean;\n}\n\ninterface StepState {\n  title: string;\n  desc: string;\n  tasks: TaskRow[];\n  worktrees: WorktreeRow[];\n  lanes: Lane[];\n  op: string;\n}\n\nconst STEPS: StepState[] = [\n  {\n    title: \"Single Workspace Pain\",\n    desc: \"Two tasks are active, but both edits would hit one directory and collide.\",\n    op: \"task_create x2\",\n    tasks: [\n      { id: 1, subject: \"Auth refactor\", status: \"in_progress\", worktree: \"\" },\n      { id: 2, subject: \"UI login polish\", status: \"in_progress\", worktree: \"\" },\n    ],\n    worktrees: [],\n    lanes: [\n      { name: \"main\", files: [\"auth/service.py\", \"ui/Login.tsx\"], highlight: true },\n      { name: \"wt/auth-refactor\", files: [] },\n      { name: \"wt/ui-login\", files: [] },\n    ],\n  },\n  {\n    title: \"Allocate Lane for Task 1\",\n    desc: \"Create a worktree lane and associate it with task 1 for clear ownership.\",\n    op: \"worktree_create(name='auth-refactor', task_id=1)\",\n    tasks: [\n      { id: 1, subject: \"Auth refactor\", status: \"in_progress\", worktree: \"auth-refactor\" },\n      { id: 2, subject: \"UI login polish\", status: \"in_progress\", worktree: \"\" },\n    ],\n    worktrees: [\n      { name: \"auth-refactor\", branch: \"wt/auth-refactor\", task: \"#1\", state: \"active\" },\n    ],\n    lanes: [\n      { name: \"main\", files: [\"ui/Login.tsx\"] },\n      { name: \"wt/auth-refactor\", files: [\"auth/service.py\"], highlight: true },\n      { name: \"wt/ui-login\", files: [] },\n    ],\n  },\n  {\n    title: \"Allocate Lane for Task 2\",\n    desc: \"Lane creation and task association can be separate. Here task 2 binds after lane creation.\",\n    op: \"worktree_create(name='ui-login')\\ntask_bind_worktree(task_id=2, worktree='ui-login')\",\n    tasks: [\n      { id: 1, subject: \"Auth refactor\", status: \"in_progress\", worktree: \"auth-refactor\" },\n      { id: 2, subject: \"UI login polish\", status: \"in_progress\", worktree: \"ui-login\" },\n    ],\n    worktrees: [\n      { name: \"auth-refactor\", branch: \"wt/auth-refactor\", task: \"#1\", state: \"active\" },\n      { name: \"ui-login\", branch: \"wt/ui-login\", task: \"#2\", state: \"active\" },\n    ],\n    lanes: [\n      { name: \"main\", files: [] },\n      { name: \"wt/auth-refactor\", files: [\"auth/service.py\"] },\n      { name: \"wt/ui-login\", files: [\"ui/Login.tsx\"], highlight: true },\n    ],\n  },\n  {\n    title: \"Run Commands in Isolated Lanes\",\n    desc: \"Each command routes by selected lane directory, not by the shared root.\",\n    op: \"worktree_run('auth-refactor', 'pytest tests/auth -q')\",\n    tasks: [\n      { id: 1, subject: \"Auth refactor\", status: \"in_progress\", worktree: \"auth-refactor\" },\n      { id: 2, subject: \"UI login polish\", status: \"in_progress\", worktree: \"ui-login\" },\n    ],\n    worktrees: [\n      { name: \"auth-refactor\", branch: \"wt/auth-refactor\", task: \"#1\", state: \"active\" },\n      { name: \"ui-login\", branch: \"wt/ui-login\", task: \"#2\", state: \"active\" },\n    ],\n    lanes: [\n      { name: \"main\", files: [] },\n      { name: \"wt/auth-refactor\", files: [\"auth/service.py\", \"tests/auth/test_login.py\"], highlight: true },\n      { name: \"wt/ui-login\", files: [\"ui/Login.tsx\", \"ui/Login.css\"] },\n    ],\n  },\n  {\n    title: \"Keep One Lane, Close Another\",\n    desc: \"Closeout can mix decisions: keep ui-login active for follow-up, remove auth-refactor and complete task 1.\",\n    op: \"worktree_keep('ui-login')\\nworktree_remove('auth-refactor', complete_task=true)\\nworktree_events(limit=10)\",\n    tasks: [\n      { id: 1, subject: \"Auth refactor\", status: \"completed\", worktree: \"\" },\n      { id: 2, subject: \"UI login polish\", status: \"in_progress\", worktree: \"ui-login\" },\n    ],\n    worktrees: [\n      { name: \"auth-refactor\", branch: \"wt/auth-refactor\", task: \"#1\", state: \"removed\" },\n      { name: \"ui-login\", branch: \"wt/ui-login\", task: \"#2\", state: \"kept\" },\n    ],\n    lanes: [\n      { name: \"main\", files: [] },\n      { name: \"wt/auth-refactor\", files: [] },\n      { name: \"wt/ui-login\", files: [\"ui/Login.tsx\"], highlight: true },\n    ],\n  },\n  {\n    title: \"Isolation + Coordination + Events\",\n    desc: \"The board tracks shared truth, worktree lanes isolate execution, and events provide auditable side-channel traces.\",\n    op: \"task_list + worktree_list + worktree_events\",\n    tasks: [\n      { id: 1, subject: \"Auth refactor\", status: \"completed\", worktree: \"\" },\n      { id: 2, subject: \"UI login polish\", status: \"in_progress\", worktree: \"ui-login\" },\n    ],\n    worktrees: [\n      { name: \"auth-refactor\", branch: \"wt/auth-refactor\", task: \"#1\", state: \"removed\" },\n      { name: \"ui-login\", branch: \"wt/ui-login\", task: \"#2\", state: \"kept\" },\n    ],\n    lanes: [\n      { name: \"main\", files: [] },\n      { name: \"wt/auth-refactor\", files: [] },\n      { name: \"wt/ui-login\", files: [\"ui/Login.tsx\"], highlight: true },\n    ],\n  },\n];\n\nfunction statusClass(status: TaskStatus): string {\n  if (status === \"completed\") return \"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300\";\n  if (status === \"in_progress\") return \"bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300\";\n  return \"bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300\";\n}\n\nfunction worktreeClass(state: WorktreeRow[\"state\"]): string {\n  if (state === \"active\") return \"border-emerald-300 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900/20\";\n  if (state === \"kept\") return \"border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20\";\n  if (state === \"removed\") return \"border-zinc-200 bg-zinc-100 opacity-70 dark:border-zinc-700 dark:bg-zinc-800\";\n  return \"border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\";\n}\n\nexport default function WorktreeTaskIsolation({ title }: { title?: string }) {\n  const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2600 });\n  const step = STEPS[vis.currentStep];\n\n  return (\n    <section className=\"min-h-[500px] space-y-4\">\n      <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-zinc-100\">\n        {title || \"Worktree Task Isolation\"}\n      </h2>\n\n      <div className=\"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\">\n        <div className=\"mb-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 font-mono text-xs text-blue-700 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-300\">\n          {step.op}\n        </div>\n\n        <div className=\"grid gap-3 lg:grid-cols-3\">\n          <div className=\"rounded-md border border-zinc-200 dark:border-zinc-700\">\n            <div className=\"border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300\">\n              Task Board (.tasks)\n            </div>\n            <div className=\"space-y-2 p-2\">\n              {step.tasks.map((task) => (\n                <motion.div\n                  key={`${task.id}-${task.status}-${task.worktree}`}\n                  initial={{ opacity: 0, y: 6 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  transition={{ duration: 0.25 }}\n                  className=\"rounded border border-zinc-200 p-2 text-xs dark:border-zinc-700\"\n                >\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"font-mono text-zinc-500 dark:text-zinc-400\">#{task.id}</span>\n                    <span className={`rounded px-1.5 py-0.5 text-[10px] font-semibold ${statusClass(task.status)}`}>\n                      {task.status}\n                    </span>\n                  </div>\n                  <div className=\"mt-1 font-medium text-zinc-800 dark:text-zinc-100\">{task.subject}</div>\n                  <div className=\"mt-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400\">\n                    worktree: {task.worktree || \"-\"}\n                  </div>\n                </motion.div>\n              ))}\n            </div>\n          </div>\n\n          <div className=\"rounded-md border border-zinc-200 dark:border-zinc-700\">\n            <div className=\"border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300\">\n              Worktree Index (.worktrees/index.json)\n            </div>\n            <div className=\"space-y-2 p-2\">\n              {step.worktrees.length === 0 && (\n                <div className=\"rounded border border-dashed border-zinc-300 px-3 py-4 text-center text-xs text-zinc-500 dark:border-zinc-700 dark:text-zinc-400\">\n                  no worktrees yet\n                </div>\n              )}\n              {step.worktrees.map((wt) => (\n                <motion.div\n                  key={`${wt.name}-${wt.state}`}\n                  initial={{ opacity: 0, y: 6 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  transition={{ duration: 0.25 }}\n                  className={`rounded border p-2 text-xs ${worktreeClass(wt.state)}`}\n                >\n                  <div className=\"font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100\">{wt.name}</div>\n                  <div className=\"font-mono text-[10px] text-zinc-500 dark:text-zinc-400\">{wt.branch}</div>\n                  <div className=\"mt-1 text-[10px] text-zinc-600 dark:text-zinc-300\">task: {wt.task}</div>\n                </motion.div>\n              ))}\n            </div>\n          </div>\n\n          <div className=\"rounded-md border border-zinc-200 dark:border-zinc-700\">\n            <div className=\"border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300\">\n              Execution Lanes\n            </div>\n            <div className=\"space-y-2 p-2\">\n              {step.lanes.map((lane) => (\n                <motion.div\n                  key={`${lane.name}-${lane.files.join(\",\")}`}\n                  initial={{ opacity: 0, x: -4 }}\n                  animate={{ opacity: 1, x: 0 }}\n                  transition={{ duration: 0.25 }}\n                  className={`rounded border p-2 text-xs ${\n                    lane.highlight\n                      ? \"border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20\"\n                      : \"border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900\"\n                  }`}\n                >\n                  <div className=\"font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100\">{lane.name}</div>\n                  <div className=\"mt-1 space-y-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400\">\n                    {lane.files.length === 0 ? (\n                      <div>(no changes)</div>\n                    ) : (\n                      lane.files.map((f) => <div key={f}>{f}</div>)\n                    )}\n                  </div>\n                </motion.div>\n              ))}\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-4 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-800/60\">\n          <div className=\"font-medium text-zinc-800 dark:text-zinc-100\">{step.title}</div>\n          <div className=\"text-zinc-600 dark:text-zinc-300\">{step.desc}</div>\n        </div>\n      </div>\n\n      <StepControls\n        currentStep={vis.currentStep}\n        totalSteps={vis.totalSteps}\n        onPrev={vis.prev}\n        onNext={vis.next}\n        onReset={vis.reset}\n        isPlaying={vis.isPlaying}\n        onToggleAutoPlay={vis.toggleAutoPlay}\n        stepTitle={step.title}\n        stepDescription={step.desc}\n      />\n    </section>\n  );\n}\n"
  },
  {
    "path": "web/src/components/visualizations/shared/step-controls.tsx",
    "content": "\"use client\";\n\nimport { Play, Pause, SkipBack, SkipForward, RotateCcw } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface StepControlsProps {\n  currentStep: number;\n  totalSteps: number;\n  onPrev: () => void;\n  onNext: () => void;\n  onReset: () => void;\n  isPlaying: boolean;\n  onToggleAutoPlay: () => void;\n  stepTitle: string;\n  stepDescription: string;\n  className?: string;\n}\n\nexport function StepControls({\n  currentStep,\n  totalSteps,\n  onPrev,\n  onNext,\n  onReset,\n  isPlaying,\n  onToggleAutoPlay,\n  stepTitle,\n  stepDescription,\n  className,\n}: StepControlsProps) {\n  return (\n    <div className={cn(\"space-y-3\", className)}>\n      {/* Annotation */}\n      <div className=\"rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 dark:border-blue-800 dark:bg-blue-950/40\">\n        <div className=\"mb-1 text-sm font-semibold text-blue-900 dark:text-blue-200\">\n          {stepTitle}\n        </div>\n        <div className=\"text-sm text-blue-700 dark:text-blue-300\">\n          {stepDescription}\n        </div>\n      </div>\n\n      {/* Controls */}\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-1\">\n          <button\n            onClick={onReset}\n            className=\"rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            title=\"Reset\"\n          >\n            <RotateCcw size={16} />\n          </button>\n          <button\n            onClick={onPrev}\n            disabled={currentStep === 0}\n            className=\"rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            title=\"Previous step\"\n          >\n            <SkipBack size={16} />\n          </button>\n          <button\n            onClick={onToggleAutoPlay}\n            className=\"rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            title={isPlaying ? \"Pause\" : \"Auto-play\"}\n          >\n            {isPlaying ? <Pause size={16} /> : <Play size={16} />}\n          </button>\n          <button\n            onClick={onNext}\n            disabled={currentStep === totalSteps - 1}\n            className=\"rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            title=\"Next step\"\n          >\n            <SkipForward size={16} />\n          </button>\n        </div>\n\n        {/* Step indicator */}\n        <div className=\"flex items-center gap-2\">\n          <div className=\"flex gap-1\">\n            {Array.from({ length: totalSteps }, (_, i) => (\n              <div\n                key={i}\n                className={cn(\n                  \"h-1.5 w-1.5 rounded-full transition-colors\",\n                  i === currentStep\n                    ? \"bg-blue-500\"\n                    : i < currentStep\n                      ? \"bg-blue-300 dark:bg-blue-700\"\n                      : \"bg-zinc-200 dark:bg-zinc-700\"\n                )}\n              />\n            ))}\n          </div>\n          <span className=\"font-mono text-xs text-zinc-400\">\n            {currentStep + 1}/{totalSteps}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/data/annotations/s01.json",
    "content": "{\n  \"version\": \"s01\",\n  \"decisions\": [\n    {\n      \"id\": \"one-tool-sufficiency\",\n      \"title\": \"Why Bash Alone Is Enough\",\n      \"description\": \"Bash can read files, write files, run arbitrary programs, pipe data between processes, and manage the filesystem. Any additional tool (read_file, write_file, etc.) would be a strict subset of what bash already provides. Adding more tools doesn't unlock new capabilities -- it just adds surface area for confusion. The model has to learn fewer tool schemas, and the implementation stays under 100 lines. This is the minimal viable agent: one tool, one loop.\",\n      \"alternatives\": \"We could have started with a richer toolset (file I/O, HTTP, database), but that would obscure the core insight: an LLM with a shell is already a general-purpose agent. Starting minimal also makes it obvious what each subsequent version actually adds.\",\n      \"zh\": {\n        \"title\": \"为什么仅靠 Bash 就够了\",\n        \"description\": \"Bash 能读写文件、运行任意程序、在进程间传递数据、管理文件系统。任何额外的工具（read_file、write_file 等）都只是 bash 已有能力的子集。增加工具并不会解锁新能力，只会增加模型需要理解的接口。模型只需学习一个工具的 schema，实现代码不超过 100 行。这就是最小可行 agent：一个工具，一个循环。\"\n      },\n      \"ja\": {\n        \"title\": \"Bash だけで十分な理由\",\n        \"description\": \"Bash はファイルの読み書き、任意のプログラムの実行、プロセス間のデータパイプ、ファイルシステムの管理が可能です。追加のツール（read_file、write_file など）は bash が既に提供している機能の部分集合に過ぎません。ツールを増やしても新しい能力は得られず、モデルが理解すべきインターフェースが増えるだけです。モデルが学習するスキーマは1つだけで、実装は100行以内に収まります。これが最小限の実用的エージェント：1つのツール、1つのループです。\"\n      }\n    },\n    {\n      \"id\": \"process-as-subagent\",\n      \"title\": \"Recursive Process Spawning as Subagent Mechanism\",\n      \"description\": \"When the agent runs `python v0.py \\\"subtask\\\"`, it spawns a completely new process with a fresh LLM context. This child process is effectively a subagent: it has its own system prompt, its own conversation history, and its own task focus. When it finishes, the parent gets the stdout result. This is subagent delegation without any framework -- just Unix process semantics. Each child process naturally isolates concerns because it literally cannot see the parent's context.\",\n      \"alternatives\": \"A framework-level subagent system (like v3's Task tool) gives more control over what tools the subagent can access and how results are returned. But at v0, the point is to show that process spawning is the most primitive form of agent delegation -- no shared memory, no message passing, just stdin/stdout.\",\n      \"zh\": {\n        \"title\": \"用递归进程创建实现子代理机制\",\n        \"description\": \"当 agent 执行 `python v0.py \\\"subtask\\\"` 时，它会创建一个全新的进程，拥有全新的 LLM 上下文。这个子进程实际上就是一个子代理：有自己的系统提示词、对话历史和任务焦点。子进程完成后，父进程通过 stdout 获取结果。这就是不依赖任何框架的子代理委派——纯粹的 Unix 进程语义。每个子进程天然隔离关注点，因为它根本看不到父进程的上下文。\"\n      },\n      \"ja\": {\n        \"title\": \"再帰プロセス生成によるサブエージェント機構\",\n        \"description\": \"エージェントが `python v0.py \\\"subtask\\\"` を実行すると、新しい LLM コンテキストを持つ完全に新しいプロセスが生成されます。この子プロセスは事実上サブエージェントです：独自のシステムプロンプト、会話履歴、タスクフォーカスを持ちます。完了すると、親プロセスは stdout で結果を受け取ります。これはフレームワークなしのサブエージェント委任です——共有メモリもメッセージパッシングもなく、stdin/stdout だけです。各子プロセスは親のコンテキストを参照できないため、関心の分離が自然に実現されます。\"\n      }\n    },\n    {\n      \"id\": \"model-drives-everything\",\n      \"title\": \"No Planning Framework -- The Model Decides\",\n      \"description\": \"There is no planner, no task queue, no state machine. The system prompt tells the model how to approach problems, and the model decides what bash command to run next based on the conversation so far. This is intentional: at this level, adding a planning layer would be premature abstraction. The model's chain-of-thought IS the plan. The agent loop just keeps asking the model what to do until it stops requesting tools.\",\n      \"alternatives\": \"Later versions (v2) add explicit planning via TodoWrite. But v0 proves that implicit planning through the model's reasoning is sufficient for many tasks. The planning framework only becomes necessary when you need external visibility into the agent's intentions.\",\n      \"zh\": {\n        \"title\": \"没有规划框架——由模型自行决策\",\n        \"description\": \"没有规划器，没有任务队列，没有状态机。系统提示词告诉模型如何处理问题，模型根据对话历史决定下一步执行什么 bash 命令。这是有意为之的：在这个层级，添加规划层属于过早抽象。模型的思维链本身就是计划。agent 循环只是不断询问模型下一步做什么，直到模型不再请求工具为止。\"\n      },\n      \"ja\": {\n        \"title\": \"計画フレームワークなし——モデルが全てを決定\",\n        \"description\": \"プランナーもタスクキューも状態マシンもありません。システムプロンプトがモデルに問題の取り組み方を伝え、モデルがこれまでの会話に基づいて次に実行する bash コマンドを決定します。これは意図的な設計です：このレベルでは計画レイヤーの追加は時期尚早な抽象化です。モデルの思考の連鎖そのものが計画です。エージェントループはモデルがツールの呼び出しを止めるまで、次の行動を問い続けるだけです。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s02.json",
    "content": "{\n  \"version\": \"s02\",\n  \"decisions\": [\n    {\n      \"id\": \"four-tools-not-twenty\",\n      \"title\": \"Why Exactly Four Tools\",\n      \"description\": \"The four tools are bash, read_file, write_file, and edit_file. Together they cover roughly 95% of coding tasks. Bash handles execution and arbitrary commands. Read_file provides precise file reading with line numbers. Write_file creates or overwrites files. Edit_file does surgical string replacement. More tools would increase the model's cognitive load -- it has to decide which tool to use, and more options means more chances of picking the wrong one. Fewer tools also means fewer tool schemas to maintain and fewer edge cases to handle.\",\n      \"alternatives\": \"We could add specialized tools (list_directory, search_files, http_request), and later versions do. But at this stage, bash already covers those use cases. The split from v0's single tool to v1's four tools is specifically about giving the model structured I/O for file operations, where bash's quoting and escaping often trips up the model.\",\n      \"zh\": {\n        \"title\": \"为什么恰好四个工具\",\n        \"description\": \"四个工具分别是 bash、read_file、write_file 和 edit_file，覆盖了大约 95% 的编程任务。Bash 处理执行和任意命令；read_file 提供带行号的精确文件读取；write_file 创建或覆盖文件；edit_file 做精确的字符串替换。工具越多，模型的认知负担越重——它必须在更多选项中做选择，选错的概率也随之增加。更少的工具也意味着更少的 schema 需要维护、更少的边界情况需要处理。\"\n      },\n      \"ja\": {\n        \"title\": \"なぜ正確に4つのツールなのか\",\n        \"description\": \"4つのツールは bash、read_file、write_file、edit_file です。これらでコーディングタスクの約95%をカバーします。Bash は実行と任意のコマンドを処理し、read_file は行番号付きの正確なファイル読み取りを提供し、write_file はファイルの作成・上書きを行い、edit_file は外科的な文字列置換を行います。ツールが増えるとモデルの認知負荷が増大し、どのツールを使うかの判断でミスが増えます。ツールが少ないことは、メンテナンスすべきスキーマとエッジケースの削減も意味します。\"\n      }\n    },\n    {\n      \"id\": \"model-as-agent\",\n      \"title\": \"The Model IS the Agent\",\n      \"description\": \"The core agent loop is trivially simple: while True, call the LLM, if it returns tool_use blocks then execute them and feed results back, if it returns only text then stop. There is no router, no decision tree, no workflow engine. The model itself decides what to do, when to stop, and how to recover from errors. The code is just plumbing that connects the model to tools. This is a philosophical stance: agent behavior emerges from the model, not from the framework.\",\n      \"alternatives\": \"Many agent frameworks add elaborate orchestration layers: ReAct loops with explicit Thought/Action/Observation parsing, LangChain-style chains, AutoGPT-style goal decomposition. These frameworks assume the model needs scaffolding to behave as an agent. Our approach assumes the model already knows how to be an agent -- it just needs tools to act on the world.\",\n      \"zh\": {\n        \"title\": \"模型本身就是代理\",\n        \"description\": \"核心 agent 循环极其简单：不断调用 LLM，如果返回 tool_use 块就执行并回传结果，如果只返回文本就停止。没有路由器，没有决策树，没有工作流引擎。模型自己决定做什么、何时停止、如何从错误中恢复。代码只是连接模型和工具的管道。这是一种设计哲学：agent 行为从模型中涌现，而非由框架定义。\"\n      },\n      \"ja\": {\n        \"title\": \"モデルそのものがエージェント\",\n        \"description\": \"コアのエージェントループは極めてシンプルです：LLM を呼び出し続け、tool_use ブロックが返されればそれを実行して結果をフィードバックし、テキストのみが返されれば停止します。ルーターも決定木もワークフローエンジンもありません。モデル自体が何をすべきか、いつ停止するか、エラーからどう回復するかを決定します。コードはモデルとツールを接続する配管に過ぎません。これは設計思想です：エージェントの振る舞いはフレームワークではなくモデルから創発するものです。\"\n      }\n    },\n    {\n      \"id\": \"explicit-tool-schemas\",\n      \"title\": \"JSON Schemas for Every Tool\",\n      \"description\": \"Each tool defines a strict JSON schema for its input parameters. For example, edit_file requires old_string and new_string as exact strings, not regex patterns. This eliminates an entire class of bugs: the model can't pass malformed input because the API validates against the schema before execution. It also makes the model's intent unambiguous -- when it calls edit_file with specific strings, there's no parsing ambiguity about what it wants to change.\",\n      \"alternatives\": \"Some agent systems let the model output free-form text that gets parsed with regex or heuristics (e.g., extracting code from markdown blocks). This is fragile -- the model might format output slightly differently and break the parser. JSON schemas trade flexibility for reliability.\",\n      \"zh\": {\n        \"title\": \"每个工具都有 JSON Schema\",\n        \"description\": \"每个工具都为输入参数定义了严格的 JSON schema。例如，edit_file 要求 old_string 和 new_string 是精确的字符串，而非正则表达式。这消除了一整类错误：模型无法传递格式错误的输入，因为 API 会在执行前校验 schema。这也使模型的意图变得明确——当它用特定字符串调用 edit_file 时，不存在关于它想修改什么的解析歧义。\"\n      },\n      \"ja\": {\n        \"title\": \"全ツールに JSON Schema を定義\",\n        \"description\": \"各ツールは入力パラメータに対して厳密な JSON Schema を定義しています。例えば edit_file は old_string と new_string を正確な文字列として要求し、正規表現は使いません。これにより一連のバグを排除できます：API がスキーマに対して実行前にバリデーションを行うため、モデルは不正な入力を渡せません。モデルの意図も明確になります――特定の文字列で edit_file を呼び出す際、何を変更したいかについて解析の曖昧さがありません。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s03.json",
    "content": "{\n  \"version\": \"s03\",\n  \"decisions\": [\n    {\n      \"id\": \"visible-planning\",\n      \"title\": \"Making Plans Visible via TodoWrite\",\n      \"description\": \"Instead of letting the model plan silently in its chain-of-thought, we force plans to be externalized through the TodoWrite tool. Each plan item has a status (pending, in_progress, completed) that gets tracked explicitly. This has three benefits: (1) users can see what the agent intends to do before it does it, (2) developers can debug agent behavior by inspecting the plan state, (3) the agent itself can refer back to its plan in later turns when earlier context has scrolled away.\",\n      \"alternatives\": \"The model could plan internally via chain-of-thought reasoning (as it does in v0/v1). Internal planning works but is invisible and ephemeral -- once the thinking scrolls out of context, the plan is lost. Claude's extended thinking is another option, but it's not inspectable by the user or by downstream tools.\",\n      \"zh\": {\n        \"title\": \"通过 TodoWrite 让计划可见\",\n        \"description\": \"我们不让模型在思维链中默默规划，而是强制通过 TodoWrite 工具将计划外化。每个计划项都有可追踪的状态（pending、in_progress、completed）。这有三个好处：(1) 用户可以在执行前看到 agent 打算做什么；(2) 开发者可以通过检查计划状态来调试 agent 行为；(3) agent 自身可以在后续轮次中引用计划，即使早期上下文已经滚出窗口。\"\n      },\n      \"ja\": {\n        \"title\": \"TodoWrite による計画の可視化\",\n        \"description\": \"モデルが思考の連鎖の中で黙って計画するのではなく、TodoWrite ツールを通じて計画を外部化することを強制します。各計画項目には追跡可能なステータス（pending、in_progress、completed）があります。利点は3つ：(1) ユーザーがエージェントの意図を実行前に確認できる、(2) 開発者が計画状態を検査してデバッグできる、(3) エージェント自身が以前のコンテキストがスクロールアウトした後でも計画を参照できる。\"\n      }\n    },\n    {\n      \"id\": \"single-in-progress\",\n      \"title\": \"Only One Task Can Be In-Progress\",\n      \"description\": \"The TodoWrite tool enforces that at most one task has status 'in_progress' at any time. If the model tries to start a second task, it must first complete or abandon the current one. This constraint prevents a subtle failure mode: models that try to 'multitask' by interleaving work on multiple items tend to lose track of state and produce half-finished results. Sequential focus produces higher quality than parallel thrashing.\",\n      \"alternatives\": \"Allowing multiple in-progress items would let the agent context-switch between tasks, which seems more flexible. In practice, LLMs handle context-switching poorly -- they lose track of which task they were working on and mix up details between tasks. The single-focus constraint is a guardrail that improves output quality.\",\n      \"zh\": {\n        \"title\": \"同一时间只允许一个任务进行中\",\n        \"description\": \"TodoWrite 工具强制要求任何时候最多只能有一个任务处于 in_progress 状态。如果模型想开始第二个任务，必须先完成或放弃当前任务。这个约束防止了一种隐蔽的失败模式：试图通过交替处理多个项目来'多任务'的模型，往往会丢失状态并产出半成品。顺序执行的专注度远高于并行切换。\"\n      },\n      \"ja\": {\n        \"title\": \"同時に進行中にできるタスクは1つだけ\",\n        \"description\": \"TodoWrite ツールは、同時に 'in_progress' 状態のタスクを最大1つに制限します。モデルが2つ目のタスクを開始しようとする場合、まず現在のタスクを完了または中断する必要があります。この制約は微妙な失敗モードを防ぎます：複数の項目を交互に処理して「マルチタスク」しようとするモデルは、状態を見失い中途半端な結果を生みがちです。逐次的な集中は並行的な切り替えよりも高品質な出力を生み出します。\"\n      }\n    },\n    {\n      \"id\": \"max-twenty-items\",\n      \"title\": \"Maximum of 20 Plan Items\",\n      \"description\": \"TodoWrite caps the plan at 20 items. This is a deliberate constraint against over-planning. Models tend to decompose tasks into increasingly fine-grained steps when unconstrained, producing 50-item plans where each step is trivial. Long plans are fragile: if step 15 fails, the remaining 35 steps may all be invalid. Short plans (under 20 items) stay at the right abstraction level and are easier to adapt when reality diverges from the plan.\",\n      \"alternatives\": \"No cap would give the model full flexibility, but in practice leads to absurdly detailed plans. A dynamic cap (proportional to task complexity) would be smarter but adds complexity. The fixed cap of 20 is a simple heuristic that works well empirically -- most real coding tasks can be expressed in 5-15 meaningful steps.\",\n      \"zh\": {\n        \"title\": \"计划项上限为 20 条\",\n        \"description\": \"TodoWrite 将计划项限制在 20 条以内。这是对过度规划的刻意约束。不加限制时，模型倾向于将任务分解成越来越细粒度的步骤，产出 50 条的计划，每一步都微不足道。冗长的计划很脆弱：如果第 15 步失败，剩下的 35 步可能全部作废。20 条以内的短计划保持在正确的抽象层级，更容易在现实偏离计划时做出调整。\"\n      },\n      \"ja\": {\n        \"title\": \"計画項目の上限は20個\",\n        \"description\": \"TodoWrite は計画を20項目に制限します。これは過度な計画に対する意図的な制約です。制約がないとモデルはタスクをどんどん細かいステップに分解し、各ステップが些末な50項目の計画を作りがちです。長い計画は脆弱です：ステップ15が失敗すると残りの35ステップは全て無効になりかねません。20項目以内の短い計画は適切な抽象度を保ち、現実が計画から逸脱した際の適応が容易です。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s04.json",
    "content": "{\n  \"version\": \"s04\",\n  \"decisions\": [\n    {\n      \"id\": \"context-isolation\",\n      \"title\": \"Subagents Get Fresh Context, Not Shared History\",\n      \"description\": \"When a parent agent spawns a subagent via the Task tool, the subagent starts with a clean message history containing only the system prompt and the delegated task description. It does NOT inherit the parent's conversation. This is context isolation: the subagent can focus entirely on its specific subtask without being distracted by hundreds of messages from the parent's broader conversation. The result is returned to the parent as a single tool_result, collapsing potentially dozens of subagent turns into one concise answer.\",\n      \"alternatives\": \"Sharing the parent's full context would give the subagent more information, but it would also flood the subagent with irrelevant details. Context window is finite -- filling it with parent history leaves less room for the subagent's own work. Fork-based approaches (copy the parent context) are a middle ground but still waste tokens on irrelevant history.\",\n      \"zh\": {\n        \"title\": \"子代理获得全新上下文，而非共享历史\",\n        \"description\": \"当父代理通过 Task 工具创建子代理时，子代理从全新的消息历史开始，只包含系统提示词和委派的任务描述，不继承父代理的对话。这就是上下文隔离：子代理可以完全专注于特定子任务，不会被父代理长达数百条消息的对话干扰。结果作为单条 tool_result 返回给父代理，将子代理可能数十轮的交互压缩为一个简洁的回答。\"\n      },\n      \"ja\": {\n        \"title\": \"サブエージェントは共有履歴ではなく新しいコンテキストを取得\",\n        \"description\": \"親エージェントが Task ツールでサブエージェントを生成すると、サブエージェントはシステムプロンプトと委任されたタスク説明のみを含むクリーンなメッセージ履歴から開始します。親の会話は引き継ぎません。これがコンテキスト分離です：サブエージェントは親の広範な会話の何百ものメッセージに気を取られることなく、特定のサブタスクに完全に集中できます。結果は単一の tool_result として親に返され、サブエージェントの数十ターンが1つの簡潔な回答に凝縮されます。\"\n      }\n    },\n    {\n      \"id\": \"tool-filtering\",\n      \"title\": \"Explore Agents Cannot Write Files\",\n      \"description\": \"When spawning a subagent with the 'Explore' type, it receives only read-only tools: bash (with restrictions), read_file, and search tools. It cannot call write_file or edit_file. This implements the principle of least privilege: an agent tasked with 'find all usages of function X' doesn't need write access. Removing write tools eliminates the risk of accidental file modification during exploration, and it also narrows the tool space so the model makes better decisions with fewer options.\",\n      \"alternatives\": \"Giving all subagents full tool access is simpler to implement but violates least privilege. A permission-request system (subagent asks parent for write access) adds complexity and latency. Static tool filtering by role is the pragmatic middle ground -- simple to implement, effective at preventing accidents.\",\n      \"zh\": {\n        \"title\": \"Explore 代理不能写入文件\",\n        \"description\": \"创建 Explore 类型的子代理时，它只获得只读工具：bash（有限制）、read_file 和搜索工具，不能调用 write_file 或 edit_file。这实现了最小权限原则：一个被委派'查找函数 X 所有使用位置'的代理不需要写权限。移除写工具消除了探索过程中误修改文件的风险，同时缩小了工具空间，让模型在更少的选项中做出更好的决策。\"\n      },\n      \"ja\": {\n        \"title\": \"Explore エージェントはファイルを書き込めない\",\n        \"description\": \"Explore タイプのサブエージェントを生成すると、読み取り専用ツールのみが提供されます：bash（制限付き）、read_file、検索ツール。write_file や edit_file は使えません。これは最小権限の原則の実装です：「関数 X の全使用箇所を見つける」タスクに書き込み権限は不要です。書き込みツールを除外することで探索中の誤ったファイル変更リスクを排除し、ツール空間を狭めてモデルがより良い判断を下せるようにします。\"\n      }\n    },\n    {\n      \"id\": \"no-recursive-task\",\n      \"title\": \"Subagents Cannot Spawn Their Own Subagents\",\n      \"description\": \"The Task tool is not included in the subagent's tool set. A subagent must complete its work directly -- it cannot delegate further. This prevents infinite delegation loops: without this constraint, an agent could spawn a subagent that spawns another subagent, each one re-delegating the same task in slightly different words, consuming tokens without making progress. One level of delegation handles the vast majority of use cases. If a task is too complex for a single subagent, the parent should decompose it differently.\",\n      \"alternatives\": \"Allowing recursive delegation (bounded by depth) would handle deeply nested tasks but adds complexity and the risk of runaway token consumption. In practice, single-level delegation covers most real-world coding tasks. Multi-level delegation is addressed in later versions (v6+) through persistent team structures instead of recursive spawning.\",\n      \"zh\": {\n        \"title\": \"子代理不能再创建子代理\",\n        \"description\": \"Task 工具不包含在子代理的工具集中。子代理必须直接完成工作，不能继续委派。这防止了无限委派循环：没有这个约束，一个代理可能创建子代理，子代理又创建子代理，每一层都用略微不同的措辞重新委派同一任务，消耗 token 却毫无进展。一层委派足以处理绝大多数场景。如果任务对单个子代理来说太复杂，应该由父代理重新分解。\"\n      },\n      \"ja\": {\n        \"title\": \"サブエージェントは自身のサブエージェントを生成できない\",\n        \"description\": \"Task ツールはサブエージェントのツールセットに含まれません。サブエージェントは作業を直接完了しなければならず、さらなる委任はできません。これにより無限委任ループを防止します：この制約がなければ、エージェントがサブエージェントを生成し、そのサブエージェントがさらにサブエージェントを生成し、それぞれが微妙に異なる言葉で同じタスクを再委任してトークンを消費するだけで進捗しない可能性があります。一段階の委任で大多数のユースケースに対応できます。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s05.json",
    "content": "{\n  \"version\": \"s05\",\n  \"decisions\": [\n    {\n      \"id\": \"tool-result-injection\",\n      \"title\": \"Skills Inject via tool_result, Not System Prompt\",\n      \"description\": \"When the agent invokes the Skill tool, the skill's content (a SKILL.md file) is returned as a tool_result in a user message, not injected into the system prompt. This is a deliberate caching optimization: the system prompt remains static across turns, which means API providers can cache it (Anthropic's prompt caching, OpenAI's system message caching). If skill content were in the system prompt, it would change every time a new skill is loaded, invalidating the cache. By putting dynamic content in tool_result, we keep the expensive system prompt cacheable while still getting skill knowledge into context.\",\n      \"alternatives\": \"Injecting skills into the system prompt is simpler and gives skills higher priority in the model's attention. But it breaks prompt caching (every skill load creates a new system prompt variant) and bloats the system prompt over time as skills accumulate. The tool_result approach keeps things cache-friendly at the cost of slightly lower attention priority.\",\n      \"zh\": {\n        \"title\": \"技能通过 tool_result 注入，而非系统提示词\",\n        \"description\": \"当 agent 调用 Skill 工具时，技能内容（SKILL.md 文件）作为 tool_result 在用户消息中返回，而非注入系统提示词。这是一个刻意的缓存优化：系统提示词在各轮次间保持静态，API 提供商可以缓存它（Anthropic 的 prompt caching、OpenAI 的 system message caching）。如果技能内容在系统提示词中，每次加载新技能都会使缓存失效。将动态内容放在 tool_result 中，既保持了昂贵的系统提示词可缓存，又让技能知识进入了上下文。\"\n      },\n      \"ja\": {\n        \"title\": \"スキルはシステムプロンプトではなく tool_result で注入\",\n        \"description\": \"エージェントが Skill ツールを呼び出すと、スキルの内容（SKILL.md ファイル）はシステムプロンプトへの注入ではなく、ユーザーメッセージ内の tool_result として返されます。これは意図的なキャッシュ最適化です：システムプロンプトはターン間で静的に保たれるため、API プロバイダーがキャッシュできます（Anthropic のプロンプトキャッシュ、OpenAI のシステムメッセージキャッシュ）。スキル内容がシステムプロンプト内にあると、新しいスキルをロードするたびにキャッシュが無効化されます。動的コンテンツを tool_result に配置することで、高コストなシステムプロンプトのキャッシュ可能性を維持しつつ、スキル知識をコンテキストに取り込めます。\"\n      }\n    },\n    {\n      \"id\": \"lazy-loading\",\n      \"title\": \"On-Demand Skill Loading Instead of Upfront\",\n      \"description\": \"Skills are not loaded at startup. The agent starts with only the skill names and descriptions (from frontmatter). When the agent decides it needs a specific skill, it calls the Skill tool, which loads the full SKILL.md body into context. This keeps the initial prompt small and focused. An agent solving a Python bug doesn't need the Kubernetes deployment skill loaded -- that would waste context window space and potentially confuse the model with irrelevant instructions.\",\n      \"alternatives\": \"Loading all skills upfront guarantees the model always has all knowledge available, but wastes tokens on irrelevant skills and may hit context limits. A recommendation system (model suggests skills, human approves) adds latency. Lazy loading lets the model self-serve the knowledge it needs, when it needs it.\",\n      \"zh\": {\n        \"title\": \"按需加载技能而非预加载\",\n        \"description\": \"技能不会在启动时加载。Agent 初始只拥有技能名称和描述（来自 frontmatter）。当 agent 判断需要特定技能时，调用 Skill 工具将完整的 SKILL.md 内容加载到上下文中。这保持了初始提示词的精简。一个正在修复 Python bug 的 agent 不需要加载 Kubernetes 部署技能——那会浪费上下文窗口空间，还可能用无关指令干扰模型。\"\n      },\n      \"ja\": {\n        \"title\": \"起動時ではなくオンデマンドでスキルを読み込み\",\n        \"description\": \"スキルは起動時に読み込まれません。エージェントは最初、スキルの名前と説明（フロントマターから）のみを持ちます。エージェントが特定のスキルが必要だと判断すると、Skill ツールを呼び出して完全な SKILL.md の内容をコンテキストに読み込みます。これにより初期プロンプトを小さく保ちます。Python のバグを修正しているエージェントに Kubernetes デプロイのスキルは不要です――コンテキストウィンドウの無駄遣いであり、無関係な指示でモデルを混乱させかねません。\"\n      }\n    },\n    {\n      \"id\": \"frontmatter-body-split\",\n      \"title\": \"YAML Frontmatter + Markdown Body in SKILL.md\",\n      \"description\": \"Each SKILL.md file has two parts: YAML frontmatter (name, description, globs) and a markdown body (the actual instructions). The frontmatter serves as metadata for the skill registry -- it's what gets listed when the agent asks 'what skills are available?' The body is the payload that gets loaded on demand. This separation means you can list 100 skills (reading only frontmatter, a few bytes each) without loading 100 full instruction sets (potentially thousands of tokens each).\",\n      \"alternatives\": \"A separate metadata file (skill.yaml + skill.md) would work but doubles the number of files. Embedding metadata in the markdown (as headings or comments) requires parsing the full file to extract metadata. Frontmatter is a well-established convention (Jekyll, Hugo, Astro) that keeps metadata and content co-located but separately parseable.\",\n      \"zh\": {\n        \"title\": \"SKILL.md 采用 YAML Frontmatter + Markdown 正文\",\n        \"description\": \"每个 SKILL.md 文件有两部分：YAML frontmatter（名称、描述、globs）和 markdown 正文（实际指令）。Frontmatter 作为技能注册表的元数据——当 agent 问'有哪些可用技能'时，展示的就是这些信息。正文是按需加载的有效负载。这种分离意味着可以列出 100 个技能（每个只读几字节的 frontmatter）而不必加载 100 套完整指令集（每套可能数千 token）。\"\n      },\n      \"ja\": {\n        \"title\": \"SKILL.md で YAML フロントマター + Markdown 本文\",\n        \"description\": \"各 SKILL.md ファイルは2つの部分で構成されます：YAML フロントマター（名前、説明、globs）と Markdown 本文（実際の指示）。フロントマターはスキルレジストリのメタデータとして機能し、エージェントが「どんなスキルが利用可能か」と問い合わせた際に一覧表示されます。本文はオンデマンドで読み込まれるペイロードです。この分離により、100個のスキル一覧表示（各数バイトのフロントマターのみ読み取り）が100個の完全な指示セット（各数千トークン）のロードなしに可能になります。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s06.json",
    "content": "{\n  \"version\": \"s06\",\n  \"decisions\": [\n    {\n      \"id\": \"three-layer-compression\",\n      \"title\": \"Three-Layer Compression Strategy\",\n      \"description\": \"Context management uses three distinct layers, each with different cost/benefit profiles. (1) Microcompact runs every turn and is nearly free: it truncates tool_result blocks from older messages, stripping verbose command output that's no longer needed. (2) Auto_compact triggers when token count exceeds a threshold: it calls the LLM to generate a conversation summary, which is expensive but dramatically reduces context size. (3) Manual compact is user-triggered for explicit 'start fresh' moments. Layering these means the cheap operation runs constantly (keeping context tidy) while the expensive operation runs rarely (only when actually needed).\",\n      \"alternatives\": \"A single compression strategy (e.g., always summarize at 80% capacity) would be simpler but wasteful -- most of the time, microcompact alone keeps things manageable. A sliding window (drop oldest N messages) is cheap but loses important context. The three-layer approach gives the best token efficiency: cheap cleanup constantly, expensive summarization rarely.\",\n      \"zh\": {\n        \"title\": \"三层压缩策略\",\n        \"description\": \"上下文管理使用三个独立的层次，各有不同的成本收益比。(1) 微压缩每轮都运行，几乎零成本：它截断旧消息中的 tool_result 块，去除不再需要的冗长命令输出。(2) 自动压缩在 token 数超过阈值时触发：调用 LLM 生成对话摘要，代价高但能大幅缩减上下文。(3) 手动压缩由用户触发，用于明确的'重新开始'场景。分层意味着低成本操作持续运行（保持上下文整洁），而高成本操作很少触发（仅在真正需要时）。\"\n      },\n      \"ja\": {\n        \"title\": \"3層圧縮戦略\",\n        \"description\": \"コンテキスト管理は、異なるコスト・効果プロファイルを持つ3つの層を使用します。(1) マイクロコンパクトは毎ターン実行されほぼ無コスト：古いメッセージの tool_result ブロックを切り詰め、不要な冗長出力を除去します。(2) 自動コンパクトはトークン数が閾値を超えると発動：LLM を呼び出して会話の要約を生成し、コストは高いがコンテキストサイズを劇的に削減します。(3) 手動コンパクトはユーザーが明示的に「最初からやり直し」する時に使用します。この階層化により、安価な操作が常に実行され（コンテキストを整頓）、高価な操作はめったに実行されません（本当に必要な時のみ）。\"\n      }\n    },\n    {\n      \"id\": \"min-savings-threshold\",\n      \"title\": \"MIN_SAVINGS = 20,000 Tokens Before Compressing\",\n      \"description\": \"Auto_compact only triggers when the estimated savings (current tokens minus estimated summary size) exceed 20,000 tokens. Compression is not free: the summary itself consumes tokens, plus there's the API call cost to generate it. If the conversation is only 25,000 tokens, compressing might save 5,000 tokens but cost an API call and produce a summary that's less coherent than the original. The 20K threshold ensures compression only happens when the savings meaningfully exceed the overhead.\",\n      \"alternatives\": \"A percentage-based threshold (compress when context is 80% full) adapts to different context window sizes but doesn't account for the fixed cost of generating a summary. A fixed threshold of 10K would compress more aggressively but often isn't worth it. The 20K value was chosen empirically: it's the point where compression savings consistently outweigh the quality loss from summarization.\",\n      \"zh\": {\n        \"title\": \"最小节省量 = 20,000 Token 才触发压缩\",\n        \"description\": \"自动压缩仅在估算节省量（当前 token 数减去预估摘要大小）超过 20,000 token 时才触发。压缩不是免费的：摘要本身会消耗 token，还有生成摘要的 API 调用成本。如果对话只有 25,000 token，压缩可能节省 5,000 token，但需要一次 API 调用，且产出的摘要可能不如原文连贯。20K 的阈值确保只在节省量明显超过开销时才进行压缩。\"\n      },\n      \"ja\": {\n        \"title\": \"圧縮前に MIN_SAVINGS = 20,000 トークンが必要\",\n        \"description\": \"自動コンパクトは推定節約量（現在のトークン数マイナス推定要約サイズ）が20,000トークンを超えた場合にのみ発動します。圧縮は無料ではありません：要約自体がトークンを消費し、さらに生成のための API コール費用がかかります。会話が25,000トークンしかない場合、圧縮で5,000トークン節約できても、API コールが必要で元の会話より一貫性の低い要約になる可能性があります。20K の閾値は、節約量がオーバーヘッドを確実に上回る場合にのみ圧縮を実行することを保証します。\"\n      }\n    },\n    {\n      \"id\": \"summary-replaces-all\",\n      \"title\": \"Summary Replaces ALL Messages, Not Partial History\",\n      \"description\": \"When auto_compact fires, it generates a summary and replaces the ENTIRE message history with that summary. It does not keep the last N messages alongside the summary. This avoids a subtle coherence problem: if you keep recent messages plus a summary of older ones, the model sees two representations of overlapping content. The summary might say 'we decided to use approach X' while a recent message still shows the deliberation process, creating contradictory signals. A clean summary is a single coherent narrative.\",\n      \"alternatives\": \"Keeping the last 5-10 messages alongside the summary preserves recent detail and gives the model more to work with. But it creates the overlap problem described above, and makes the total context size less predictable. Some systems use a 'sliding window + summary' approach which works but requires careful tuning of the overlap region.\",\n      \"zh\": {\n        \"title\": \"摘要替换全部消息，而非保留部分历史\",\n        \"description\": \"自动压缩触发时，生成摘要并替换全部消息历史，不会在摘要旁保留最近的 N 条消息。这避免了一个微妙的连贯性问题：如果同时保留近期消息和旧消息的摘要，模型会看到重叠内容的两种表示。摘要可能说'我们决定使用方案 X'，而近期消息仍在展示讨论过程，产生矛盾信号。干净的摘要是一个连贯的单一叙述。\"\n      },\n      \"ja\": {\n        \"title\": \"要約が部分的な履歴ではなく全メッセージを置換\",\n        \"description\": \"自動コンパクトが発動すると、要約を生成してメッセージ履歴の全体をその要約で置換します。要約と並べて直近 N 件のメッセージを保持することはしません。これにより微妙な一貫性の問題を回避します：直近のメッセージと古いメッセージの要約を併存させると、モデルは重複するコンテンツの2つの表現を見ることになります。要約が「アプローチ X を使うことに決めた」と言う一方で、直近のメッセージにはまだ検討過程が表示されているかもしれず、矛盾するシグナルを生じます。クリーンな要約は単一の一貫した物語です。\"\n      }\n    },\n    {\n      \"id\": \"transcript-archival\",\n      \"title\": \"Full Conversation Archived to JSONL on Disk\",\n      \"description\": \"Even though context is compressed in memory, the full uncompressed conversation is appended to a JSONL file on disk. Every message, every tool call, every result -- nothing is lost. This means compression is a lossy operation on the in-memory context but a lossless operation on the permanent record. Post-hoc analysis (debugging agent behavior, computing token usage, training data extraction) can always work from the complete transcript. The JSONL format is append-only, making it safe for concurrent writes and easy to stream-process.\",\n      \"alternatives\": \"Not archiving saves disk space but makes debugging hard -- when the agent makes a mistake, you can't see what it was 'thinking' 200 messages ago because that context was compressed away. Database storage (SQLite) would provide queryability but adds a dependency. JSONL is the simplest format that supports append-only writes and line-by-line processing.\",\n      \"zh\": {\n        \"title\": \"完整对话以 JSONL 格式归档到磁盘\",\n        \"description\": \"尽管上下文在内存中被压缩，完整的未压缩对话仍会追加到磁盘上的 JSONL 文件中。每条消息、每次工具调用、每个结果都不会丢失。压缩对内存上下文是有损操作，但对永久记录是无损的。事后分析（调试 agent 行为、计算 token 用量、提取训练数据）始终可以基于完整记录进行。JSONL 格式仅追加写入，对并发写入安全，易于流式处理。\"\n      },\n      \"ja\": {\n        \"title\": \"完全な会話を JSONL としてディスクに保存\",\n        \"description\": \"メモリ上でコンテキストが圧縮されても、完全な非圧縮会話はディスク上の JSONL ファイルに追記されます。全てのメッセージ、全てのツール呼び出し、全ての結果――何も失われません。圧縮はインメモリコンテキストに対しては不可逆ですが、永続記録に対しては可逆です。事後分析（エージェントの挙動デバッグ、トークン使用量の計算、学習データの抽出）は常に完全な記録から行えます。JSONL フォーマットは追記専用で、並行書き込みに安全であり行単位の処理が容易です。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s07.json",
    "content": "{\n  \"version\": \"s07\",\n  \"decisions\": [\n    {\n      \"id\": \"file-based-persistence\",\n      \"title\": \"Tasks Stored as JSON Files, Not In-Memory\",\n      \"description\": \"Tasks are persisted as JSON files in a .tasks/ directory on the filesystem instead of being held in memory. This has three critical benefits: (1) Tasks survive process crashes -- if the agent dies mid-task, the task board is still on disk when it restarts. (2) Multiple agents can read and write to the same task directory, enabling multi-agent coordination without shared memory. (3) Humans can inspect and manually edit task files for debugging. The filesystem becomes the shared database.\",\n      \"alternatives\": \"In-memory storage (like v2's TodoWrite) is simpler and faster but loses state on crash and doesn't work across multiple agent processes. A proper database (SQLite, Redis) would provide ACID guarantees and better concurrency, but adds a dependency and operational complexity. Files are the zero-dependency persistence layer that works everywhere.\",\n      \"zh\": {\n        \"title\": \"任务存储为 JSON 文件，而非内存\",\n        \"description\": \"任务以 JSON 文件形式持久化在 .tasks/ 目录中，而非保存在内存里。这有三个关键好处：(1) 任务在进程崩溃后仍然存在——如果 agent 在任务中途崩溃，重启后任务板仍在磁盘上；(2) 多个 agent 可以读写同一任务目录，无需共享内存即可实现多代理协调；(3) 人类可以查看和手动编辑任务文件来调试。文件系统就是共享数据库。\"\n      },\n      \"ja\": {\n        \"title\": \"タスクをメモリではなく JSON ファイルとして保存\",\n        \"description\": \"タスクはメモリ内ではなく .tasks/ ディレクトリに JSON ファイルとして永続化されます。3つの重要な利点があります：(1) プロセスのクラッシュ後もタスクが存続する――エージェントがタスク途中でクラッシュしても、再起動時にタスクボードはディスク上に残っています。(2) 複数のエージェントが同じタスクディレクトリを読み書きでき、共有メモリなしにマルチエージェント連携が可能になります。(3) 人間がデバッグのためにタスクファイルを検査・手動編集できます。ファイルシステムが共有データベースになります。\"\n      }\n    },\n    {\n      \"id\": \"dependency-graph\",\n      \"title\": \"Tasks Have blocks/blockedBy Dependency Fields\",\n      \"description\": \"Each task can declare which other tasks it blocks (downstream dependents) and which tasks block it (upstream dependencies). An agent will not start a task that has unresolved blockedBy dependencies. This is essential for multi-agent coordination: when Agent A is writing the database schema and Agent B needs to write queries against it, Agent B's task is blockedBy Agent A's task. Without dependencies, both agents might start simultaneously and Agent B would work against a schema that doesn't exist yet.\",\n      \"alternatives\": \"Simple priority ordering (high/medium/low) doesn't capture 'task B literally cannot start until task A finishes.' A centralized coordinator that assigns tasks in order would work but creates a single point of failure and bottleneck. Declarative dependencies let each agent independently determine what it can work on by reading the task files.\",\n      \"zh\": {\n        \"title\": \"任务具有 blocks/blockedBy 依赖字段\",\n        \"description\": \"每个任务可以声明它阻塞哪些任务（下游依赖）以及它被哪些任务阻塞（上游依赖）。Agent 不会开始有未解决 blockedBy 依赖的任务。这对多代理协调至关重要：当 Agent A 在编写数据库 schema、Agent B 需要写查询时，Agent B 的任务被 Agent A 的任务阻塞。没有依赖关系，两个 agent 可能同时开始，而 Agent B 会针对一个尚不存在的 schema 工作。\"\n      },\n      \"ja\": {\n        \"title\": \"タスクに blocks/blockedBy 依存関係フィールド\",\n        \"description\": \"各タスクは、自分がブロックするタスク（下流の依存先）と、自分をブロックするタスク（上流の依存元）を宣言できます。エージェントは未解決の blockedBy 依存がある タスクを開始しません。これはマルチエージェント連携に不可欠です：エージェント A がデータベーススキーマを書いていてエージェント B がそれに対するクエリを書く必要がある場合、B のタスクは A のタスクにブロックされます。依存関係がなければ両エージェントが同時に開始し、B はまだ存在しないスキーマに対して作業することになります。\"\n      }\n    },\n    {\n      \"id\": \"task-default-todo-coexistence\",\n      \"title\": \"Task as Course Default, Todo Still Useful\",\n      \"description\": \"TaskManager extends the Todo mental model and becomes the default workflow from s07 onward in this course. Both track work items with statuses, but TaskManager adds file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership fields, and multi-process coordination. Todo remains useful for short, linear, one-shot tracking where heavyweight coordination is unnecessary.\",\n      \"alternatives\": \"Using only Todo keeps the model minimal but weak for long-running or collaborative work. Using only Task everywhere maximizes consistency but can feel heavy for tiny one-off tasks.\",\n      \"zh\": {\n        \"title\": \"Task 为课程主线，Todo 仍有适用场景\",\n        \"description\": \"TaskManager 延续了 Todo 的心智模型，并在本课程 s07 之后成为默认主线。两者都管理带状态的任务项，但 TaskManager 增加了文件持久化（崩溃后可恢复）、依赖追踪（blocks/blockedBy）、owner 字段与多进程协作能力。Todo 仍适合短、线性、一次性的轻量跟踪。\"\n      },\n      \"ja\": {\n        \"title\": \"Task を主線にしつつ Todo も併存\",\n        \"description\": \"TaskManager は Todo のメンタルモデルを拡張し、本コースでは s07 以降のデフォルトになる。どちらもステータス付き作業項目を扱うが、TaskManager にはファイル永続化（クラッシュ耐性）、依存関係追跡（blocks/blockedBy）、owner、マルチプロセス協調がある。Todo は短く直線的な単発作業では引き続き有効。\"\n      }\n    },\n    {\n      \"id\": \"task-write-discipline\",\n      \"title\": \"Durability Needs Write Discipline\",\n      \"description\": \"File persistence reduces context loss, but it does not remove concurrent-write risks by itself. Before writing task state, reload the JSON, validate expected status/dependency fields, and then save atomically. This prevents one agent from silently overwriting another agent's transition.\",\n      \"alternatives\": \"Blind overwrite writes are simpler but can corrupt coordination state under parallel execution. A database with optimistic locking would enforce stronger safety, but the course keeps file-based state for zero-dependency teaching.\",\n      \"zh\": {\n        \"title\": \"持久化仍需要写入纪律\",\n        \"description\": \"文件持久化能降低上下文丢失，但不会自动消除并发写入风险。写任务状态前应先重读 JSON、校验 `status/blockedBy` 是否符合预期，再原子写回，避免不同 agent 悄悄覆盖彼此状态。\"\n      },\n      \"ja\": {\n        \"title\": \"耐久性には書き込み規律が必要\",\n        \"description\": \"ファイル永続化だけでは並行書き込み競合は防げない。更新前に JSON を再読込し、`status/blockedBy` を検証して原子的に保存することで、他エージェントの遷移上書きを防ぐ。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s08.json",
    "content": "{\n  \"version\": \"s08\",\n  \"decisions\": [\n    {\n      \"id\": \"notification-bus\",\n      \"title\": \"threading.Queue as the Notification Bus\",\n      \"description\": \"Background task results are delivered via a threading.Queue instead of direct callbacks. The background thread puts a notification on the queue when its work completes. The main agent loop polls the queue before each LLM call. This decoupling is important: the background thread doesn't need to know anything about the main loop's state or timing. It just drops a message on the queue and moves on. The main loop picks it up at its own pace -- never mid-API-call, never mid-tool-execution. No race conditions, no callback hell.\",\n      \"alternatives\": \"Direct callbacks (background thread calls a function in the main thread) would deliver results faster but create thread-safety issues -- the callback might fire while the main thread is in the middle of building a request. Event-driven systems (asyncio, event emitters) work but add complexity. A queue is the simplest thread-safe communication primitive.\",\n      \"zh\": {\n        \"title\": \"用 threading.Queue 作为通知总线\",\n        \"description\": \"后台任务结果通过 threading.Queue 传递，而非直接回调。后台线程在工作完成时向队列放入通知，主 agent 循环在每次 LLM 调用前轮询队列。这种解耦很重要：后台线程无需了解主循环的状态或时序，只需往队列放入消息然后继续。主循环按自己的节奏取出消息——永远不会在 API 调用中途或工具执行中途。没有竞争条件，没有回调地狱。\"\n      },\n      \"ja\": {\n        \"title\": \"threading.Queue を通知バスとして使用\",\n        \"description\": \"バックグラウンドタスクの結果は直接コールバックではなく threading.Queue を通じて配信されます。バックグラウンドスレッドは作業完了時にキューに通知を投入します。メインのエージェントループは各 LLM 呼び出しの前にキューをポーリングします。この疎結合が重要です：バックグラウンドスレッドはメインループの状態やタイミングを一切知る必要がありません。キューにメッセージを入れて先に進むだけです。メインループは自分のペースで取り出します――API 呼び出しの途中でもツール実行の途中でもありません。レースコンディションもコールバック地獄もありません。\"\n      }\n    },\n    {\n      \"id\": \"daemon-threads\",\n      \"title\": \"Background Tasks Run as Daemon Threads\",\n      \"description\": \"Background task threads are created with daemon=True. In Python, daemon threads are killed automatically when the main thread exits. This prevents a common problem: if the main agent completes its work and exits, but a background thread is still running (waiting on a long API call, stuck in a loop), the process would hang indefinitely. With daemon threads, exit is clean -- the main thread finishes, all daemon threads die, process exits. No zombie processes, no cleanup code needed.\",\n      \"alternatives\": \"Non-daemon threads with explicit cleanup (join with timeout, then terminate) give more control over shutdown but require careful lifecycle management. Process-based parallelism (multiprocessing) provides stronger isolation but higher overhead. Daemon threads are the pragmatic choice: minimal code, correct behavior in the common case.\",\n      \"zh\": {\n        \"title\": \"后台任务以守护线程运行\",\n        \"description\": \"后台任务线程以 daemon=True 创建。在 Python 中，守护线程在主线程退出时自动被终止。这防止了一个常见问题：如果主 agent 完成工作并退出，但后台线程仍在运行（等待一个长时间 API 调用或陷入循环），进程会无限挂起。使用守护线程，退出是干净的——主线程结束，所有守护线程自动终止，进程退出。没有僵尸进程，不需要清理代码。\"\n      },\n      \"ja\": {\n        \"title\": \"バックグラウンドタスクはデーモンスレッドとして実行\",\n        \"description\": \"バックグラウンドタスクのスレッドは daemon=True で作成されます。Python ではデーモンスレッドはメインスレッドの終了時に自動的に終了されます。これにより一般的な問題を防ぎます：メインエージェントが作業を完了して終了しても、バックグラウンドスレッドがまだ実行中（長い API 呼び出しを待機、ループに陥っている）だとプロセスが無限にハングします。デーモンスレッドならクリーンに終了できます――メインスレッドが終了すると全デーモンスレッドが自動終了し、プロセスが終了します。ゾンビプロセスもクリーンアップコードも不要です。\"\n      }\n    },\n    {\n      \"id\": \"attachment-format\",\n      \"title\": \"Structured Notification Format with Type Tags\",\n      \"description\": \"Notifications from background tasks use a structured format: {\\\"type\\\": \\\"attachment\\\", \\\"attachment\\\": {status, result, ...}} instead of plain text strings. The type tag lets the main loop handle different notification types differently: an 'attachment' might be injected into the conversation as a tool_result, while a 'status_update' might just update a progress indicator. Machine-readable notifications also enable programmatic filtering (show only errors, suppress progress updates) and UI rendering (display status as a progress bar, not raw text).\",\n      \"alternatives\": \"Plain text notifications are simpler but lose structure. The main loop would have to parse free-form text to determine what happened, which is fragile. A class hierarchy (StatusNotification, ResultNotification, ErrorNotification) is more Pythonic but less portable -- JSON structures work the same way regardless of language or serialization format.\",\n      \"zh\": {\n        \"title\": \"带类型标签的结构化通知格式\",\n        \"description\": \"后台任务的通知使用结构化格式：{\\\"type\\\": \\\"attachment\\\", \\\"attachment\\\": {status, result, ...}}，而非纯文本字符串。类型标签让主循环可以区别处理不同通知类型：attachment 可能作为 tool_result 注入对话，而 status_update 可能只更新进度指示器。机器可读的通知还支持程序化过滤（只显示错误、抑制进度更新）和 UI 渲染（将状态显示为进度条而非原始文本）。\"\n      },\n      \"ja\": {\n        \"title\": \"型タグ付き構造化通知フォーマット\",\n        \"description\": \"バックグラウンドタスクからの通知は構造化フォーマットを使用します：プレーンテキストではなく {\\\"type\\\": \\\"attachment\\\", \\\"attachment\\\": {status, result, ...}} です。型タグによりメインループは異なる通知タイプを異なる方法で処理できます：attachment は会話に tool_result として注入され、status_update は進捗インジケーターの更新のみを行うかもしれません。機械可読な通知はプログラム的なフィルタリング（エラーのみ表示、進捗更新の抑制）や UI レンダリング（ステータスを生テキストではなくプログレスバーとして表示）も可能にします。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s09.json",
    "content": "{\n  \"version\": \"s09\",\n  \"decisions\": [\n    {\n      \"id\": \"teammate-vs-subagent\",\n      \"title\": \"Persistent Teammates vs One-Shot Subagents\",\n      \"description\": \"In s04, subagents are ephemeral: spawn, do one task, return result, die. Their knowledge dies with them. In s09, teammates are persistent threads with identity (name, role) and config files. A teammate can complete task A, then be assigned task B, carrying forward everything it learned. Persistent teammates accumulate project knowledge, understand established patterns, and don't need to re-read the same files for every task.\",\n      \"alternatives\": \"One-shot subagents (s04 style) are simpler and provide perfect context isolation -- no risk of one task's context polluting another. But the re-learning cost is high: every new task starts from zero. A middle ground (subagents with shared memory/knowledge base) was considered but adds complexity without the full benefit of persistent identity and state.\",\n      \"zh\": {\n        \"title\": \"持久化队友 vs 一次性子智能体\",\n        \"description\": \"在 s04 中，子智能体是临时的：创建、执行一个任务、返回结果、销毁。它们的知识随之消亡。在 s09 中，队友是具有身份（名称、角色）和配置文件的持久化线程。队友可以完成任务 A，然后被分配任务 B，并携带之前学到的所有知识。持久化队友积累项目知识，理解已建立的模式，不需要为每个任务重新阅读相同的文件。\"\n      },\n      \"ja\": {\n        \"title\": \"永続的なチームメイト vs 使い捨てサブエージェント\",\n        \"description\": \"s04 ではサブエージェントは一時的です：生成、1つのタスクを実行、結果を返却、消滅。その知識も一緒に消えます。s09 ではチームメイトはアイデンティティ（名前、役割）と設定ファイルを持つ永続的なスレッドです。チームメイトはタスク A を完了した後、学んだ全てを引き継いでタスク B に割り当てられます。永続的なチームメイトはプロジェクトの知識を蓄積し、確立されたパターンを理解し、タスクごとに同じファイルを再読する必要がありません。\"\n      }\n    },\n    {\n      \"id\": \"file-based-team-config\",\n      \"title\": \"Team Config Persisted to .teams/{name}/config.json\",\n      \"description\": \"Team structure (member names, roles, agent IDs) is stored in a JSON config file, not in any agent's memory. Any agent can discover its teammates by reading the config file -- no need for a discovery service or shared memory. If an agent crashes and restarts, it reads the config to find out who else is on the team. This is consistent with the s07 philosophy: the filesystem is the coordination layer.\",\n      \"alternatives\": \"In-memory team registries are faster but don't survive process restarts and require a central process to maintain. Service discovery (like DNS or a discovery server) is more robust at scale but overkill for a local multi-agent system. File-based config is the simplest approach that works across independent processes.\",\n      \"zh\": {\n        \"title\": \"团队配置持久化到 .teams/{name}/config.json\",\n        \"description\": \"团队结构（成员名称、角色、agent ID）存储在 JSON 配置文件中，而非任何 agent 的内存中。任何 agent 都可以通过读取配置文件发现队友——无需发现服务或共享内存。如果 agent 崩溃并重启，它读取配置即可知道团队中还有谁。这与 s07 的理念一致：文件系统就是协调层。配置文件人类可读，便于手动添加或移除团队成员、调试团队配置问题。\"\n      },\n      \"ja\": {\n        \"title\": \"チーム設定を .teams/{name}/config.json に永続化\",\n        \"description\": \"チーム構成（メンバー名、役割、エージェント ID）はエージェントのメモリではなく JSON 設定ファイルに保存されます。どのエージェントも設定ファイルを読むことでチームメイトを発見できます――ディスカバリーサービスや共有メモリは不要です。エージェントがクラッシュして再起動した場合、設定を読んで他のチームメンバーを把握します。これは s07 の思想と一貫しています：ファイルシステムが連携レイヤーです。\"\n      }\n    },\n    {\n      \"id\": \"tool-filtering-by-role\",\n      \"title\": \"Teammates Get Subset of Tools, Lead Gets All\",\n      \"description\": \"The team lead receives ALL_TOOLS (including spawn, send, read_inbox, etc.) while teammates receive TEAMMATE_TOOLS (a reduced set focused on task execution). This enforces a clear separation of concerns: teammates focus on doing work (coding, testing, researching), while the lead focuses on coordination (creating tasks, assigning work, managing communication). Giving teammates coordination tools would let them create their own sub-teams or reassign tasks, undermining the lead's ability to maintain a coherent plan.\",\n      \"alternatives\": \"Giving all agents identical tools is simpler and more egalitarian, but in practice leads to coordination chaos -- multiple agents trying to manage each other, creating conflicting task assignments. Static role-based filtering is predictable and easy to reason about.\",\n      \"zh\": {\n        \"title\": \"队友获得工具子集，组长获得全部工具\",\n        \"description\": \"团队组长获得 ALL_TOOLS（包括 spawn、send、read_inbox 等），而队友获得 TEAMMATE_TOOLS（专注于任务执行的精简工具集）。这强制了清晰的职责分离：队友专注于做事（编码、测试、研究），组长专注于协调（创建任务、分配工作、管理沟通）。给队友协调工具会让他们创建自己的子团队或重新分配任务，破坏组长维持连贯计划的能力。\"\n      },\n      \"ja\": {\n        \"title\": \"チームメイトはツールのサブセット、リーダーは全ツール\",\n        \"description\": \"チームリーダーは ALL_TOOLS（spawn、send、read_inbox など含む）を受け取り、チームメイトは TEAMMATE_TOOLS（タスク実行に特化した縮小セット）を受け取ります。これにより明確な関心の分離が強制されます：チームメイトは作業（コーディング、テスト、調査）に集中し、リーダーは調整（タスク作成、作業割り当て、コミュニケーション管理）に集中します。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s10.json",
    "content": "{\n  \"version\": \"s10\",\n  \"decisions\": [\n    {\n      \"id\": \"jsonl-inbox\",\n      \"title\": \"JSONL Inbox Files Instead of Shared Memory\",\n      \"description\": \"Each teammate has its own inbox file (a JSONL file in the team directory). Sending a message means appending a JSON line to the recipient's inbox file. Reading messages means reading the inbox file and tracking which line was last read. JSONL is append-only by nature, which means concurrent writers don't corrupt each other's data (appends to different file positions). This works across processes without any shared memory, mutex, or IPC mechanism. It's also crash-safe: if the writer crashes mid-append, the worst case is one partial line that the reader can skip.\",\n      \"alternatives\": \"Shared memory (Python multiprocessing.Queue) would be faster but doesn't work if agents are separate processes launched independently. A message broker (Redis, RabbitMQ) provides robust pub/sub but adds infrastructure dependencies. Unix domain sockets would work but are harder to debug (no human-readable message log). JSONL files are the simplest approach that provides persistence, cross-process communication, and debuggability.\",\n      \"zh\": {\n        \"title\": \"JSONL 收件箱文件而非共享内存\",\n        \"description\": \"每个队友都有自己的收件箱文件（团队目录中的 JSONL 文件）。发送消息意味着向接收者的收件箱文件追加一行 JSON。读取消息意味着读取收件箱文件并追踪上次读到的行。JSONL 天然是仅追加的，这意味着并发写入不会破坏彼此的数据（追加到不同的文件位置）。这在无需共享内存、互斥锁或 IPC 机制的情况下跨进程工作。它也是崩溃安全的：如果写入者在追加中途崩溃，最坏情况是一行不完整的数据，读取者可以跳过。\"\n      },\n      \"ja\": {\n        \"title\": \"共有メモリではなく JSONL インボックスファイル\",\n        \"description\": \"各チームメイトはチームディレクトリ内に独自のインボックスファイル（JSONL ファイル）を持ちます。メッセージの送信は受信者のインボックスファイルに JSON 行を追記することです。メッセージの読み取りはインボックスファイルを読んで最後に読んだ行を追跡することです。JSONL は本質的に追記専用で、並行ライターが互いのデータを破壊しません（異なるファイル位置への追記）。共有メモリ、ミューテックス、IPC メカニズムなしにプロセス間で動作します。\"\n      }\n    },\n    {\n      \"id\": \"five-message-types\",\n      \"title\": \"Exactly Five Message Types Cover All Coordination Patterns\",\n      \"description\": \"The messaging system supports exactly five types: (1) 'message' for point-to-point communication between two agents, (2) 'broadcast' for team-wide announcements, (3) 'shutdown_request' for graceful termination, (4) 'shutdown_response' for acknowledging shutdown, (5) 'plan_approval_response' for the lead to approve or reject a teammate's plan. These five types map to the fundamental coordination patterns: direct communication, broadcast, lifecycle management, and approval workflows.\",\n      \"alternatives\": \"A single generic message type with metadata fields would be more flexible but makes it harder to enforce protocol correctness. Many more types (10+) would provide finer-grained semantics but increase the model's decision burden. Five types is the sweet spot where every type has a clear, distinct purpose.\",\n      \"zh\": {\n        \"title\": \"恰好五种消息类型覆盖所有协调模式\",\n        \"description\": \"消息系统恰好支持五种类型：(1) message 用于两个 agent 间的点对点通信；(2) broadcast 用于全团队公告；(3) shutdown_request 用于优雅终止；(4) shutdown_response 用于确认终止；(5) plan_approval_response 用于组长批准或拒绝队友的计划。这五种类型映射到基本协调模式：直接通信、广播、生命周期管理和审批流程。\"\n      },\n      \"ja\": {\n        \"title\": \"正確に5つのメッセージタイプで全連携パターンをカバー\",\n        \"description\": \"メッセージングシステムは正確に5つのタイプをサポートします：(1) message は2つのエージェント間のポイントツーポイント通信、(2) broadcast はチーム全体への通知、(3) shutdown_request はグレースフルな終了要求、(4) shutdown_response はシャットダウンの確認応答、(5) plan_approval_response はリーダーによるチームメイトの計画の承認・却下。\"\n      }\n    },\n    {\n      \"id\": \"inbox-before-api-call\",\n      \"title\": \"Check Inbox Before Every LLM Call\",\n      \"description\": \"Teammates check their inbox file at the top of every agent loop iteration, before calling the LLM API. This ensures maximum responsiveness to incoming messages: a shutdown request is seen within one loop iteration (typically seconds), not after the current task completes (potentially minutes). The inbox check is cheap (read a small file, check if new lines exist) compared to the LLM call (seconds of latency, thousands of tokens). This placement also means incoming messages can influence the next LLM call -- a message saying 'stop working on X, switch to Y' takes effect immediately.\",\n      \"alternatives\": \"Checking inbox after each tool execution would be more responsive but adds overhead to every tool call, which is more frequent than LLM calls. A separate watcher thread could monitor the inbox continuously but adds threading complexity. Checking once per LLM call is the pragmatic sweet spot: responsive enough for coordination, cheap enough to not impact performance.\",\n      \"zh\": {\n        \"title\": \"每次 LLM 调用前检查收件箱\",\n        \"description\": \"队友在每次 agent 循环迭代的顶部、调用 LLM API 之前检查收件箱文件。这确保了对传入消息的最大响应性：一个终止请求会在一个循环迭代内被看到（通常几秒钟），而非在当前任务完成后（可能数分钟）。收件箱检查成本很低（读取小文件，检查是否有新行），相比 LLM 调用（秒级延迟，数千 token）微不足道。这个位置还意味着传入消息可以影响下一次 LLM 调用——一条'停止 X，转去做 Y'的消息会立即生效。\"\n      },\n      \"ja\": {\n        \"title\": \"毎回の LLM 呼び出し前にインボックスを確認\",\n        \"description\": \"チームメイトはエージェントループの各イテレーションの冒頭、LLM API を呼び出す前にインボックスファイルを確認します。これにより受信メッセージへの応答性を最大化します：シャットダウンリクエストは1ループイテレーション以内（通常数秒）で確認され、現在のタスク完了後（数分かかる可能性）ではありません。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s11.json",
    "content": "{\n  \"version\": \"s11\",\n  \"decisions\": [\n    {\n      \"id\": \"polling-not-events\",\n      \"title\": \"Polling for Unclaimed Tasks Instead of Event-Driven Notification\",\n      \"description\": \"Autonomous teammates poll the shared task board every ~1 second to find unclaimed tasks, rather than waiting for event-driven notifications. Polling is fundamentally simpler than pub/sub: there's no subscription management, no event routing, no missed-event bugs. With file-based persistence, polling is just 'read the directory listing' -- a cheap operation that works regardless of how many agents are running. The 1-second interval balances responsiveness (new tasks are discovered quickly) against filesystem overhead (not hammering the disk with reads).\",\n      \"alternatives\": \"Event-driven notification (file watchers via inotify/fsevents, or a pub/sub channel) would reduce latency from seconds to milliseconds. But file watchers are platform-specific and unreliable across network filesystems. A message broker would work but adds infrastructure. For a system where tasks take minutes to complete, discovering new tasks in 1 second instead of 10 milliseconds makes no practical difference.\",\n      \"zh\": {\n        \"title\": \"轮询未认领任务而非事件驱动通知\",\n        \"description\": \"自主队友每隔约 1 秒轮询共享任务板以寻找未认领的任务，而非等待事件驱动的通知。轮询从根本上比发布/订阅更简单：没有订阅管理、没有事件路由、没有事件丢失的 bug。在基于文件的持久化下，轮询就是'读取目录列表'——一个低成本操作，无论有多少 agent 在运行都能正常工作。1 秒的间隔平衡了响应性（新任务被快速发现）和文件系统开销（不会过度读取磁盘）。\"\n      },\n      \"ja\": {\n        \"title\": \"イベント駆動通知ではなくポーリングで未割り当てタスクを発見\",\n        \"description\": \"自律的なチームメイトはイベント駆動の通知を待つのではなく、約1秒ごとに共有タスクボードをポーリングして未割り当てタスクを探します。ポーリングはパブ/サブより根本的にシンプルです：サブスクリプション管理、イベントルーティング、イベント欠落バグがありません。ファイルベースの永続化では、ポーリングは「ディレクトリ一覧を読む」だけで、実行中のエージェント数に関係なく動作する安価な操作です。\"\n      }\n    },\n    {\n      \"id\": \"idle-timeout\",\n      \"title\": \"60-Second Idle Timeout Before Self-Termination\",\n      \"description\": \"When an autonomous teammate has no tasks to work on and no messages in its inbox, it waits up to 60 seconds before giving up and shutting down. This prevents zombie teammates that wait forever for work that never comes -- a real problem when the lead forgets to send a shutdown request, or when all remaining tasks are blocked on external events. The 60-second window is long enough that a brief gap between task completions and new task creation won't cause premature shutdown, but short enough that unused teammates don't waste resources.\",\n      \"alternatives\": \"No timeout (wait forever) risks zombie processes. A very short timeout (5s) causes premature exits when the lead is simply thinking or typing. A heartbeat system (lead periodically pings teammates to keep them alive) works but adds protocol complexity. The 60-second fixed timeout is a good default that balances false-positive exits against resource waste.\",\n      \"zh\": {\n        \"title\": \"空闲 60 秒后自动终止\",\n        \"description\": \"当自主队友没有任务可做且收件箱中没有消息时，它最多等待 60 秒后放弃并关闭。这防止了永远等待不会到来的工作的僵尸队友——这在组长忘记发送关闭请求、或所有剩余任务都被外部事件阻塞时是真实存在的问题。60 秒窗口足够长，不会因为任务完成到新任务创建之间的短暂间隔而导致过早关闭；又足够短，不会让闲置队友浪费资源。\"\n      },\n      \"ja\": {\n        \"title\": \"60秒のアイドルタイムアウトで自動終了\",\n        \"description\": \"自律的なチームメイトが作業するタスクもインボックスのメッセージもない場合、最大60秒待ってから諦めてシャットダウンします。これにより永遠に来ない仕事を待ち続けるゾンビチームメイトを防ぎます。60秒のウィンドウはタスク完了から新タスク作成までの短い間隔で早期シャットダウンが起きない十分な長さであり、かつ未使用のチームメイトがリソースを浪費しない十分な短さです。\"\n      }\n    },\n    {\n      \"id\": \"identity-after-compression\",\n      \"title\": \"Re-Inject Teammate Identity After Context Compression\",\n      \"description\": \"When auto_compact compresses the conversation, the resulting summary loses crucial metadata: the teammate's name, which team it belongs to, and its agent_id. Without this information, the teammate can't claim tasks (tasks are owned by name), can't check its inbox (inbox files are keyed by agent_id), and can't identify itself in messages. So after every auto_compact, the system re-injects a structured identity block into the conversation: 'You are [name] on team [team], your agent_id is [id], your inbox is at [path].' This is the minimum context needed for the teammate to remain functional after memory loss.\",\n      \"alternatives\": \"Putting identity in the system prompt (which survives compression) would avoid this problem, but violates the cache-friendly static-system-prompt design from s05. Embedding identity in the summary prompt ('when summarizing, always include your name and team') is unreliable -- the LLM might omit it. Explicit post-compression injection is deterministic and guaranteed to work.\",\n      \"zh\": {\n        \"title\": \"上下文压缩后重新注入队友身份\",\n        \"description\": \"自动压缩对话时，生成的摘要会丢失关键元数据：队友的名称、所属团队和 agent_id。没有这些信息，队友无法认领任务（任务按名称归属）、无法检查收件箱（收件箱文件以 agent_id 为键）、也无法在消息中表明身份。因此每次自动压缩后，系统会向对话中重新注入一个结构化的身份块：'你是 [team] 团队的 [name]，你的 agent_id 是 [id]，你的收件箱在 [path]。'这是队友在记忆丢失后保持功能所需的最小上下文。\"\n      },\n      \"ja\": {\n        \"title\": \"コンテキスト圧縮後にチームメイトのアイデンティティを再注入\",\n        \"description\": \"自動コンパクトが会話を圧縮すると、生成された要約は重要なメタデータを失います：チームメイトの名前、所属チーム、agent_id。この情報がなければチームメイトはタスクを申告できず（タスクは名前で所有）、インボックスを確認できず（インボックスファイルは agent_id をキーとする）、メッセージで自分を識別できません。そのため自動コンパクトの後、システムは構造化されたアイデンティティブロックを会話に再注入します。これはメモリ喪失後もチームメイトが機能し続けるために必要な最小限のコンテキストです。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/annotations/s12.json",
    "content": "{\n  \"version\": \"s12\",\n  \"decisions\": [\n    {\n      \"id\": \"shared-board-isolated-lanes\",\n      \"title\": \"Shared Task Board + Isolated Execution Lanes\",\n      \"description\": \"The task board remains shared and centralized in `.tasks/`, while file edits happen in per-task worktree directories. This separation preserves global visibility (who owns what, what is done) without forcing everyone to edit inside one mutable directory. Coordination stays simple because there is one board, and execution stays safe because each lane is isolated.\",\n      \"alternatives\": \"A single shared workspace is simpler but causes edit collisions and mixed git state. Fully independent task stores per lane avoid collisions but lose team-level visibility and make planning harder.\",\n      \"zh\": {\n        \"title\": \"共享任务板 + 隔离执行通道\",\n        \"description\": \"任务板继续集中在 `.tasks/`，而文件改动发生在按任务划分的 worktree 目录中。这样既保留了全局可见性（谁在做什么、完成到哪），又避免所有人同时写同一目录导致冲突。协调层简单（一个任务板），执行层安全（多条隔离通道）。\"\n      },\n      \"ja\": {\n        \"title\": \"共有タスクボード + 分離実行レーン\",\n        \"description\": \"タスクボードは `.tasks/` に集約しつつ、実際の編集はタスクごとの worktree ディレクトリで行う。これにより全体の可視性（担当と進捗）を維持しながら、単一ディレクトリでの衝突を回避できる。調整は1つのボードで単純化され、実行はレーン分離で安全になる。\"\n      }\n    },\n    {\n      \"id\": \"index-file-lifecycle\",\n      \"title\": \"Explicit Worktree Lifecycle Index\",\n      \"description\": \"`.worktrees/index.json` records each worktree's name, path, branch, task_id, and status. This makes lifecycle state inspectable and recoverable even after context compression or process restarts. The index also provides a deterministic source for list/status/remove operations.\",\n      \"alternatives\": \"Relying only on `git worktree list` removes local bookkeeping but loses task binding metadata and custom lifecycle states. Keeping all state only in memory is simpler in code but breaks recoverability.\",\n      \"zh\": {\n        \"title\": \"显式 worktree 生命周期索引\",\n        \"description\": \"`.worktrees/index.json` 记录每个 worktree 的名称、路径、分支、task_id 与状态。即使上下文压缩或进程重启，这些生命周期状态仍可检查和恢复。它也为 list/status/remove 提供了确定性的本地数据源。\"\n      },\n      \"ja\": {\n        \"title\": \"明示的な worktree ライフサイクル索引\",\n        \"description\": \"`.worktrees/index.json` に name/path/branch/task_id/status を記録することで、コンテキスト圧縮やプロセス再起動後も状態を追跡できる。list/status/remove の挙動もこの索引を基準に決定できる。\"\n      }\n    },\n    {\n      \"id\": \"lane-cwd-routing-and-reentry-guard\",\n      \"title\": \"Lane-Scoped CWD Routing + Re-entry Guard\",\n      \"description\": \"Commands are routed to a worktree's directory via `worktree_run(name, command)` using the `cwd` parameter. A re-entry guard prevents accidentally running inside an already-active worktree context, keeping lifecycle ownership unambiguous.\",\n      \"alternatives\": \"Global cwd mutation is easy to implement but can leak context across parallel work. Allowing silent re-entry makes lifecycle ownership ambiguous and complicates teardown behavior.\",\n      \"zh\": {\n        \"title\": \"按通道 cwd 路由 + 禁止重入\",\n        \"description\": \"命令通过 `worktree_run(name, command)` 使用 `cwd` 参数路由到 worktree 目录。重入保护避免了在已激活的 worktree 上下文中意外二次进入，保持生命周期归属清晰。\"\n      },\n      \"ja\": {\n        \"title\": \"レーン単位 cwd ルーティング + 再入防止\",\n        \"description\": \"`worktree_run(name, command)` で `cwd` パラメータを使いコマンドを worktree ディレクトリへ転送する。再入ガードにより active な worktree への二重入場を防ぎ、ライフサイクルの帰属を明確に保つ。\"\n      }\n    },\n    {\n      \"id\": \"event-stream-observability\",\n      \"title\": \"Append-Only Lifecycle Event Stream\",\n      \"description\": \"Lifecycle events are appended to `.worktrees/events.jsonl` (`worktree.create.*`, `worktree.remove.*`, `task.completed`). This turns hidden transitions into queryable records and makes failures explicit (`*.failed`) instead of silent.\",\n      \"alternatives\": \"Relying only on console logs is lighter but fragile during long sessions and hard to audit. A full event bus infrastructure is powerful but heavier than needed for this teaching baseline.\",\n      \"zh\": {\n        \"title\": \"追加式生命周期事件流\",\n        \"description\": \"生命周期事件写入 `.worktrees/events.jsonl`（如 `worktree.create.*`、`worktree.remove.*`、`task.completed`）。这样状态迁移可查询、可追踪，失败也会以 `*.failed` 显式暴露，而不是静默丢失。\"\n      },\n      \"ja\": {\n        \"title\": \"追記型ライフサイクルイベント\",\n        \"description\": \"ライフサイクルイベントを `.worktrees/events.jsonl` に追記する（`worktree.create.*`、`worktree.remove.*`、`task.completed` など）。遷移が可観測になり、失敗も `*.failed` として明示できる。\"\n      }\n    },\n    {\n      \"id\": \"task-worktree-closeout\",\n      \"title\": \"Close Task and Workspace Together\",\n      \"description\": \"`worktree_remove(..., complete_task=true)` allows a single closeout step: remove the isolated directory and mark the bound task completed. Closeout remains an explicit tool-driven transition (`worktree_keep` / `worktree_remove`) rather than hidden automatic cleanup. This reduces dangling state where a task says done but its temporary lane remains active (or the reverse).\",\n      \"alternatives\": \"Keeping closeout fully manual gives flexibility but increases operational drift. Fully automatic removal on every completion risks deleting a workspace before final review.\",\n      \"zh\": {\n        \"title\": \"任务与工作区一起收尾\",\n        \"description\": \"`worktree_remove(..., complete_task=true)` 允许在一个动作里完成收尾：删除隔离目录并把绑定任务标记为 completed。收尾保持为显式工具驱动迁移（`worktree_keep` / `worktree_remove`），而不是隐藏的自动清理。这样可减少状态悬挂（任务已完成但临时工作区仍活跃，或反过来）。\"\n      },\n      \"ja\": {\n        \"title\": \"タスクとワークスペースを同時にクローズ\",\n        \"description\": \"`worktree_remove(..., complete_task=true)` により、分離ディレクトリ削除とタスク完了更新を1ステップで実行できる。クローズ処理は `worktree_keep` / `worktree_remove` の明示ツール遷移として扱い、暗黙の自動清掃にはしない。\"\n      }\n    },\n    {\n      \"id\": \"event-stream-side-channel\",\n      \"title\": \"Event Stream Is Observability Side-Channel\",\n      \"description\": \"Lifecycle events improve auditability, but the source of truth remains task/worktree state files. Events should be read as transition traces, not as a replacement state machine.\",\n      \"alternatives\": \"Using logs alone hides structured transitions; using events as the only state source risks drift when replay/repair semantics are undefined.\",\n      \"zh\": {\n        \"title\": \"事件流是观测旁路，不是状态机替身\",\n        \"description\": \"生命周期事件提升可审计性，但真实状态源仍是任务/工作区状态文件。事件更适合做迁移轨迹，而不是替代主状态机。\"\n      },\n      \"ja\": {\n        \"title\": \"イベントは観測サイドチャネルであり状態機械の代替ではない\",\n        \"description\": \"ライフサイクルイベントは監査性を高めるが、真の状態源は task/worktree 状態ファイルのまま。イベントは遷移トレースとして扱い、主状態機械の代替にしない。\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/execution-flows.ts",
    "content": "import type { FlowNode, FlowEdge } from \"@/types/agent-data\";\n\nexport interface FlowDefinition {\n  nodes: FlowNode[];\n  edges: FlowEdge[];\n}\n\nconst FLOW_WIDTH = 600;\nconst COL_CENTER = FLOW_WIDTH / 2;\nconst COL_LEFT = 140;\nconst COL_RIGHT = FLOW_WIDTH - 140;\n\nexport const EXECUTION_FLOWS: Record<string, FlowDefinition> = {\n  s01: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 190 },\n      { id: \"bash\", label: \"Execute Bash\", type: \"subprocess\", x: COL_LEFT, y: 280 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_LEFT, y: 360 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 280 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"bash\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"bash\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s02: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 190 },\n      { id: \"dispatch\", label: \"Tool Dispatch\", type: \"process\", x: COL_LEFT, y: 280 },\n      { id: \"exec\", label: \"bash / read / write / edit\", type: \"subprocess\", x: COL_LEFT, y: 360 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_LEFT, y: 440 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 280 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"dispatch\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"dispatch\", to: \"exec\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s03: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"todo\", label: \"Create Todos\", type: \"process\", x: COL_CENTER, y: 100 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 180 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 260 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT, y: 340 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_LEFT, y: 410 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 340 },\n    ],\n    edges: [\n      { from: \"start\", to: \"todo\" },\n      { from: \"todo\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"exec\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s04: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 190 },\n      { id: \"is_task\", label: \"task tool?\", type: \"decision\", x: COL_LEFT, y: 280 },\n      { id: \"spawn\", label: \"Spawn Subagent\\n(fresh messages[])\", type: \"subprocess\", x: 60, y: 380 },\n      { id: \"sub_loop\", label: \"Subagent Loop\", type: \"process\", x: 60, y: 460 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT + 80, y: 380 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_CENTER, y: 540 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 280 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"is_task\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"is_task\", to: \"spawn\", label: \"task\" },\n      { from: \"is_task\", to: \"exec\", label: \"other\" },\n      { from: \"spawn\", to: \"sub_loop\" },\n      { from: \"sub_loop\", to: \"append\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s05: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 190 },\n      { id: \"is_skill\", label: \"load_skill?\", type: \"decision\", x: COL_LEFT, y: 280 },\n      { id: \"load\", label: \"Read SKILL.md\", type: \"subprocess\", x: 60, y: 370 },\n      { id: \"inject\", label: \"Inject via\\ntool_result\", type: \"process\", x: 60, y: 450 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT + 80, y: 370 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_CENTER, y: 530 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 280 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"is_skill\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"is_skill\", to: \"load\", label: \"skill\" },\n      { from: \"is_skill\", to: \"exec\", label: \"other\" },\n      { from: \"load\", to: \"inject\" },\n      { from: \"inject\", to: \"append\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s06: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"compress_check\", label: \"Over token\\nlimit?\", type: \"decision\", x: COL_CENTER, y: 110 },\n      { id: \"compress\", label: \"Compress Context\", type: \"subprocess\", x: COL_RIGHT, y: 110 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 200 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 280 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT, y: 360 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_LEFT, y: 430 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 360 },\n    ],\n    edges: [\n      { from: \"start\", to: \"compress_check\" },\n      { from: \"compress_check\", to: \"compress\", label: \"yes\" },\n      { from: \"compress_check\", to: \"llm\", label: \"no\" },\n      { from: \"compress\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"exec\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"compress_check\" },\n    ],\n  },\n  s07: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 190 },\n      { id: \"is_task\", label: \"task_manager?\", type: \"decision\", x: COL_LEFT, y: 280 },\n      { id: \"crud\", label: \"CRUD Task\\n(file-based)\", type: \"subprocess\", x: 60, y: 370 },\n      { id: \"dep_check\", label: \"Check\\nDependencies\", type: \"process\", x: 60, y: 450 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT + 80, y: 370 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_CENTER, y: 530 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 280 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"is_task\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"is_task\", to: \"crud\", label: \"task\" },\n      { from: \"is_task\", to: \"exec\", label: \"other\" },\n      { from: \"crud\", to: \"dep_check\" },\n      { from: \"dep_check\", to: \"append\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s08: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 190 },\n      { id: \"bg_check\", label: \"Background?\", type: \"decision\", x: COL_LEFT, y: 280 },\n      { id: \"bg_spawn\", label: \"Spawn Thread\", type: \"subprocess\", x: 60, y: 370 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT + 80, y: 370 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_CENTER, y: 450 },\n      { id: \"notify\", label: \"Notification\\nQueue\", type: \"process\", x: 60, y: 450 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 280 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"bg_check\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"bg_check\", to: \"bg_spawn\", label: \"bg\" },\n      { from: \"bg_check\", to: \"exec\", label: \"fg\" },\n      { from: \"bg_spawn\", to: \"notify\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n      { from: \"notify\", to: \"llm\" },\n    ],\n  },\n  s09: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\\n(team lead)\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 200 },\n      { id: \"is_team\", label: \"Team tool?\", type: \"decision\", x: COL_LEFT, y: 290 },\n      { id: \"spawn\", label: \"Spawn\\nTeammate\", type: \"subprocess\", x: 60, y: 390 },\n      { id: \"msg\", label: \"Send Message\\n(JSONL inbox)\", type: \"subprocess\", x: 60, y: 470 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT + 80, y: 390 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_CENTER, y: 550 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 290 },\n      { id: \"teammate\", label: \"Teammate Agent\\n(own loop)\", type: \"process\", x: COL_RIGHT, y: 470 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"is_team\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"is_team\", to: \"spawn\", label: \"spawn\" },\n      { from: \"is_team\", to: \"exec\", label: \"other\" },\n      { from: \"spawn\", to: \"teammate\" },\n      { from: \"spawn\", to: \"msg\" },\n      { from: \"msg\", to: \"append\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s10: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\\n(team lead)\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 200 },\n      { id: \"is_proto\", label: \"Protocol?\", type: \"decision\", x: COL_LEFT, y: 290 },\n      { id: \"shutdown\", label: \"Shutdown\\nRequest\", type: \"subprocess\", x: 60, y: 390 },\n      { id: \"fsm\", label: \"FSM:\\npending->approved\", type: \"process\", x: 60, y: 470 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT + 80, y: 390 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_CENTER, y: 550 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 290 },\n      { id: \"teammate\", label: \"Teammate\\nreceives request_id\", type: \"process\", x: COL_RIGHT, y: 470 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"is_proto\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"is_proto\", to: \"shutdown\", label: \"shutdown\" },\n      { from: \"is_proto\", to: \"exec\", label: \"other\" },\n      { from: \"shutdown\", to: \"fsm\" },\n      { from: \"fsm\", to: \"teammate\" },\n      { from: \"teammate\", to: \"append\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n  s11: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"inbox\", label: \"Check Inbox\", type: \"process\", x: COL_CENTER, y: 100 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 180 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 260 },\n      { id: \"exec\", label: \"Execute Tool\", type: \"subprocess\", x: COL_LEFT, y: 340 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_LEFT, y: 410 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 340 },\n      { id: \"idle\", label: \"Idle Cycle\", type: \"process\", x: COL_RIGHT, y: 420 },\n      { id: \"poll\", label: \"Poll Tasks\\n+ Auto-Claim\", type: \"subprocess\", x: COL_RIGHT, y: 500 },\n    ],\n    edges: [\n      { from: \"start\", to: \"inbox\" },\n      { from: \"inbox\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"exec\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"exec\", to: \"append\" },\n      { from: \"append\", to: \"llm\" },\n      { from: \"end\", to: \"idle\" },\n      { from: \"idle\", to: \"poll\" },\n      { from: \"poll\", to: \"inbox\" },\n    ],\n  },\n  s12: {\n    nodes: [\n      { id: \"start\", label: \"User Input\", type: \"start\", x: COL_CENTER, y: 30 },\n      { id: \"llm\", label: \"LLM Call\", type: \"process\", x: COL_CENTER, y: 110 },\n      { id: \"tool_check\", label: \"tool_use?\", type: \"decision\", x: COL_CENTER, y: 190 },\n      { id: \"is_wt\", label: \"worktree tool?\", type: \"decision\", x: COL_LEFT, y: 280 },\n      { id: \"task\", label: \"Task Board\\\\n(.tasks)\", type: \"process\", x: 60, y: 360 },\n      { id: \"wt_create\", label: \"Allocate / Enter\\\\nWorktree\", type: \"subprocess\", x: 60, y: 440 },\n      { id: \"wt_run\", label: \"Run in\\\\nIsolated Dir\", type: \"subprocess\", x: COL_LEFT + 80, y: 360 },\n      { id: \"wt_close\", label: \"Closeout:\\\\nworktree_keep / remove\", type: \"process\", x: COL_LEFT + 80, y: 440 },\n      { id: \"events\", label: \"Emit Lifecycle Events\\\\n(side-channel)\", type: \"process\", x: COL_RIGHT, y: 420 },\n      { id: \"events_read\", label: \"Optional Read\\\\nworktree_events\", type: \"subprocess\", x: COL_RIGHT, y: 520 },\n      { id: \"append\", label: \"Append Result\", type: \"process\", x: COL_CENTER, y: 530 },\n      { id: \"end\", label: \"Output\", type: \"end\", x: COL_RIGHT, y: 280 },\n    ],\n    edges: [\n      { from: \"start\", to: \"llm\" },\n      { from: \"llm\", to: \"tool_check\" },\n      { from: \"tool_check\", to: \"is_wt\", label: \"yes\" },\n      { from: \"tool_check\", to: \"end\", label: \"no\" },\n      { from: \"is_wt\", to: \"task\", label: \"task ops\" },\n      { from: \"is_wt\", to: \"wt_create\", label: \"create/bind\" },\n      { from: \"is_wt\", to: \"wt_run\", label: \"run/status\" },\n      { from: \"task\", to: \"wt_create\", label: \"allocate lane\" },\n      { from: \"wt_create\", to: \"wt_run\" },\n      { from: \"task\", to: \"append\", label: \"task result\" },\n      { from: \"wt_create\", to: \"events\", label: \"emit create\" },\n      { from: \"wt_create\", to: \"append\", label: \"create result\" },\n      { from: \"wt_run\", to: \"wt_close\" },\n      { from: \"wt_run\", to: \"append\", label: \"run/status result\" },\n      { from: \"wt_close\", to: \"events\", label: \"emit closeout\" },\n      { from: \"wt_close\", to: \"append\", label: \"closeout result\" },\n      { from: \"events\", to: \"events_read\", label: \"optional query\" },\n      { from: \"events_read\", to: \"append\", label: \"events result\" },\n      { from: \"append\", to: \"llm\" },\n    ],\n  },\n};\n\nexport function getFlowForVersion(version: string): FlowDefinition | null {\n  return EXECUTION_FLOWS[version] ?? null;\n}\n"
  },
  {
    "path": "web/src/data/generated/docs.json",
    "content": "[\n  {\n    \"version\": \"s01\",\n    \"locale\": \"en\",\n    \"title\": \"s01: The Agent Loop\",\n    \"content\": \"# s01: The Agent Loop\\n\\n`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"One loop & Bash is all you need\\\"* -- one tool + one loop = an agent.\\n\\n## Problem\\n\\nA language model can reason about code, but it can't *touch* the real world -- can't read files, run tests, or check errors. Without a loop, every tool call requires you to manually copy-paste results back. You become the loop.\\n\\n## Solution\\n\\n```\\n+--------+      +-------+      +---------+\\n|  User  | ---> |  LLM  | ---> |  Tool   |\\n| prompt |      |       |      | execute |\\n+--------+      +---+---+      +----+----+\\n                    ^                |\\n                    |   tool_result  |\\n                    +----------------+\\n                    (loop until stop_reason != \\\"tool_use\\\")\\n```\\n\\nOne exit condition controls the entire flow. The loop runs until the model stops calling tools.\\n\\n## How It Works\\n\\n1. User prompt becomes the first message.\\n\\n```python\\nmessages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n```\\n\\n2. Send messages + tool definitions to the LLM.\\n\\n```python\\nresponse = client.messages.create(\\n    model=MODEL, system=SYSTEM, messages=messages,\\n    tools=TOOLS, max_tokens=8000,\\n)\\n```\\n\\n3. Append the assistant response. Check `stop_reason` -- if the model didn't call a tool, we're done.\\n\\n```python\\nmessages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\nif response.stop_reason != \\\"tool_use\\\":\\n    return\\n```\\n\\n4. Execute each tool call, collect results, append as a user message. Loop back to step 2.\\n\\n```python\\nresults = []\\nfor block in response.content:\\n    if block.type == \\\"tool_use\\\":\\n        output = run_bash(block.input[\\\"command\\\"])\\n        results.append({\\n            \\\"type\\\": \\\"tool_result\\\",\\n            \\\"tool_use_id\\\": block.id,\\n            \\\"content\\\": output,\\n        })\\nmessages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n```\\n\\nAssembled into one function:\\n\\n```python\\ndef agent_loop(query):\\n    messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": query}]\\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                output = run_bash(block.input[\\\"command\\\"])\\n                results.append({\\n                    \\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": output,\\n                })\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n```\\n\\nThat's the entire agent in under 30 lines. Everything else in this course layers on top -- without changing the loop.\\n\\n## What Changed\\n\\n| Component     | Before     | After                          |\\n|---------------|------------|--------------------------------|\\n| Agent loop    | (none)     | `while True` + stop_reason     |\\n| Tools         | (none)     | `bash` (one tool)              |\\n| Messages      | (none)     | Accumulating list              |\\n| Control flow  | (none)     | `stop_reason != \\\"tool_use\\\"`    |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s01_agent_loop.py\\n```\\n\\n1. `Create a file called hello.py that prints \\\"Hello, World!\\\"`\\n2. `List all Python files in this directory`\\n3. `What is the current git branch?`\\n4. `Create a directory called test_output and write 3 files in it`\\n\"\n  },\n  {\n    \"version\": \"s02\",\n    \"locale\": \"en\",\n    \"title\": \"s02: Tool Use\",\n    \"content\": \"# s02: Tool Use\\n\\n`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"Adding a tool means adding one handler\\\"* -- the loop stays the same; new tools register into the dispatch map.\\n\\n## Problem\\n\\nWith only `bash`, the agent shells out for everything. `cat` truncates unpredictably, `sed` fails on special characters, and every bash call is an unconstrained security surface. Dedicated tools like `read_file` and `write_file` let you enforce path sandboxing at the tool level.\\n\\nThe key insight: adding tools does not require changing the loop.\\n\\n## Solution\\n\\n```\\n+--------+      +-------+      +------------------+\\n|  User  | ---> |  LLM  | ---> | Tool Dispatch    |\\n| prompt |      |       |      | {                |\\n+--------+      +---+---+      |   bash: run_bash |\\n                    ^           |   read: run_read |\\n                    |           |   write: run_wr  |\\n                    +-----------+   edit: run_edit |\\n                    tool_result | }                |\\n                                +------------------+\\n\\nThe dispatch map is a dict: {tool_name: handler_function}.\\nOne lookup replaces any if/elif chain.\\n```\\n\\n## How It Works\\n\\n1. Each tool gets a handler function. Path sandboxing prevents workspace escape.\\n\\n```python\\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_read(path: str, limit: int = None) -> str:\\n    text = safe_path(path).read_text()\\n    lines = text.splitlines()\\n    if limit and limit < len(lines):\\n        lines = lines[:limit]\\n    return \\\"\\\\n\\\".join(lines)[:50000]\\n```\\n\\n2. The dispatch map links tool names to handlers.\\n\\n```python\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"],\\n                                        kw[\\\"new_text\\\"]),\\n}\\n```\\n\\n3. In the loop, look up the handler by name. The loop body itself is unchanged from s01.\\n\\n```python\\nfor block in response.content:\\n    if block.type == \\\"tool_use\\\":\\n        handler = TOOL_HANDLERS.get(block.name)\\n        output = handler(**block.input) if handler \\\\\\n            else f\\\"Unknown tool: {block.name}\\\"\\n        results.append({\\n            \\\"type\\\": \\\"tool_result\\\",\\n            \\\"tool_use_id\\\": block.id,\\n            \\\"content\\\": output,\\n        })\\n```\\n\\nAdd a tool = add a handler + add a schema entry. The loop never changes.\\n\\n## What Changed From s01\\n\\n| Component      | Before (s01)       | After (s02)                |\\n|----------------|--------------------|----------------------------|\\n| Tools          | 1 (bash only)      | 4 (bash, read, write, edit)|\\n| Dispatch       | Hardcoded bash call | `TOOL_HANDLERS` dict       |\\n| Path safety    | None               | `safe_path()` sandbox      |\\n| Agent loop     | Unchanged          | Unchanged                  |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s02_tool_use.py\\n```\\n\\n1. `Read the file requirements.txt`\\n2. `Create a file called greet.py with a greet(name) function`\\n3. `Edit greet.py to add a docstring to the function`\\n4. `Read greet.py to verify the edit worked`\\n\"\n  },\n  {\n    \"version\": \"s03\",\n    \"locale\": \"en\",\n    \"title\": \"s03: TodoWrite\",\n    \"content\": \"# s03: TodoWrite\\n\\n`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"An agent without a plan drifts\\\"* -- list the steps first, then execute.\\n\\n## Problem\\n\\nOn multi-step tasks, the model loses track. It repeats work, skips steps, or wanders off. Long conversations make this worse -- the system prompt fades as tool results fill the context. A 10-step refactoring might complete steps 1-3, then the model starts improvising because it forgot steps 4-10.\\n\\n## Solution\\n\\n```\\n+--------+      +-------+      +---------+\\n|  User  | ---> |  LLM  | ---> | Tools   |\\n| prompt |      |       |      | + todo  |\\n+--------+      +---+---+      +----+----+\\n                    ^                |\\n                    |   tool_result  |\\n                    +----------------+\\n                          |\\n              +-----------+-----------+\\n              | TodoManager state     |\\n              | [ ] task A            |\\n              | [>] task B  <- doing  |\\n              | [x] task C            |\\n              +-----------------------+\\n                          |\\n              if rounds_since_todo >= 3:\\n                inject <reminder> into tool_result\\n```\\n\\n## How It Works\\n\\n1. TodoManager stores items with statuses. Only one item can be `in_progress` at a time.\\n\\n```python\\nclass TodoManager:\\n    def update(self, items: list) -> str:\\n        validated, in_progress_count = [], 0\\n        for item in items:\\n            status = item.get(\\\"status\\\", \\\"pending\\\")\\n            if status == \\\"in_progress\\\":\\n                in_progress_count += 1\\n            validated.append({\\\"id\\\": item[\\\"id\\\"], \\\"text\\\": item[\\\"text\\\"],\\n                              \\\"status\\\": status})\\n        if in_progress_count > 1:\\n            raise ValueError(\\\"Only one task can be in_progress\\\")\\n        self.items = validated\\n        return self.render()\\n```\\n\\n2. The `todo` tool goes into the dispatch map like any other tool.\\n\\n```python\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"todo\\\": lambda **kw: TODO.update(kw[\\\"items\\\"]),\\n}\\n```\\n\\n3. A nag reminder injects a nudge if the model goes 3+ rounds without calling `todo`.\\n\\n```python\\nif rounds_since_todo >= 3 and messages:\\n    last = messages[-1]\\n    if last[\\\"role\\\"] == \\\"user\\\" and isinstance(last.get(\\\"content\\\"), list):\\n        last[\\\"content\\\"].insert(0, {\\n            \\\"type\\\": \\\"text\\\",\\n            \\\"text\\\": \\\"<reminder>Update your todos.</reminder>\\\",\\n        })\\n```\\n\\nThe \\\"one in_progress at a time\\\" constraint forces sequential focus. The nag reminder creates accountability.\\n\\n## What Changed From s02\\n\\n| Component      | Before (s02)     | After (s03)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 4                | 5 (+todo)                  |\\n| Planning       | None             | TodoManager with statuses  |\\n| Nag injection  | None             | `<reminder>` after 3 rounds|\\n| Agent loop     | Simple dispatch  | + rounds_since_todo counter|\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s03_todo_write.py\\n```\\n\\n1. `Refactor the file hello.py: add type hints, docstrings, and a main guard`\\n2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py`\\n3. `Review all Python files and fix any style issues`\\n\"\n  },\n  {\n    \"version\": \"s04\",\n    \"locale\": \"en\",\n    \"title\": \"s04: Subagents\",\n    \"content\": \"# s04: Subagents\\n\\n`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"Break big tasks down; each subtask gets a clean context\\\"* -- subagents use independent messages[], keeping the main conversation clean.\\n\\n## Problem\\n\\nAs the agent works, its messages array grows. Every file read, every bash output stays in context permanently. \\\"What testing framework does this project use?\\\" might require reading 5 files, but the parent only needs the answer: \\\"pytest.\\\"\\n\\n## Solution\\n\\n```\\nParent agent                     Subagent\\n+------------------+             +------------------+\\n| messages=[...]   |             | messages=[]      | <-- fresh\\n|                  |  dispatch   |                  |\\n| tool: task       | ----------> | while tool_use:  |\\n|   prompt=\\\"...\\\"   |             |   call tools     |\\n|                  |  summary    |   append results |\\n|   result = \\\"...\\\" | <---------- | return last text |\\n+------------------+             +------------------+\\n\\nParent context stays clean. Subagent context is discarded.\\n```\\n\\n## How It Works\\n\\n1. The parent gets a `task` tool. The child gets all base tools except `task` (no recursive spawning).\\n\\n```python\\nPARENT_TOOLS = CHILD_TOOLS + [\\n    {\\\"name\\\": \\\"task\\\",\\n     \\\"description\\\": \\\"Spawn a subagent with fresh context.\\\",\\n     \\\"input_schema\\\": {\\n         \\\"type\\\": \\\"object\\\",\\n         \\\"properties\\\": {\\\"prompt\\\": {\\\"type\\\": \\\"string\\\"}},\\n         \\\"required\\\": [\\\"prompt\\\"],\\n     }},\\n]\\n```\\n\\n2. The subagent starts with `messages=[]` and runs its own loop. Only the final text returns to the parent.\\n\\n```python\\ndef run_subagent(prompt: str) -> str:\\n    sub_messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n    for _ in range(30):  # safety limit\\n        response = client.messages.create(\\n            model=MODEL, system=SUBAGENT_SYSTEM,\\n            messages=sub_messages,\\n            tools=CHILD_TOOLS, max_tokens=8000,\\n        )\\n        sub_messages.append({\\\"role\\\": \\\"assistant\\\",\\n                             \\\"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 = TOOL_HANDLERS.get(block.name)\\n                output = handler(**block.input)\\n                results.append({\\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": str(output)[:50000]})\\n        sub_messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n    return \\\"\\\".join(\\n        b.text for b in response.content if hasattr(b, \\\"text\\\")\\n    ) or \\\"(no summary)\\\"\\n```\\n\\nThe child's entire message history (possibly 30+ tool calls) is discarded. The parent receives a one-paragraph summary as a normal `tool_result`.\\n\\n## What Changed From s03\\n\\n| Component      | Before (s03)     | After (s04)               |\\n|----------------|------------------|---------------------------|\\n| Tools          | 5                | 5 (base) + task (parent)  |\\n| Context        | Single shared    | Parent + child isolation  |\\n| Subagent       | None             | `run_subagent()` function |\\n| Return value   | N/A              | Summary text only         |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s04_subagent.py\\n```\\n\\n1. `Use a subtask to find what testing framework this project uses`\\n2. `Delegate: read all .py files and summarize what each one does`\\n3. `Use a task to create a new module, then verify it from here`\\n\"\n  },\n  {\n    \"version\": \"s05\",\n    \"locale\": \"en\",\n    \"title\": \"s05: Skills\",\n    \"content\": \"# s05: Skills\\n\\n`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"Load knowledge when you need it, not upfront\\\"* -- inject via tool_result, not the system prompt.\\n\\n## Problem\\n\\nYou want the agent to follow domain-specific workflows: git conventions, testing patterns, code review checklists. Putting everything in the system prompt wastes tokens on unused skills. 10 skills at 2000 tokens each = 20,000 tokens, most of which are irrelevant to any given task.\\n\\n## Solution\\n\\n```\\nSystem prompt (Layer 1 -- always present):\\n+--------------------------------------+\\n| You are a coding agent.              |\\n| Skills available:                    |\\n|   - git: Git workflow helpers        |  ~100 tokens/skill\\n|   - test: Testing best practices     |\\n+--------------------------------------+\\n\\nWhen model calls load_skill(\\\"git\\\"):\\n+--------------------------------------+\\n| tool_result (Layer 2 -- on demand):  |\\n| <skill name=\\\"git\\\">                   |\\n|   Full git workflow instructions...  |  ~2000 tokens\\n|   Step 1: ...                        |\\n| </skill>                             |\\n+--------------------------------------+\\n```\\n\\nLayer 1: skill *names* in system prompt (cheap). Layer 2: full *body* via tool_result (on demand).\\n\\n## How It Works\\n\\n1. Each skill is a directory containing a `SKILL.md` with YAML frontmatter.\\n\\n```\\nskills/\\n  pdf/\\n    SKILL.md       # ---\\\\n name: pdf\\\\n description: Process PDF files\\\\n ---\\\\n ...\\n  code-review/\\n    SKILL.md       # ---\\\\n name: code-review\\\\n description: Review code\\\\n ---\\\\n ...\\n```\\n\\n2. SkillLoader scans for `SKILL.md` files, uses the directory name as the skill identifier.\\n\\n```python\\nclass SkillLoader:\\n    def __init__(self, skills_dir: Path):\\n        self.skills = {}\\n        for f in sorted(skills_dir.rglob(\\\"SKILL.md\\\")):\\n            text = f.read_text()\\n            meta, body = self._parse_frontmatter(text)\\n            name = meta.get(\\\"name\\\", f.parent.name)\\n            self.skills[name] = {\\\"meta\\\": meta, \\\"body\\\": body}\\n\\n    def get_descriptions(self) -> str:\\n        lines = []\\n        for name, skill in self.skills.items():\\n            desc = skill[\\\"meta\\\"].get(\\\"description\\\", \\\"\\\")\\n            lines.append(f\\\"  - {name}: {desc}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def get_content(self, name: str) -> str:\\n        skill = self.skills.get(name)\\n        if not skill:\\n            return f\\\"Error: Unknown skill '{name}'.\\\"\\n        return f\\\"<skill name=\\\\\\\"{name}\\\\\\\">\\\\n{skill['body']}\\\\n</skill>\\\"\\n```\\n\\n3. Layer 1 goes into the system prompt. Layer 2 is just another tool handler.\\n\\n```python\\nSYSTEM = f\\\"\\\"\\\"You are a coding agent at {WORKDIR}.\\nSkills available:\\n{SKILL_LOADER.get_descriptions()}\\\"\\\"\\\"\\n\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"load_skill\\\": lambda **kw: SKILL_LOADER.get_content(kw[\\\"name\\\"]),\\n}\\n```\\n\\nThe model learns what skills exist (cheap) and loads them when relevant (expensive).\\n\\n## What Changed From s04\\n\\n| Component      | Before (s04)     | After (s05)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 5 (base + task)  | 5 (base + load_skill)      |\\n| System prompt  | Static string    | + skill descriptions       |\\n| Knowledge      | None             | skills/\\\\*/SKILL.md files   |\\n| Injection      | None             | Two-layer (system + result)|\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s05_skill_loading.py\\n```\\n\\n1. `What skills are available?`\\n2. `Load the agent-builder skill and follow its instructions`\\n3. `I need to do a code review -- load the relevant skill first`\\n4. `Build an MCP server using the mcp-builder skill`\\n\"\n  },\n  {\n    \"version\": \"s06\",\n    \"locale\": \"en\",\n    \"title\": \"s06: Context Compact\",\n    \"content\": \"# s06: Context Compact\\n\\n`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"Context will fill up; you need a way to make room\\\"* -- three-layer compression strategy for infinite sessions.\\n\\n## Problem\\n\\nThe context window is finite. A single `read_file` on a 1000-line file costs ~4000 tokens. After reading 30 files and running 20 bash commands, you hit 100,000+ tokens. The agent cannot work on large codebases without compression.\\n\\n## Solution\\n\\nThree layers, increasing in aggressiveness:\\n\\n```\\nEvery turn:\\n+------------------+\\n| Tool call result |\\n+------------------+\\n        |\\n        v\\n[Layer 1: micro_compact]        (silent, every turn)\\n  Replace tool_result > 3 turns old\\n  with \\\"[Previous: used {tool_name}]\\\"\\n        |\\n        v\\n[Check: tokens > 50000?]\\n   |               |\\n   no              yes\\n   |               |\\n   v               v\\ncontinue    [Layer 2: auto_compact]\\n              Save transcript to .transcripts/\\n              LLM summarizes conversation.\\n              Replace all messages with [summary].\\n                    |\\n                    v\\n            [Layer 3: compact tool]\\n              Model calls compact explicitly.\\n              Same summarization as auto_compact.\\n```\\n\\n## How It Works\\n\\n1. **Layer 1 -- micro_compact**: Before each LLM call, replace old tool results with placeholders.\\n\\n```python\\ndef micro_compact(messages: list) -> list:\\n    tool_results = []\\n    for i, msg in enumerate(messages):\\n        if msg[\\\"role\\\"] == \\\"user\\\" and isinstance(msg.get(\\\"content\\\"), list):\\n            for j, part in enumerate(msg[\\\"content\\\"]):\\n                if isinstance(part, dict) and part.get(\\\"type\\\") == \\\"tool_result\\\":\\n                    tool_results.append((i, j, part))\\n    if len(tool_results) <= KEEP_RECENT:\\n        return messages\\n    for _, _, part in tool_results[:-KEEP_RECENT]:\\n        if len(part.get(\\\"content\\\", \\\"\\\")) > 100:\\n            part[\\\"content\\\"] = f\\\"[Previous: used {tool_name}]\\\"\\n    return messages\\n```\\n\\n2. **Layer 2 -- auto_compact**: When tokens exceed threshold, save full transcript to disk, then ask the LLM to summarize.\\n\\n```python\\ndef auto_compact(messages: list) -> list:\\n    # Save transcript for recovery\\n    transcript_path = TRANSCRIPT_DIR / f\\\"transcript_{int(time.time())}.jsonl\\\"\\n    with open(transcript_path, \\\"w\\\") as f:\\n        for msg in messages:\\n            f.write(json.dumps(msg, default=str) + \\\"\\\\n\\\")\\n    # LLM summarizes\\n    response = client.messages.create(\\n        model=MODEL,\\n        messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\":\\n            \\\"Summarize this conversation for continuity...\\\"\\n            + json.dumps(messages, default=str)[:80000]}],\\n        max_tokens=2000,\\n    )\\n    return [\\n        {\\\"role\\\": \\\"user\\\", \\\"content\\\": f\\\"[Compressed]\\\\n\\\\n{response.content[0].text}\\\"},\\n        {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"Understood. Continuing.\\\"},\\n    ]\\n```\\n\\n3. **Layer 3 -- manual compact**: The `compact` tool triggers the same summarization on demand.\\n\\n4. The loop integrates all three:\\n\\n```python\\ndef agent_loop(messages: list):\\n    while True:\\n        micro_compact(messages)                        # Layer 1\\n        if estimate_tokens(messages) > THRESHOLD:\\n            messages[:] = auto_compact(messages)       # Layer 2\\n        response = client.messages.create(...)\\n        # ... tool execution ...\\n        if manual_compact:\\n            messages[:] = auto_compact(messages)       # Layer 3\\n```\\n\\nTranscripts preserve full history on disk. Nothing is truly lost -- just moved out of active context.\\n\\n## What Changed From s05\\n\\n| Component      | Before (s05)     | After (s06)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 5                | 5 (base + compact)         |\\n| Context mgmt   | None             | Three-layer compression    |\\n| Micro-compact  | None             | Old results -> placeholders|\\n| Auto-compact   | None             | Token threshold trigger    |\\n| Transcripts    | None             | Saved to .transcripts/     |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s06_context_compact.py\\n```\\n\\n1. `Read every Python file in the agents/ directory one by one` (watch micro-compact replace old results)\\n2. `Keep reading files until compression triggers automatically`\\n3. `Use the compact tool to manually compress the conversation`\\n\"\n  },\n  {\n    \"version\": \"s07\",\n    \"locale\": \"en\",\n    \"title\": \"s07: Task System\",\n    \"content\": \"# s07: Task System\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"Break big goals into small tasks, order them, persist to disk\\\"* -- a file-based task graph with dependencies, laying the foundation for multi-agent collaboration.\\n\\n## Problem\\n\\ns03's TodoManager is a flat checklist in memory: no ordering, no dependencies, no status beyond done-or-not. Real goals have structure -- task B depends on task A, tasks C and D can run in parallel, task E waits for both C and D.\\n\\nWithout explicit relationships, the agent can't tell what's ready, what's blocked, or what can run concurrently. And because the list lives only in memory, context compression (s06) wipes it clean.\\n\\n## Solution\\n\\nPromote the checklist into a **task graph** persisted to disk. Each task is a JSON file with status, dependencies (`blockedBy`), and dependents (`blocks`). The graph answers three questions at any moment:\\n\\n- **What's ready?** -- tasks with `pending` status and empty `blockedBy`.\\n- **What's blocked?** -- tasks waiting on unfinished dependencies.\\n- **What's done?** -- `completed` tasks, whose completion automatically unblocks dependents.\\n\\n```\\n.tasks/\\n  task_1.json  {\\\"id\\\":1, \\\"status\\\":\\\"completed\\\"}\\n  task_2.json  {\\\"id\\\":2, \\\"blockedBy\\\":[1], \\\"status\\\":\\\"pending\\\"}\\n  task_3.json  {\\\"id\\\":3, \\\"blockedBy\\\":[1], \\\"status\\\":\\\"pending\\\"}\\n  task_4.json  {\\\"id\\\":4, \\\"blockedBy\\\":[2,3], \\\"status\\\":\\\"pending\\\"}\\n\\nTask graph (DAG):\\n                 +----------+\\n            +--> | task 2   | --+\\n            |    | pending  |   |\\n+----------+     +----------+    +--> +----------+\\n| task 1   |                          | task 4   |\\n| completed| --> +----------+    +--> | blocked  |\\n+----------+     | task 3   | --+     +----------+\\n                 | pending  |\\n                 +----------+\\n\\nOrdering:     task 1 must finish before 2 and 3\\nParallelism:  tasks 2 and 3 can run at the same time\\nDependencies: task 4 waits for both 2 and 3\\nStatus:       pending -> in_progress -> completed\\n```\\n\\nThis task graph becomes the coordination backbone for everything after s07: background execution (s08), multi-agent teams (s09+), and worktree isolation (s12) all read from and write to this same structure.\\n\\n## How It Works\\n\\n1. **TaskManager**: one JSON file per task, CRUD with dependency graph.\\n\\n```python\\nclass TaskManager:\\n    def __init__(self, tasks_dir: Path):\\n        self.dir = tasks_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self._next_id = self._max_id() + 1\\n\\n    def create(self, subject, description=\\\"\\\"):\\n        task = {\\\"id\\\": self._next_id, \\\"subject\\\": subject,\\n                \\\"status\\\": \\\"pending\\\", \\\"blockedBy\\\": [],\\n                \\\"blocks\\\": [], \\\"owner\\\": \\\"\\\"}\\n        self._save(task)\\n        self._next_id += 1\\n        return json.dumps(task, indent=2)\\n```\\n\\n2. **Dependency resolution**: completing a task clears its ID from every other task's `blockedBy` list, automatically unblocking dependents.\\n\\n```python\\ndef _clear_dependency(self, completed_id):\\n    for f in self.dir.glob(\\\"task_*.json\\\"):\\n        task = json.loads(f.read_text())\\n        if completed_id in task.get(\\\"blockedBy\\\", []):\\n            task[\\\"blockedBy\\\"].remove(completed_id)\\n            self._save(task)\\n```\\n\\n3. **Status + dependency wiring**: `update` handles transitions and dependency edges.\\n\\n```python\\ndef update(self, task_id, status=None,\\n           add_blocked_by=None, add_blocks=None):\\n    task = self._load(task_id)\\n    if status:\\n        task[\\\"status\\\"] = status\\n        if status == \\\"completed\\\":\\n            self._clear_dependency(task_id)\\n    self._save(task)\\n```\\n\\n4. Four task tools go into the dispatch map.\\n\\n```python\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"task_create\\\": lambda **kw: TASKS.create(kw[\\\"subject\\\"]),\\n    \\\"task_update\\\": lambda **kw: TASKS.update(kw[\\\"task_id\\\"], kw.get(\\\"status\\\")),\\n    \\\"task_list\\\":   lambda **kw: TASKS.list_all(),\\n    \\\"task_get\\\":    lambda **kw: TASKS.get(kw[\\\"task_id\\\"]),\\n}\\n```\\n\\nFrom s07 onward, the task graph is the default for multi-step work. s03's Todo remains for quick single-session checklists.\\n\\n## What Changed From s06\\n\\n| Component | Before (s06) | After (s07) |\\n|---|---|---|\\n| Tools | 5 | 8 (`task_create/update/list/get`) |\\n| Planning model | Flat checklist (in-memory) | Task graph with dependencies (on disk) |\\n| Relationships | None | `blockedBy` + `blocks` edges |\\n| Status tracking | Done or not | `pending` -> `in_progress` -> `completed` |\\n| Persistence | Lost on compression | Survives compression and restarts |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s07_task_system.py\\n```\\n\\n1. `Create 3 tasks: \\\"Setup project\\\", \\\"Write code\\\", \\\"Write tests\\\". Make them depend on each other in order.`\\n2. `List all tasks and show the dependency graph`\\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\\n\"\n  },\n  {\n    \"version\": \"s08\",\n    \"locale\": \"en\",\n    \"title\": \"s08: Background Tasks\",\n    \"content\": \"# s08: Background Tasks\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12`\\n\\n> *\\\"Run slow operations in the background; the agent keeps thinking\\\"* -- daemon threads run commands, inject notifications on completion.\\n\\n## Problem\\n\\nSome commands take minutes: `npm install`, `pytest`, `docker build`. With a blocking loop, the model sits idle waiting. If the user asks \\\"install dependencies and while that runs, create the config file,\\\" the agent does them sequentially, not in parallel.\\n\\n## Solution\\n\\n```\\nMain thread                Background thread\\n+-----------------+        +-----------------+\\n| agent loop      |        | subprocess runs |\\n| ...             |        | ...             |\\n| [LLM call] <---+------- | enqueue(result) |\\n|  ^drain queue   |        +-----------------+\\n+-----------------+\\n\\nTimeline:\\nAgent --[spawn A]--[spawn B]--[other work]----\\n             |          |\\n             v          v\\n          [A runs]   [B runs]      (parallel)\\n             |          |\\n             +-- results injected before next LLM call --+\\n```\\n\\n## How It Works\\n\\n1. BackgroundManager tracks tasks with a thread-safe notification queue.\\n\\n```python\\nclass BackgroundManager:\\n    def __init__(self):\\n        self.tasks = {}\\n        self._notification_queue = []\\n        self._lock = threading.Lock()\\n```\\n\\n2. `run()` starts a daemon thread and returns immediately.\\n\\n```python\\ndef run(self, command: str) -> str:\\n    task_id = str(uuid.uuid4())[:8]\\n    self.tasks[task_id] = {\\\"status\\\": \\\"running\\\", \\\"command\\\": command}\\n    thread = threading.Thread(\\n        target=self._execute, args=(task_id, command), daemon=True)\\n    thread.start()\\n    return f\\\"Background task {task_id} started\\\"\\n```\\n\\n3. When the subprocess finishes, its result goes into the notification queue.\\n\\n```python\\ndef _execute(self, task_id, command):\\n    try:\\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\\n            capture_output=True, text=True, timeout=300)\\n        output = (r.stdout + r.stderr).strip()[:50000]\\n    except subprocess.TimeoutExpired:\\n        output = \\\"Error: Timeout (300s)\\\"\\n    with self._lock:\\n        self._notification_queue.append({\\n            \\\"task_id\\\": task_id, \\\"result\\\": output[:500]})\\n```\\n\\n4. The agent loop drains notifications before each LLM call.\\n\\n```python\\ndef agent_loop(messages: list):\\n    while True:\\n        notifs = BG.drain_notifications()\\n        if notifs:\\n            notif_text = \\\"\\\\n\\\".join(\\n                f\\\"[bg:{n['task_id']}] {n['result']}\\\" for n in notifs)\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<background-results>\\\\n{notif_text}\\\\n\\\"\\n                           f\\\"</background-results>\\\"})\\n            messages.append({\\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted background results.\\\"})\\n        response = client.messages.create(...)\\n```\\n\\nThe loop stays single-threaded. Only subprocess I/O is parallelized.\\n\\n## What Changed From s07\\n\\n| Component      | Before (s07)     | After (s08)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 8                | 6 (base + background_run + check)|\\n| Execution      | Blocking only    | Blocking + background threads|\\n| Notification   | None             | Queue drained per loop     |\\n| Concurrency    | None             | Daemon threads             |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s08_background_tasks.py\\n```\\n\\n1. `Run \\\"sleep 5 && echo done\\\" in the background, then create a file while it runs`\\n2. `Start 3 background tasks: \\\"sleep 2\\\", \\\"sleep 4\\\", \\\"sleep 6\\\". Check their status.`\\n3. `Run pytest in the background and keep working on other things`\\n\"\n  },\n  {\n    \"version\": \"s09\",\n    \"locale\": \"en\",\n    \"title\": \"s09: Agent Teams\",\n    \"content\": \"# s09: Agent Teams\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12`\\n\\n> *\\\"When the task is too big for one, delegate to teammates\\\"* -- persistent teammates + async mailboxes.\\n\\n## Problem\\n\\nSubagents (s04) are disposable: spawn, work, return summary, die. No identity, no memory between invocations. Background tasks (s08) run shell commands but can't make LLM-guided decisions.\\n\\nReal teamwork needs: (1) persistent agents that outlive a single prompt, (2) identity and lifecycle management, (3) a communication channel between agents.\\n\\n## Solution\\n\\n```\\nTeammate lifecycle:\\n  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\\n\\nCommunication:\\n  .team/\\n    config.json           <- team roster + statuses\\n    inbox/\\n      alice.jsonl         <- append-only, drain-on-read\\n      bob.jsonl\\n      lead.jsonl\\n\\n              +--------+    send(\\\"alice\\\",\\\"bob\\\",\\\"...\\\")    +--------+\\n              | alice  | -----------------------------> |  bob   |\\n              | loop   |    bob.jsonl << {json_line}    |  loop  |\\n              +--------+                                +--------+\\n                   ^                                         |\\n                   |        BUS.read_inbox(\\\"alice\\\")          |\\n                   +---- alice.jsonl -> read + drain ---------+\\n```\\n\\n## How It Works\\n\\n1. TeammateManager maintains config.json with the team roster.\\n\\n```python\\nclass TeammateManager:\\n    def __init__(self, team_dir: Path):\\n        self.dir = team_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self.config_path = self.dir / \\\"config.json\\\"\\n        self.config = self._load_config()\\n        self.threads = {}\\n```\\n\\n2. `spawn()` creates a teammate and starts its agent loop in a thread.\\n\\n```python\\ndef spawn(self, name: str, role: str, prompt: str) -> str:\\n    member = {\\\"name\\\": name, \\\"role\\\": role, \\\"status\\\": \\\"working\\\"}\\n    self.config[\\\"members\\\"].append(member)\\n    self._save_config()\\n    thread = threading.Thread(\\n        target=self._teammate_loop,\\n        args=(name, role, prompt), daemon=True)\\n    thread.start()\\n    return f\\\"Spawned teammate '{name}' (role: {role})\\\"\\n```\\n\\n3. MessageBus: append-only JSONL inboxes. `send()` appends a JSON line; `read_inbox()` reads all and drains.\\n\\n```python\\nclass MessageBus:\\n    def send(self, sender, to, content, msg_type=\\\"message\\\", extra=None):\\n        msg = {\\\"type\\\": msg_type, \\\"from\\\": sender,\\n               \\\"content\\\": content, \\\"timestamp\\\": time.time()}\\n        if extra:\\n            msg.update(extra)\\n        with open(self.dir / f\\\"{to}.jsonl\\\", \\\"a\\\") as f:\\n            f.write(json.dumps(msg) + \\\"\\\\n\\\")\\n\\n    def read_inbox(self, name):\\n        path = self.dir / f\\\"{name}.jsonl\\\"\\n        if not path.exists(): return \\\"[]\\\"\\n        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]\\n        path.write_text(\\\"\\\")  # drain\\n        return json.dumps(msgs, indent=2)\\n```\\n\\n4. Each teammate checks its inbox before every LLM call, injecting received messages into context.\\n\\n```python\\ndef _teammate_loop(self, name, role, prompt):\\n    messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n    for _ in range(50):\\n        inbox = BUS.read_inbox(name)\\n        if inbox != \\\"[]\\\":\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{inbox}</inbox>\\\"})\\n            messages.append({\\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted inbox messages.\\\"})\\n        response = client.messages.create(...)\\n        if response.stop_reason != \\\"tool_use\\\":\\n            break\\n        # execute tools, append results...\\n    self._find_member(name)[\\\"status\\\"] = \\\"idle\\\"\\n```\\n\\n## What Changed From s08\\n\\n| Component      | Before (s08)     | After (s09)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 6                | 9 (+spawn/send/read_inbox) |\\n| Agents         | Single           | Lead + N teammates         |\\n| Persistence    | None             | config.json + JSONL inboxes|\\n| Threads        | Background cmds  | Full agent loops per thread|\\n| Lifecycle      | Fire-and-forget  | idle -> working -> idle    |\\n| Communication  | None             | message + broadcast        |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s09_agent_teams.py\\n```\\n\\n1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.`\\n2. `Broadcast \\\"status update: phase 1 complete\\\" to all teammates`\\n3. `Check the lead inbox for any messages`\\n4. Type `/team` to see the team roster with statuses\\n5. Type `/inbox` to manually check the lead's inbox\\n\"\n  },\n  {\n    \"version\": \"s10\",\n    \"locale\": \"en\",\n    \"title\": \"s10: Team Protocols\",\n    \"content\": \"# s10: Team Protocols\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12`\\n\\n> *\\\"Teammates need shared communication rules\\\"* -- one request-response pattern drives all negotiation.\\n\\n## Problem\\n\\nIn s09, teammates work and communicate but lack structured coordination:\\n\\n**Shutdown**: Killing a thread leaves files half-written and config.json stale. You need a handshake: the lead requests, the teammate approves (finish and exit) or rejects (keep working).\\n\\n**Plan approval**: When the lead says \\\"refactor the auth module,\\\" the teammate starts immediately. For high-risk changes, the lead should review the plan first.\\n\\nBoth share the same structure: one side sends a request with a unique ID, the other responds referencing that ID.\\n\\n## Solution\\n\\n```\\nShutdown Protocol            Plan Approval Protocol\\n==================           ======================\\n\\nLead             Teammate    Teammate           Lead\\n  |                 |           |                 |\\n  |--shutdown_req-->|           |--plan_req------>|\\n  | {req_id:\\\"abc\\\"}  |           | {req_id:\\\"xyz\\\"}  |\\n  |                 |           |                 |\\n  |<--shutdown_resp-|           |<--plan_resp-----|\\n  | {req_id:\\\"abc\\\",  |           | {req_id:\\\"xyz\\\",  |\\n  |  approve:true}  |           |  approve:true}  |\\n\\nShared FSM:\\n  [pending] --approve--> [approved]\\n  [pending] --reject---> [rejected]\\n\\nTrackers:\\n  shutdown_requests = {req_id: {target, status}}\\n  plan_requests     = {req_id: {from, plan, status}}\\n```\\n\\n## How It Works\\n\\n1. The lead initiates shutdown by generating a request_id and sending through the inbox.\\n\\n```python\\nshutdown_requests = {}\\n\\ndef handle_shutdown_request(teammate: str) -> str:\\n    req_id = str(uuid.uuid4())[:8]\\n    shutdown_requests[req_id] = {\\\"target\\\": teammate, \\\"status\\\": \\\"pending\\\"}\\n    BUS.send(\\\"lead\\\", teammate, \\\"Please shut down gracefully.\\\",\\n             \\\"shutdown_request\\\", {\\\"request_id\\\": req_id})\\n    return f\\\"Shutdown request {req_id} sent (status: pending)\\\"\\n```\\n\\n2. The teammate receives the request and responds with approve/reject.\\n\\n```python\\nif tool_name == \\\"shutdown_response\\\":\\n    req_id = args[\\\"request_id\\\"]\\n    approve = args[\\\"approve\\\"]\\n    shutdown_requests[req_id][\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(sender, \\\"lead\\\", args.get(\\\"reason\\\", \\\"\\\"),\\n             \\\"shutdown_response\\\",\\n             {\\\"request_id\\\": req_id, \\\"approve\\\": approve})\\n```\\n\\n3. Plan approval follows the identical pattern. The teammate submits a plan (generating a request_id), the lead reviews (referencing the same request_id).\\n\\n```python\\nplan_requests = {}\\n\\ndef handle_plan_review(request_id, approve, feedback=\\\"\\\"):\\n    req = plan_requests[request_id]\\n    req[\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(\\\"lead\\\", req[\\\"from\\\"], feedback,\\n             \\\"plan_approval_response\\\",\\n             {\\\"request_id\\\": request_id, \\\"approve\\\": approve})\\n```\\n\\nOne FSM, two applications. The same `pending -> approved | rejected` state machine handles any request-response protocol.\\n\\n## What Changed From s09\\n\\n| Component      | Before (s09)     | After (s10)                  |\\n|----------------|------------------|------------------------------|\\n| Tools          | 9                | 12 (+shutdown_req/resp +plan)|\\n| Shutdown       | Natural exit only| Request-response handshake   |\\n| Plan gating    | None             | Submit/review with approval  |\\n| Correlation    | None             | request_id per request       |\\n| FSM            | None             | pending -> approved/rejected |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s10_team_protocols.py\\n```\\n\\n1. `Spawn alice as a coder. Then request her shutdown.`\\n2. `List teammates to see alice's status after shutdown approval`\\n3. `Spawn bob with a risky refactoring task. Review and reject his plan.`\\n4. `Spawn charlie, have him submit a plan, then approve it.`\\n5. Type `/team` to monitor statuses\\n\"\n  },\n  {\n    \"version\": \"s11\",\n    \"locale\": \"en\",\n    \"title\": \"s11: Autonomous Agents\",\n    \"content\": \"# s11: Autonomous Agents\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12`\\n\\n> *\\\"Teammates scan the board and claim tasks themselves\\\"* -- no need for the lead to assign each one.\\n\\n## Problem\\n\\nIn s09-s10, teammates only work when explicitly told to. The lead must spawn each one with a specific prompt. 10 unclaimed tasks on the board? The lead assigns each one manually. Doesn't scale.\\n\\nTrue autonomy: teammates scan the task board themselves, claim unclaimed tasks, work on them, then look for more.\\n\\nOne subtlety: after context compression (s06), the agent might forget who it is. Identity re-injection fixes this.\\n\\n## Solution\\n\\n```\\nTeammate lifecycle with idle cycle:\\n\\n+-------+\\n| spawn |\\n+---+---+\\n    |\\n    v\\n+-------+   tool_use     +-------+\\n| WORK  | <------------- |  LLM  |\\n+---+---+                +-------+\\n    |\\n    | stop_reason != tool_use (or idle tool called)\\n    v\\n+--------+\\n|  IDLE  |  poll every 5s for up to 60s\\n+---+----+\\n    |\\n    +---> check inbox --> message? ----------> WORK\\n    |\\n    +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\\n    |\\n    +---> 60s timeout ----------------------> SHUTDOWN\\n\\nIdentity re-injection after compression:\\n  if len(messages) <= 3:\\n    messages.insert(0, identity_block)\\n```\\n\\n## How It Works\\n\\n1. The teammate loop has two phases: WORK and IDLE. When the LLM stops calling tools (or calls `idle`), the teammate enters IDLE.\\n\\n```python\\ndef _loop(self, name, role, prompt):\\n    while True:\\n        # -- WORK PHASE --\\n        messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n        for _ in range(50):\\n            response = client.messages.create(...)\\n            if response.stop_reason != \\\"tool_use\\\":\\n                break\\n            # execute tools...\\n            if idle_requested:\\n                break\\n\\n        # -- IDLE PHASE --\\n        self._set_status(name, \\\"idle\\\")\\n        resume = self._idle_poll(name, messages)\\n        if not resume:\\n            self._set_status(name, \\\"shutdown\\\")\\n            return\\n        self._set_status(name, \\\"working\\\")\\n```\\n\\n2. The idle phase polls inbox and task board in a loop.\\n\\n```python\\ndef _idle_poll(self, name, messages):\\n    for _ in range(IDLE_TIMEOUT // POLL_INTERVAL):  # 60s / 5s = 12\\n        time.sleep(POLL_INTERVAL)\\n        inbox = BUS.read_inbox(name)\\n        if inbox:\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{inbox}</inbox>\\\"})\\n            return True\\n        unclaimed = scan_unclaimed_tasks()\\n        if unclaimed:\\n            claim_task(unclaimed[0][\\\"id\\\"], name)\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<auto-claimed>Task #{unclaimed[0]['id']}: \\\"\\n                           f\\\"{unclaimed[0]['subject']}</auto-claimed>\\\"})\\n            return True\\n    return False  # timeout -> shutdown\\n```\\n\\n3. Task board scanning: find pending, unowned, unblocked tasks.\\n\\n```python\\ndef scan_unclaimed_tasks() -> list:\\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 not task.get(\\\"blockedBy\\\")):\\n            unclaimed.append(task)\\n    return unclaimed\\n```\\n\\n4. Identity re-injection: when context is too short (compression happened), insert an identity block.\\n\\n```python\\nif len(messages) <= 3:\\n    messages.insert(0, {\\\"role\\\": \\\"user\\\",\\n        \\\"content\\\": f\\\"<identity>You are '{name}', role: {role}, \\\"\\n                   f\\\"team: {team_name}. Continue your work.</identity>\\\"})\\n    messages.insert(1, {\\\"role\\\": \\\"assistant\\\",\\n        \\\"content\\\": f\\\"I am {name}. Continuing.\\\"})\\n```\\n\\n## What Changed From s10\\n\\n| Component      | Before (s10)     | After (s11)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 12               | 14 (+idle, +claim_task)    |\\n| Autonomy       | Lead-directed    | Self-organizing            |\\n| Idle phase     | None             | Poll inbox + task board    |\\n| Task claiming  | Manual only      | Auto-claim unclaimed tasks |\\n| Identity       | System prompt    | + re-injection after compress|\\n| Timeout        | None             | 60s idle -> auto shutdown  |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s11_autonomous_agents.py\\n```\\n\\n1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.`\\n2. `Spawn a coder teammate and let it find work from the task board itself`\\n3. `Create tasks with dependencies. Watch teammates respect the blocked order.`\\n4. Type `/tasks` to see the task board with owners\\n5. Type `/team` to monitor who is working vs idle\\n\"\n  },\n  {\n    \"version\": \"s12\",\n    \"locale\": \"en\",\n    \"title\": \"s12: Worktree + Task Isolation\",\n    \"content\": \"# s12: Worktree + Task Isolation\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\\n\\n> *\\\"Each works in its own directory, no interference\\\"* -- tasks manage goals, worktrees manage directories, bound by ID.\\n\\n## Problem\\n\\nBy s11, agents can claim and complete tasks autonomously. But every task runs in one shared directory. Two agents refactoring different modules at the same time will collide: agent A edits `config.py`, agent B edits `config.py`, unstaged changes mix, and neither can roll back cleanly.\\n\\nThe task board tracks *what to do* but has no opinion about *where to do it*. The fix: give each task its own git worktree directory. Tasks manage goals, worktrees manage execution context. Bind them by task ID.\\n\\n## Solution\\n\\n```\\nControl plane (.tasks/)             Execution plane (.worktrees/)\\n+------------------+                +------------------------+\\n| task_1.json      |                | auth-refactor/         |\\n|   status: in_progress  <------>   branch: wt/auth-refactor\\n|   worktree: \\\"auth-refactor\\\"   |   task_id: 1             |\\n+------------------+                +------------------------+\\n| task_2.json      |                | ui-login/              |\\n|   status: pending    <------>     branch: wt/ui-login\\n|   worktree: \\\"ui-login\\\"       |   task_id: 2             |\\n+------------------+                +------------------------+\\n                                    |\\n                          index.json (worktree registry)\\n                          events.jsonl (lifecycle log)\\n\\nState machines:\\n  Task:     pending -> in_progress -> completed\\n  Worktree: absent  -> active      -> removed | kept\\n```\\n\\n## How It Works\\n\\n1. **Create a task.** Persist the goal first.\\n\\n```python\\nTASKS.create(\\\"Implement auth refactor\\\")\\n# -> .tasks/task_1.json  status=pending  worktree=\\\"\\\"\\n```\\n\\n2. **Create a worktree and bind to the task.** Passing `task_id` auto-advances the task to `in_progress`.\\n\\n```python\\nWORKTREES.create(\\\"auth-refactor\\\", task_id=1)\\n# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD\\n# -> index.json gets new entry, task_1.json gets worktree=\\\"auth-refactor\\\"\\n```\\n\\nThe binding writes state to both sides:\\n\\n```python\\ndef bind_worktree(self, task_id, worktree):\\n    task = self._load(task_id)\\n    task[\\\"worktree\\\"] = worktree\\n    if task[\\\"status\\\"] == \\\"pending\\\":\\n        task[\\\"status\\\"] = \\\"in_progress\\\"\\n    self._save(task)\\n```\\n\\n3. **Run commands in the worktree.** `cwd` points to the isolated directory.\\n\\n```python\\nsubprocess.run(command, shell=True, cwd=worktree_path,\\n               capture_output=True, text=True, timeout=300)\\n```\\n\\n4. **Close out.** Two choices:\\n   - `worktree_keep(name)` -- preserve the directory for later.\\n   - `worktree_remove(name, complete_task=True)` -- remove directory, complete the bound task, emit event. One call handles teardown + completion.\\n\\n```python\\ndef remove(self, name, force=False, complete_task=False):\\n    self._run_git([\\\"worktree\\\", \\\"remove\\\", wt[\\\"path\\\"]])\\n    if complete_task and wt.get(\\\"task_id\\\") is not None:\\n        self.tasks.update(wt[\\\"task_id\\\"], status=\\\"completed\\\")\\n        self.tasks.unbind_worktree(wt[\\\"task_id\\\"])\\n        self.events.emit(\\\"task.completed\\\", ...)\\n```\\n\\n5. **Event stream.** Every lifecycle step emits to `.worktrees/events.jsonl`:\\n\\n```json\\n{\\n  \\\"event\\\": \\\"worktree.remove.after\\\",\\n  \\\"task\\\": {\\\"id\\\": 1, \\\"status\\\": \\\"completed\\\"},\\n  \\\"worktree\\\": {\\\"name\\\": \\\"auth-refactor\\\", \\\"status\\\": \\\"removed\\\"},\\n  \\\"ts\\\": 1730000000\\n}\\n```\\n\\nEvents emitted: `worktree.create.before/after/failed`, `worktree.remove.before/after/failed`, `worktree.keep`, `task.completed`.\\n\\nAfter a crash, state reconstructs from `.tasks/` + `.worktrees/index.json` on disk. Conversation memory is volatile; file state is durable.\\n\\n## What Changed From s11\\n\\n| Component          | Before (s11)               | After (s12)                                  |\\n|--------------------|----------------------------|----------------------------------------------|\\n| Coordination       | Task board (owner/status)  | Task board + explicit worktree binding       |\\n| Execution scope    | Shared directory           | Task-scoped isolated directory               |\\n| Recoverability     | Task status only           | Task status + worktree index                 |\\n| Teardown           | Task completion            | Task completion + explicit keep/remove       |\\n| Lifecycle visibility | Implicit in logs         | Explicit events in `.worktrees/events.jsonl` |\\n\\n## Try It\\n\\n```sh\\ncd learn-claude-code\\npython agents/s12_worktree_task_isolation.py\\n```\\n\\n1. `Create tasks for backend auth and frontend login page, then list tasks.`\\n2. `Create worktree \\\"auth-refactor\\\" for task 1, then bind task 2 to a new worktree \\\"ui-login\\\".`\\n3. `Run \\\"git status --short\\\" in worktree \\\"auth-refactor\\\".`\\n4. `Keep worktree \\\"ui-login\\\", then list worktrees and inspect events.`\\n5. `Remove worktree \\\"auth-refactor\\\" with complete_task=true, then list tasks/worktrees/events.`\\n\"\n  },\n  {\n    \"version\": \"s01\",\n    \"locale\": \"zh\",\n    \"title\": \"s01: The Agent Loop (智能体循环)\",\n    \"content\": \"# s01: The Agent Loop (智能体循环)\\n\\n`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"One loop & Bash is all you need\\\"* -- 一个工具 + 一个循环 = 一个智能体。\\n\\n## 问题\\n\\n语言模型能推理代码, 但碰不到真实世界 -- 不能读文件、跑测试、看报错。没有循环, 每次工具调用你都得手动把结果粘回去。你自己就是那个循环。\\n\\n## 解决方案\\n\\n```\\n+--------+      +-------+      +---------+\\n|  User  | ---> |  LLM  | ---> |  Tool   |\\n| prompt |      |       |      | execute |\\n+--------+      +---+---+      +----+----+\\n                    ^                |\\n                    |   tool_result  |\\n                    +----------------+\\n                    (loop until stop_reason != \\\"tool_use\\\")\\n```\\n\\n一个退出条件控制整个流程。循环持续运行, 直到模型不再调用工具。\\n\\n## 工作原理\\n\\n1. 用户 prompt 作为第一条消息。\\n\\n```python\\nmessages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n```\\n\\n2. 将消息和工具定义一起发给 LLM。\\n\\n```python\\nresponse = client.messages.create(\\n    model=MODEL, system=SYSTEM, messages=messages,\\n    tools=TOOLS, max_tokens=8000,\\n)\\n```\\n\\n3. 追加助手响应。检查 `stop_reason` -- 如果模型没有调用工具, 结束。\\n\\n```python\\nmessages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\nif response.stop_reason != \\\"tool_use\\\":\\n    return\\n```\\n\\n4. 执行每个工具调用, 收集结果, 作为 user 消息追加。回到第 2 步。\\n\\n```python\\nresults = []\\nfor block in response.content:\\n    if block.type == \\\"tool_use\\\":\\n        output = run_bash(block.input[\\\"command\\\"])\\n        results.append({\\n            \\\"type\\\": \\\"tool_result\\\",\\n            \\\"tool_use_id\\\": block.id,\\n            \\\"content\\\": output,\\n        })\\nmessages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n```\\n\\n组装为一个完整函数:\\n\\n```python\\ndef agent_loop(query):\\n    messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": query}]\\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                output = run_bash(block.input[\\\"command\\\"])\\n                results.append({\\n                    \\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": output,\\n                })\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n```\\n\\n不到 30 行, 这就是整个智能体。后面 11 个章节都在这个循环上叠加机制 -- 循环本身始终不变。\\n\\n## 变更内容\\n\\n| 组件          | 之前       | 之后                           |\\n|---------------|------------|--------------------------------|\\n| Agent loop    | (无)       | `while True` + stop_reason     |\\n| Tools         | (无)       | `bash` (单一工具)              |\\n| Messages      | (无)       | 累积式消息列表                 |\\n| Control flow  | (无)       | `stop_reason != \\\"tool_use\\\"`    |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s01_agent_loop.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Create a file called hello.py that prints \\\"Hello, World!\\\"`\\n2. `List all Python files in this directory`\\n3. `What is the current git branch?`\\n4. `Create a directory called test_output and write 3 files in it`\\n\"\n  },\n  {\n    \"version\": \"s02\",\n    \"locale\": \"zh\",\n    \"title\": \"s02: Tool Use (工具使用)\",\n    \"content\": \"# s02: Tool Use (工具使用)\\n\\n`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"加一个工具, 只加一个 handler\\\"* -- 循环不用动, 新工具注册进 dispatch map 就行。\\n\\n## 问题\\n\\n只有 `bash` 时, 所有操作都走 shell。`cat` 截断不可预测, `sed` 遇到特殊字符就崩, 每次 bash 调用都是不受约束的安全面。专用工具 (`read_file`, `write_file`) 可以在工具层面做路径沙箱。\\n\\n关键洞察: 加工具不需要改循环。\\n\\n## 解决方案\\n\\n```\\n+--------+      +-------+      +------------------+\\n|  User  | ---> |  LLM  | ---> | Tool Dispatch    |\\n| prompt |      |       |      | {                |\\n+--------+      +---+---+      |   bash: run_bash |\\n                    ^           |   read: run_read |\\n                    |           |   write: run_wr  |\\n                    +-----------+   edit: run_edit |\\n                    tool_result | }                |\\n                                +------------------+\\n\\nThe dispatch map is a dict: {tool_name: handler_function}.\\nOne lookup replaces any if/elif chain.\\n```\\n\\n## 工作原理\\n\\n1. 每个工具有一个处理函数。路径沙箱防止逃逸工作区。\\n\\n```python\\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_read(path: str, limit: int = None) -> str:\\n    text = safe_path(path).read_text()\\n    lines = text.splitlines()\\n    if limit and limit < len(lines):\\n        lines = lines[:limit]\\n    return \\\"\\\\n\\\".join(lines)[:50000]\\n```\\n\\n2. dispatch map 将工具名映射到处理函数。\\n\\n```python\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"],\\n                                        kw[\\\"new_text\\\"]),\\n}\\n```\\n\\n3. 循环中按名称查找处理函数。循环体本身与 s01 完全一致。\\n\\n```python\\nfor block in response.content:\\n    if block.type == \\\"tool_use\\\":\\n        handler = TOOL_HANDLERS.get(block.name)\\n        output = handler(**block.input) if handler \\\\\\n            else f\\\"Unknown tool: {block.name}\\\"\\n        results.append({\\n            \\\"type\\\": \\\"tool_result\\\",\\n            \\\"tool_use_id\\\": block.id,\\n            \\\"content\\\": output,\\n        })\\n```\\n\\n加工具 = 加 handler + 加 schema。循环永远不变。\\n\\n## 相对 s01 的变更\\n\\n| 组件           | 之前 (s01)         | 之后 (s02)                     |\\n|----------------|--------------------|--------------------------------|\\n| Tools          | 1 (仅 bash)        | 4 (bash, read, write, edit)    |\\n| Dispatch       | 硬编码 bash 调用   | `TOOL_HANDLERS` 字典           |\\n| 路径安全       | 无                 | `safe_path()` 沙箱             |\\n| Agent loop     | 不变               | 不变                           |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s02_tool_use.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Read the file requirements.txt`\\n2. `Create a file called greet.py with a greet(name) function`\\n3. `Edit greet.py to add a docstring to the function`\\n4. `Read greet.py to verify the edit worked`\\n\"\n  },\n  {\n    \"version\": \"s03\",\n    \"locale\": \"zh\",\n    \"title\": \"s03: TodoWrite (待办写入)\",\n    \"content\": \"# s03: TodoWrite (待办写入)\\n\\n`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"没有计划的 agent 走哪算哪\\\"* -- 先列步骤再动手, 完成率翻倍。\\n\\n## 问题\\n\\n多步任务中, 模型会丢失进度 -- 重复做过的事、跳步、跑偏。对话越长越严重: 工具结果不断填满上下文, 系统提示的影响力逐渐被稀释。一个 10 步重构可能做完 1-3 步就开始即兴发挥, 因为 4-10 步已经被挤出注意力了。\\n\\n## 解决方案\\n\\n```\\n+--------+      +-------+      +---------+\\n|  User  | ---> |  LLM  | ---> | Tools   |\\n| prompt |      |       |      | + todo  |\\n+--------+      +---+---+      +----+----+\\n                    ^                |\\n                    |   tool_result  |\\n                    +----------------+\\n                          |\\n              +-----------+-----------+\\n              | TodoManager state     |\\n              | [ ] task A            |\\n              | [>] task B  <- doing  |\\n              | [x] task C            |\\n              +-----------------------+\\n                          |\\n              if rounds_since_todo >= 3:\\n                inject <reminder> into tool_result\\n```\\n\\n## 工作原理\\n\\n1. TodoManager 存储带状态的项目。同一时间只允许一个 `in_progress`。\\n\\n```python\\nclass TodoManager:\\n    def update(self, items: list) -> str:\\n        validated, in_progress_count = [], 0\\n        for item in items:\\n            status = item.get(\\\"status\\\", \\\"pending\\\")\\n            if status == \\\"in_progress\\\":\\n                in_progress_count += 1\\n            validated.append({\\\"id\\\": item[\\\"id\\\"], \\\"text\\\": item[\\\"text\\\"],\\n                              \\\"status\\\": status})\\n        if in_progress_count > 1:\\n            raise ValueError(\\\"Only one task can be in_progress\\\")\\n        self.items = validated\\n        return self.render()\\n```\\n\\n2. `todo` 工具和其他工具一样加入 dispatch map。\\n\\n```python\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"todo\\\": lambda **kw: TODO.update(kw[\\\"items\\\"]),\\n}\\n```\\n\\n3. nag reminder: 模型连续 3 轮以上不调用 `todo` 时注入提醒。\\n\\n```python\\nif rounds_since_todo >= 3 and messages:\\n    last = messages[-1]\\n    if last[\\\"role\\\"] == \\\"user\\\" and isinstance(last.get(\\\"content\\\"), list):\\n        last[\\\"content\\\"].insert(0, {\\n            \\\"type\\\": \\\"text\\\",\\n            \\\"text\\\": \\\"<reminder>Update your todos.</reminder>\\\",\\n        })\\n```\\n\\n\\\"同时只能有一个 in_progress\\\" 强制顺序聚焦。nag reminder 制造问责压力 -- 你不更新计划, 系统就追着你问。\\n\\n## 相对 s02 的变更\\n\\n| 组件           | 之前 (s02)       | 之后 (s03)                     |\\n|----------------|------------------|--------------------------------|\\n| Tools          | 4                | 5 (+todo)                      |\\n| 规划           | 无               | 带状态的 TodoManager           |\\n| Nag 注入       | 无               | 3 轮后注入 `<reminder>`        |\\n| Agent loop     | 简单分发         | + rounds_since_todo 计数器     |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s03_todo_write.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Refactor the file hello.py: add type hints, docstrings, and a main guard`\\n2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py`\\n3. `Review all Python files and fix any style issues`\\n\"\n  },\n  {\n    \"version\": \"s04\",\n    \"locale\": \"zh\",\n    \"title\": \"s04: Subagents (子智能体)\",\n    \"content\": \"# s04: Subagents (子智能体)\\n\\n`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"大任务拆小, 每个小任务干净的上下文\\\"* -- 子智能体用独立 messages[], 不污染主对话。\\n\\n## 问题\\n\\n智能体工作越久, messages 数组越胖。每次读文件、跑命令的输出都永久留在上下文里。\\\"这个项目用什么测试框架?\\\" 可能要读 5 个文件, 但父智能体只需要一个词: \\\"pytest。\\\"\\n\\n## 解决方案\\n\\n```\\nParent agent                     Subagent\\n+------------------+             +------------------+\\n| messages=[...]   |             | messages=[]      | <-- fresh\\n|                  |  dispatch   |                  |\\n| tool: task       | ----------> | while tool_use:  |\\n|   prompt=\\\"...\\\"   |             |   call tools     |\\n|                  |  summary    |   append results |\\n|   result = \\\"...\\\" | <---------- | return last text |\\n+------------------+             +------------------+\\n\\nParent context stays clean. Subagent context is discarded.\\n```\\n\\n## 工作原理\\n\\n1. 父智能体有一个 `task` 工具。子智能体拥有除 `task` 外的所有基础工具 (禁止递归生成)。\\n\\n```python\\nPARENT_TOOLS = CHILD_TOOLS + [\\n    {\\\"name\\\": \\\"task\\\",\\n     \\\"description\\\": \\\"Spawn a subagent with fresh context.\\\",\\n     \\\"input_schema\\\": {\\n         \\\"type\\\": \\\"object\\\",\\n         \\\"properties\\\": {\\\"prompt\\\": {\\\"type\\\": \\\"string\\\"}},\\n         \\\"required\\\": [\\\"prompt\\\"],\\n     }},\\n]\\n```\\n\\n2. 子智能体以 `messages=[]` 启动, 运行自己的循环。只有最终文本返回给父智能体。\\n\\n```python\\ndef run_subagent(prompt: str) -> str:\\n    sub_messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n    for _ in range(30):  # safety limit\\n        response = client.messages.create(\\n            model=MODEL, system=SUBAGENT_SYSTEM,\\n            messages=sub_messages,\\n            tools=CHILD_TOOLS, max_tokens=8000,\\n        )\\n        sub_messages.append({\\\"role\\\": \\\"assistant\\\",\\n                             \\\"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 = TOOL_HANDLERS.get(block.name)\\n                output = handler(**block.input)\\n                results.append({\\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": str(output)[:50000]})\\n        sub_messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n    return \\\"\\\".join(\\n        b.text for b in response.content if hasattr(b, \\\"text\\\")\\n    ) or \\\"(no summary)\\\"\\n```\\n\\n子智能体可能跑了 30+ 次工具调用, 但整个消息历史直接丢弃。父智能体收到的只是一段摘要文本, 作为普通 `tool_result` 返回。\\n\\n## 相对 s03 的变更\\n\\n| 组件           | 之前 (s03)       | 之后 (s04)                    |\\n|----------------|------------------|-------------------------------|\\n| Tools          | 5                | 5 (基础) + task (仅父端)      |\\n| 上下文         | 单一共享         | 父 + 子隔离                   |\\n| Subagent       | 无               | `run_subagent()` 函数         |\\n| 返回值         | 不适用           | 仅摘要文本                    |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s04_subagent.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Use a subtask to find what testing framework this project uses`\\n2. `Delegate: read all .py files and summarize what each one does`\\n3. `Use a task to create a new module, then verify it from here`\\n\"\n  },\n  {\n    \"version\": \"s05\",\n    \"locale\": \"zh\",\n    \"title\": \"s05: Skills (技能加载)\",\n    \"content\": \"# s05: Skills (技能加载)\\n\\n`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"用到什么知识, 临时加载什么知识\\\"* -- 通过 tool_result 注入, 不塞 system prompt。\\n\\n## 问题\\n\\n你希望智能体遵循特定领域的工作流: git 约定、测试模式、代码审查清单。全塞进系统提示太浪费 -- 10 个技能, 每个 2000 token, 就是 20,000 token, 大部分跟当前任务毫无关系。\\n\\n## 解决方案\\n\\n```\\nSystem prompt (Layer 1 -- always present):\\n+--------------------------------------+\\n| You are a coding agent.              |\\n| Skills available:                    |\\n|   - git: Git workflow helpers        |  ~100 tokens/skill\\n|   - test: Testing best practices     |\\n+--------------------------------------+\\n\\nWhen model calls load_skill(\\\"git\\\"):\\n+--------------------------------------+\\n| tool_result (Layer 2 -- on demand):  |\\n| <skill name=\\\"git\\\">                   |\\n|   Full git workflow instructions...  |  ~2000 tokens\\n|   Step 1: ...                        |\\n| </skill>                             |\\n+--------------------------------------+\\n```\\n\\n第一层: 系统提示中放技能名称 (低成本)。第二层: tool_result 中按需放完整内容。\\n\\n## 工作原理\\n\\n1. 每个技能是一个目录, 包含 `SKILL.md` 文件和 YAML frontmatter。\\n\\n```\\nskills/\\n  pdf/\\n    SKILL.md       # ---\\\\n name: pdf\\\\n description: Process PDF files\\\\n ---\\\\n ...\\n  code-review/\\n    SKILL.md       # ---\\\\n name: code-review\\\\n description: Review code\\\\n ---\\\\n ...\\n```\\n\\n2. SkillLoader 递归扫描 `SKILL.md` 文件, 用目录名作为技能标识。\\n\\n```python\\nclass SkillLoader:\\n    def __init__(self, skills_dir: Path):\\n        self.skills = {}\\n        for f in sorted(skills_dir.rglob(\\\"SKILL.md\\\")):\\n            text = f.read_text()\\n            meta, body = self._parse_frontmatter(text)\\n            name = meta.get(\\\"name\\\", f.parent.name)\\n            self.skills[name] = {\\\"meta\\\": meta, \\\"body\\\": body}\\n\\n    def get_descriptions(self) -> str:\\n        lines = []\\n        for name, skill in self.skills.items():\\n            desc = skill[\\\"meta\\\"].get(\\\"description\\\", \\\"\\\")\\n            lines.append(f\\\"  - {name}: {desc}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def get_content(self, name: str) -> str:\\n        skill = self.skills.get(name)\\n        if not skill:\\n            return f\\\"Error: Unknown skill '{name}'.\\\"\\n        return f\\\"<skill name=\\\\\\\"{name}\\\\\\\">\\\\n{skill['body']}\\\\n</skill>\\\"\\n```\\n\\n3. 第一层写入系统提示。第二层不过是 dispatch map 中的又一个工具。\\n\\n```python\\nSYSTEM = f\\\"\\\"\\\"You are a coding agent at {WORKDIR}.\\nSkills available:\\n{SKILL_LOADER.get_descriptions()}\\\"\\\"\\\"\\n\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"load_skill\\\": lambda **kw: SKILL_LOADER.get_content(kw[\\\"name\\\"]),\\n}\\n```\\n\\n模型知道有哪些技能 (便宜), 需要时再加载完整内容 (贵)。\\n\\n## 相对 s04 的变更\\n\\n| 组件           | 之前 (s04)       | 之后 (s05)                     |\\n|----------------|------------------|--------------------------------|\\n| Tools          | 5 (基础 + task)  | 5 (基础 + load_skill)          |\\n| 系统提示       | 静态字符串       | + 技能描述列表                 |\\n| 知识库         | 无               | skills/\\\\*/SKILL.md 文件        |\\n| 注入方式       | 无               | 两层 (系统提示 + result)       |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s05_skill_loading.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `What skills are available?`\\n2. `Load the agent-builder skill and follow its instructions`\\n3. `I need to do a code review -- load the relevant skill first`\\n4. `Build an MCP server using the mcp-builder skill`\\n\"\n  },\n  {\n    \"version\": \"s06\",\n    \"locale\": \"zh\",\n    \"title\": \"s06: Context Compact (上下文压缩)\",\n    \"content\": \"# s06: Context Compact (上下文压缩)\\n\\n`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"上下文总会满, 要有办法腾地方\\\"* -- 三层压缩策略, 换来无限会话。\\n\\n## 问题\\n\\n上下文窗口是有限的。读一个 1000 行的文件就吃掉 ~4000 token; 读 30 个文件、跑 20 条命令, 轻松突破 100k token。不压缩, 智能体根本没法在大项目里干活。\\n\\n## 解决方案\\n\\n三层压缩, 激进程度递增:\\n\\n```\\nEvery turn:\\n+------------------+\\n| Tool call result |\\n+------------------+\\n        |\\n        v\\n[Layer 1: micro_compact]        (silent, every turn)\\n  Replace tool_result > 3 turns old\\n  with \\\"[Previous: used {tool_name}]\\\"\\n        |\\n        v\\n[Check: tokens > 50000?]\\n   |               |\\n   no              yes\\n   |               |\\n   v               v\\ncontinue    [Layer 2: auto_compact]\\n              Save transcript to .transcripts/\\n              LLM summarizes conversation.\\n              Replace all messages with [summary].\\n                    |\\n                    v\\n            [Layer 3: compact tool]\\n              Model calls compact explicitly.\\n              Same summarization as auto_compact.\\n```\\n\\n## 工作原理\\n\\n1. **第一层 -- micro_compact**: 每次 LLM 调用前, 将旧的 tool result 替换为占位符。\\n\\n```python\\ndef micro_compact(messages: list) -> list:\\n    tool_results = []\\n    for i, msg in enumerate(messages):\\n        if msg[\\\"role\\\"] == \\\"user\\\" and isinstance(msg.get(\\\"content\\\"), list):\\n            for j, part in enumerate(msg[\\\"content\\\"]):\\n                if isinstance(part, dict) and part.get(\\\"type\\\") == \\\"tool_result\\\":\\n                    tool_results.append((i, j, part))\\n    if len(tool_results) <= KEEP_RECENT:\\n        return messages\\n    for _, _, part in tool_results[:-KEEP_RECENT]:\\n        if len(part.get(\\\"content\\\", \\\"\\\")) > 100:\\n            part[\\\"content\\\"] = f\\\"[Previous: used {tool_name}]\\\"\\n    return messages\\n```\\n\\n2. **第二层 -- auto_compact**: token 超过阈值时, 保存完整对话到磁盘, 让 LLM 做摘要。\\n\\n```python\\ndef auto_compact(messages: list) -> list:\\n    # Save transcript for recovery\\n    transcript_path = TRANSCRIPT_DIR / f\\\"transcript_{int(time.time())}.jsonl\\\"\\n    with open(transcript_path, \\\"w\\\") as f:\\n        for msg in messages:\\n            f.write(json.dumps(msg, default=str) + \\\"\\\\n\\\")\\n    # LLM summarizes\\n    response = client.messages.create(\\n        model=MODEL,\\n        messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\":\\n            \\\"Summarize this conversation for continuity...\\\"\\n            + json.dumps(messages, default=str)[:80000]}],\\n        max_tokens=2000,\\n    )\\n    return [\\n        {\\\"role\\\": \\\"user\\\", \\\"content\\\": f\\\"[Compressed]\\\\n\\\\n{response.content[0].text}\\\"},\\n        {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"Understood. Continuing.\\\"},\\n    ]\\n```\\n\\n3. **第三层 -- manual compact**: `compact` 工具按需触发同样的摘要机制。\\n\\n4. 循环整合三层:\\n\\n```python\\ndef agent_loop(messages: list):\\n    while True:\\n        micro_compact(messages)                        # Layer 1\\n        if estimate_tokens(messages) > THRESHOLD:\\n            messages[:] = auto_compact(messages)       # Layer 2\\n        response = client.messages.create(...)\\n        # ... tool execution ...\\n        if manual_compact:\\n            messages[:] = auto_compact(messages)       # Layer 3\\n```\\n\\n完整历史通过 transcript 保存在磁盘上。信息没有真正丢失, 只是移出了活跃上下文。\\n\\n## 相对 s05 的变更\\n\\n| 组件           | 之前 (s05)       | 之后 (s06)                     |\\n|----------------|------------------|--------------------------------|\\n| Tools          | 5                | 5 (基础 + compact)             |\\n| 上下文管理     | 无               | 三层压缩                       |\\n| Micro-compact  | 无               | 旧结果 -> 占位符               |\\n| Auto-compact   | 无               | token 阈值触发                 |\\n| Transcripts    | 无               | 保存到 .transcripts/           |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s06_context_compact.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Read every Python file in the agents/ directory one by one` (观察 micro-compact 替换旧结果)\\n2. `Keep reading files until compression triggers automatically`\\n3. `Use the compact tool to manually compress the conversation`\\n\"\n  },\n  {\n    \"version\": \"s07\",\n    \"locale\": \"zh\",\n    \"title\": \"s07: Task System (任务系统)\",\n    \"content\": \"# s07: Task System (任务系统)\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"大目标要拆成小任务, 排好序, 记在磁盘上\\\"* -- 文件持久化的任务图, 为多 agent 协作打基础。\\n\\n## 问题\\n\\ns03 的 TodoManager 只是内存中的扁平清单: 没有顺序、没有依赖、状态只有做完没做完。真实目标是有结构的 -- 任务 B 依赖任务 A, 任务 C 和 D 可以并行, 任务 E 要等 C 和 D 都完成。\\n\\n没有显式的关系, 智能体分不清什么能做、什么被卡住、什么能同时跑。而且清单只活在内存里, 上下文压缩 (s06) 一跑就没了。\\n\\n## 解决方案\\n\\n把扁平清单升级为持久化到磁盘的**任务图**。每个任务是一个 JSON 文件, 有状态、前置依赖 (`blockedBy`) 和后置依赖 (`blocks`)。任务图随时回答三个问题:\\n\\n- **什么可以做?** -- 状态为 `pending` 且 `blockedBy` 为空的任务。\\n- **什么被卡住?** -- 等待前置任务完成的任务。\\n- **什么做完了?** -- 状态为 `completed` 的任务, 完成时自动解锁后续任务。\\n\\n```\\n.tasks/\\n  task_1.json  {\\\"id\\\":1, \\\"status\\\":\\\"completed\\\"}\\n  task_2.json  {\\\"id\\\":2, \\\"blockedBy\\\":[1], \\\"status\\\":\\\"pending\\\"}\\n  task_3.json  {\\\"id\\\":3, \\\"blockedBy\\\":[1], \\\"status\\\":\\\"pending\\\"}\\n  task_4.json  {\\\"id\\\":4, \\\"blockedBy\\\":[2,3], \\\"status\\\":\\\"pending\\\"}\\n\\n任务图 (DAG):\\n                 +----------+\\n            +--> | task 2   | --+\\n            |    | pending  |   |\\n+----------+     +----------+    +--> +----------+\\n| task 1   |                          | task 4   |\\n| completed| --> +----------+    +--> | blocked  |\\n+----------+     | task 3   | --+     +----------+\\n                 | pending  |\\n                 +----------+\\n\\n顺序:   task 1 必须先完成, 才能开始 2 和 3\\n并行:   task 2 和 3 可以同时执行\\n依赖:   task 4 要等 2 和 3 都完成\\n状态:   pending -> in_progress -> completed\\n```\\n\\n这个任务图是 s07 之后所有机制的协调骨架: 后台执行 (s08)、多 agent 团队 (s09+)、worktree 隔离 (s12) 都读写这同一个结构。\\n\\n## 工作原理\\n\\n1. **TaskManager**: 每个任务一个 JSON 文件, CRUD + 依赖图。\\n\\n```python\\nclass TaskManager:\\n    def __init__(self, tasks_dir: Path):\\n        self.dir = tasks_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self._next_id = self._max_id() + 1\\n\\n    def create(self, subject, description=\\\"\\\"):\\n        task = {\\\"id\\\": self._next_id, \\\"subject\\\": subject,\\n                \\\"status\\\": \\\"pending\\\", \\\"blockedBy\\\": [],\\n                \\\"blocks\\\": [], \\\"owner\\\": \\\"\\\"}\\n        self._save(task)\\n        self._next_id += 1\\n        return json.dumps(task, indent=2)\\n```\\n\\n2. **依赖解除**: 完成任务时, 自动将其 ID 从其他任务的 `blockedBy` 中移除, 解锁后续任务。\\n\\n```python\\ndef _clear_dependency(self, completed_id):\\n    for f in self.dir.glob(\\\"task_*.json\\\"):\\n        task = json.loads(f.read_text())\\n        if completed_id in task.get(\\\"blockedBy\\\", []):\\n            task[\\\"blockedBy\\\"].remove(completed_id)\\n            self._save(task)\\n```\\n\\n3. **状态变更 + 依赖关联**: `update` 处理状态转换和依赖边。\\n\\n```python\\ndef update(self, task_id, status=None,\\n           add_blocked_by=None, add_blocks=None):\\n    task = self._load(task_id)\\n    if status:\\n        task[\\\"status\\\"] = status\\n        if status == \\\"completed\\\":\\n            self._clear_dependency(task_id)\\n    self._save(task)\\n```\\n\\n4. 四个任务工具加入 dispatch map。\\n\\n```python\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"task_create\\\": lambda **kw: TASKS.create(kw[\\\"subject\\\"]),\\n    \\\"task_update\\\": lambda **kw: TASKS.update(kw[\\\"task_id\\\"], kw.get(\\\"status\\\")),\\n    \\\"task_list\\\":   lambda **kw: TASKS.list_all(),\\n    \\\"task_get\\\":    lambda **kw: TASKS.get(kw[\\\"task_id\\\"]),\\n}\\n```\\n\\n从 s07 起, 任务图是多步工作的默认选择。s03 的 Todo 仍可用于单次会话内的快速清单。\\n\\n## 相对 s06 的变更\\n\\n| 组件 | 之前 (s06) | 之后 (s07) |\\n|---|---|---|\\n| Tools | 5 | 8 (`task_create/update/list/get`) |\\n| 规划模型 | 扁平清单 (仅内存) | 带依赖关系的任务图 (磁盘) |\\n| 关系 | 无 | `blockedBy` + `blocks` 边 |\\n| 状态追踪 | 做完没做完 | `pending` -> `in_progress` -> `completed` |\\n| 持久化 | 压缩后丢失 | 压缩和重启后存活 |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s07_task_system.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Create 3 tasks: \\\"Setup project\\\", \\\"Write code\\\", \\\"Write tests\\\". Make them depend on each other in order.`\\n2. `List all tasks and show the dependency graph`\\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\\n\"\n  },\n  {\n    \"version\": \"s08\",\n    \"locale\": \"zh\",\n    \"title\": \"s08: Background Tasks (后台任务)\",\n    \"content\": \"# s08: Background Tasks (后台任务)\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12`\\n\\n> *\\\"慢操作丢后台, agent 继续想下一步\\\"* -- 后台线程跑命令, 完成后注入通知。\\n\\n## 问题\\n\\n有些命令要跑好几分钟: `npm install`、`pytest`、`docker build`。阻塞式循环下模型只能干等。用户说 \\\"装依赖, 顺便建个配置文件\\\", 智能体却只能一个一个来。\\n\\n## 解决方案\\n\\n```\\nMain thread                Background thread\\n+-----------------+        +-----------------+\\n| agent loop      |        | subprocess runs |\\n| ...             |        | ...             |\\n| [LLM call] <---+------- | enqueue(result) |\\n|  ^drain queue   |        +-----------------+\\n+-----------------+\\n\\nTimeline:\\nAgent --[spawn A]--[spawn B]--[other work]----\\n             |          |\\n             v          v\\n          [A runs]   [B runs]      (parallel)\\n             |          |\\n             +-- results injected before next LLM call --+\\n```\\n\\n## 工作原理\\n\\n1. BackgroundManager 用线程安全的通知队列追踪任务。\\n\\n```python\\nclass BackgroundManager:\\n    def __init__(self):\\n        self.tasks = {}\\n        self._notification_queue = []\\n        self._lock = threading.Lock()\\n```\\n\\n2. `run()` 启动守护线程, 立即返回。\\n\\n```python\\ndef run(self, command: str) -> str:\\n    task_id = str(uuid.uuid4())[:8]\\n    self.tasks[task_id] = {\\\"status\\\": \\\"running\\\", \\\"command\\\": command}\\n    thread = threading.Thread(\\n        target=self._execute, args=(task_id, command), daemon=True)\\n    thread.start()\\n    return f\\\"Background task {task_id} started\\\"\\n```\\n\\n3. 子进程完成后, 结果进入通知队列。\\n\\n```python\\ndef _execute(self, task_id, command):\\n    try:\\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\\n            capture_output=True, text=True, timeout=300)\\n        output = (r.stdout + r.stderr).strip()[:50000]\\n    except subprocess.TimeoutExpired:\\n        output = \\\"Error: Timeout (300s)\\\"\\n    with self._lock:\\n        self._notification_queue.append({\\n            \\\"task_id\\\": task_id, \\\"result\\\": output[:500]})\\n```\\n\\n4. 每次 LLM 调用前排空通知队列。\\n\\n```python\\ndef agent_loop(messages: list):\\n    while True:\\n        notifs = BG.drain_notifications()\\n        if notifs:\\n            notif_text = \\\"\\\\n\\\".join(\\n                f\\\"[bg:{n['task_id']}] {n['result']}\\\" for n in notifs)\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<background-results>\\\\n{notif_text}\\\\n\\\"\\n                           f\\\"</background-results>\\\"})\\n            messages.append({\\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted background results.\\\"})\\n        response = client.messages.create(...)\\n```\\n\\n循环保持单线程。只有子进程 I/O 被并行化。\\n\\n## 相对 s07 的变更\\n\\n| 组件           | 之前 (s07)       | 之后 (s08)                         |\\n|----------------|------------------|------------------------------------|\\n| Tools          | 8                | 6 (基础 + background_run + check)  |\\n| 执行方式       | 仅阻塞           | 阻塞 + 后台线程                    |\\n| 通知机制       | 无               | 每轮排空的队列                     |\\n| 并发           | 无               | 守护线程                           |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s08_background_tasks.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Run \\\"sleep 5 && echo done\\\" in the background, then create a file while it runs`\\n2. `Start 3 background tasks: \\\"sleep 2\\\", \\\"sleep 4\\\", \\\"sleep 6\\\". Check their status.`\\n3. `Run pytest in the background and keep working on other things`\\n\"\n  },\n  {\n    \"version\": \"s09\",\n    \"locale\": \"zh\",\n    \"title\": \"s09: Agent Teams (智能体团队)\",\n    \"content\": \"# s09: Agent Teams (智能体团队)\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12`\\n\\n> *\\\"任务太大一个人干不完, 要能分给队友\\\"* -- 持久化队友 + JSONL 邮箱。\\n\\n## 问题\\n\\n子智能体 (s04) 是一次性的: 生成、干活、返回摘要、消亡。没有身份, 没有跨调用的记忆。后台任务 (s08) 能跑 shell 命令, 但做不了 LLM 引导的决策。\\n\\n真正的团队协作需要三样东西: (1) 能跨多轮对话存活的持久智能体, (2) 身份和生命周期管理, (3) 智能体之间的通信通道。\\n\\n## 解决方案\\n\\n```\\nTeammate lifecycle:\\n  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\\n\\nCommunication:\\n  .team/\\n    config.json           <- team roster + statuses\\n    inbox/\\n      alice.jsonl         <- append-only, drain-on-read\\n      bob.jsonl\\n      lead.jsonl\\n\\n              +--------+    send(\\\"alice\\\",\\\"bob\\\",\\\"...\\\")    +--------+\\n              | alice  | -----------------------------> |  bob   |\\n              | loop   |    bob.jsonl << {json_line}    |  loop  |\\n              +--------+                                +--------+\\n                   ^                                         |\\n                   |        BUS.read_inbox(\\\"alice\\\")          |\\n                   +---- alice.jsonl -> read + drain ---------+\\n```\\n\\n## 工作原理\\n\\n1. TeammateManager 通过 config.json 维护团队名册。\\n\\n```python\\nclass TeammateManager:\\n    def __init__(self, team_dir: Path):\\n        self.dir = team_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self.config_path = self.dir / \\\"config.json\\\"\\n        self.config = self._load_config()\\n        self.threads = {}\\n```\\n\\n2. `spawn()` 创建队友并在线程中启动 agent loop。\\n\\n```python\\ndef spawn(self, name: str, role: str, prompt: str) -> str:\\n    member = {\\\"name\\\": name, \\\"role\\\": role, \\\"status\\\": \\\"working\\\"}\\n    self.config[\\\"members\\\"].append(member)\\n    self._save_config()\\n    thread = threading.Thread(\\n        target=self._teammate_loop,\\n        args=(name, role, prompt), daemon=True)\\n    thread.start()\\n    return f\\\"Spawned teammate '{name}' (role: {role})\\\"\\n```\\n\\n3. MessageBus: append-only 的 JSONL 收件箱。`send()` 追加一行; `read_inbox()` 读取全部并清空。\\n\\n```python\\nclass MessageBus:\\n    def send(self, sender, to, content, msg_type=\\\"message\\\", extra=None):\\n        msg = {\\\"type\\\": msg_type, \\\"from\\\": sender,\\n               \\\"content\\\": content, \\\"timestamp\\\": time.time()}\\n        if extra:\\n            msg.update(extra)\\n        with open(self.dir / f\\\"{to}.jsonl\\\", \\\"a\\\") as f:\\n            f.write(json.dumps(msg) + \\\"\\\\n\\\")\\n\\n    def read_inbox(self, name):\\n        path = self.dir / f\\\"{name}.jsonl\\\"\\n        if not path.exists(): return \\\"[]\\\"\\n        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]\\n        path.write_text(\\\"\\\")  # drain\\n        return json.dumps(msgs, indent=2)\\n```\\n\\n4. 每个队友在每次 LLM 调用前检查收件箱, 将消息注入上下文。\\n\\n```python\\ndef _teammate_loop(self, name, role, prompt):\\n    messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n    for _ in range(50):\\n        inbox = BUS.read_inbox(name)\\n        if inbox != \\\"[]\\\":\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{inbox}</inbox>\\\"})\\n            messages.append({\\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted inbox messages.\\\"})\\n        response = client.messages.create(...)\\n        if response.stop_reason != \\\"tool_use\\\":\\n            break\\n        # execute tools, append results...\\n    self._find_member(name)[\\\"status\\\"] = \\\"idle\\\"\\n```\\n\\n## 相对 s08 的变更\\n\\n| 组件           | 之前 (s08)       | 之后 (s09)                         |\\n|----------------|------------------|------------------------------------|\\n| Tools          | 6                | 9 (+spawn/send/read_inbox)         |\\n| 智能体数量     | 单一             | 领导 + N 个队友                    |\\n| 持久化         | 无               | config.json + JSONL 收件箱         |\\n| 线程           | 后台命令         | 每线程完整 agent loop              |\\n| 生命周期       | 一次性           | idle -> working -> idle            |\\n| 通信           | 无               | message + broadcast                |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s09_agent_teams.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.`\\n2. `Broadcast \\\"status update: phase 1 complete\\\" to all teammates`\\n3. `Check the lead inbox for any messages`\\n4. 输入 `/team` 查看团队名册和状态\\n5. 输入 `/inbox` 手动检查领导的收件箱\\n\"\n  },\n  {\n    \"version\": \"s10\",\n    \"locale\": \"zh\",\n    \"title\": \"s10: Team Protocols (团队协议)\",\n    \"content\": \"# s10: Team Protocols (团队协议)\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12`\\n\\n> *\\\"队友之间要有统一的沟通规矩\\\"* -- 一个 request-response 模式驱动所有协商。\\n\\n## 问题\\n\\ns09 中队友能干活能通信, 但缺少结构化协调:\\n\\n**关机**: 直接杀线程会留下写了一半的文件和过期的 config.json。需要握手 -- 领导请求, 队友批准 (收尾退出) 或拒绝 (继续干)。\\n\\n**计划审批**: 领导说 \\\"重构认证模块\\\", 队友立刻开干。高风险变更应该先过审。\\n\\n两者结构一样: 一方发带唯一 ID 的请求, 另一方引用同一 ID 响应。\\n\\n## 解决方案\\n\\n```\\nShutdown Protocol            Plan Approval Protocol\\n==================           ======================\\n\\nLead             Teammate    Teammate           Lead\\n  |                 |           |                 |\\n  |--shutdown_req-->|           |--plan_req------>|\\n  | {req_id:\\\"abc\\\"}  |           | {req_id:\\\"xyz\\\"}  |\\n  |                 |           |                 |\\n  |<--shutdown_resp-|           |<--plan_resp-----|\\n  | {req_id:\\\"abc\\\",  |           | {req_id:\\\"xyz\\\",  |\\n  |  approve:true}  |           |  approve:true}  |\\n\\nShared FSM:\\n  [pending] --approve--> [approved]\\n  [pending] --reject---> [rejected]\\n\\nTrackers:\\n  shutdown_requests = {req_id: {target, status}}\\n  plan_requests     = {req_id: {from, plan, status}}\\n```\\n\\n## 工作原理\\n\\n1. 领导生成 request_id, 通过收件箱发起关机请求。\\n\\n```python\\nshutdown_requests = {}\\n\\ndef handle_shutdown_request(teammate: str) -> str:\\n    req_id = str(uuid.uuid4())[:8]\\n    shutdown_requests[req_id] = {\\\"target\\\": teammate, \\\"status\\\": \\\"pending\\\"}\\n    BUS.send(\\\"lead\\\", teammate, \\\"Please shut down gracefully.\\\",\\n             \\\"shutdown_request\\\", {\\\"request_id\\\": req_id})\\n    return f\\\"Shutdown request {req_id} sent (status: pending)\\\"\\n```\\n\\n2. 队友收到请求后, 用 approve/reject 响应。\\n\\n```python\\nif tool_name == \\\"shutdown_response\\\":\\n    req_id = args[\\\"request_id\\\"]\\n    approve = args[\\\"approve\\\"]\\n    shutdown_requests[req_id][\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(sender, \\\"lead\\\", args.get(\\\"reason\\\", \\\"\\\"),\\n             \\\"shutdown_response\\\",\\n             {\\\"request_id\\\": req_id, \\\"approve\\\": approve})\\n```\\n\\n3. 计划审批遵循完全相同的模式。队友提交计划 (生成 request_id), 领导审查 (引用同一个 request_id)。\\n\\n```python\\nplan_requests = {}\\n\\ndef handle_plan_review(request_id, approve, feedback=\\\"\\\"):\\n    req = plan_requests[request_id]\\n    req[\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(\\\"lead\\\", req[\\\"from\\\"], feedback,\\n             \\\"plan_approval_response\\\",\\n             {\\\"request_id\\\": request_id, \\\"approve\\\": approve})\\n```\\n\\n一个 FSM, 两种用途。同样的 `pending -> approved | rejected` 状态机可以套用到任何请求-响应协议上。\\n\\n## 相对 s09 的变更\\n\\n| 组件           | 之前 (s09)       | 之后 (s10)                           |\\n|----------------|------------------|--------------------------------------|\\n| Tools          | 9                | 12 (+shutdown_req/resp +plan)        |\\n| 关机           | 仅自然退出       | 请求-响应握手                        |\\n| 计划门控       | 无               | 提交/审查与审批                      |\\n| 关联           | 无               | 每个请求一个 request_id              |\\n| FSM            | 无               | pending -> approved/rejected         |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s10_team_protocols.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Spawn alice as a coder. Then request her shutdown.`\\n2. `List teammates to see alice's status after shutdown approval`\\n3. `Spawn bob with a risky refactoring task. Review and reject his plan.`\\n4. `Spawn charlie, have him submit a plan, then approve it.`\\n5. 输入 `/team` 监控状态\\n\"\n  },\n  {\n    \"version\": \"s11\",\n    \"locale\": \"zh\",\n    \"title\": \"s11: Autonomous Agents (自治智能体)\",\n    \"content\": \"# s11: Autonomous Agents (自治智能体)\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12`\\n\\n> *\\\"队友自己看看板, 有活就认领\\\"* -- 不需要领导逐个分配, 自组织。\\n\\n## 问题\\n\\ns09-s10 中, 队友只在被明确指派时才动。领导得给每个队友写 prompt, 任务看板上 10 个未认领的任务得手动分配。这扩展不了。\\n\\n真正的自治: 队友自己扫描任务看板, 认领没人做的任务, 做完再找下一个。\\n\\n一个细节: 上下文压缩 (s06) 后智能体可能忘了自己是谁。身份重注入解决这个问题。\\n\\n## 解决方案\\n\\n```\\nTeammate lifecycle with idle cycle:\\n\\n+-------+\\n| spawn |\\n+---+---+\\n    |\\n    v\\n+-------+   tool_use     +-------+\\n| WORK  | <------------- |  LLM  |\\n+---+---+                +-------+\\n    |\\n    | stop_reason != tool_use (or idle tool called)\\n    v\\n+--------+\\n|  IDLE  |  poll every 5s for up to 60s\\n+---+----+\\n    |\\n    +---> check inbox --> message? ----------> WORK\\n    |\\n    +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\\n    |\\n    +---> 60s timeout ----------------------> SHUTDOWN\\n\\nIdentity re-injection after compression:\\n  if len(messages) <= 3:\\n    messages.insert(0, identity_block)\\n```\\n\\n## 工作原理\\n\\n1. 队友循环分两个阶段: WORK 和 IDLE。LLM 停止调用工具 (或调用了 `idle`) 时, 进入 IDLE。\\n\\n```python\\ndef _loop(self, name, role, prompt):\\n    while True:\\n        # -- WORK PHASE --\\n        messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n        for _ in range(50):\\n            response = client.messages.create(...)\\n            if response.stop_reason != \\\"tool_use\\\":\\n                break\\n            # execute tools...\\n            if idle_requested:\\n                break\\n\\n        # -- IDLE PHASE --\\n        self._set_status(name, \\\"idle\\\")\\n        resume = self._idle_poll(name, messages)\\n        if not resume:\\n            self._set_status(name, \\\"shutdown\\\")\\n            return\\n        self._set_status(name, \\\"working\\\")\\n```\\n\\n2. 空闲阶段循环轮询收件箱和任务看板。\\n\\n```python\\ndef _idle_poll(self, name, messages):\\n    for _ in range(IDLE_TIMEOUT // POLL_INTERVAL):  # 60s / 5s = 12\\n        time.sleep(POLL_INTERVAL)\\n        inbox = BUS.read_inbox(name)\\n        if inbox:\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{inbox}</inbox>\\\"})\\n            return True\\n        unclaimed = scan_unclaimed_tasks()\\n        if unclaimed:\\n            claim_task(unclaimed[0][\\\"id\\\"], name)\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<auto-claimed>Task #{unclaimed[0]['id']}: \\\"\\n                           f\\\"{unclaimed[0]['subject']}</auto-claimed>\\\"})\\n            return True\\n    return False  # timeout -> shutdown\\n```\\n\\n3. 任务看板扫描: 找 pending 状态、无 owner、未被阻塞的任务。\\n\\n```python\\ndef scan_unclaimed_tasks() -> list:\\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 not task.get(\\\"blockedBy\\\")):\\n            unclaimed.append(task)\\n    return unclaimed\\n```\\n\\n4. 身份重注入: 上下文过短 (说明发生了压缩) 时, 在开头插入身份块。\\n\\n```python\\nif len(messages) <= 3:\\n    messages.insert(0, {\\\"role\\\": \\\"user\\\",\\n        \\\"content\\\": f\\\"<identity>You are '{name}', role: {role}, \\\"\\n                   f\\\"team: {team_name}. Continue your work.</identity>\\\"})\\n    messages.insert(1, {\\\"role\\\": \\\"assistant\\\",\\n        \\\"content\\\": f\\\"I am {name}. Continuing.\\\"})\\n```\\n\\n## 相对 s10 的变更\\n\\n| 组件           | 之前 (s10)       | 之后 (s11)                       |\\n|----------------|------------------|----------------------------------|\\n| Tools          | 12               | 14 (+idle, +claim_task)          |\\n| 自治性         | 领导指派         | 自组织                           |\\n| 空闲阶段       | 无               | 轮询收件箱 + 任务看板            |\\n| 任务认领       | 仅手动           | 自动认领未分配任务               |\\n| 身份           | 系统提示         | + 压缩后重注入                   |\\n| 超时           | 无               | 60 秒空闲 -> 自动关机            |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s11_autonomous_agents.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.`\\n2. `Spawn a coder teammate and let it find work from the task board itself`\\n3. `Create tasks with dependencies. Watch teammates respect the blocked order.`\\n4. 输入 `/tasks` 查看带 owner 的任务看板\\n5. 输入 `/team` 监控谁在工作、谁在空闲\\n\"\n  },\n  {\n    \"version\": \"s12\",\n    \"locale\": \"zh\",\n    \"title\": \"s12: Worktree + Task Isolation (Worktree 任务隔离)\",\n    \"content\": \"# s12: Worktree + Task Isolation (Worktree 任务隔离)\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\\n\\n> *\\\"各干各的目录, 互不干扰\\\"* -- 任务管目标, worktree 管目录, 按 ID 绑定。\\n\\n## 问题\\n\\n到 s11, 智能体已经能自主认领和完成任务。但所有任务共享一个目录。两个智能体同时重构不同模块 -- A 改 `config.py`, B 也改 `config.py`, 未提交的改动互相污染, 谁也没法干净回滚。\\n\\n任务板管 \\\"做什么\\\" 但不管 \\\"在哪做\\\"。解法: 给每个任务一个独立的 git worktree 目录, 用任务 ID 把两边关联起来。\\n\\n## 解决方案\\n\\n```\\nControl plane (.tasks/)             Execution plane (.worktrees/)\\n+------------------+                +------------------------+\\n| task_1.json      |                | auth-refactor/         |\\n|   status: in_progress  <------>   branch: wt/auth-refactor\\n|   worktree: \\\"auth-refactor\\\"   |   task_id: 1             |\\n+------------------+                +------------------------+\\n| task_2.json      |                | ui-login/              |\\n|   status: pending    <------>     branch: wt/ui-login\\n|   worktree: \\\"ui-login\\\"       |   task_id: 2             |\\n+------------------+                +------------------------+\\n                                    |\\n                          index.json (worktree registry)\\n                          events.jsonl (lifecycle log)\\n\\nState machines:\\n  Task:     pending -> in_progress -> completed\\n  Worktree: absent  -> active      -> removed | kept\\n```\\n\\n## 工作原理\\n\\n1. **创建任务。** 先把目标持久化。\\n\\n```python\\nTASKS.create(\\\"Implement auth refactor\\\")\\n# -> .tasks/task_1.json  status=pending  worktree=\\\"\\\"\\n```\\n\\n2. **创建 worktree 并绑定任务。** 传入 `task_id` 自动将任务推进到 `in_progress`。\\n\\n```python\\nWORKTREES.create(\\\"auth-refactor\\\", task_id=1)\\n# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD\\n# -> index.json gets new entry, task_1.json gets worktree=\\\"auth-refactor\\\"\\n```\\n\\n绑定同时写入两侧状态:\\n\\n```python\\ndef bind_worktree(self, task_id, worktree):\\n    task = self._load(task_id)\\n    task[\\\"worktree\\\"] = worktree\\n    if task[\\\"status\\\"] == \\\"pending\\\":\\n        task[\\\"status\\\"] = \\\"in_progress\\\"\\n    self._save(task)\\n```\\n\\n3. **在 worktree 中执行命令。** `cwd` 指向隔离目录。\\n\\n```python\\nsubprocess.run(command, shell=True, cwd=worktree_path,\\n               capture_output=True, text=True, timeout=300)\\n```\\n\\n4. **收尾。** 两种选择:\\n   - `worktree_keep(name)` -- 保留目录供后续使用。\\n   - `worktree_remove(name, complete_task=True)` -- 删除目录, 完成绑定任务, 发出事件。一个调用搞定拆除 + 完成。\\n\\n```python\\ndef remove(self, name, force=False, complete_task=False):\\n    self._run_git([\\\"worktree\\\", \\\"remove\\\", wt[\\\"path\\\"]])\\n    if complete_task and wt.get(\\\"task_id\\\") is not None:\\n        self.tasks.update(wt[\\\"task_id\\\"], status=\\\"completed\\\")\\n        self.tasks.unbind_worktree(wt[\\\"task_id\\\"])\\n        self.events.emit(\\\"task.completed\\\", ...)\\n```\\n\\n5. **事件流。** 每个生命周期步骤写入 `.worktrees/events.jsonl`:\\n\\n```json\\n{\\n  \\\"event\\\": \\\"worktree.remove.after\\\",\\n  \\\"task\\\": {\\\"id\\\": 1, \\\"status\\\": \\\"completed\\\"},\\n  \\\"worktree\\\": {\\\"name\\\": \\\"auth-refactor\\\", \\\"status\\\": \\\"removed\\\"},\\n  \\\"ts\\\": 1730000000\\n}\\n```\\n\\n事件类型: `worktree.create.before/after/failed`, `worktree.remove.before/after/failed`, `worktree.keep`, `task.completed`。\\n\\n崩溃后从 `.tasks/` + `.worktrees/index.json` 重建现场。会话记忆是易失的; 磁盘状态是持久的。\\n\\n## 相对 s11 的变更\\n\\n| 组件               | 之前 (s11)                 | 之后 (s12)                                   |\\n|--------------------|----------------------------|----------------------------------------------|\\n| 协调               | 任务板 (owner/status)      | 任务板 + worktree 显式绑定                   |\\n| 执行范围           | 共享目录                   | 每个任务独立目录                             |\\n| 可恢复性           | 仅任务状态                 | 任务状态 + worktree 索引                     |\\n| 收尾               | 任务完成                   | 任务完成 + 显式 keep/remove                  |\\n| 生命周期可见性     | 隐式日志                   | `.worktrees/events.jsonl` 显式事件流         |\\n\\n## 试一试\\n\\n```sh\\ncd learn-claude-code\\npython agents/s12_worktree_task_isolation.py\\n```\\n\\n试试这些 prompt (英文 prompt 对 LLM 效果更好, 也可以用中文):\\n\\n1. `Create tasks for backend auth and frontend login page, then list tasks.`\\n2. `Create worktree \\\"auth-refactor\\\" for task 1, then bind task 2 to a new worktree \\\"ui-login\\\".`\\n3. `Run \\\"git status --short\\\" in worktree \\\"auth-refactor\\\".`\\n4. `Keep worktree \\\"ui-login\\\", then list worktrees and inspect events.`\\n5. `Remove worktree \\\"auth-refactor\\\" with complete_task=true, then list tasks/worktrees/events.`\\n\"\n  },\n  {\n    \"version\": \"s01\",\n    \"locale\": \"ja\",\n    \"title\": \"s01: The Agent Loop\",\n    \"content\": \"# s01: The Agent Loop\\n\\n`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"One loop & Bash is all you need\\\"* -- 1つのツール + 1つのループ = エージェント。\\n\\n## 問題\\n\\n言語モデルはコードについて推論できるが、現実世界に触れられない。ファイルを読めず、テストを実行できず、エラーを確認できない。ループがなければ、ツール呼び出しのたびにユーザーが手動で結果をコピーペーストする必要がある。つまりユーザー自身がループになる。\\n\\n## 解決策\\n\\n```\\n+--------+      +-------+      +---------+\\n|  User  | ---> |  LLM  | ---> |  Tool   |\\n| prompt |      |       |      | execute |\\n+--------+      +---+---+      +----+----+\\n                    ^                |\\n                    |   tool_result  |\\n                    +----------------+\\n                    (loop until stop_reason != \\\"tool_use\\\")\\n```\\n\\n1つの終了条件がフロー全体を制御する。モデルがツール呼び出しを止めるまでループが回り続ける。\\n\\n## 仕組み\\n\\n1. ユーザーのプロンプトが最初のメッセージになる。\\n\\n```python\\nmessages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n```\\n\\n2. メッセージとツール定義をLLMに送信する。\\n\\n```python\\nresponse = client.messages.create(\\n    model=MODEL, system=SYSTEM, messages=messages,\\n    tools=TOOLS, max_tokens=8000,\\n)\\n```\\n\\n3. アシスタントのレスポンスを追加し、`stop_reason`を確認する。ツールが呼ばれなければ終了。\\n\\n```python\\nmessages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\nif response.stop_reason != \\\"tool_use\\\":\\n    return\\n```\\n\\n4. 各ツール呼び出しを実行し、結果を収集してuserメッセージとして追加。ステップ2に戻る。\\n\\n```python\\nresults = []\\nfor block in response.content:\\n    if block.type == \\\"tool_use\\\":\\n        output = run_bash(block.input[\\\"command\\\"])\\n        results.append({\\n            \\\"type\\\": \\\"tool_result\\\",\\n            \\\"tool_use_id\\\": block.id,\\n            \\\"content\\\": output,\\n        })\\nmessages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n```\\n\\n1つの関数にまとめると:\\n\\n```python\\ndef agent_loop(query):\\n    messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": query}]\\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                output = run_bash(block.input[\\\"command\\\"])\\n                results.append({\\n                    \\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": output,\\n                })\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n```\\n\\nこれでエージェント全体が30行未満に収まる。本コースの残りはすべてこのループの上に積み重なる -- ループ自体は変わらない。\\n\\n## 変更点\\n\\n| Component     | Before     | After                          |\\n|---------------|------------|--------------------------------|\\n| Agent loop    | (none)     | `while True` + stop_reason     |\\n| Tools         | (none)     | `bash` (one tool)              |\\n| Messages      | (none)     | Accumulating list              |\\n| Control flow  | (none)     | `stop_reason != \\\"tool_use\\\"`    |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s01_agent_loop.py\\n```\\n\\n1. `Create a file called hello.py that prints \\\"Hello, World!\\\"`\\n2. `List all Python files in this directory`\\n3. `What is the current git branch?`\\n4. `Create a directory called test_output and write 3 files in it`\\n\"\n  },\n  {\n    \"version\": \"s02\",\n    \"locale\": \"ja\",\n    \"title\": \"s02: Tool Use\",\n    \"content\": \"# s02: Tool Use\\n\\n`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"ツールを足すなら、ハンドラーを1つ足すだけ\\\"* -- ループは変わらない。新ツールは dispatch map に登録するだけ。\\n\\n## 問題\\n\\n`bash`だけでは、エージェントは何でもシェル経由で行う。`cat`は予測不能に切り詰め、`sed`は特殊文字で壊れ、すべてのbash呼び出しが制約のないセキュリティ面になる。`read_file`や`write_file`のような専用ツールなら、ツールレベルでパスのサンドボックス化を強制できる。\\n\\n重要な点: ツールを追加してもループの変更は不要。\\n\\n## 解決策\\n\\n```\\n+--------+      +-------+      +------------------+\\n|  User  | ---> |  LLM  | ---> | Tool Dispatch    |\\n| prompt |      |       |      | {                |\\n+--------+      +---+---+      |   bash: run_bash |\\n                    ^           |   read: run_read |\\n                    |           |   write: run_wr  |\\n                    +-----------+   edit: run_edit |\\n                    tool_result | }                |\\n                                +------------------+\\n\\nThe dispatch map is a dict: {tool_name: handler_function}.\\nOne lookup replaces any if/elif chain.\\n```\\n\\n## 仕組み\\n\\n1. 各ツールにハンドラ関数を定義する。パスのサンドボックス化でワークスペース外への脱出を防ぐ。\\n\\n```python\\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_read(path: str, limit: int = None) -> str:\\n    text = safe_path(path).read_text()\\n    lines = text.splitlines()\\n    if limit and limit < len(lines):\\n        lines = lines[:limit]\\n    return \\\"\\\\n\\\".join(lines)[:50000]\\n```\\n\\n2. ディスパッチマップがツール名とハンドラを結びつける。\\n\\n```python\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"],\\n                                        kw[\\\"new_text\\\"]),\\n}\\n```\\n\\n3. ループ内で名前によりハンドラをルックアップする。ループ本体はs01から不変。\\n\\n```python\\nfor block in response.content:\\n    if block.type == \\\"tool_use\\\":\\n        handler = TOOL_HANDLERS.get(block.name)\\n        output = handler(**block.input) if handler \\\\\\n            else f\\\"Unknown tool: {block.name}\\\"\\n        results.append({\\n            \\\"type\\\": \\\"tool_result\\\",\\n            \\\"tool_use_id\\\": block.id,\\n            \\\"content\\\": output,\\n        })\\n```\\n\\nツール追加 = ハンドラ追加 + スキーマ追加。ループは決して変わらない。\\n\\n## s01からの変更点\\n\\n| Component      | Before (s01)       | After (s02)                |\\n|----------------|--------------------|----------------------------|\\n| Tools          | 1 (bash only)      | 4 (bash, read, write, edit)|\\n| Dispatch       | Hardcoded bash call | `TOOL_HANDLERS` dict       |\\n| Path safety    | None               | `safe_path()` sandbox      |\\n| Agent loop     | Unchanged          | Unchanged                  |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s02_tool_use.py\\n```\\n\\n1. `Read the file requirements.txt`\\n2. `Create a file called greet.py with a greet(name) function`\\n3. `Edit greet.py to add a docstring to the function`\\n4. `Read greet.py to verify the edit worked`\\n\"\n  },\n  {\n    \"version\": \"s03\",\n    \"locale\": \"ja\",\n    \"title\": \"s03: TodoWrite\",\n    \"content\": \"# s03: TodoWrite\\n\\n`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"計画のないエージェントは行き当たりばったり\\\"* -- まずステップを書き出し、それから実行。\\n\\n## 問題\\n\\nマルチステップのタスクで、モデルは途中で迷子になる。作業を繰り返したり、ステップを飛ばしたり、脱線したりする。長い会話になるほど悪化する -- ツール結果がコンテキストを埋めるにつれ、システムプロンプトの影響力が薄れる。10ステップのリファクタリングでステップ1-3を完了した後、残りを忘れて即興を始めてしまう。\\n\\n## 解決策\\n\\n```\\n+--------+      +-------+      +---------+\\n|  User  | ---> |  LLM  | ---> | Tools   |\\n| prompt |      |       |      | + todo  |\\n+--------+      +---+---+      +----+----+\\n                    ^                |\\n                    |   tool_result  |\\n                    +----------------+\\n                          |\\n              +-----------+-----------+\\n              | TodoManager state     |\\n              | [ ] task A            |\\n              | [>] task B  <- doing  |\\n              | [x] task C            |\\n              +-----------------------+\\n                          |\\n              if rounds_since_todo >= 3:\\n                inject <reminder> into tool_result\\n```\\n\\n## 仕組み\\n\\n1. TodoManagerはアイテムのリストをステータス付きで保持する。`in_progress`にできるのは同時に1つだけ。\\n\\n```python\\nclass TodoManager:\\n    def update(self, items: list) -> str:\\n        validated, in_progress_count = [], 0\\n        for item in items:\\n            status = item.get(\\\"status\\\", \\\"pending\\\")\\n            if status == \\\"in_progress\\\":\\n                in_progress_count += 1\\n            validated.append({\\\"id\\\": item[\\\"id\\\"], \\\"text\\\": item[\\\"text\\\"],\\n                              \\\"status\\\": status})\\n        if in_progress_count > 1:\\n            raise ValueError(\\\"Only one task can be in_progress\\\")\\n        self.items = validated\\n        return self.render()\\n```\\n\\n2. `todo`ツールは他のツールと同様にディスパッチマップに追加される。\\n\\n```python\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"todo\\\": lambda **kw: TODO.update(kw[\\\"items\\\"]),\\n}\\n```\\n\\n3. nagリマインダーが、モデルが3ラウンド以上`todo`を呼ばなかった場合にナッジを注入する。\\n\\n```python\\nif rounds_since_todo >= 3 and messages:\\n    last = messages[-1]\\n    if last[\\\"role\\\"] == \\\"user\\\" and isinstance(last.get(\\\"content\\\"), list):\\n        last[\\\"content\\\"].insert(0, {\\n            \\\"type\\\": \\\"text\\\",\\n            \\\"text\\\": \\\"<reminder>Update your todos.</reminder>\\\",\\n        })\\n```\\n\\n「一度にin_progressは1つだけ」の制約が逐次的な集中を強制し、nagリマインダーが説明責任を生む。\\n\\n## s02からの変更点\\n\\n| Component      | Before (s02)     | After (s03)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 4                | 5 (+todo)                  |\\n| Planning       | None             | TodoManager with statuses  |\\n| Nag injection  | None             | `<reminder>` after 3 rounds|\\n| Agent loop     | Simple dispatch  | + rounds_since_todo counter|\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s03_todo_write.py\\n```\\n\\n1. `Refactor the file hello.py: add type hints, docstrings, and a main guard`\\n2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py`\\n3. `Review all Python files and fix any style issues`\\n\"\n  },\n  {\n    \"version\": \"s04\",\n    \"locale\": \"ja\",\n    \"title\": \"s04: Subagents\",\n    \"content\": \"# s04: Subagents\\n\\n`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"大きなタスクを分割し、各サブタスクにクリーンなコンテキストを\\\"* -- サブエージェントは独立した messages[] を使い、メイン会話を汚さない。\\n\\n## 問題\\n\\nエージェントが作業するにつれ、messages配列は膨張し続ける。すべてのファイル読み取り、すべてのbash出力がコンテキストに永久に残る。「このプロジェクトはどのテストフレームワークを使っているか」という質問は5つのファイルを読む必要があるかもしれないが、親に必要なのは「pytest」という答えだけだ。\\n\\n## 解決策\\n\\n```\\nParent agent                     Subagent\\n+------------------+             +------------------+\\n| messages=[...]   |             | messages=[]      | <-- fresh\\n|                  |  dispatch   |                  |\\n| tool: task       | ----------> | while tool_use:  |\\n|   prompt=\\\"...\\\"   |             |   call tools     |\\n|                  |  summary    |   append results |\\n|   result = \\\"...\\\" | <---------- | return last text |\\n+------------------+             +------------------+\\n\\nParent context stays clean. Subagent context is discarded.\\n```\\n\\n## 仕組み\\n\\n1. 親に`task`ツールを追加する。子は`task`を除くすべての基本ツールを取得する(再帰的な生成は不可)。\\n\\n```python\\nPARENT_TOOLS = CHILD_TOOLS + [\\n    {\\\"name\\\": \\\"task\\\",\\n     \\\"description\\\": \\\"Spawn a subagent with fresh context.\\\",\\n     \\\"input_schema\\\": {\\n         \\\"type\\\": \\\"object\\\",\\n         \\\"properties\\\": {\\\"prompt\\\": {\\\"type\\\": \\\"string\\\"}},\\n         \\\"required\\\": [\\\"prompt\\\"],\\n     }},\\n]\\n```\\n\\n2. サブエージェントは`messages=[]`で開始し、自身のループを実行する。最終テキストだけが親に返る。\\n\\n```python\\ndef run_subagent(prompt: str) -> str:\\n    sub_messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n    for _ in range(30):  # safety limit\\n        response = client.messages.create(\\n            model=MODEL, system=SUBAGENT_SYSTEM,\\n            messages=sub_messages,\\n            tools=CHILD_TOOLS, max_tokens=8000,\\n        )\\n        sub_messages.append({\\\"role\\\": \\\"assistant\\\",\\n                             \\\"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 = TOOL_HANDLERS.get(block.name)\\n                output = handler(**block.input)\\n                results.append({\\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": str(output)[:50000]})\\n        sub_messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n    return \\\"\\\".join(\\n        b.text for b in response.content if hasattr(b, \\\"text\\\")\\n    ) or \\\"(no summary)\\\"\\n```\\n\\n子のメッセージ履歴全体(30回以上のツール呼び出し)は破棄される。親は1段落の要約を通常の`tool_result`として受け取る。\\n\\n## s03からの変更点\\n\\n| Component      | Before (s03)     | After (s04)               |\\n|----------------|------------------|---------------------------|\\n| Tools          | 5                | 5 (base) + task (parent)  |\\n| Context        | Single shared    | Parent + child isolation  |\\n| Subagent       | None             | `run_subagent()` function |\\n| Return value   | N/A              | Summary text only         |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s04_subagent.py\\n```\\n\\n1. `Use a subtask to find what testing framework this project uses`\\n2. `Delegate: read all .py files and summarize what each one does`\\n3. `Use a task to create a new module, then verify it from here`\\n\"\n  },\n  {\n    \"version\": \"s05\",\n    \"locale\": \"ja\",\n    \"title\": \"s05: Skills\",\n    \"content\": \"# s05: Skills\\n\\n`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"必要な知識を、必要な時に読み込む\\\"* -- system prompt ではなく tool_result で注入。\\n\\n## 問題\\n\\nエージェントにドメイン固有のワークフローを遵守させたい: gitの規約、テストパターン、コードレビューチェックリスト。すべてをシステムプロンプトに入れると、使われないスキルにトークンを浪費する。10スキル x 2000トークン = 20,000トークン、ほとんどが任意のタスクに無関係だ。\\n\\n## 解決策\\n\\n```\\nSystem prompt (Layer 1 -- always present):\\n+--------------------------------------+\\n| You are a coding agent.              |\\n| Skills available:                    |\\n|   - git: Git workflow helpers        |  ~100 tokens/skill\\n|   - test: Testing best practices     |\\n+--------------------------------------+\\n\\nWhen model calls load_skill(\\\"git\\\"):\\n+--------------------------------------+\\n| tool_result (Layer 2 -- on demand):  |\\n| <skill name=\\\"git\\\">                   |\\n|   Full git workflow instructions...  |  ~2000 tokens\\n|   Step 1: ...                        |\\n| </skill>                             |\\n+--------------------------------------+\\n```\\n\\n第1層: スキル*名*をシステムプロンプトに(低コスト)。第2層: スキル*本体*をtool_resultに(オンデマンド)。\\n\\n## 仕組み\\n\\n1. 各スキルは `SKILL.md` ファイルを含むディレクトリとして配置される。\\n\\n```\\nskills/\\n  pdf/\\n    SKILL.md       # ---\\\\n name: pdf\\\\n description: Process PDF files\\\\n ---\\\\n ...\\n  code-review/\\n    SKILL.md       # ---\\\\n name: code-review\\\\n description: Review code\\\\n ---\\\\n ...\\n```\\n\\n2. SkillLoaderが `SKILL.md` を再帰的に探索し、ディレクトリ名をスキル識別子として使用する。\\n\\n```python\\nclass SkillLoader:\\n    def __init__(self, skills_dir: Path):\\n        self.skills = {}\\n        for f in sorted(skills_dir.rglob(\\\"SKILL.md\\\")):\\n            text = f.read_text()\\n            meta, body = self._parse_frontmatter(text)\\n            name = meta.get(\\\"name\\\", f.parent.name)\\n            self.skills[name] = {\\\"meta\\\": meta, \\\"body\\\": body}\\n\\n    def get_descriptions(self) -> str:\\n        lines = []\\n        for name, skill in self.skills.items():\\n            desc = skill[\\\"meta\\\"].get(\\\"description\\\", \\\"\\\")\\n            lines.append(f\\\"  - {name}: {desc}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def get_content(self, name: str) -> str:\\n        skill = self.skills.get(name)\\n        if not skill:\\n            return f\\\"Error: Unknown skill '{name}'.\\\"\\n        return f\\\"<skill name=\\\\\\\"{name}\\\\\\\">\\\\n{skill['body']}\\\\n</skill>\\\"\\n```\\n\\n3. 第1層はシステムプロンプトに配置。第2層は通常のツールハンドラ。\\n\\n```python\\nSYSTEM = f\\\"\\\"\\\"You are a coding agent at {WORKDIR}.\\nSkills available:\\n{SKILL_LOADER.get_descriptions()}\\\"\\\"\\\"\\n\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"load_skill\\\": lambda **kw: SKILL_LOADER.get_content(kw[\\\"name\\\"]),\\n}\\n```\\n\\nモデルはどのスキルが存在するかを知り(低コスト)、関連する時にだけ読み込む(高コスト)。\\n\\n## s04からの変更点\\n\\n| Component      | Before (s04)     | After (s05)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 5 (base + task)  | 5 (base + load_skill)      |\\n| System prompt  | Static string    | + skill descriptions       |\\n| Knowledge      | None             | skills/\\\\*/SKILL.md files   |\\n| Injection      | None             | Two-layer (system + result)|\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s05_skill_loading.py\\n```\\n\\n1. `What skills are available?`\\n2. `Load the agent-builder skill and follow its instructions`\\n3. `I need to do a code review -- load the relevant skill first`\\n4. `Build an MCP server using the mcp-builder skill`\\n\"\n  },\n  {\n    \"version\": \"s06\",\n    \"locale\": \"ja\",\n    \"title\": \"s06: Context Compact\",\n    \"content\": \"# s06: Context Compact\\n\\n`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"コンテキストはいつか溢れる、空ける手段が要る\\\"* -- 3層圧縮で無限セッションを実現。\\n\\n## 問題\\n\\nコンテキストウィンドウは有限だ。1000行のファイルに対する`read_file`1回で約4000トークンを消費する。30ファイルを読み20回のbashコマンドを実行すると、100,000トークン超。圧縮なしでは、エージェントは大規模コードベースで作業できない。\\n\\n## 解決策\\n\\n積極性を段階的に上げる3層構成:\\n\\n```\\nEvery turn:\\n+------------------+\\n| Tool call result |\\n+------------------+\\n        |\\n        v\\n[Layer 1: micro_compact]        (silent, every turn)\\n  Replace tool_result > 3 turns old\\n  with \\\"[Previous: used {tool_name}]\\\"\\n        |\\n        v\\n[Check: tokens > 50000?]\\n   |               |\\n   no              yes\\n   |               |\\n   v               v\\ncontinue    [Layer 2: auto_compact]\\n              Save transcript to .transcripts/\\n              LLM summarizes conversation.\\n              Replace all messages with [summary].\\n                    |\\n                    v\\n            [Layer 3: compact tool]\\n              Model calls compact explicitly.\\n              Same summarization as auto_compact.\\n```\\n\\n## 仕組み\\n\\n1. **第1層 -- micro_compact**: 各LLM呼び出しの前に、古いツール結果をプレースホルダーに置換する。\\n\\n```python\\ndef micro_compact(messages: list) -> list:\\n    tool_results = []\\n    for i, msg in enumerate(messages):\\n        if msg[\\\"role\\\"] == \\\"user\\\" and isinstance(msg.get(\\\"content\\\"), list):\\n            for j, part in enumerate(msg[\\\"content\\\"]):\\n                if isinstance(part, dict) and part.get(\\\"type\\\") == \\\"tool_result\\\":\\n                    tool_results.append((i, j, part))\\n    if len(tool_results) <= KEEP_RECENT:\\n        return messages\\n    for _, _, part in tool_results[:-KEEP_RECENT]:\\n        if len(part.get(\\\"content\\\", \\\"\\\")) > 100:\\n            part[\\\"content\\\"] = f\\\"[Previous: used {tool_name}]\\\"\\n    return messages\\n```\\n\\n2. **第2層 -- auto_compact**: トークンが閾値を超えたら、完全なトランスクリプトをディスクに保存し、LLMに要約を依頼する。\\n\\n```python\\ndef auto_compact(messages: list) -> list:\\n    # Save transcript for recovery\\n    transcript_path = TRANSCRIPT_DIR / f\\\"transcript_{int(time.time())}.jsonl\\\"\\n    with open(transcript_path, \\\"w\\\") as f:\\n        for msg in messages:\\n            f.write(json.dumps(msg, default=str) + \\\"\\\\n\\\")\\n    # LLM summarizes\\n    response = client.messages.create(\\n        model=MODEL,\\n        messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\":\\n            \\\"Summarize this conversation for continuity...\\\"\\n            + json.dumps(messages, default=str)[:80000]}],\\n        max_tokens=2000,\\n    )\\n    return [\\n        {\\\"role\\\": \\\"user\\\", \\\"content\\\": f\\\"[Compressed]\\\\n\\\\n{response.content[0].text}\\\"},\\n        {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"Understood. Continuing.\\\"},\\n    ]\\n```\\n\\n3. **第3層 -- manual compact**: `compact`ツールが同じ要約処理をオンデマンドでトリガーする。\\n\\n4. ループが3層すべてを統合する:\\n\\n```python\\ndef agent_loop(messages: list):\\n    while True:\\n        micro_compact(messages)                        # Layer 1\\n        if estimate_tokens(messages) > THRESHOLD:\\n            messages[:] = auto_compact(messages)       # Layer 2\\n        response = client.messages.create(...)\\n        # ... tool execution ...\\n        if manual_compact:\\n            messages[:] = auto_compact(messages)       # Layer 3\\n```\\n\\nトランスクリプトがディスク上に完全な履歴を保持する。何も真に失われず、アクティブなコンテキストの外に移動されるだけ。\\n\\n## s05からの変更点\\n\\n| Component      | Before (s05)     | After (s06)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 5                | 5 (base + compact)         |\\n| Context mgmt   | None             | Three-layer compression    |\\n| Micro-compact  | None             | Old results -> placeholders|\\n| Auto-compact   | None             | Token threshold trigger    |\\n| Transcripts    | None             | Saved to .transcripts/     |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s06_context_compact.py\\n```\\n\\n1. `Read every Python file in the agents/ directory one by one` (micro-compactが古い結果を置換するのを観察する)\\n2. `Keep reading files until compression triggers automatically`\\n3. `Use the compact tool to manually compress the conversation`\\n\"\n  },\n  {\n    \"version\": \"s07\",\n    \"locale\": \"ja\",\n    \"title\": \"s07: Task System\",\n    \"content\": \"# s07: Task System\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12`\\n\\n> *\\\"大きな目標を小タスクに分解し、順序付けし、ディスクに記録する\\\"* -- ファイルベースのタスクグラフ、マルチエージェント協調の基盤。\\n\\n## 問題\\n\\ns03のTodoManagerはメモリ上のフラットなチェックリストに過ぎない: 順序なし、依存関係なし、ステータスは完了か未完了のみ。実際の目標には構造がある -- タスクBはタスクAに依存し、タスクCとDは並行実行でき、タスクEはCとDの両方を待つ。\\n\\n明示的な関係がなければ、エージェントは何が実行可能で、何がブロックされ、何が同時に走れるかを判断できない。しかもリストはメモリ上にしかないため、コンテキスト圧縮(s06)で消える。\\n\\n## 解決策\\n\\nフラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)・後方依存(`blocks`)を持つ。タスクグラフは常に3つの問いに答える:\\n\\n- **何が実行可能か?** -- `pending`ステータスで`blockedBy`が空のタスク。\\n- **何がブロックされているか?** -- 未完了の依存を待つタスク。\\n- **何が完了したか?** -- `completed`のタスク。完了時に後続タスクを自動的にアンブロックする。\\n\\n```\\n.tasks/\\n  task_1.json  {\\\"id\\\":1, \\\"status\\\":\\\"completed\\\"}\\n  task_2.json  {\\\"id\\\":2, \\\"blockedBy\\\":[1], \\\"status\\\":\\\"pending\\\"}\\n  task_3.json  {\\\"id\\\":3, \\\"blockedBy\\\":[1], \\\"status\\\":\\\"pending\\\"}\\n  task_4.json  {\\\"id\\\":4, \\\"blockedBy\\\":[2,3], \\\"status\\\":\\\"pending\\\"}\\n\\nタスクグラフ (DAG):\\n                 +----------+\\n            +--> | task 2   | --+\\n            |    | pending  |   |\\n+----------+     +----------+    +--> +----------+\\n| task 1   |                          | task 4   |\\n| completed| --> +----------+    +--> | blocked  |\\n+----------+     | task 3   | --+     +----------+\\n                 | pending  |\\n                 +----------+\\n\\n順序:       task 1 は 2 と 3 より先に完了する必要がある\\n並行:       task 2 と 3 は同時に実行できる\\n依存:       task 4 は 2 と 3 の両方を待つ\\nステータス: pending -> in_progress -> completed\\n```\\n\\nこのタスクグラフは s07 以降の全メカニズムの協調バックボーンとなる: バックグラウンド実行(s08)、マルチエージェントチーム(s09+)、worktree分離(s12)はすべてこの同じ構造を読み書きする。\\n\\n## 仕組み\\n\\n1. **TaskManager**: タスクごとに1つのJSONファイル、依存グラフ付きCRUD。\\n\\n```python\\nclass TaskManager:\\n    def __init__(self, tasks_dir: Path):\\n        self.dir = tasks_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self._next_id = self._max_id() + 1\\n\\n    def create(self, subject, description=\\\"\\\"):\\n        task = {\\\"id\\\": self._next_id, \\\"subject\\\": subject,\\n                \\\"status\\\": \\\"pending\\\", \\\"blockedBy\\\": [],\\n                \\\"blocks\\\": [], \\\"owner\\\": \\\"\\\"}\\n        self._save(task)\\n        self._next_id += 1\\n        return json.dumps(task, indent=2)\\n```\\n\\n2. **依存解除**: タスク完了時に、他タスクの`blockedBy`リストから完了IDを除去し、後続タスクをアンブロックする。\\n\\n```python\\ndef _clear_dependency(self, completed_id):\\n    for f in self.dir.glob(\\\"task_*.json\\\"):\\n        task = json.loads(f.read_text())\\n        if completed_id in task.get(\\\"blockedBy\\\", []):\\n            task[\\\"blockedBy\\\"].remove(completed_id)\\n            self._save(task)\\n```\\n\\n3. **ステータス遷移 + 依存配線**: `update`がステータス変更と依存エッジを担う。\\n\\n```python\\ndef update(self, task_id, status=None,\\n           add_blocked_by=None, add_blocks=None):\\n    task = self._load(task_id)\\n    if status:\\n        task[\\\"status\\\"] = status\\n        if status == \\\"completed\\\":\\n            self._clear_dependency(task_id)\\n    self._save(task)\\n```\\n\\n4. 4つのタスクツールをディスパッチマップに追加する。\\n\\n```python\\nTOOL_HANDLERS = {\\n    # ...base tools...\\n    \\\"task_create\\\": lambda **kw: TASKS.create(kw[\\\"subject\\\"]),\\n    \\\"task_update\\\": lambda **kw: TASKS.update(kw[\\\"task_id\\\"], kw.get(\\\"status\\\")),\\n    \\\"task_list\\\":   lambda **kw: TASKS.list_all(),\\n    \\\"task_get\\\":    lambda **kw: TASKS.get(kw[\\\"task_id\\\"]),\\n}\\n```\\n\\ns07以降、タスクグラフがマルチステップ作業のデフォルト。s03のTodoは軽量な単一セッション用チェックリストとして残る。\\n\\n## s06からの変更点\\n\\n| コンポーネント | Before (s06) | After (s07) |\\n|---|---|---|\\n| Tools | 5 | 8 (`task_create/update/list/get`) |\\n| 計画モデル | フラットチェックリスト (メモリ) | 依存関係付きタスクグラフ (ディスク) |\\n| 関係 | なし | `blockedBy` + `blocks` エッジ |\\n| ステータス追跡 | 完了か未完了 | `pending` -> `in_progress` -> `completed` |\\n| 永続性 | 圧縮で消失 | 圧縮・再起動後も存続 |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s07_task_system.py\\n```\\n\\n1. `Create 3 tasks: \\\"Setup project\\\", \\\"Write code\\\", \\\"Write tests\\\". Make them depend on each other in order.`\\n2. `List all tasks and show the dependency graph`\\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\\n\"\n  },\n  {\n    \"version\": \"s08\",\n    \"locale\": \"ja\",\n    \"title\": \"s08: Background Tasks\",\n    \"content\": \"# s08: Background Tasks\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12`\\n\\n> *\\\"遅い操作はバックグラウンドへ、エージェントは次を考え続ける\\\"* -- デーモンスレッドがコマンド実行、完了後に通知を注入。\\n\\n## 問題\\n\\n一部のコマンドは数分かかる: `npm install`、`pytest`、`docker build`。ブロッキングループでは、モデルはサブプロセスの完了を待って座っている。ユーザーが「依存関係をインストールして、その間にconfigファイルを作って」と言っても、エージェントは並列ではなく逐次的に処理する。\\n\\n## 解決策\\n\\n```\\nMain thread                Background thread\\n+-----------------+        +-----------------+\\n| agent loop      |        | subprocess runs |\\n| ...             |        | ...             |\\n| [LLM call] <---+------- | enqueue(result) |\\n|  ^drain queue   |        +-----------------+\\n+-----------------+\\n\\nTimeline:\\nAgent --[spawn A]--[spawn B]--[other work]----\\n             |          |\\n             v          v\\n          [A runs]   [B runs]      (parallel)\\n             |          |\\n             +-- results injected before next LLM call --+\\n```\\n\\n## 仕組み\\n\\n1. BackgroundManagerがスレッドセーフな通知キューでタスクを追跡する。\\n\\n```python\\nclass BackgroundManager:\\n    def __init__(self):\\n        self.tasks = {}\\n        self._notification_queue = []\\n        self._lock = threading.Lock()\\n```\\n\\n2. `run()`がデーモンスレッドを開始し、即座にリターンする。\\n\\n```python\\ndef run(self, command: str) -> str:\\n    task_id = str(uuid.uuid4())[:8]\\n    self.tasks[task_id] = {\\\"status\\\": \\\"running\\\", \\\"command\\\": command}\\n    thread = threading.Thread(\\n        target=self._execute, args=(task_id, command), daemon=True)\\n    thread.start()\\n    return f\\\"Background task {task_id} started\\\"\\n```\\n\\n3. サブプロセス完了時に、結果を通知キューへ。\\n\\n```python\\ndef _execute(self, task_id, command):\\n    try:\\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\\n            capture_output=True, text=True, timeout=300)\\n        output = (r.stdout + r.stderr).strip()[:50000]\\n    except subprocess.TimeoutExpired:\\n        output = \\\"Error: Timeout (300s)\\\"\\n    with self._lock:\\n        self._notification_queue.append({\\n            \\\"task_id\\\": task_id, \\\"result\\\": output[:500]})\\n```\\n\\n4. エージェントループが各LLM呼び出しの前に通知をドレインする。\\n\\n```python\\ndef agent_loop(messages: list):\\n    while True:\\n        notifs = BG.drain_notifications()\\n        if notifs:\\n            notif_text = \\\"\\\\n\\\".join(\\n                f\\\"[bg:{n['task_id']}] {n['result']}\\\" for n in notifs)\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<background-results>\\\\n{notif_text}\\\\n\\\"\\n                           f\\\"</background-results>\\\"})\\n            messages.append({\\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted background results.\\\"})\\n        response = client.messages.create(...)\\n```\\n\\nループはシングルスレッドのまま。サブプロセスI/Oだけが並列化される。\\n\\n## s07からの変更点\\n\\n| Component      | Before (s07)     | After (s08)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 8                | 6 (base + background_run + check)|\\n| Execution      | Blocking only    | Blocking + background threads|\\n| Notification   | None             | Queue drained per loop     |\\n| Concurrency    | None             | Daemon threads             |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s08_background_tasks.py\\n```\\n\\n1. `Run \\\"sleep 5 && echo done\\\" in the background, then create a file while it runs`\\n2. `Start 3 background tasks: \\\"sleep 2\\\", \\\"sleep 4\\\", \\\"sleep 6\\\". Check their status.`\\n3. `Run pytest in the background and keep working on other things`\\n\"\n  },\n  {\n    \"version\": \"s09\",\n    \"locale\": \"ja\",\n    \"title\": \"s09: Agent Teams\",\n    \"content\": \"# s09: Agent Teams\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12`\\n\\n> *\\\"一人で終わらないなら、チームメイトに任せる\\\"* -- 永続チームメイト + 非同期メールボックス。\\n\\n## 問題\\n\\nサブエージェント(s04)は使い捨てだ: 生成し、作業し、要約を返し、消滅する。アイデンティティもなく、呼び出し間の記憶もない。バックグラウンドタスク(s08)はシェルコマンドを実行するが、LLM誘導の意思決定はできない。\\n\\n本物のチームワークには: (1)単一プロンプトを超えて存続する永続エージェント、(2)アイデンティティとライフサイクル管理、(3)エージェント間の通信チャネルが必要だ。\\n\\n## 解決策\\n\\n```\\nTeammate lifecycle:\\n  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\\n\\nCommunication:\\n  .team/\\n    config.json           <- team roster + statuses\\n    inbox/\\n      alice.jsonl         <- append-only, drain-on-read\\n      bob.jsonl\\n      lead.jsonl\\n\\n              +--------+    send(\\\"alice\\\",\\\"bob\\\",\\\"...\\\")    +--------+\\n              | alice  | -----------------------------> |  bob   |\\n              | loop   |    bob.jsonl << {json_line}    |  loop  |\\n              +--------+                                +--------+\\n                   ^                                         |\\n                   |        BUS.read_inbox(\\\"alice\\\")          |\\n                   +---- alice.jsonl -> read + drain ---------+\\n```\\n\\n## 仕組み\\n\\n1. TeammateManagerがconfig.jsonでチーム名簿を管理する。\\n\\n```python\\nclass TeammateManager:\\n    def __init__(self, team_dir: Path):\\n        self.dir = team_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self.config_path = self.dir / \\\"config.json\\\"\\n        self.config = self._load_config()\\n        self.threads = {}\\n```\\n\\n2. `spawn()`がチームメイトを作成し、そのエージェントループをスレッドで開始する。\\n\\n```python\\ndef spawn(self, name: str, role: str, prompt: str) -> str:\\n    member = {\\\"name\\\": name, \\\"role\\\": role, \\\"status\\\": \\\"working\\\"}\\n    self.config[\\\"members\\\"].append(member)\\n    self._save_config()\\n    thread = threading.Thread(\\n        target=self._teammate_loop,\\n        args=(name, role, prompt), daemon=True)\\n    thread.start()\\n    return f\\\"Spawned teammate '{name}' (role: {role})\\\"\\n```\\n\\n3. MessageBus: 追記専用のJSONLインボックス。`send()`がJSON行を追記し、`read_inbox()`がすべて読み取ってドレインする。\\n\\n```python\\nclass MessageBus:\\n    def send(self, sender, to, content, msg_type=\\\"message\\\", extra=None):\\n        msg = {\\\"type\\\": msg_type, \\\"from\\\": sender,\\n               \\\"content\\\": content, \\\"timestamp\\\": time.time()}\\n        if extra:\\n            msg.update(extra)\\n        with open(self.dir / f\\\"{to}.jsonl\\\", \\\"a\\\") as f:\\n            f.write(json.dumps(msg) + \\\"\\\\n\\\")\\n\\n    def read_inbox(self, name):\\n        path = self.dir / f\\\"{name}.jsonl\\\"\\n        if not path.exists(): return \\\"[]\\\"\\n        msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l]\\n        path.write_text(\\\"\\\")  # drain\\n        return json.dumps(msgs, indent=2)\\n```\\n\\n4. 各チームメイトは各LLM呼び出しの前にインボックスを確認し、受信メッセージをコンテキストに注入する。\\n\\n```python\\ndef _teammate_loop(self, name, role, prompt):\\n    messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n    for _ in range(50):\\n        inbox = BUS.read_inbox(name)\\n        if inbox != \\\"[]\\\":\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{inbox}</inbox>\\\"})\\n            messages.append({\\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted inbox messages.\\\"})\\n        response = client.messages.create(...)\\n        if response.stop_reason != \\\"tool_use\\\":\\n            break\\n        # execute tools, append results...\\n    self._find_member(name)[\\\"status\\\"] = \\\"idle\\\"\\n```\\n\\n## s08からの変更点\\n\\n| Component      | Before (s08)     | After (s09)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 6                | 9 (+spawn/send/read_inbox) |\\n| Agents         | Single           | Lead + N teammates         |\\n| Persistence    | None             | config.json + JSONL inboxes|\\n| Threads        | Background cmds  | Full agent loops per thread|\\n| Lifecycle      | Fire-and-forget  | idle -> working -> idle    |\\n| Communication  | None             | message + broadcast        |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s09_agent_teams.py\\n```\\n\\n1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.`\\n2. `Broadcast \\\"status update: phase 1 complete\\\" to all teammates`\\n3. `Check the lead inbox for any messages`\\n4. `/team`と入力してステータス付きのチーム名簿を確認する\\n5. `/inbox`と入力してリーダーのインボックスを手動確認する\\n\"\n  },\n  {\n    \"version\": \"s10\",\n    \"locale\": \"ja\",\n    \"title\": \"s10: Team Protocols\",\n    \"content\": \"# s10: Team Protocols\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12`\\n\\n> *\\\"チームメイト間には統一の通信ルールが必要\\\"* -- 1つの request-response パターンが全交渉を駆動。\\n\\n## 問題\\n\\ns09ではチームメイトが作業し通信するが、構造化された協調がない:\\n\\n**シャットダウン**: スレッドを強制終了するとファイルが中途半端に書かれ、config.jsonが不正な状態になる。ハンドシェイクが必要 -- リーダーが要求し、チームメイトが承認(完了して退出)か拒否(作業継続)する。\\n\\n**プラン承認**: リーダーが「認証モジュールをリファクタリングして」と言うと、チームメイトは即座に開始する。リスクの高い変更では、実行前にリーダーが計画をレビューすべきだ。\\n\\n両方とも同じ構造: 一方がユニークIDを持つリクエストを送り、他方がそのIDで応答する。\\n\\n## 解決策\\n\\n```\\nShutdown Protocol            Plan Approval Protocol\\n==================           ======================\\n\\nLead             Teammate    Teammate           Lead\\n  |                 |           |                 |\\n  |--shutdown_req-->|           |--plan_req------>|\\n  | {req_id:\\\"abc\\\"}  |           | {req_id:\\\"xyz\\\"}  |\\n  |                 |           |                 |\\n  |<--shutdown_resp-|           |<--plan_resp-----|\\n  | {req_id:\\\"abc\\\",  |           | {req_id:\\\"xyz\\\",  |\\n  |  approve:true}  |           |  approve:true}  |\\n\\nShared FSM:\\n  [pending] --approve--> [approved]\\n  [pending] --reject---> [rejected]\\n\\nTrackers:\\n  shutdown_requests = {req_id: {target, status}}\\n  plan_requests     = {req_id: {from, plan, status}}\\n```\\n\\n## 仕組み\\n\\n1. リーダーがrequest_idを生成し、インボックス経由でシャットダウンを開始する。\\n\\n```python\\nshutdown_requests = {}\\n\\ndef handle_shutdown_request(teammate: str) -> str:\\n    req_id = str(uuid.uuid4())[:8]\\n    shutdown_requests[req_id] = {\\\"target\\\": teammate, \\\"status\\\": \\\"pending\\\"}\\n    BUS.send(\\\"lead\\\", teammate, \\\"Please shut down gracefully.\\\",\\n             \\\"shutdown_request\\\", {\\\"request_id\\\": req_id})\\n    return f\\\"Shutdown request {req_id} sent (status: pending)\\\"\\n```\\n\\n2. チームメイトがリクエストを受信し、承認または拒否で応答する。\\n\\n```python\\nif tool_name == \\\"shutdown_response\\\":\\n    req_id = args[\\\"request_id\\\"]\\n    approve = args[\\\"approve\\\"]\\n    shutdown_requests[req_id][\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(sender, \\\"lead\\\", args.get(\\\"reason\\\", \\\"\\\"),\\n             \\\"shutdown_response\\\",\\n             {\\\"request_id\\\": req_id, \\\"approve\\\": approve})\\n```\\n\\n3. プラン承認も同一パターン。チームメイトがプランを提出(request_idを生成)、リーダーがレビュー(同じrequest_idを参照)。\\n\\n```python\\nplan_requests = {}\\n\\ndef handle_plan_review(request_id, approve, feedback=\\\"\\\"):\\n    req = plan_requests[request_id]\\n    req[\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(\\\"lead\\\", req[\\\"from\\\"], feedback,\\n             \\\"plan_approval_response\\\",\\n             {\\\"request_id\\\": request_id, \\\"approve\\\": approve})\\n```\\n\\n1つのFSM、2つの応用。同じ`pending -> approved | rejected`状態機械が、あらゆるリクエスト-レスポンスプロトコルに適用できる。\\n\\n## s09からの変更点\\n\\n| Component      | Before (s09)     | After (s10)                  |\\n|----------------|------------------|------------------------------|\\n| Tools          | 9                | 12 (+shutdown_req/resp +plan)|\\n| Shutdown       | Natural exit only| Request-response handshake   |\\n| Plan gating    | None             | Submit/review with approval  |\\n| Correlation    | None             | request_id per request       |\\n| FSM            | None             | pending -> approved/rejected |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s10_team_protocols.py\\n```\\n\\n1. `Spawn alice as a coder. Then request her shutdown.`\\n2. `List teammates to see alice's status after shutdown approval`\\n3. `Spawn bob with a risky refactoring task. Review and reject his plan.`\\n4. `Spawn charlie, have him submit a plan, then approve it.`\\n5. `/team`と入力してステータスを監視する\\n\"\n  },\n  {\n    \"version\": \"s11\",\n    \"locale\": \"ja\",\n    \"title\": \"s11: Autonomous Agents\",\n    \"content\": \"# s11: Autonomous Agents\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12`\\n\\n> *\\\"チームメイトが自らボードを見て、仕事を取る\\\"* -- リーダーが逐一割り振る必要はない。\\n\\n## 問題\\n\\ns09-s10では、チームメイトは明示的に指示された時のみ作業する。リーダーは各チームメイトを特定のプロンプトでspawnしなければならない。タスクボードに未割り当てのタスクが10個あっても、リーダーが手動で各タスクを割り当てる。これはスケールしない。\\n\\n真の自律性とは、チームメイトが自分で作業を見つけること: タスクボードをスキャンし、未確保のタスクを確保し、作業し、完了したら次を探す。\\n\\nもう1つの問題: コンテキスト圧縮(s06)後にエージェントが自分の正体を忘れる可能性がある。アイデンティティ再注入がこれを解決する。\\n\\n## 解決策\\n\\n```\\nTeammate lifecycle with idle cycle:\\n\\n+-------+\\n| spawn |\\n+---+---+\\n    |\\n    v\\n+-------+   tool_use     +-------+\\n| WORK  | <------------- |  LLM  |\\n+---+---+                +-------+\\n    |\\n    | stop_reason != tool_use (or idle tool called)\\n    v\\n+--------+\\n|  IDLE  |  poll every 5s for up to 60s\\n+---+----+\\n    |\\n    +---> check inbox --> message? ----------> WORK\\n    |\\n    +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\\n    |\\n    +---> 60s timeout ----------------------> SHUTDOWN\\n\\nIdentity re-injection after compression:\\n  if len(messages) <= 3:\\n    messages.insert(0, identity_block)\\n```\\n\\n## 仕組み\\n\\n1. チームメイトのループはWORKとIDLEの2フェーズ。LLMがツール呼び出しを止めた時(または`idle`ツールを呼んだ時)、IDLEフェーズに入る。\\n\\n```python\\ndef _loop(self, name, role, prompt):\\n    while True:\\n        # -- WORK PHASE --\\n        messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n        for _ in range(50):\\n            response = client.messages.create(...)\\n            if response.stop_reason != \\\"tool_use\\\":\\n                break\\n            # execute tools...\\n            if idle_requested:\\n                break\\n\\n        # -- IDLE PHASE --\\n        self._set_status(name, \\\"idle\\\")\\n        resume = self._idle_poll(name, messages)\\n        if not resume:\\n            self._set_status(name, \\\"shutdown\\\")\\n            return\\n        self._set_status(name, \\\"working\\\")\\n```\\n\\n2. IDLEフェーズがインボックスとタスクボードをポーリングする。\\n\\n```python\\ndef _idle_poll(self, name, messages):\\n    for _ in range(IDLE_TIMEOUT // POLL_INTERVAL):  # 60s / 5s = 12\\n        time.sleep(POLL_INTERVAL)\\n        inbox = BUS.read_inbox(name)\\n        if inbox:\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{inbox}</inbox>\\\"})\\n            return True\\n        unclaimed = scan_unclaimed_tasks()\\n        if unclaimed:\\n            claim_task(unclaimed[0][\\\"id\\\"], name)\\n            messages.append({\\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<auto-claimed>Task #{unclaimed[0]['id']}: \\\"\\n                           f\\\"{unclaimed[0]['subject']}</auto-claimed>\\\"})\\n            return True\\n    return False  # timeout -> shutdown\\n```\\n\\n3. タスクボードスキャン: pendingかつ未割り当てかつブロックされていないタスクを探す。\\n\\n```python\\ndef scan_unclaimed_tasks() -> list:\\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 not task.get(\\\"blockedBy\\\")):\\n            unclaimed.append(task)\\n    return unclaimed\\n```\\n\\n4. アイデンティティ再注入: コンテキストが短すぎる(圧縮が起きた)場合にアイデンティティブロックを挿入する。\\n\\n```python\\nif len(messages) <= 3:\\n    messages.insert(0, {\\\"role\\\": \\\"user\\\",\\n        \\\"content\\\": f\\\"<identity>You are '{name}', role: {role}, \\\"\\n                   f\\\"team: {team_name}. Continue your work.</identity>\\\"})\\n    messages.insert(1, {\\\"role\\\": \\\"assistant\\\",\\n        \\\"content\\\": f\\\"I am {name}. Continuing.\\\"})\\n```\\n\\n## s10からの変更点\\n\\n| Component      | Before (s10)     | After (s11)                |\\n|----------------|------------------|----------------------------|\\n| Tools          | 12               | 14 (+idle, +claim_task)    |\\n| Autonomy       | Lead-directed    | Self-organizing            |\\n| Idle phase     | None             | Poll inbox + task board    |\\n| Task claiming  | Manual only      | Auto-claim unclaimed tasks |\\n| Identity       | System prompt    | + re-injection after compress|\\n| Timeout        | None             | 60s idle -> auto shutdown  |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s11_autonomous_agents.py\\n```\\n\\n1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.`\\n2. `Spawn a coder teammate and let it find work from the task board itself`\\n3. `Create tasks with dependencies. Watch teammates respect the blocked order.`\\n4. `/tasks`と入力してオーナー付きのタスクボードを確認する\\n5. `/team`と入力して誰が作業中でアイドルかを監視する\\n\"\n  },\n  {\n    \"version\": \"s12\",\n    \"locale\": \"ja\",\n    \"title\": \"s12: Worktree + Task Isolation\",\n    \"content\": \"# s12: Worktree + Task Isolation\\n\\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\\n\\n> *\\\"各自のディレクトリで作業し、互いに干渉しない\\\"* -- タスクは目標を管理、worktree はディレクトリを管理、IDで紐付け。\\n\\n## 問題\\n\\ns11までにエージェントはタスクを自律的に確保して完了できるようになった。しかし全タスクが1つの共有ディレクトリで走る。2つのエージェントが同時に異なるモジュールをリファクタリングすると衝突する: 片方が`config.py`を編集し、もう片方も`config.py`を編集し、未コミットの変更が混ざり合い、どちらもクリーンにロールバックできない。\\n\\nタスクボードは*何をやるか*を追跡するが、*どこでやるか*には関知しない。解決策: 各タスクに専用のgit worktreeディレクトリを与える。タスクが目標を管理し、worktreeが実行コンテキストを管理する。タスクIDで紐付ける。\\n\\n## 解決策\\n\\n```\\nControl plane (.tasks/)             Execution plane (.worktrees/)\\n+------------------+                +------------------------+\\n| task_1.json      |                | auth-refactor/         |\\n|   status: in_progress  <------>   branch: wt/auth-refactor\\n|   worktree: \\\"auth-refactor\\\"   |   task_id: 1             |\\n+------------------+                +------------------------+\\n| task_2.json      |                | ui-login/              |\\n|   status: pending    <------>     branch: wt/ui-login\\n|   worktree: \\\"ui-login\\\"       |   task_id: 2             |\\n+------------------+                +------------------------+\\n                                    |\\n                          index.json (worktree registry)\\n                          events.jsonl (lifecycle log)\\n\\nState machines:\\n  Task:     pending -> in_progress -> completed\\n  Worktree: absent  -> active      -> removed | kept\\n```\\n\\n## 仕組み\\n\\n1. **タスクを作成する。** まず目標を永続化する。\\n\\n```python\\nTASKS.create(\\\"Implement auth refactor\\\")\\n# -> .tasks/task_1.json  status=pending  worktree=\\\"\\\"\\n```\\n\\n2. **worktreeを作成してタスクに紐付ける。** `task_id`を渡すと、タスクが自動的に`in_progress`に遷移する。\\n\\n```python\\nWORKTREES.create(\\\"auth-refactor\\\", task_id=1)\\n# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD\\n# -> index.json gets new entry, task_1.json gets worktree=\\\"auth-refactor\\\"\\n```\\n\\n紐付けは両側に状態を書き込む:\\n\\n```python\\ndef bind_worktree(self, task_id, worktree):\\n    task = self._load(task_id)\\n    task[\\\"worktree\\\"] = worktree\\n    if task[\\\"status\\\"] == \\\"pending\\\":\\n        task[\\\"status\\\"] = \\\"in_progress\\\"\\n    self._save(task)\\n```\\n\\n3. **worktree内でコマンドを実行する。** `cwd`が分離ディレクトリを指す。\\n\\n```python\\nsubprocess.run(command, shell=True, cwd=worktree_path,\\n               capture_output=True, text=True, timeout=300)\\n```\\n\\n4. **終了処理。** 2つの選択肢:\\n   - `worktree_keep(name)` -- ディレクトリを保持する。\\n   - `worktree_remove(name, complete_task=True)` -- ディレクトリを削除し、紐付けられたタスクを完了し、イベントを発行する。1回の呼び出しで後片付けと完了を処理する。\\n\\n```python\\ndef remove(self, name, force=False, complete_task=False):\\n    self._run_git([\\\"worktree\\\", \\\"remove\\\", wt[\\\"path\\\"]])\\n    if complete_task and wt.get(\\\"task_id\\\") is not None:\\n        self.tasks.update(wt[\\\"task_id\\\"], status=\\\"completed\\\")\\n        self.tasks.unbind_worktree(wt[\\\"task_id\\\"])\\n        self.events.emit(\\\"task.completed\\\", ...)\\n```\\n\\n5. **イベントストリーム。** ライフサイクルの各ステップが`.worktrees/events.jsonl`に記録される:\\n\\n```json\\n{\\n  \\\"event\\\": \\\"worktree.remove.after\\\",\\n  \\\"task\\\": {\\\"id\\\": 1, \\\"status\\\": \\\"completed\\\"},\\n  \\\"worktree\\\": {\\\"name\\\": \\\"auth-refactor\\\", \\\"status\\\": \\\"removed\\\"},\\n  \\\"ts\\\": 1730000000\\n}\\n```\\n\\n発行されるイベント: `worktree.create.before/after/failed`, `worktree.remove.before/after/failed`, `worktree.keep`, `task.completed`。\\n\\nクラッシュ後も`.tasks/` + `.worktrees/index.json`から状態を再構築できる。会話メモリは揮発性だが、ファイル状態は永続的だ。\\n\\n## s11からの変更点\\n\\n| Component          | Before (s11)               | After (s12)                                  |\\n|--------------------|----------------------------|----------------------------------------------|\\n| Coordination       | Task board (owner/status)  | Task board + explicit worktree binding       |\\n| Execution scope    | Shared directory           | Task-scoped isolated directory               |\\n| Recoverability     | Task status only           | Task status + worktree index                 |\\n| Teardown           | Task completion            | Task completion + explicit keep/remove       |\\n| Lifecycle visibility | Implicit in logs         | Explicit events in `.worktrees/events.jsonl` |\\n\\n## 試してみる\\n\\n```sh\\ncd learn-claude-code\\npython agents/s12_worktree_task_isolation.py\\n```\\n\\n1. `Create tasks for backend auth and frontend login page, then list tasks.`\\n2. `Create worktree \\\"auth-refactor\\\" for task 1, then bind task 2 to a new worktree \\\"ui-login\\\".`\\n3. `Run \\\"git status --short\\\" in worktree \\\"auth-refactor\\\".`\\n4. `Keep worktree \\\"ui-login\\\", then list worktrees and inspect events.`\\n5. `Remove worktree \\\"auth-refactor\\\" with complete_task=true, then list tasks/worktrees/events.`\\n\"\n  }\n]"
  },
  {
    "path": "web/src/data/generated/versions.json",
    "content": "{\n  \"versions\": [\n    {\n      \"id\": \"s01\",\n      \"filename\": \"s01_agent_loop.py\",\n      \"title\": \"The Agent Loop\",\n      \"subtitle\": \"Bash is All You Need\",\n      \"loc\": 84,\n      \"tools\": [\n        \"bash\"\n      ],\n      \"newTools\": [\n        \"bash\"\n      ],\n      \"coreAddition\": \"Single-tool agent loop\",\n      \"keyInsight\": \"The minimal agent kernel is a while loop + one tool\",\n      \"classes\": [],\n      \"functions\": [\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 53\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 67\n        }\n      ],\n      \"layer\": \"tools\",\n      \"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\\\"\\\"\\\"\\n\\nimport os\\nimport subprocess\\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\\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\\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\\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        # Append assistant turn\\n        messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\n        # If the model didn't call a tool, we're done\\n        if response.stop_reason != \\\"tool_use\\\":\\n            return\\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({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id,\\n                                \\\"content\\\": output})\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\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        response_content = history[-1][\\\"content\\\"]\\n        if isinstance(response_content, list):\\n            for block in response_content:\\n                if hasattr(block, \\\"text\\\"):\\n                    print(block.text)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s02\",\n      \"filename\": \"s02_tool_use.py\",\n      \"title\": \"Tools\",\n      \"subtitle\": \"One Handler Per Tool\",\n      \"loc\": 115,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\"\n      ],\n      \"newTools\": [\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\"\n      ],\n      \"coreAddition\": \"Tool dispatch map\",\n      \"keyInsight\": \"The loop stays the same; new tools register into the dispatch map\",\n      \"classes\": [],\n      \"functions\": [\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 40\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 47\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 60\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 71\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 81\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 113\n        }\n      ],\n      \"layer\": \"tools\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns02_tool_use.py - Tools\\n\\nThe agent loop from s01 didn't change. We just added tools to the array\\nand a dispatch map to route calls.\\n\\n    +----------+      +-------+      +------------------+\\n    |   User   | ---> |  LLM  | ---> | Tool Dispatch    |\\n    |  prompt  |      |       |      | {                |\\n    +----------+      +---+---+      |   bash: run_bash |\\n                          ^          |   read: run_read |\\n                          |          |   write: run_wr  |\\n                          +----------+   edit: run_edit |\\n                          tool_result| }                |\\n                                     +------------------+\\n\\nKey insight: \\\"The loop didn't change at all. I just added tools.\\\"\\n\\\"\\\"\\\"\\n\\nimport os\\nimport subprocess\\nfrom pathlib import Path\\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\\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\\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    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, 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) -> str:\\n    try:\\n        text = safe_path(path).read_text()\\n        lines = text.splitlines()\\n        if limit and limit < len(lines):\\n            lines = lines[:limit] + [f\\\"... ({len(lines) - limit} more lines)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\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\\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\\n    try:\\n        fp = safe_path(path)\\n        content = fp.read_text()\\n        if old_text not in content:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(content.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\n# -- The dispatch map: {tool_name: handler} --\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\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 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 file.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"path\\\": {\\\"type\\\": \\\"string\\\"}, \\\"old_text\\\": {\\\"type\\\": \\\"string\\\"}, \\\"new_text\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"path\\\", \\\"old_text\\\", \\\"new_text\\\"]}},\\n]\\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        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                print(f\\\"> {block.name}: {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    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        print()\\n\"\n    },\n    {\n      \"id\": \"s03\",\n      \"filename\": \"s03_todo_write.py\",\n      \"title\": \"TodoWrite\",\n      \"subtitle\": \"Plan Before You Act\",\n      \"loc\": 171,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"todo\"\n      ],\n      \"newTools\": [\n        \"todo\"\n      ],\n      \"coreAddition\": \"TodoManager + nag reminder\",\n      \"keyInsight\": \"An agent without a plan drifts; list the steps first, then execute\",\n      \"classes\": [\n        {\n          \"name\": \"TodoManager\",\n          \"startLine\": 51,\n          \"endLine\": 87\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 92\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 98\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 110\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 119\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 128\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 163\n        }\n      ],\n      \"layer\": \"planning\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns03_todo_write.py - TodoWrite\\n\\nThe model tracks its own progress via a TodoManager. A nag reminder\\nforces it to keep updating when it forgets.\\n\\n    +----------+      +-------+      +---------+\\n    |   User   | ---> |  LLM  | ---> | Tools   |\\n    |  prompt  |      |       |      | + todo  |\\n    +----------+      +---+---+      +----+----+\\n                          ^               |\\n                          |   tool_result |\\n                          +---------------+\\n                                |\\n                    +-----------+-----------+\\n                    | TodoManager state     |\\n                    | [ ] task A            |\\n                    | [>] task B <- doing   |\\n                    | [x] task C            |\\n                    +-----------------------+\\n                                |\\n                    if rounds_since_todo >= 3:\\n                      inject <reminder>\\n\\nKey insight: \\\"The agent can track its own progress -- and I can see it.\\\"\\n\\\"\\\"\\\"\\n\\nimport os\\nimport subprocess\\nfrom pathlib import Path\\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\\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}.\\nUse the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.\\nPrefer tools over prose.\\\"\\\"\\\"\\n\\n\\n# -- TodoManager: structured state the LLM writes to --\\nclass TodoManager:\\n    def __init__(self):\\n        self.items = []\\n\\n    def update(self, items: list) -> str:\\n        if len(items) > 20:\\n            raise ValueError(\\\"Max 20 todos allowed\\\")\\n        validated = []\\n        in_progress_count = 0\\n        for i, item in enumerate(items):\\n            text = str(item.get(\\\"text\\\", \\\"\\\")).strip()\\n            status = str(item.get(\\\"status\\\", \\\"pending\\\")).lower()\\n            item_id = str(item.get(\\\"id\\\", str(i + 1)))\\n            if not text:\\n                raise ValueError(f\\\"Item {item_id}: text required\\\")\\n            if status not in (\\\"pending\\\", \\\"in_progress\\\", \\\"completed\\\"):\\n                raise ValueError(f\\\"Item {item_id}: invalid status '{status}'\\\")\\n            if status == \\\"in_progress\\\":\\n                in_progress_count += 1\\n            validated.append({\\\"id\\\": item_id, \\\"text\\\": text, \\\"status\\\": status})\\n        if in_progress_count > 1:\\n            raise ValueError(\\\"Only one task can be in_progress at a time\\\")\\n        self.items = validated\\n        return self.render()\\n\\n    def render(self) -> str:\\n        if not self.items:\\n            return \\\"No todos.\\\"\\n        lines = []\\n        for item in self.items:\\n            marker = {\\\"pending\\\": \\\"[ ]\\\", \\\"in_progress\\\": \\\"[>]\\\", \\\"completed\\\": \\\"[x]\\\"}[item[\\\"status\\\"]]\\n            lines.append(f\\\"{marker} #{item['id']}: {item['text']}\\\")\\n        done = sum(1 for t in self.items if t[\\\"status\\\"] == \\\"completed\\\")\\n        lines.append(f\\\"\\\\n({done}/{len(self.items)} completed)\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n\\nTODO = TodoManager()\\n\\n\\n# -- Tool implementations --\\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    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, 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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\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\\\"\\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        fp = safe_path(path)\\n        content = fp.read_text()\\n        if old_text not in content:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(content.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"todo\\\":       lambda **kw: TODO.update(kw[\\\"items\\\"]),\\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 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 file.\\\",\\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\\\": \\\"todo\\\", \\\"description\\\": \\\"Update task list. Track progress on multi-step tasks.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"items\\\": {\\\"type\\\": \\\"array\\\", \\\"items\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"id\\\": {\\\"type\\\": \\\"string\\\"}, \\\"text\\\": {\\\"type\\\": \\\"string\\\"}, \\\"status\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": [\\\"pending\\\", \\\"in_progress\\\", \\\"completed\\\"]}}, \\\"required\\\": [\\\"id\\\", \\\"text\\\", \\\"status\\\"]}}}, \\\"required\\\": [\\\"items\\\"]}},\\n]\\n\\n\\n# -- Agent loop with nag reminder injection --\\ndef agent_loop(messages: list):\\n    rounds_since_todo = 0\\n    while True:\\n        # Nag reminder is injected below, alongside tool results\\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        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        used_todo = False\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id, \\\"content\\\": str(output)})\\n                if block.name == \\\"todo\\\":\\n                    used_todo = True\\n        rounds_since_todo = 0 if used_todo else rounds_since_todo + 1\\n        if rounds_since_todo >= 3:\\n            results.insert(0, {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"<reminder>Update your todos.</reminder>\\\"})\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\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        print()\\n\"\n    },\n    {\n      \"id\": \"s04\",\n      \"filename\": \"s04_subagent.py\",\n      \"title\": \"Subagents\",\n      \"subtitle\": \"Clean Context Per Subtask\",\n      \"loc\": 146,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"task\"\n      ],\n      \"newTools\": [\n        \"task\"\n      ],\n      \"coreAddition\": \"Subagent spawn with isolated messages[]\",\n      \"keyInsight\": \"Subagents use independent messages[], keeping the main conversation clean\",\n      \"classes\": [],\n      \"functions\": [\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 46\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 52\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 64\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 73\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 82\n        },\n        {\n          \"name\": \"run_subagent\",\n          \"signature\": \"def run_subagent(prompt: str)\",\n          \"startLine\": 115\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 143\n        }\n      ],\n      \"layer\": \"planning\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns04_subagent.py - Subagents\\n\\nSpawn a child agent with fresh messages=[]. The child works in its own\\ncontext, sharing the filesystem, then returns only a summary to the parent.\\n\\n    Parent agent                     Subagent\\n    +------------------+             +------------------+\\n    | messages=[...]   |             | messages=[]      |  <-- fresh\\n    |                  |  dispatch   |                  |\\n    | tool: task       | ---------->| while tool_use:  |\\n    |   prompt=\\\"...\\\"   |            |   call tools     |\\n    |   description=\\\"\\\" |            |   append results |\\n    |                  |  summary   |                  |\\n    |   result = \\\"...\\\" | <--------- | return last text |\\n    +------------------+             +------------------+\\n              |\\n    Parent context stays clean.\\n    Subagent context is discarded.\\n\\nKey insight: \\\"Process isolation gives context isolation for free.\\\"\\n\\\"\\\"\\\"\\n\\nimport os\\nimport subprocess\\nfrom pathlib import Path\\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\\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 the task tool to delegate exploration or subtasks.\\\"\\nSUBAGENT_SYSTEM = f\\\"You are a coding subagent at {WORKDIR}. Complete the given task, then summarize your findings.\\\"\\n\\n\\n# -- Tool implementations shared by parent and child --\\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    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, 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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\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\\\"\\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        fp = safe_path(path)\\n        content = fp.read_text()\\n        if old_text not in content:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(content.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n}\\n\\n# Child gets all base tools except task (no recursive spawning)\\nCHILD_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\\\"}, \\\"limit\\\": {\\\"type\\\": \\\"integer\\\"}}, \\\"required\\\": [\\\"path\\\"]}},\\n    {\\\"name\\\": \\\"write_file\\\", \\\"description\\\": \\\"Write content to 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 file.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"path\\\": {\\\"type\\\": \\\"string\\\"}, \\\"old_text\\\": {\\\"type\\\": \\\"string\\\"}, \\\"new_text\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"path\\\", \\\"old_text\\\", \\\"new_text\\\"]}},\\n]\\n\\n\\n# -- Subagent: fresh context, filtered tools, summary-only return --\\ndef run_subagent(prompt: str) -> str:\\n    sub_messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]  # fresh context\\n    for _ in range(30):  # safety limit\\n        response = client.messages.create(\\n            model=MODEL, system=SUBAGENT_SYSTEM, messages=sub_messages,\\n            tools=CHILD_TOOLS, max_tokens=8000,\\n        )\\n        sub_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 = TOOL_HANDLERS.get(block.name)\\n                output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                results.append({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id, \\\"content\\\": str(output)[:50000]})\\n        sub_messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n    # Only the final text returns to the parent -- child context is discarded\\n    return \\\"\\\".join(b.text for b in response.content if hasattr(b, \\\"text\\\")) or \\\"(no summary)\\\"\\n\\n\\n# -- Parent tools: base tools + task dispatcher --\\nPARENT_TOOLS = CHILD_TOOLS + [\\n    {\\\"name\\\": \\\"task\\\", \\\"description\\\": \\\"Spawn a subagent with fresh context. It shares the filesystem but not conversation history.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"prompt\\\": {\\\"type\\\": \\\"string\\\"}, \\\"description\\\": {\\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Short description of the task\\\"}}, \\\"required\\\": [\\\"prompt\\\"]}},\\n]\\n\\n\\ndef agent_loop(messages: list):\\n    while True:\\n        response = client.messages.create(\\n            model=MODEL, system=SYSTEM, messages=messages,\\n            tools=PARENT_TOOLS, max_tokens=8000,\\n        )\\n        messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\n        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                if block.name == \\\"task\\\":\\n                    desc = block.input.get(\\\"description\\\", \\\"subtask\\\")\\n                    print(f\\\"> task ({desc}): {block.input['prompt'][:80]}\\\")\\n                    output = run_subagent(block.input[\\\"prompt\\\"])\\n                else:\\n                    handler = TOOL_HANDLERS.get(block.name)\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                print(f\\\"  {str(output)[:200]}\\\")\\n                results.append({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id, \\\"content\\\": str(output)})\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\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        history.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n        agent_loop(history)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s05\",\n      \"filename\": \"s05_skill_loading.py\",\n      \"title\": \"Skills\",\n      \"subtitle\": \"Load on Demand\",\n      \"loc\": 182,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"load_skill\"\n      ],\n      \"newTools\": [\n        \"load_skill\"\n      ],\n      \"coreAddition\": \"SkillLoader + two-layer injection\",\n      \"keyInsight\": \"Inject knowledge via tool_result when needed, not upfront in the system prompt\",\n      \"classes\": [\n        {\n          \"name\": \"SkillLoader\",\n          \"startLine\": 57,\n          \"endLine\": 105\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 117\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 123\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 135\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 144\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 153\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 187\n        }\n      ],\n      \"layer\": \"planning\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns05_skill_loading.py - Skills\\n\\nTwo-layer skill injection that avoids bloating the system prompt:\\n\\n    Layer 1 (cheap): skill names in system prompt (~100 tokens/skill)\\n    Layer 2 (on demand): full skill body in tool_result\\n\\n    skills/\\n      pdf/\\n        SKILL.md          <-- frontmatter (name, description) + body\\n      code-review/\\n        SKILL.md\\n\\n    System prompt:\\n    +--------------------------------------+\\n    | You are a coding agent.              |\\n    | Skills available:                    |\\n    |   - pdf: Process PDF files...        |  <-- Layer 1: metadata only\\n    |   - code-review: Review code...      |\\n    +--------------------------------------+\\n\\n    When model calls load_skill(\\\"pdf\\\"):\\n    +--------------------------------------+\\n    | tool_result:                         |\\n    | <skill>                              |\\n    |   Full PDF processing instructions   |  <-- Layer 2: full body\\n    |   Step 1: ...                        |\\n    |   Step 2: ...                        |\\n    | </skill>                             |\\n    +--------------------------------------+\\n\\nKey insight: \\\"Don't put everything in the system prompt. Load on demand.\\\"\\n\\\"\\\"\\\"\\n\\nimport os\\nimport re\\nimport subprocess\\nfrom pathlib import Path\\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\\nWORKDIR = Path.cwd()\\nclient = Anthropic(base_url=os.getenv(\\\"ANTHROPIC_BASE_URL\\\"))\\nMODEL = os.environ[\\\"MODEL_ID\\\"]\\nSKILLS_DIR = WORKDIR / \\\"skills\\\"\\n\\n\\n# -- SkillLoader: scan skills/<name>/SKILL.md with YAML frontmatter --\\nclass SkillLoader:\\n    def __init__(self, skills_dir: Path):\\n        self.skills_dir = skills_dir\\n        self.skills = {}\\n        self._load_all()\\n\\n    def _load_all(self):\\n        if not self.skills_dir.exists():\\n            return\\n        for f in sorted(self.skills_dir.rglob(\\\"SKILL.md\\\")):\\n            text = f.read_text()\\n            meta, body = self._parse_frontmatter(text)\\n            name = meta.get(\\\"name\\\", f.parent.name)\\n            self.skills[name] = {\\\"meta\\\": meta, \\\"body\\\": body, \\\"path\\\": str(f)}\\n\\n    def _parse_frontmatter(self, text: str) -> tuple:\\n        \\\"\\\"\\\"Parse YAML frontmatter between --- delimiters.\\\"\\\"\\\"\\n        match = re.match(r\\\"^---\\\\n(.*?)\\\\n---\\\\n(.*)\\\", text, re.DOTALL)\\n        if not match:\\n            return {}, text\\n        meta = {}\\n        for line in match.group(1).strip().splitlines():\\n            if \\\":\\\" in line:\\n                key, val = line.split(\\\":\\\", 1)\\n                meta[key.strip()] = val.strip()\\n        return meta, match.group(2).strip()\\n\\n    def get_descriptions(self) -> str:\\n        \\\"\\\"\\\"Layer 1: short descriptions for the system prompt.\\\"\\\"\\\"\\n        if not self.skills:\\n            return \\\"(no skills available)\\\"\\n        lines = []\\n        for name, skill in self.skills.items():\\n            desc = skill[\\\"meta\\\"].get(\\\"description\\\", \\\"No description\\\")\\n            tags = skill[\\\"meta\\\"].get(\\\"tags\\\", \\\"\\\")\\n            line = f\\\"  - {name}: {desc}\\\"\\n            if tags:\\n                line += f\\\" [{tags}]\\\"\\n            lines.append(line)\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def get_content(self, name: str) -> str:\\n        \\\"\\\"\\\"Layer 2: full skill body returned in tool_result.\\\"\\\"\\\"\\n        skill = self.skills.get(name)\\n        if not skill:\\n            return f\\\"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}\\\"\\n        return f\\\"<skill name=\\\\\\\"{name}\\\\\\\">\\\\n{skill['body']}\\\\n</skill>\\\"\\n\\n\\nSKILL_LOADER = SkillLoader(SKILLS_DIR)\\n\\n# Layer 1: skill metadata injected into system prompt\\nSYSTEM = f\\\"\\\"\\\"You are a coding agent at {WORKDIR}.\\nUse load_skill to access specialized knowledge before tackling unfamiliar topics.\\n\\nSkills available:\\n{SKILL_LOADER.get_descriptions()}\\\"\\\"\\\"\\n\\n\\n# -- Tool implementations --\\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    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, 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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\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\\\"\\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        fp = safe_path(path)\\n        content = fp.read_text()\\n        if old_text not in content:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(content.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"load_skill\\\": lambda **kw: SKILL_LOADER.get_content(kw[\\\"name\\\"]),\\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 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 file.\\\",\\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\\\": \\\"load_skill\\\", \\\"description\\\": \\\"Load specialized knowledge by name.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"name\\\": {\\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Skill name to load\\\"}}, \\\"required\\\": [\\\"name\\\"]}},\\n]\\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        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id, \\\"content\\\": str(output)})\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\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        history.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n        agent_loop(history)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s06\",\n      \"filename\": \"s06_context_compact.py\",\n      \"title\": \"Compact\",\n      \"subtitle\": \"Three-Layer Compression\",\n      \"loc\": 200,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"compact\"\n      ],\n      \"newTools\": [\n        \"compact\"\n      ],\n      \"coreAddition\": \"micro-compact + auto-compact + archival\",\n      \"keyInsight\": \"Context will fill up; three-layer compression strategy enables infinite sessions\",\n      \"classes\": [],\n      \"functions\": [\n        {\n          \"name\": \"estimate_tokens\",\n          \"signature\": \"def estimate_tokens(messages: list)\",\n          \"startLine\": 61\n        },\n        {\n          \"name\": \"micro_compact\",\n          \"signature\": \"def micro_compact(messages: list)\",\n          \"startLine\": 67\n        },\n        {\n          \"name\": \"auto_compact\",\n          \"signature\": \"def auto_compact(messages: list)\",\n          \"startLine\": 97\n        },\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 124\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 130\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 142\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 151\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 160\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 194\n        }\n      ],\n      \"layer\": \"memory\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns06_context_compact.py - Compact\\n\\nThree-layer compression pipeline so the agent can work forever:\\n\\n    Every turn:\\n    +------------------+\\n    | Tool call result |\\n    +------------------+\\n            |\\n            v\\n    [Layer 1: micro_compact]        (silent, every turn)\\n      Replace tool_result content older than last 3\\n      with \\\"[Previous: used {tool_name}]\\\"\\n            |\\n            v\\n    [Check: tokens > 50000?]\\n       |               |\\n       no              yes\\n       |               |\\n       v               v\\n    continue    [Layer 2: auto_compact]\\n                  Save full transcript to .transcripts/\\n                  Ask LLM to summarize conversation.\\n                  Replace all messages with [summary].\\n                        |\\n                        v\\n                [Layer 3: compact tool]\\n                  Model calls compact -> immediate summarization.\\n                  Same as auto, triggered manually.\\n\\nKey insight: \\\"The agent can forget strategically and keep working forever.\\\"\\n\\\"\\\"\\\"\\n\\nimport json\\nimport os\\nimport subprocess\\nimport time\\nfrom pathlib import Path\\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\\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.\\\"\\n\\nTHRESHOLD = 50000\\nTRANSCRIPT_DIR = WORKDIR / \\\".transcripts\\\"\\nKEEP_RECENT = 3\\n\\n\\ndef estimate_tokens(messages: list) -> int:\\n    \\\"\\\"\\\"Rough token count: ~4 chars per token.\\\"\\\"\\\"\\n    return len(str(messages)) // 4\\n\\n\\n# -- Layer 1: micro_compact - replace old tool results with placeholders --\\ndef micro_compact(messages: list) -> list:\\n    # Collect (msg_index, part_index, tool_result_dict) for all tool_result entries\\n    tool_results = []\\n    for msg_idx, msg in enumerate(messages):\\n        if msg[\\\"role\\\"] == \\\"user\\\" and isinstance(msg.get(\\\"content\\\"), list):\\n            for part_idx, part in enumerate(msg[\\\"content\\\"]):\\n                if isinstance(part, dict) and part.get(\\\"type\\\") == \\\"tool_result\\\":\\n                    tool_results.append((msg_idx, part_idx, part))\\n    if len(tool_results) <= KEEP_RECENT:\\n        return messages\\n    # Find tool_name for each result by matching tool_use_id in prior assistant messages\\n    tool_name_map = {}\\n    for msg in messages:\\n        if msg[\\\"role\\\"] == \\\"assistant\\\":\\n            content = msg.get(\\\"content\\\", [])\\n            if isinstance(content, list):\\n                for block in content:\\n                    if hasattr(block, \\\"type\\\") and block.type == \\\"tool_use\\\":\\n                        tool_name_map[block.id] = block.name\\n    # Clear old results (keep last KEEP_RECENT)\\n    to_clear = tool_results[:-KEEP_RECENT]\\n    for _, _, result in to_clear:\\n        if isinstance(result.get(\\\"content\\\"), str) and len(result[\\\"content\\\"]) > 100:\\n            tool_id = result.get(\\\"tool_use_id\\\", \\\"\\\")\\n            tool_name = tool_name_map.get(tool_id, \\\"unknown\\\")\\n            result[\\\"content\\\"] = f\\\"[Previous: used {tool_name}]\\\"\\n    return messages\\n\\n\\n# -- Layer 2: auto_compact - save transcript, summarize, replace messages --\\ndef auto_compact(messages: list) -> list:\\n    # Save full transcript to disk\\n    TRANSCRIPT_DIR.mkdir(exist_ok=True)\\n    transcript_path = TRANSCRIPT_DIR / f\\\"transcript_{int(time.time())}.jsonl\\\"\\n    with open(transcript_path, \\\"w\\\") as f:\\n        for msg in messages:\\n            f.write(json.dumps(msg, default=str) + \\\"\\\\n\\\")\\n    print(f\\\"[transcript saved: {transcript_path}]\\\")\\n    # Ask LLM to summarize\\n    conversation_text = json.dumps(messages, default=str)[:80000]\\n    response = client.messages.create(\\n        model=MODEL,\\n        messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\":\\n            \\\"Summarize this conversation for continuity. Include: \\\"\\n            \\\"1) What was accomplished, 2) Current state, 3) Key decisions made. \\\"\\n            \\\"Be concise but preserve critical details.\\\\n\\\\n\\\" + conversation_text}],\\n        max_tokens=2000,\\n    )\\n    summary = response.content[0].text\\n    # Replace all messages with compressed summary\\n    return [\\n        {\\\"role\\\": \\\"user\\\", \\\"content\\\": f\\\"[Conversation compressed. Transcript: {transcript_path}]\\\\n\\\\n{summary}\\\"},\\n        {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"Understood. I have the context from the summary. Continuing.\\\"},\\n    ]\\n\\n\\n# -- Tool implementations --\\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    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, 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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\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\\\"\\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        fp = safe_path(path)\\n        content = fp.read_text()\\n        if old_text not in content:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(content.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":       lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":  lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":  lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"compact\\\":    lambda **kw: \\\"Manual compression requested.\\\",\\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 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 file.\\\",\\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\\\": \\\"compact\\\", \\\"description\\\": \\\"Trigger manual conversation compression.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"focus\\\": {\\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"What to preserve in the summary\\\"}}}},\\n]\\n\\n\\ndef agent_loop(messages: list):\\n    while True:\\n        # Layer 1: micro_compact before each LLM call\\n        micro_compact(messages)\\n        # Layer 2: auto_compact if token estimate exceeds threshold\\n        if estimate_tokens(messages) > THRESHOLD:\\n            print(\\\"[auto_compact triggered]\\\")\\n            messages[:] = auto_compact(messages)\\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        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        manual_compact = False\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                if block.name == \\\"compact\\\":\\n                    manual_compact = True\\n                    output = \\\"Compressing...\\\"\\n                else:\\n                    handler = TOOL_HANDLERS.get(block.name)\\n                    try:\\n                        output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                    except Exception as e:\\n                        output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id, \\\"content\\\": str(output)})\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n        # Layer 3: manual compact triggered by the compact tool\\n        if manual_compact:\\n            print(\\\"[manual compact]\\\")\\n            messages[:] = auto_compact(messages)\\n\\n\\nif __name__ == \\\"__main__\\\":\\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        history.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n        agent_loop(history)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s07\",\n      \"filename\": \"s07_task_system.py\",\n      \"title\": \"Tasks\",\n      \"subtitle\": \"Task Graph + Dependencies\",\n      \"loc\": 202,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"task_create\",\n        \"task_update\",\n        \"task_list\",\n        \"task_get\"\n      ],\n      \"newTools\": [\n        \"task_create\",\n        \"task_update\",\n        \"task_list\",\n        \"task_get\"\n      ],\n      \"coreAddition\": \"TaskManager with file-based state + dependency graph\",\n      \"keyInsight\": \"A file-based task graph with ordering, parallelism, and dependencies -- the coordination backbone for multi-agent work\",\n      \"classes\": [\n        {\n          \"name\": \"TaskManager\",\n          \"startLine\": 46,\n          \"endLine\": 125\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 130\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 136\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 148\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 157\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 166\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 209\n        }\n      ],\n      \"layer\": \"planning\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns07_task_system.py - Tasks\\n\\nTasks persist as JSON files in .tasks/ so they survive context compression.\\nEach task has a dependency graph (blockedBy/blocks).\\n\\n    .tasks/\\n      task_1.json  {\\\"id\\\":1, \\\"subject\\\":\\\"...\\\", \\\"status\\\":\\\"completed\\\", ...}\\n      task_2.json  {\\\"id\\\":2, \\\"blockedBy\\\":[1], \\\"status\\\":\\\"pending\\\", ...}\\n      task_3.json  {\\\"id\\\":3, \\\"blockedBy\\\":[2], \\\"blocks\\\":[], ...}\\n\\n    Dependency resolution:\\n    +----------+     +----------+     +----------+\\n    | task 1   | --> | task 2   | --> | task 3   |\\n    | complete |     | blocked  |     | blocked  |\\n    +----------+     +----------+     +----------+\\n         |                ^\\n         +--- completing task 1 removes it from task 2's blockedBy\\n\\nKey insight: \\\"State that survives compression -- because it's outside the conversation.\\\"\\n\\\"\\\"\\\"\\n\\nimport json\\nimport os\\nimport subprocess\\nfrom pathlib import Path\\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\\nWORKDIR = Path.cwd()\\nclient = Anthropic(base_url=os.getenv(\\\"ANTHROPIC_BASE_URL\\\"))\\nMODEL = os.environ[\\\"MODEL_ID\\\"]\\nTASKS_DIR = WORKDIR / \\\".tasks\\\"\\n\\nSYSTEM = f\\\"You are a coding agent at {WORKDIR}. Use task tools to plan and track work.\\\"\\n\\n\\n# -- TaskManager: CRUD with dependency graph, persisted as JSON files --\\nclass TaskManager:\\n    def __init__(self, tasks_dir: Path):\\n        self.dir = tasks_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self._next_id = self._max_id() + 1\\n\\n    def _max_id(self) -> int:\\n        ids = [int(f.stem.split(\\\"_\\\")[1]) for f in self.dir.glob(\\\"task_*.json\\\")]\\n        return max(ids) if ids else 0\\n\\n    def _load(self, task_id: int) -> dict:\\n        path = self.dir / f\\\"task_{task_id}.json\\\"\\n        if not path.exists():\\n            raise ValueError(f\\\"Task {task_id} not found\\\")\\n        return json.loads(path.read_text())\\n\\n    def _save(self, task: dict):\\n        path = self.dir / f\\\"task_{task['id']}.json\\\"\\n        path.write_text(json.dumps(task, indent=2))\\n\\n    def create(self, subject: str, description: str = \\\"\\\") -> str:\\n        task = {\\n            \\\"id\\\": self._next_id, \\\"subject\\\": subject, \\\"description\\\": description,\\n            \\\"status\\\": \\\"pending\\\", \\\"blockedBy\\\": [], \\\"blocks\\\": [], \\\"owner\\\": \\\"\\\",\\n        }\\n        self._save(task)\\n        self._next_id += 1\\n        return json.dumps(task, indent=2)\\n\\n    def get(self, task_id: int) -> str:\\n        return json.dumps(self._load(task_id), indent=2)\\n\\n    def update(self, task_id: int, status: str = None,\\n               add_blocked_by: list = None, add_blocks: list = None) -> str:\\n        task = self._load(task_id)\\n        if status:\\n            if status not in (\\\"pending\\\", \\\"in_progress\\\", \\\"completed\\\"):\\n                raise ValueError(f\\\"Invalid status: {status}\\\")\\n            task[\\\"status\\\"] = status\\n            # When a task is completed, remove it from all other tasks' blockedBy\\n            if status == \\\"completed\\\":\\n                self._clear_dependency(task_id)\\n        if add_blocked_by:\\n            task[\\\"blockedBy\\\"] = list(set(task[\\\"blockedBy\\\"] + add_blocked_by))\\n        if add_blocks:\\n            task[\\\"blocks\\\"] = list(set(task[\\\"blocks\\\"] + add_blocks))\\n            # Bidirectional: also update the blocked tasks' blockedBy lists\\n            for blocked_id in add_blocks:\\n                try:\\n                    blocked = self._load(blocked_id)\\n                    if task_id not in blocked[\\\"blockedBy\\\"]:\\n                        blocked[\\\"blockedBy\\\"].append(task_id)\\n                        self._save(blocked)\\n                except ValueError:\\n                    pass\\n        self._save(task)\\n        return json.dumps(task, indent=2)\\n\\n    def _clear_dependency(self, completed_id: int):\\n        \\\"\\\"\\\"Remove completed_id from all other tasks' blockedBy lists.\\\"\\\"\\\"\\n        for f in self.dir.glob(\\\"task_*.json\\\"):\\n            task = json.loads(f.read_text())\\n            if completed_id in task.get(\\\"blockedBy\\\", []):\\n                task[\\\"blockedBy\\\"].remove(completed_id)\\n                self._save(task)\\n\\n    def list_all(self) -> str:\\n        tasks = []\\n        for f in sorted(self.dir.glob(\\\"task_*.json\\\")):\\n            tasks.append(json.loads(f.read_text()))\\n        if not tasks:\\n            return \\\"No tasks.\\\"\\n        lines = []\\n        for t in tasks:\\n            marker = {\\\"pending\\\": \\\"[ ]\\\", \\\"in_progress\\\": \\\"[>]\\\", \\\"completed\\\": \\\"[x]\\\"}.get(t[\\\"status\\\"], \\\"[?]\\\")\\n            blocked = f\\\" (blocked by: {t['blockedBy']})\\\" if t.get(\\\"blockedBy\\\") else \\\"\\\"\\n            lines.append(f\\\"{marker} #{t['id']}: {t['subject']}{blocked}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n\\nTASKS = TaskManager(TASKS_DIR)\\n\\n\\n# -- Base tool implementations --\\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    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, 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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\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\\\"\\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        fp = safe_path(path)\\n        c = fp.read_text()\\n        if old_text not in c:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(c.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":        lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":   lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\":  lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":   lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"task_create\\\": lambda **kw: TASKS.create(kw[\\\"subject\\\"], kw.get(\\\"description\\\", \\\"\\\")),\\n    \\\"task_update\\\": lambda **kw: TASKS.update(kw[\\\"task_id\\\"], kw.get(\\\"status\\\"), kw.get(\\\"addBlockedBy\\\"), kw.get(\\\"addBlocks\\\")),\\n    \\\"task_list\\\":   lambda **kw: TASKS.list_all(),\\n    \\\"task_get\\\":    lambda **kw: TASKS.get(kw[\\\"task_id\\\"]),\\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 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 file.\\\",\\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\\\": \\\"task_create\\\", \\\"description\\\": \\\"Create a new task.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"subject\\\": {\\\"type\\\": \\\"string\\\"}, \\\"description\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"subject\\\"]}},\\n    {\\\"name\\\": \\\"task_update\\\", \\\"description\\\": \\\"Update a task's status or dependencies.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"}, \\\"status\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": [\\\"pending\\\", \\\"in_progress\\\", \\\"completed\\\"]}, \\\"addBlockedBy\\\": {\\\"type\\\": \\\"array\\\", \\\"items\\\": {\\\"type\\\": \\\"integer\\\"}}, \\\"addBlocks\\\": {\\\"type\\\": \\\"array\\\", \\\"items\\\": {\\\"type\\\": \\\"integer\\\"}}}, \\\"required\\\": [\\\"task_id\\\"]}},\\n    {\\\"name\\\": \\\"task_list\\\", \\\"description\\\": \\\"List all tasks with status summary.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"task_get\\\", \\\"description\\\": \\\"Get full details of a task by ID.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"}}, \\\"required\\\": [\\\"task_id\\\"]}},\\n]\\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        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id, \\\"content\\\": str(output)})\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\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        history.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n        agent_loop(history)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s08\",\n      \"filename\": \"s08_background_tasks.py\",\n      \"title\": \"Background Tasks\",\n      \"subtitle\": \"Background Threads + Notifications\",\n      \"loc\": 193,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"background_run\",\n        \"check_background\"\n      ],\n      \"newTools\": [\n        \"background_run\",\n        \"check_background\"\n      ],\n      \"coreAddition\": \"BackgroundManager + notification queue\",\n      \"keyInsight\": \"Run slow operations in the background; the agent keeps thinking ahead\",\n      \"classes\": [\n        {\n          \"name\": \"BackgroundManager\",\n          \"startLine\": 49,\n          \"endLine\": 109\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 114\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 120\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 132\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 141\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 150\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 187\n        }\n      ],\n      \"layer\": \"concurrency\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns08_background_tasks.py - Background Tasks\\n\\nRun commands in background threads. A notification queue is drained\\nbefore each LLM call to deliver results.\\n\\n    Main thread                Background thread\\n    +-----------------+        +-----------------+\\n    | agent loop      |        | task executes   |\\n    | ...             |        | ...             |\\n    | [LLM call] <---+------- | enqueue(result) |\\n    |  ^drain queue   |        +-----------------+\\n    +-----------------+\\n\\n    Timeline:\\n    Agent ----[spawn A]----[spawn B]----[other work]----\\n                 |              |\\n                 v              v\\n              [A runs]      [B runs]        (parallel)\\n                 |              |\\n                 +-- notification queue --> [results injected]\\n\\nKey insight: \\\"Fire and forget -- the agent doesn't block while the command runs.\\\"\\n\\\"\\\"\\\"\\n\\nimport os\\nimport subprocess\\nimport threading\\nimport uuid\\nfrom pathlib import Path\\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\\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 background_run for long-running commands.\\\"\\n\\n\\n# -- BackgroundManager: threaded execution + notification queue --\\nclass BackgroundManager:\\n    def __init__(self):\\n        self.tasks = {}  # task_id -> {status, result, command}\\n        self._notification_queue = []  # completed task results\\n        self._lock = threading.Lock()\\n\\n    def run(self, command: str) -> str:\\n        \\\"\\\"\\\"Start a background thread, return task_id immediately.\\\"\\\"\\\"\\n        task_id = str(uuid.uuid4())[:8]\\n        self.tasks[task_id] = {\\\"status\\\": \\\"running\\\", \\\"result\\\": None, \\\"command\\\": command}\\n        thread = threading.Thread(\\n            target=self._execute, args=(task_id, command), daemon=True\\n        )\\n        thread.start()\\n        return f\\\"Background task {task_id} started: {command[:80]}\\\"\\n\\n    def _execute(self, task_id: str, command: str):\\n        \\\"\\\"\\\"Thread target: run subprocess, capture output, push to queue.\\\"\\\"\\\"\\n        try:\\n            r = subprocess.run(\\n                command, shell=True, cwd=WORKDIR,\\n                capture_output=True, text=True, timeout=300\\n            )\\n            output = (r.stdout + r.stderr).strip()[:50000]\\n            status = \\\"completed\\\"\\n        except subprocess.TimeoutExpired:\\n            output = \\\"Error: Timeout (300s)\\\"\\n            status = \\\"timeout\\\"\\n        except Exception as e:\\n            output = f\\\"Error: {e}\\\"\\n            status = \\\"error\\\"\\n        self.tasks[task_id][\\\"status\\\"] = status\\n        self.tasks[task_id][\\\"result\\\"] = output or \\\"(no output)\\\"\\n        with self._lock:\\n            self._notification_queue.append({\\n                \\\"task_id\\\": task_id,\\n                \\\"status\\\": status,\\n                \\\"command\\\": command[:80],\\n                \\\"result\\\": (output or \\\"(no output)\\\")[:500],\\n            })\\n\\n    def check(self, task_id: str = None) -> str:\\n        \\\"\\\"\\\"Check status of one task or list all.\\\"\\\"\\\"\\n        if task_id:\\n            t = self.tasks.get(task_id)\\n            if not t:\\n                return f\\\"Error: Unknown task {task_id}\\\"\\n            return f\\\"[{t['status']}] {t['command'][:60]}\\\\n{t.get('result') or '(running)'}\\\"\\n        lines = []\\n        for tid, t in self.tasks.items():\\n            lines.append(f\\\"{tid}: [{t['status']}] {t['command'][:60]}\\\")\\n        return \\\"\\\\n\\\".join(lines) if lines else \\\"No background tasks.\\\"\\n\\n    def drain_notifications(self) -> list:\\n        \\\"\\\"\\\"Return and clear all pending completion notifications.\\\"\\\"\\\"\\n        with self._lock:\\n            notifs = list(self._notification_queue)\\n            self._notification_queue.clear()\\n        return notifs\\n\\n\\nBG = BackgroundManager()\\n\\n\\n# -- Tool implementations --\\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    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, 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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\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\\\"\\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        fp = safe_path(path)\\n        c = fp.read_text()\\n        if old_text not in c:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(c.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":             lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":        lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\":       lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":        lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"background_run\\\":   lambda **kw: BG.run(kw[\\\"command\\\"]),\\n    \\\"check_background\\\": lambda **kw: BG.check(kw.get(\\\"task_id\\\")),\\n}\\n\\nTOOLS = [\\n    {\\\"name\\\": \\\"bash\\\", \\\"description\\\": \\\"Run a shell command (blocking).\\\",\\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 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 file.\\\",\\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\\\": \\\"background_run\\\", \\\"description\\\": \\\"Run command in background thread. Returns task_id immediately.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"command\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"command\\\"]}},\\n    {\\\"name\\\": \\\"check_background\\\", \\\"description\\\": \\\"Check background task status. Omit task_id to list all.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"task_id\\\": {\\\"type\\\": \\\"string\\\"}}}},\\n]\\n\\n\\ndef agent_loop(messages: list):\\n    while True:\\n        # Drain background notifications and inject as system message before LLM call\\n        notifs = BG.drain_notifications()\\n        if notifs and messages:\\n            notif_text = \\\"\\\\n\\\".join(\\n                f\\\"[bg:{n['task_id']}] {n['status']}: {n['result']}\\\" for n in notifs\\n            )\\n            messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": f\\\"<background-results>\\\\n{notif_text}\\\\n</background-results>\\\"})\\n            messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"Noted background results.\\\"})\\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        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\\"type\\\": \\\"tool_result\\\", \\\"tool_use_id\\\": block.id, \\\"content\\\": str(output)})\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    history = []\\n    while True:\\n        try:\\n            query = input(\\\"\\\\033[36ms08 >> \\\\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()\\n\"\n    },\n    {\n      \"id\": \"s09\",\n      \"filename\": \"s09_agent_teams.py\",\n      \"title\": \"Agent Teams\",\n      \"subtitle\": \"Teammates + Mailboxes\",\n      \"loc\": 343,\n      \"tools\": [\n        \"alice\",\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"send_message\",\n        \"read_inbox\",\n        \"spawn_teammate\",\n        \"list_teammates\",\n        \"broadcast\"\n      ],\n      \"newTools\": [\n        \"alice\",\n        \"send_message\",\n        \"read_inbox\",\n        \"spawn_teammate\",\n        \"list_teammates\",\n        \"broadcast\"\n      ],\n      \"coreAddition\": \"TeammateManager + file-based mailbox\",\n      \"keyInsight\": \"When one agent can't finish, delegate to persistent teammates via async mailboxes\",\n      \"classes\": [\n        {\n          \"name\": \"MessageBus\",\n          \"startLine\": 77,\n          \"endLine\": 118\n        },\n        {\n          \"name\": \"TeammateManager\",\n          \"startLine\": 123,\n          \"endLine\": 249\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"_safe_path\",\n          \"signature\": \"def _safe_path(p: str)\",\n          \"startLine\": 254\n        },\n        {\n          \"name\": \"_run_bash\",\n          \"signature\": \"def _run_bash(command: str)\",\n          \"startLine\": 261\n        },\n        {\n          \"name\": \"_run_read\",\n          \"signature\": \"def _run_read(path: str, limit: int = None)\",\n          \"startLine\": 276\n        },\n        {\n          \"name\": \"_run_write\",\n          \"signature\": \"def _run_write(path: str, content: str)\",\n          \"startLine\": 286\n        },\n        {\n          \"name\": \"_run_edit\",\n          \"signature\": \"def _run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 296\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 344\n        }\n      ],\n      \"layer\": \"collaboration\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns09_agent_teams.py - Agent Teams\\n\\nPersistent named agents with file-based JSONL inboxes. Each teammate runs\\nits own agent loop in a separate thread. Communication via append-only inboxes.\\n\\n    Subagent (s04):  spawn -> execute -> return summary -> destroyed\\n    Teammate (s09):  spawn -> work -> idle -> work -> ... -> shutdown\\n\\n    .team/config.json                   .team/inbox/\\n    +----------------------------+      +------------------+\\n    | {\\\"team_name\\\": \\\"default\\\",   |      | alice.jsonl      |\\n    |  \\\"members\\\": [              |      | bob.jsonl        |\\n    |    {\\\"name\\\":\\\"alice\\\",        |      | lead.jsonl       |\\n    |     \\\"role\\\":\\\"coder\\\",        |      +------------------+\\n    |     \\\"status\\\":\\\"idle\\\"}       |\\n    |  ]}                        |      send_message(\\\"alice\\\", \\\"fix bug\\\"):\\n    +----------------------------+        open(\\\"alice.jsonl\\\", \\\"a\\\").write(msg)\\n\\n                                        read_inbox(\\\"alice\\\"):\\n    spawn_teammate(\\\"alice\\\",\\\"coder\\\",...)   msgs = [json.loads(l) for l in ...]\\n         |                                open(\\\"alice.jsonl\\\", \\\"w\\\").close()\\n         v                                return msgs  # drain\\n    Thread: alice             Thread: bob\\n    +------------------+      +------------------+\\n    | agent_loop       |      | agent_loop       |\\n    | status: working  |      | status: idle     |\\n    | ... runs tools   |      | ... waits ...    |\\n    | status -> idle   |      |                  |\\n    +------------------+      +------------------+\\n\\n    5 message types (all declared, not all handled here):\\n    +-------------------------+-----------------------------------+\\n    | message                 | Normal text message               |\\n    | broadcast               | Sent to all teammates             |\\n    | shutdown_request        | Request graceful shutdown (s10)   |\\n    | shutdown_response       | Approve/reject shutdown (s10)     |\\n    | plan_approval_response  | Approve/reject plan (s10)         |\\n    +-------------------------+-----------------------------------+\\n\\nKey insight: \\\"Teammates that can talk to each other.\\\"\\n\\\"\\\"\\\"\\n\\nimport json\\nimport os\\nimport subprocess\\nimport threading\\nimport time\\nfrom pathlib import Path\\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\\\"]\\nTEAM_DIR = WORKDIR / \\\".team\\\"\\nINBOX_DIR = TEAM_DIR / \\\"inbox\\\"\\n\\nSYSTEM = f\\\"You are a team lead at {WORKDIR}. Spawn teammates and communicate via inboxes.\\\"\\n\\nVALID_MSG_TYPES = {\\n    \\\"message\\\",\\n    \\\"broadcast\\\",\\n    \\\"shutdown_request\\\",\\n    \\\"shutdown_response\\\",\\n    \\\"plan_approval_response\\\",\\n}\\n\\n\\n# -- MessageBus: JSONL inbox per teammate --\\nclass MessageBus:\\n    def __init__(self, inbox_dir: Path):\\n        self.dir = inbox_dir\\n        self.dir.mkdir(parents=True, exist_ok=True)\\n\\n    def send(self, sender: str, to: str, content: str,\\n             msg_type: str = \\\"message\\\", extra: dict = None) -> str:\\n        if msg_type not in VALID_MSG_TYPES:\\n            return f\\\"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}\\\"\\n        msg = {\\n            \\\"type\\\": msg_type,\\n            \\\"from\\\": sender,\\n            \\\"content\\\": content,\\n            \\\"timestamp\\\": time.time(),\\n        }\\n        if extra:\\n            msg.update(extra)\\n        inbox_path = self.dir / f\\\"{to}.jsonl\\\"\\n        with open(inbox_path, \\\"a\\\") as f:\\n            f.write(json.dumps(msg) + \\\"\\\\n\\\")\\n        return f\\\"Sent {msg_type} to {to}\\\"\\n\\n    def read_inbox(self, name: str) -> list:\\n        inbox_path = self.dir / f\\\"{name}.jsonl\\\"\\n        if not inbox_path.exists():\\n            return []\\n        messages = []\\n        for line in inbox_path.read_text().strip().splitlines():\\n            if line:\\n                messages.append(json.loads(line))\\n        inbox_path.write_text(\\\"\\\")\\n        return messages\\n\\n    def broadcast(self, sender: str, content: str, teammates: list) -> str:\\n        count = 0\\n        for name in teammates:\\n            if name != sender:\\n                self.send(sender, name, content, \\\"broadcast\\\")\\n                count += 1\\n        return f\\\"Broadcast to {count} teammates\\\"\\n\\n\\nBUS = MessageBus(INBOX_DIR)\\n\\n\\n# -- TeammateManager: persistent named agents with config.json --\\nclass TeammateManager:\\n    def __init__(self, team_dir: Path):\\n        self.dir = team_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self.config_path = self.dir / \\\"config.json\\\"\\n        self.config = self._load_config()\\n        self.threads = {}\\n\\n    def _load_config(self) -> dict:\\n        if self.config_path.exists():\\n            return json.loads(self.config_path.read_text())\\n        return {\\\"team_name\\\": \\\"default\\\", \\\"members\\\": []}\\n\\n    def _save_config(self):\\n        self.config_path.write_text(json.dumps(self.config, indent=2))\\n\\n    def _find_member(self, name: str) -> dict:\\n        for m in self.config[\\\"members\\\"]:\\n            if m[\\\"name\\\"] == name:\\n                return m\\n        return None\\n\\n    def spawn(self, name: str, role: str, prompt: str) -> str:\\n        member = self._find_member(name)\\n        if member:\\n            if member[\\\"status\\\"] not in (\\\"idle\\\", \\\"shutdown\\\"):\\n                return f\\\"Error: '{name}' is currently {member['status']}\\\"\\n            member[\\\"status\\\"] = \\\"working\\\"\\n            member[\\\"role\\\"] = role\\n        else:\\n            member = {\\\"name\\\": name, \\\"role\\\": role, \\\"status\\\": \\\"working\\\"}\\n            self.config[\\\"members\\\"].append(member)\\n        self._save_config()\\n        thread = threading.Thread(\\n            target=self._teammate_loop,\\n            args=(name, role, prompt),\\n            daemon=True,\\n        )\\n        self.threads[name] = thread\\n        thread.start()\\n        return f\\\"Spawned '{name}' (role: {role})\\\"\\n\\n    def _teammate_loop(self, name: str, role: str, prompt: str):\\n        sys_prompt = (\\n            f\\\"You are '{name}', role: {role}, at {WORKDIR}. \\\"\\n            f\\\"Use send_message to communicate. Complete your task.\\\"\\n        )\\n        messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n        tools = self._teammate_tools()\\n        for _ in range(50):\\n            inbox = BUS.read_inbox(name)\\n            for msg in inbox:\\n                messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": json.dumps(msg)})\\n            try:\\n                response = client.messages.create(\\n                    model=MODEL,\\n                    system=sys_prompt,\\n                    messages=messages,\\n                    tools=tools,\\n                    max_tokens=8000,\\n                )\\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                    output = self._exec(name, block.name, block.input)\\n                    print(f\\\"  [{name}] {block.name}: {str(output)[:120]}\\\")\\n                    results.append({\\n                        \\\"type\\\": \\\"tool_result\\\",\\n                        \\\"tool_use_id\\\": block.id,\\n                        \\\"content\\\": str(output),\\n                    })\\n            messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n        member = self._find_member(name)\\n        if member and member[\\\"status\\\"] != \\\"shutdown\\\":\\n            member[\\\"status\\\"] = \\\"idle\\\"\\n            self._save_config()\\n\\n    def _exec(self, sender: str, tool_name: str, args: dict) -> str:\\n        # these base tools are unchanged from s02\\n        if tool_name == \\\"bash\\\":\\n            return _run_bash(args[\\\"command\\\"])\\n        if tool_name == \\\"read_file\\\":\\n            return _run_read(args[\\\"path\\\"])\\n        if tool_name == \\\"write_file\\\":\\n            return _run_write(args[\\\"path\\\"], args[\\\"content\\\"])\\n        if tool_name == \\\"edit_file\\\":\\n            return _run_edit(args[\\\"path\\\"], args[\\\"old_text\\\"], args[\\\"new_text\\\"])\\n        if tool_name == \\\"send_message\\\":\\n            return BUS.send(sender, args[\\\"to\\\"], args[\\\"content\\\"], args.get(\\\"msg_type\\\", \\\"message\\\"))\\n        if tool_name == \\\"read_inbox\\\":\\n            return json.dumps(BUS.read_inbox(sender), indent=2)\\n        return f\\\"Unknown tool: {tool_name}\\\"\\n\\n    def _teammate_tools(self) -> list:\\n        # these base tools are unchanged from s02\\n        return [\\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 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 file.\\\",\\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\\\": \\\"send_message\\\", \\\"description\\\": \\\"Send message to a teammate.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"to\\\": {\\\"type\\\": \\\"string\\\"}, \\\"content\\\": {\\\"type\\\": \\\"string\\\"}, \\\"msg_type\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": list(VALID_MSG_TYPES)}}, \\\"required\\\": [\\\"to\\\", \\\"content\\\"]}},\\n            {\\\"name\\\": \\\"read_inbox\\\", \\\"description\\\": \\\"Read and drain your inbox.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n        ]\\n\\n    def list_all(self) -> str:\\n        if not self.config[\\\"members\\\"]:\\n            return \\\"No teammates.\\\"\\n        lines = [f\\\"Team: {self.config['team_name']}\\\"]\\n        for m in self.config[\\\"members\\\"]:\\n            lines.append(f\\\"  {m['name']} ({m['role']}): {m['status']}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def member_names(self) -> list:\\n        return [m[\\\"name\\\"] for m in self.config[\\\"members\\\"]]\\n\\n\\nTEAM = TeammateManager(TEAM_DIR)\\n\\n\\n# -- Base tool implementations (these base tools are unchanged from s02) --\\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    dangerous = [\\\"rm -rf /\\\", \\\"sudo\\\", \\\"shutdown\\\", \\\"reboot\\\"]\\n    if any(d in command for d in dangerous):\\n        return \\\"Error: Dangerous command blocked\\\"\\n    try:\\n        r = subprocess.run(\\n            command, shell=True, cwd=WORKDIR,\\n            capture_output=True, text=True, timeout=120,\\n        )\\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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\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\\\"\\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        fp = _safe_path(path)\\n        c = fp.read_text()\\n        if old_text not in c:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(c.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\n# -- Lead tool dispatch (9 tools) --\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":            lambda **kw: _run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":       lambda **kw: _run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\":      lambda **kw: _run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":       lambda **kw: _run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"spawn_teammate\\\":  lambda **kw: TEAM.spawn(kw[\\\"name\\\"], kw[\\\"role\\\"], kw[\\\"prompt\\\"]),\\n    \\\"list_teammates\\\":  lambda **kw: TEAM.list_all(),\\n    \\\"send_message\\\":    lambda **kw: BUS.send(\\\"lead\\\", kw[\\\"to\\\"], kw[\\\"content\\\"], kw.get(\\\"msg_type\\\", \\\"message\\\")),\\n    \\\"read_inbox\\\":      lambda **kw: json.dumps(BUS.read_inbox(\\\"lead\\\"), indent=2),\\n    \\\"broadcast\\\":       lambda **kw: BUS.broadcast(\\\"lead\\\", kw[\\\"content\\\"], TEAM.member_names()),\\n}\\n\\n# these base tools are unchanged from s02\\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 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 file.\\\",\\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\\\": \\\"spawn_teammate\\\", \\\"description\\\": \\\"Spawn a persistent teammate that runs in its own thread.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"name\\\": {\\\"type\\\": \\\"string\\\"}, \\\"role\\\": {\\\"type\\\": \\\"string\\\"}, \\\"prompt\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"name\\\", \\\"role\\\", \\\"prompt\\\"]}},\\n    {\\\"name\\\": \\\"list_teammates\\\", \\\"description\\\": \\\"List all teammates with name, role, status.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"send_message\\\", \\\"description\\\": \\\"Send a message to a teammate's inbox.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"to\\\": {\\\"type\\\": \\\"string\\\"}, \\\"content\\\": {\\\"type\\\": \\\"string\\\"}, \\\"msg_type\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": list(VALID_MSG_TYPES)}}, \\\"required\\\": [\\\"to\\\", \\\"content\\\"]}},\\n    {\\\"name\\\": \\\"read_inbox\\\", \\\"description\\\": \\\"Read and drain the lead's inbox.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"broadcast\\\", \\\"description\\\": \\\"Send a message to all teammates.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"content\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"content\\\"]}},\\n]\\n\\n\\ndef agent_loop(messages: list):\\n    while True:\\n        inbox = BUS.read_inbox(\\\"lead\\\")\\n        if inbox:\\n            messages.append({\\n                \\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{json.dumps(inbox, indent=2)}</inbox>\\\",\\n            })\\n            messages.append({\\n                \\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted inbox messages.\\\",\\n            })\\n        response = client.messages.create(\\n            model=MODEL,\\n            system=SYSTEM,\\n            messages=messages,\\n            tools=TOOLS,\\n            max_tokens=8000,\\n        )\\n        messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\n        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\n                    \\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": str(output),\\n                })\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    history = []\\n    while True:\\n        try:\\n            query = input(\\\"\\\\033[36ms09 >> \\\\033[0m\\\")\\n        except (EOFError, KeyboardInterrupt):\\n            break\\n        if query.strip().lower() in (\\\"q\\\", \\\"exit\\\", \\\"\\\"):\\n            break\\n        if query.strip() == \\\"/team\\\":\\n            print(TEAM.list_all())\\n            continue\\n        if query.strip() == \\\"/inbox\\\":\\n            print(json.dumps(BUS.read_inbox(\\\"lead\\\"), indent=2))\\n            continue\\n        history.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n        agent_loop(history)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s10\",\n      \"filename\": \"s10_team_protocols.py\",\n      \"title\": \"Team Protocols\",\n      \"subtitle\": \"Shared Communication Rules\",\n      \"loc\": 414,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"send_message\",\n        \"read_inbox\",\n        \"shutdown_response\",\n        \"plan_approval\",\n        \"spawn_teammate\",\n        \"list_teammates\",\n        \"broadcast\",\n        \"shutdown_request\"\n      ],\n      \"newTools\": [\n        \"shutdown_response\",\n        \"plan_approval\",\n        \"shutdown_request\"\n      ],\n      \"coreAddition\": \"request_id correlation for two protocols\",\n      \"keyInsight\": \"One request-response pattern drives all team negotiation\",\n      \"classes\": [\n        {\n          \"name\": \"MessageBus\",\n          \"startLine\": 87,\n          \"endLine\": 128\n        },\n        {\n          \"name\": \"TeammateManager\",\n          \"startLine\": 133,\n          \"endLine\": 290\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"_safe_path\",\n          \"signature\": \"def _safe_path(p: str)\",\n          \"startLine\": 295\n        },\n        {\n          \"name\": \"_run_bash\",\n          \"signature\": \"def _run_bash(command: str)\",\n          \"startLine\": 302\n        },\n        {\n          \"name\": \"_run_read\",\n          \"signature\": \"def _run_read(path: str, limit: int = None)\",\n          \"startLine\": 317\n        },\n        {\n          \"name\": \"_run_write\",\n          \"signature\": \"def _run_write(path: str, content: str)\",\n          \"startLine\": 327\n        },\n        {\n          \"name\": \"_run_edit\",\n          \"signature\": \"def _run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 337\n        },\n        {\n          \"name\": \"handle_shutdown_request\",\n          \"signature\": \"def handle_shutdown_request(teammate: str)\",\n          \"startLine\": 350\n        },\n        {\n          \"name\": \"handle_plan_review\",\n          \"signature\": \"def handle_plan_review(request_id: str, approve: bool, feedback: str = \\\"\\\")\",\n          \"startLine\": 361\n        },\n        {\n          \"name\": \"_check_shutdown_status\",\n          \"signature\": \"def _check_shutdown_status(request_id: str)\",\n          \"startLine\": 375\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 425\n        }\n      ],\n      \"layer\": \"collaboration\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns10_team_protocols.py - Team Protocols\\n\\nShutdown protocol and plan approval protocol, both using the same\\nrequest_id correlation pattern. Builds on s09's team messaging.\\n\\n    Shutdown FSM: pending -> approved | rejected\\n\\n    Lead                              Teammate\\n    +---------------------+          +---------------------+\\n    | shutdown_request     |          |                     |\\n    | {                    | -------> | receives request    |\\n    |   request_id: abc    |          | decides: approve?   |\\n    | }                    |          |                     |\\n    +---------------------+          +---------------------+\\n                                             |\\n    +---------------------+          +-------v-------------+\\n    | shutdown_response    | <------- | shutdown_response   |\\n    | {                    |          | {                   |\\n    |   request_id: abc    |          |   request_id: abc   |\\n    |   approve: true      |          |   approve: true     |\\n    | }                    |          | }                   |\\n    +---------------------+          +---------------------+\\n            |\\n            v\\n    status -> \\\"shutdown\\\", thread stops\\n\\n    Plan approval FSM: pending -> approved | rejected\\n\\n    Teammate                          Lead\\n    +---------------------+          +---------------------+\\n    | plan_approval        |          |                     |\\n    | submit: {plan:\\\"...\\\"}| -------> | reviews plan text   |\\n    +---------------------+          | approve/reject?     |\\n                                     +---------------------+\\n                                             |\\n    +---------------------+          +-------v-------------+\\n    | plan_approval_resp   | <------- | plan_approval       |\\n    | {approve: true}      |          | review: {req_id,    |\\n    +---------------------+          |   approve: true}     |\\n                                     +---------------------+\\n\\n    Trackers: {request_id: {\\\"target|from\\\": name, \\\"status\\\": \\\"pending|...\\\"}}\\n\\nKey insight: \\\"Same request_id correlation pattern, two domains.\\\"\\n\\\"\\\"\\\"\\n\\nimport json\\nimport os\\nimport subprocess\\nimport threading\\nimport time\\nimport uuid\\nfrom pathlib import Path\\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\\\"]\\nTEAM_DIR = WORKDIR / \\\".team\\\"\\nINBOX_DIR = TEAM_DIR / \\\"inbox\\\"\\n\\nSYSTEM = f\\\"You are a team lead at {WORKDIR}. Manage teammates with shutdown and plan approval protocols.\\\"\\n\\nVALID_MSG_TYPES = {\\n    \\\"message\\\",\\n    \\\"broadcast\\\",\\n    \\\"shutdown_request\\\",\\n    \\\"shutdown_response\\\",\\n    \\\"plan_approval_response\\\",\\n}\\n\\n# -- Request trackers: correlate by request_id --\\nshutdown_requests = {}\\nplan_requests = {}\\n_tracker_lock = threading.Lock()\\n\\n\\n# -- MessageBus: JSONL inbox per teammate --\\nclass MessageBus:\\n    def __init__(self, inbox_dir: Path):\\n        self.dir = inbox_dir\\n        self.dir.mkdir(parents=True, exist_ok=True)\\n\\n    def send(self, sender: str, to: str, content: str,\\n             msg_type: str = \\\"message\\\", extra: dict = None) -> str:\\n        if msg_type not in VALID_MSG_TYPES:\\n            return f\\\"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}\\\"\\n        msg = {\\n            \\\"type\\\": msg_type,\\n            \\\"from\\\": sender,\\n            \\\"content\\\": content,\\n            \\\"timestamp\\\": time.time(),\\n        }\\n        if extra:\\n            msg.update(extra)\\n        inbox_path = self.dir / f\\\"{to}.jsonl\\\"\\n        with open(inbox_path, \\\"a\\\") as f:\\n            f.write(json.dumps(msg) + \\\"\\\\n\\\")\\n        return f\\\"Sent {msg_type} to {to}\\\"\\n\\n    def read_inbox(self, name: str) -> list:\\n        inbox_path = self.dir / f\\\"{name}.jsonl\\\"\\n        if not inbox_path.exists():\\n            return []\\n        messages = []\\n        for line in inbox_path.read_text().strip().splitlines():\\n            if line:\\n                messages.append(json.loads(line))\\n        inbox_path.write_text(\\\"\\\")\\n        return messages\\n\\n    def broadcast(self, sender: str, content: str, teammates: list) -> str:\\n        count = 0\\n        for name in teammates:\\n            if name != sender:\\n                self.send(sender, name, content, \\\"broadcast\\\")\\n                count += 1\\n        return f\\\"Broadcast to {count} teammates\\\"\\n\\n\\nBUS = MessageBus(INBOX_DIR)\\n\\n\\n# -- TeammateManager with shutdown + plan approval --\\nclass TeammateManager:\\n    def __init__(self, team_dir: Path):\\n        self.dir = team_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self.config_path = self.dir / \\\"config.json\\\"\\n        self.config = self._load_config()\\n        self.threads = {}\\n\\n    def _load_config(self) -> dict:\\n        if self.config_path.exists():\\n            return json.loads(self.config_path.read_text())\\n        return {\\\"team_name\\\": \\\"default\\\", \\\"members\\\": []}\\n\\n    def _save_config(self):\\n        self.config_path.write_text(json.dumps(self.config, indent=2))\\n\\n    def _find_member(self, name: str) -> dict:\\n        for m in self.config[\\\"members\\\"]:\\n            if m[\\\"name\\\"] == name:\\n                return m\\n        return None\\n\\n    def spawn(self, name: str, role: str, prompt: str) -> str:\\n        member = self._find_member(name)\\n        if member:\\n            if member[\\\"status\\\"] not in (\\\"idle\\\", \\\"shutdown\\\"):\\n                return f\\\"Error: '{name}' is currently {member['status']}\\\"\\n            member[\\\"status\\\"] = \\\"working\\\"\\n            member[\\\"role\\\"] = role\\n        else:\\n            member = {\\\"name\\\": name, \\\"role\\\": role, \\\"status\\\": \\\"working\\\"}\\n            self.config[\\\"members\\\"].append(member)\\n        self._save_config()\\n        thread = threading.Thread(\\n            target=self._teammate_loop,\\n            args=(name, role, prompt),\\n            daemon=True,\\n        )\\n        self.threads[name] = thread\\n        thread.start()\\n        return f\\\"Spawned '{name}' (role: {role})\\\"\\n\\n    def _teammate_loop(self, name: str, role: str, prompt: str):\\n        sys_prompt = (\\n            f\\\"You are '{name}', role: {role}, at {WORKDIR}. \\\"\\n            f\\\"Submit plans via plan_approval before major work. \\\"\\n            f\\\"Respond to shutdown_request with shutdown_response.\\\"\\n        )\\n        messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n        tools = self._teammate_tools()\\n        should_exit = False\\n        for _ in range(50):\\n            inbox = BUS.read_inbox(name)\\n            for msg in inbox:\\n                messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": json.dumps(msg)})\\n            if should_exit:\\n                break\\n            try:\\n                response = client.messages.create(\\n                    model=MODEL,\\n                    system=sys_prompt,\\n                    messages=messages,\\n                    tools=tools,\\n                    max_tokens=8000,\\n                )\\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                    output = self._exec(name, block.name, block.input)\\n                    print(f\\\"  [{name}] {block.name}: {str(output)[:120]}\\\")\\n                    results.append({\\n                        \\\"type\\\": \\\"tool_result\\\",\\n                        \\\"tool_use_id\\\": block.id,\\n                        \\\"content\\\": str(output),\\n                    })\\n                    if block.name == \\\"shutdown_response\\\" and block.input.get(\\\"approve\\\"):\\n                        should_exit = True\\n            messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n        member = self._find_member(name)\\n        if member:\\n            member[\\\"status\\\"] = \\\"shutdown\\\" if should_exit else \\\"idle\\\"\\n            self._save_config()\\n\\n    def _exec(self, sender: str, tool_name: str, args: dict) -> str:\\n        # these base tools are unchanged from s02\\n        if tool_name == \\\"bash\\\":\\n            return _run_bash(args[\\\"command\\\"])\\n        if tool_name == \\\"read_file\\\":\\n            return _run_read(args[\\\"path\\\"])\\n        if tool_name == \\\"write_file\\\":\\n            return _run_write(args[\\\"path\\\"], args[\\\"content\\\"])\\n        if tool_name == \\\"edit_file\\\":\\n            return _run_edit(args[\\\"path\\\"], args[\\\"old_text\\\"], args[\\\"new_text\\\"])\\n        if tool_name == \\\"send_message\\\":\\n            return BUS.send(sender, args[\\\"to\\\"], args[\\\"content\\\"], args.get(\\\"msg_type\\\", \\\"message\\\"))\\n        if tool_name == \\\"read_inbox\\\":\\n            return json.dumps(BUS.read_inbox(sender), indent=2)\\n        if tool_name == \\\"shutdown_response\\\":\\n            req_id = args[\\\"request_id\\\"]\\n            approve = args[\\\"approve\\\"]\\n            with _tracker_lock:\\n                if req_id in shutdown_requests:\\n                    shutdown_requests[req_id][\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n            BUS.send(\\n                sender, \\\"lead\\\", args.get(\\\"reason\\\", \\\"\\\"),\\n                \\\"shutdown_response\\\", {\\\"request_id\\\": req_id, \\\"approve\\\": approve},\\n            )\\n            return f\\\"Shutdown {'approved' if approve else 'rejected'}\\\"\\n        if tool_name == \\\"plan_approval\\\":\\n            plan_text = args.get(\\\"plan\\\", \\\"\\\")\\n            req_id = str(uuid.uuid4())[:8]\\n            with _tracker_lock:\\n                plan_requests[req_id] = {\\\"from\\\": sender, \\\"plan\\\": plan_text, \\\"status\\\": \\\"pending\\\"}\\n            BUS.send(\\n                sender, \\\"lead\\\", plan_text, \\\"plan_approval_response\\\",\\n                {\\\"request_id\\\": req_id, \\\"plan\\\": plan_text},\\n            )\\n            return f\\\"Plan submitted (request_id={req_id}). Waiting for lead approval.\\\"\\n        return f\\\"Unknown tool: {tool_name}\\\"\\n\\n    def _teammate_tools(self) -> list:\\n        # these base tools are unchanged from s02\\n        return [\\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 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 file.\\\",\\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\\\": \\\"send_message\\\", \\\"description\\\": \\\"Send message to a teammate.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"to\\\": {\\\"type\\\": \\\"string\\\"}, \\\"content\\\": {\\\"type\\\": \\\"string\\\"}, \\\"msg_type\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": list(VALID_MSG_TYPES)}}, \\\"required\\\": [\\\"to\\\", \\\"content\\\"]}},\\n            {\\\"name\\\": \\\"read_inbox\\\", \\\"description\\\": \\\"Read and drain your inbox.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n            {\\\"name\\\": \\\"shutdown_response\\\", \\\"description\\\": \\\"Respond to a shutdown request. Approve to shut down, reject to keep working.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"request_id\\\": {\\\"type\\\": \\\"string\\\"}, \\\"approve\\\": {\\\"type\\\": \\\"boolean\\\"}, \\\"reason\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"request_id\\\", \\\"approve\\\"]}},\\n            {\\\"name\\\": \\\"plan_approval\\\", \\\"description\\\": \\\"Submit a plan for lead approval. Provide plan text.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"plan\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"plan\\\"]}},\\n        ]\\n\\n    def list_all(self) -> str:\\n        if not self.config[\\\"members\\\"]:\\n            return \\\"No teammates.\\\"\\n        lines = [f\\\"Team: {self.config['team_name']}\\\"]\\n        for m in self.config[\\\"members\\\"]:\\n            lines.append(f\\\"  {m['name']} ({m['role']}): {m['status']}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def member_names(self) -> list:\\n        return [m[\\\"name\\\"] for m in self.config[\\\"members\\\"]]\\n\\n\\nTEAM = TeammateManager(TEAM_DIR)\\n\\n\\n# -- Base tool implementations (these base tools are unchanged from s02) --\\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    dangerous = [\\\"rm -rf /\\\", \\\"sudo\\\", \\\"shutdown\\\", \\\"reboot\\\"]\\n    if any(d in command for d in dangerous):\\n        return \\\"Error: Dangerous command blocked\\\"\\n    try:\\n        r = subprocess.run(\\n            command, shell=True, cwd=WORKDIR,\\n            capture_output=True, text=True, timeout=120,\\n        )\\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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\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\\\"\\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        fp = _safe_path(path)\\n        c = fp.read_text()\\n        if old_text not in c:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(c.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\n# -- Lead-specific protocol handlers --\\ndef handle_shutdown_request(teammate: str) -> str:\\n    req_id = str(uuid.uuid4())[:8]\\n    with _tracker_lock:\\n        shutdown_requests[req_id] = {\\\"target\\\": teammate, \\\"status\\\": \\\"pending\\\"}\\n    BUS.send(\\n        \\\"lead\\\", teammate, \\\"Please shut down gracefully.\\\",\\n        \\\"shutdown_request\\\", {\\\"request_id\\\": req_id},\\n    )\\n    return f\\\"Shutdown request {req_id} sent to '{teammate}' (status: pending)\\\"\\n\\n\\ndef handle_plan_review(request_id: str, approve: bool, feedback: str = \\\"\\\") -> str:\\n    with _tracker_lock:\\n        req = plan_requests.get(request_id)\\n    if not req:\\n        return f\\\"Error: Unknown plan request_id '{request_id}'\\\"\\n    with _tracker_lock:\\n        req[\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(\\n        \\\"lead\\\", req[\\\"from\\\"], feedback, \\\"plan_approval_response\\\",\\n        {\\\"request_id\\\": request_id, \\\"approve\\\": approve, \\\"feedback\\\": feedback},\\n    )\\n    return f\\\"Plan {req['status']} for '{req['from']}'\\\"\\n\\n\\ndef _check_shutdown_status(request_id: str) -> str:\\n    with _tracker_lock:\\n        return json.dumps(shutdown_requests.get(request_id, {\\\"error\\\": \\\"not found\\\"}))\\n\\n\\n# -- Lead tool dispatch (12 tools) --\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":              lambda **kw: _run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":         lambda **kw: _run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\":        lambda **kw: _run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":         lambda **kw: _run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"spawn_teammate\\\":    lambda **kw: TEAM.spawn(kw[\\\"name\\\"], kw[\\\"role\\\"], kw[\\\"prompt\\\"]),\\n    \\\"list_teammates\\\":    lambda **kw: TEAM.list_all(),\\n    \\\"send_message\\\":      lambda **kw: BUS.send(\\\"lead\\\", kw[\\\"to\\\"], kw[\\\"content\\\"], kw.get(\\\"msg_type\\\", \\\"message\\\")),\\n    \\\"read_inbox\\\":        lambda **kw: json.dumps(BUS.read_inbox(\\\"lead\\\"), indent=2),\\n    \\\"broadcast\\\":         lambda **kw: BUS.broadcast(\\\"lead\\\", kw[\\\"content\\\"], TEAM.member_names()),\\n    \\\"shutdown_request\\\":  lambda **kw: handle_shutdown_request(kw[\\\"teammate\\\"]),\\n    \\\"shutdown_response\\\": lambda **kw: _check_shutdown_status(kw.get(\\\"request_id\\\", \\\"\\\")),\\n    \\\"plan_approval\\\":     lambda **kw: handle_plan_review(kw[\\\"request_id\\\"], kw[\\\"approve\\\"], kw.get(\\\"feedback\\\", \\\"\\\")),\\n}\\n\\n# these base tools are unchanged from s02\\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 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 file.\\\",\\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\\\": \\\"spawn_teammate\\\", \\\"description\\\": \\\"Spawn a persistent teammate.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"name\\\": {\\\"type\\\": \\\"string\\\"}, \\\"role\\\": {\\\"type\\\": \\\"string\\\"}, \\\"prompt\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"name\\\", \\\"role\\\", \\\"prompt\\\"]}},\\n    {\\\"name\\\": \\\"list_teammates\\\", \\\"description\\\": \\\"List all teammates.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"send_message\\\", \\\"description\\\": \\\"Send a message to a teammate.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"to\\\": {\\\"type\\\": \\\"string\\\"}, \\\"content\\\": {\\\"type\\\": \\\"string\\\"}, \\\"msg_type\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": list(VALID_MSG_TYPES)}}, \\\"required\\\": [\\\"to\\\", \\\"content\\\"]}},\\n    {\\\"name\\\": \\\"read_inbox\\\", \\\"description\\\": \\\"Read and drain the lead's inbox.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"broadcast\\\", \\\"description\\\": \\\"Send a message to all teammates.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"content\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"content\\\"]}},\\n    {\\\"name\\\": \\\"shutdown_request\\\", \\\"description\\\": \\\"Request a teammate to shut down gracefully. Returns a request_id for tracking.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"teammate\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"teammate\\\"]}},\\n    {\\\"name\\\": \\\"shutdown_response\\\", \\\"description\\\": \\\"Check the status of a shutdown request by request_id.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"request_id\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"request_id\\\"]}},\\n    {\\\"name\\\": \\\"plan_approval\\\", \\\"description\\\": \\\"Approve or reject a teammate's plan. Provide request_id + approve + optional feedback.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"request_id\\\": {\\\"type\\\": \\\"string\\\"}, \\\"approve\\\": {\\\"type\\\": \\\"boolean\\\"}, \\\"feedback\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"request_id\\\", \\\"approve\\\"]}},\\n]\\n\\n\\ndef agent_loop(messages: list):\\n    while True:\\n        inbox = BUS.read_inbox(\\\"lead\\\")\\n        if inbox:\\n            messages.append({\\n                \\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{json.dumps(inbox, indent=2)}</inbox>\\\",\\n            })\\n            messages.append({\\n                \\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted inbox messages.\\\",\\n            })\\n        response = client.messages.create(\\n            model=MODEL,\\n            system=SYSTEM,\\n            messages=messages,\\n            tools=TOOLS,\\n            max_tokens=8000,\\n        )\\n        messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\n        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\n                    \\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": str(output),\\n                })\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    history = []\\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        if query.strip() == \\\"/team\\\":\\n            print(TEAM.list_all())\\n            continue\\n        if query.strip() == \\\"/inbox\\\":\\n            print(json.dumps(BUS.read_inbox(\\\"lead\\\"), indent=2))\\n            continue\\n        history.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n        agent_loop(history)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s11\",\n      \"filename\": \"s11_autonomous_agents.py\",\n      \"title\": \"Autonomous Agents\",\n      \"subtitle\": \"Scan Board, Claim Tasks\",\n      \"loc\": 494,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"send_message\",\n        \"read_inbox\",\n        \"shutdown_response\",\n        \"plan_approval\",\n        \"idle\",\n        \"claim_task\",\n        \"spawn_teammate\",\n        \"list_teammates\",\n        \"broadcast\",\n        \"shutdown_request\"\n      ],\n      \"newTools\": [\n        \"idle\",\n        \"claim_task\"\n      ],\n      \"coreAddition\": \"Task board polling + timeout-based self-governance\",\n      \"keyInsight\": \"Teammates scan the board and claim tasks themselves; no need for the lead to assign each one\",\n      \"classes\": [\n        {\n          \"name\": \"MessageBus\",\n          \"startLine\": 80,\n          \"endLine\": 121\n        },\n        {\n          \"name\": \"TeammateManager\",\n          \"startLine\": 159,\n          \"endLine\": 368\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"scan_unclaimed_tasks\",\n          \"signature\": \"def scan_unclaimed_tasks()\",\n          \"startLine\": 126\n        },\n        {\n          \"name\": \"claim_task\",\n          \"signature\": \"def claim_task(task_id: int, owner: str)\",\n          \"startLine\": 138\n        },\n        {\n          \"name\": \"make_identity_block\",\n          \"signature\": \"def make_identity_block(name: str, role: str, team_name: str)\",\n          \"startLine\": 151\n        },\n        {\n          \"name\": \"_safe_path\",\n          \"signature\": \"def _safe_path(p: str)\",\n          \"startLine\": 373\n        },\n        {\n          \"name\": \"_run_bash\",\n          \"signature\": \"def _run_bash(command: str)\",\n          \"startLine\": 380\n        },\n        {\n          \"name\": \"_run_read\",\n          \"signature\": \"def _run_read(path: str, limit: int = None)\",\n          \"startLine\": 395\n        },\n        {\n          \"name\": \"_run_write\",\n          \"signature\": \"def _run_write(path: str, content: str)\",\n          \"startLine\": 405\n        },\n        {\n          \"name\": \"_run_edit\",\n          \"signature\": \"def _run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 415\n        },\n        {\n          \"name\": \"handle_shutdown_request\",\n          \"signature\": \"def handle_shutdown_request(teammate: str)\",\n          \"startLine\": 428\n        },\n        {\n          \"name\": \"handle_plan_review\",\n          \"signature\": \"def handle_plan_review(request_id: str, approve: bool, feedback: str = \\\"\\\")\",\n          \"startLine\": 439\n        },\n        {\n          \"name\": \"_check_shutdown_status\",\n          \"signature\": \"def _check_shutdown_status(request_id: str)\",\n          \"startLine\": 453\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 509\n        }\n      ],\n      \"layer\": \"collaboration\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns11_autonomous_agents.py - Autonomous Agents\\n\\nIdle cycle with task board polling, auto-claiming unclaimed tasks, and\\nidentity re-injection after context compression. Builds on s10's protocols.\\n\\n    Teammate lifecycle:\\n    +-------+\\n    | spawn |\\n    +---+---+\\n        |\\n        v\\n    +-------+  tool_use    +-------+\\n    | WORK  | <----------- |  LLM  |\\n    +---+---+              +-------+\\n        |\\n        | stop_reason != tool_use\\n        v\\n    +--------+\\n    | IDLE   | poll every 5s for up to 60s\\n    +---+----+\\n        |\\n        +---> check inbox -> message? -> resume WORK\\n        |\\n        +---> scan .tasks/ -> unclaimed? -> claim -> resume WORK\\n        |\\n        +---> timeout (60s) -> shutdown\\n\\n    Identity re-injection after compression:\\n    messages = [identity_block, ...remaining...]\\n    \\\"You are 'coder', role: backend, team: my-team\\\"\\n\\nKey insight: \\\"The agent finds work itself.\\\"\\n\\\"\\\"\\\"\\n\\nimport json\\nimport os\\nimport subprocess\\nimport threading\\nimport time\\nimport uuid\\nfrom pathlib import Path\\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\\\"]\\nTEAM_DIR = WORKDIR / \\\".team\\\"\\nINBOX_DIR = TEAM_DIR / \\\"inbox\\\"\\nTASKS_DIR = WORKDIR / \\\".tasks\\\"\\n\\nPOLL_INTERVAL = 5\\nIDLE_TIMEOUT = 60\\n\\nSYSTEM = f\\\"You are a team lead at {WORKDIR}. Teammates are autonomous -- they find work themselves.\\\"\\n\\nVALID_MSG_TYPES = {\\n    \\\"message\\\",\\n    \\\"broadcast\\\",\\n    \\\"shutdown_request\\\",\\n    \\\"shutdown_response\\\",\\n    \\\"plan_approval_response\\\",\\n}\\n\\n# -- Request trackers --\\nshutdown_requests = {}\\nplan_requests = {}\\n_tracker_lock = threading.Lock()\\n_claim_lock = threading.Lock()\\n\\n\\n# -- MessageBus: JSONL inbox per teammate --\\nclass MessageBus:\\n    def __init__(self, inbox_dir: Path):\\n        self.dir = inbox_dir\\n        self.dir.mkdir(parents=True, exist_ok=True)\\n\\n    def send(self, sender: str, to: str, content: str,\\n             msg_type: str = \\\"message\\\", extra: dict = None) -> str:\\n        if msg_type not in VALID_MSG_TYPES:\\n            return f\\\"Error: Invalid type '{msg_type}'. Valid: {VALID_MSG_TYPES}\\\"\\n        msg = {\\n            \\\"type\\\": msg_type,\\n            \\\"from\\\": sender,\\n            \\\"content\\\": content,\\n            \\\"timestamp\\\": time.time(),\\n        }\\n        if extra:\\n            msg.update(extra)\\n        inbox_path = self.dir / f\\\"{to}.jsonl\\\"\\n        with open(inbox_path, \\\"a\\\") as f:\\n            f.write(json.dumps(msg) + \\\"\\\\n\\\")\\n        return f\\\"Sent {msg_type} to {to}\\\"\\n\\n    def read_inbox(self, name: str) -> list:\\n        inbox_path = self.dir / f\\\"{name}.jsonl\\\"\\n        if not inbox_path.exists():\\n            return []\\n        messages = []\\n        for line in inbox_path.read_text().strip().splitlines():\\n            if line:\\n                messages.append(json.loads(line))\\n        inbox_path.write_text(\\\"\\\")\\n        return messages\\n\\n    def broadcast(self, sender: str, content: str, teammates: list) -> str:\\n        count = 0\\n        for name in teammates:\\n            if name != sender:\\n                self.send(sender, name, content, \\\"broadcast\\\")\\n                count += 1\\n        return f\\\"Broadcast to {count} teammates\\\"\\n\\n\\nBUS = MessageBus(INBOX_DIR)\\n\\n\\n# -- Task board scanning --\\ndef scan_unclaimed_tasks() -> list:\\n    TASKS_DIR.mkdir(exist_ok=True)\\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 not task.get(\\\"blockedBy\\\")):\\n            unclaimed.append(task)\\n    return unclaimed\\n\\n\\ndef claim_task(task_id: int, owner: str) -> str:\\n    with _claim_lock:\\n        path = TASKS_DIR / f\\\"task_{task_id}.json\\\"\\n        if not path.exists():\\n            return f\\\"Error: Task {task_id} not found\\\"\\n        task = json.loads(path.read_text())\\n        task[\\\"owner\\\"] = owner\\n        task[\\\"status\\\"] = \\\"in_progress\\\"\\n        path.write_text(json.dumps(task, indent=2))\\n    return f\\\"Claimed task #{task_id} for {owner}\\\"\\n\\n\\n# -- Identity re-injection after compression --\\ndef make_identity_block(name: str, role: str, team_name: str) -> dict:\\n    return {\\n        \\\"role\\\": \\\"user\\\",\\n        \\\"content\\\": f\\\"<identity>You are '{name}', role: {role}, team: {team_name}. Continue your work.</identity>\\\",\\n    }\\n\\n\\n# -- Autonomous TeammateManager --\\nclass TeammateManager:\\n    def __init__(self, team_dir: Path):\\n        self.dir = team_dir\\n        self.dir.mkdir(exist_ok=True)\\n        self.config_path = self.dir / \\\"config.json\\\"\\n        self.config = self._load_config()\\n        self.threads = {}\\n\\n    def _load_config(self) -> dict:\\n        if self.config_path.exists():\\n            return json.loads(self.config_path.read_text())\\n        return {\\\"team_name\\\": \\\"default\\\", \\\"members\\\": []}\\n\\n    def _save_config(self):\\n        self.config_path.write_text(json.dumps(self.config, indent=2))\\n\\n    def _find_member(self, name: str) -> dict:\\n        for m in self.config[\\\"members\\\"]:\\n            if m[\\\"name\\\"] == name:\\n                return m\\n        return None\\n\\n    def _set_status(self, name: str, status: str):\\n        member = self._find_member(name)\\n        if member:\\n            member[\\\"status\\\"] = status\\n            self._save_config()\\n\\n    def spawn(self, name: str, role: str, prompt: str) -> str:\\n        member = self._find_member(name)\\n        if member:\\n            if member[\\\"status\\\"] not in (\\\"idle\\\", \\\"shutdown\\\"):\\n                return f\\\"Error: '{name}' is currently {member['status']}\\\"\\n            member[\\\"status\\\"] = \\\"working\\\"\\n            member[\\\"role\\\"] = role\\n        else:\\n            member = {\\\"name\\\": name, \\\"role\\\": role, \\\"status\\\": \\\"working\\\"}\\n            self.config[\\\"members\\\"].append(member)\\n        self._save_config()\\n        thread = threading.Thread(\\n            target=self._loop,\\n            args=(name, role, prompt),\\n            daemon=True,\\n        )\\n        self.threads[name] = thread\\n        thread.start()\\n        return f\\\"Spawned '{name}' (role: {role})\\\"\\n\\n    def _loop(self, name: str, role: str, prompt: str):\\n        team_name = self.config[\\\"team_name\\\"]\\n        sys_prompt = (\\n            f\\\"You are '{name}', role: {role}, team: {team_name}, at {WORKDIR}. \\\"\\n            f\\\"Use idle tool when you have no more work. You will auto-claim new tasks.\\\"\\n        )\\n        messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n        tools = self._teammate_tools()\\n\\n        while True:\\n            # -- WORK PHASE: standard agent loop --\\n            for _ in range(50):\\n                inbox = BUS.read_inbox(name)\\n                for msg in inbox:\\n                    if msg.get(\\\"type\\\") == \\\"shutdown_request\\\":\\n                        self._set_status(name, \\\"shutdown\\\")\\n                        return\\n                    messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": json.dumps(msg)})\\n                try:\\n                    response = client.messages.create(\\n                        model=MODEL,\\n                        system=sys_prompt,\\n                        messages=messages,\\n                        tools=tools,\\n                        max_tokens=8000,\\n                    )\\n                except Exception:\\n                    self._set_status(name, \\\"idle\\\")\\n                    return\\n                messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\n                if response.stop_reason != \\\"tool_use\\\":\\n                    break\\n                results = []\\n                idle_requested = False\\n                for block in response.content:\\n                    if block.type == \\\"tool_use\\\":\\n                        if block.name == \\\"idle\\\":\\n                            idle_requested = True\\n                            output = \\\"Entering idle phase. Will poll for new tasks.\\\"\\n                        else:\\n                            output = self._exec(name, block.name, block.input)\\n                        print(f\\\"  [{name}] {block.name}: {str(output)[:120]}\\\")\\n                        results.append({\\n                            \\\"type\\\": \\\"tool_result\\\",\\n                            \\\"tool_use_id\\\": block.id,\\n                            \\\"content\\\": str(output),\\n                        })\\n                messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n                if idle_requested:\\n                    break\\n\\n            # -- IDLE PHASE: poll for inbox messages and unclaimed tasks --\\n            self._set_status(name, \\\"idle\\\")\\n            resume = False\\n            polls = IDLE_TIMEOUT // max(POLL_INTERVAL, 1)\\n            for _ in range(polls):\\n                time.sleep(POLL_INTERVAL)\\n                inbox = BUS.read_inbox(name)\\n                if inbox:\\n                    for msg in inbox:\\n                        if msg.get(\\\"type\\\") == \\\"shutdown_request\\\":\\n                            self._set_status(name, \\\"shutdown\\\")\\n                            return\\n                        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": json.dumps(msg)})\\n                    resume = True\\n                    break\\n                unclaimed = scan_unclaimed_tasks()\\n                if unclaimed:\\n                    task = unclaimed[0]\\n                    claim_task(task[\\\"id\\\"], name)\\n                    task_prompt = (\\n                        f\\\"<auto-claimed>Task #{task['id']}: {task['subject']}\\\\n\\\"\\n                        f\\\"{task.get('description', '')}</auto-claimed>\\\"\\n                    )\\n                    if len(messages) <= 3:\\n                        messages.insert(0, make_identity_block(name, role, team_name))\\n                        messages.insert(1, {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": f\\\"I am {name}. Continuing.\\\"})\\n                    messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": task_prompt})\\n                    messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": f\\\"Claimed task #{task['id']}. Working on it.\\\"})\\n                    resume = True\\n                    break\\n\\n            if not resume:\\n                self._set_status(name, \\\"shutdown\\\")\\n                return\\n            self._set_status(name, \\\"working\\\")\\n\\n    def _exec(self, sender: str, tool_name: str, args: dict) -> str:\\n        # these base tools are unchanged from s02\\n        if tool_name == \\\"bash\\\":\\n            return _run_bash(args[\\\"command\\\"])\\n        if tool_name == \\\"read_file\\\":\\n            return _run_read(args[\\\"path\\\"])\\n        if tool_name == \\\"write_file\\\":\\n            return _run_write(args[\\\"path\\\"], args[\\\"content\\\"])\\n        if tool_name == \\\"edit_file\\\":\\n            return _run_edit(args[\\\"path\\\"], args[\\\"old_text\\\"], args[\\\"new_text\\\"])\\n        if tool_name == \\\"send_message\\\":\\n            return BUS.send(sender, args[\\\"to\\\"], args[\\\"content\\\"], args.get(\\\"msg_type\\\", \\\"message\\\"))\\n        if tool_name == \\\"read_inbox\\\":\\n            return json.dumps(BUS.read_inbox(sender), indent=2)\\n        if tool_name == \\\"shutdown_response\\\":\\n            req_id = args[\\\"request_id\\\"]\\n            with _tracker_lock:\\n                if req_id in shutdown_requests:\\n                    shutdown_requests[req_id][\\\"status\\\"] = \\\"approved\\\" if args[\\\"approve\\\"] else \\\"rejected\\\"\\n            BUS.send(\\n                sender, \\\"lead\\\", args.get(\\\"reason\\\", \\\"\\\"),\\n                \\\"shutdown_response\\\", {\\\"request_id\\\": req_id, \\\"approve\\\": args[\\\"approve\\\"]},\\n            )\\n            return f\\\"Shutdown {'approved' if args['approve'] else 'rejected'}\\\"\\n        if tool_name == \\\"plan_approval\\\":\\n            plan_text = args.get(\\\"plan\\\", \\\"\\\")\\n            req_id = str(uuid.uuid4())[:8]\\n            with _tracker_lock:\\n                plan_requests[req_id] = {\\\"from\\\": sender, \\\"plan\\\": plan_text, \\\"status\\\": \\\"pending\\\"}\\n            BUS.send(\\n                sender, \\\"lead\\\", plan_text, \\\"plan_approval_response\\\",\\n                {\\\"request_id\\\": req_id, \\\"plan\\\": plan_text},\\n            )\\n            return f\\\"Plan submitted (request_id={req_id}). Waiting for approval.\\\"\\n        if tool_name == \\\"claim_task\\\":\\n            return claim_task(args[\\\"task_id\\\"], sender)\\n        return f\\\"Unknown tool: {tool_name}\\\"\\n\\n    def _teammate_tools(self) -> list:\\n        # these base tools are unchanged from s02\\n        return [\\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 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 file.\\\",\\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\\\": \\\"send_message\\\", \\\"description\\\": \\\"Send message to a teammate.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"to\\\": {\\\"type\\\": \\\"string\\\"}, \\\"content\\\": {\\\"type\\\": \\\"string\\\"}, \\\"msg_type\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": list(VALID_MSG_TYPES)}}, \\\"required\\\": [\\\"to\\\", \\\"content\\\"]}},\\n            {\\\"name\\\": \\\"read_inbox\\\", \\\"description\\\": \\\"Read and drain your inbox.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n            {\\\"name\\\": \\\"shutdown_response\\\", \\\"description\\\": \\\"Respond to a shutdown request.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"request_id\\\": {\\\"type\\\": \\\"string\\\"}, \\\"approve\\\": {\\\"type\\\": \\\"boolean\\\"}, \\\"reason\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"request_id\\\", \\\"approve\\\"]}},\\n            {\\\"name\\\": \\\"plan_approval\\\", \\\"description\\\": \\\"Submit a plan for lead approval.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"plan\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"plan\\\"]}},\\n            {\\\"name\\\": \\\"idle\\\", \\\"description\\\": \\\"Signal that you have no more work. Enters idle polling phase.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n            {\\\"name\\\": \\\"claim_task\\\", \\\"description\\\": \\\"Claim a task from the task board by ID.\\\",\\n             \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"}}, \\\"required\\\": [\\\"task_id\\\"]}},\\n        ]\\n\\n    def list_all(self) -> str:\\n        if not self.config[\\\"members\\\"]:\\n            return \\\"No teammates.\\\"\\n        lines = [f\\\"Team: {self.config['team_name']}\\\"]\\n        for m in self.config[\\\"members\\\"]:\\n            lines.append(f\\\"  {m['name']} ({m['role']}): {m['status']}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def member_names(self) -> list:\\n        return [m[\\\"name\\\"] for m in self.config[\\\"members\\\"]]\\n\\n\\nTEAM = TeammateManager(TEAM_DIR)\\n\\n\\n# -- Base tool implementations (these base tools are unchanged from s02) --\\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    dangerous = [\\\"rm -rf /\\\", \\\"sudo\\\", \\\"shutdown\\\", \\\"reboot\\\"]\\n    if any(d in command for d in dangerous):\\n        return \\\"Error: Dangerous command blocked\\\"\\n    try:\\n        r = subprocess.run(\\n            command, shell=True, cwd=WORKDIR,\\n            capture_output=True, text=True, timeout=120,\\n        )\\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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\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\\\"\\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        fp = _safe_path(path)\\n        c = fp.read_text()\\n        if old_text not in c:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(c.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\n# -- Lead-specific protocol handlers --\\ndef handle_shutdown_request(teammate: str) -> str:\\n    req_id = str(uuid.uuid4())[:8]\\n    with _tracker_lock:\\n        shutdown_requests[req_id] = {\\\"target\\\": teammate, \\\"status\\\": \\\"pending\\\"}\\n    BUS.send(\\n        \\\"lead\\\", teammate, \\\"Please shut down gracefully.\\\",\\n        \\\"shutdown_request\\\", {\\\"request_id\\\": req_id},\\n    )\\n    return f\\\"Shutdown request {req_id} sent to '{teammate}'\\\"\\n\\n\\ndef handle_plan_review(request_id: str, approve: bool, feedback: str = \\\"\\\") -> str:\\n    with _tracker_lock:\\n        req = plan_requests.get(request_id)\\n    if not req:\\n        return f\\\"Error: Unknown plan request_id '{request_id}'\\\"\\n    with _tracker_lock:\\n        req[\\\"status\\\"] = \\\"approved\\\" if approve else \\\"rejected\\\"\\n    BUS.send(\\n        \\\"lead\\\", req[\\\"from\\\"], feedback, \\\"plan_approval_response\\\",\\n        {\\\"request_id\\\": request_id, \\\"approve\\\": approve, \\\"feedback\\\": feedback},\\n    )\\n    return f\\\"Plan {req['status']} for '{req['from']}'\\\"\\n\\n\\ndef _check_shutdown_status(request_id: str) -> str:\\n    with _tracker_lock:\\n        return json.dumps(shutdown_requests.get(request_id, {\\\"error\\\": \\\"not found\\\"}))\\n\\n\\n# -- Lead tool dispatch (14 tools) --\\nTOOL_HANDLERS = {\\n    \\\"bash\\\":              lambda **kw: _run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\":         lambda **kw: _run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\":        lambda **kw: _run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\":         lambda **kw: _run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"spawn_teammate\\\":    lambda **kw: TEAM.spawn(kw[\\\"name\\\"], kw[\\\"role\\\"], kw[\\\"prompt\\\"]),\\n    \\\"list_teammates\\\":    lambda **kw: TEAM.list_all(),\\n    \\\"send_message\\\":      lambda **kw: BUS.send(\\\"lead\\\", kw[\\\"to\\\"], kw[\\\"content\\\"], kw.get(\\\"msg_type\\\", \\\"message\\\")),\\n    \\\"read_inbox\\\":        lambda **kw: json.dumps(BUS.read_inbox(\\\"lead\\\"), indent=2),\\n    \\\"broadcast\\\":         lambda **kw: BUS.broadcast(\\\"lead\\\", kw[\\\"content\\\"], TEAM.member_names()),\\n    \\\"shutdown_request\\\":  lambda **kw: handle_shutdown_request(kw[\\\"teammate\\\"]),\\n    \\\"shutdown_response\\\": lambda **kw: _check_shutdown_status(kw.get(\\\"request_id\\\", \\\"\\\")),\\n    \\\"plan_approval\\\":     lambda **kw: handle_plan_review(kw[\\\"request_id\\\"], kw[\\\"approve\\\"], kw.get(\\\"feedback\\\", \\\"\\\")),\\n    \\\"idle\\\":              lambda **kw: \\\"Lead does not idle.\\\",\\n    \\\"claim_task\\\":        lambda **kw: claim_task(kw[\\\"task_id\\\"], \\\"lead\\\"),\\n}\\n\\n# these base tools are unchanged from s02\\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 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 file.\\\",\\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\\\": \\\"spawn_teammate\\\", \\\"description\\\": \\\"Spawn an autonomous teammate.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"name\\\": {\\\"type\\\": \\\"string\\\"}, \\\"role\\\": {\\\"type\\\": \\\"string\\\"}, \\\"prompt\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"name\\\", \\\"role\\\", \\\"prompt\\\"]}},\\n    {\\\"name\\\": \\\"list_teammates\\\", \\\"description\\\": \\\"List all teammates.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"send_message\\\", \\\"description\\\": \\\"Send a message to a teammate.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"to\\\": {\\\"type\\\": \\\"string\\\"}, \\\"content\\\": {\\\"type\\\": \\\"string\\\"}, \\\"msg_type\\\": {\\\"type\\\": \\\"string\\\", \\\"enum\\\": list(VALID_MSG_TYPES)}}, \\\"required\\\": [\\\"to\\\", \\\"content\\\"]}},\\n    {\\\"name\\\": \\\"read_inbox\\\", \\\"description\\\": \\\"Read and drain the lead's inbox.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"broadcast\\\", \\\"description\\\": \\\"Send a message to all teammates.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"content\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"content\\\"]}},\\n    {\\\"name\\\": \\\"shutdown_request\\\", \\\"description\\\": \\\"Request a teammate to shut down.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"teammate\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"teammate\\\"]}},\\n    {\\\"name\\\": \\\"shutdown_response\\\", \\\"description\\\": \\\"Check shutdown request status.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"request_id\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"request_id\\\"]}},\\n    {\\\"name\\\": \\\"plan_approval\\\", \\\"description\\\": \\\"Approve or reject a teammate's plan.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"request_id\\\": {\\\"type\\\": \\\"string\\\"}, \\\"approve\\\": {\\\"type\\\": \\\"boolean\\\"}, \\\"feedback\\\": {\\\"type\\\": \\\"string\\\"}}, \\\"required\\\": [\\\"request_id\\\", \\\"approve\\\"]}},\\n    {\\\"name\\\": \\\"idle\\\", \\\"description\\\": \\\"Enter idle state (for lead -- rarely used).\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}},\\n    {\\\"name\\\": \\\"claim_task\\\", \\\"description\\\": \\\"Claim a task from the board by ID.\\\",\\n     \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"}}, \\\"required\\\": [\\\"task_id\\\"]}},\\n]\\n\\n\\ndef agent_loop(messages: list):\\n    while True:\\n        inbox = BUS.read_inbox(\\\"lead\\\")\\n        if inbox:\\n            messages.append({\\n                \\\"role\\\": \\\"user\\\",\\n                \\\"content\\\": f\\\"<inbox>{json.dumps(inbox, indent=2)}</inbox>\\\",\\n            })\\n            messages.append({\\n                \\\"role\\\": \\\"assistant\\\",\\n                \\\"content\\\": \\\"Noted inbox messages.\\\",\\n            })\\n        response = client.messages.create(\\n            model=MODEL,\\n            system=SYSTEM,\\n            messages=messages,\\n            tools=TOOLS,\\n            max_tokens=8000,\\n        )\\n        messages.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.content})\\n        if response.stop_reason != \\\"tool_use\\\":\\n            return\\n        results = []\\n        for block in response.content:\\n            if block.type == \\\"tool_use\\\":\\n                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append({\\n                    \\\"type\\\": \\\"tool_result\\\",\\n                    \\\"tool_use_id\\\": block.id,\\n                    \\\"content\\\": str(output),\\n                })\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    history = []\\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        if query.strip() == \\\"/team\\\":\\n            print(TEAM.list_all())\\n            continue\\n        if query.strip() == \\\"/inbox\\\":\\n            print(json.dumps(BUS.read_inbox(\\\"lead\\\"), indent=2))\\n            continue\\n        if query.strip() == \\\"/tasks\\\":\\n            TASKS_DIR.mkdir(exist_ok=True)\\n            for f in sorted(TASKS_DIR.glob(\\\"task_*.json\\\")):\\n                t = json.loads(f.read_text())\\n                marker = {\\\"pending\\\": \\\"[ ]\\\", \\\"in_progress\\\": \\\"[>]\\\", \\\"completed\\\": \\\"[x]\\\"}.get(t[\\\"status\\\"], \\\"[?]\\\")\\n                owner = f\\\" @{t['owner']}\\\" if t.get(\\\"owner\\\") else \\\"\\\"\\n                print(f\\\"  {marker} #{t['id']}: {t['subject']}{owner}\\\")\\n            continue\\n        history.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": query})\\n        agent_loop(history)\\n        print()\\n\"\n    },\n    {\n      \"id\": \"s12\",\n      \"filename\": \"s12_worktree_task_isolation.py\",\n      \"title\": \"Worktree + Task Isolation\",\n      \"subtitle\": \"Isolate by Directory\",\n      \"loc\": 689,\n      \"tools\": [\n        \"bash\",\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\",\n        \"task_create\",\n        \"task_list\",\n        \"task_get\",\n        \"task_update\",\n        \"task_bind_worktree\",\n        \"worktree_create\",\n        \"worktree_list\",\n        \"worktree_status\",\n        \"worktree_run\",\n        \"worktree_remove\",\n        \"worktree_keep\",\n        \"worktree_events\"\n      ],\n      \"newTools\": [\n        \"task_create\",\n        \"task_list\",\n        \"task_get\",\n        \"task_update\",\n        \"task_bind_worktree\",\n        \"worktree_create\",\n        \"worktree_list\",\n        \"worktree_status\",\n        \"worktree_run\",\n        \"worktree_remove\",\n        \"worktree_keep\",\n        \"worktree_events\"\n      ],\n      \"coreAddition\": \"Composable worktree lifecycle + event stream over a shared task board\",\n      \"keyInsight\": \"Each works in its own directory; tasks manage goals, worktrees manage directories, bound by ID\",\n      \"classes\": [\n        {\n          \"name\": \"EventBus\",\n          \"startLine\": 82,\n          \"endLine\": 120\n        },\n        {\n          \"name\": \"TaskManager\",\n          \"startLine\": 121,\n          \"endLine\": 218\n        },\n        {\n          \"name\": \"WorktreeManager\",\n          \"startLine\": 224,\n          \"endLine\": 472\n        }\n      ],\n      \"functions\": [\n        {\n          \"name\": \"detect_repo_root\",\n          \"signature\": \"def detect_repo_root(cwd: Path)\",\n          \"startLine\": 52\n        },\n        {\n          \"name\": \"safe_path\",\n          \"signature\": \"def safe_path(p: str)\",\n          \"startLine\": 477\n        },\n        {\n          \"name\": \"run_bash\",\n          \"signature\": \"def run_bash(command: str)\",\n          \"startLine\": 484\n        },\n        {\n          \"name\": \"run_read\",\n          \"signature\": \"def run_read(path: str, limit: int = None)\",\n          \"startLine\": 503\n        },\n        {\n          \"name\": \"run_write\",\n          \"signature\": \"def run_write(path: str, content: str)\",\n          \"startLine\": 513\n        },\n        {\n          \"name\": \"run_edit\",\n          \"signature\": \"def run_edit(path: str, old_text: str, new_text: str)\",\n          \"startLine\": 523\n        },\n        {\n          \"name\": \"agent_loop\",\n          \"signature\": \"def agent_loop(messages: list)\",\n          \"startLine\": 728\n        }\n      ],\n      \"layer\": \"collaboration\",\n      \"source\": \"#!/usr/bin/env python3\\n\\\"\\\"\\\"\\ns12_worktree_task_isolation.py - Worktree + Task Isolation\\n\\nDirectory-level isolation for parallel task execution.\\nTasks are the control plane and worktrees are the execution plane.\\n\\n    .tasks/task_12.json\\n      {\\n        \\\"id\\\": 12,\\n        \\\"subject\\\": \\\"Implement auth refactor\\\",\\n        \\\"status\\\": \\\"in_progress\\\",\\n        \\\"worktree\\\": \\\"auth-refactor\\\"\\n      }\\n\\n    .worktrees/index.json\\n      {\\n        \\\"worktrees\\\": [\\n          {\\n            \\\"name\\\": \\\"auth-refactor\\\",\\n            \\\"path\\\": \\\".../.worktrees/auth-refactor\\\",\\n            \\\"branch\\\": \\\"wt/auth-refactor\\\",\\n            \\\"task_id\\\": 12,\\n            \\\"status\\\": \\\"active\\\"\\n          }\\n        ]\\n      }\\n\\nKey insight: \\\"Isolate by directory, coordinate by task ID.\\\"\\n\\\"\\\"\\\"\\n\\nimport json\\nimport os\\nimport re\\nimport subprocess\\nimport time\\nfrom pathlib import Path\\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\\nWORKDIR = Path.cwd()\\nclient = Anthropic(base_url=os.getenv(\\\"ANTHROPIC_BASE_URL\\\"))\\nMODEL = os.environ[\\\"MODEL_ID\\\"]\\n\\n\\ndef detect_repo_root(cwd: Path) -> Path | None:\\n    \\\"\\\"\\\"Return git repo root if cwd is inside a repo, else None.\\\"\\\"\\\"\\n    try:\\n        r = subprocess.run(\\n            [\\\"git\\\", \\\"rev-parse\\\", \\\"--show-toplevel\\\"],\\n            cwd=cwd,\\n            capture_output=True,\\n            text=True,\\n            timeout=10,\\n        )\\n        if r.returncode != 0:\\n            return None\\n        root = Path(r.stdout.strip())\\n        return root if root.exists() else None\\n    except Exception:\\n        return None\\n\\n\\nREPO_ROOT = detect_repo_root(WORKDIR) or WORKDIR\\n\\nSYSTEM = (\\n    f\\\"You are a coding agent at {WORKDIR}. \\\"\\n    \\\"Use task + worktree tools for multi-task work. \\\"\\n    \\\"For parallel or risky changes: create tasks, allocate worktree lanes, \\\"\\n    \\\"run commands in those lanes, then choose keep/remove for closeout. \\\"\\n    \\\"Use worktree_events when you need lifecycle visibility.\\\"\\n)\\n\\n\\n# -- EventBus: append-only lifecycle events for observability --\\nclass EventBus:\\n    def __init__(self, event_log_path: Path):\\n        self.path = event_log_path\\n        self.path.parent.mkdir(parents=True, exist_ok=True)\\n        if not self.path.exists():\\n            self.path.write_text(\\\"\\\")\\n\\n    def emit(\\n        self,\\n        event: str,\\n        task: dict | None = None,\\n        worktree: dict | None = None,\\n        error: str | None = None,\\n    ):\\n        payload = {\\n            \\\"event\\\": event,\\n            \\\"ts\\\": time.time(),\\n            \\\"task\\\": task or {},\\n            \\\"worktree\\\": worktree or {},\\n        }\\n        if error:\\n            payload[\\\"error\\\"] = error\\n        with self.path.open(\\\"a\\\", encoding=\\\"utf-8\\\") as f:\\n            f.write(json.dumps(payload) + \\\"\\\\n\\\")\\n\\n    def list_recent(self, limit: int = 20) -> str:\\n        n = max(1, min(int(limit or 20), 200))\\n        lines = self.path.read_text(encoding=\\\"utf-8\\\").splitlines()\\n        recent = lines[-n:]\\n        items = []\\n        for line in recent:\\n            try:\\n                items.append(json.loads(line))\\n            except Exception:\\n                items.append({\\\"event\\\": \\\"parse_error\\\", \\\"raw\\\": line})\\n        return json.dumps(items, indent=2)\\n\\n\\n# -- TaskManager: persistent task board with optional worktree binding --\\nclass TaskManager:\\n    def __init__(self, tasks_dir: Path):\\n        self.dir = tasks_dir\\n        self.dir.mkdir(parents=True, exist_ok=True)\\n        self._next_id = self._max_id() + 1\\n\\n    def _max_id(self) -> int:\\n        ids = []\\n        for f in self.dir.glob(\\\"task_*.json\\\"):\\n            try:\\n                ids.append(int(f.stem.split(\\\"_\\\")[1]))\\n            except Exception:\\n                pass\\n        return max(ids) if ids else 0\\n\\n    def _path(self, task_id: int) -> Path:\\n        return self.dir / f\\\"task_{task_id}.json\\\"\\n\\n    def _load(self, task_id: int) -> dict:\\n        path = self._path(task_id)\\n        if not path.exists():\\n            raise ValueError(f\\\"Task {task_id} not found\\\")\\n        return json.loads(path.read_text())\\n\\n    def _save(self, task: dict):\\n        self._path(task[\\\"id\\\"]).write_text(json.dumps(task, indent=2))\\n\\n    def create(self, subject: str, description: str = \\\"\\\") -> str:\\n        task = {\\n            \\\"id\\\": self._next_id,\\n            \\\"subject\\\": subject,\\n            \\\"description\\\": description,\\n            \\\"status\\\": \\\"pending\\\",\\n            \\\"owner\\\": \\\"\\\",\\n            \\\"worktree\\\": \\\"\\\",\\n            \\\"blockedBy\\\": [],\\n            \\\"created_at\\\": time.time(),\\n            \\\"updated_at\\\": time.time(),\\n        }\\n        self._save(task)\\n        self._next_id += 1\\n        return json.dumps(task, indent=2)\\n\\n    def get(self, task_id: int) -> str:\\n        return json.dumps(self._load(task_id), indent=2)\\n\\n    def exists(self, task_id: int) -> bool:\\n        return self._path(task_id).exists()\\n\\n    def update(self, task_id: int, status: str = None, owner: str = None) -> str:\\n        task = self._load(task_id)\\n        if status:\\n            if status not in (\\\"pending\\\", \\\"in_progress\\\", \\\"completed\\\"):\\n                raise ValueError(f\\\"Invalid status: {status}\\\")\\n            task[\\\"status\\\"] = status\\n        if owner is not None:\\n            task[\\\"owner\\\"] = owner\\n        task[\\\"updated_at\\\"] = time.time()\\n        self._save(task)\\n        return json.dumps(task, indent=2)\\n\\n    def bind_worktree(self, task_id: int, worktree: str, owner: str = \\\"\\\") -> str:\\n        task = self._load(task_id)\\n        task[\\\"worktree\\\"] = worktree\\n        if owner:\\n            task[\\\"owner\\\"] = owner\\n        if task[\\\"status\\\"] == \\\"pending\\\":\\n            task[\\\"status\\\"] = \\\"in_progress\\\"\\n        task[\\\"updated_at\\\"] = time.time()\\n        self._save(task)\\n        return json.dumps(task, indent=2)\\n\\n    def unbind_worktree(self, task_id: int) -> str:\\n        task = self._load(task_id)\\n        task[\\\"worktree\\\"] = \\\"\\\"\\n        task[\\\"updated_at\\\"] = time.time()\\n        self._save(task)\\n        return json.dumps(task, indent=2)\\n\\n    def list_all(self) -> str:\\n        tasks = []\\n        for f in sorted(self.dir.glob(\\\"task_*.json\\\")):\\n            tasks.append(json.loads(f.read_text()))\\n        if not tasks:\\n            return \\\"No tasks.\\\"\\n        lines = []\\n        for t in tasks:\\n            marker = {\\n                \\\"pending\\\": \\\"[ ]\\\",\\n                \\\"in_progress\\\": \\\"[>]\\\",\\n                \\\"completed\\\": \\\"[x]\\\",\\n            }.get(t[\\\"status\\\"], \\\"[?]\\\")\\n            owner = f\\\" owner={t['owner']}\\\" if t.get(\\\"owner\\\") else \\\"\\\"\\n            wt = f\\\" wt={t['worktree']}\\\" if t.get(\\\"worktree\\\") else \\\"\\\"\\n            lines.append(f\\\"{marker} #{t['id']}: {t['subject']}{owner}{wt}\\\")\\n        return \\\"\\\\n\\\".join(lines)\\n\\n\\nTASKS = TaskManager(REPO_ROOT / \\\".tasks\\\")\\nEVENTS = EventBus(REPO_ROOT / \\\".worktrees\\\" / \\\"events.jsonl\\\")\\n\\n\\n# -- WorktreeManager: create/list/run/remove git worktrees + lifecycle index --\\nclass WorktreeManager:\\n    def __init__(self, repo_root: Path, tasks: TaskManager, events: EventBus):\\n        self.repo_root = repo_root\\n        self.tasks = tasks\\n        self.events = events\\n        self.dir = repo_root / \\\".worktrees\\\"\\n        self.dir.mkdir(parents=True, exist_ok=True)\\n        self.index_path = self.dir / \\\"index.json\\\"\\n        if not self.index_path.exists():\\n            self.index_path.write_text(json.dumps({\\\"worktrees\\\": []}, indent=2))\\n        self.git_available = self._is_git_repo()\\n\\n    def _is_git_repo(self) -> bool:\\n        try:\\n            r = subprocess.run(\\n                [\\\"git\\\", \\\"rev-parse\\\", \\\"--is-inside-work-tree\\\"],\\n                cwd=self.repo_root,\\n                capture_output=True,\\n                text=True,\\n                timeout=10,\\n            )\\n            return r.returncode == 0\\n        except Exception:\\n            return False\\n\\n    def _run_git(self, args: list[str]) -> str:\\n        if not self.git_available:\\n            raise RuntimeError(\\\"Not in a git repository. worktree tools require git.\\\")\\n        r = subprocess.run(\\n            [\\\"git\\\", *args],\\n            cwd=self.repo_root,\\n            capture_output=True,\\n            text=True,\\n            timeout=120,\\n        )\\n        if r.returncode != 0:\\n            msg = (r.stdout + r.stderr).strip()\\n            raise RuntimeError(msg or f\\\"git {' '.join(args)} failed\\\")\\n        return (r.stdout + r.stderr).strip() or \\\"(no output)\\\"\\n\\n    def _load_index(self) -> dict:\\n        return json.loads(self.index_path.read_text())\\n\\n    def _save_index(self, data: dict):\\n        self.index_path.write_text(json.dumps(data, indent=2))\\n\\n    def _find(self, name: str) -> dict | None:\\n        idx = self._load_index()\\n        for wt in idx.get(\\\"worktrees\\\", []):\\n            if wt.get(\\\"name\\\") == name:\\n                return wt\\n        return None\\n\\n    def _validate_name(self, name: str):\\n        if not re.fullmatch(r\\\"[A-Za-z0-9._-]{1,40}\\\", name or \\\"\\\"):\\n            raise ValueError(\\n                \\\"Invalid worktree name. Use 1-40 chars: letters, numbers, ., _, -\\\"\\n            )\\n\\n    def create(self, name: str, task_id: int = None, base_ref: str = \\\"HEAD\\\") -> str:\\n        self._validate_name(name)\\n        if self._find(name):\\n            raise ValueError(f\\\"Worktree '{name}' already exists in index\\\")\\n        if task_id is not None and not self.tasks.exists(task_id):\\n            raise ValueError(f\\\"Task {task_id} not found\\\")\\n\\n        path = self.dir / name\\n        branch = f\\\"wt/{name}\\\"\\n        self.events.emit(\\n            \\\"worktree.create.before\\\",\\n            task={\\\"id\\\": task_id} if task_id is not None else {},\\n            worktree={\\\"name\\\": name, \\\"base_ref\\\": base_ref},\\n        )\\n        try:\\n            self._run_git([\\\"worktree\\\", \\\"add\\\", \\\"-b\\\", branch, str(path), base_ref])\\n\\n            entry = {\\n                \\\"name\\\": name,\\n                \\\"path\\\": str(path),\\n                \\\"branch\\\": branch,\\n                \\\"task_id\\\": task_id,\\n                \\\"status\\\": \\\"active\\\",\\n                \\\"created_at\\\": time.time(),\\n            }\\n\\n            idx = self._load_index()\\n            idx[\\\"worktrees\\\"].append(entry)\\n            self._save_index(idx)\\n\\n            if task_id is not None:\\n                self.tasks.bind_worktree(task_id, name)\\n\\n            self.events.emit(\\n                \\\"worktree.create.after\\\",\\n                task={\\\"id\\\": task_id} if task_id is not None else {},\\n                worktree={\\n                    \\\"name\\\": name,\\n                    \\\"path\\\": str(path),\\n                    \\\"branch\\\": branch,\\n                    \\\"status\\\": \\\"active\\\",\\n                },\\n            )\\n            return json.dumps(entry, indent=2)\\n        except Exception as e:\\n            self.events.emit(\\n                \\\"worktree.create.failed\\\",\\n                task={\\\"id\\\": task_id} if task_id is not None else {},\\n                worktree={\\\"name\\\": name, \\\"base_ref\\\": base_ref},\\n                error=str(e),\\n            )\\n            raise\\n\\n    def list_all(self) -> str:\\n        idx = self._load_index()\\n        wts = idx.get(\\\"worktrees\\\", [])\\n        if not wts:\\n            return \\\"No worktrees in index.\\\"\\n        lines = []\\n        for wt in wts:\\n            suffix = f\\\" task={wt['task_id']}\\\" if wt.get(\\\"task_id\\\") else \\\"\\\"\\n            lines.append(\\n                f\\\"[{wt.get('status', 'unknown')}] {wt['name']} -> \\\"\\n                f\\\"{wt['path']} ({wt.get('branch', '-')}){suffix}\\\"\\n            )\\n        return \\\"\\\\n\\\".join(lines)\\n\\n    def status(self, name: str) -> str:\\n        wt = self._find(name)\\n        if not wt:\\n            return f\\\"Error: Unknown worktree '{name}'\\\"\\n        path = Path(wt[\\\"path\\\"])\\n        if not path.exists():\\n            return f\\\"Error: Worktree path missing: {path}\\\"\\n        r = subprocess.run(\\n            [\\\"git\\\", \\\"status\\\", \\\"--short\\\", \\\"--branch\\\"],\\n            cwd=path,\\n            capture_output=True,\\n            text=True,\\n            timeout=60,\\n        )\\n        text = (r.stdout + r.stderr).strip()\\n        return text or \\\"Clean worktree\\\"\\n\\n    def run(self, name: str, 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\\n        wt = self._find(name)\\n        if not wt:\\n            return f\\\"Error: Unknown worktree '{name}'\\\"\\n        path = Path(wt[\\\"path\\\"])\\n        if not path.exists():\\n            return f\\\"Error: Worktree path missing: {path}\\\"\\n\\n        try:\\n            r = subprocess.run(\\n                command,\\n                shell=True,\\n                cwd=path,\\n                capture_output=True,\\n                text=True,\\n                timeout=300,\\n            )\\n            out = (r.stdout + r.stderr).strip()\\n            return out[:50000] if out else \\\"(no output)\\\"\\n        except subprocess.TimeoutExpired:\\n            return \\\"Error: Timeout (300s)\\\"\\n\\n    def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str:\\n        wt = self._find(name)\\n        if not wt:\\n            return f\\\"Error: Unknown worktree '{name}'\\\"\\n\\n        self.events.emit(\\n            \\\"worktree.remove.before\\\",\\n            task={\\\"id\\\": wt.get(\\\"task_id\\\")} if wt.get(\\\"task_id\\\") is not None else {},\\n            worktree={\\\"name\\\": name, \\\"path\\\": wt.get(\\\"path\\\")},\\n        )\\n        try:\\n            args = [\\\"worktree\\\", \\\"remove\\\"]\\n            if force:\\n                args.append(\\\"--force\\\")\\n            args.append(wt[\\\"path\\\"])\\n            self._run_git(args)\\n\\n            if complete_task and wt.get(\\\"task_id\\\") is not None:\\n                task_id = wt[\\\"task_id\\\"]\\n                before = json.loads(self.tasks.get(task_id))\\n                self.tasks.update(task_id, status=\\\"completed\\\")\\n                self.tasks.unbind_worktree(task_id)\\n                self.events.emit(\\n                    \\\"task.completed\\\",\\n                    task={\\n                        \\\"id\\\": task_id,\\n                        \\\"subject\\\": before.get(\\\"subject\\\", \\\"\\\"),\\n                        \\\"status\\\": \\\"completed\\\",\\n                    },\\n                    worktree={\\\"name\\\": name},\\n                )\\n\\n            idx = self._load_index()\\n            for item in idx.get(\\\"worktrees\\\", []):\\n                if item.get(\\\"name\\\") == name:\\n                    item[\\\"status\\\"] = \\\"removed\\\"\\n                    item[\\\"removed_at\\\"] = time.time()\\n            self._save_index(idx)\\n\\n            self.events.emit(\\n                \\\"worktree.remove.after\\\",\\n                task={\\\"id\\\": wt.get(\\\"task_id\\\")} if wt.get(\\\"task_id\\\") is not None else {},\\n                worktree={\\\"name\\\": name, \\\"path\\\": wt.get(\\\"path\\\"), \\\"status\\\": \\\"removed\\\"},\\n            )\\n            return f\\\"Removed worktree '{name}'\\\"\\n        except Exception as e:\\n            self.events.emit(\\n                \\\"worktree.remove.failed\\\",\\n                task={\\\"id\\\": wt.get(\\\"task_id\\\")} if wt.get(\\\"task_id\\\") is not None else {},\\n                worktree={\\\"name\\\": name, \\\"path\\\": wt.get(\\\"path\\\")},\\n                error=str(e),\\n            )\\n            raise\\n\\n    def keep(self, name: str) -> str:\\n        wt = self._find(name)\\n        if not wt:\\n            return f\\\"Error: Unknown worktree '{name}'\\\"\\n\\n        idx = self._load_index()\\n        kept = None\\n        for item in idx.get(\\\"worktrees\\\", []):\\n            if item.get(\\\"name\\\") == name:\\n                item[\\\"status\\\"] = \\\"kept\\\"\\n                item[\\\"kept_at\\\"] = time.time()\\n                kept = item\\n        self._save_index(idx)\\n\\n        self.events.emit(\\n            \\\"worktree.keep\\\",\\n            task={\\\"id\\\": wt.get(\\\"task_id\\\")} if wt.get(\\\"task_id\\\") is not None else {},\\n            worktree={\\n                \\\"name\\\": name,\\n                \\\"path\\\": wt.get(\\\"path\\\"),\\n                \\\"status\\\": \\\"kept\\\",\\n            },\\n        )\\n        return json.dumps(kept, indent=2) if kept else f\\\"Error: Unknown worktree '{name}'\\\"\\n\\n\\nWORKTREES = WorktreeManager(REPO_ROOT, TASKS, EVENTS)\\n\\n\\n# -- Base tools (kept minimal, same style as previous sessions) --\\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    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(\\n            command,\\n            shell=True,\\n            cwd=WORKDIR,\\n            capture_output=True,\\n            text=True,\\n            timeout=120,\\n        )\\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) -> 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)\\\"]\\n        return \\\"\\\\n\\\".join(lines)[:50000]\\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\\\"\\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        fp = safe_path(path)\\n        c = fp.read_text()\\n        if old_text not in c:\\n            return f\\\"Error: Text not found in {path}\\\"\\n        fp.write_text(c.replace(old_text, new_text, 1))\\n        return f\\\"Edited {path}\\\"\\n    except Exception as e:\\n        return f\\\"Error: {e}\\\"\\n\\n\\nTOOL_HANDLERS = {\\n    \\\"bash\\\": lambda **kw: run_bash(kw[\\\"command\\\"]),\\n    \\\"read_file\\\": lambda **kw: run_read(kw[\\\"path\\\"], kw.get(\\\"limit\\\")),\\n    \\\"write_file\\\": lambda **kw: run_write(kw[\\\"path\\\"], kw[\\\"content\\\"]),\\n    \\\"edit_file\\\": lambda **kw: run_edit(kw[\\\"path\\\"], kw[\\\"old_text\\\"], kw[\\\"new_text\\\"]),\\n    \\\"task_create\\\": lambda **kw: TASKS.create(kw[\\\"subject\\\"], kw.get(\\\"description\\\", \\\"\\\")),\\n    \\\"task_list\\\": lambda **kw: TASKS.list_all(),\\n    \\\"task_get\\\": lambda **kw: TASKS.get(kw[\\\"task_id\\\"]),\\n    \\\"task_update\\\": lambda **kw: TASKS.update(kw[\\\"task_id\\\"], kw.get(\\\"status\\\"), kw.get(\\\"owner\\\")),\\n    \\\"task_bind_worktree\\\": lambda **kw: TASKS.bind_worktree(kw[\\\"task_id\\\"], kw[\\\"worktree\\\"], kw.get(\\\"owner\\\", \\\"\\\")),\\n    \\\"worktree_create\\\": lambda **kw: WORKTREES.create(kw[\\\"name\\\"], kw.get(\\\"task_id\\\"), kw.get(\\\"base_ref\\\", \\\"HEAD\\\")),\\n    \\\"worktree_list\\\": lambda **kw: WORKTREES.list_all(),\\n    \\\"worktree_status\\\": lambda **kw: WORKTREES.status(kw[\\\"name\\\"]),\\n    \\\"worktree_run\\\": lambda **kw: WORKTREES.run(kw[\\\"name\\\"], kw[\\\"command\\\"]),\\n    \\\"worktree_keep\\\": lambda **kw: WORKTREES.keep(kw[\\\"name\\\"]),\\n    \\\"worktree_remove\\\": lambda **kw: WORKTREES.remove(kw[\\\"name\\\"], kw.get(\\\"force\\\", False), kw.get(\\\"complete_task\\\", False)),\\n    \\\"worktree_events\\\": lambda **kw: EVENTS.list_recent(kw.get(\\\"limit\\\", 20)),\\n}\\n\\nTOOLS = [\\n    {\\n        \\\"name\\\": \\\"bash\\\",\\n        \\\"description\\\": \\\"Run a shell command in the current workspace (blocking).\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\\"command\\\": {\\\"type\\\": \\\"string\\\"}},\\n            \\\"required\\\": [\\\"command\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"read_file\\\",\\n        \\\"description\\\": \\\"Read file contents.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"path\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"limit\\\": {\\\"type\\\": \\\"integer\\\"},\\n            },\\n            \\\"required\\\": [\\\"path\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"write_file\\\",\\n        \\\"description\\\": \\\"Write content to file.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"path\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"content\\\": {\\\"type\\\": \\\"string\\\"},\\n            },\\n            \\\"required\\\": [\\\"path\\\", \\\"content\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"edit_file\\\",\\n        \\\"description\\\": \\\"Replace exact text in file.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"path\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"old_text\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"new_text\\\": {\\\"type\\\": \\\"string\\\"},\\n            },\\n            \\\"required\\\": [\\\"path\\\", \\\"old_text\\\", \\\"new_text\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"task_create\\\",\\n        \\\"description\\\": \\\"Create a new task on the shared task board.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"subject\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"description\\\": {\\\"type\\\": \\\"string\\\"},\\n            },\\n            \\\"required\\\": [\\\"subject\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"task_list\\\",\\n        \\\"description\\\": \\\"List all tasks with status, owner, and worktree binding.\\\",\\n        \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}},\\n    },\\n    {\\n        \\\"name\\\": \\\"task_get\\\",\\n        \\\"description\\\": \\\"Get task details by ID.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"}},\\n            \\\"required\\\": [\\\"task_id\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"task_update\\\",\\n        \\\"description\\\": \\\"Update task status or owner.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"},\\n                \\\"status\\\": {\\n                    \\\"type\\\": \\\"string\\\",\\n                    \\\"enum\\\": [\\\"pending\\\", \\\"in_progress\\\", \\\"completed\\\"],\\n                },\\n                \\\"owner\\\": {\\\"type\\\": \\\"string\\\"},\\n            },\\n            \\\"required\\\": [\\\"task_id\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"task_bind_worktree\\\",\\n        \\\"description\\\": \\\"Bind a task to a worktree name.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"},\\n                \\\"worktree\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"owner\\\": {\\\"type\\\": \\\"string\\\"},\\n            },\\n            \\\"required\\\": [\\\"task_id\\\", \\\"worktree\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"worktree_create\\\",\\n        \\\"description\\\": \\\"Create a git worktree and optionally bind it to a task.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"name\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"task_id\\\": {\\\"type\\\": \\\"integer\\\"},\\n                \\\"base_ref\\\": {\\\"type\\\": \\\"string\\\"},\\n            },\\n            \\\"required\\\": [\\\"name\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"worktree_list\\\",\\n        \\\"description\\\": \\\"List worktrees tracked in .worktrees/index.json.\\\",\\n        \\\"input_schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}},\\n    },\\n    {\\n        \\\"name\\\": \\\"worktree_status\\\",\\n        \\\"description\\\": \\\"Show git status for one worktree.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\\"name\\\": {\\\"type\\\": \\\"string\\\"}},\\n            \\\"required\\\": [\\\"name\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"worktree_run\\\",\\n        \\\"description\\\": \\\"Run a shell command in a named worktree directory.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"name\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"command\\\": {\\\"type\\\": \\\"string\\\"},\\n            },\\n            \\\"required\\\": [\\\"name\\\", \\\"command\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"worktree_remove\\\",\\n        \\\"description\\\": \\\"Remove a worktree and optionally mark its bound task completed.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\n                \\\"name\\\": {\\\"type\\\": \\\"string\\\"},\\n                \\\"force\\\": {\\\"type\\\": \\\"boolean\\\"},\\n                \\\"complete_task\\\": {\\\"type\\\": \\\"boolean\\\"},\\n            },\\n            \\\"required\\\": [\\\"name\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"worktree_keep\\\",\\n        \\\"description\\\": \\\"Mark a worktree as kept in lifecycle state without removing it.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\\"name\\\": {\\\"type\\\": \\\"string\\\"}},\\n            \\\"required\\\": [\\\"name\\\"],\\n        },\\n    },\\n    {\\n        \\\"name\\\": \\\"worktree_events\\\",\\n        \\\"description\\\": \\\"List recent worktree/task lifecycle events from .worktrees/events.jsonl.\\\",\\n        \\\"input_schema\\\": {\\n            \\\"type\\\": \\\"object\\\",\\n            \\\"properties\\\": {\\\"limit\\\": {\\\"type\\\": \\\"integer\\\"}},\\n        },\\n    },\\n]\\n\\n\\ndef agent_loop(messages: list):\\n    while True:\\n        response = client.messages.create(\\n            model=MODEL,\\n            system=SYSTEM,\\n            messages=messages,\\n            tools=TOOLS,\\n            max_tokens=8000,\\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                handler = TOOL_HANDLERS.get(block.name)\\n                try:\\n                    output = handler(**block.input) if handler else f\\\"Unknown tool: {block.name}\\\"\\n                except Exception as e:\\n                    output = f\\\"Error: {e}\\\"\\n                print(f\\\"> {block.name}: {str(output)[:200]}\\\")\\n                results.append(\\n                    {\\n                        \\\"type\\\": \\\"tool_result\\\",\\n                        \\\"tool_use_id\\\": block.id,\\n                        \\\"content\\\": str(output),\\n                    }\\n                )\\n        messages.append({\\\"role\\\": \\\"user\\\", \\\"content\\\": results})\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    print(f\\\"Repo root for s12: {REPO_ROOT}\\\")\\n    if not WORKTREES.git_available:\\n        print(\\\"Note: Not in a git repo. worktree_* tools will return errors.\\\")\\n\\n    history = []\\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)\\n        print()\\n\"\n    }\n  ],\n  \"diffs\": [\n    {\n      \"from\": \"s01\",\n      \"to\": \"s02\",\n      \"newClasses\": [],\n      \"newFunctions\": [\n        \"safe_path\",\n        \"run_read\",\n        \"run_write\",\n        \"run_edit\"\n      ],\n      \"newTools\": [\n        \"read_file\",\n        \"write_file\",\n        \"edit_file\"\n      ],\n      \"locDelta\": 31\n    },\n    {\n      \"from\": \"s02\",\n      \"to\": \"s03\",\n      \"newClasses\": [\n        \"TodoManager\"\n      ],\n      \"newFunctions\": [],\n      \"newTools\": [\n        \"todo\"\n      ],\n      \"locDelta\": 56\n    },\n    {\n      \"from\": \"s03\",\n      \"to\": \"s04\",\n      \"newClasses\": [],\n      \"newFunctions\": [\n        \"run_subagent\"\n      ],\n      \"newTools\": [\n        \"task\"\n      ],\n      \"locDelta\": -25\n    },\n    {\n      \"from\": \"s04\",\n      \"to\": \"s05\",\n      \"newClasses\": [\n        \"SkillLoader\"\n      ],\n      \"newFunctions\": [],\n      \"newTools\": [\n        \"load_skill\"\n      ],\n      \"locDelta\": 36\n    },\n    {\n      \"from\": \"s05\",\n      \"to\": \"s06\",\n      \"newClasses\": [],\n      \"newFunctions\": [\n        \"estimate_tokens\",\n        \"micro_compact\",\n        \"auto_compact\"\n      ],\n      \"newTools\": [\n        \"compact\"\n      ],\n      \"locDelta\": 18\n    },\n    {\n      \"from\": \"s06\",\n      \"to\": \"s07\",\n      \"newClasses\": [\n        \"TaskManager\"\n      ],\n      \"newFunctions\": [],\n      \"newTools\": [\n        \"task_create\",\n        \"task_update\",\n        \"task_list\",\n        \"task_get\"\n      ],\n      \"locDelta\": 2\n    },\n    {\n      \"from\": \"s07\",\n      \"to\": \"s08\",\n      \"newClasses\": [\n        \"BackgroundManager\"\n      ],\n      \"newFunctions\": [],\n      \"newTools\": [\n        \"background_run\",\n        \"check_background\"\n      ],\n      \"locDelta\": -9\n    },\n    {\n      \"from\": \"s08\",\n      \"to\": \"s09\",\n      \"newClasses\": [\n        \"MessageBus\",\n        \"TeammateManager\"\n      ],\n      \"newFunctions\": [\n        \"_safe_path\",\n        \"_run_bash\",\n        \"_run_read\",\n        \"_run_write\",\n        \"_run_edit\"\n      ],\n      \"newTools\": [\n        \"alice\",\n        \"send_message\",\n        \"read_inbox\",\n        \"spawn_teammate\",\n        \"list_teammates\",\n        \"broadcast\"\n      ],\n      \"locDelta\": 150\n    },\n    {\n      \"from\": \"s09\",\n      \"to\": \"s10\",\n      \"newClasses\": [],\n      \"newFunctions\": [\n        \"handle_shutdown_request\",\n        \"handle_plan_review\",\n        \"_check_shutdown_status\"\n      ],\n      \"newTools\": [\n        \"shutdown_response\",\n        \"plan_approval\",\n        \"shutdown_request\"\n      ],\n      \"locDelta\": 71\n    },\n    {\n      \"from\": \"s10\",\n      \"to\": \"s11\",\n      \"newClasses\": [],\n      \"newFunctions\": [\n        \"scan_unclaimed_tasks\",\n        \"claim_task\",\n        \"make_identity_block\"\n      ],\n      \"newTools\": [\n        \"idle\",\n        \"claim_task\"\n      ],\n      \"locDelta\": 80\n    },\n    {\n      \"from\": \"s11\",\n      \"to\": \"s12\",\n      \"newClasses\": [\n        \"EventBus\",\n        \"TaskManager\",\n        \"WorktreeManager\"\n      ],\n      \"newFunctions\": [\n        \"detect_repo_root\",\n        \"safe_path\",\n        \"run_bash\",\n        \"run_read\",\n        \"run_write\",\n        \"run_edit\"\n      ],\n      \"newTools\": [\n        \"task_create\",\n        \"task_list\",\n        \"task_get\",\n        \"task_update\",\n        \"task_bind_worktree\",\n        \"worktree_create\",\n        \"worktree_list\",\n        \"worktree_status\",\n        \"worktree_run\",\n        \"worktree_remove\",\n        \"worktree_keep\",\n        \"worktree_events\"\n      ],\n      \"locDelta\": 195\n    }\n  ]\n}"
  },
  {
    "path": "web/src/data/scenarios/s01.json",
    "content": "{\n  \"version\": \"s01\",\n  \"title\": \"The Agent Loop\",\n  \"description\": \"A minimal agent that uses only bash to accomplish tasks\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Create a file called hello.py that prints 'Hello, World!'\",\n      \"annotation\": \"User sends a task to the agent\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"I'll create that file for you using bash.\",\n      \"annotation\": \"The model decides to use its only tool: bash\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"echo 'print(\\\"Hello, World!\\\")' > hello.py\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Tool call: the model generates a bash command\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Bash returns empty output (success)\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Let me verify the file was created correctly.\",\n      \"annotation\": \"The loop continues: model sees the result and decides next action\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"cat hello.py\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Another bash call to verify\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"print(\\\"Hello, World!\\\")\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"File contents confirmed\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Done! I've created hello.py with the print statement. You can run it with `python hello.py`.\",\n      \"annotation\": \"stop_reason != 'tool_use' -> loop breaks, agent responds to user\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s02.json",
    "content": "{\n  \"version\": \"s02\",\n  \"title\": \"Tools\",\n  \"description\": \"Agent with read, write, edit, and bash tools\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Add a function called greet(name) to hello.py\",\n      \"annotation\": \"User requests a code modification\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Let me read the current file first.\",\n      \"annotation\": \"Model chooses read_file instead of bash cat\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"hello.py\",\n      \"toolName\": \"read_file\",\n      \"annotation\": \"Dedicated read tool provides cleaner file access\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"print(\\\"Hello, World!\\\")\",\n      \"toolName\": \"read_file\",\n      \"annotation\": \"File contents returned to the model\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"hello.py\\n---\\ndef greet(name):\\n    print(f\\\"Hello, {name}!\\\")\\n\\ngreet(\\\"World\\\")\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"write_file replaces the entire file content\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"File written successfully\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"Write confirmed\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"I've updated hello.py with a greet(name) function that takes a name parameter.\",\n      \"annotation\": \"4 tools beat 1: each tool has a clear, focused purpose\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s03.json",
    "content": "{\n  \"version\": \"s03\",\n  \"title\": \"TodoWrite\",\n  \"description\": \"Agent creates a visible plan before executing\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Build a CLI calculator that supports add, subtract, multiply, divide\",\n      \"annotation\": \"A multi-step task that benefits from planning\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"[\\n  { \\\"id\\\": 1, \\\"task\\\": \\\"Create calc.py with argument parsing\\\", \\\"status\\\": \\\"in_progress\\\" },\\n  { \\\"id\\\": 2, \\\"task\\\": \\\"Implement 4 operations\\\", \\\"status\\\": \\\"pending\\\" },\\n  { \\\"id\\\": 3, \\\"task\\\": \\\"Add error handling\\\", \\\"status\\\": \\\"pending\\\" },\\n  { \\\"id\\\": 4, \\\"task\\\": \\\"Test all operations\\\", \\\"status\\\": \\\"pending\\\" }\\n]\",\n      \"toolName\": \"todo_write\",\n      \"annotation\": \"TodoManager creates a visible plan BEFORE any code is written\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Todo list created with 4 tasks\",\n      \"toolName\": \"todo_write\",\n      \"annotation\": \"The plan is now visible and trackable\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"calc.py\\n---\\nimport sys\\n\\ndef calculate(op, a, b):\\n    ops = {'+': a+b, '-': a-b, '*': a*b, '/': a/b}\\n    return ops.get(op, 'Unknown operator')\\n\\nif __name__ == '__main__':\\n    a, op, b = float(sys.argv[1]), sys.argv[2], float(sys.argv[3])\\n    print(calculate(op, a, b))\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"Executing task #1 and #2: create file with operations\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"File written successfully\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"Code written\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"python calc.py 10 + 5\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Executing task #4: testing\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"15.0\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Test passes\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Calculator built and tested. All 4 operations work correctly.\",\n      \"annotation\": \"Visible plans improve task completion by making progress trackable\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s04.json",
    "content": "{\n  \"version\": \"s04\",\n  \"title\": \"Subagents\",\n  \"description\": \"Agent spawns a subagent for isolated subtask execution\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Refactor the calculator: split into modules and add tests\",\n      \"annotation\": \"Complex task suitable for delegation\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"I'll delegate the test writing to a subagent while I handle the refactoring.\",\n      \"annotation\": \"Agent decides to split work using the Task tool\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"Write unit tests for calc.py covering all 4 operations and edge cases like division by zero\",\n      \"toolName\": \"task\",\n      \"annotation\": \"Task tool spawns a subagent with its own isolated context\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[Subagent spawned] Context: fresh messages[], same tools, scoped prompt\",\n      \"annotation\": \"Subagent gets clean context -- no confusion from parent's conversation\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Created test_calc.py with 8 test cases. All passing.\",\n      \"toolName\": \"task\",\n      \"annotation\": \"Subagent completes and returns a summary to parent\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"calc.py -> calc/operations.py, calc/cli.py\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"Parent continues with its own task: refactoring\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Files written successfully\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"Modules split\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Refactored into calc/operations.py and calc/cli.py. Tests were written by a subagent and are all passing.\",\n      \"annotation\": \"Context isolation prevents confusion between tasks\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s05.json",
    "content": "{\n  \"version\": \"s05\",\n  \"title\": \"Skills\",\n  \"description\": \"Agent loads external knowledge from SKILL.md files\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Set up a new Next.js project with TypeScript\",\n      \"annotation\": \"Task requires framework-specific knowledge\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[SkillLoader] Scanning for SKILL.md files... Found: nextjs.SKILL.md\",\n      \"annotation\": \"SkillLoader finds a matching skill file before the loop runs\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[Skill injected] nextjs.SKILL.md -> tool_result message appended\",\n      \"annotation\": \"Skill content is injected as a tool_result, not bloating the system prompt\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"I have knowledge about Next.js setup. Let me create the project structure.\",\n      \"annotation\": \"Model now has framework-specific knowledge from the skill file\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Using skill-provided knowledge for correct flags\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Success! Created my-app at /workspace/my-app\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Project created with correct configuration\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Next.js project created with TypeScript, Tailwind CSS, ESLint, App Router, and src/ directory.\",\n      \"annotation\": \"Skills inject via tool_result, not system prompt -- keeping context lean\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s06.json",
    "content": "{\n  \"version\": \"s06\",\n  \"title\": \"Compact\",\n  \"description\": \"Agent compresses context when tokens exceed threshold\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Analyze all Python files in this project and summarize each one\",\n      \"annotation\": \"A task that will generate large amounts of context\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"find . -name '*.py' -type f\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Finding all files to analyze\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"./src/main.py\\n./src/utils.py\\n./src/models.py\\n./tests/test_main.py\\n... (28 more files)\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"32 files found -- reading all of them will generate massive context\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"I'll read and analyze each file systematically.\",\n      \"annotation\": \"Agent begins processing files one by one\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"./src/main.py\",\n      \"toolName\": \"read_file\",\n      \"annotation\": \"Reading file 1 of 32...\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"[... 450 lines of code ...]\",\n      \"toolName\": \"read_file\",\n      \"annotation\": \"Large file content added to messages[]\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[ContextManager] Token count: 148,231 / 150,000 threshold\\nTriggering 3-layer compression:\\n  1. Drop tool_result content > 1000 chars -> replaced with summary\\n  2. Merge sequential assistant turns\\n  3. Summarize oldest 50% of conversation\\nResult: 148,231 -> 42,087 tokens\",\n      \"annotation\": \"Compression fires automatically when approaching the limit\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Continuing analysis with compressed context. Files 1-18 have been summarized, proceeding with remaining files.\",\n      \"annotation\": \"Forgetting old tool results enables infinite work -- the agent can process any number of files\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s07.json",
    "content": "{\n  \"version\": \"s07\",\n  \"title\": \"Tasks\",\n  \"description\": \"Agent uses persistent file-based tasks with dependencies\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Build a REST API with user auth, database, and deployment config\",\n      \"annotation\": \"Multi-part task requiring dependency tracking\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"create_task(\\\"Set up database schema\\\", deps=[])\\ncreate_task(\\\"Implement user model\\\", deps=[1])\\ncreate_task(\\\"Add auth endpoints\\\", deps=[2])\\ncreate_task(\\\"Write deployment config\\\", deps=[3])\",\n      \"toolName\": \"task_manager\",\n      \"annotation\": \"TaskManager creates file-based tasks with explicit dependencies\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Created 4 tasks in .tasks/\\n  #1 Set up database schema [ready]\\n  #2 Implement user model [blocked by #1]\\n  #3 Add auth endpoints [blocked by #2]\\n  #4 Write deployment config [blocked by #3]\",\n      \"toolName\": \"task_manager\",\n      \"annotation\": \"Tasks persisted to .tasks/ directory as JSON files\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"schema.sql\\n---\\nCREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE, password_hash TEXT);\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"Working on task #1\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"File written successfully\",\n      \"toolName\": \"write_file\",\n      \"annotation\": \"Schema created\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"complete_task(1)\",\n      \"toolName\": \"task_manager\",\n      \"annotation\": \"Completing task #1 automatically unblocks task #2\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Task #1 completed. Task #2 is now ready.\",\n      \"toolName\": \"task_manager\",\n      \"annotation\": \"File-based persistence outlives process memory\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Database schema is done. Moving to user model implementation.\",\n      \"annotation\": \"Dependency graph ensures tasks execute in correct order\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s08.json",
    "content": "{\n  \"version\": \"s08\",\n  \"title\": \"Background Tasks\",\n  \"description\": \"Agent executes long-running tasks in background threads\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Run the test suite and while it's running, fix the linting errors\",\n      \"annotation\": \"Two tasks that can run concurrently\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"I'll start the tests in the background and fix linting in parallel.\",\n      \"annotation\": \"Agent recognizes opportunity for concurrent execution\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"pytest tests/ -v --timeout=300\",\n      \"toolName\": \"background_task\",\n      \"annotation\": \"BackgroundManager spawns a thread for long-running test suite\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[BackgroundManager] Thread spawned: task_001\\nRunning: pytest tests/ -v --timeout=300\\nStatus: in_progress\",\n      \"annotation\": \"Thread is running independently -- main loop continues\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Background task started: task_001\",\n      \"toolName\": \"background_task\",\n      \"annotation\": \"Control returns immediately to the agent\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"ruff check --fix .\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Agent works on linting while tests run in background\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"Fixed 12 linting errors in 5 files\",\n      \"toolName\": \"bash\",\n      \"annotation\": \"Linting fixed concurrently with tests\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[Notification] Background task_001 completed\\nResult: 47 tests passed, 2 failed\\nFailed: test_auth.py::test_token_expiry, test_api.py::test_rate_limit\",\n      \"annotation\": \"Background thread notifies main loop via notification queue\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Linting is fixed. Tests finished: 47 passed, 2 failed. I'll fix the failing tests now.\",\n      \"annotation\": \"Non-blocking execution via threads + queue enables parallel work\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s09.json",
    "content": "{\n  \"version\": \"s09\",\n  \"title\": \"Agent Teams\",\n  \"description\": \"Persistent teammates communicate via JSONL inboxes\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Build a full-stack app: frontend in React, backend in Python, with tests\",\n      \"annotation\": \"Large task requiring team collaboration\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"spawn_teammate(\\n  name: \\\"frontend-dev\\\",\\n  role: \\\"React specialist\\\"\\n)\\nspawn_teammate(\\n  name: \\\"backend-dev\\\",\\n  role: \\\"Python API specialist\\\"\\n)\",\n      \"toolName\": \"teammate_manager\",\n      \"annotation\": \"Unlike subagents (s04) that die after one task, teammates persist\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[TeammateManager] Team created:\\n  .team/config.json written\\n  frontend-dev [idle] - own thread, own agent loop\\n  backend-dev [idle] - own thread, own agent loop\",\n      \"annotation\": \"Each teammate runs its own while(tool_use) loop in a separate thread\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"send_message(\\n  to: \\\"backend-dev\\\",\\n  content: \\\"Build the user API: GET/POST /users\\\"\\n)\",\n      \"toolName\": \"send_message\",\n      \"annotation\": \"Message appended to .team/inbox/backend-dev.jsonl\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[backend-dev] Checking inbox... 1 new message\\n  From: lead\\n  Content: \\\"Build the user API: GET/POST /users\\\"\\n  (inbox drained after read)\",\n      \"annotation\": \"Inbox is read-then-clear: prevents duplicate processing\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[backend-dev -> frontend-dev] Reply via inbox:\\n  \\\"API ready: GET /users, POST /users, GET /users/:id\\\"\\n  Written to .team/inbox/frontend-dev.jsonl\",\n      \"annotation\": \"Teammates message each other through the same JSONL mechanism\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Team is communicating via file-based inboxes. Each teammate has its own .jsonl file -- append to send, drain to read.\",\n      \"annotation\": \"JSONL inboxes decouple sender timing from receiver timing\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s10.json",
    "content": "{\n  \"version\": \"s10\",\n  \"title\": \"Team Protocols\",\n  \"description\": \"Request-response protocols for shutdown and plan approval\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Work is done. Shut down the team gracefully.\",\n      \"annotation\": \"Can't just kill threads -- need coordinated shutdown\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"shutdown_request(\\n  teammate: \\\"backend-dev\\\",\\n  request_id: \\\"req_abc123\\\"\\n)\",\n      \"toolName\": \"shutdown_teammate\",\n      \"annotation\": \"request_id correlates the request with the response\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[Shutdown FSM] req_abc123: pending\\n  Lead -> backend-dev inbox:\\n  { type: \\\"shutdown_request\\\", request_id: \\\"req_abc123\\\" }\",\n      \"annotation\": \"FSM tracks state: pending -> approved | rejected\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[backend-dev] Received shutdown request req_abc123\\n  Current task: none (idle)\\n  Decision: approve\\n  -> lead inbox: { type: \\\"shutdown_response\\\", request_id: \\\"req_abc123\\\", approve: true }\",\n      \"annotation\": \"Teammate decides whether to approve based on its own state\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[Shutdown FSM] req_abc123: approved\\n  backend-dev thread stopped\\n  Status: idle -> shutdown\",\n      \"annotation\": \"Same request_id pattern works for plan approval too\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"backend-dev shut down gracefully. The same request_id + FSM pattern handles plan approval: teammate submits plan, lead approves/rejects with the correlated request_id.\",\n      \"annotation\": \"One pattern (request_id correlation), two applications (shutdown + plan approval)\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s11.json",
    "content": "{\n  \"version\": \"s11\",\n  \"title\": \"Autonomous Agents\",\n  \"description\": \"Teammates self-govern with idle cycles and auto-claiming\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Implement the full feature backlog\",\n      \"annotation\": \"Open-ended task for autonomous team execution\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"create_tasks([\\n  { title: \\\"User authentication\\\", status: \\\"open\\\" },\\n  { title: \\\"Dashboard UI\\\", status: \\\"open\\\" },\\n  { title: \\\"API rate limiting\\\", status: \\\"open\\\" },\\n  { title: \\\"Integration tests\\\", status: \\\"open\\\" }\\n])\",\n      \"toolName\": \"task_manager\",\n      \"annotation\": \"Tasks created with no owner -- available for auto-claiming\"\n    },\n    {\n      \"type\": \"tool_result\",\n      \"content\": \"4 tasks created on shared board\",\n      \"toolName\": \"task_manager\",\n      \"annotation\": \"Unassigned tasks visible to all teammates\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[frontend-dev] Idle cycle triggered\\n  1. Check inbox -> 0 messages\\n  2. Poll task board -> found unowned task #2 \\\"Dashboard UI\\\"\\n  3. Auto-claim task #2\\n  4. Status: idle -> working\",\n      \"annotation\": \"Idle cycle: check inbox, poll tasks, auto-claim, resume work\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[backend-dev] Idle cycle triggered\\n  1. Check inbox -> 0 messages\\n  2. Poll task board -> found unowned task #1 \\\"User authentication\\\"\\n  3. Auto-claim task #1\\n  4. Status: idle -> working\",\n      \"annotation\": \"Multiple teammates claim different tasks concurrently\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"[tester] Idle cycle triggered\\n  1. Check inbox -> 0 messages\\n  2. Poll task board -> task #4 blocked by #1, #2, #3\\n  3. No claimable tasks\\n  4. Status: idle (will retry in 30s)\",\n      \"annotation\": \"Timeout-based polling prevents busy-waiting\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Team is self-organizing: frontend-dev claimed Dashboard UI, backend-dev claimed User auth. Tester is waiting for dependencies to clear.\",\n      \"annotation\": \"Polling + timeout makes teammates autonomous -- no micromanagement needed\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/data/scenarios/s12.json",
    "content": "{\n  \"version\": \"s12\",\n  \"title\": \"Worktree + Task Isolation\",\n  \"description\": \"Use a shared task board with optional worktree lanes for clean parallel execution\",\n  \"steps\": [\n    {\n      \"type\": \"user_message\",\n      \"content\": \"Implement auth refactor and login UI updates in parallel\",\n      \"annotation\": \"Two active tasks in one workspace would collide\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"task_create(subject: \\\"Auth refactor\\\")\\ntask_create(subject: \\\"Login UI polish\\\")\",\n      \"toolName\": \"task_manager\",\n      \"annotation\": \"Shared board remains the coordination source of truth\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"worktree_create(name: \\\"auth-refactor\\\", task_id: 1)\\nworktree_create(name: \\\"ui-login\\\")\\ntask_bind_worktree(task_id: 2, worktree: \\\"ui-login\\\")\",\n      \"toolName\": \"worktree_manager\",\n      \"annotation\": \"Lane allocation and task association are composable; task 2 binds after lane creation\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"worktree.create.before/after emitted\\n.tasks/task_1.json -> { status: \\\"in_progress\\\", worktree: \\\"auth-refactor\\\" }\\n.tasks/task_2.json -> { status: \\\"in_progress\\\", worktree: \\\"ui-login\\\" }\\n.worktrees/index.json updated\",\n      \"annotation\": \"Control-plane state remains canonical; hook-style consumers can react to lifecycle events without owning canonical state writes\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"worktree_run(name: \\\"auth-refactor\\\", command: \\\"pytest tests/auth -q\\\")\\nworktree_run(name: \\\"ui-login\\\", command: \\\"npm test -- login\\\")\",\n      \"toolName\": \"worktree_run\",\n      \"annotation\": \"In this teaching runtime, commands route by lane-scoped cwd; other runtimes may use session-level directory switches. The invariant is explicit execution context.\"\n    },\n    {\n      \"type\": \"tool_call\",\n      \"content\": \"worktree_keep(name: \\\"ui-login\\\")\\nworktree_remove(name: \\\"auth-refactor\\\", complete_task: true)\\nworktree_events(limit: 10)\",\n      \"toolName\": \"worktree_manager\",\n      \"annotation\": \"Closeout is explicit tool-driven state transition: mix keep/remove decisions and query lifecycle events in one pass\"\n    },\n    {\n      \"type\": \"system_event\",\n      \"content\": \"worktree.keep emitted for ui-login\\nworktree.remove.before/after emitted for auth-refactor\\ntask.completed emitted for #1\\n.worktrees/events.jsonl appended\",\n      \"annotation\": \"Lifecycle transitions become explicit records while task/worktree files remain source-of-truth\"\n    },\n    {\n      \"type\": \"assistant_text\",\n      \"content\": \"Task board handles coordination, worktrees handle isolation. Parallel tracks stay clean and auditable.\",\n      \"annotation\": \"Coordinate in one board, isolate by lane only where needed, and run optional policy/audit side effects from lifecycle events\"\n    }\n  ]\n}\n"
  },
  {
    "path": "web/src/hooks/useDarkMode.ts",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\n\nexport function useDarkMode(): boolean {\n  const [isDark, setIsDark] = useState(false);\n\n  useEffect(() => {\n    const html = document.documentElement;\n    setIsDark(html.classList.contains(\"dark\"));\n\n    const observer = new MutationObserver(() => {\n      setIsDark(html.classList.contains(\"dark\"));\n    });\n\n    observer.observe(html, { attributes: true, attributeFilter: [\"class\"] });\n    return () => observer.disconnect();\n  }, []);\n\n  return isDark;\n}\n\nexport interface SvgPalette {\n  nodeFill: string;\n  nodeStroke: string;\n  nodeText: string;\n  activeNodeFill: string;\n  activeNodeStroke: string;\n  activeNodeText: string;\n  endNodeFill: string;\n  endNodeStroke: string;\n  edgeStroke: string;\n  activeEdgeStroke: string;\n  arrowFill: string;\n  labelFill: string;\n  bgSubtle: string;\n}\n\nexport function useSvgPalette(): SvgPalette {\n  const isDark = useDarkMode();\n\n  if (isDark) {\n    return {\n      nodeFill: \"#27272a\",\n      nodeStroke: \"#3f3f46\",\n      nodeText: \"#d4d4d8\",\n      activeNodeFill: \"#3b82f6\",\n      activeNodeStroke: \"#2563eb\",\n      activeNodeText: \"#ffffff\",\n      endNodeFill: \"#a855f7\",\n      endNodeStroke: \"#9333ea\",\n      edgeStroke: \"#52525b\",\n      activeEdgeStroke: \"#3b82f6\",\n      arrowFill: \"#71717a\",\n      labelFill: \"#a1a1aa\",\n      bgSubtle: \"#18181b\",\n    };\n  }\n\n  return {\n    nodeFill: \"#e2e8f0\",\n    nodeStroke: \"#cbd5e1\",\n    nodeText: \"#475569\",\n    activeNodeFill: \"#3b82f6\",\n    activeNodeStroke: \"#2563eb\",\n    activeNodeText: \"#ffffff\",\n    endNodeFill: \"#a855f7\",\n    endNodeStroke: \"#9333ea\",\n    edgeStroke: \"#cbd5e1\",\n    activeEdgeStroke: \"#3b82f6\",\n    arrowFill: \"#94a3b8\",\n    labelFill: \"#94a3b8\",\n    bgSubtle: \"#f8fafc\",\n  };\n}\n"
  },
  {
    "path": "web/src/hooks/useSimulator.ts",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport type { SimStep } from \"@/types/agent-data\";\n\ninterface SimulatorState {\n  currentIndex: number;\n  isPlaying: boolean;\n  speed: number;\n}\n\nexport function useSimulator(steps: SimStep[]) {\n  const [state, setState] = useState<SimulatorState>({\n    currentIndex: -1,\n    isPlaying: false,\n    speed: 1,\n  });\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const clearTimer = useCallback(() => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n      timerRef.current = null;\n    }\n  }, []);\n\n  const stepForward = useCallback(() => {\n    setState((prev) => {\n      if (prev.currentIndex >= steps.length - 1) {\n        return { ...prev, isPlaying: false };\n      }\n      return { ...prev, currentIndex: prev.currentIndex + 1 };\n    });\n  }, [steps.length]);\n\n  const play = useCallback(() => {\n    setState((prev) => {\n      if (prev.currentIndex >= steps.length - 1) {\n        return prev;\n      }\n      return { ...prev, isPlaying: true };\n    });\n  }, [steps.length]);\n\n  const pause = useCallback(() => {\n    clearTimer();\n    setState((prev) => ({ ...prev, isPlaying: false }));\n  }, [clearTimer]);\n\n  const reset = useCallback(() => {\n    clearTimer();\n    setState({ currentIndex: -1, isPlaying: false, speed: state.speed });\n  }, [clearTimer, state.speed]);\n\n  const setSpeed = useCallback((speed: number) => {\n    setState((prev) => ({ ...prev, speed }));\n  }, []);\n\n  useEffect(() => {\n    if (state.isPlaying && state.currentIndex < steps.length - 1) {\n      const delay = 1200 / state.speed;\n      timerRef.current = setTimeout(() => {\n        stepForward();\n      }, delay);\n    } else if (state.isPlaying && state.currentIndex >= steps.length - 1) {\n      setState((prev) => ({ ...prev, isPlaying: false }));\n    }\n    return () => clearTimer();\n  }, [state.isPlaying, state.currentIndex, state.speed, steps.length, stepForward, clearTimer]);\n\n  return {\n    currentIndex: state.currentIndex,\n    isPlaying: state.isPlaying,\n    speed: state.speed,\n    visibleSteps: steps.slice(0, state.currentIndex + 1),\n    totalSteps: steps.length,\n    isComplete: state.currentIndex >= steps.length - 1,\n    play,\n    pause,\n    stepForward,\n    reset,\n    setSpeed,\n  };\n}\n"
  },
  {
    "path": "web/src/hooks/useSteppedVisualization.ts",
    "content": "\"use client\";\n\nimport { useState, useCallback, useEffect, useRef } from \"react\";\n\ninterface SteppedVisualizationOptions {\n  totalSteps: number;\n  autoPlayInterval?: number; // ms, default 2000\n}\n\ninterface SteppedVisualizationReturn {\n  currentStep: number;\n  totalSteps: number;\n  next: () => void;\n  prev: () => void;\n  reset: () => void;\n  goToStep: (step: number) => void;\n  isPlaying: boolean;\n  toggleAutoPlay: () => void;\n  isFirstStep: boolean;\n  isLastStep: boolean;\n}\n\nexport function useSteppedVisualization({\n  totalSteps,\n  autoPlayInterval = 2000,\n}: SteppedVisualizationOptions): SteppedVisualizationReturn {\n  const [currentStep, setCurrentStep] = useState(0);\n  const [isPlaying, setIsPlaying] = useState(false);\n  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n  const next = useCallback(() => {\n    setCurrentStep((prev) => Math.min(prev + 1, totalSteps - 1));\n  }, [totalSteps]);\n\n  const prev = useCallback(() => {\n    setCurrentStep((prev) => Math.max(prev - 1, 0));\n  }, []);\n\n  const reset = useCallback(() => {\n    setCurrentStep(0);\n    setIsPlaying(false);\n  }, []);\n\n  const goToStep = useCallback(\n    (step: number) => {\n      setCurrentStep(Math.max(0, Math.min(step, totalSteps - 1)));\n    },\n    [totalSteps]\n  );\n\n  const toggleAutoPlay = useCallback(() => {\n    setIsPlaying((prev) => !prev);\n  }, []);\n\n  useEffect(() => {\n    if (isPlaying) {\n      intervalRef.current = setInterval(() => {\n        setCurrentStep((prev) => {\n          if (prev >= totalSteps - 1) {\n            setIsPlaying(false);\n            return prev;\n          }\n          return prev + 1;\n        });\n      }, autoPlayInterval);\n    }\n    return () => {\n      if (intervalRef.current) clearInterval(intervalRef.current);\n    };\n  }, [isPlaying, totalSteps, autoPlayInterval]);\n\n  return {\n    currentStep,\n    totalSteps,\n    next,\n    prev,\n    reset,\n    goToStep,\n    isPlaying,\n    toggleAutoPlay,\n    isFirstStep: currentStep === 0,\n    isLastStep: currentStep === totalSteps - 1,\n  };\n}\n"
  },
  {
    "path": "web/src/i18n/messages/en.json",
    "content": "{\n  \"meta\": { \"title\": \"Learn Claude Code\", \"description\": \"Build a nano Claude Code-like agent from 0 to 1, one mechanism at a time\" },\n  \"nav\": { \"home\": \"Home\", \"timeline\": \"Timeline\", \"compare\": \"Compare\", \"layers\": \"Layers\", \"github\": \"GitHub\" },\n  \"home\": { \"hero_title\": \"Learn Claude Code\", \"hero_subtitle\": \"Build a nano Claude Code-like agent from 0 to 1, one mechanism at a time\", \"start\": \"Start Learning\", \"core_pattern\": \"The Core Pattern\", \"core_pattern_desc\": \"Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Production systems add policy, permissions, and lifecycle layers on top.\", \"learning_path\": \"Learning Path\", \"learning_path_desc\": \"12 progressive sessions, from a simple loop to isolated autonomous execution\", \"layers_title\": \"Architectural Layers\", \"layers_desc\": \"Five orthogonal concerns that compose into a complete agent\", \"loc\": \"LOC\", \"learn_more\": \"Learn More\", \"versions_in_layer\": \"versions\", \"message_flow\": \"Message Growth\", \"message_flow_desc\": \"Watch the messages array grow as the agent loop executes\" },\n  \"version\": { \"loc\": \"lines of code\", \"tools\": \"tools\", \"new\": \"New\", \"prev\": \"Previous\", \"next\": \"Next\", \"view_source\": \"View Source\", \"view_diff\": \"View Diff\", \"design_decisions\": \"Design Decisions\", \"whats_new\": \"What's New\", \"tutorial\": \"Tutorial\", \"simulator\": \"Agent Loop Simulator\", \"execution_flow\": \"Execution Flow\", \"architecture\": \"Architecture\", \"concept_viz\": \"Concept Visualization\", \"alternatives\": \"Alternatives Considered\", \"tab_learn\": \"Learn\", \"tab_simulate\": \"Simulate\", \"tab_code\": \"Code\", \"tab_deep_dive\": \"Deep Dive\" },\n  \"sim\": { \"play\": \"Play\", \"pause\": \"Pause\", \"step\": \"Step\", \"reset\": \"Reset\", \"speed\": \"Speed\", \"step_of\": \"of\" },\n  \"timeline\": { \"title\": \"Learning Path\", \"subtitle\": \"s01 to s12: Progressive Agent Design\", \"layer_legend\": \"Layer Legend\", \"loc_growth\": \"LOC Growth\", \"learn_more\": \"Learn More\" },\n  \"layers\": {\n    \"title\": \"Architectural Layers\",\n    \"subtitle\": \"Five orthogonal concerns that compose into a complete agent\",\n    \"tools\": \"What the agent CAN do. The foundation: tools give the model capabilities to interact with the world.\",\n    \"planning\": \"How work is organized. From simple todo lists to dependency-aware task boards shared across agents.\",\n    \"memory\": \"Keeping context within limits. Compression strategies that let agents work infinitely without losing coherence.\",\n    \"concurrency\": \"Non-blocking execution. Background threads and notification buses for parallel work.\",\n    \"collaboration\": \"Multi-agent coordination. Teams, messaging, and autonomous teammates that think for themselves.\"\n  },\n  \"compare\": {\n    \"title\": \"Compare Versions\",\n    \"subtitle\": \"See what changed between any two versions\",\n    \"select_a\": \"Version A\",\n    \"select_b\": \"Version B\",\n    \"loc_delta\": \"LOC Delta\",\n    \"lines\": \"lines\",\n    \"new_tools_in_b\": \"New Tools in B\",\n    \"new_classes_in_b\": \"New Classes in B\",\n    \"new_functions_in_b\": \"New Functions in B\",\n    \"tool_comparison\": \"Tool Comparison\",\n    \"only_in\": \"Only in\",\n    \"shared\": \"Shared\",\n    \"none\": \"None\",\n    \"source_diff\": \"Source Code Diff\",\n    \"empty_hint\": \"Select two versions above to compare them.\",\n    \"architecture\": \"Architecture\"\n  },\n  \"diff\": {\n    \"new_classes\": \"New Classes\",\n    \"new_tools\": \"New Tools\",\n    \"new_functions\": \"New Functions\",\n    \"loc_delta\": \"LOC Delta\"\n  },\n  \"sessions\": {\n    \"s01\": \"The Agent Loop\",\n    \"s02\": \"Tools\",\n    \"s03\": \"TodoWrite\",\n    \"s04\": \"Subagents\",\n    \"s05\": \"Skills\",\n    \"s06\": \"Compact\",\n    \"s07\": \"Tasks\",\n    \"s08\": \"Background Tasks\",\n    \"s09\": \"Agent Teams\",\n    \"s10\": \"Team Protocols\",\n    \"s11\": \"Autonomous Agents\",\n    \"s12\": \"Worktree + Task Isolation\"\n  },\n  \"layer_labels\": {\n    \"tools\": \"Tools & Execution\",\n    \"planning\": \"Planning & Coordination\",\n    \"memory\": \"Memory Management\",\n    \"concurrency\": \"Concurrency\",\n    \"collaboration\": \"Collaboration\"\n  },\n  \"viz\": {\n    \"s01\": \"The Agent While-Loop\",\n    \"s02\": \"Tool Dispatch Map\",\n    \"s03\": \"TodoWrite Nag System\",\n    \"s04\": \"Subagent Context Isolation\",\n    \"s05\": \"On-Demand Skill Loading\",\n    \"s06\": \"Three-Layer Context Compression\",\n    \"s07\": \"Task Dependency Graph\",\n    \"s08\": \"Background Task Lanes\",\n    \"s09\": \"Agent Team Mailboxes\",\n    \"s10\": \"FSM Team Protocols\",\n    \"s11\": \"Autonomous Agent Cycle\",\n    \"s12\": \"Worktree Task Isolation\"\n  }\n}\n"
  },
  {
    "path": "web/src/i18n/messages/ja.json",
    "content": "{\n  \"meta\": { \"title\": \"Learn Claude Code\", \"description\": \"0 から 1 へ nano Claude Code-like agent を構築し、毎回 1 つの仕組みを追加\" },\n  \"nav\": { \"home\": \"ホーム\", \"timeline\": \"学習パス\", \"compare\": \"バージョン比較\", \"layers\": \"アーキテクチャ層\", \"github\": \"GitHub\" },\n  \"home\": { \"hero_title\": \"Learn Claude Code\", \"hero_subtitle\": \"0 から 1 へ nano Claude Code-like agent を構築し、毎回 1 つの仕組みを追加\", \"start\": \"学習を始める\", \"core_pattern\": \"コアパターン\", \"core_pattern_desc\": \"すべての AI コーディングエージェントは同じループを共有する：モデルを呼び出し、ツールを実行し、結果を返す。実運用ではこの上にポリシー、権限、ライフサイクル層が重なる。\", \"learning_path\": \"学習パス\", \"learning_path_desc\": \"12の段階的セッション、シンプルなループから分離された自律実行まで\", \"layers_title\": \"アーキテクチャ層\", \"layers_desc\": \"5つの直交する関心事が完全なエージェントを構成\", \"loc\": \"行\", \"learn_more\": \"詳細を見る\", \"versions_in_layer\": \"バージョン\", \"message_flow\": \"メッセージの増加\", \"message_flow_desc\": \"エージェントループ実行時のメッセージ配列の成長を観察\" },\n  \"version\": { \"loc\": \"行のコード\", \"tools\": \"ツール\", \"new\": \"新規\", \"prev\": \"前のバージョン\", \"next\": \"次のバージョン\", \"view_source\": \"ソースを見る\", \"view_diff\": \"差分を見る\", \"design_decisions\": \"設計判断\", \"whats_new\": \"新機能\", \"tutorial\": \"チュートリアル\", \"simulator\": \"エージェントループシミュレーター\", \"execution_flow\": \"実行フロー\", \"architecture\": \"アーキテクチャ\", \"concept_viz\": \"コンセプト可視化\", \"alternatives\": \"検討された代替案\", \"tab_learn\": \"学習\", \"tab_simulate\": \"シミュレーション\", \"tab_code\": \"ソースコード\", \"tab_deep_dive\": \"詳細分析\" },\n  \"sim\": { \"play\": \"再生\", \"pause\": \"一時停止\", \"step\": \"ステップ\", \"reset\": \"リセット\", \"speed\": \"速度\", \"step_of\": \"/\" },\n  \"timeline\": { \"title\": \"学習パス\", \"subtitle\": \"s01からs12へ：段階的エージェント設計\", \"layer_legend\": \"レイヤー凡例\", \"loc_growth\": \"コード量の推移\", \"learn_more\": \"詳細を見る\" },\n  \"layers\": {\n    \"title\": \"アーキテクチャ層\",\n    \"subtitle\": \"5つの直交する関心事が完全なエージェントを構成\",\n    \"tools\": \"エージェントができること。基盤：ツールがモデルに外部世界と対話する能力を与える。\",\n    \"planning\": \"作業の組織化。シンプルなToDoリストからエージェント間で共有される依存関係対応タスクボードまで。\",\n    \"memory\": \"コンテキスト制限内での記憶保持。圧縮戦略によりエージェントが一貫性を失わずに無限に作業可能。\",\n    \"concurrency\": \"ノンブロッキング実行。バックグラウンドスレッドと通知バスによる並列作業。\",\n    \"collaboration\": \"マルチエージェント連携。チーム、メッセージング、自律的に考えるチームメイト。\"\n  },\n  \"compare\": {\n    \"title\": \"バージョン比較\",\n    \"subtitle\": \"任意の2つのバージョン間の変更を確認\",\n    \"select_a\": \"バージョンA\",\n    \"select_b\": \"バージョンB\",\n    \"loc_delta\": \"コード量の差分\",\n    \"lines\": \"行\",\n    \"new_tools_in_b\": \"Bの新規ツール\",\n    \"new_classes_in_b\": \"Bの新規クラス\",\n    \"new_functions_in_b\": \"Bの新規関数\",\n    \"tool_comparison\": \"ツール比較\",\n    \"only_in\": \"のみ\",\n    \"shared\": \"共通\",\n    \"none\": \"なし\",\n    \"source_diff\": \"ソースコード差分\",\n    \"empty_hint\": \"上で2つのバージョンを選択して比較してください。\",\n    \"architecture\": \"アーキテクチャ\"\n  },\n  \"diff\": {\n    \"new_classes\": \"新規クラス\",\n    \"new_tools\": \"新規ツール\",\n    \"new_functions\": \"新規関数\",\n    \"loc_delta\": \"コード量の差分\"\n  },\n  \"sessions\": {\n    \"s01\": \"エージェントループ\",\n    \"s02\": \"ツール\",\n    \"s03\": \"TodoWrite\",\n    \"s04\": \"サブエージェント\",\n    \"s05\": \"スキル\",\n    \"s06\": \"コンテキスト圧縮\",\n    \"s07\": \"タスクシステム\",\n    \"s08\": \"バックグラウンドタスク\",\n    \"s09\": \"エージェントチーム\",\n    \"s10\": \"チームプロトコル\",\n    \"s11\": \"自律エージェント\",\n    \"s12\": \"Worktree + タスク分離\"\n  },\n  \"layer_labels\": {\n    \"tools\": \"ツールと実行\",\n    \"planning\": \"計画と調整\",\n    \"memory\": \"メモリ管理\",\n    \"concurrency\": \"並行処理\",\n    \"collaboration\": \"コラボレーション\"\n  },\n  \"viz\": {\n    \"s01\": \"エージェント Whileループ\",\n    \"s02\": \"ツールディスパッチマップ\",\n    \"s03\": \"TodoWrite リマインドシステム\",\n    \"s04\": \"サブエージェント コンテキスト分離\",\n    \"s05\": \"オンデマンド スキルローディング\",\n    \"s06\": \"3層コンテキスト圧縮\",\n    \"s07\": \"タスク依存関係グラフ\",\n    \"s08\": \"バックグラウンドタスクレーン\",\n    \"s09\": \"エージェントチーム メールボックス\",\n    \"s10\": \"FSM チームプロトコル\",\n    \"s11\": \"自律エージェントサイクル\",\n    \"s12\": \"Worktree タスク分離\"\n  }\n}\n"
  },
  {
    "path": "web/src/i18n/messages/zh.json",
    "content": "{\n  \"meta\": { \"title\": \"Learn Claude Code\", \"description\": \"从 0 到 1 构建 nano Claude Code-like agent，每次只加一个机制\" },\n  \"nav\": { \"home\": \"首页\", \"timeline\": \"学习路径\", \"compare\": \"版本对比\", \"layers\": \"架构层\", \"github\": \"GitHub\" },\n  \"home\": { \"hero_title\": \"Learn Claude Code\", \"hero_subtitle\": \"从 0 到 1 构建 nano Claude Code-like agent，每次只加一个机制\", \"start\": \"开始学习\", \"core_pattern\": \"核心模式\", \"core_pattern_desc\": \"所有 AI 编程 Agent 共享同一个循环：调用模型、执行工具、回传结果。生产级系统会在其上叠加策略、权限和生命周期层。\", \"learning_path\": \"学习路径\", \"learning_path_desc\": \"12 个渐进式课程，从简单循环到隔离化自治执行\", \"layers_title\": \"架构层次\", \"layers_desc\": \"五个正交关注点组合成完整的 Agent\", \"loc\": \"行\", \"learn_more\": \"了解更多\", \"versions_in_layer\": \"个版本\", \"message_flow\": \"消息增长\", \"message_flow_desc\": \"观察 Agent 循环执行时消息数组的增长\" },\n  \"version\": { \"loc\": \"行代码\", \"tools\": \"个工具\", \"new\": \"新增\", \"prev\": \"上一版\", \"next\": \"下一版\", \"view_source\": \"查看源码\", \"view_diff\": \"查看变更\", \"design_decisions\": \"设计决策\", \"whats_new\": \"新增内容\", \"tutorial\": \"教程\", \"simulator\": \"Agent 循环模拟器\", \"execution_flow\": \"执行流程\", \"architecture\": \"架构\", \"concept_viz\": \"概念可视化\", \"alternatives\": \"替代方案\", \"tab_learn\": \"学习\", \"tab_simulate\": \"模拟\", \"tab_code\": \"源码\", \"tab_deep_dive\": \"深入探索\" },\n  \"sim\": { \"play\": \"播放\", \"pause\": \"暂停\", \"step\": \"单步\", \"reset\": \"重置\", \"speed\": \"速度\", \"step_of\": \"/\" },\n  \"timeline\": { \"title\": \"学习路径\", \"subtitle\": \"s01 到 s12：渐进式 Agent 设计\", \"layer_legend\": \"层次图例\", \"loc_growth\": \"代码量增长\", \"learn_more\": \"了解更多\" },\n  \"layers\": {\n    \"title\": \"架构层次\",\n    \"subtitle\": \"五个正交关注点组合成完整的 Agent\",\n    \"tools\": \"Agent 能做什么。基础层：工具赋予模型与外部世界交互的能力。\",\n    \"planning\": \"如何组织工作。从简单的待办列表到跨 Agent 共享的依赖感知任务板。\",\n    \"memory\": \"在上下文限制内保持记忆。压缩策略让 Agent 可以无限工作而不失去连贯性。\",\n    \"concurrency\": \"非阻塞执行。后台线程和通知总线实现并行工作。\",\n    \"collaboration\": \"多 Agent 协作。团队、消息传递和能独立思考的自主队友。\"\n  },\n  \"compare\": {\n    \"title\": \"版本对比\",\n    \"subtitle\": \"查看任意两个版本之间的变化\",\n    \"select_a\": \"版本 A\",\n    \"select_b\": \"版本 B\",\n    \"loc_delta\": \"代码量差异\",\n    \"lines\": \"行\",\n    \"new_tools_in_b\": \"B 中新增工具\",\n    \"new_classes_in_b\": \"B 中新增类\",\n    \"new_functions_in_b\": \"B 中新增函数\",\n    \"tool_comparison\": \"工具对比\",\n    \"only_in\": \"仅在\",\n    \"shared\": \"共有\",\n    \"none\": \"无\",\n    \"source_diff\": \"源码差异\",\n    \"empty_hint\": \"请在上方选择两个版本进行对比。\",\n    \"architecture\": \"架构\"\n  },\n  \"diff\": {\n    \"new_classes\": \"新增类\",\n    \"new_tools\": \"新增工具\",\n    \"new_functions\": \"新增函数\",\n    \"loc_delta\": \"代码量差异\"\n  },\n  \"sessions\": {\n    \"s01\": \"Agent 循环\",\n    \"s02\": \"工具\",\n    \"s03\": \"TodoWrite\",\n    \"s04\": \"子 Agent\",\n    \"s05\": \"技能\",\n    \"s06\": \"上下文压缩\",\n    \"s07\": \"任务系统\",\n    \"s08\": \"后台任务\",\n    \"s09\": \"Agent 团队\",\n    \"s10\": \"团队协议\",\n    \"s11\": \"自主 Agent\",\n    \"s12\": \"Worktree + 任务隔离\"\n  },\n  \"layer_labels\": {\n    \"tools\": \"工具与执行\",\n    \"planning\": \"规划与协调\",\n    \"memory\": \"内存管理\",\n    \"concurrency\": \"并发\",\n    \"collaboration\": \"协作\"\n  },\n  \"viz\": {\n    \"s01\": \"Agent While 循环\",\n    \"s02\": \"工具分发映射\",\n    \"s03\": \"TodoWrite 提醒系统\",\n    \"s04\": \"子 Agent 上下文隔离\",\n    \"s05\": \"按需技能加载\",\n    \"s06\": \"三层上下文压缩\",\n    \"s07\": \"任务依赖图\",\n    \"s08\": \"后台任务通道\",\n    \"s09\": \"Agent 团队邮箱\",\n    \"s10\": \"FSM 团队协议\",\n    \"s11\": \"自主 Agent 循环\",\n    \"s12\": \"Worktree 任务隔离\"\n  }\n}\n"
  },
  {
    "path": "web/src/lib/constants.ts",
    "content": "export const VERSION_ORDER = [\n  \"s01\", \"s02\", \"s03\", \"s04\", \"s05\", \"s06\", \"s07\", \"s08\", \"s09\", \"s10\", \"s11\", \"s12\"\n] as const;\n\nexport const LEARNING_PATH = VERSION_ORDER;\n\nexport type VersionId = typeof LEARNING_PATH[number];\n\nexport const VERSION_META: Record<string, {\n  title: string;\n  subtitle: string;\n  coreAddition: string;\n  keyInsight: string;\n  layer: \"tools\" | \"planning\" | \"memory\" | \"concurrency\" | \"collaboration\";\n  prevVersion: string | null;\n}> = {\n  s01: { title: \"The Agent Loop\", subtitle: \"Bash is All You Need\", coreAddition: \"Single-tool agent loop\", keyInsight: \"The minimal agent kernel is a while loop + one tool\", layer: \"tools\", prevVersion: null },\n  s02: { title: \"Tools\", subtitle: \"One Handler Per Tool\", coreAddition: \"Tool dispatch map\", keyInsight: \"The loop stays the same; new tools register into the dispatch map\", layer: \"tools\", prevVersion: \"s01\" },\n  s03: { title: \"TodoWrite\", subtitle: \"Plan Before You Act\", coreAddition: \"TodoManager + nag reminder\", keyInsight: \"An agent without a plan drifts; list the steps first, then execute\", layer: \"planning\", prevVersion: \"s02\" },\n  s04: { title: \"Subagents\", subtitle: \"Clean Context Per Subtask\", coreAddition: \"Subagent spawn with isolated messages[]\", keyInsight: \"Subagents use independent messages[], keeping the main conversation clean\", layer: \"planning\", prevVersion: \"s03\" },\n  s05: { title: \"Skills\", subtitle: \"Load on Demand\", coreAddition: \"SkillLoader + two-layer injection\", keyInsight: \"Inject knowledge via tool_result when needed, not upfront in the system prompt\", layer: \"planning\", prevVersion: \"s04\" },\n  s06: { title: \"Compact\", subtitle: \"Three-Layer Compression\", coreAddition: \"micro-compact + auto-compact + archival\", keyInsight: \"Context will fill up; three-layer compression strategy enables infinite sessions\", layer: \"memory\", prevVersion: \"s05\" },\n  s07: { title: \"Tasks\", subtitle: \"Task Graph + Dependencies\", coreAddition: \"TaskManager with file-based state + dependency graph\", keyInsight: \"A file-based task graph with ordering, parallelism, and dependencies -- the coordination backbone for multi-agent work\", layer: \"planning\", prevVersion: \"s06\" },\n  s08: { title: \"Background Tasks\", subtitle: \"Background Threads + Notifications\", coreAddition: \"BackgroundManager + notification queue\", keyInsight: \"Run slow operations in the background; the agent keeps thinking ahead\", layer: \"concurrency\", prevVersion: \"s07\" },\n  s09: { title: \"Agent Teams\", subtitle: \"Teammates + Mailboxes\", coreAddition: \"TeammateManager + file-based mailbox\", keyInsight: \"When one agent can't finish, delegate to persistent teammates via async mailboxes\", layer: \"collaboration\", prevVersion: \"s08\" },\n  s10: { title: \"Team Protocols\", subtitle: \"Shared Communication Rules\", coreAddition: \"request_id correlation for two protocols\", keyInsight: \"One request-response pattern drives all team negotiation\", layer: \"collaboration\", prevVersion: \"s09\" },\n  s11: { title: \"Autonomous Agents\", subtitle: \"Scan Board, Claim Tasks\", coreAddition: \"Task board polling + timeout-based self-governance\", keyInsight: \"Teammates scan the board and claim tasks themselves; no need for the lead to assign each one\", layer: \"collaboration\", prevVersion: \"s10\" },\n  s12: { title: \"Worktree + Task Isolation\", subtitle: \"Isolate by Directory\", coreAddition: \"Composable worktree lifecycle + event stream over a shared task board\", keyInsight: \"Each works in its own directory; tasks manage goals, worktrees manage directories, bound by ID\", layer: \"collaboration\", prevVersion: \"s11\" },\n};\n\nexport const LAYERS = [\n  { id: \"tools\" as const, label: \"Tools & Execution\", color: \"#3B82F6\", versions: [\"s01\", \"s02\"] },\n  { id: \"planning\" as const, label: \"Planning & Coordination\", color: \"#10B981\", versions: [\"s03\", \"s04\", \"s05\", \"s07\"] },\n  { id: \"memory\" as const, label: \"Memory Management\", color: \"#8B5CF6\", versions: [\"s06\"] },\n  { id: \"concurrency\" as const, label: \"Concurrency\", color: \"#F59E0B\", versions: [\"s08\"] },\n  { id: \"collaboration\" as const, label: \"Collaboration\", color: \"#EF4444\", versions: [\"s09\", \"s10\", \"s11\", \"s12\"] },\n] as const;\n"
  },
  {
    "path": "web/src/lib/i18n-server.ts",
    "content": "import en from \"@/i18n/messages/en.json\";\nimport zh from \"@/i18n/messages/zh.json\";\nimport ja from \"@/i18n/messages/ja.json\";\n\ntype Messages = typeof en;\n\nconst messagesMap: Record<string, Messages> = { en, zh, ja };\n\nexport function getTranslations(locale: string, namespace: string) {\n  const messages = messagesMap[locale] || en;\n  const ns = (messages as Record<string, Record<string, string>>)[namespace];\n  const fallbackNs = (en as Record<string, Record<string, string>>)[namespace];\n  return (key: string): string => {\n    return ns?.[key] || fallbackNs?.[key] || key;\n  };\n}\n"
  },
  {
    "path": "web/src/lib/i18n.tsx",
    "content": "\"use client\";\nimport { createContext, useContext, ReactNode } from \"react\";\nimport en from \"@/i18n/messages/en.json\";\nimport zh from \"@/i18n/messages/zh.json\";\nimport ja from \"@/i18n/messages/ja.json\";\n\ntype Messages = typeof en;\n\nconst messagesMap: Record<string, Messages> = { en, zh, ja };\n\nconst I18nContext = createContext<{ locale: string; messages: Messages }>({\n  locale: \"en\",\n  messages: en,\n});\n\nexport function I18nProvider({ locale, children }: { locale: string; children: ReactNode }) {\n  const messages = messagesMap[locale] || en;\n  return (\n    <I18nContext.Provider value={{ locale, messages }}>\n      {children}\n    </I18nContext.Provider>\n  );\n}\n\nexport function useTranslations(namespace?: string) {\n  const { messages } = useContext(I18nContext);\n  return (key: string) => {\n    const ns = namespace ? (messages as any)[namespace] : messages;\n    if (!ns) return key;\n    return (ns as any)[key] || key;\n  };\n}\n\nexport function useLocale() {\n  return useContext(I18nContext).locale;\n}\n"
  },
  {
    "path": "web/src/lib/utils.ts",
    "content": "export function cn(...classes: (string | undefined | null | false)[]) {\n  return classes.filter(Boolean).join(\" \");\n}\n"
  },
  {
    "path": "web/src/types/agent-data.ts",
    "content": "export interface AgentVersion {\n  id: string;\n  filename: string;\n  title: string;\n  subtitle: string;\n  loc: number;\n  tools: string[];\n  newTools: string[];\n  coreAddition: string;\n  keyInsight: string;\n  classes: { name: string; startLine: number; endLine: number }[];\n  functions: { name: string; signature: string; startLine: number }[];\n  layer: \"tools\" | \"planning\" | \"memory\" | \"concurrency\" | \"collaboration\";\n  source: string;\n}\n\nexport interface VersionDiff {\n  from: string;\n  to: string;\n  newClasses: string[];\n  newFunctions: string[];\n  newTools: string[];\n  locDelta: number;\n}\n\nexport interface DocContent {\n  version: string;\n  locale: \"en\" | \"zh\" | \"ja\";\n  title: string;\n  content: string; // raw markdown\n}\n\nexport interface VersionIndex {\n  versions: AgentVersion[];\n  diffs: VersionDiff[];\n}\n\nexport type SimStepType =\n  | \"user_message\"\n  | \"assistant_text\"\n  | \"tool_call\"\n  | \"tool_result\"\n  | \"system_event\";\n\nexport interface SimStep {\n  type: SimStepType;\n  content: string;\n  annotation: string;\n  toolName?: string;\n  toolInput?: string;\n}\n\nexport interface Scenario {\n  version: string;\n  title: string;\n  description: string;\n  steps: SimStep[];\n}\n\nexport interface FlowNode {\n  id: string;\n  label: string;\n  type: \"start\" | \"process\" | \"decision\" | \"subprocess\" | \"end\";\n  x: number;\n  y: number;\n}\n\nexport interface FlowEdge {\n  from: string;\n  to: string;\n  label?: string;\n}\n"
  },
  {
    "path": "web/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2018\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\",\n    \"**/*.mts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "web/vercel.json",
    "content": "{\n  \"redirects\": [\n    {\n      \"source\": \"/:path(.*)\",\n      \"has\": [\n        {\n          \"type\": \"host\",\n          \"value\": \"learn-claude-agents.vercel.app\"\n        }\n      ],\n      \"destination\": \"https://learn.shareai.run/:path\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/\",\n      \"destination\": \"/en\",\n      \"permanent\": false\n    }\n  ]\n}\n"
  }
]