diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3eb47f9c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +This is a curated teaching repository. Each chapter (`sNN_topic/`) builds one idea on top of the last, and the code is deliberately minimal. The simplifications are intentional, so the lesson stays legible. Contributions are welcome when they serve that goal. + +## Before opening a PR + +1. Tie it to a specific issue. Say what is wrong and why your change is the right fix. One PR addresses one issue. +2. Keep teaching code minimal. Do not add production hardening, defensive parsing, error handling layers, abstractions, or a test framework unless the chapter is specifically about that. A simplification that looks like a bug is often deliberate, so ask in an issue first. +3. Keep the three languages in sync. Any change to a chapter's `code.py` or a README must be reflected in all three language READMEs in that chapter folder, with the code blocks identical across them. +4. Target the current course files (`sNN_topic/`), not legacy mirrors. +5. Disclose AI assistance. Using an agent is fine. Say so, and make sure you have read and stand behind the change. + +## What gets closed without detailed review + +To keep the review queue useful, we close: + +1. Bulk PRs generated by an agent and opened across many unrelated repositories at once. We cannot responsibly review code the author has not engaged with. +2. Changes that expand a chapter's scope or structure against the teaching design. +3. PRs not tied to an issue, or that restate a deliberate simplification as a bug. + +Focused, clearly explained contributions from people who have actually worked with the material are always welcome. + + +# 贡献指南 + +这是一个**教学仓库**。每一章(`sNN_topic/`)在上一章的基础上只讲清一个点,代码刻意保持最简。简化是有意为之,为的是让讲解保持清晰。欢迎服务于这个目标的贡献。 + +## 提 PR 之前 + +1. 对应一个具体 issue。说清问题是什么、为什么你的改法是对的。一个 PR 只解决一个问题。 +2. 教学代码保持最简。不要加生产级的防御解析、错误处理层、抽象或测试框架,除非这一章本身就在讲这些。某处"像 bug"的简化往往是刻意的,先在 issue 里问一声。 +3. 三语保持同步。对某章 `code.py` 或 README 的改动,必须同步到该章目录下三种语言的 README,且其中代码块完全一致。 +4. 改现行课程文件(`sNN_topic/`),不要改旧的镜像目录。 +5. 声明 AI 协助。用 agent 没问题,但请说明,并确保你读过、也认可这份改动。 + +## 哪些会被直接关闭(不做详细评审) + +1. 在大量互不相关的仓库里一次性批量提交、由 agent 生成的 PR。作者自己都没参与的代码,我们无法负责任地评审。 +2. 让某章范围或结构超出教学设计的改动。 +3. 没有对应 issue、或把刻意的简化当成 bug 来"修"的 PR。 + +真正用过这份材料、聚焦且讲清理由的贡献,我们始终欢迎。 diff --git a/README-ja.md b/README-ja.md index 08a4812e..794d9baa 100644 --- a/README-ja.md +++ b/README-ja.md @@ -409,13 +409,13 @@ learn-claude-code/ Skill & LSP 対応、Windows 対応、GLM / MiniMax / DeepSeek 等のオープンモデルに接続可能。インストールしてすぐ使える。 -GitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)** +GitHub: **[shareAI-lab/Kode-CLI](https://github.com/shareAI-lab/Kode-CLI)** ### Kode Agent SDK -- アプリにエージェント機能を埋め込む 公式 Claude Code Agent SDK は内部で完全な CLI プロセスと通信する -- 同時ユーザーごとに独立のターミナルプロセスが必要。Kode SDK は独立ライブラリでユーザーごとのプロセスオーバーヘッドがなく、バックエンド、ブラウザ拡張、組み込みデバイス等に埋め込み可能。 -GitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)** +GitHub: **[shareAI-lab/kode-agent-sdk](https://github.com/shareAI-lab/kode-agent-sdk)** --- diff --git a/README-zh.md b/README-zh.md index 519e5f01..79c92340 100644 --- a/README-zh.md +++ b/README-zh.md @@ -410,13 +410,13 @@ learn-claude-code/ 支持 Skill & LSP, 适配 Windows, 可接 GLM / MiniMax / DeepSeek 等开放模型。装完即用。 -GitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)** +GitHub: **[shareAI-lab/Kode-CLI](https://github.com/shareAI-lab/Kode-CLI)** ### Kode Agent SDK -- 把 Agent 能力嵌入你的应用 官方 Claude Code Agent SDK 底层与完整 CLI 进程通信 -- 每个并发用户 = 一个终端进程。Kode SDK 是独立库, 无 per-user 进程开销, 可嵌入后端、浏览器插件、嵌入式设备等任意运行时。 -GitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)** +GitHub: **[shareAI-lab/kode-agent-sdk](https://github.com/shareAI-lab/kode-agent-sdk)** --- diff --git a/README.md b/README.md index e9f0c931..5acc0f20 100644 --- a/README.md +++ b/README.md @@ -415,7 +415,7 @@ After 20 lessons, you understand harness engineering from the inside out. Two pa Skill and LSP support, Windows compatible, works with GLM / MiniMax / DeepSeek and other open models. Install and go. -GitHub: **[shareAI-lab/Kode-Agent](https://github.com/shareAI-lab/Kode-Agent)** +GitHub: **[shareAI-lab/Kode-CLI](https://github.com/shareAI-lab/Kode-CLI)** ### Kode Agent SDK -- Embed Agent Capabilities in Your Application diff --git a/agents/s09_agent_teams.py b/agents/s09_agent_teams.py index 90f6760d..bd666552 100644 --- a/agents/s09_agent_teams.py +++ b/agents/s09_agent_teams.py @@ -260,7 +260,7 @@ def _safe_path(p: str) -> Path: def _run_bash(command: str) -> str: - dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"] + dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: diff --git a/agents/s10_team_protocols.py b/agents/s10_team_protocols.py index d5475359..3f9923da 100644 --- a/agents/s10_team_protocols.py +++ b/agents/s10_team_protocols.py @@ -301,7 +301,7 @@ def _safe_path(p: str) -> Path: def _run_bash(command: str) -> str: - dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"] + dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: diff --git a/agents/s11_autonomous_agents.py b/agents/s11_autonomous_agents.py index 3aec416b..c3a62cda 100644 --- a/agents/s11_autonomous_agents.py +++ b/agents/s11_autonomous_agents.py @@ -389,7 +389,7 @@ def _safe_path(p: str) -> Path: def _run_bash(command: str) -> str: - dangerous = ["rm -rf /", "sudo", "shutdown", "reboot"] + dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked" try: diff --git a/docs/en/s01-the-agent-loop.md b/docs/en/s01-the-agent-loop.md index 40564686..1ef11c72 100644 --- a/docs/en/s01-the-agent-loop.md +++ b/docs/en/s01-the-agent-loop.md @@ -1,6 +1,6 @@ # s01: The Agent Loop -`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` +`[ s01 ] > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` > *"One loop & Bash is all you need"* -- one tool + one loop = an agent. > diff --git a/docs/en/s02-tool-use.md b/docs/en/s02-tool-use.md index 279774b8..9d16a445 100644 --- a/docs/en/s02-tool-use.md +++ b/docs/en/s02-tool-use.md @@ -1,6 +1,6 @@ # s02: Tool Use -`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` +`s01 > [ s02 ] > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` > *"Adding a tool means adding one handler"* -- the loop stays the same; new tools register into the dispatch map. > diff --git a/docs/en/s03-todo-write.md b/docs/en/s03-todo-write.md index e4461147..b72d49d7 100644 --- a/docs/en/s03-todo-write.md +++ b/docs/en/s03-todo-write.md @@ -1,6 +1,6 @@ # s03: TodoWrite -`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` +`s01 > s02 > [ s03 ] > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` > *"An agent without a plan drifts"* -- list the steps first, then execute. > diff --git a/docs/en/s04-subagent.md b/docs/en/s04-subagent.md index 8a6ff2a6..c830eda2 100644 --- a/docs/en/s04-subagent.md +++ b/docs/en/s04-subagent.md @@ -1,6 +1,6 @@ # s04: Subagents -`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` +`s01 > s02 > s03 > [ s04 ] > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` > *"Break big tasks down; each subtask gets a clean context"* -- subagents use independent messages[], keeping the main conversation clean. > diff --git a/docs/en/s05-skill-loading.md b/docs/en/s05-skill-loading.md index 0cf19385..1e81a611 100644 --- a/docs/en/s05-skill-loading.md +++ b/docs/en/s05-skill-loading.md @@ -1,6 +1,6 @@ # s05: Skills -`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12` +`s01 > s02 > s03 > s04 > [ s05 ] > s06 | s07 > s08 > s09 > s10 > s11 > s12` > *"Load knowledge when you need it, not upfront"* -- inject via tool_result, not the system prompt. > diff --git a/docs/en/s07-task-system.md b/docs/en/s07-task-system.md index b110d0ca..562c6425 100644 --- a/docs/en/s07-task-system.md +++ b/docs/en/s07-task-system.md @@ -1,6 +1,6 @@ # s07: Task System -`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12` +`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] > s08 > s09 > s10 > s11 > s12` > *"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. > diff --git a/docs/en/s08-background-tasks.md b/docs/en/s08-background-tasks.md index 5a98f212..7fdb92b7 100644 --- a/docs/en/s08-background-tasks.md +++ b/docs/en/s08-background-tasks.md @@ -1,6 +1,6 @@ # s08: Background Tasks -`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12` +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] > s09 > s10 > s11 > s12` > *"Run slow operations in the background; the agent keeps thinking"* -- daemon threads run commands, inject notifications on completion. > diff --git a/docs/en/s09-agent-teams.md b/docs/en/s09-agent-teams.md index 9f19723a..e69aa3dd 100644 --- a/docs/en/s09-agent-teams.md +++ b/docs/en/s09-agent-teams.md @@ -1,6 +1,6 @@ # s09: Agent Teams -`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12` +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] > s10 > s11 > s12` > *"When the task is too big for one, delegate to teammates"* -- persistent teammates + async mailboxes. > diff --git a/docs/en/s10-team-protocols.md b/docs/en/s10-team-protocols.md index e784e5ee..3513de67 100644 --- a/docs/en/s10-team-protocols.md +++ b/docs/en/s10-team-protocols.md @@ -1,6 +1,6 @@ # s10: Team Protocols -`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12` +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] > s11 > s12` > *"Teammates need shared communication rules"* -- one request-response pattern drives all negotiation. > diff --git a/docs/en/s11-autonomous-agents.md b/docs/en/s11-autonomous-agents.md index a3c28367..8af6aada 100644 --- a/docs/en/s11-autonomous-agents.md +++ b/docs/en/s11-autonomous-agents.md @@ -1,6 +1,6 @@ # s11: Autonomous Agents -`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12` +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] > s12` > *"Teammates scan the board and claim tasks themselves"* -- no need for the lead to assign each one. > diff --git a/s12_task_system/code.py b/s12_task_system/code.py index d9da2527..7f442ebf 100644 --- a/s12_task_system/code.py +++ b/s12_task_system/code.py @@ -373,4 +373,6 @@ if __name__ == "__main__": for block in history[-1]["content"]: if getattr(block, "type", None) == "text": print(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + print(block.get("text", "")) print() diff --git a/s13_background_tasks/code.py b/s13_background_tasks/code.py index 49a03ad2..a96eabca 100644 --- a/s13_background_tasks/code.py +++ b/s13_background_tasks/code.py @@ -475,4 +475,6 @@ if __name__ == "__main__": for block in history[-1]["content"]: if getattr(block, "type", None) == "text": print(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + print(block.get("text", "")) print() diff --git a/s15_agent_teams/code.py b/s15_agent_teams/code.py index 5b0fd7d3..143a73e8 100644 --- a/s15_agent_teams/code.py +++ b/s15_agent_teams/code.py @@ -973,6 +973,8 @@ if __name__ == "__main__": for block in history[-1]["content"]: if getattr(block, "type", None) == "text": print(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + print(block.get("text", "")) # Announce once when every teammate has finished and its output drained. if active_teammates: diff --git a/s16_team_protocols/code.py b/s16_team_protocols/code.py index c1ec6636..d7993fb9 100644 --- a/s16_team_protocols/code.py +++ b/s16_team_protocols/code.py @@ -868,6 +868,8 @@ if __name__ == "__main__": for block in history[-1]["content"]: if getattr(block, "type", None) == "text": print(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + print(block.get("text", "")) # Check inbox → route protocol + inject into history inbox_msgs = consume_lead_inbox(route_protocol=True) diff --git a/s17_autonomous_agents/README.en.md b/s17_autonomous_agents/README.en.md index 4269b498..2d676faf 100644 --- a/s17_autonomous_agents/README.en.md +++ b/s17_autonomous_agents/README.en.md @@ -42,13 +42,13 @@ After completing a task, the teammate doesn't exit. It enters the IDLE phase — IDLE_POLL_INTERVAL = 5 # seconds IDLE_TIMEOUT = 60 # seconds -def idle_poll(agent_name, messages, name, role) -> str: +def idle_poll(name, messages, role) -> str: """Return 'work', 'shutdown', or 'timeout'.""" for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL): time.sleep(IDLE_POLL_INTERVAL) # ① Check inbox (priority) - inbox = BUS.read_inbox(agent_name) + inbox = BUS.read_inbox(name) if inbox: # shutdown_request handled immediately for msg in inbox: @@ -63,7 +63,7 @@ def idle_poll(agent_name, messages, name, role) -> str: unclaimed = scan_unclaimed_tasks() if unclaimed: task = unclaimed[0] - result = claim_task(task["id"], agent_name) + result = claim_task(task["id"], name) if "Claimed" in result: messages.append(...) return "work" @@ -126,7 +126,7 @@ while True: break # WORK phase ends # IDLE phase - idle_result = idle_poll(name, messages, name, role) + idle_result = idle_poll(name, messages, role) if idle_result == "shutdown": break if idle_result == "timeout": @@ -268,4 +268,4 @@ Teaching version's `idle_poll()` merges CC's four mechanisms into one polling fu - + diff --git a/s17_autonomous_agents/README.ja.md b/s17_autonomous_agents/README.ja.md index 1de34c37..a88642d8 100644 --- a/s17_autonomous_agents/README.ja.md +++ b/s17_autonomous_agents/README.ja.md @@ -42,13 +42,13 @@ S16 の教学版 MessageBus とプロトコルツールを踏襲。本章の追 IDLE_POLL_INTERVAL = 5 # seconds IDLE_TIMEOUT = 60 # seconds -def idle_poll(agent_name, messages, name, role) -> str: +def idle_poll(name, messages, role) -> str: """Return 'work', 'shutdown', or 'timeout'.""" for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL): time.sleep(IDLE_POLL_INTERVAL) # ① 受信箱確認(優先) - inbox = BUS.read_inbox(agent_name) + inbox = BUS.read_inbox(name) if inbox: # shutdown_request は即座に処理 for msg in inbox: @@ -63,7 +63,7 @@ def idle_poll(agent_name, messages, name, role) -> str: unclaimed = scan_unclaimed_tasks() if unclaimed: task = unclaimed[0] - result = claim_task(task["id"], agent_name) + result = claim_task(task["id"], name) if "Claimed" in result: messages.append(...) return "work" @@ -126,7 +126,7 @@ while True: break # WORK フェーズ終了 # IDLE フェーズ - idle_result = idle_poll(name, messages, name, role) + idle_result = idle_poll(name, messages, role) if idle_result == "shutdown": break if idle_result == "timeout": @@ -268,4 +268,4 @@ s18 Worktree Isolation → 各タスクに専用の作業ディレクトリ、 - + diff --git a/s17_autonomous_agents/README.md b/s17_autonomous_agents/README.md index 769da6ca..b3192c2d 100644 --- a/s17_autonomous_agents/README.md +++ b/s17_autonomous_agents/README.md @@ -42,13 +42,13 @@ s16 的队友能通信、能握手关机。但每个队友等 Lead 分配任务 IDLE_POLL_INTERVAL = 5 # seconds IDLE_TIMEOUT = 60 # seconds -def idle_poll(agent_name, messages, name, role) -> str: +def idle_poll(name, messages, role) -> str: """Return 'work', 'shutdown', or 'timeout'.""" for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL): time.sleep(IDLE_POLL_INTERVAL) # ① 检查收件箱(优先) - inbox = BUS.read_inbox(agent_name) + inbox = BUS.read_inbox(name) if inbox: # shutdown_request 立即处理 for msg in inbox: @@ -63,7 +63,7 @@ def idle_poll(agent_name, messages, name, role) -> str: unclaimed = scan_unclaimed_tasks() if unclaimed: task = unclaimed[0] - result = claim_task(task["id"], agent_name) + result = claim_task(task["id"], name) if "Claimed" in result: messages.append(...) return "work" @@ -126,7 +126,7 @@ while True: break # WORK 阶段结束 # IDLE phase - idle_result = idle_poll(name, messages, name, role) + idle_result = idle_poll(name, messages, role) if idle_result == "shutdown": break if idle_result == "timeout": @@ -268,4 +268,4 @@ s18 Worktree Isolation → 每个任务有自己的工作目录,互不干扰 - + diff --git a/s17_autonomous_agents/code.py b/s17_autonomous_agents/code.py index 318e2f3c..71d97beb 100644 --- a/s17_autonomous_agents/code.py +++ b/s17_autonomous_agents/code.py @@ -301,14 +301,13 @@ def scan_unclaimed_tasks() -> list[dict]: return unclaimed -def idle_poll(agent_name: str, messages: list, - name: str, role: str) -> str: +def idle_poll(name: str, messages: list, role: str) -> str: """Poll for 60s. Return 'work', 'shutdown', or 'timeout'.""" for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL): time.sleep(IDLE_POLL_INTERVAL) # Check inbox — dispatch protocol messages first - inbox = BUS.read_inbox(agent_name) + inbox = BUS.read_inbox(name) if inbox: # Check for shutdown_request for msg in inbox: @@ -331,7 +330,7 @@ def idle_poll(agent_name: str, messages: list, unclaimed = scan_unclaimed_tasks() if unclaimed: task = unclaimed[0] - result = claim_task(task["id"], agent_name) + result = claim_task(task["id"], name) if "Claimed" in result: messages.append({"role": "user", "content": f"Task {task['id']}: " @@ -498,7 +497,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str: break # IDLE phase (s17 new) - idle_result = idle_poll(name, messages, name, role) + idle_result = idle_poll(name, messages, role) if idle_result == "shutdown": break if idle_result == "timeout": @@ -800,6 +799,8 @@ if __name__ == "__main__": for block in history[-1]["content"]: if getattr(block, "type", None) == "text": print(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + print(block.get("text", "")) # Consume lead inbox: route protocol + inject into history inbox = consume_lead_inbox(route_protocol=True) diff --git a/s18_worktree_isolation/code.py b/s18_worktree_isolation/code.py index c1ed9d75..c00fcf44 100644 --- a/s18_worktree_isolation/code.py +++ b/s18_worktree_isolation/code.py @@ -984,6 +984,8 @@ if __name__ == "__main__": for block in history[-1]["content"]: if getattr(block, "type", None) == "text": print(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + print(block.get("text", "")) # Consume lead inbox: route protocol + inject into history inbox = consume_lead_inbox(route_protocol=True) diff --git a/s19_mcp_plugin/code.py b/s19_mcp_plugin/code.py index fb3c6c04..eed5acec 100644 --- a/s19_mcp_plugin/code.py +++ b/s19_mcp_plugin/code.py @@ -1013,6 +1013,8 @@ if __name__ == "__main__": for block in history[-1]["content"]: if getattr(block, "type", None) == "text": print(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + print(block.get("text", "")) inbox = consume_lead_inbox(route_protocol=True) if inbox: diff --git a/s20_comprehensive/code.py b/s20_comprehensive/code.py index 543bab11..722aba15 100644 --- a/s20_comprehensive/code.py +++ b/s20_comprehensive/code.py @@ -2063,8 +2063,8 @@ def print_turn_assistants(messages: list, turn_start: int): if msg.get("role") != "assistant": continue for block in msg.get("content", []): - if getattr(block, "type", None) == "text": - terminal_print(block.text) + if block_type(block) == "text": + terminal_print(block["text"] if isinstance(block, dict) else block.text) def cron_autorun_loop(history: list, context: dict):