cli-agent-runner 0.1.6__tar.gz → 0.1.7__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 (140) hide show
  1. cli_agent_runner-0.1.7/.githooks/commit-msg +33 -0
  2. cli_agent_runner-0.1.7/.github/workflows/ci.yml +105 -0
  3. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/CHANGELOG.md +58 -0
  4. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/CONTRIBUTING.md +8 -0
  5. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/PKG-INFO +6 -5
  6. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/README.md +5 -4
  7. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/_version.py +2 -2
  8. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/agent_runtime.py +8 -19
  9. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/api.py +8 -2
  10. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/api_types.py +1 -0
  11. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/common.py +1 -1
  12. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/init_cmd.py +8 -2
  13. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/config.py +8 -4
  14. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/defenses.py +4 -4
  15. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/monitor.py +2 -2
  16. cli_agent_runner-0.1.7/agent_runner/presets/aider.toml +30 -0
  17. cli_agent_runner-0.1.7/agent_runner/presets/claude.toml +29 -0
  18. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/runner.py +1 -1
  19. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/scaffold.py +26 -38
  20. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/docs/README.md +9 -6
  21. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/docs/architecture.md +6 -6
  22. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/docs/configuration.md +33 -6
  23. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/docs/quickstart.md +11 -6
  24. cli_agent_runner-0.1.7/docs/recipes/aider.md +117 -0
  25. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/docs/runbook.md +14 -3
  26. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/conftest.py +27 -0
  27. cli_agent_runner-0.1.7/tests/contract/test_public_api_surface.py +130 -0
  28. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/integration/test_run_one_round_with_fake_agent.py +39 -0
  29. cli_agent_runner-0.1.7/tests/integration/test_scaffold_presets.py +80 -0
  30. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_module_sizes.py +7 -4
  31. cli_agent_runner-0.1.7/tests/invariants/test_no_ai_signatures.py +83 -0
  32. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_peek_schema_version.py +2 -2
  33. cli_agent_runner-0.1.7/tests/literate/__init__.py +0 -0
  34. cli_agent_runner-0.1.7/tests/unit/__init__.py +0 -0
  35. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_agent_runtime.py +23 -12
  36. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_cli_init_install.py +27 -0
  37. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_config.py +96 -14
  38. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_defenses.py +26 -2
  39. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_monitor_detectors.py +5 -5
  40. cli_agent_runner-0.1.7/tests/unit/test_presets.py +87 -0
  41. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_scaffold.py +38 -0
  42. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_service_unit.py +1 -1
  43. cli_agent_runner-0.1.6/.github/workflows/ci.yml +0 -57
  44. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.codecov.yml +0 -0
  45. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  46. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  47. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  48. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  49. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.github/workflows/release.yml +0 -0
  50. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.gitignore +0 -0
  51. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/.vulture-whitelist.py +0 -0
  52. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/CODE_OF_CONDUCT.md +0 -0
  53. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/LICENSE +0 -0
  54. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/README.zh.md +0 -0
  55. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/SECURITY.md +0 -0
  56. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/__init__.py +0 -0
  57. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/_docgen.py +0 -0
  58. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/_registry.py +0 -0
  59. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/__init__.py +0 -0
  60. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/__main__.py +0 -0
  61. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/install_cmd.py +0 -0
  62. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/monitor_cmd.py +0 -0
  63. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/peek_cmd.py +0 -0
  64. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/round_cmd.py +0 -0
  65. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/serve_cmd.py +0 -0
  66. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/cli/service_cmd.py +0 -0
  67. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/context_store.py +0 -0
  68. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/detector_helpers.py +0 -0
  69. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/events.py +0 -0
  70. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/hooks.py +0 -0
  71. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/lifecycle.py +0 -0
  72. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/metrics.py +0 -0
  73. {cli_agent_runner-0.1.6/tests → cli_agent_runner-0.1.7/agent_runner/presets}/__init__.py +0 -0
  74. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/prompt_loader.py +0 -0
  75. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/round_view.py +0 -0
  76. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/service_unit.py +0 -0
  77. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/startup_check.py +0 -0
  78. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/agent_runner/vcs_state.py +0 -0
  79. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/build.sh +0 -0
  80. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/deploy/example-agent-runner.toml +0 -0
  81. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/deploy/launchd.plist.tmpl +0 -0
  82. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/deploy/run-loop.sh +0 -0
  83. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/deploy/systemd.service.tmpl +0 -0
  84. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/docs/commands.md +0 -0
  85. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/docs/plugins.md +0 -0
  86. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/pyproject.toml +0 -0
  87. {cli_agent_runner-0.1.6/tests/e2e → cli_agent_runner-0.1.7/tests}/__init__.py +0 -0
  88. {cli_agent_runner-0.1.6/tests/integration → cli_agent_runner-0.1.7/tests/contract}/__init__.py +0 -0
  89. {cli_agent_runner-0.1.6/tests/invariants → cli_agent_runner-0.1.7/tests/e2e}/__init__.py +0 -0
  90. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/e2e/conftest.py +0 -0
  91. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  92. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/e2e/test_e2e_install_systemd.py +0 -0
  93. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  94. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  95. {cli_agent_runner-0.1.6/tests/literate → cli_agent_runner-0.1.7/tests/integration}/__init__.py +0 -0
  96. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/integration/test_context_enricher_namespacing.py +0 -0
  97. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/integration/test_install_dry_run.py +0 -0
  98. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/integration/test_monitor_seeded.py +0 -0
  99. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/integration/test_plugin_detector_loaded.py +0 -0
  100. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/integration/test_run_loop_backoff.py +0 -0
  101. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/integration/test_serve_loop.py +0 -0
  102. {cli_agent_runner-0.1.6/tests/unit → cli_agent_runner-0.1.7/tests/invariants}/__init__.py +0 -0
  103. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_architecture.py +0 -0
  104. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_atomic_write_enforced.py +0 -0
  105. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_catalogs.py +0 -0
  106. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_docs_generated.py +0 -0
  107. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_event_kind_registry.py +0 -0
  108. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_module_boundaries.py +0 -0
  109. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  110. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  111. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_round_result_stable.py +0 -0
  112. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  113. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/literate/parser.py +0 -0
  114. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/literate/test_parser.py +0 -0
  115. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/literate/test_quickstart.py +0 -0
  116. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_api_observation.py +0 -0
  117. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_api_service.py +0 -0
  118. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_api_types.py +0 -0
  119. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_auto_stop_gating.py +0 -0
  120. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_cli.py +0 -0
  121. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_cli_common.py +0 -0
  122. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  123. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_context_store.py +0 -0
  124. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_detector_helpers.py +0 -0
  125. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_detector_protocol.py +0 -0
  126. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_docgen.py +0 -0
  127. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_events.py +0 -0
  128. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_hook_failure_isolation.py +0 -0
  129. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_hooks.py +0 -0
  130. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_init_entry_points.py +0 -0
  131. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_lifecycle.py +0 -0
  132. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_metrics.py +0 -0
  133. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_monitor_assembly.py +0 -0
  134. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_monitor_remote.py +0 -0
  135. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_peek_argparse.py +0 -0
  136. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_prompt_loader.py +0 -0
  137. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_round_view.py +0 -0
  138. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_runner.py +0 -0
  139. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_startup_check.py +0 -0
  140. {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.7}/tests/unit/test_vcs_state.py +0 -0
@@ -0,0 +1,33 @@
1
+ #!/bin/sh
2
+ # Reject commit messages that contain AI-tool attribution.
3
+ #
4
+ # Rationale: this repository's history is curated to credit only the human
5
+ # author. AI-tool trailers (Co-Authored-By: Claude, 🤖 Generated with ..., etc.)
6
+ # create false "claude" / "anthropic" entries in GitHub's Contributors widget
7
+ # and bloat commit log noise.
8
+ #
9
+ # Activate per clone: git config core.hooksPath .githooks
10
+ # Bypass (rarely needed): git commit --no-verify
11
+
12
+ MSG_FILE="$1"
13
+
14
+ # Strip git-comment lines before checking
15
+ CONTENT=$(grep -v '^#' "$MSG_FILE" || true)
16
+
17
+ # Case-insensitive regex; tab/space tolerant
18
+ FORBIDDEN_REGEX='([Cc]o-[Aa]uthored-[Bb]y:|🤖|[Gg]enerated with [Cc]laude|[Gg]enerated with \[Cursor\]|noreply@anthropic\.com)'
19
+
20
+ if printf '%s\n' "$CONTENT" | grep -E -q "$FORBIDDEN_REGEX"; then
21
+ echo "" >&2
22
+ echo "commit-msg hook: AI-tool attribution detected in commit message." >&2
23
+ echo "" >&2
24
+ echo "Offending lines:" >&2
25
+ printf '%s\n' "$CONTENT" | grep -E -n "$FORBIDDEN_REGEX" | sed 's/^/ /' >&2
26
+ echo "" >&2
27
+ echo "Strip Co-Authored-By trailers, 🤖, 'Generated with ...', and" >&2
28
+ echo "noreply@anthropic.com refs, then retry." >&2
29
+ echo "" >&2
30
+ echo "(Bypass: git commit --no-verify — use sparingly, requires explicit" >&2
31
+ echo " review since CI lint-commits job will also reject the push.)" >&2
32
+ exit 1
33
+ fi
@@ -0,0 +1,105 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ # Cancel in-flight runs on the same PR ref when a new push lands.
10
+ # Push-to-main runs always complete (cancel-in-progress only on PRs).
11
+ concurrency:
12
+ group: ci-${{ github.ref }}
13
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
14
+
15
+ jobs:
16
+ lint-commits:
17
+ name: lint commit messages (no AI-tool attribution)
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 2
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ # PR: scan only new commits the PR introduces (base..head).
24
+ # Push to main: scan the just-pushed commits (before..after).
25
+ # fetch-depth=0 ensures both refs are available.
26
+ fetch-depth: 0
27
+ - name: scan new commit messages
28
+ env:
29
+ GH_EVENT: ${{ github.event_name }}
30
+ BEFORE_SHA: ${{ github.event.before }}
31
+ BASE_REF: ${{ github.event.pull_request.base.sha }}
32
+ HEAD_REF: ${{ github.event.pull_request.head.sha }}
33
+ run: |
34
+ set -euo pipefail
35
+ if [ "$GH_EVENT" = "pull_request" ]; then
36
+ RANGE="${BASE_REF}..${HEAD_REF}"
37
+ else
38
+ # push event; before is 0..0 on first push or branch creation
39
+ if [ -z "${BEFORE_SHA:-}" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
40
+ RANGE="HEAD~1..HEAD"
41
+ else
42
+ RANGE="${BEFORE_SHA}..HEAD"
43
+ fi
44
+ fi
45
+ echo "Scanning commit-message bodies in $RANGE"
46
+ # Collect messages joined by NUL bytes; grep -P for fixed alternation.
47
+ BODIES=$(git log --format='%B%x00' "$RANGE" || true)
48
+ if [ -z "$BODIES" ]; then
49
+ echo " (no commits in range)"
50
+ exit 0
51
+ fi
52
+ PATTERN='(Co-Authored-By:|🤖|Generated with Claude|Generated with \[Cursor\]|noreply@anthropic\.com)'
53
+ if printf '%s' "$BODIES" | grep -i -E -q "$PATTERN"; then
54
+ echo "::error::AI-tool attribution detected in new commit messages."
55
+ echo "Offending lines:"
56
+ printf '%s' "$BODIES" | grep -i -n -E "$PATTERN" || true
57
+ echo ""
58
+ echo "Strip Co-Authored-By trailers, 🤖, 'Generated with ...',"
59
+ echo "and noreply@anthropic.com refs, then force-push the fixed range."
60
+ exit 1
61
+ fi
62
+ echo " clean"
63
+
64
+ test:
65
+ name: test py${{ matrix.python }} / ${{ matrix.os }}
66
+ runs-on: ${{ matrix.os }}
67
+ timeout-minutes: 15
68
+ strategy:
69
+ fail-fast: false
70
+ matrix:
71
+ python: ["3.11", "3.12", "3.13"]
72
+ os: [ubuntu-latest, macos-latest]
73
+ # Steps below mirror ./build.sh check, inlined for per-step Actions UI
74
+ # granularity AND because coverage requires extra pytest flags not in
75
+ # the local `test` task. CONTRIBUTING.md states the parity intentionally.
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+ with:
79
+ fetch-depth: 0
80
+ - uses: actions/setup-python@v5
81
+ with:
82
+ python-version: ${{ matrix.python }}
83
+ cache: pip
84
+ - name: install
85
+ run: pip install -e ".[dev]"
86
+ - name: ruff
87
+ run: |
88
+ ruff check .
89
+ ruff format --check .
90
+ - name: tests with coverage
91
+ run: pytest -q --ignore=tests/e2e --ignore=tests/literate --cov --cov-report=xml --cov-report=term
92
+ - name: docs CI gate
93
+ run: |
94
+ python -m agent_runner._docgen
95
+ git diff --exit-code docs/
96
+ - name: literate quickstart
97
+ run: pytest tests/literate/ -q
98
+ - name: upload coverage
99
+ if: matrix.python == '3.12' && matrix.os == 'ubuntu-latest'
100
+ uses: codecov/codecov-action@v4
101
+ with:
102
+ files: ./coverage.xml
103
+ fail_ci_if_error: false
104
+ env:
105
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -7,6 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] - 2026-05-13
11
+
12
+ ### Migration for existing 0.1.6 users (DOWNSTREAM CONSUMERS READ THIS)
13
+
14
+ If you maintain an `agent-runner.toml` by hand (rather than via `agent-runner init`),
15
+ you must add an `[agent.env]` block to preserve the Claude self-update suppression
16
+ that 0.1.6 injected implicitly. **Without this, mid-loop self-updates can race with
17
+ the supervisor.**
18
+
19
+ Add to your `agent-runner.toml`:
20
+
21
+ ```toml
22
+ [agent.env]
23
+ DISABLE_AUTOUPDATER = "1"
24
+ CLAUDE_CODE_EFFORT_LEVEL = "xhigh"
25
+ ```
26
+
27
+ Or regenerate cleanly:
28
+
29
+ ```bash
30
+ agent-runner init --preset claude --force
31
+ ```
32
+
33
+ Plugin authors (Argus Gateway, etc.): no public API was renamed or removed
34
+ from your import surface. The deleted symbols (`agent_runner.agent_runtime.CRITICAL_ENV_DEFAULTS`,
35
+ `agent_runner.agent_runtime.merge_critical_envs`) were internal — not part of
36
+ the documented plugin API. A new public-API contract test
37
+ (`tests/contract/test_public_api_surface.py`) locks in `api_types`, `events`,
38
+ `hooks`, `monitor`, `detector_helpers` import surfaces so future refactors
39
+ can't silently drop names you rely on.
40
+
41
+ ### Added
42
+ - `agent-runner init --preset {claude,aider}` selects between bundled CLI presets
43
+ (default: `claude`). New preset directory: `agent_runner/presets/`.
44
+ - `[agent.env]` TOML block — per-CLI env injections, replacing the hardcoded
45
+ `CRITICAL_ENV_DEFAULTS` constant. Empty dict by default.
46
+ - `docs/recipes/aider.md` — aider integration recipe.
47
+ - `tests/contract/test_public_api_surface.py` — public-API surface snapshot for
48
+ plugin authors.
49
+
50
+ ### Changed
51
+ - Core code (`agent_runtime.py`, `config.py`, `runner.py`, `scaffold.py`,
52
+ `defenses.py`) is now truly provider-agnostic: zero hardcoded Claude defaults.
53
+ Claude remains the reference example throughout the docs, but its specifics
54
+ live in `agent_runner/presets/claude.toml` (shipped as package data).
55
+ - `MonitorConfig.auth_fail_hint` default is now empty string; per-CLI hint
56
+ comes from the preset.
57
+ - `PEEK_SCHEMA_VERSION` bumped 1.3 → 1.4. `InitResult` gains `preset: str` field.
58
+ - `agent_runner/defenses.py` `critical_envs_injection` row now reads from
59
+ `cfg.agent.env.keys()` (not the deleted constant); state is "active" iff
60
+ the config defines any env injections, else "off".
61
+
62
+ ### Removed
63
+ - `agent_runner.agent_runtime.CRITICAL_ENV_DEFAULTS` constant.
64
+ - `agent_runner.agent_runtime.merge_critical_envs()` function.
65
+ - `agent_runner.config._DEFAULT_AUTH_HINT` constant's Claude-specific default
66
+ string (replaced with `""`; per-CLI hint now in preset files).
67
+
10
68
  ## [0.1.6] - 2026-05-12
11
69
 
12
70
  Zero-feature maintenance release — internal cleanup pass after the 0.1.x plugin
@@ -9,6 +9,7 @@ git clone https://github.com/wan9yu/cli-agent-runner.git
9
9
  cd cli-agent-runner
10
10
  python3 -m venv .venv && source .venv/bin/activate
11
11
  pip install -e ".[dev]"
12
+ git config core.hooksPath .githooks # enables the commit-msg lint hook
12
13
  ./build.sh check
13
14
  ```
14
15
 
@@ -16,6 +17,13 @@ pip install -e ".[dev]"
16
17
  + integration tests, the literate quickstart, and the docs CI gate. It's
17
18
  what GitHub Actions runs on every push and PR.
18
19
 
20
+ `git config core.hooksPath .githooks` activates the in-repo
21
+ [`.githooks/commit-msg`](.githooks/commit-msg) hook which rejects commit
22
+ messages containing `Co-Authored-By:` trailers, robot emojis, or other
23
+ AI-tool attribution patterns. The same check runs in CI (`lint-commits`
24
+ job) and as a pytest invariant (`tests/invariants/test_no_ai_signatures.py`)
25
+ — defense in depth.
26
+
19
27
  ## Workflow
20
28
 
21
29
  1. Open an issue first for non-trivial changes — saves wasted work on both sides.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-agent-runner
3
- Version: 0.1.6
3
+ Version: 0.1.7
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
@@ -41,10 +41,11 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  # agent-runner
43
43
 
44
- A restart-on-exit supervisor for autonomous CLI agents. Spawn an agent (Claude
45
- Code, a custom CLI, anything) round-after-round under defenses that prevent
46
- the failure modes that bite in production: stuck rounds, orphan commits,
47
- OAuth burn loops, full disks, runaway memory.
44
+ A restart-on-exit supervisor for autonomous coding CLIs. Tested with Claude
45
+ Code and aider out of the box; any prompt-arg CLI via custom config. Spawn
46
+ the agent round-after-round under defenses that prevent the failure modes
47
+ that bite in production: stuck rounds, orphan commits, OAuth burn loops,
48
+ full disks, runaway memory.
48
49
 
49
50
  ```
50
51
  ┌──────────────────────────────────────────┐
@@ -4,10 +4,11 @@
4
4
 
5
5
  # agent-runner
6
6
 
7
- A restart-on-exit supervisor for autonomous CLI agents. Spawn an agent (Claude
8
- Code, a custom CLI, anything) round-after-round under defenses that prevent
9
- the failure modes that bite in production: stuck rounds, orphan commits,
10
- OAuth burn loops, full disks, runaway memory.
7
+ A restart-on-exit supervisor for autonomous coding CLIs. Tested with Claude
8
+ Code and aider out of the box; any prompt-arg CLI via custom config. Spawn
9
+ the agent round-after-round under defenses that prevent the failure modes
10
+ that bite in production: stuck rounds, orphan commits, OAuth burn loops,
11
+ full disks, runaway memory.
11
12
 
12
13
  ```
13
14
  ┌──────────────────────────────────────────┐
@@ -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.6'
22
- __version_tuple__ = version_tuple = (0, 1, 6)
21
+ __version__ = version = '0.1.7'
22
+ __version_tuple__ = version_tuple = (0, 1, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1,10 +1,11 @@
1
- """Agent subprocess management — ONLY module that spawns the claude CLI.
1
+ """Agent subprocess management — spawns the configured agent CLI process.
2
2
 
3
3
  Defenses encoded here:
4
4
  - R725: SIGTERM handler reaps process group before runner exits
5
5
  - R1128: ROUND_TIMEOUT is wall-clock hard wall (no activity-based extension)
6
6
  - #307: start_new_session=True isolates subprocess in its own pgrp
7
- - env injection: DISABLE_AUTOUPDATER=1 + CLAUDE_CODE_EFFORT_LEVEL caller-provided
7
+ - env injection: per-CLI envs come from AgentConfig.env (preset-supplied);
8
+ no implicit injection in this module.
8
9
  """
9
10
 
10
11
  from __future__ import annotations
@@ -97,26 +98,14 @@ def run(
97
98
  log_file.close()
98
99
 
99
100
 
100
- CRITICAL_ENV_DEFAULTS: dict[str, str] = {
101
- "DISABLE_AUTOUPDATER": "1", # do not let claude self-update mid-loop
102
- "CLAUDE_CODE_EFFORT_LEVEL": "xhigh", # full effort, not default
103
- }
104
-
105
-
106
- def merge_critical_envs(user_env: dict[str, str]) -> dict[str, str]:
107
- """Merge user env with CRITICAL_ENV_DEFAULTS — critical always wins."""
108
- merged = dict(user_env)
109
- merged.update(CRITICAL_ENV_DEFAULTS)
110
- return merged
111
-
112
-
113
101
  def install_sigterm_reaper(reaper: Callable[[], None]) -> object:
114
102
  """Install a SIGTERM handler that calls ``reaper()`` first.
115
103
 
116
- R725 defense: when supervisor receives SIGTERM (e.g. systemctl stop, manual
117
- kill), bash wrapper would otherwise respawn fresh runner while old claude
118
- keeps running → two claudes race on the same git tree, second commit can
119
- swallow first commit's chat-room entry. Reaper terminates pgroup first.
104
+ R725 defense: when the supervisor receives SIGTERM (e.g. systemctl stop,
105
+ manual kill), the bash wrapper would otherwise respawn a fresh runner
106
+ while the old agent keeps running → two agent processes race on the same
107
+ git tree, the second commit can swallow the first commit's chat-room
108
+ entry. Reaper terminates pgroup first.
120
109
 
121
110
  Returns the previous SIGTERM handler so caller can restore it.
122
111
  """
@@ -71,10 +71,16 @@ def _systemctl_user(*args: str) -> None:
71
71
  # init / install / uninstall
72
72
 
73
73
 
74
- def init(work_dir: Path | None = None, *, force: bool = False, commit: bool = True) -> InitResult:
74
+ def init(
75
+ work_dir: Path | None = None,
76
+ *,
77
+ preset: str = "claude",
78
+ force: bool = False,
79
+ commit: bool = True,
80
+ ) -> InitResult:
75
81
  if work_dir is None:
76
82
  work_dir = Path.cwd()
77
- return scaffold_project(work_dir, force=force, commit=commit)
83
+ return scaffold_project(work_dir, preset=preset, force=force, commit=commit)
78
84
 
79
85
 
80
86
  def install(
@@ -130,6 +130,7 @@ class InitResult:
130
130
  work_dir: Path
131
131
  files_created: list[Path]
132
132
  committed: bool
133
+ preset: str = "claude" # 0.1.7+; default for backward compat with synthesised InitResults
133
134
 
134
135
 
135
136
  @dataclass(frozen=True)
@@ -15,7 +15,7 @@ from agent_runner.events import plugin_event_kinds
15
15
  from agent_runner.hooks import plugin_context_enrichers
16
16
  from agent_runner.monitor import plugin_detectors
17
17
 
18
- PEEK_SCHEMA_VERSION = "1.3"
18
+ PEEK_SCHEMA_VERSION = "1.4"
19
19
 
20
20
 
21
21
  def cfg_from_args(args) -> Config:
@@ -8,6 +8,12 @@ from agent_runner.cli.common import emit, fail, work_dir_from_args
8
8
 
9
9
  def add_parser(sub, parent) -> None:
10
10
  p = sub.add_parser("init", parents=[parent], help="Scaffold agent-runner project files")
11
+ p.add_argument(
12
+ "--preset",
13
+ choices=["claude", "aider"],
14
+ default="claude",
15
+ help="Which agent CLI preset to scaffold (default: claude)",
16
+ )
11
17
  p.add_argument("--force", action="store_true", help="Overwrite existing toml")
12
18
  g = p.add_mutually_exclusive_group()
13
19
  g.add_argument(
@@ -24,8 +30,8 @@ def add_parser(sub, parent) -> None:
24
30
  def cmd(args) -> int:
25
31
  work_dir = work_dir_from_args(args)
26
32
  try:
27
- result = api.init(work_dir, force=args.force, commit=args.commit)
28
- except (FileExistsError, RuntimeError) as e:
33
+ result = api.init(work_dir, preset=args.preset, force=args.force, commit=args.commit)
34
+ except (FileExistsError, RuntimeError, FileNotFoundError) as e:
29
35
  return fail(str(e))
30
36
  emit(result, json_mode=getattr(args, "json", False))
31
37
  return 0
@@ -15,6 +15,7 @@ class AgentConfig:
15
15
  command: list[str]
16
16
  prompt_arg_template: list[str]
17
17
  name: str | None = None
18
+ env: dict[str, str] = field(default_factory=dict)
18
19
 
19
20
 
20
21
  @dataclass(frozen=True)
@@ -38,14 +39,16 @@ class VcsConfig:
38
39
  stash_idempotency_s: int = 5
39
40
 
40
41
 
41
- # Default auth-failure detection regex — claude-aware. Migrated from monitor.py
42
- # to config.py as the SSOT for the oauth_fail detector. Plugins / non-claude
43
- # providers override via [monitor].auth_fail_patterns.
42
+ # Default auth-failure detection regex — matches common OAuth/401/expired-session
43
+ # vocabularies. Presets override [monitor].auth_fail_hint per CLI.
44
44
  _DEFAULT_AUTH_PATTERNS: list[str] = [
45
45
  r"\b(oauth|unauthorized|401|api[_ ]key|"
46
46
  r"auth(entication)?[_ -]?(failed|error|expired)|session.*expired)\b",
47
47
  ]
48
- _DEFAULT_AUTH_HINT: str = "Run `claude /login` on the supervisor host or refresh ANTHROPIC_API_KEY"
48
+ # Default auth-failure hint is empty per-CLI hints come from preset files
49
+ # (agent_runner/presets/*.toml) which write `[monitor].auth_fail_hint` into the
50
+ # user's agent-runner.toml at scaffold time.
51
+ _DEFAULT_AUTH_HINT: str = ""
49
52
 
50
53
  # Default allow-list of detector names whose ``stop_service`` action is honored.
51
54
  # Plugin detectors must be added explicitly by the operator to opt them in.
@@ -94,6 +97,7 @@ def load_config(toml_path: Path) -> Config:
94
97
  command=list(_require(agent_d, "command")),
95
98
  prompt_arg_template=list(_require(agent_d, "prompt_arg_template")),
96
99
  name=agent_d.get("name"),
100
+ env={str(k): str(v) for k, v in agent_d.get("env", {}).items()},
97
101
  )
98
102
  raw_work_dir = str(_require(raw, "runtime", "work_dir"))
99
103
  work_dir = _expand_path(raw_work_dir, "").resolve()
@@ -13,7 +13,6 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import Any
15
15
 
16
- from agent_runner.agent_runtime import CRITICAL_ENV_DEFAULTS
17
16
  from agent_runner.config import Config
18
17
 
19
18
 
@@ -73,12 +72,13 @@ def catalog(cfg: Config) -> list[Defense]:
73
72
  ),
74
73
  Defense(
75
74
  name="critical_envs_injection",
76
- value=list(CRITICAL_ENV_DEFAULTS.keys()),
75
+ value=sorted(cfg.agent.env.keys()),
77
76
  codifies=(
78
- "DISABLE_AUTOUPDATER + CLAUDE_CODE_EFFORT_LEVEL stop claude self-updates mid-loop"
77
+ "Env injection via [agent.env] block — preset-supplied per CLI "
78
+ "(e.g. DISABLE_AUTOUPDATER for claude prevents mid-loop self-updates)"
79
79
  ),
80
80
  guarded_by=None,
81
- current_state="active",
81
+ current_state="active" if cfg.agent.env else "off",
82
82
  ),
83
83
  Defense(
84
84
  name="startup_smoke_check",
@@ -28,7 +28,7 @@ from agent_runner.api_types import (
28
28
  ServiceStatus,
29
29
  SystemMetrics,
30
30
  )
31
- from agent_runner.config import _DEFAULT_AUTH_HINT, _DEFAULT_AUTH_PATTERNS
31
+ from agent_runner.config import _DEFAULT_AUTH_PATTERNS
32
32
  from agent_runner.context_store import read_json
33
33
  from agent_runner.events import emit as emit_event
34
34
  from agent_runner.events import now_iso_ms, parse_iso_ms
@@ -278,7 +278,7 @@ def detect_oauth_fail(
278
278
  "matches": matches,
279
279
  "window": total,
280
280
  "threshold": threshold,
281
- "hint": hint if hint is not None else _DEFAULT_AUTH_HINT,
281
+ "hint": hint if hint is not None else "",
282
282
  },
283
283
  auto_action="stop_service",
284
284
  )
@@ -0,0 +1,30 @@
1
+ # agent-runner.toml — generated by `agent-runner init --preset aider`.
2
+ #
3
+ # Prereqs:
4
+ # - aider installed (`pipx install aider-chat`)
5
+ # - your model provider env var set (OPENAI_API_KEY / ANTHROPIC_API_KEY /
6
+ # DEEPSEEK_API_KEY / OPENROUTER_API_KEY / etc.) — `aider --models` lists
7
+ # detected providers.
8
+
9
+ [agent]
10
+ command = ["aider", "--yes-always", "--no-stream", "--analytics-disable"]
11
+ prompt_arg_template = ["--message", "{prompt}"]
12
+ name = "aider"
13
+ # [agent.env] omitted — aider needs no env injection.
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
+ orphan_action = "stash"
27
+ stash_idempotency_s = 5
28
+
29
+ [monitor]
30
+ auth_fail_hint = "Verify your model provider env var (OPENAI_API_KEY / ANTHROPIC_API_KEY / DEEPSEEK_API_KEY / etc.); run `aider --models` to list detected providers."
@@ -0,0 +1,29 @@
1
+ # agent-runner.toml — generated by `agent-runner init --preset claude`.
2
+
3
+ [agent]
4
+ command = ["claude", "--model", "claude-opus-4-7",
5
+ "--dangerously-skip-permissions",
6
+ "--verbose", "--output-format", "stream-json"]
7
+ prompt_arg_template = ["-p", "{prompt}"]
8
+ name = "claude"
9
+
10
+ [agent.env]
11
+ DISABLE_AUTOUPDATER = "1"
12
+ CLAUDE_CODE_EFFORT_LEVEL = "xhigh"
13
+
14
+ [runtime]
15
+ work_dir = "."
16
+ log_dir = "~/.agent-runner/{project}/logs"
17
+ round_timeout_s = 1800
18
+ restart_delay_s = 3
19
+
20
+ [prompt]
21
+ file = "./prompts/main.md"
22
+ inject_context = true
23
+
24
+ [vcs]
25
+ orphan_action = "stash"
26
+ stash_idempotency_s = 5
27
+
28
+ [monitor]
29
+ auth_fail_hint = "Run `claude /login` on the supervisor host or refresh ANTHROPIC_API_KEY."
@@ -230,7 +230,7 @@ def _run_one_round_inner(cfg: Config) -> RoundResult:
230
230
  prompt=prompt,
231
231
  timeout_s=cfg.runtime.round_timeout_s,
232
232
  log_path=log_path,
233
- env_extra=agent_runtime.merge_critical_envs({}),
233
+ env_extra=dict(cfg.agent.env),
234
234
  )
235
235
  events.emit(
236
236
  log_dir,
@@ -1,55 +1,31 @@
1
1
  """Project scaffold for `agent-runner init`.
2
2
 
3
3
  Writes three files into a git repo:
4
- agent-runner.toml — copy of example template, project name substituted
4
+ agent-runner.toml — copy of selected preset, project name substituted
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`.
10
+
8
11
  Optionally commits in one step (default true via the CLI).
9
12
  """
10
13
 
11
14
  from __future__ import annotations
12
15
 
16
+ import importlib.resources
13
17
  import subprocess # noqa: TID251 — scaffold needs git for the commit step
14
18
  from pathlib import Path
15
19
 
16
20
  from agent_runner.api_types import InitResult
17
21
  from agent_runner.vcs_state import is_git_repo
18
22
 
19
- _TOML_TEMPLATE = """\
20
- # agent-runner.toml generated by `agent-runner init`. Edit fields as needed.
21
-
22
- # 'claude' below is the reference agent. Replace with any prompt-arg CLI:
23
- # command = ["gemini", "chat"], prompt_arg_template = ["--prompt", "{prompt}"]
24
- [agent]
25
- command = ["claude", "--model", "claude-opus-4-7",
26
- "--dangerously-skip-permissions",
27
- "--verbose", "--output-format", "stream-json"]
28
- prompt_arg_template = ["-p", "{prompt}"]
29
- # name = "claude" # optional — provider identifier; defaults to command[0]
30
-
31
- [runtime]
32
- work_dir = "."
33
- log_dir = "~/.agent-runner/{project}/logs"
34
- round_timeout_s = 1800
35
- restart_delay_s = 3
36
-
37
- [prompt]
38
- file = "./prompts/main.md"
39
- inject_context = true
40
-
41
- # [phases] # optional — uncomment for phase rotation
42
- # list = ["diverge", "converge", "refine"]
43
-
44
- [vcs]
45
- orphan_action = "stash"
46
- stash_idempotency_s = 5
47
-
48
- # [monitor] # optional — auto-stop policy overrides
49
- # auto_stop_on = ["oauth_fail", "disk_critical"]
50
- # disk_warning_pct = 90.0
51
- # disk_critical_pct = 95.0
52
- """
23
+
24
+ def _load_preset(name: str) -> str:
25
+ """Read preset TOML text; raises FileNotFoundError if name unknown."""
26
+ presets = importlib.resources.files("agent_runner.presets")
27
+ return (presets / f"{name}.toml").read_text(encoding="utf-8")
28
+
53
29
 
54
30
  _PROMPT_TEMPLATE = """\
55
31
  # Agent Prompt
@@ -68,7 +44,13 @@ auto-stash if you forget, but explicit commits with meaningful messages are bett
68
44
  _GITIGNORE_LINE = "logs/"
69
45
 
70
46
 
71
- def scaffold_project(work_dir: Path, *, force: bool, commit: bool) -> InitResult:
47
+ def scaffold_project(
48
+ work_dir: Path,
49
+ *,
50
+ preset: str = "claude",
51
+ force: bool,
52
+ commit: bool,
53
+ ) -> InitResult:
72
54
  if not is_git_repo(work_dir):
73
55
  raise RuntimeError(f"{work_dir} is not a git working tree — run `git init` first")
74
56
 
@@ -83,7 +65,8 @@ def scaffold_project(work_dir: Path, *, force: bool, commit: bool) -> InitResult
83
65
  files_created: list[Path] = []
84
66
 
85
67
  project = work_dir.resolve().name or "default"
86
- toml_path.write_text(_TOML_TEMPLATE.replace("{project}", project))
68
+ toml_text = _load_preset(preset).replace("{project}", project)
69
+ toml_path.write_text(toml_text)
87
70
  files_created.append(toml_path)
88
71
 
89
72
  prompt_dir.mkdir(parents=True, exist_ok=True)
@@ -119,4 +102,9 @@ def scaffold_project(work_dir: Path, *, force: bool, commit: bool) -> InitResult
119
102
  )
120
103
  committed = r.returncode == 0 # may fail if nothing changed
121
104
 
122
- return InitResult(work_dir=work_dir, files_created=files_created, committed=committed)
105
+ return InitResult(
106
+ work_dir=work_dir,
107
+ files_created=files_created,
108
+ committed=committed,
109
+ preset=preset,
110
+ )