cli-agent-runner 0.1.39__tar.gz → 0.1.41__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 (232) hide show
  1. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.gitignore +3 -0
  2. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/CHANGELOG.md +17 -1
  3. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/PKG-INFO +1 -1
  4. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/_emit.py +13 -7
  5. cli_agent_runner-0.1.41/agent_runner/_redact.py +102 -0
  6. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/_version.py +2 -2
  7. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/agent_runtime.py +27 -19
  8. cli_agent_runner-0.1.41/agent_runner/builtin_plugins/codewhale.py +133 -0
  9. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/init_cmd.py +13 -1
  10. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/hooks.py +4 -3
  11. cli_agent_runner-0.1.41/agent_runner/presets/codewhale.toml +30 -0
  12. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/scaffold.py +2 -2
  13. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/commands.md +4 -1
  14. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/configuration.md +11 -7
  15. cli_agent_runner-0.1.41/docs/migrations/0.1.40.md +42 -0
  16. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/quickstart.md +1 -1
  17. cli_agent_runner-0.1.41/docs/recipes/codewhale.md +98 -0
  18. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/runbook.md +12 -1
  19. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/thesis.md +26 -1
  20. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/pyproject.toml +1 -0
  21. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_grace_kill_emission.py +3 -3
  22. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_scaffold_presets.py +2 -2
  23. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_doc_claims_match_ssot.py +11 -0
  24. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_agent_runtime_grace.py +47 -5
  25. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_claude_error_detector.py +17 -0
  26. cli_agent_runner-0.1.41/tests/unit/test_codewhale_plugin.py +155 -0
  27. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_hooks.py +9 -0
  28. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_presets.py +30 -2
  29. cli_agent_runner-0.1.41/tests/unit/test_redact.py +136 -0
  30. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.codecov.yml +0 -0
  31. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  32. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  33. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  34. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  35. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.github/workflows/ci.yml +0 -0
  36. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.github/workflows/release.yml +0 -0
  37. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/.vulture-whitelist.py +0 -0
  38. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/CODE_OF_CONDUCT.md +0 -0
  39. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/CONTRIBUTING.md +0 -0
  40. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/LICENSE +0 -0
  41. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/README.md +0 -0
  42. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/README.zh.md +0 -0
  43. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/SECURITY.md +0 -0
  44. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/__init__.py +0 -0
  45. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/_docgen.py +0 -0
  46. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/_registry.py +0 -0
  47. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/_substrate.py +0 -0
  48. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/_throttle.py +0 -0
  49. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/api.py +0 -0
  50. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/api_types.py +0 -0
  51. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/builtin_plugins/__init__.py +0 -0
  52. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/builtin_plugins/_constants.py +0 -0
  53. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  54. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/builtin_plugins/gemini.py +0 -0
  55. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/__init__.py +0 -0
  56. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/__main__.py +0 -0
  57. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/common.py +0 -0
  58. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/events_cmd.py +0 -0
  59. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/install_cmd.py +0 -0
  60. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/monitor_cmd.py +0 -0
  61. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/peek_cmd.py +0 -0
  62. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/round_cmd.py +0 -0
  63. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/serve_cmd.py +0 -0
  64. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/service_cmd.py +0 -0
  65. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/cli/upgrade_cmd.py +0 -0
  66. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/config.py +0 -0
  67. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/context_store.py +0 -0
  68. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/defenses.py +0 -0
  69. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/detector_helpers.py +0 -0
  70. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/events.py +0 -0
  71. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/http_progress.py +0 -0
  72. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/lifecycle.py +0 -0
  73. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/metrics.py +0 -0
  74. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/monitor.py +0 -0
  75. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/presets/__init__.py +0 -0
  76. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/presets/aider.toml +0 -0
  77. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/presets/claude.toml +0 -0
  78. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/presets/gemini.toml +0 -0
  79. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/prompt_loader.py +0 -0
  80. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/round_log.py +0 -0
  81. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/round_view.py +0 -0
  82. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/runner.py +0 -0
  83. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/service_unit.py +0 -0
  84. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/startup_check.py +0 -0
  85. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/agent_runner/vcs_state.py +0 -0
  86. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/build.sh +0 -0
  87. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/deploy/example-agent-runner.toml +0 -0
  88. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/deploy/launchd.plist.tmpl +0 -0
  89. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/deploy/run-loop.sh +0 -0
  90. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/deploy/systemd.service.tmpl +0 -0
  91. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/README.md +0 -0
  92. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/architecture.md +0 -0
  93. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/events.md +0 -0
  94. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/long-running-agents.md +0 -0
  95. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/marketing/README.md +0 -0
  96. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/marketing/promo-cn.html +0 -0
  97. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.16.md +0 -0
  98. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.17.md +0 -0
  99. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.19.md +0 -0
  100. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.20.md +0 -0
  101. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.21.md +0 -0
  102. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.22.md +0 -0
  103. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.23.md +0 -0
  104. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.24.md +0 -0
  105. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.25.md +0 -0
  106. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.26.md +0 -0
  107. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.27.md +0 -0
  108. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.28.md +0 -0
  109. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.29.md +0 -0
  110. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.30.md +0 -0
  111. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.31.md +0 -0
  112. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.32.md +0 -0
  113. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.33.md +0 -0
  114. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.34.md +0 -0
  115. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.35.md +0 -0
  116. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.36.md +0 -0
  117. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.37.md +0 -0
  118. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.38.md +0 -0
  119. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/migrations/0.1.39.md +0 -0
  120. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/plugins.md +0 -0
  121. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/docs/recipes/aider.md +0 -0
  122. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/__init__.py +0 -0
  123. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/_test_helpers.py +0 -0
  124. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/conftest.py +0 -0
  125. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/contract/__init__.py +0 -0
  126. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/contract/test_public_api_surface.py +0 -0
  127. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/e2e/__init__.py +0 -0
  128. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/e2e/conftest.py +0 -0
  129. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  130. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/e2e/test_e2e_install_systemd.py +0 -0
  131. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  132. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  133. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  134. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  135. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  136. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/__init__.py +0 -0
  137. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_bounded_run.py +0 -0
  138. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_context_enricher_namespacing.py +0 -0
  139. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_fresh_eyes_signal.py +0 -0
  140. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_install_dry_run.py +0 -0
  141. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_monitor_seeded.py +0 -0
  142. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_plugin_detector_loaded.py +0 -0
  143. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_plugin_owned_paths.py +0 -0
  144. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_plugin_real_flow.py +0 -0
  145. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  146. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_serve_loop.py +0 -0
  147. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_substrate_fingerprint.py +0 -0
  148. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/integration/test_transient_error_backoff.py +0 -0
  149. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/__init__.py +0 -0
  150. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_architecture.py +0 -0
  151. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_atomic_write_enforced.py +0 -0
  152. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_catalogs.py +0 -0
  153. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_classification_ssot.py +0 -0
  154. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_docs_generated.py +0 -0
  155. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_entry_points_resolve.py +0 -0
  156. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_event_kind_registry.py +0 -0
  157. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_event_kinds_ssot.py +0 -0
  158. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_events_doc_contract.py +0 -0
  159. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_layer_2_loop_size.py +0 -0
  160. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_module_boundaries.py +0 -0
  161. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_module_sizes.py +0 -0
  162. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_no_ai_signatures.py +0 -0
  163. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  164. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_peek_schema_version.py +0 -0
  165. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  166. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_round_result_stable.py +0 -0
  167. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  168. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/invariants/test_upstream_schema_canary.py +0 -0
  169. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/literate/__init__.py +0 -0
  170. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/literate/parser.py +0 -0
  171. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/literate/test_parser.py +0 -0
  172. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/literate/test_quickstart.py +0 -0
  173. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/__init__.py +0 -0
  174. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_agent_runtime.py +0 -0
  175. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_agent_runtime_progress.py +0 -0
  176. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_assemble_prompt.py +0 -0
  177. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_events_stream.py +0 -0
  178. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_install.py +0 -0
  179. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_observation.py +0 -0
  180. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_read_round_num.py +0 -0
  181. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_resolve_phase.py +0 -0
  182. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_service.py +0 -0
  183. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_api_types.py +0 -0
  184. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_auto_stop_gating.py +0 -0
  185. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_cli.py +0 -0
  186. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_cli_common.py +0 -0
  187. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_cli_init_install.py +0 -0
  188. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_cli_monitor_http.py +0 -0
  189. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  190. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_cli_upgrade.py +0 -0
  191. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_config.py +0 -0
  192. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_config_fresh_eyes.py +0 -0
  193. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_config_max_rounds.py +0 -0
  194. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_config_stop_file.py +0 -0
  195. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  196. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_config_transient_error_action.py +0 -0
  197. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_context_store.py +0 -0
  198. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_defenses.py +0 -0
  199. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_detector_helpers.py +0 -0
  200. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_detector_protocol.py +0 -0
  201. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_docgen.py +0 -0
  202. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_events.py +0 -0
  203. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_events_cmd.py +0 -0
  204. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  205. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_gemini_plugin.py +0 -0
  206. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_hook_failure_isolation.py +0 -0
  207. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_http_progress.py +0 -0
  208. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_init_entry_points.py +0 -0
  209. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_lifecycle.py +0 -0
  210. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_metrics.py +0 -0
  211. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_monitor_assembly.py +0 -0
  212. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  213. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  214. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  215. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_monitor_detectors.py +0 -0
  216. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_monitor_remote.py +0 -0
  217. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_peek_argparse.py +0 -0
  218. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_peek_select.py +0 -0
  219. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_prompt_loader.py +0 -0
  220. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_round_log_helpers.py +0 -0
  221. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_round_view.py +0 -0
  222. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_runner.py +0 -0
  223. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_runner_throttle.py +0 -0
  224. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_scaffold.py +0 -0
  225. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_serve_cmd_bounded.py +0 -0
  226. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_serve_round_log.py +0 -0
  227. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_serve_sentinel.py +0 -0
  228. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_serve_startup_hooks.py +0 -0
  229. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_service_unit.py +0 -0
  230. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_startup_check.py +0 -0
  231. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_substrate.py +0 -0
  232. {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.41}/tests/unit/test_vcs_state.py +0 -0
@@ -27,3 +27,6 @@ agent_runner/_version.py
27
27
  .coverage
28
28
  coverage.xml
29
29
  htmlcov/
30
+
31
+ # Personal /team-onboarding artifact — not project content
32
+ ONBOARDING.md
@@ -5,7 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.1.41] - 2026-06-07
9
+
10
+ ### Added
11
+ - New `codewhale` preset — supervise Hmbown/CodeWhale (DeepSeek terminal agent) via `codewhale exec --auto --output-format stream-json`. `agent-runner init --preset codewhale`.
12
+ - New built-in `codewhale_error_detector` plugin — emits `agent_usage_recorded` (model + token counts) from codewhale's stream-json output. Transient-error classification is best-effort (mappable buckets only); auth failures surface via the existing monitor `oauth_fail` detector.
13
+
14
+ ## [0.1.40] - 2026-05-31
15
+
16
+ ### Security
17
+ - Grace-kill child-process fields (`live_children` / `ignored_children` in `round_grace_extended` / `round_grace_kill`) no longer store raw command lines — only the executable basename + pid (and, for ignored children, which ignore-pattern matched). This structurally prevents secrets passed in a child's arguments from reaching `events-*.jsonl`. The field shape changed from a list of strings to a list of objects.
18
+ - Free-text event excerpts that can carry agent output — `transient_error_detected.raw`, `hook_failed.error_message`/`traceback`, `serve_startup_hook_failed.exc_msg` — are now best-effort redacted (auth headers, tokens, credential URLs, `KEY=value` secrets, known key-prefixes, JWT, PEM).
19
+ - Pre-0.1.40 `events-*.jsonl` may contain unredacted argv/excerpts — see `docs/migrations/0.1.40.md`.
20
+
21
+ ### Changed
22
+ - Docs: `configuration.md` `[monitor.host_health]` example points to the generated schema table instead of restating default values.
23
+
24
+ ## [0.1.39] - 2026-05-29
9
25
 
10
26
  ### Fixed
11
27
  - Grace-kill (`max_grace_after_result_s`) is no longer defeated by long-lived helper subprocesses (e.g. claude's persistent Bash-tool shell-snapshot). `[runtime] grace_kill_ignore_patterns` lists regexes for cmdlines to exclude from the liveness count; the claude preset ships a matching default.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.39
3
+ Version: 0.1.41
4
4
  Summary: Restart-on-exit supervisor for autonomous CLI agents
5
5
  Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
6
6
  Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
@@ -112,8 +112,10 @@ def emit_transient_error_detected(
112
112
  raw: str,
113
113
  ) -> None:
114
114
  """Emit detection of a transient agent error (rate limit / 5xx / timeout)."""
115
+ from agent_runner._redact import redact_secrets
115
116
  from agent_runner.events import TRANSIENT_ERROR_DETECTED, emit
116
117
 
118
+ raw = redact_secrets(raw)
117
119
  emit(
118
120
  log_dir,
119
121
  TRANSIENT_ERROR_DETECTED,
@@ -234,12 +236,15 @@ def emit_round_grace_kill(
234
236
  *,
235
237
  round_num: int,
236
238
  grace_s: int,
237
- live_children: list[str] | None = None,
239
+ live_children: list[dict] | None = None,
238
240
  ) -> None:
239
241
  """Emit when the subprocess was killed because the grace-after-result timer
240
242
  expired AND the agent's process group had no live worker processes left
241
243
  (a genuine hang). Distinct from round_grace_extended (grace elapsed but a
242
244
  worker was still running) and round_timeout_kill (wall-clock exceeded).
245
+
246
+ live_children: list of ``{"name": <exe basename>, "pid": <int>}`` dicts
247
+ (0.1.40+; previously list of cmdline strings).
243
248
  """
244
249
  from agent_runner.events import ROUND_GRACE_KILL, emit
245
250
 
@@ -257,17 +262,18 @@ def emit_round_grace_extended(
257
262
  *,
258
263
  round_num: int,
259
264
  grace_s: int,
260
- live_children: list[str],
261
- ignored_children: list[str] | None = None,
265
+ live_children: list[dict],
266
+ ignored_children: list[dict] | None = None,
262
267
  ) -> None:
263
268
  """Emit when the grace-after-result timer expired but the agent still had
264
269
  live worker processes (e.g. a backgrounded build), so the round was NOT
265
270
  killed; it continues until it finishes or hits round_timeout_s.
266
271
 
267
- ignored_children: cmdlines that matched a grace_kill_ignore_patterns entry
268
- and were excluded from the liveness count — useful for verifying
269
- patterns are firing and for noticing when an upstream CLI changes
270
- its helper path.
272
+ live_children: list of ``{"name": <exe basename>, "pid": <int>}`` dicts
273
+ (0.1.40+; previously list of cmdline strings).
274
+ ignored_children: list of ``{"name": ..., "pid": ..., "matched": <pattern>}``
275
+ dicts for children that matched a grace_kill_ignore_patterns entry
276
+ and were excluded from the liveness count (0.1.40+; previously cmdline strings).
271
277
  """
272
278
  from agent_runner.events import ROUND_GRACE_EXTENDED, emit
273
279
 
@@ -0,0 +1,102 @@
1
+ """Mask secret-bearing tokens in semi-trusted free text before it is persisted
2
+ to durable, default-readable event logs (events-*.jsonl).
3
+
4
+ Single source of truth for secret-shaping. Pure, dependency-free, idempotent.
5
+ BEST-EFFORT: applied only to free-text excerpts (transient error output, hook
6
+ exception messages/tracebacks), never as the sole control. The grace-kill child
7
+ fields avoid this entirely by storing basename+pid, not argv.
8
+
9
+ Patterns are length/charset-anchored so benign argv (sk-report.md, psql -h db,
10
+ 'Basic auth disabled') passes through unchanged.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+
17
+ _MASK = "<redacted>"
18
+
19
+ _LONG_FLAGS = [
20
+ "--token",
21
+ "--password",
22
+ "--passwd",
23
+ "--api-key",
24
+ "--apikey",
25
+ "--secret",
26
+ "--secret-access-key",
27
+ "--access-key",
28
+ "--auth",
29
+ "--authorization",
30
+ "--client-secret",
31
+ "--aws-session-token",
32
+ "--bearer",
33
+ "--auth-token",
34
+ "--private-key",
35
+ ]
36
+ # (prefix, min-tail-len) — anchored so short filenames don't match.
37
+ _PREFIX_RES = [
38
+ r"sk-ant-[A-Za-z0-9\-_]{16,}",
39
+ r"sk-[A-Za-z0-9]{16,}",
40
+ r"ghp_[A-Za-z0-9]{20,}",
41
+ r"gho_[A-Za-z0-9]{20,}",
42
+ r"ghs_[A-Za-z0-9]{20,}",
43
+ r"ghu_[A-Za-z0-9]{20,}",
44
+ r"github_pat_[A-Za-z0-9_]{20,}",
45
+ r"xox[bpars]-[A-Za-z0-9-]{10,}",
46
+ r"xapp-[A-Za-z0-9-]{10,}",
47
+ r"AKIA[0-9A-Z]{16}",
48
+ r"ASIA[0-9A-Z]{16}",
49
+ r"AIza[0-9A-Za-z_\-]{35}",
50
+ r"glpat-[A-Za-z0-9_\-]{20}",
51
+ r"ya29\.[A-Za-z0-9._\-]{20,}",
52
+ r"(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}",
53
+ r"npm_[A-Za-z0-9]{20,}",
54
+ r"hf_[A-Za-z0-9]{20,}",
55
+ ]
56
+
57
+ _FLAG_RE = re.compile(r"(?i)(" + "|".join(re.escape(f) for f in _LONG_FLAGS) + r")(\s+|=)(\S+)")
58
+ # Short HTTP-basic flag `-u user:pass` (curl/wget). Case-SENSITIVE so `-U`
59
+ # (psql/pg username) is left alone; a colon is required so `sort -u file` and
60
+ # bare `-u username` (no password) are not masked; and the value must NOT be a
61
+ # URL (`://`) — a `-u <url>` is left for _URL_USERINFO_RE, which masks only the
62
+ # userinfo and preserves the host (e.g. redis://<redacted>@cache:6379/0).
63
+ _SHORT_USER_RE = re.compile(r"(?<![\w-])-u(\s+|=)(?!\S*://)(\S+:\S+)")
64
+ _HEADER_NAME_RE = re.compile(
65
+ r"(?im)\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie|"
66
+ r"X-Api-Key|X-Auth-Token|X-Amz-Security-Token)(\s*:\s*)([^\r\n]+)"
67
+ )
68
+ _SCHEME_RE = re.compile(
69
+ r"\b(Bearer|Basic|Token|ApiKey|Digest|Negotiate|NTLM)(\s+)([A-Za-z0-9+/=._\-]{12,})"
70
+ )
71
+ _URL_USERINFO_RE = re.compile(r"(?i)([a-z][a-z0-9+.\-]*://)([^/\s@]+)@")
72
+ _URL_QUERY_RE = re.compile(
73
+ r"(?i)([?&#](?:access_token|api_?key|token|auth|secret|sig|signature|"
74
+ r"password|client_secret|x-amz-security-token|x-amz-signature)=)([^&#\s]+)"
75
+ )
76
+ _ENV_RE = re.compile(
77
+ r"(?i)((?:(?<=\s)|^)[A-Za-z_][A-Za-z0-9_]*"
78
+ r"(?:PASSWORD|PASSWD|PWD|TOKEN|SECRET|API[_-]?KEY|ACCESS[_-]?KEY|CREDENTIAL|PRIVATE[_-]?KEY)"
79
+ r"[A-Za-z0-9_]*)=(\S+)"
80
+ )
81
+ _PREFIX_RE = re.compile(r"(?<![A-Za-z0-9])(?:" + "|".join(_PREFIX_RES) + r")")
82
+ _JWT_RE = re.compile(r"\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+")
83
+ _PEM_RE = re.compile(
84
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.DOTALL
85
+ )
86
+
87
+
88
+ def redact_secrets(text: str) -> str:
89
+ """Return *text* with secret-bearing tokens replaced by ``<redacted>``."""
90
+ if not text:
91
+ return text
92
+ out = _PEM_RE.sub(_MASK, text)
93
+ out = _ENV_RE.sub(rf"\1={_MASK}", out)
94
+ out = _FLAG_RE.sub(rf"\1\2{_MASK}", out)
95
+ out = _SHORT_USER_RE.sub(rf"-u\1{_MASK}", out)
96
+ out = _HEADER_NAME_RE.sub(rf"\1\2{_MASK}", out)
97
+ out = _SCHEME_RE.sub(rf"\1\2{_MASK}", out)
98
+ out = _URL_USERINFO_RE.sub(rf"\1{_MASK}@", out)
99
+ out = _URL_QUERY_RE.sub(rf"\1{_MASK}", out)
100
+ out = _JWT_RE.sub(_MASK, out)
101
+ out = _PREFIX_RE.sub(_MASK, out)
102
+ return out
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.39'
22
- __version_tuple__ = version_tuple = (0, 1, 39)
21
+ __version__ = version = '0.1.41'
22
+ __version_tuple__ = version_tuple = (0, 1, 41)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -31,7 +31,7 @@ class RunResult:
31
31
  timed_out: bool
32
32
  pid: int
33
33
  killed_for_grace: bool = False
34
- grace_kill_children: list[str] = field(default_factory=list)
34
+ grace_kill_children: list[dict] = field(default_factory=list)
35
35
 
36
36
 
37
37
  def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
@@ -63,35 +63,43 @@ def _live_children(
63
63
  *,
64
64
  ignore_patterns: list[re.Pattern[str]] | None = None,
65
65
  max_n: int = 5,
66
- max_len: int = 120,
67
- ) -> tuple[list[str], list[str]]:
68
- """Cmdlines of live (non-zombie) descendants of ``proc``, split into
69
- ``(live, ignored)``: ``live`` is what counts toward the grace-kill
70
- liveness check; ``ignored`` matched an ``ignore_patterns`` entry and is
71
- excluded (e.g. claude's persistent shell-snapshot helper). Both lists
72
- are bounded by ``max_n``/``max_len`` to keep events small. ``ignore_patterns
73
- is None`` no filtering, ``ignored`` is empty, ``live`` matches 0.1.38.
66
+ ) -> tuple[list[dict], list[dict]]:
67
+ """Live (non-zombie) descendants of ``proc``, split into ``(live, ignored)``.
68
+
69
+ Each entry is ``{"name": <executable basename>, "pid": <int>}``; an ignored
70
+ entry also carries ``"matched": <pattern str>``. We store only basename+pid,
71
+ NOT argv process arguments are where secrets leak (PGPASSWORD=…, --api-key
72
+ …, redis://:pass@…) and these lists are persisted to events-*.jsonl.
73
+ Ignore-pattern MATCHING runs against the full cmdline (detection unchanged);
74
+ only what we STORE is minimized.
74
75
  """
75
76
  try:
76
77
  parent = psutil.Process(proc.pid)
77
78
  except (psutil.NoSuchProcess, psutil.AccessDenied):
78
79
  return [], []
79
- live: list[str] = []
80
- ignored: list[str] = []
80
+ live: list[dict] = []
81
+ ignored: list[dict] = []
81
82
  for child in parent.children(recursive=True):
82
83
  try:
83
84
  if child.status() == psutil.STATUS_ZOMBIE:
84
85
  continue
85
- line = " ".join(child.cmdline()) or child.name()
86
+ argv = child.cmdline()
87
+ full = " ".join(argv) or child.name() # MATCHING only
88
+ name = (Path(argv[0]).name if argv else "") or child.name()
89
+ pid = child.pid
86
90
  except (psutil.NoSuchProcess, psutil.AccessDenied):
87
91
  continue
88
- short = line[:max_len]
89
- if ignore_patterns and any(p.search(line) for p in ignore_patterns):
92
+ matched = None
93
+ if ignore_patterns:
94
+ for p in ignore_patterns:
95
+ if p.search(full):
96
+ matched = p.pattern
97
+ break
98
+ if matched is not None:
90
99
  if len(ignored) < max_n:
91
- ignored.append(short)
92
- else:
93
- if len(live) < max_n:
94
- live.append(short)
100
+ ignored.append({"name": name, "pid": pid, "matched": matched})
101
+ elif len(live) < max_n:
102
+ live.append({"name": name, "pid": pid})
95
103
  if len(live) >= max_n and len(ignored) >= max_n:
96
104
  break
97
105
  return live, ignored
@@ -114,7 +122,7 @@ def run(
114
122
  max_grace_after_result_s: int = 0,
115
123
  progress_callback: Callable[[dict], None] | None = None,
116
124
  progress_interval_s: int = 0,
117
- on_grace_extended: Callable[[list[str], list[str]], None] | None = None,
125
+ on_grace_extended: Callable[[list[dict], list[dict]], None] | None = None,
118
126
  grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
119
127
  ) -> RunResult:
120
128
  """Spawn the agent subprocess and wait for exit or timeout.
@@ -0,0 +1,133 @@
1
+ """Built-in post_round_hook for codewhale CLI: usage events + transient classifier.
2
+
3
+ Third built-in plugin (after claude, gemini). Parses codewhale's `exec
4
+ --output-format stream-json` NDJSON stdout tail; emits agent_usage_recorded
5
+ from the terminal metadata record. Transient-error classification is
6
+ best-effort and emits ONLY when an error maps to an existing bucket (like
7
+ gemini): codewhale's exec stdout surfaces a {"type":"error"} record, but the
8
+ only observed case so far is auth failure (oauth_fail territory, not a
9
+ transient bucket), so nothing maps yet -- usage-only today. 429/5xx mapping
10
+ is added when a real rate-limit sample is captured.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import time
17
+ from collections import deque
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from agent_runner.api import (
22
+ emit_agent_usage_recorded,
23
+ emit_transient_error_detected,
24
+ )
25
+ from agent_runner.builtin_plugins._constants import (
26
+ _5XX_STATUSES,
27
+ _BACK_OFF_DEFAULTS,
28
+ _RAW_CAP,
29
+ _TAIL_LINES,
30
+ )
31
+ from agent_runner.hooks import HookContext, register_post_round_hook
32
+
33
+
34
+ class CodewhaleErrorDetector:
35
+ """Parse codewhale round log tail; emit usage + transient_error_detected events."""
36
+
37
+ name = "codewhale_error_detector"
38
+
39
+ def after_round(self, ctx: HookContext, result: Any) -> None:
40
+ if ctx.agent_binary != "codewhale":
41
+ return
42
+ log_path = ctx.agent_log_path
43
+ if log_path is None or not log_path.exists():
44
+ return
45
+ parsed = _parse_codewhale_log(log_path)
46
+ if parsed.get("transient_error"):
47
+ emit_transient_error_detected(
48
+ ctx.log_dir, round_num=ctx.round_num, **parsed["transient_error"]
49
+ )
50
+ if parsed.get("usage"):
51
+ emit_agent_usage_recorded(
52
+ ctx.log_dir,
53
+ round_num=ctx.round_num,
54
+ phase=ctx.phase or "",
55
+ success=(result.exit_code == 0 and not result.timed_out),
56
+ **parsed["usage"],
57
+ )
58
+
59
+
60
+ def _parse_codewhale_log(log_path: Path) -> dict[str, Any]:
61
+ """Scan last _TAIL_LINES of codewhale NDJSON; extract usage from the metadata
62
+ record; classify any {"type":"error"} that maps to a transient bucket.
63
+
64
+ Tolerates non-JSON lines (codewhale prefixes some stdout with terminal
65
+ escapes) via per-line try/except.
66
+ """
67
+ with log_path.open("r", encoding="utf-8", errors="replace") as f:
68
+ tail = deque(f, maxlen=_TAIL_LINES)
69
+ metadata: dict | None = None
70
+ error_event: dict | None = None
71
+ for line in tail:
72
+ line = line.strip()
73
+ if not line:
74
+ continue
75
+ try:
76
+ event = json.loads(line)
77
+ except json.JSONDecodeError:
78
+ continue
79
+ if not isinstance(event, dict):
80
+ continue
81
+ etype = event.get("type")
82
+ if etype == "metadata":
83
+ metadata = event.get("meta") or {}
84
+ elif etype == "error":
85
+ error_event = event
86
+
87
+ out: dict[str, Any] = {}
88
+
89
+ if metadata:
90
+ out["usage"] = {
91
+ "agent": "codewhale",
92
+ "model": str(metadata.get("model", "unknown")),
93
+ "input_tokens": int(metadata.get("input_tokens", 0)),
94
+ "output_tokens": int(metadata.get("output_tokens", 0)),
95
+ "cached_tokens": 0, # codewhale exec stdout exposes no cache counts
96
+ "cost_usd": None, # codewhale exec stdout exposes no USD
97
+ "duration_ms": 0, # not in exec metadata
98
+ }
99
+
100
+ if error_event is not None:
101
+ classification = _classify_codewhale_error(error_event)
102
+ if classification:
103
+ duration = _BACK_OFF_DEFAULTS[classification]
104
+ out["transient_error"] = {
105
+ "classification": classification,
106
+ "agent": "codewhale",
107
+ "reset_at_epoch": int(time.time() + duration),
108
+ "raw": str(error_event.get("error", "error"))[:_RAW_CAP],
109
+ }
110
+ return out
111
+
112
+
113
+ def _classify_codewhale_error(error_event: dict[str, Any]) -> str | None:
114
+ """Map a codewhale {"type":"error"} record to a transient bucket, or None.
115
+
116
+ None means 'not a transient error' (e.g. auth failure -> handled by the
117
+ monitor's oauth_fail log-scan, not the transient classifier). codewhale's
118
+ error record currently carries only a free-text 'error' string with no
119
+ status code; until a real rate-limit/5xx sample is captured we cannot map
120
+ to rate_limit_model / api_transient_5xx / api_timeout, so we return None.
121
+ A future revision keys on a numeric status field once observed.
122
+ """
123
+ code = error_event.get("code") or error_event.get("status_code")
124
+ if code == 429:
125
+ return "rate_limit_model"
126
+ if code in _5XX_STATUSES:
127
+ return "api_transient_5xx"
128
+ if code == 408:
129
+ return "api_timeout"
130
+ return None
131
+
132
+
133
+ register_post_round_hook(CodewhaleErrorDetector())
@@ -2,15 +2,27 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import importlib.resources
6
+
5
7
  from agent_runner import api
6
8
  from agent_runner.cli.common import emit, fail, work_dir_from_args
7
9
 
8
10
 
11
+ def _preset_names() -> list[str]:
12
+ """Discover scaffold presets from the shipped ``agent_runner/presets/*.toml``.
13
+
14
+ Derived (not hardcoded) so adding a preset is a single new .toml file — the
15
+ ``--preset`` choices and validation track the filesystem automatically.
16
+ """
17
+ presets = importlib.resources.files("agent_runner.presets")
18
+ return sorted(p.name[:-5] for p in presets.iterdir() if p.name.endswith(".toml"))
19
+
20
+
9
21
  def add_parser(sub, parent) -> None:
10
22
  p = sub.add_parser("init", parents=[parent], help="Scaffold agent-runner project files")
11
23
  p.add_argument(
12
24
  "--preset",
13
- choices=["claude", "aider", "gemini"],
25
+ choices=_preset_names(),
14
26
  default="claude",
15
27
  help="Which agent CLI preset to scaffold (default: claude)",
16
28
  )
@@ -35,6 +35,7 @@ from pathlib import Path
35
35
  from typing import Any, Protocol, runtime_checkable
36
36
 
37
37
  from agent_runner import events
38
+ from agent_runner._redact import redact_secrets
38
39
  from agent_runner._registry import ensure_unique
39
40
 
40
41
  _HEAD_BYTES = 1024
@@ -201,7 +202,7 @@ def run_serve_startup_hooks(cfg: Any, log_dir: Path) -> bool:
201
202
  hook(cfg)
202
203
  except Exception as e: # noqa: BLE001 — hook is plugin contract; any failure aborts serve
203
204
  exc_type = type(e).__name__
204
- exc_msg = str(e)[:200]
205
+ exc_msg = redact_secrets(str(e))[:200]
205
206
  print(
206
207
  f"agent-runner: serve_startup_hook {hook.name} failed: {exc_type}: {exc_msg}",
207
208
  file=sys.stderr,
@@ -232,6 +233,6 @@ def _summarize_error(exc: BaseException, tb: str) -> dict[str, str]:
232
233
  trimmed = tb[:_HEAD_BYTES] + _TRUNC_MARKER + tb[-_TAIL_BYTES:]
233
234
  return {
234
235
  "error_type": type(exc).__name__,
235
- "error_message": str(exc),
236
- "traceback": trimmed,
236
+ "error_message": redact_secrets(str(exc)),
237
+ "traceback": redact_secrets(trimmed),
237
238
  }
@@ -0,0 +1,30 @@
1
+ # agent-runner.toml — generated by `agent-runner init --preset codewhale`.
2
+ #
3
+ # Prereqs:
4
+ # - codewhale installed (ships `codewhale` + `codewhale-tui`; both on PATH):
5
+ # npm i -g codewhale (or cargo/brew per CodeWhale docs)
6
+ # - DEEPSEEK_API_KEY set on the supervisor host (or a key saved via
7
+ # `codewhale auth set`; resolution order is config > keyring > env)
8
+ # - work_dir is a git repo
9
+
10
+ [agent]
11
+ command = ["codewhale", "exec", "--auto", "--output-format", "stream-json"]
12
+ prompt_arg_template = ["{prompt}"]
13
+ name = "codewhale"
14
+
15
+ [runtime]
16
+ work_dir = "."
17
+ log_dir = "~/.agent-runner/{project}/logs"
18
+ round_timeout_s = 1800
19
+ restart_delay_s = 3
20
+
21
+ [prompt]
22
+ file = "./prompts/main.md"
23
+ inject_context = true
24
+
25
+ [vcs]
26
+ dirty_action = "stash"
27
+ stash_idempotency_s = 5
28
+
29
+ [monitor]
30
+ auth_fail_hint = "Run `codewhale auth status` to inspect provider/credentials, or set DEEPSEEK_API_KEY on the supervisor host."
@@ -5,8 +5,8 @@ Writes three files into a git repo:
5
5
  prompts/main.md — neutral 8-line placeholder
6
6
  .gitignore — append "logs/" if missing
7
7
 
8
- Available presets ship as package data in `agent_runner/presets/*.toml`.
9
- Currently: `claude`, `aider`, `gemini`.
8
+ Available presets ship as package data in `agent_runner/presets/*.toml`;
9
+ `agent-runner init --preset <name>` discovers them from that directory.
10
10
 
11
11
  Optionally commits in one step (default true via the CLI).
12
12
  """
@@ -36,7 +36,7 @@ appends `logs/` to `.gitignore`. By default also creates a git commit.
36
36
 
37
37
  Flags:
38
38
 
39
- - `--preset {claude,aider,gemini}` — agent CLI preset to scaffold (default: `claude`)
39
+ - `--preset {claude,aider,gemini,codewhale}` — agent CLI preset to scaffold (default: `claude`)
40
40
  - `--force` — overwrite an existing `agent-runner.toml`
41
41
  - `--no-commit` — skip the initial git commit
42
42
 
@@ -101,6 +101,9 @@ back to package-only mode automatically.
101
101
  `--no-restart` forces package-only even on a systemd --user host (upgrade the
102
102
  package now, restart your service yourself).
103
103
 
104
+ Operator walkthrough (per-deployment decision table, rollback, failure modes,
105
+ postmortem trail): see `docs/runbook.md` § "Upgrading agent-runner".
106
+
104
107
  ## Observation
105
108
 
106
109
  ### `agent-runner peek [flags]`
@@ -103,10 +103,11 @@ working tree:
103
103
  `[agent.env]` is a flat `dict[str, str]` of environment variables injected into
104
104
  the agent subprocess **per round**. This is preset-supplied per CLI: e.g. the
105
105
  claude preset sets `DISABLE_AUTOUPDATER=1` to prevent mid-loop self-updates;
106
- the aider preset omits `[agent.env]` entirely. Override these values in your
107
- project's `agent-runner.toml` only when you need to deviate from the preset
108
- default. The runtime merges `[agent.env]` on top of the supervisor's own env;
109
- unset (empty string) does not unset an inherited variable.
106
+ the aider and codewhale presets omit `[agent.env]` entirely (both resolve their
107
+ API keys from the ambient environment or their own keyrings). Override these
108
+ values in your project's `agent-runner.toml` only when you need to deviate from
109
+ the preset default. The runtime merges `[agent.env]` on top of the supervisor's
110
+ own env; unset (empty string) does not unset an inherited variable.
110
111
 
111
112
  ## `[monitor].auth_fail_hint` (preset-supplied)
112
113
 
@@ -117,6 +118,8 @@ guidance without authoring it themselves:
117
118
  - `--preset claude` → recommend `claude /login` / refresh `ANTHROPIC_API_KEY`.
118
119
  - `--preset aider` → verify provider env var (`OPENAI_API_KEY` /
119
120
  `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / etc.); run `aider --models`.
121
+ - `--preset codewhale` → run `codewhale auth status` to inspect provider
122
+ credentials, or set `DEEPSEEK_API_KEY` on the supervisor host.
120
123
 
121
124
  Override in your `agent-runner.toml` if you ship a custom CLI.
122
125
 
@@ -212,9 +215,10 @@ round_progress_interval_s = 0 # 0 = disabled; set >0 to emit round_progress hea
212
215
  # supervisor_stale_threshold_s = 2700 # unset = round_timeout_s * 1.5; 0 = disable
213
216
 
214
217
  [monitor.host_health]
215
- mem_avail_min_mb = 200 # mem_pressure fires when mem_available_mb < this
216
- disk_warning_pct = 90.0 # disk_warning fires when disk_used_pct >= this
217
- disk_critical_pct = 95.0 # disk_critical fires when disk_used_pct >= this
218
+ # Thresholds for mem_pressure / disk_warning / disk_critical. Defaults are
219
+ # authoritative in the config-schema table above set a field here only to
220
+ # override. (mem_avail_min_mb: mem_pressure when mem_available_mb below it;
221
+ # disk_warning_pct / disk_critical_pct: fire when disk_used_pct at/above.)
218
222
  ```
219
223
 
220
224
  Comment out individual entries to disable; e.g. `# auto_stop_on = []` disables
@@ -0,0 +1,42 @@
1
+ # Migrating to 0.1.40
2
+
3
+ ## TL;DR
4
+
5
+ ```bash
6
+ pip install --upgrade cli-agent-runner==0.1.40
7
+ ```
8
+
9
+ A security release. No config or behavior change for the supervisor. One
10
+ consumer-visible field-shape change (below).
11
+
12
+ ## Breaking: grace-kill child fields are now objects, not strings
13
+
14
+ `round_grace_extended` / `round_grace_kill` previously carried
15
+ `live_children` / `ignored_children` as lists of command-line **strings**.
16
+ They are now lists of **objects**:
17
+
18
+ - `live_children`: `[{"name": "<exe basename>", "pid": <int>}, ...]`
19
+ - `ignored_children`: `[{"name": ..., "pid": ..., "matched": "<pattern>"}, ...]`
20
+
21
+ If you parse these fields, update your consumer. The change exists because the
22
+ old strings were full process command lines, which could contain a secret
23
+ passed in a child's arguments (a DB password, an API key, a credential URL).
24
+ Storing only basename + pid removes that leak by construction. Ignore-pattern
25
+ matching is unchanged — it still runs against the full command line; only what
26
+ is **stored** is minimized.
27
+
28
+ ## Best-effort redaction of free-text excerpts
29
+
30
+ `transient_error_detected.raw`, `hook_failed.error_message`/`traceback`, and
31
+ `serve_startup_hook_failed.exc_msg` are now passed through a redactor that masks
32
+ auth headers, tokens, credential URLs, `KEY=value` secrets, known key-prefixes,
33
+ JWTs, and PEM blocks. This is **best-effort defense-in-depth**, not a guarantee:
34
+ do not pass secrets as command-line arguments to supervised agents, and treat
35
+ `events-*.jsonl` as sensitive.
36
+
37
+ ## One-time action (if applicable)
38
+
39
+ If pre-0.1.40 `events-*.jsonl` were shipped to a log aggregator, shared, or
40
+ fetched cross-host (`monitor --host`), review them for secret-bearing child
41
+ argv / error excerpts and rotate any exposed credential. Post-upgrade events
42
+ are protected.
@@ -37,7 +37,7 @@ Edit `prompts/main.md` to describe what the agent should do per round.
37
37
  Edit `agent-runner.toml` if you need to change `round_timeout_s` or `[phases]`.
38
38
 
39
39
  The default preset (`--preset claude`) invokes `claude`. Other built-in
40
- presets: `--preset aider` and `--preset gemini`. To use any other CLI,
40
+ presets: `--preset aider`, `--preset gemini`, and `--preset codewhale`. To use any other CLI,
41
41
  edit `agent.command` to your CLI's invocation and `agent.prompt_arg_template`
42
42
  to its prompt-argument syntax — for example:
43
43