aicp-cli 0.7.0__tar.gz → 0.9.0__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 (59) hide show
  1. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/CHANGELOG.md +28 -0
  2. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/PKG-INFO +115 -54
  3. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/README.md +114 -53
  4. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/pyproject.toml +1 -1
  5. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/cli.py +15 -8
  6. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/config.py +12 -11
  7. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/menu.py +12 -6
  8. aicp_cli-0.9.0/src/aicp/notify.py +49 -0
  9. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/present.py +4 -3
  10. aicp_cli-0.9.0/src/aicp/telegram_notify.py +57 -0
  11. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/conftest.py +7 -0
  12. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_cli.py +6 -11
  13. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_config.py +7 -7
  14. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_menu.py +17 -0
  15. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_menu_skills.py +4 -2
  16. aicp_cli-0.9.0/tests/test_notify.py +115 -0
  17. aicp_cli-0.9.0/tests/test_telegram_notify.py +61 -0
  18. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/uv.lock +2 -2
  19. aicp_cli-0.7.0/src/aicp/notify.py +0 -87
  20. aicp_cli-0.7.0/tests/test_notify.py +0 -165
  21. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/.github/workflows/ci.yml +0 -0
  22. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/.github/workflows/release.yml +0 -0
  23. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/.gitignore +0 -0
  24. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/CLAUDE.md +0 -0
  25. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/LICENSE +0 -0
  26. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/TODO.md +0 -0
  27. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/config.example.json +0 -0
  28. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/docs/images/aicp-run.png +0 -0
  29. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/skills/commit.md +0 -0
  30. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/skills/safe-git-push/SKILL.md +0 -0
  31. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/skills/safe-git-push/scripts/safe_push.py +0 -0
  32. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/__init__.py +0 -0
  33. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/_keyreader.py +0 -0
  34. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/_utils.py +0 -0
  35. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/agentcfg.py +0 -0
  36. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/agents.json +0 -0
  37. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/agents.py +0 -0
  38. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/budget.py +0 -0
  39. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/contracts.py +0 -0
  40. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/gitflow.py +0 -0
  41. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/i18n.py +0 -0
  42. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/quota.py +0 -0
  43. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/runner.py +0 -0
  44. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/secrets.py +0 -0
  45. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/skills.py +0 -0
  46. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/timing.py +0 -0
  47. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_agentcfg.py +0 -0
  48. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_agents.py +0 -0
  49. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_budget.py +0 -0
  50. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_cross_platform.py +0 -0
  51. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_gitflow.py +0 -0
  52. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_i18n.py +0 -0
  53. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_present.py +0 -0
  54. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_quota.py +0 -0
  55. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_runner.py +0 -0
  56. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_secrets.py +0 -0
  57. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_skills.py +0 -0
  58. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_timing.py +0 -0
  59. {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_undo.py +0 -0
@@ -1,3 +1,27 @@
1
+ ## [0.9.0] - 2026-09-19
2
+
3
+ ### 🚀 Features
4
+
5
+ - **notify:** Use direct Telegram delivery and row highlight
6
+
7
+ ### 🐛 Bug Fixes
8
+
9
+ - Switch notifications to bot api
10
+
11
+ ### 🧪 Testing
12
+
13
+ - Isolate Telegram credentials
14
+ ## [0.8.0] - 2026-09-18
15
+
16
+ ### 🐛 Bug Fixes
17
+
18
+ - **agents:** Keep agent listing widths stable across toggles
19
+ - **cli:** Parenthesize concatenated f-string for ruff ISC004
20
+
21
+ ### 📚 Documentation
22
+
23
+ - **readme:** Document configurable agent chain and skills field
24
+ - **changelog:** Release v0.8.0
1
25
  ## [0.7.0] - 2026-09-17
2
26
 
3
27
  ### 🚀 Features
@@ -7,6 +31,10 @@
7
31
  ### 🐛 Bug Fixes
8
32
 
9
33
  - **menu:** Size panels to their own content
34
+
35
+ ### 📚 Documentation
36
+
37
+ - **changelog:** Release v0.7.0
10
38
  ## [0.6.0] - 2026-09-17
11
39
 
12
40
  ### 📚 Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aicp-cli
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: AI commit + push, with a git-verified result summary
5
5
  Project-URL: Repository, https://github.com/weskao/aicp
6
6
  Project-URL: Issues, https://github.com/weskao/aicp/issues
@@ -14,11 +14,19 @@ Description-Content-Type: text/markdown
14
14
 
15
15
  AI commit + push, with a git-verified result summary.
16
16
 
17
- `aicp` runs an AI coding CLI to write your commit message(s) and push, trying
18
- a fallback chain of six CLIs — `copilot` → `agy` → `codex` → `claude` →
19
- `vibe` → `grok` — until one exits 0; anything not installed is skipped. It
17
+ `aicp` runs an AI coding CLI to write your commit message(s) and push, walking
18
+ a fallback chain until one exits 0; anything not installed is skipped. It
20
19
  sends that CLI two literal prompts, `/commit` then `/safe-git-push`.
21
20
 
21
+ **The chain is yours to arrange, and it isn't a fixed set.** The built-in
22
+ registry ships six CLIs — `copilot`, `agy`, `codex`, `claude`, `vibe`,
23
+ `grok` — but that's only the default *order*, not a hardcoded pipeline. Move
24
+ any one of them to the front with `aicp --swap-ai`, set the whole order from
25
+ `aicp --config` or `AICP_CLI_ORDER`, and add your own CLIs (or drop built-in
26
+ ones) with `aicp --agents`. Every agent in the resolved registry is a
27
+ first-class link in the chain — there is no privileged first CLI and no cap
28
+ on how many you run with.
29
+
22
30
  The part that matters: **aicp never trusts the CLI's own account of what
23
31
  happened.** An AI CLI can print "pushed!" and exit 0 while `/safe-git-push`
24
32
  quietly aborted inside it. So every number in the result summary — new
@@ -28,17 +36,36 @@ failed `git fetch` is never read as "already in sync" either: a stale
28
36
  remote-tracking ref resolves just fine and would otherwise report a clean
29
37
  push that never reached the remote.
30
38
 
31
- [Install](#install) · [Set up skills](#set-up-skills) · [Run it](#run-it) ·
32
- [`--undo`](#--undo) · [Automation / CI](#automation--ci) ·
33
- [Configuration](#configuration) · [Custom agents](#custom-agents) · [Safety](#safety) ·
34
- [Platform support](#platform-support)
39
+ ## Contents
40
+
41
+ **Getting started**
42
+
43
+ - [Requirements](#requirements)
44
+ - [Install](#install)
45
+ - [Set up skills](#set-up-skills)
46
+ - [Run it](#run-it)
47
+
48
+ **Using it**
49
+
50
+ - [Fallback order](#fallback-order) — reorder the chain, `--swap-ai`
51
+ - [Fallback results and quota limits](#fallback-results-and-quota-limits)
52
+ - [`--undo`](#--undo) — take back the last commit
53
+ - [Automation / CI](#automation--ci) — the non-interactive flags
54
+
55
+ **Reference**
56
+
57
+ - [Configuration](#configuration) — every setting and env var
58
+ - [Custom agents](#custom-agents) — add your own AI CLI
59
+ - [Safety](#safety) — the pre-commit secret scan
60
+ - [Platform support](#platform-support) — macOS, Linux, Windows
35
61
 
36
62
  ## Requirements
37
63
 
38
64
  - Python 3.10 or newer
39
65
  - [`uv`](https://docs.astral.sh/uv/) — the install path below
40
66
  - `git`
41
- - At least one of the six AI CLIs above on `PATH`
67
+ - At least one AI CLI from the chain on `PATH` (a built-in one, or your own
68
+ via [Custom agents](#custom-agents))
42
69
 
43
70
  The Python package itself has no runtime dependencies.
44
71
 
@@ -79,44 +106,26 @@ Targets follow each CLI's own config directory, not its binary name — `agy`
79
106
  | CLI | Config dir | `/commit` installed? | `/safe-git-push` installed? |
80
107
  | --- | --- | --- | --- |
81
108
  | `claude` | `~/.claude` | No — keeps your existing `commands/commit.md` | Yes |
82
- | `codex` | `~/.codex` | No — keeps your existing `/commit` | Yes |
83
- | `copilot` | `~/.copilot` | No — keeps your existing `/commit` | Yes |
84
- | `agy` (Gemini CLI) | `~/.gemini` | No — keeps your existing `/commit` | Yes |
85
- | `vibe` | `~/.vibe` | No — keeps your existing `/commit` | Yes |
86
- | `grok` | `$GROK_HOME` when set, otherwise `~/.grok` | No — keeps your existing `/commit` | Yes |
87
-
88
- `/commit` is never installed: aicp keeps each CLI's existing command or skill
89
- definition. Claude resolves that definition from `commands/commit.md`; the
90
- other CLIs keep their existing `/commit` as well. Every configured CLI gets
91
- `/safe-git-push`.
109
+ | `codex` | `~/.codex` | Yes | Yes |
110
+ | `copilot` | `~/.copilot` | Yes | Yes |
111
+ | `agy` (Gemini CLI) | `~/.gemini` | Yes | Yes |
112
+ | `vibe` | `~/.vibe` | Yes | Yes |
113
+ | `grok` | `$GROK_HOME` when set, otherwise `~/.grok` | Yes | Yes |
114
+
115
+ Every configured CLI gets `/safe-git-push`. `/commit` is installed everywhere
116
+ *except* `claude`, which already resolves `/commit` from
117
+ `commands/commit.md` — a second definition under `skills/` would compete with
118
+ it. Which skills an agent gets is data, not a branch: it's the `skills` field
119
+ of that agent's registry entry, so a [custom agent](#custom-agents) declares
120
+ its own list the same way.
121
+
122
+ The vendored text is rewritten per CLI on install — the shipped copy talks
123
+ about `.claude/` and `CLAUDE.md`, which mean nothing to `codex` or `agy`, so
124
+ each install substitutes that agent's own `config_dir` name and `memory_file`.
92
125
 
93
126
  A CLI whose config directory doesn't exist at all is skipped, never created —
94
127
  aicp only ever installs into a CLI you've actually set up.
95
128
 
96
- ### Fallback results and quota limits
97
-
98
- The opening run panel prints the resolved chain, and the commit panel and
99
- final RESULT table name the CLI that handled each step (`—` when skipped).
100
- When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
101
- outcome as `quota`, notifies through the usual notification path, and excludes
102
- that CLI from the rest of that one commit/push flow. The exclusion also
103
- persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
104
- hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
105
- normally stands for hours and every run inside that window would otherwise burn
106
- a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
107
- long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
108
- deleting the file clears it.
109
-
110
- Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
111
- hangs on its rate limit instead of exiting is indistinguishable from one merely
112
- running long, and sidelining it for an hour on that guess costs more than the
113
- retry does.
114
-
115
- Exact detection is intentionally narrow. It is supported for Codex, Claude,
116
- Vibe, and Grok only; Copilot and `agy` have no verified quota signature, so a
117
- nonzero exit from either remains an ordinary failure and is still eligible for
118
- the next step.
119
-
120
129
  ## Run it
121
130
 
122
131
  The whole command surface is two things to remember:
@@ -141,6 +150,52 @@ same as `aicp --config`.
141
150
  right now": which steps run, message language, fallback CLI order, the
142
151
  skills install/upgrade view, and a health check.
143
152
 
153
+ ### Fallback order
154
+
155
+ The order is a saved preference, not a property of the tool. Three ways to
156
+ set it, all writing the same `aicp_cli_order` key:
157
+
158
+ ```sh
159
+ aicp --swap-ai # pick a CLI, it trades places with whoever holds #1
160
+ aicp --config # the AI CLI order row, arrow keys
161
+ ```
162
+
163
+ ```sh
164
+ # ~/.aicp/config.json — or AICP_CLI_ORDER="claude codex" aicp for one run
165
+ { "aicp_cli_order": "claude codex copilot agy vibe grok" }
166
+ ```
167
+
168
+ `--swap-ai` is a rotation, not a destructive set: the CLI you pick moves to
169
+ `#1` and the old `#1` takes its place, so nothing falls out of the chain. It
170
+ lists every agent in the registry, including ones whose binary isn't on
171
+ `PATH` — a missing binary must not hide a choice you're about to install.
172
+ Naming only the first few is enough; everything you leave out is appended
173
+ behind them in registry order.
174
+
175
+ ### Fallback results and quota limits
176
+
177
+ The opening run panel prints the resolved chain, and the commit panel and
178
+ final RESULT table name the CLI that handled each step (`—` when skipped).
179
+ When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
180
+ outcome as `quota`, notifies through the usual notification path, and excludes
181
+ that CLI from the rest of that one commit/push flow. The exclusion also
182
+ persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
183
+ hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
184
+ normally stands for hours and every run inside that window would otherwise burn
185
+ a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
186
+ long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
187
+ deleting the file clears it.
188
+
189
+ Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
190
+ hangs on its rate limit instead of exiting is indistinguishable from one merely
191
+ running long, and sidelining it for an hour on that guess costs more than the
192
+ retry does.
193
+
194
+ Exact detection is intentionally narrow. It is supported for Codex, Claude,
195
+ Vibe, and Grok only; Copilot, `agy` and any agent you add yourself have no
196
+ verified quota signature, so a nonzero exit from those remains an ordinary
197
+ failure and is still eligible for the next step.
198
+
144
199
  ### `--undo`
145
200
 
146
201
  ```sh
@@ -223,7 +278,7 @@ integration tests and installed-wheel checks on all three operating systems.
223
278
  | `config_dir` | Absolute path or `~/`-relative directory; home is resolved when used. |
224
279
  | `config_dir_env` | Optional environment variable overriding that directory (currently `GROK_HOME`). |
225
280
  | `skills_dir` | Relative directory inside `config_dir` where skills are installed. |
226
- | `skills` | Vendored skills to install: `/safe-git-push`. Every CLI keeps its existing `/commit`. |
281
+ | `skills` | Which vendored skills to install for this agent — any of `safe-git-push`, `commit`. Omitting `commit` is how `claude` keeps its own `commands/commit.md`. |
227
282
  | `memory_file` | Instruction filename used when adapting vendored skill text. Project directory references use the basename of `config_dir`. |
228
283
  | `args` | Argument array containing exactly one standalone `{prompt}`, replaced with the complete prompt as one argument. No shell evaluation. |
229
284
 
@@ -351,7 +406,7 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
351
406
  | `AICP_DO_COMMIT` | `1` | Run the `/commit` step. `0` = only push what's already committed. |
352
407
  | `AICP_DO_PUSH` | `1` | Run the `/safe-git-push` step. `0` = commit and stop. |
353
408
  | `AICP_LANG` | `en` | Message language: `en` or `zh-TW`, everywhere including notifications. |
354
- | `AICP_CLI_ORDER` | `copilot agy codex claude vibe grok` | Fallback order. A prefix is enough — any roster name left out is appended after it, in roster order. An unknown or repeated name is refused outright and the default order is used. |
409
+ | `AICP_CLI_ORDER` | registry order (`copilot agy codex claude vibe grok` out of the box) | Fallback order, space-separated. A prefix is enough — any agent left out is appended after it, in registry order, so adding an agent never invalidates an order you already saved. An unknown or repeated name is refused outright and the default order is used. |
355
410
  | `AICP_TZ` | `Asia/Taipei` | IANA zone name used to render commit timestamps. Anything else falls back to the default. |
356
411
  | `AICP_TZ_LABEL` | `UTC+8` | Cosmetic label shown beside those timestamps; not validated. |
357
412
  | `AICP_STEP_TIMEOUT` | *(unset)* | Pins every CLI's per-step budget in seconds, skipping the formula and history below entirely. |
@@ -364,16 +419,22 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
364
419
  | `AICP_QUOTA_COOLDOWN` | `3600` | Seconds a CLI that reported a quota/rate-limit signal stays skipped, across runs (state in `~/.aicp/quota.json`). `0` switches the feature off; anything not a plain integer in 1..604800 falls back to the default. |
365
420
  | `AICP_SKIP_SECRET_SCAN` | *(unset)* | `1` bypasses the pre-commit secret scan for one run — the documented escape for a false positive. |
366
421
  | `AICP_CONFIG` | `~/.aicp/config.json` | Which file this loader reads. Environment-variable only — a file can't rename itself. |
367
- | `AICP_TG_SEND` | `~/.claude/scripts/tg-send.sh` | The Telegram send script run at the end of a notification. **Environment-variable only** — refused if set in `config.json`. |
368
422
  | `AICP_TIMING_LOG` | `~/.aicp/timing.log` | Where per-CLI timing rows are appended (rotated at 5 MB, 5 kept). **Environment-variable only** — refused if set in `config.json`. |
369
-
370
- `AICP_TG_SEND` and `AICP_TIMING_LOG` are refused from `config.json` on
371
- purpose: both name a path that then gets *executed* (`AICP_TG_SEND`, run as a
372
- script) or *written and rotated* (`AICP_TIMING_LOG`, `mkdir -p` / `>>` / a
373
- rename). A config file is exactly the kind of thing that can arrive synced
374
- from someone else's dotfiles repo, so anything that becomes a command or a
375
- filesystem sink stays a real-environment-only decision — set it in your
376
- shell, not the file.
423
+ | `TG_BOT_TOKEN` | *(unset)* | Telegram bot token used to send the end-of-run notification — sent directly to the Bot API via stdlib HTTP (`aicp.telegram_notify`), no external script or project required. Same name `~/.claude/scripts/tg-send.sh` already uses, so an existing setup carries over. Not part of the `AICP_*` config system at all (see below), so it can only ever be a real environment variable. |
424
+ | `TG_CHAT_ID` | *(unset)* | Telegram chat to notify. Same rules as `TG_BOT_TOKEN`. |
425
+
426
+ Either `TG_BOT_TOKEN` or `TG_CHAT_ID` missing (or the request failing) just
427
+ means the notification prints to the terminal instead — nothing about the
428
+ commit/push run itself depends on it.
429
+
430
+ `AICP_TIMING_LOG` is refused from `config.json` on purpose: it names a path
431
+ that then gets *written and rotated* (`mkdir -p` / `>>` / a rename). A config
432
+ file is exactly the kind of thing that can arrive synced from someone else's
433
+ dotfiles repo, so anything that becomes a filesystem sink stays a
434
+ real-environment-only decision — set it in your shell, not the file.
435
+ (`TG_BOT_TOKEN`/`TG_CHAT_ID` don't need this treatment: they aren't
436
+ `AICP_`-prefixed, so `config.json` was never able to supply them to begin
437
+ with.)
377
438
 
378
439
  ## Safety
379
440
 
@@ -2,11 +2,19 @@
2
2
 
3
3
  AI commit + push, with a git-verified result summary.
4
4
 
5
- `aicp` runs an AI coding CLI to write your commit message(s) and push, trying
6
- a fallback chain of six CLIs — `copilot` → `agy` → `codex` → `claude` →
7
- `vibe` → `grok` — until one exits 0; anything not installed is skipped. It
5
+ `aicp` runs an AI coding CLI to write your commit message(s) and push, walking
6
+ a fallback chain until one exits 0; anything not installed is skipped. It
8
7
  sends that CLI two literal prompts, `/commit` then `/safe-git-push`.
9
8
 
9
+ **The chain is yours to arrange, and it isn't a fixed set.** The built-in
10
+ registry ships six CLIs — `copilot`, `agy`, `codex`, `claude`, `vibe`,
11
+ `grok` — but that's only the default *order*, not a hardcoded pipeline. Move
12
+ any one of them to the front with `aicp --swap-ai`, set the whole order from
13
+ `aicp --config` or `AICP_CLI_ORDER`, and add your own CLIs (or drop built-in
14
+ ones) with `aicp --agents`. Every agent in the resolved registry is a
15
+ first-class link in the chain — there is no privileged first CLI and no cap
16
+ on how many you run with.
17
+
10
18
  The part that matters: **aicp never trusts the CLI's own account of what
11
19
  happened.** An AI CLI can print "pushed!" and exit 0 while `/safe-git-push`
12
20
  quietly aborted inside it. So every number in the result summary — new
@@ -16,17 +24,36 @@ failed `git fetch` is never read as "already in sync" either: a stale
16
24
  remote-tracking ref resolves just fine and would otherwise report a clean
17
25
  push that never reached the remote.
18
26
 
19
- [Install](#install) · [Set up skills](#set-up-skills) · [Run it](#run-it) ·
20
- [`--undo`](#--undo) · [Automation / CI](#automation--ci) ·
21
- [Configuration](#configuration) · [Custom agents](#custom-agents) · [Safety](#safety) ·
22
- [Platform support](#platform-support)
27
+ ## Contents
28
+
29
+ **Getting started**
30
+
31
+ - [Requirements](#requirements)
32
+ - [Install](#install)
33
+ - [Set up skills](#set-up-skills)
34
+ - [Run it](#run-it)
35
+
36
+ **Using it**
37
+
38
+ - [Fallback order](#fallback-order) — reorder the chain, `--swap-ai`
39
+ - [Fallback results and quota limits](#fallback-results-and-quota-limits)
40
+ - [`--undo`](#--undo) — take back the last commit
41
+ - [Automation / CI](#automation--ci) — the non-interactive flags
42
+
43
+ **Reference**
44
+
45
+ - [Configuration](#configuration) — every setting and env var
46
+ - [Custom agents](#custom-agents) — add your own AI CLI
47
+ - [Safety](#safety) — the pre-commit secret scan
48
+ - [Platform support](#platform-support) — macOS, Linux, Windows
23
49
 
24
50
  ## Requirements
25
51
 
26
52
  - Python 3.10 or newer
27
53
  - [`uv`](https://docs.astral.sh/uv/) — the install path below
28
54
  - `git`
29
- - At least one of the six AI CLIs above on `PATH`
55
+ - At least one AI CLI from the chain on `PATH` (a built-in one, or your own
56
+ via [Custom agents](#custom-agents))
30
57
 
31
58
  The Python package itself has no runtime dependencies.
32
59
 
@@ -67,44 +94,26 @@ Targets follow each CLI's own config directory, not its binary name — `agy`
67
94
  | CLI | Config dir | `/commit` installed? | `/safe-git-push` installed? |
68
95
  | --- | --- | --- | --- |
69
96
  | `claude` | `~/.claude` | No — keeps your existing `commands/commit.md` | Yes |
70
- | `codex` | `~/.codex` | No — keeps your existing `/commit` | Yes |
71
- | `copilot` | `~/.copilot` | No — keeps your existing `/commit` | Yes |
72
- | `agy` (Gemini CLI) | `~/.gemini` | No — keeps your existing `/commit` | Yes |
73
- | `vibe` | `~/.vibe` | No — keeps your existing `/commit` | Yes |
74
- | `grok` | `$GROK_HOME` when set, otherwise `~/.grok` | No — keeps your existing `/commit` | Yes |
75
-
76
- `/commit` is never installed: aicp keeps each CLI's existing command or skill
77
- definition. Claude resolves that definition from `commands/commit.md`; the
78
- other CLIs keep their existing `/commit` as well. Every configured CLI gets
79
- `/safe-git-push`.
97
+ | `codex` | `~/.codex` | Yes | Yes |
98
+ | `copilot` | `~/.copilot` | Yes | Yes |
99
+ | `agy` (Gemini CLI) | `~/.gemini` | Yes | Yes |
100
+ | `vibe` | `~/.vibe` | Yes | Yes |
101
+ | `grok` | `$GROK_HOME` when set, otherwise `~/.grok` | Yes | Yes |
102
+
103
+ Every configured CLI gets `/safe-git-push`. `/commit` is installed everywhere
104
+ *except* `claude`, which already resolves `/commit` from
105
+ `commands/commit.md` — a second definition under `skills/` would compete with
106
+ it. Which skills an agent gets is data, not a branch: it's the `skills` field
107
+ of that agent's registry entry, so a [custom agent](#custom-agents) declares
108
+ its own list the same way.
109
+
110
+ The vendored text is rewritten per CLI on install — the shipped copy talks
111
+ about `.claude/` and `CLAUDE.md`, which mean nothing to `codex` or `agy`, so
112
+ each install substitutes that agent's own `config_dir` name and `memory_file`.
80
113
 
81
114
  A CLI whose config directory doesn't exist at all is skipped, never created —
82
115
  aicp only ever installs into a CLI you've actually set up.
83
116
 
84
- ### Fallback results and quota limits
85
-
86
- The opening run panel prints the resolved chain, and the commit panel and
87
- final RESULT table name the CLI that handled each step (`—` when skipped).
88
- When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
89
- outcome as `quota`, notifies through the usual notification path, and excludes
90
- that CLI from the rest of that one commit/push flow. The exclusion also
91
- persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
92
- hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
93
- normally stands for hours and every run inside that window would otherwise burn
94
- a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
95
- long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
96
- deleting the file clears it.
97
-
98
- Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
99
- hangs on its rate limit instead of exiting is indistinguishable from one merely
100
- running long, and sidelining it for an hour on that guess costs more than the
101
- retry does.
102
-
103
- Exact detection is intentionally narrow. It is supported for Codex, Claude,
104
- Vibe, and Grok only; Copilot and `agy` have no verified quota signature, so a
105
- nonzero exit from either remains an ordinary failure and is still eligible for
106
- the next step.
107
-
108
117
  ## Run it
109
118
 
110
119
  The whole command surface is two things to remember:
@@ -129,6 +138,52 @@ same as `aicp --config`.
129
138
  right now": which steps run, message language, fallback CLI order, the
130
139
  skills install/upgrade view, and a health check.
131
140
 
141
+ ### Fallback order
142
+
143
+ The order is a saved preference, not a property of the tool. Three ways to
144
+ set it, all writing the same `aicp_cli_order` key:
145
+
146
+ ```sh
147
+ aicp --swap-ai # pick a CLI, it trades places with whoever holds #1
148
+ aicp --config # the AI CLI order row, arrow keys
149
+ ```
150
+
151
+ ```sh
152
+ # ~/.aicp/config.json — or AICP_CLI_ORDER="claude codex" aicp for one run
153
+ { "aicp_cli_order": "claude codex copilot agy vibe grok" }
154
+ ```
155
+
156
+ `--swap-ai` is a rotation, not a destructive set: the CLI you pick moves to
157
+ `#1` and the old `#1` takes its place, so nothing falls out of the chain. It
158
+ lists every agent in the registry, including ones whose binary isn't on
159
+ `PATH` — a missing binary must not hide a choice you're about to install.
160
+ Naming only the first few is enough; everything you leave out is appended
161
+ behind them in registry order.
162
+
163
+ ### Fallback results and quota limits
164
+
165
+ The opening run panel prints the resolved chain, and the commit panel and
166
+ final RESULT table name the CLI that handled each step (`—` when skipped).
167
+ When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
168
+ outcome as `quota`, notifies through the usual notification path, and excludes
169
+ that CLI from the rest of that one commit/push flow. The exclusion also
170
+ persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
171
+ hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
172
+ normally stands for hours and every run inside that window would otherwise burn
173
+ a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
174
+ long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
175
+ deleting the file clears it.
176
+
177
+ Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
178
+ hangs on its rate limit instead of exiting is indistinguishable from one merely
179
+ running long, and sidelining it for an hour on that guess costs more than the
180
+ retry does.
181
+
182
+ Exact detection is intentionally narrow. It is supported for Codex, Claude,
183
+ Vibe, and Grok only; Copilot, `agy` and any agent you add yourself have no
184
+ verified quota signature, so a nonzero exit from those remains an ordinary
185
+ failure and is still eligible for the next step.
186
+
132
187
  ### `--undo`
133
188
 
134
189
  ```sh
@@ -211,7 +266,7 @@ integration tests and installed-wheel checks on all three operating systems.
211
266
  | `config_dir` | Absolute path or `~/`-relative directory; home is resolved when used. |
212
267
  | `config_dir_env` | Optional environment variable overriding that directory (currently `GROK_HOME`). |
213
268
  | `skills_dir` | Relative directory inside `config_dir` where skills are installed. |
214
- | `skills` | Vendored skills to install: `/safe-git-push`. Every CLI keeps its existing `/commit`. |
269
+ | `skills` | Which vendored skills to install for this agent — any of `safe-git-push`, `commit`. Omitting `commit` is how `claude` keeps its own `commands/commit.md`. |
215
270
  | `memory_file` | Instruction filename used when adapting vendored skill text. Project directory references use the basename of `config_dir`. |
216
271
  | `args` | Argument array containing exactly one standalone `{prompt}`, replaced with the complete prompt as one argument. No shell evaluation. |
217
272
 
@@ -339,7 +394,7 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
339
394
  | `AICP_DO_COMMIT` | `1` | Run the `/commit` step. `0` = only push what's already committed. |
340
395
  | `AICP_DO_PUSH` | `1` | Run the `/safe-git-push` step. `0` = commit and stop. |
341
396
  | `AICP_LANG` | `en` | Message language: `en` or `zh-TW`, everywhere including notifications. |
342
- | `AICP_CLI_ORDER` | `copilot agy codex claude vibe grok` | Fallback order. A prefix is enough — any roster name left out is appended after it, in roster order. An unknown or repeated name is refused outright and the default order is used. |
397
+ | `AICP_CLI_ORDER` | registry order (`copilot agy codex claude vibe grok` out of the box) | Fallback order, space-separated. A prefix is enough — any agent left out is appended after it, in registry order, so adding an agent never invalidates an order you already saved. An unknown or repeated name is refused outright and the default order is used. |
343
398
  | `AICP_TZ` | `Asia/Taipei` | IANA zone name used to render commit timestamps. Anything else falls back to the default. |
344
399
  | `AICP_TZ_LABEL` | `UTC+8` | Cosmetic label shown beside those timestamps; not validated. |
345
400
  | `AICP_STEP_TIMEOUT` | *(unset)* | Pins every CLI's per-step budget in seconds, skipping the formula and history below entirely. |
@@ -352,16 +407,22 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
352
407
  | `AICP_QUOTA_COOLDOWN` | `3600` | Seconds a CLI that reported a quota/rate-limit signal stays skipped, across runs (state in `~/.aicp/quota.json`). `0` switches the feature off; anything not a plain integer in 1..604800 falls back to the default. |
353
408
  | `AICP_SKIP_SECRET_SCAN` | *(unset)* | `1` bypasses the pre-commit secret scan for one run — the documented escape for a false positive. |
354
409
  | `AICP_CONFIG` | `~/.aicp/config.json` | Which file this loader reads. Environment-variable only — a file can't rename itself. |
355
- | `AICP_TG_SEND` | `~/.claude/scripts/tg-send.sh` | The Telegram send script run at the end of a notification. **Environment-variable only** — refused if set in `config.json`. |
356
410
  | `AICP_TIMING_LOG` | `~/.aicp/timing.log` | Where per-CLI timing rows are appended (rotated at 5 MB, 5 kept). **Environment-variable only** — refused if set in `config.json`. |
357
-
358
- `AICP_TG_SEND` and `AICP_TIMING_LOG` are refused from `config.json` on
359
- purpose: both name a path that then gets *executed* (`AICP_TG_SEND`, run as a
360
- script) or *written and rotated* (`AICP_TIMING_LOG`, `mkdir -p` / `>>` / a
361
- rename). A config file is exactly the kind of thing that can arrive synced
362
- from someone else's dotfiles repo, so anything that becomes a command or a
363
- filesystem sink stays a real-environment-only decision — set it in your
364
- shell, not the file.
411
+ | `TG_BOT_TOKEN` | *(unset)* | Telegram bot token used to send the end-of-run notification — sent directly to the Bot API via stdlib HTTP (`aicp.telegram_notify`), no external script or project required. Same name `~/.claude/scripts/tg-send.sh` already uses, so an existing setup carries over. Not part of the `AICP_*` config system at all (see below), so it can only ever be a real environment variable. |
412
+ | `TG_CHAT_ID` | *(unset)* | Telegram chat to notify. Same rules as `TG_BOT_TOKEN`. |
413
+
414
+ Either `TG_BOT_TOKEN` or `TG_CHAT_ID` missing (or the request failing) just
415
+ means the notification prints to the terminal instead — nothing about the
416
+ commit/push run itself depends on it.
417
+
418
+ `AICP_TIMING_LOG` is refused from `config.json` on purpose: it names a path
419
+ that then gets *written and rotated* (`mkdir -p` / `>>` / a rename). A config
420
+ file is exactly the kind of thing that can arrive synced from someone else's
421
+ dotfiles repo, so anything that becomes a filesystem sink stays a
422
+ real-environment-only decision — set it in your shell, not the file.
423
+ (`TG_BOT_TOKEN`/`TG_CHAT_ID` don't need this treatment: they aren't
424
+ `AICP_`-prefixed, so `config.json` was never able to supply them to begin
425
+ with.)
365
426
 
366
427
  ## Safety
367
428
 
@@ -2,7 +2,7 @@
2
2
  # PyPI distribution name only — the `aicp` name was taken in 2021 by an
3
3
  # unrelated project. The command, import path and repo stay `aicp`.
4
4
  name = "aicp-cli"
5
- version = "0.7.0"
5
+ version = "0.9.0"
6
6
  description = "AI commit + push, with a git-verified result summary"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.10"
@@ -91,12 +91,11 @@ def export_settings(settings: config.Settings) -> None:
91
91
 
92
92
  Safe by construction with respect to :data:`aicp.config.DENYLIST`:
93
93
  ``settings.values`` is what :func:`aicp.config.load_config` accepted, and
94
- that loader drops every denied key — ``AICP_TG_SEND`` (reaches ``bash
95
- "$value"``), ``AICP_TIMING_LOG`` (path-mutating sinks) and ``AICP_CONFIG``
96
- (would re-point the next ``persist_key`` write) — before they ever reach
97
- here. So a file can never smuggle an exec-path or path-mutation knob into
98
- the environment through this door. Whoever adds the next such knob adds it
99
- to ``DENYLIST``, not to a filter here.
94
+ that loader drops every denied key — ``AICP_TIMING_LOG`` (path-mutating
95
+ sinks) and ``AICP_CONFIG`` (would re-point the next ``persist_key``
96
+ write) — before they ever reach here. So a file can never smuggle a
97
+ path-mutation knob into the environment through this door. Whoever adds
98
+ the next such knob adds it to ``DENYLIST``, not to a filter here.
100
99
 
101
100
  Two things this deliberately does NOT close, both stated rather than left
102
101
  to be discovered:
@@ -305,9 +304,17 @@ def _doctor(*, as_json: bool) -> int:
305
304
 
306
305
 
307
306
  def _agents_list() -> int:
307
+ inventory = agents.inventory()
308
+ exec_w = max((present.width(row.executable) for row in inventory), default=0)
308
309
  rows = [
309
- (row.name, f"{row.executable} {DIM}· {t(*agentcfg.STATE_LABELS[row.state])}{RESET}")
310
- for row in agents.inventory()
310
+ (
311
+ row.name,
312
+ (
313
+ f"{row.executable}{' ' * (exec_w - present.width(row.executable))}"
314
+ f" {DIM}· {t(*agentcfg.STATE_LABELS[row.state])}{RESET}"
315
+ ),
316
+ )
317
+ for row in inventory
311
318
  ]
312
319
  _echo(present.render(rows, t("agents_title", "AGENTS")))
313
320
  _dim(t("agents_hint", " aicp --agents set <name> executable=… · disable <name> · reset <name>"))
@@ -29,22 +29,23 @@ later used AS a command or a path. Hence three more layers:
29
29
  a key is written ``aicp_do_commit``, matching the environment's
30
30
  ``AICP_DO_COMMIT`` only once case-folded on read — see
31
31
  :func:`_read_json_object`). A line naming ``PATH`` is inert text to this
32
- loader, not an assignment.
32
+ loader, not an assignment. (Telegram credentials, ``TG_BOT_TOKEN`` and
33
+ ``TG_CHAT_ID``, aren't ``AICP_``-prefixed at all, so they never reach this
34
+ loader in the first place — see :mod:`aicp.notify`.)
33
35
  2. **Value charset allowlist** — letters, digits and
34
36
  ``/ . _ : @ + -`` plus whitespace. ``=`` and ``,`` are excluded because in
35
37
  the zsh original a value like ``PATH=0`` reaching an arithmetic context
36
38
  would assign into the real ``$PATH``; ``$``/backtick/``;``/``|``/parens
37
39
  are excluded as defence in depth for whatever the next consumer of these
38
40
  values does with them.
39
- 3. **Denylist** (:data:`DENYLIST`) — ``AICP_TG_SEND`` (reaches
40
- ``bash "$value"``), ``AICP_TIMING_LOG`` (reaches ``mkdir -p``, ``>>``,
41
- ``mv -f``, ``rm -f``) and ``AICP_CONFIG`` (names the file this loader
42
- reads, and the file :func:`persist_key` then ``mkdir -p``s and atomically
43
- replaces) are ENVIRONMENT-VARIABLE ONLY. All three are plain literal paths
44
- that sail through the charset allowlist, and all three were real holes.
45
- Whoever adds the next knob that flows into an exec path or a path-mutating
46
- sink adds its name here — the charset allowlist does not protect against
47
- this class at all.
41
+ 3. **Denylist** (:data:`DENYLIST`) — ``AICP_TIMING_LOG`` (reaches
42
+ ``mkdir -p``, ``>>``, ``mv -f``, ``rm -f``) and ``AICP_CONFIG`` (names the
43
+ file this loader reads, and the file :func:`persist_key` then
44
+ ``mkdir -p``s and atomically replaces) are ENVIRONMENT-VARIABLE ONLY. Both
45
+ are plain literal paths that sail through the charset allowlist, and both
46
+ were real holes. Whoever adds the next knob that flows into an exec path
47
+ or a path-mutating sink adds its name here — the charset allowlist does
48
+ not protect against this class at all.
48
49
 
49
50
  ``AICP_CONFIG`` is the subtlest of the three, because a file naming
50
51
  *itself* looks inert: nothing in this module acts on the value. The zsh
@@ -107,7 +108,7 @@ __all__ = [
107
108
  #: synced from someone else's dotfiles repo. ``AICP_CONFIG`` belongs here for
108
109
  #: the reason spelled out in this module's docstring — a file must not be able
109
110
  #: to rename the file the next write lands on.
110
- DENYLIST = frozenset({"AICP_TG_SEND", "AICP_TIMING_LOG", "AICP_CONFIG"})
111
+ DENYLIST = frozenset({"AICP_TIMING_LOG", "AICP_CONFIG"})
111
112
 
112
113
  #: Knobs resolved from the system, never from configuration — see
113
114
  #: :func:`timeout_bin`. Kept as a set so the reason is greppable from both