coderio 0.4.3__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. {coderio-0.4.3 → coderio-0.4.4}/.github/workflows/ci.yml +7 -5
  2. {coderio-0.4.3 → coderio-0.4.4}/CHANGELOG.md +43 -0
  3. {coderio-0.4.3 → coderio-0.4.4}/PKG-INFO +7 -3
  4. {coderio-0.4.3 → coderio-0.4.4}/README.md +6 -2
  5. {coderio-0.4.3 → coderio-0.4.4}/README_en.md +6 -2
  6. {coderio-0.4.3 → coderio-0.4.4}/docs/coderio-architecture.md +16 -11
  7. {coderio-0.4.3 → coderio-0.4.4}/pyproject.toml +4 -3
  8. coderio-0.4.4/src/coderio/agent/custom_agents.py +94 -0
  9. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/deep_loop.py +139 -24
  10. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/harness.py +7 -5
  11. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/harness_middleware.py +16 -2
  12. coderio-0.4.4/src/coderio/agent/plan_artifact.py +130 -0
  13. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/prompts.py +2 -1
  14. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/commands.py +35 -4
  15. coderio-0.4.4/src/coderio/cli/custom_commands.py +150 -0
  16. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/tui.py +37 -291
  17. coderio-0.4.4/src/coderio/cli/tui_runtime.py +375 -0
  18. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/session/message.py +3 -1
  19. coderio-0.4.4/src/coderio/skills/parser.py +77 -0
  20. coderio-0.4.4/src/coderio/tools/checkpoint.py +105 -0
  21. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/edit_file.py +5 -0
  22. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/multi_edit.py +4 -0
  23. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/write_file.py +6 -1
  24. coderio-0.4.4/tests/agent/test_custom_agents.py +190 -0
  25. coderio-0.4.4/tests/agent/test_plan_artifact.py +180 -0
  26. coderio-0.4.4/tests/cli/test_custom_commands.py +327 -0
  27. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_tui_picker.py +1 -1
  28. coderio-0.4.4/tests/cli/test_tui_runtime.py +138 -0
  29. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_ux_features.py +2 -1
  30. {coderio-0.4.3 → coderio-0.4.4}/tests/test_seams.py +669 -0
  31. coderio-0.4.4/tests/tools/test_checkpoint.py +318 -0
  32. coderio-0.4.3/src/coderio/skills/parser.py +0 -50
  33. {coderio-0.4.3 → coderio-0.4.4}/.github/dependabot.yml +0 -0
  34. {coderio-0.4.3 → coderio-0.4.4}/.github/workflows/release.yml +0 -0
  35. {coderio-0.4.3 → coderio-0.4.4}/.gitignore +0 -0
  36. {coderio-0.4.3 → coderio-0.4.4}/CONTRIBUTING.md +0 -0
  37. {coderio-0.4.3 → coderio-0.4.4}/LICENSE +0 -0
  38. {coderio-0.4.3 → coderio-0.4.4}/THIRD_PARTY_LICENSES.md +0 -0
  39. {coderio-0.4.3 → coderio-0.4.4}/scripts/verify_deepagent_live.py +0 -0
  40. {coderio-0.4.3 → coderio-0.4.4}/scripts/verify_harness_live.py +0 -0
  41. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/__init__.py +0 -0
  42. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/__init__.py +0 -0
  43. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/_deepagents_compat.py +0 -0
  44. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/command_review.py +0 -0
  45. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/hooks.py +0 -0
  46. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/permission_middleware.py +0 -0
  47. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/skill_tool.py +0 -0
  48. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/state.py +0 -0
  49. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/agent/stream.py +0 -0
  50. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/__init__.py +0 -0
  51. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/app.py +0 -0
  52. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/credentials.py +0 -0
  53. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/mcp_cmd.py +0 -0
  54. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/multimodal.py +0 -0
  55. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/onboarding.py +0 -0
  56. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/providers.py +0 -0
  57. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/render.py +0 -0
  58. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/repl.py +0 -0
  59. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/run_cmd.py +0 -0
  60. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/skills_cmd.py +0 -0
  61. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/stream.py +0 -0
  62. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/tui_onboarding.py +0 -0
  63. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/tui_screens.py +0 -0
  64. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/cli/tui_widgets.py +0 -0
  65. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/config/__init__.py +0 -0
  66. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/config/bootstrap.py +0 -0
  67. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/config/loader.py +0 -0
  68. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/config/models.py +0 -0
  69. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/config/trust.py +0 -0
  70. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/llm/__init__.py +0 -0
  71. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/llm/factory.py +0 -0
  72. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/llm/probe.py +0 -0
  73. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/mcp_loader.py +0 -0
  74. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/session/__init__.py +0 -0
  75. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/session/store.py +0 -0
  76. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/__init__.py +0 -0
  77. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/bundled/executing-plans/SKILL.md +0 -0
  78. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/LICENSE +0 -0
  79. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/README.md +0 -0
  80. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +0 -0
  81. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +0 -0
  82. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/code-review/SKILL.md +0 -0
  83. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/code-review/evals/evals.json +0 -0
  84. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/commit-message/SKILL.md +0 -0
  85. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/commit-message/evals/evals.json +0 -0
  86. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/debugging/SKILL.md +0 -0
  87. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/debugging/evals/evals.json +0 -0
  88. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/error-handling/SKILL.md +0 -0
  89. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/error-handling/evals/evals.json +0 -0
  90. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +0 -0
  91. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +0 -0
  92. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/naming/SKILL.md +0 -0
  93. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/naming/evals/evals.json +0 -0
  94. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +0 -0
  95. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +0 -0
  96. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/spec-writing/SKILL.md +0 -0
  97. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +0 -0
  98. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +0 -0
  99. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +0 -0
  100. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/testing/SKILL.md +0 -0
  101. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/testing/evals/evals.json +0 -0
  102. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +0 -0
  103. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +0 -0
  104. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/models.py +0 -0
  105. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/skills/store.py +0 -0
  106. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/__init__.py +0 -0
  107. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/base.py +0 -0
  108. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/bash.py +0 -0
  109. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/command_policy.py +0 -0
  110. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/glob_tool.py +0 -0
  111. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/grep_tool.py +0 -0
  112. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/linux_sandbox.py +0 -0
  113. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/list_dir.py +0 -0
  114. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/note.py +0 -0
  115. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/permission.py +0 -0
  116. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/read_file.py +0 -0
  117. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/sandbox_runner.py +0 -0
  118. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/todo.py +0 -0
  119. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/web_fetch.py +0 -0
  120. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/web_search.py +0 -0
  121. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/win_job.py +0 -0
  122. {coderio-0.4.3 → coderio-0.4.4}/src/coderio/tools/win_sandbox.py +0 -0
  123. {coderio-0.4.3 → coderio-0.4.4}/tests/__init__.py +0 -0
  124. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/__init__.py +0 -0
  125. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/conftest.py +0 -0
  126. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_command_review.py +0 -0
  127. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_deep_integration.py +0 -0
  128. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_deep_loop.py +0 -0
  129. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_deep_loop_unit.py +0 -0
  130. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_deepagents_compat.py +0 -0
  131. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_harness.py +0 -0
  132. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_harness_middleware.py +0 -0
  133. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_hooks.py +0 -0
  134. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_perf_baseline.py +0 -0
  135. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_prompts.py +0 -0
  136. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_skill_tool.py +0 -0
  137. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_state.py +0 -0
  138. {coderio-0.4.3 → coderio-0.4.4}/tests/agent/test_stream.py +0 -0
  139. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/__init__.py +0 -0
  140. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_app.py +0 -0
  141. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_command_menu.py +0 -0
  142. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_commands.py +0 -0
  143. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_credentials.py +0 -0
  144. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_input_isolation.py +0 -0
  145. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_live_thinking.py +0 -0
  146. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_mcp_cmd.py +0 -0
  147. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_multimodal.py +0 -0
  148. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_onboarding.py +0 -0
  149. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_onboarding_logic.py +0 -0
  150. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_providers.py +0 -0
  151. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_render.py +0 -0
  152. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_repl_loop.py +0 -0
  153. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_repl_reset.py +0 -0
  154. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_run_cmd.py +0 -0
  155. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_skills_cmd.py +0 -0
  156. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_status_bar.py +0 -0
  157. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_stream.py +0 -0
  158. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_truncation.py +0 -0
  159. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_tui_onboarding.py +0 -0
  160. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_tui_screens.py +0 -0
  161. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_tui_startup.py +0 -0
  162. {coderio-0.4.3 → coderio-0.4.4}/tests/cli/test_tui_widgets.py +0 -0
  163. {coderio-0.4.3 → coderio-0.4.4}/tests/config/__init__.py +0 -0
  164. {coderio-0.4.3 → coderio-0.4.4}/tests/config/test_bootstrap.py +0 -0
  165. {coderio-0.4.3 → coderio-0.4.4}/tests/config/test_loader.py +0 -0
  166. {coderio-0.4.3 → coderio-0.4.4}/tests/config/test_models.py +0 -0
  167. {coderio-0.4.3 → coderio-0.4.4}/tests/config/test_trust.py +0 -0
  168. {coderio-0.4.3 → coderio-0.4.4}/tests/conftest.py +0 -0
  169. {coderio-0.4.3 → coderio-0.4.4}/tests/e2e/__init__.py +0 -0
  170. {coderio-0.4.3 → coderio-0.4.4}/tests/llm/__init__.py +0 -0
  171. {coderio-0.4.3 → coderio-0.4.4}/tests/llm/test_factory.py +0 -0
  172. {coderio-0.4.3 → coderio-0.4.4}/tests/llm/test_probe.py +0 -0
  173. {coderio-0.4.3 → coderio-0.4.4}/tests/session/__init__.py +0 -0
  174. {coderio-0.4.3 → coderio-0.4.4}/tests/session/test_message.py +0 -0
  175. {coderio-0.4.3 → coderio-0.4.4}/tests/session/test_store.py +0 -0
  176. {coderio-0.4.3 → coderio-0.4.4}/tests/skills/__init__.py +0 -0
  177. {coderio-0.4.3 → coderio-0.4.4}/tests/skills/test_bundled.py +0 -0
  178. {coderio-0.4.3 → coderio-0.4.4}/tests/skills/test_parser.py +0 -0
  179. {coderio-0.4.3 → coderio-0.4.4}/tests/skills/test_skill_tools.py +0 -0
  180. {coderio-0.4.3 → coderio-0.4.4}/tests/skills/test_store.py +0 -0
  181. {coderio-0.4.3 → coderio-0.4.4}/tests/test_mcp_loader.py +0 -0
  182. {coderio-0.4.3 → coderio-0.4.4}/tests/test_p0_stability.py +0 -0
  183. {coderio-0.4.3 → coderio-0.4.4}/tests/test_repl.py +0 -0
  184. {coderio-0.4.3 → coderio-0.4.4}/tests/test_version.py +0 -0
  185. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/__init__.py +0 -0
  186. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_bash.py +0 -0
  187. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_command_policy.py +0 -0
  188. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_concrete_gates.py +0 -0
  189. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_edit_prefix.py +0 -0
  190. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_file_tools.py +0 -0
  191. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_glob_tool.py +0 -0
  192. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_grep_tool.py +0 -0
  193. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_list_dir.py +0 -0
  194. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_multi_edit.py +0 -0
  195. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_note.py +0 -0
  196. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_permission.py +0 -0
  197. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_registry.py +0 -0
  198. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_sandbox.py +0 -0
  199. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_search_tools.py +0 -0
  200. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_todo.py +0 -0
  201. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_web.py +0 -0
  202. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_web_fetch.py +0 -0
  203. {coderio-0.4.3 → coderio-0.4.4}/tests/tools/test_win_job.py +0 -0
  204. {coderio-0.4.3 → coderio-0.4.4}/uv.lock +0 -0
@@ -76,13 +76,15 @@ jobs:
76
76
 
77
77
  - name: Run tests with coverage
78
78
  # Enforces a coverage floor so coverage can't silently regress.
79
- # The 70% floor is conservative (current baseline is ~75%) — it catches
80
- # gross regressions (a module losing its tests) without failing on
81
- # minor fluctuations. pyproject.toml's [tool.coverage.report]
82
- # fail_under=60 is the local-dev default; CI enforces the stricter 70%.
79
+ # Measured baseline 2026-08-24: 82% total (weakest: sandbox_runner 43%
80
+ # — POSIX-only branches skip on Windows). The 75% floor keeps real
81
+ # headroom for platform variance while catching gross regressions
82
+ # (a module losing its tests). pyproject.toml's
83
+ # [tool.coverage.report] fail_under=60 is the local-dev default;
84
+ # CI enforces the stricter 75%.
83
85
  shell: bash
84
86
  run: |
85
- uv run python -m pytest -q --cov=coderio --cov-report=term-missing --cov-fail-under=70
87
+ uv run python -m pytest -q --cov=coderio --cov-report=term-missing --cov-fail-under=75
86
88
 
87
89
  - name: Type check (mypy)
88
90
  # HARD GATE for clean modules. pyproject.toml's [[tool.mypy.overrides]]
@@ -6,6 +6,49 @@ All notable changes to coderio are documented here. The format follows
6
6
  `pyproject.toml`'s `[project].version` — `coderio.__version__` reads it via
7
7
  `importlib.metadata`.
8
8
 
9
+ ## [0.4.4] — 2026-08-24
10
+
11
+ ### Added — extensibility & safety-of-agency features (each adversarially reviewed, mutation-verified)
12
+ - **Custom slash commands**: `.coderio/commands/*.md` (project layer overrides user layer)
13
+ turn prompt templates into `/commands` with `$ARGUMENTS` substitution and optional
14
+ frontmatter descriptions. Built-ins can never be shadowed — exact matches refused at
15
+ expansion, case variants dropped at discovery (Windows case-insensitive-FS spoof
16
+ surface). Bodies route straight to the engine, never re-enter built-in dispatch: a
17
+ repo file with body `/mode full` cannot flip the permission gate.
18
+ - **Custom subagents**: `.coderio/agents/*.md` define personas invokable via
19
+ `task(subagent_type=...)`. Persona-only customization — every assembled spec rides
20
+ the same PLAN-gated read-only middleware stack as the built-in research agent;
21
+ reserved names dropped at discovery AND re-filtered at wiring time. Adversarially
22
+ verified end-to-end: write tools physically unbound, nested task() blocked on three
23
+ independent layers.
24
+ - **File checkpoints + `/undo`**: every structured write (write_file/edit_file/
25
+ multi_edit) snapshots the target's pre-write state; `/undo` reverts step by step.
26
+ multi_edit is one undo step; error paths consume no depth; failed restores re-push
27
+ for retry; bounded at 50 entries / 64MB; deliberately survives /clear. Shell-side
28
+ writes (redirects) bypass it — OS sandboxing is the layer for that class.
29
+ - **Plan artifact**: the todo list mirrors to `<project>/.coderio/plan.md` after every
30
+ successful write_todos; user edits between messages are adopted at next turn start
31
+ (human override wins) with an injected note so the model doesn't execute a stale plan.
32
+
33
+ ### Fixed
34
+ - Three prompt surfaces (PlanGate nudge, CompletionGate force-continue, CODE-workflow)
35
+ directed the model to coderio's legacy `todo()` tool whose store is an orphan the
36
+ gates never check — completion instructions could loop forever. All now point at
37
+ `write_todos`, the canonical surface.
38
+ - Custom-command discovery anchor joined at the caller after a runtime audit caught
39
+ production loading zero commands while globbing root-level *.md files as agents.
40
+ - Subagent discovery anchors walk up to the project root like skills/config/trust
41
+ (launching from a repo subdirectory previously missed project-layer definitions).
42
+ - Frontmatter descriptions are truncated to 1024 chars and control-char-stripped —
43
+ a 90KB description previously reached the main model verbatim every turn.
44
+
45
+ ### Changed
46
+ - CI coverage floor raised 70% → 75% against a measured 82% baseline (gate verified
47
+ to trip); stale coverage comments corrected.
48
+ - tui.py decomposed (1627 → 1332 lines): input dispatch + session lifecycle extracted
49
+ to testable TuiRuntime; behavior verified statement-equivalent by AST diff + real
50
+ Textual input-routing tests.
51
+
9
52
  ## [0.4.3] — 2026-08-21
10
53
 
11
54
  ### Security — P1/P2 hardening (audit-verified, adversarial + mutation tested)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coderio
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: A skill-driven coding agent — structural harness, foldable thinking TUI, deepagents engine.
5
5
  Author: coderio contributors
6
6
  License-Expression: MIT
@@ -90,13 +90,17 @@ Zhipu **GLM Coding Plan** and StepFun **Step Plan** work out of the box (direct
90
90
  ## Feature highlights
91
91
 
92
92
  - **Interactive TUI**: streaming output, foldable thinking (Ctrl+O), collapsible TODO panel, vertical permission menu, task interruption (Esc), slash-command autocomplete, session management
93
+ - **Custom slash commands**: `.coderio/commands/*.md` (project/user layers) turn prompt templates into `/commands` with `$ARGUMENTS` substitution; built-ins can never be shadowed
94
+ - **Custom subagents**: `.coderio/agents/*.md` define personas invokable via `task(subagent_type=...)` — you customize WHO the agent is, its capabilities stay on the read-only stack
95
+ - **File rollback**: every structured agent write is auto-checkpointed; `/undo` reverts step by step (a bad edit is one command from gone)
96
+ - **Plan artifact**: the task list auto-mirrors to `.coderio/plan.md`; edit it by hand and the agent adopts your version at the next turn
93
97
  - **Headless mode**: `coderio run "task"` one-shot execution (CI / scripts / benchmarks) with graded exit codes
94
98
  - **MCP support**: connect external tools via `.mcp.json` (Claude Code-compatible format), managed with `coderio mcp`
95
99
  - **Lifecycle hooks**: `[[hooks]]` run your commands at PreToolUse / PostToolUse / UserPromptSubmit (exit 2 = block) — IO contract compatible with Claude Code
96
100
  - **Three-layer skills**: bundled + user + project, progressive disclosure saves context
97
101
  - **Context governance**: auto-compaction (60% window trigger), large-block offload, sqlite checkpoints across turns
98
102
  - **Subagents**: research (read-only, double-enforced) + general-purpose (inherits the main agent's full security stack)
99
- - **Engineering discipline**: 850+ tests, 80% coverage, mypy hard gate, uv.lock, 3 OS × 2 Python CI matrix
103
+ - **Engineering discipline**: 950+ tests, 82% coverage (CI floor 75%), mypy hard gate, uv.lock, 3 OS × 2 Python CI matrix
100
104
 
101
105
  <details>
102
106
  <summary><b>Config example</b> (click to expand)</summary>
@@ -132,7 +136,7 @@ coderio mcp add github --type http --url ... # manage MCP
132
136
  coderio skills install # install skill suites
133
137
  ```
134
138
 
135
- Type `/` inside the TUI for all commands (/resume sessions, /mode permissions, /profile configs, /think unfold reasoning).
139
+ Type `/` inside the TUI for all commands (/resume sessions, /mode permissions, /undo file writes, /think unfold reasoning).
136
140
 
137
141
  ## Known limitations
138
142
 
@@ -45,13 +45,17 @@ coderio # 首次启动进入 onboarding 向导(选 provider、填 API key
45
45
  ## 特性一览
46
46
 
47
47
  - **交互式 TUI**:流式输出、思考折叠(Ctrl+O)、可折叠 TODO 面板、纵向权限菜单、任务中断(Esc)、slash 命令补全、会话管理
48
+ - **自定义 slash 命令**:`.coderio/commands/*.md`(项目级/用户级)把提示词模板变成 `/命令`,`$ARGUMENTS` 占位符替换,内置命令不可被遮蔽
49
+ - **自定义子代理**:`.coderio/agents/*.md` 定义 `task(subagent_type=...)` 可调的专属人格——只定制"是谁",能力恒为只读栈
50
+ - **文件回滚**:agent 的每次结构化写盘自动快照,`/undo` 一键逐级恢复(改坏文件不再可怕)
51
+ - **计划产物**:任务清单自动镜像到 `.coderio/plan.md`,你手动编辑它,下一轮 agent 自动采纳你的版本
48
52
  - **headless 模式**:`coderio run "任务"` 单次运行(CI / 脚本 / benchmark),退出码分级
49
53
  - **MCP 支持**:`.mcp.json`(Claude Code 兼容格式)接入外部工具,`coderio mcp` 命令行管理
50
54
  - **生命周期 hooks**:`[[hooks]]` 在 PreToolUse / PostToolUse / UserPromptSubmit 等事件执行你的命令(exit 2 = 阻断),IO 契约与 Claude Code 兼容
51
55
  - **skills 三层加载**:bundled + 用户 + 项目层,渐进披露省上下文
52
56
  - **上下文治理**:自动压缩(60% 窗口触发)、大块 offload、sqlite 检查点跨轮持久化
53
57
  - **子 agent**:research(只读,双重强制)+ general-purpose(继承主 agent 全部安全层)
54
- - **工程纪律**:850+ 测试、80% 覆盖率、mypy 硬门、uv.lock 锁定、3 OS × 2 Python CI 矩阵
58
+ - **工程纪律**:950+ 测试、82% 覆盖率(CI 卡 75% 下限)、mypy 硬门、uv.lock 锁定、3 OS × 2 Python CI 矩阵
55
59
 
56
60
  <details>
57
61
  <summary><b>配置示例</b>(点击展开)</summary>
@@ -87,7 +91,7 @@ coderio mcp add github --type http --url ... # 管理 MCP
87
91
  coderio skills install # 安装 skill 套件
88
92
  ```
89
93
 
90
- TUI 内输入 `/` 查看全部命令(/resume 恢复会话、/mode 切权限、/profile 切配置、/think 展开思考)。
94
+ TUI 内输入 `/` 查看全部命令(/resume 恢复会话、/mode 切权限、/undo 回滚文件写入、/think 展开思考)。
91
95
 
92
96
  ## 已知限制
93
97
 
@@ -45,13 +45,17 @@ Zhipu **GLM Coding Plan** and StepFun **Step Plan** work out of the box (direct
45
45
  ## Feature highlights
46
46
 
47
47
  - **Interactive TUI**: streaming output, foldable thinking (Ctrl+O), collapsible TODO panel, vertical permission menu, task interruption (Esc), slash-command autocomplete, session management
48
+ - **Custom slash commands**: `.coderio/commands/*.md` (project/user layers) turn prompt templates into `/commands` with `$ARGUMENTS` substitution; built-ins can never be shadowed
49
+ - **Custom subagents**: `.coderio/agents/*.md` define personas invokable via `task(subagent_type=...)` — you customize WHO the agent is, its capabilities stay on the read-only stack
50
+ - **File rollback**: every structured agent write is auto-checkpointed; `/undo` reverts step by step (a bad edit is one command from gone)
51
+ - **Plan artifact**: the task list auto-mirrors to `.coderio/plan.md`; edit it by hand and the agent adopts your version at the next turn
48
52
  - **Headless mode**: `coderio run "task"` one-shot execution (CI / scripts / benchmarks) with graded exit codes
49
53
  - **MCP support**: connect external tools via `.mcp.json` (Claude Code-compatible format), managed with `coderio mcp`
50
54
  - **Lifecycle hooks**: `[[hooks]]` run your commands at PreToolUse / PostToolUse / UserPromptSubmit (exit 2 = block) — IO contract compatible with Claude Code
51
55
  - **Three-layer skills**: bundled + user + project, progressive disclosure saves context
52
56
  - **Context governance**: auto-compaction (60% window trigger), large-block offload, sqlite checkpoints across turns
53
57
  - **Subagents**: research (read-only, double-enforced) + general-purpose (inherits the main agent's full security stack)
54
- - **Engineering discipline**: 850+ tests, 80% coverage, mypy hard gate, uv.lock, 3 OS × 2 Python CI matrix
58
+ - **Engineering discipline**: 950+ tests, 82% coverage (CI floor 75%), mypy hard gate, uv.lock, 3 OS × 2 Python CI matrix
55
59
 
56
60
  <details>
57
61
  <summary><b>Config example</b> (click to expand)</summary>
@@ -87,7 +91,7 @@ coderio mcp add github --type http --url ... # manage MCP
87
91
  coderio skills install # install skill suites
88
92
  ```
89
93
 
90
- Type `/` inside the TUI for all commands (/resume sessions, /mode permissions, /profile configs, /think unfold reasoning).
94
+ Type `/` inside the TUI for all commands (/resume sessions, /mode permissions, /undo file writes, /think unfold reasoning).
91
95
 
92
96
  ## Known limitations
93
97
 
@@ -39,12 +39,12 @@ coderio 是一个**技能驱动的编程 agent**:它的"骨架"是 Lion-Skills
39
39
 
40
40
  | 模块 | 行数 | 职责 | 关键文件 |
41
41
  |------|------|------|----------|
42
- | `agent/` | ~2500 | deepagents 引擎、harness/permission middleware、提示词构建、流式协议 | deep_loop.py, harness_middleware.py, permission_middleware.py, harness.py, prompts.py |
43
- | `cli/` | ~3800 | Typer 应用、Textual TUI、Rich 流式 UI、slash 命令、凭证/onboarding | tui.py, repl.py, stream.py, app.py, onboarding.py |
44
- | `tools/` | ~1100 | 工具集 + 权限门 + langchain 适配(路径隔离由 deepagents virtual_mode 处理) | bash.py, permission.py, base.py |
45
- | `config/` | ~400 | 三层 TOML 配置合并 + 用户目录 bootstrap | loader.py, models.py |
46
- | `skills/` | ~220 | SkillStore 三层加载(bundled < user < project) | store.py, parser.py, models.py |
47
- | `session/` | 260 | jsonl 追加式会话存储 + resume + 压缩截断 | store.py, message.py |
42
+ | `agent/` | ~3600 | deepagents 引擎、harness/permission middleware、计划产物、自定义子代理、提示词构建、流式协议 | deep_loop.py, harness_middleware.py, permission_middleware.py, harness.py, plan_artifact.py, custom_agents.py, prompts.py |
43
+ | `cli/` | ~5300 | Typer 应用、Textual TUI(渲染)+ TuiRuntime(输入分发/会话生命周期)、自定义 slash 命令、Rich 流式 UI、凭证/onboarding | tui.py, tui_runtime.py, repl.py, stream.py, app.py, commands.py, custom_commands.py, tui_onboarding.py |
44
+ | `tools/` | ~3000 | 工具集 + 文件写入 checkpoint + 权限门 + 命令黑名单 + OS 沙箱 + langchain 适配 | bash.py, permission.py, command_policy.py, checkpoint.py, base.py |
45
+ | `config/` | ~900 | 三层 TOML 配置合并 + 仓库信任门 + 用户目录 bootstrap | loader.py, models.py, trust.py |
46
+ | `skills/` | ~255 | SkillStore 三层加载(bundled < user < project)+ 共享 frontmatter 解析 | store.py, parser.py, models.py |
47
+ | `session/` | ~330 | jsonl 追加式会话存储 + resume + 压缩截断 | store.py, message.py |
48
48
  | `llm/` | 320 | 模型工厂 + provider context window 探测 | factory.py, probe.py |
49
49
 
50
50
  ### 依赖方向(关键约束)
@@ -268,12 +268,13 @@ is_interrupted()(用户中断检查,agent 线程在每轮开头调用)
268
268
 
269
269
  `NullStream` 全空实现,用于测试/headless。
270
270
 
271
- ### 6.5 TUI 交互(`cli/tui.py`)
271
+ ### 6.5 TUI 交互(`cli/tui.py` + `cli/tui_runtime.py`)
272
272
 
273
273
  Textual 8.x App,核心设计:
274
274
 
275
- - **线程模型**:agent 在 Textual Worker 后台线程跑,UI 更新通过 `_render_q`(thread-safe deque)+ 60ms 定时器排空
276
- - **流式渲染**:dict 分派表(`_RENDER_DISPATCH`)映射 action → handler,每个 handler返回 streaming/final/none 决定滚动策略
275
+ - **渲染与分发分离**(S3 拆分):`CoderioTUI` 只管 widget 树与流式渲染;输入路由、slash 处理、会话生命周期在 `TuiRuntime`(两阶段构造:先建 runtime,`bind()` 时把带 TUI 引用的 gate 接上——confirm 模式否则会在 Textual 接管终端后死于 input() 死锁)
276
+ - **线程模型**:agent 在后台线程跑,UI 更新通过 `_render_q`(thread-safe deque)+ 定时器排空
277
+ - **流式渲染**:dict 分派表映射 action → handler,每个 handler 返回 streaming/final/none 决定滚动策略
277
278
  - **中断**:`Esc` / `⏹ 中断` 按钮 → `_interrupted` 标志位,agent 流循环检查 `is_interrupted()` → `InterruptedError` → 黄色"已中断"面板
278
279
  - **confirm 模式**:`TuiPermissionGate` 用 `ConfirmMenu`(纵向选择菜单)+ `threading.Event` 跨线程同步,↑↓ 选择 + Enter 确认
279
280
  - **可视化选择器**:`/mode`(ModePickerScreen)、`/profile`(ProfilePickerScreen)、`/resume`(SessionPickerScreen)
@@ -290,14 +291,18 @@ Textual 8.x App,核心设计:
290
291
  | 类别 | 工具 |
291
292
  |------|------|
292
293
  | 读 | read_file, list_dir, glob, grep |
293
- | 写 | write_file, edit_file, multi_edit |
294
+ | 写 | write_file, edit_file, multi_edit(写前自动 checkpoint,`/undo` 可回滚)|
294
295
  | 执行 | bash(Git Bash,Windows 自动探测,.venv 自动激活,进程树超时杀)|
295
- | 计划 | todo(TodoStore,harness 读它)|
296
+ | 计划 | write_todos(deepagents 原生,镜像到 `.coderio/plan.md` 供用户编辑)|
296
297
  | 外部 | web_search, web_fetch |
297
298
  | 记忆 | note(跨会话长期记忆)|
298
299
 
299
300
  **权限门**(`permission.py`):plan / confirm / auto_edit / full 四模式。`DESTRUCTIVE_TOOLS`(write_file/edit_file/multi_edit/execute/web_fetch/note)在 plan 模式全挡、confirm 模式逐个问、auto_edit 自动放行文件编辑但仍问高危工具、full 全放。
300
301
 
302
+ **文件 checkpoint**(`checkpoint.py`,S4):三个结构化写工具落盘前快照进内存栈(50 条 / 64MB 上限),`/undo` 逐级回滚;bash 重定向等 shell 路径不覆盖——OS 级沙箱才是那一层的答案。
303
+
304
+ **计划产物**(`plan_artifact.py`,S5):write_todos 成功后任务清单镜像到 `<project>/.coderio/plan.md`;用户在两轮之间手改它,下一轮开始自动采纳其版本并注入提示。
305
+
301
306
  **命令审查层**(`command_review.py` + `command_policy.py`):权限门只管"哪个工具能执行",不管"命令内容是什么"。shell(execute)不受 virtual_mode 约束,所以额外加了一层 `CommandReviewMiddleware`——内置黑名单挡住 `rm -rf /`、`mkfs`、fork bomb、`dd of=/dev/`、`shutdown` 等破坏性命令。即使 FULL 模式也挡(安全优先于"full=全放行"字面语义)。用户可在 config.toml `[tools].blocked_commands` 追加正则黑名单。`network_allowed=false` 可禁用 web_fetch/web_search(离线模式)。
302
307
 
303
308
  **这不是真 OS 沙箱**:混淆命令(base64 解码、变量展开)可绕过正则。目标是挡住意外/粗心的破坏(占真实事故绝大多数),不是对抗性模型。真隔离需要容器/seccomp(未来工作)。
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "coderio"
8
- version = "0.4.3"
8
+ version = "0.4.4"
9
9
  description = "A skill-driven coding agent — structural harness, foldable thinking TUI, deepagents engine."
10
10
  readme = "README_en.md"
11
11
  license = "MIT"
@@ -190,10 +190,11 @@ asyncio_mode = "auto"
190
190
  source = ["coderio"]
191
191
 
192
192
  [tool.coverage.report]
193
- # Local-dev coverage floor. CI enforces a stricter 70% via --cov-fail-under
193
+ # Local-dev coverage floor. CI enforces a stricter 75% via --cov-fail-under
194
194
  # (see .github/workflows/ci.yml). This 60% is the "running tests in your
195
195
  # editor" safety net — lower than CI so local runs don't fail on partial
196
- # test selection. Current baseline: ~75% total, deep_loop 75%, middleware 80%+.
196
+ # test selection. Measured baseline 2026-08-24: 82% total; known weakest
197
+ # sandbox_runner 43% (POSIX-only branches skip on Windows).
197
198
  fail_under = 60
198
199
 
199
200
  [tool.mypy]
@@ -0,0 +1,94 @@
1
+ """Custom subagents: ``.coderio/agents/*.md`` (project) + ``~/.coderio/agents/*.md`` (user).
2
+
3
+ A file's filename (minus .md) is the subagent NAME used via
4
+ ``task(subagent_type="<name>")``; optional frontmatter ``description`` tells
5
+ the MAIN model when to delegate; the body is the subagent's SYSTEM PROMPT.
6
+
7
+ SECURITY MODEL — this is persona customization only, never tool escalation.
8
+ Every discovered agent is assembled onto the SAME read-only middleware stack
9
+ as the built-in research subagent (hooks + PermissionMiddleware(PLAN) +
10
+ CommandReviewMiddleware): it can read and search, never write or execute,
11
+ regardless of what its prompt says or which permission mode the caller runs
12
+ in. A hostile repo can therefore at worst add a misleading persona/prompt —
13
+ the same trust surface as SKILL.md bodies.
14
+
15
+ Names colliding with built-in subagent types ("research", "general-purpose")
16
+ are dropped at discovery — exact AND case variants — so a repo file can't
17
+ hijack or spoof a trusted type (same near-miss logic as custom commands).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ from coderio.skills.parser import sanitize_description, split_frontmatter
26
+
27
+ # Subagent types coderio itself provides. Discovered files with these names
28
+ # (any casing) are ignored — task(subagent_type=...) is matched by deepagents
29
+ # verbatim, so an exact-collision file would shadow trusted behavior, and on
30
+ # Windows's case-insensitive FS a case variant is reachable while looking like
31
+ # the trusted type in transcripts.
32
+ RESERVED_AGENT_NAMES = frozenset({"research", "general-purpose"})
33
+ RESERVED_CI = frozenset(n.lower() for n in RESERVED_AGENT_NAMES)
34
+
35
+ MAX_AGENT_BYTES = 128 * 1024 # system prompts are small; bigger = junk/bomb
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class CustomAgent:
40
+ """One discovered custom subagent definition."""
41
+
42
+ name: str # task(subagent_type=...) value, without leading slash
43
+ description: str # shown to the MAIN model for delegation decisions
44
+ system_prompt: str # the subagent's persona/instructions
45
+ source_layer: str # "user" | "project" — for /help-style listings
46
+
47
+
48
+ def _load_layer(layer_dir: Path | None, layer_name: str) -> dict[str, CustomAgent]:
49
+ out: dict[str, CustomAgent] = {}
50
+ if not layer_dir:
51
+ return out
52
+ layer_dir = Path(layer_dir)
53
+ if not layer_dir.is_dir():
54
+ return out
55
+ for md in sorted(layer_dir.glob("*.md")):
56
+ # Name from FILENAME (not frontmatter) — mirrors custom commands: the
57
+ # tree the user sees must match the string they pass to task().
58
+ name = md.stem
59
+ if not name or any(ch.isspace() for ch in name):
60
+ continue # task() takes a bare token; spaced names are unreachable
61
+ try:
62
+ if md.stat().st_size > MAX_AGENT_BYTES:
63
+ continue
64
+ raw = md.read_text(encoding="utf-8", errors="replace")
65
+ except OSError:
66
+ continue
67
+ raw = raw.replace("\x00", "") # providers 400 on NUL
68
+ fm, body = split_frontmatter(raw)
69
+ body = body.strip()
70
+ if not body:
71
+ continue # empty persona would send the subagent in blind
72
+ out[name] = CustomAgent(
73
+ name=name,
74
+ description=sanitize_description(fm.get("description", "")),
75
+ system_prompt=body,
76
+ source_layer=layer_name,
77
+ )
78
+ return out
79
+
80
+
81
+ def discover_custom_agents(
82
+ project_dir: Path | str | None = None,
83
+ user_dir: Path | str | None = None,
84
+ ) -> dict[str, CustomAgent]:
85
+ """Discover custom subagent definitions from both layers.
86
+
87
+ Both params are LAYER DIRECTORIES whose immediate ``*.md`` children define
88
+ agents (e.g. ``<project>/.coderio/agents``) — NOT a project root; callers
89
+ join the anchor themselves (same convention as load_skill_store). Project
90
+ layer overrides user layer on conflict. Reserved names are dropped.
91
+ """
92
+ merged = _load_layer(Path(user_dir) if user_dir else None, "user")
93
+ merged.update(_load_layer(Path(project_dir) if project_dir else None, "project"))
94
+ return {k: v for k, v in merged.items() if k.lower() not in RESERVED_CI and k not in RESERVED_AGENT_NAMES}
@@ -298,22 +298,18 @@ def _build_extra_tools(tools, skill_store, active_skills):
298
298
  return extra
299
299
 
300
300
 
301
- def _build_research_subagent(command_policy=None, hook_runner=None):
302
- """Return the research subagent spec (read-only, physically isolated).
303
-
304
- Tool exclusion uses the compat layer (_deepagents_compat) so that a
305
- deepagents API change degrades gracefully instead of crashing.
306
-
307
- HooksMiddleware (2026-08-14 v3 audit #12): without it, ``task()``-delegated
308
- work bypassed the user's PreToolUse/PostToolUse hooks entirely. The runner
309
- is shared with the main agent (stateless across fire() calls).
310
-
311
- Permission + CommandReview: execution-time enforcement on top of the
301
+ def _readonly_subagent_middleware(command_policy=None, hook_runner=None) -> list:
302
+ """Assemble the READ-ONLY middleware stack shared by the research subagent
303
+ and every user-defined custom subagent (hooks → PermissionMiddleware(PLAN)
304
+ → CommandReviewMiddleware). One function so the two can never drift: if a
305
+ custom agent got a weaker stack, task(subagent_type=...) would be a
306
+ privilege-escalation primitive dressed as a feature.
307
+
308
+ Permission + CommandReview are execution-time enforcement on top of the
312
309
  model-visibility whitelist. The whitelist filters what the model SEES,
313
- these middlewares gate what actually RUNS. This subagent uses its own
314
- hardcoded PLAN gate so it can never be upgraded to write access via the
315
- caller's permission mode (an FULL/auto caller must not turn the "read-only"
316
- subagent into a writing one).
310
+ these middlewares gate what actually RUNS. The hardcoded PLAN gate means a
311
+ FULL/auto caller cannot upgrade the subagent to write access, and neither
312
+ can anything written in a custom .md system prompt.
317
313
  """
318
314
  from coderio.agent._deepagents_compat import make_research_subagent_middleware
319
315
 
@@ -331,6 +327,19 @@ def _build_research_subagent(command_policy=None, hook_runner=None):
331
327
 
332
328
  policy = command_policy or CommandPolicy.default()
333
329
  middleware.append(CommandReviewMiddleware(policy))
330
+ return middleware
331
+
332
+
333
+ def _build_research_subagent(command_policy=None, hook_runner=None):
334
+ """Return the research subagent spec (read-only, physically isolated).
335
+
336
+ Tool exclusion uses the compat layer (_deepagents_compat) so that a
337
+ deepagents API change degrades gracefully instead of crashing.
338
+
339
+ HooksMiddleware (2026-08-14 v3 audit #12): without it, ``task()``-delegated
340
+ work bypassed the user's PreToolUse/PostToolUse hooks entirely. The runner
341
+ is shared with the main agent (stateless across fire() calls).
342
+ """
334
343
  return {
335
344
  "name": "research",
336
345
  "description": (
@@ -355,7 +364,40 @@ def _build_research_subagent(command_policy=None, hook_runner=None):
355
364
  "- The calling agent only sees your final message, not your "
356
365
  "intermediate tool calls — make sure your answer is complete."
357
366
  ),
358
- "middleware": middleware,
367
+ "middleware": _readonly_subagent_middleware(command_policy, hook_runner),
368
+ }
369
+
370
+
371
+ def _drop_trusted_name_collisions(custom_specs: list[dict], trusted_specs: list[dict]) -> list[dict]:
372
+ """Defense-in-depth name filter applied at WIRING time (see call site).
373
+
374
+ deepagents builds {name: spec} LAST-WINS and custom specs sit at the END
375
+ of the subagents list — if discovery's reserved-name drop ever regressed,
376
+ a repo file named research.md would silently REPLACE the trusted spec.
377
+ This second filter makes a single-layer regression non-escalating
378
+ (adversarial-review recommendation). Case-insensitive to mirror the
379
+ discovery-layer rule even though the engine matches exactly.
380
+ """
381
+ trusted_lower = {s["name"].lower() for s in trusted_specs}
382
+ return [s for s in custom_specs if s["name"].lower() not in trusted_lower]
383
+
384
+
385
+ def _build_custom_subagent(agent, command_policy=None, hook_runner=None):
386
+ """Wrap a discovered CustomAgent (.coderio/agents/*.md) as a subagent spec.
387
+
388
+ Persona comes from the file; the SECURITY STACK always comes from
389
+ _readonly_subagent_middleware — custom definitions customize WHO the agent
390
+ pretends to be, never WHAT it can do.
391
+ """
392
+ description = agent.description or (
393
+ f"Custom read-only research agent ({agent.source_layer} layer, "
394
+ f".coderio/agents/{agent.name}.md). Reads and searches only."
395
+ )
396
+ return {
397
+ "name": agent.name,
398
+ "description": description,
399
+ "system_prompt": agent.system_prompt,
400
+ "middleware": _readonly_subagent_middleware(command_policy, hook_runner),
359
401
  }
360
402
 
361
403
 
@@ -425,14 +467,17 @@ def _build_general_purpose_subagent(gate, command_policy, stream=None, hook_runn
425
467
  }
426
468
 
427
469
 
428
- def _build_inputs(checkpointer, user_input: str, session: Session) -> dict:
470
+ def _build_inputs(checkpointer, user_input: str | list[dict[str, Any]], session: Session) -> dict:
429
471
  """Build the messages input for the agent stream.
430
472
 
431
473
  With a checkpointer: only pass the new user message (deepagents restores
432
474
  prior state from sqlite). Without: pass full conversation history.
433
475
  """
434
476
  if checkpointer is not None:
435
- return {"messages": [HumanMessage(content=user_input)]}
477
+ # langchain declares list[str | dict] while we carry list[dict[str, Any]];
478
+ # list invariance flags the narrower list even though every element
479
+ # satisfies the wider union at runtime.
480
+ return {"messages": [HumanMessage(content=user_input)]} # type: ignore[arg-type]
436
481
  return {"messages": _build_history_messages(session.messages)}
437
482
 
438
483
 
@@ -455,7 +500,9 @@ def _run_stream(agent, inputs, thread_id, recursion_limit, stream, session, seen
455
500
 
456
501
 
457
502
  def run_deep_agent(
458
- user_input: str,
503
+ # str OR multimodal content-block list (docstring below) — build_user_content
504
+ # returns either depending on whether images are attached.
505
+ user_input: str | list[dict[str, Any]],
459
506
  model,
460
507
  session: Session,
461
508
  stream=None,
@@ -547,6 +594,40 @@ def run_deep_agent(
547
594
  if ups.context:
548
595
  user_input = f"{prompt_text}\n\n[hook context]\n{ups.context}"
549
596
 
597
+ # Plan artifact (.coderio/plan.md, S5): the harness's todo list mirrored to
598
+ # an editable file. Anchor walks up like skills/config/trust (SA-4 lesson —
599
+ # a literal runtime dir would miss project plans when launched from a
600
+ # subdirectory). The SAME TodoStore instance feeds both the middleware
601
+ # mirror and the artifact, so write_todos → materialize and turn-start
602
+ # adopt stay in sync. Subagent HarnessMiddleware gets neither: the plan has
603
+ # exactly one owner. Adoption runs BEFORE the hooks/session append below so
604
+ # the injected note lands inside this turn's user message.
605
+ from coderio.agent.plan_artifact import AdoptionNote, PlanArtifact
606
+ from coderio.config.loader import _find_project_dir
607
+ from coderio.tools.todo import TodoStore
608
+
609
+ plan_artifact = None
610
+ adoption_note = ""
611
+ if harness_enabled:
612
+ plan_artifact = PlanArtifact(
613
+ anchor=_find_project_dir(project_dir) / ".coderio",
614
+ store=TodoStore(),
615
+ )
616
+ adopted = plan_artifact.adopt_if_edited()
617
+ if adopted:
618
+ adoption_note = AdoptionNote(count=adopted, path=plan_artifact.path).render()
619
+
620
+ # Plan artifact adoption (S5): if the user edited .coderio/plan.md between
621
+ # turns, their version already replaced the todo store above — tell the
622
+ # model so it doesn't keep executing a stale plan.
623
+ if adoption_note:
624
+ if isinstance(user_input, str):
625
+ user_input = f"{user_input}\n\n{adoption_note}"
626
+ else:
627
+ # Multimodal content blocks: append as an extra text block so
628
+ # attached images survive (same concern as the hook context above).
629
+ user_input = [*user_input, {"type": "text", "text": adoption_note}]
630
+
550
631
  session.append(Message.user(user_input))
551
632
 
552
633
  sp = _resolve_system_prompt(system_prompt, skill_store, active_skills)
@@ -557,7 +638,15 @@ def run_deep_agent(
557
638
  from coderio.agent.hooks import HooksMiddleware
558
639
 
559
640
  middleware.append(HooksMiddleware(hook_runner))
560
- middleware.append(HarnessMiddleware(stream=stream, enabled=harness_enabled))
641
+
642
+ middleware.append(
643
+ HarnessMiddleware(
644
+ stream=stream,
645
+ enabled=harness_enabled,
646
+ todos=plan_artifact.store if plan_artifact is not None else None,
647
+ plan_artifact=plan_artifact,
648
+ )
649
+ )
561
650
  if gate is not None:
562
651
  middleware.append(PermissionMiddleware(gate))
563
652
  # Command-content review: always active (even in FULL mode). Blocks rm -rf /,
@@ -581,16 +670,42 @@ def run_deep_agent(
581
670
  bash_shell=bash_shell,
582
671
  )
583
672
 
673
+ # Custom user/project subagents (.coderio/agents/*.md). Layer dirs joined
674
+ # HERE (not inside discovery) mirroring load_skill_store's caller-joins
675
+ # convention — passing the bare project root would glob every root *.md
676
+ # into an agent definition (same trap the custom-commands audit caught).
677
+ #
678
+ # ANCHOR PARITY (seam SA-4): skills/config/trust all anchor at
679
+ # _find_project_dir(search_from) which WALKS UP to the project root;
680
+ # agents used to anchor at the literal runtime dir (workdir or cwd), so a
681
+ # launch from a repo subdirectory silently loaded zero project-layer
682
+ # agents while project-layer skills still loaded — the exact
683
+ # discovery-vs-loading scope asymmetry of incident #3. Walk up with the
684
+ # same rule; workspace_root stays the starting point when set.
685
+ from coderio.agent.custom_agents import discover_custom_agents
686
+ from coderio.config.loader import _find_project_dir
687
+
688
+ trusted_specs = [
689
+ _build_research_subagent(command_policy=command_policy, hook_runner=hook_runner),
690
+ _build_general_purpose_subagent(gate, command_policy, stream=stream, hook_runner=hook_runner),
691
+ ]
692
+ custom_specs = [
693
+ _build_custom_subagent(ca, command_policy=command_policy, hook_runner=hook_runner)
694
+ for ca in discover_custom_agents(
695
+ project_dir=_find_project_dir(project_dir) / ".coderio" / "agents",
696
+ user_dir=Path.home() / ".coderio" / "agents",
697
+ ).values()
698
+ ]
699
+ # Defense in depth at wiring time — see _drop_trusted_name_collisions.
700
+ custom_specs = _drop_trusted_name_collisions(custom_specs, trusted_specs)
701
+
584
702
  extra_lc_tools = _build_extra_tools(tools, skill_store, active_skills)
585
703
 
586
704
  build_kwargs: dict[str, Any] = {
587
705
  "model": model,
588
706
  "middleware": middleware,
589
707
  "backend": backend,
590
- "subagents": [
591
- _build_research_subagent(command_policy=command_policy, hook_runner=hook_runner),
592
- _build_general_purpose_subagent(gate, command_policy, stream=stream, hook_runner=hook_runner),
593
- ],
708
+ "subagents": [*trusted_specs, *custom_specs],
594
709
  }
595
710
  if sp:
596
711
  build_kwargs["system_prompt"] = sp
@@ -526,8 +526,10 @@ class Harness:
526
526
  self.state.plan_nudged = True
527
527
  return (
528
528
  "\n[nudge] You're writing code but have no task list yet. For non-trivial "
529
- 'work, call todo(action="add", ...) first to decompose the task into '
530
- "verifiable steps. (Trivial fixes like a typo can ignore this.)"
529
+ "work, decompose the task into verifiable steps via the write_todos tool "
530
+ "first. Your task list is mirrored to .coderio/plan.md — the user may "
531
+ "edit that file between messages, and edits are adopted automatically. "
532
+ "(Trivial fixes like a typo can ignore this.)"
531
533
  )
532
534
 
533
535
  # ------------------------------------------------------ check_termination (hard gates)
@@ -628,9 +630,9 @@ class Harness:
628
630
  return (
629
631
  True,
630
632
  f"[harness] Your task list has {len(pending)} unfinished item(s). Mark "
631
- 'them complete via todo(action="update", status="completed") only if '
632
- "truly done, or finish the remaining work. Do not claim overall "
633
- "completion with pending todos.",
633
+ 'them complete via write_todos (status="completed") only if truly '
634
+ "done, or finish the remaining work. Do not claim overall completion "
635
+ "with pending todos.",
634
636
  None,
635
637
  )
636
638