ai-push-hooks 0.3.0 → 0.3.2
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.
- package/CHANGELOG.md +78 -2
- package/README.md +80 -993
- package/SECURITY.md +21 -16
- package/ai-push-hooks.toml +1 -1
- package/bin/ai-push-hooks.js +6 -6
- package/docs/configuration.md +263 -0
- package/package.json +4 -3
- package/pyproject.toml +12 -3
- package/src/ai_push_hooks/artifacts.py +19 -19
- package/src/ai_push_hooks/cli.py +24 -8
- package/src/ai_push_hooks/config.py +239 -54
- package/src/ai_push_hooks/engine.py +94 -43
- package/src/ai_push_hooks/executors/apply.py +124 -46
- package/src/ai_push_hooks/executors/ask.py +30 -536
- package/src/ai_push_hooks/executors/exec.py +73 -818
- package/src/ai_push_hooks/executors/runner_workflow.py +42 -20
- package/src/ai_push_hooks/executors/runners/claude.py +18 -6
- package/src/ai_push_hooks/executors/runners/codex.py +9 -3
- package/src/ai_push_hooks/executors/runners/command.py +25 -9
- package/src/ai_push_hooks/executors/runners/contracts.py +52 -25
- package/src/ai_push_hooks/executors/runners/opencode.py +109 -27
- package/src/ai_push_hooks/executors/runners/opencode_support.py +286 -0
- package/src/ai_push_hooks/executors/runners/process.py +97 -17
- package/src/ai_push_hooks/executors/runners/registry.py +31 -9
- package/src/ai_push_hooks/executors/step_commands.py +65 -20
- package/src/ai_push_hooks/git_utils.py +899 -0
- package/src/ai_push_hooks/hook.py +49 -13
- package/src/ai_push_hooks/install.py +40 -18
- package/src/ai_push_hooks/modules/beads.py +19 -8
- package/src/ai_push_hooks/modules/docs.py +146 -96
- package/src/ai_push_hooks/modules/pr.py +19 -8
- package/src/ai_push_hooks/paths.py +6 -2
- package/src/ai_push_hooks/plugin_loader.py +182 -105
- package/src/ai_push_hooks/plugins.py +3 -1
- package/src/ai_push_hooks/prompts_builtin.py +1 -1
- package/src/ai_push_hooks/types.py +48 -27
- package/vendor/README.md +15 -0
- package/vendor/requirements.txt +1 -0
- package/vendor/tomli-2.4.0-py3-none-any.whl +0 -0
- package/run.sh +0 -29
package/README.md
CHANGED
|
@@ -1,1058 +1,145 @@
|
|
|
1
1
|
# ai-push-hooks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/shanebishop1/ai-push-hooks/actions/workflows/ci.yml) [](https://www.npmjs.com/package/ai-push-hooks/v/beta)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Agentic linting for the rules your coding agent forgot.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`AGENTS.md` tells an agent how to work. **ai-push-hooks checks whether it followed through.** Think of it as the inverse of `AGENTS.md`: a second pass over outgoing changes before `git push`, catching guidelines the agent forgot or neglected.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Use **OpenCode, Codex, or Claude Code** to check rules that need judgment, not just a regex. Report violations, apply scoped fixes, and block pushes with explicit checks. It's a hedge against missed instructions, not a guarantee that AI catches everything.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- [Python 3.10–3.13](https://www.python.org/downloads/). Python is required even when installing the npm wrapper. The wrapper probes Python 3.14, 3.13, 3.12, 3.11, 3.10, then `python`; the 3.14 probe is not a beta support claim. Python 3.10 additionally needs the `tomli` package available to that interpreter.
|
|
13
|
-
- A runner CLI is optional for workflows that use only deterministic steps, but the `minimal-docs` starter uses OpenCode for `ask` and `apply`. The selected runner still needs its normal provider/model authentication: for OpenCode, `opencode auth list` is a useful check; Codex, Claude, and custom commands use their own user-managed setup.
|
|
14
|
-
- [GitHub CLI (`gh`)](https://cli.github.com/manual/installation) is optional and needed only for `gh_pr_create`.
|
|
15
|
-
- [Beads (`bd`)](https://github.com/steveyegge/beads) is optional and needed only for Beads alignment steps. The integration requires the native `bd` CLI; Beads-Rust (`br`) is not a supported substitute.
|
|
16
|
-
- [Lefthook](https://lefthook.dev/installation/) and [Mise](https://mise.jdx.dev/getting-started.html) are optional hook-manager/tool-version alternatives described below.
|
|
11
|
+
## Rules Worth Checking
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
| Your rule | What the check looks for |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| No empty marketing speak. | Vague claims and filler in changed pages. |
|
|
16
|
+
| Use our typed API client. | Components making direct backend requests. |
|
|
17
|
+
| Test behavior, including failures. | Changed behavior without meaningful test coverage. |
|
|
18
|
+
| Keep migrations backward-compatible. | Destructive changes that break a rolling deployment. |
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
python -m pip install ai-push-hooks==0.3.0
|
|
25
|
-
ai-push-hooks init --template minimal-docs
|
|
26
|
-
ai-push-hooks install
|
|
27
|
-
```
|
|
20
|
+
Write your own rules in prompts or supply a rules file as context. These are examples of checks you configure, not built-in guarantees.
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
with wheel and npm artifacts in disposable repositories, including a real
|
|
31
|
-
local push that succeeds and a second push that is rejected without changing
|
|
32
|
-
the bare remote. The first command above can instead be `uv tool install
|
|
33
|
-
ai-push-hooks` or `pipx install ai-push-hooks` when using an isolated
|
|
34
|
-
application environment.
|
|
22
|
+
## Why Now?
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
the `beta` dist-tag; PyPI has no separate beta channel, so Python installation
|
|
38
|
-
must select the exact `0.3.0` version. Published `0.1.19` artifacts retain
|
|
39
|
-
historical provenance and must not be assumed to contain this release's
|
|
40
|
-
`install` command.
|
|
24
|
+
With lower-cost models such as GPT 5.6 Luna, GLM 5.3 Flash, Muse Spark 1.3, and Gemini Flash, running several focused agentic checks on each push can be practical, rather than reserving AI review for special occasions. Keep context narrow and measure your workflow's cost and latency. GPT 5.6 Luna is the default.
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
availability, pricing, and authentication are provider-dependent. Model strings
|
|
44
|
-
are opaque runner-specific identifiers; ai-push-hooks does not maintain a model
|
|
45
|
-
allowlist. To use a free OpenCode Zen model, run `opencode models opencode`,
|
|
46
|
-
choose a model currently marked free, and set `[llm].model` to that full
|
|
47
|
-
identifier. Free-model availability changes over time, so do not treat the model
|
|
48
|
-
used in the recorded preview below as a permanent recommendation or default.
|
|
26
|
+
## Quick Start
|
|
49
27
|
|
|
50
|
-
|
|
28
|
+
Install the [ai-push-hooks skill](https://github.com/shanebishop1/ai-push-hooks/blob/main/skills/ai-push-hooks/SKILL.md), tell your agent what intelligent checks you want before pushes, and let it set up the modules for you.
|
|
51
29
|
|
|
52
30
|
```bash
|
|
53
31
|
npm install --save-dev ai-push-hooks@beta
|
|
54
|
-
npx --no-install ai-push-hooks init
|
|
32
|
+
npx --no-install ai-push-hooks init
|
|
55
33
|
npx --no-install ai-push-hooks install
|
|
56
|
-
# or: pnpm add -D ai-push-hooks && pnpm exec ai-push-hooks install
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Use `ai-push-hooks@0.3.0` instead of `@beta` when an exact npm version pin is
|
|
60
|
-
required.
|
|
61
|
-
|
|
62
|
-
The npm package does not contain a Python runtime. Ensure the Python
|
|
63
|
-
requirement above is on `PATH` for the hook process; on Python 3.10 install
|
|
64
|
-
`tomli` in that same environment. `npx --no-install` avoids an accidental
|
|
65
|
-
registry lookup or global-package fallback.
|
|
66
|
-
|
|
67
|
-
### Version boundary
|
|
68
|
-
|
|
69
|
-
**Published `0.3.0` beta.** This release uses `type = "ask"` (not `llm` or
|
|
70
|
-
`agent`), adds repository-local Python callbacks and direct `exec`/`assert`
|
|
71
|
-
commands, and supports named runner profiles. The previous `0.2.1` beta used
|
|
72
|
-
`type = "llm"`; there is no compatibility alias, so update configurations when
|
|
73
|
-
upgrading to this release.
|
|
74
|
-
|
|
75
|
-
**Deferred/proposed.** Automatic discovery, installed-module references,
|
|
76
|
-
package-relative hook loading, a plugin SDK, remote services, sandboxing, and a
|
|
77
|
-
universal external-agent observer remain out of scope. Architecture-doc
|
|
78
|
-
changes are deferred; this README documents usage and trust boundaries only.
|
|
79
|
-
|
|
80
|
-
`install` resolves Git's effective hook path without changing Git
|
|
81
|
-
configuration. It accepts `install [--force]`, creates missing repository-local
|
|
82
|
-
hook directories, writes atomically, and makes the delegate executable. An
|
|
83
|
-
existing regular hook is refused unless `--force` is explicit; symlinks,
|
|
84
|
-
reparse points, FIFOs, directories, external/shared `core.hooksPath` values,
|
|
85
|
-
and linked-worktree shared hooks are refused even with `--force`. `--force`
|
|
86
|
-
replaces a regular existing hook; it does not merge or back it up. Inspect a
|
|
87
|
-
hook before using it and keep a copy if it contains work you need.
|
|
88
|
-
|
|
89
|
-
The generated delegate preserves Git's hook arguments, standard input, and
|
|
90
|
-
exit status. When installed through the local npm wrapper, it records the
|
|
91
|
-
absolute Node and package-script paths, so Git does not need
|
|
92
|
-
`node_modules/.bin` on `PATH`; keep that local package installation in place.
|
|
93
|
-
Python console installs similarly record their executable when it can be
|
|
94
|
-
resolved. The fallback delegate fails clearly with status 127 if
|
|
95
|
-
`ai-push-hooks` is not on the hook process's `PATH`.
|
|
96
|
-
|
|
97
|
-
### Mise (pinned tool option)
|
|
98
|
-
|
|
99
|
-
Pin an approved published release in the consuming repository:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
mise use npm:ai-push-hooks@0.3.0
|
|
103
34
|
```
|
|
104
35
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```toml
|
|
108
|
-
[tools]
|
|
109
|
-
"npm:ai-push-hooks" = "0.3.0"
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
After checking in `mise.toml`, other contributors can install the pinned tool with `mise install`.
|
|
113
|
-
|
|
114
|
-
### Runner profiles and access modes
|
|
115
|
-
|
|
116
|
-
> **Published in the `0.3.0` beta:** selectable runner profiles are available
|
|
117
|
-
> in both the Python wheel and npm package. The default OpenCode profile retains
|
|
118
|
-
> its artifact-only compatibility boundary; project access remains explicit.
|
|
119
|
-
|
|
120
|
-
`ask` and `apply` steps select a strict named profile. `[llm].runner` is the
|
|
121
|
-
workflow default; a `runner` on an individual `ask` or `apply` step overrides it.
|
|
122
|
-
`runner` is rejected on `collect`, `exec`, and `assert`. Every referenced name
|
|
123
|
-
must exist under `[runners.<name>]`, except `opencode`, which has an implicit
|
|
124
|
-
compatibility profile. Unknown profile fields, missing names, invalid
|
|
125
|
-
placeholders, and type-inapplicable fields fail during config loading rather
|
|
126
|
-
than being ignored.
|
|
127
|
-
|
|
128
|
-
The four static profile types are `opencode`, `codex`, `claude`, and `command`.
|
|
129
|
-
`model` is an opaque identifier passed to the selected runner. A profile's
|
|
130
|
-
`project_access` is either `artifacts` or `project`; it defaults to `artifacts`
|
|
131
|
-
for OpenCode and `project` for the other three types. The existing flat
|
|
132
|
-
`[llm]` form remains the shipped default:
|
|
133
|
-
|
|
134
|
-
```toml
|
|
135
|
-
[llm]
|
|
136
|
-
runner = "opencode"
|
|
137
|
-
model = "openai/gpt-5.6-terra"
|
|
138
|
-
variant = ""
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
It is equivalent to an OpenCode profile with `project_access = "artifacts"`.
|
|
142
|
-
That means read-only analysis uses an empty scratch directory with only
|
|
143
|
-
validated hook artifacts attached, with OpenCode tools denied, and compatibility
|
|
144
|
-
apply uses a private staging projection limited to eligible files matching its
|
|
145
|
-
allowlist. It does **not** silently become project-aware. In explicit OpenCode
|
|
146
|
-
project mode, read/list/glob/grep can inspect the real checkout for analysis;
|
|
147
|
-
edits, shell, tasks, web, sharing, plugins, MCP, and project/global config remain
|
|
148
|
-
restricted.
|
|
149
|
-
Opt into OpenCode project reads explicitly:
|
|
150
|
-
|
|
151
|
-
```toml
|
|
152
|
-
[llm]
|
|
153
|
-
runner = "opencode-project"
|
|
154
|
-
|
|
155
|
-
[runners.opencode-project]
|
|
156
|
-
type = "opencode"
|
|
157
|
-
model = "openai/gpt-5.6-terra"
|
|
158
|
-
project_access = "project"
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
The other built-in examples are:
|
|
162
|
-
|
|
163
|
-
```toml
|
|
164
|
-
[llm]
|
|
165
|
-
runner = "codex-review"
|
|
166
|
-
|
|
167
|
-
[runners.codex-review]
|
|
168
|
-
type = "codex"
|
|
169
|
-
model = "gpt-5.6-codex"
|
|
170
|
-
project_access = "project"
|
|
171
|
-
|
|
172
|
-
[runners.claude-review]
|
|
173
|
-
type = "claude"
|
|
174
|
-
model = "sonnet"
|
|
175
|
-
project_access = "project"
|
|
176
|
-
|
|
177
|
-
[[modules.docs.steps]]
|
|
178
|
-
id = "analyze"
|
|
179
|
-
type = "ask"
|
|
180
|
-
runner = "claude-review" # per-step override
|
|
181
|
-
fallback_prompt_id = "docs-analysis-basic"
|
|
182
|
-
inputs = ["collect/push.diff"]
|
|
183
|
-
output = "issues.json"
|
|
184
|
-
schema = "docs_issue_array"
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
The generic command profile is the supported way to describe a Pi invocation;
|
|
188
|
-
Pi is not a fourth built-in adapter:
|
|
189
|
-
|
|
190
|
-
```toml
|
|
191
|
-
[runners.pi-apply]
|
|
192
|
-
type = "command"
|
|
193
|
-
model = "provider/exact-model-id"
|
|
194
|
-
project_access = "project"
|
|
195
|
-
prompt_transport = "stdin"
|
|
196
|
-
command = [
|
|
197
|
-
"pi", "--print", "--no-session", "--no-extensions", "--no-skills",
|
|
198
|
-
"--no-prompt-templates", "--no-themes", "--no-context-files",
|
|
199
|
-
"--tools", "read,grep,find,ls,edit,write", "--model", "{model}"
|
|
200
|
-
]
|
|
201
|
-
|
|
202
|
-
[[modules.docs.steps]]
|
|
203
|
-
id = "apply"
|
|
204
|
-
type = "apply"
|
|
205
|
-
runner = "pi-apply"
|
|
206
|
-
fallback_prompt_id = "docs-apply-basic"
|
|
207
|
-
allow_paths = ["README.md", "docs/**/*.md"]
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Command profiles are argv vectors, never shell command strings. The executable
|
|
211
|
-
is launched with no implicit shell, shell expansion, pipes, redirection,
|
|
212
|
-
globbing, or command substitution. `stdin` (the default) sends the complete
|
|
213
|
-
instruction/artifact packet and then EOF. `argv` requires exactly one whole
|
|
214
|
-
argument `{prompt}`. The other whole-argument placeholders are `{model}`,
|
|
215
|
-
`{cwd}`, and `{stage}`; substring forms such as `--prompt={prompt}` are
|
|
216
|
-
rejected. An argv prompt can be visible in process listings, so stdin is the
|
|
217
|
-
safer default. Custom commands inherit the hook environment and have no
|
|
218
|
-
ai-push-hooks lifecycle or permission enforcement; stdout is the final result,
|
|
219
|
-
and stderr is diagnostic only.
|
|
220
|
-
|
|
221
|
-
### Project visibility and apply boundary
|
|
222
|
-
|
|
223
|
-
For `project_access = "project"`, an analysis runner receives the real
|
|
224
|
-
repository root as its cwd. For `apply`, it receives a point-in-time temporary
|
|
225
|
-
projection, never the real checkout. Project apply copies eligible tracked or
|
|
226
|
-
unignored ordinary files, including the current dirty baseline, then allows
|
|
227
|
-
propagation only for paths matching that step's `allow_paths`. Ignored files are
|
|
228
|
-
excluded even when tracked-but-ignored (the `--no-index` ignore check is
|
|
229
|
-
intentional). Git metadata, casefolded/Unicode-normalized `AGENTS.md` paths,
|
|
230
|
-
symlinks/reparse points, and special files are excluded or rejected.
|
|
36
|
+
Install and authenticate your chosen AI CLI. The current `init` starter checks documentation; replace its configuration with the rules-checking example below to check `AGENTS.md` instead. Then push normally.
|
|
231
37
|
|
|
232
|
-
|
|
233
|
-
of staging content, with Git metadata snapshots bounded at 20,000 entries and
|
|
234
|
-
64 MiB. Runner input artifacts are capped at an aggregate 16 MiB, and each
|
|
235
|
-
captured child stdout/stderr stream is capped at 16 MiB. Changes outside the
|
|
236
|
-
allowlist, unsafe modes (setuid/setgid/sticky), destination type/content/mode
|
|
237
|
-
conflicts, or protected Git-state changes fail closed. Existing baseline checks
|
|
238
|
-
and atomic file replacement reduce lost updates; they are not an atomic
|
|
239
|
-
compare-and-swap against an arbitrary external writer. No rollback is attempted
|
|
240
|
-
over pre-existing user changes.
|
|
38
|
+
[Other installation options](docs/configuration.md#installation) | [Existing hook managers](docs/configuration.md#hook-managers)
|
|
241
39
|
|
|
242
|
-
##
|
|
40
|
+
## Deterministic Demo
|
|
243
41
|
|
|
244
|
-
|
|
245
|
-
step may use one repository-local Python callback, or `exec`/`assert` may use a
|
|
246
|
-
direct argv command. This is configuration-defined trusted code, not a plugin
|
|
247
|
-
framework or SDK.
|
|
42
|
+

|
|
248
43
|
|
|
249
|
-
|
|
44
|
+
This generated GIF summarizes actual hook results from a disposable repository with a local deterministic command runner; it is not a live screen recording or live AI. It makes no provider calls, remote pushes, or Beads changes. Regenerate it with `uv run --no-project --with pillow==11.3.0 python docs/demo/generate.py`.
|
|
250
45
|
|
|
251
|
-
|
|
252
|
-
`checks/hooks.py:collect_context`. The file is a contained, ordinary `.py`
|
|
253
|
-
file; symlink/reparse traversal, attribute chains, installed-module references,
|
|
254
|
-
and package-relative loading are not supported. The callback is imported only
|
|
255
|
-
after the enabled-module gate, `when_env` gate, and input resolution. A source
|
|
256
|
-
file is loaded once per run (including concurrent collectors), with no `sys.path`,
|
|
257
|
-
cwd, or environment mutation. It may import standard-library or already
|
|
258
|
-
installed dependencies from the interpreter running the hook; the host never
|
|
259
|
-
runs `pip`. There is no hot reload, isolation sandbox, or enforceable hard
|
|
260
|
-
timeout for in-process Python.
|
|
46
|
+
## Example: Check Your Rules
|
|
261
47
|
|
|
262
|
-
|
|
263
|
-
`module_id`, and `step_id` identify the call; `inputs` is an insertion-ordered,
|
|
264
|
-
read-only mapping from logical artifact references to validated `Path` values;
|
|
265
|
-
`options` and `prior_module_metadata` are recursive read-only snapshots; and
|
|
266
|
-
`push` contains bounded push facts (`branch_name`, `checked_out_branch`,
|
|
267
|
-
`base_branch`, `ranges`, `changed_files`, `diff_text`, and `push_updates`). The
|
|
268
|
-
existing thread-safe `logger` is also available. These values are immutable API
|
|
269
|
-
containers, not read-only filesystem handles.
|
|
48
|
+
This workflow supplies the outgoing diff and your root `AGENTS.md` explicitly, asks for violations, and blocks if any are reported. It does not rely on the runner automatically loading agent instructions.
|
|
270
49
|
|
|
271
|
-
|
|
50
|
+
In `checks/hooks.py`:
|
|
272
51
|
|
|
273
52
|
```python
|
|
274
|
-
# checks/hooks.py
|
|
275
53
|
import json
|
|
276
54
|
|
|
277
55
|
from ai_push_hooks.plugins import CollectorResult, PluginContext
|
|
278
56
|
|
|
279
57
|
|
|
280
|
-
def
|
|
281
|
-
return CollectorResult(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
58
|
+
def collect_rules(context: PluginContext) -> CollectorResult:
|
|
59
|
+
return CollectorResult(artifacts={
|
|
60
|
+
"push.diff": context.push.diff_text,
|
|
61
|
+
"rules.txt": (context.repo_root / "AGENTS.md").read_text(encoding="utf-8"),
|
|
62
|
+
})
|
|
285
63
|
|
|
286
64
|
|
|
287
|
-
def
|
|
288
|
-
|
|
289
|
-
return {"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def assert_policy(context: PluginContext) -> dict:
|
|
293
|
-
result = json.loads(context.inputs["check/result.json"].read_text(encoding="utf-8"))
|
|
294
|
-
ok = result["file_count"] <= context.options["max_files"]
|
|
295
|
-
return {"ok": ok, "message": "too many changed files" if not ok else ""}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
The corresponding step declarations are:
|
|
299
|
-
|
|
300
|
-
```toml
|
|
301
|
-
[[modules.quality.steps]]
|
|
302
|
-
id = "context"
|
|
303
|
-
type = "collect"
|
|
304
|
-
python = "checks/hooks.py:collect_context"
|
|
305
|
-
options = { include_generated = false, severity = "high" }
|
|
306
|
-
|
|
307
|
-
[[modules.quality.steps]]
|
|
308
|
-
id = "check"
|
|
309
|
-
type = "exec"
|
|
310
|
-
python = "checks/hooks.py:run_check"
|
|
311
|
-
inputs = ["context/files.json"]
|
|
312
|
-
options = { severity = "high" }
|
|
313
|
-
|
|
314
|
-
[[modules.quality.steps]]
|
|
315
|
-
id = "policy"
|
|
316
|
-
type = "assert"
|
|
317
|
-
python = "checks/hooks.py:assert_policy"
|
|
318
|
-
inputs = ["check/result.json"]
|
|
319
|
-
options = { max_files = 25 }
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
`collect` callbacks return `CollectorResult` (artifacts, metadata, and optional
|
|
323
|
-
module skip state). `exec` callbacks return a JSON-serializable `dict`, saved
|
|
324
|
-
as `result.json` without automatically merging into module metadata. `assert`
|
|
325
|
-
callbacks return a JSON-serializable `dict` with an actual boolean `ok` and,
|
|
326
|
-
when present, a string `message`; the report is saved before `ok = false` blocks.
|
|
327
|
-
Artifact names and serialization are validated, each plugin artifact is bounded
|
|
328
|
-
to 16 MiB, and the aggregate collect payload is bounded to 64 MiB. Callback
|
|
329
|
-
exceptions, import failures, `SystemExit`, coroutine functions, and awaitable
|
|
330
|
-
returns fail closed with a concise named error; `KeyboardInterrupt` is not
|
|
331
|
-
swallowed. Callback `print()` calls and direct host filesystem writes are
|
|
332
|
-
outside host sanitization and remain the author's responsibility.
|
|
333
|
-
|
|
334
|
-
`collect` callbacks retain the read-only/concurrent scheduling class and may
|
|
335
|
-
overlap up to `max_parallel`; callback authors must make them concurrency-safe.
|
|
336
|
-
Python `exec` and `assert` steps are serialized with other mutating work.
|
|
337
|
-
|
|
338
|
-
### Direct argv commands
|
|
339
|
-
|
|
340
|
-
For `exec` and `assert`, `command` is a non-empty argv array. It runs with
|
|
341
|
-
`shell = false`, the repository root as cwd, the inherited user environment,
|
|
342
|
-
and stdin closed with EOF by default. `stdin = "<logical-ref>"` instead streams
|
|
343
|
-
that exact declared input artifact. The default command timeout is **60 seconds**;
|
|
344
|
-
configured values must be positive. There is no command allowlist or `trusted`
|
|
345
|
-
flag, and an explicit `bash -c` is the user's choice to adopt shell semantics.
|
|
346
|
-
|
|
347
|
-
```toml
|
|
348
|
-
[[modules.quality.steps]]
|
|
349
|
-
id = "lint"
|
|
350
|
-
type = "exec"
|
|
351
|
-
command = ["{python}", "scripts/lint_changed.py", "{input:context/files.json}"]
|
|
352
|
-
inputs = ["context/files.json"]
|
|
353
|
-
stdin = "context/files.json"
|
|
354
|
-
timeout_seconds = 60
|
|
355
|
-
|
|
356
|
-
[[modules.quality.steps]]
|
|
357
|
-
id = "policy-command"
|
|
358
|
-
type = "assert"
|
|
359
|
-
command = ["bash", "-c", "test -s \"$1\"", "assert", "{input:lint/result.json}"]
|
|
360
|
-
inputs = ["lint/result.json"]
|
|
65
|
+
def assert_rules(context: PluginContext) -> dict:
|
|
66
|
+
issues = json.loads(context.inputs["review/issues.json"].read_text(encoding="utf-8"))
|
|
67
|
+
return {"ok": issues == [], "message": "Review findings in review/issues.json."}
|
|
361
68
|
```
|
|
362
69
|
|
|
363
|
-
|
|
364
|
-
token: `{repo}` is the canonical repository root, `{python}` is the interpreter
|
|
365
|
-
running the hook, and `{input:<logical-ref>}` is a validated declared input.
|
|
366
|
-
The token grammar is one `{...}` pair containing only letters, digits, `_`, `.`,
|
|
367
|
-
`/`, `:`, or `-`; unknown tokens in that grammar (such as `{repos}`) are
|
|
368
|
-
rejected, as are embedded recognized tokens such as `--path={repo}`. Other
|
|
369
|
-
braces are literal command text, so Bash brace expansion, `awk '{print $1}'`,
|
|
370
|
-
and `python -c 'print({"a": 1})'` pass unchanged. Substitution is not shell
|
|
371
|
-
parsing or host-side string interpolation.
|
|
372
|
-
|
|
373
|
-
Both streams are captured as private, unredacted step artifacts named
|
|
374
|
-
`stdout.txt` and `stderr.txt`, including empty streams, and are not printed to
|
|
375
|
-
the console by default. `result.json` records the return code, artifact
|
|
376
|
-
references, and truncation flags. Valid stream text is UTF-8 and preserves
|
|
377
|
-
Unicode exactly; invalid UTF-8, a missing executable, timeout, signal, or a
|
|
378
|
-
stream exceeding the **16 MiB per-stream** bound is a process error and fails
|
|
379
|
-
closed. Captured output is retained when a process started. Exec requires exit
|
|
380
|
-
zero (an empty stdout is still success). Assert records `ok = true` for exit
|
|
381
|
-
zero; a nonzero exit records `ok = false` plus a bounded redacted message,
|
|
382
|
-
saves all reports first, and then blocks. Commands run in the real checkout and
|
|
383
|
-
may modify it; this is intentionally different from `apply`'s protected
|
|
384
|
-
staging projection. Exec/assert commands are serialized.
|
|
385
|
-
|
|
386
|
-
Only the workflow-level `general.allow_push_on_error = true` (or its explicit
|
|
387
|
-
`AI_PUSH_HOOKS_ALLOW_PUSH_ON_ERROR=1` override) changes a failure into a
|
|
388
|
-
fail-open warning. There is no per-command fail-open switch or custom success
|
|
389
|
-
exit-code list.
|
|
390
|
-
|
|
391
|
-
### Ask and apply are separate
|
|
392
|
-
|
|
393
|
-
`ask` reads/reasons and returns a response. Omitting `schema` deliberately gives
|
|
394
|
-
plain text and does not implicitly block a push or perform an action:
|
|
395
|
-
|
|
396
|
-
```toml
|
|
397
|
-
[[modules.review.steps]]
|
|
398
|
-
id = "summary"
|
|
399
|
-
type = "ask"
|
|
400
|
-
runner = "reviewer"
|
|
401
|
-
prompt = "Summarize the outgoing change in plain text."
|
|
402
|
-
output = "summary.txt"
|
|
403
|
-
# No schema: this is a response, not a verdict.
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
`apply` inspects a staging projection and may edit it. A preceding `ask` is not
|
|
407
|
-
required, and JSON is not required. This standalone, no-JSON apply flow declares
|
|
408
|
-
the workflow, enables its module, selects a runner, and supplies an explicit
|
|
409
|
-
allowlist:
|
|
70
|
+
In `ai-push-hooks.toml`:
|
|
410
71
|
|
|
411
72
|
```toml
|
|
412
73
|
[llm]
|
|
413
|
-
runner = "
|
|
74
|
+
runner = "opencode"
|
|
414
75
|
|
|
415
|
-
[runners.
|
|
76
|
+
[runners.opencode]
|
|
416
77
|
type = "opencode"
|
|
417
|
-
model = "openai/gpt-5.6-
|
|
78
|
+
model = "openai/gpt-5.6-luna"
|
|
418
79
|
project_access = "project"
|
|
419
80
|
|
|
420
81
|
[workflow]
|
|
421
|
-
modules = ["
|
|
422
|
-
|
|
423
|
-
[modules.docs]
|
|
424
|
-
enabled = true
|
|
425
|
-
|
|
426
|
-
[[modules.docs.steps]]
|
|
427
|
-
id = "apply-docs"
|
|
428
|
-
type = "apply"
|
|
429
|
-
runner = "docs-apply"
|
|
430
|
-
prompt = "Inspect the readable project projection and fix only factual drift in the allowed files."
|
|
431
|
-
allow_paths = ["README.md", "docs/**/*.md"]
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
The apply projection and propagation checks remain distinct: readable project
|
|
435
|
-
files may exceed the propagation allowlist, but only allowlisted changes can
|
|
436
|
-
propagate and any other staging change fails. The existing filename-specific
|
|
437
|
-
legacy shortcut also remains: an apply input whose filename ends in
|
|
438
|
-
`issues.json` containing the empty JSON list skips apply. It is legacy behavior,
|
|
439
|
-
not a general condition language or a replacement for an explicit policy step.
|
|
440
|
-
|
|
441
|
-
### Safeguards versus user policy
|
|
442
|
-
|
|
443
|
-
| Surface | Host-enforced safeguard | User/trusted-author policy |
|
|
444
|
-
| --- | --- | --- |
|
|
445
|
-
| Python reference/loading | Contained no-follow regular file; lazy reached-only import; per-run cache | Callback imports, direct writes, prints, and termination behavior |
|
|
446
|
-
| Python context/results | Frozen snapshots; exact result types; JSON/artifact bounds; fail-closed malformed results | Semantic correctness and concurrency safety |
|
|
447
|
-
| Command | Direct argv, cwd/stdin contract, timeout, bounded capture, UTF-8 validation, private artifacts | Inherited environment, explicit `bash -c`, executable behavior, checkout writes |
|
|
448
|
-
| Assert | Strict Python boolean or command exit status; report saved before failure | Business verdict logic and whether a verdict should block |
|
|
449
|
-
| Ask | Selected runner/profile, access mode, schema handling, and existing runner controls | Model quality; add `assert` if findings should block |
|
|
450
|
-
| Apply | Staging inventory, allowlist propagation, Git/baseline/integrity checks | Runner/tool policy and prompt intent; no OS sandbox or atomic CAS claim |
|
|
451
|
-
|
|
452
|
-
### OpenCode isolation limits
|
|
453
|
-
|
|
454
|
-
OpenCode uses `--pure`, isolated home/config/cache/state directories, disabled
|
|
455
|
-
sharing, and an ai-push-hooks-owned agent configuration. External plugins,
|
|
456
|
-
project/global configuration, MCP servers, instructions, and custom providers
|
|
457
|
-
from inherited global configuration are not loaded. Built-in plugins remain
|
|
458
|
-
available, including built-in authentication such as Codex OAuth. The existing
|
|
459
|
-
OpenCode data directory and recognized provider environment variables (including
|
|
460
|
-
`OPENAI_API_KEY`) are retained/forwarded, and OpenCode chooses authentication
|
|
461
|
-
using its own normal precedence.
|
|
462
|
-
|
|
463
|
-
These are permission and temporary-workspace controls, not an operating-system
|
|
464
|
-
sandbox. Every runner is a local program with the invoking user's OS identity;
|
|
465
|
-
same-user code can access other host paths. There is no mandatory command
|
|
466
|
-
allowlist, shell parser, container, credential broker, or trust prompt. Use an
|
|
467
|
-
external sandbox, container, VM, or low-privilege account when that boundary is
|
|
468
|
-
required. The scheduler may overlap `collect`/`ask` work up to `max_parallel`,
|
|
469
|
-
so a trusted custom `ask` command must really be safe for concurrent access;
|
|
470
|
-
`apply` remains globally serialized but custom command behavior is not enforced.
|
|
471
|
-
|
|
472
|
-
### Invocation, lifecycle, and output
|
|
473
|
-
|
|
474
|
-
All adapters use direct argv execution, explicit cwd, separate stdout/stderr
|
|
475
|
-
capture, and the configured per-invocation timeout. The built-in mappings are:
|
|
476
|
-
|
|
477
|
-
| Profile | Analysis | Apply |
|
|
478
|
-
| --- | --- | --- |
|
|
479
|
-
| OpenCode | `opencode run --agent ... --pure --format json --model ...` with native attachments | same isolated agent in the selected staging projection |
|
|
480
|
-
| Codex | `codex exec --json --color never --sandbox read-only --ephemeral --cd <project> [--model <model>] -` | same with `--sandbox workspace-write` and `--skip-git-repo-check` |
|
|
481
|
-
| Claude | `claude -p --output-format json --no-session-persistence [--model <model>]` plus tested read-only flags | same with tested edit/write flags |
|
|
482
|
-
| Command/Pi | configured argv, prompt on stdin by default | configured argv in staging; host-side propagation checks still apply |
|
|
483
|
-
|
|
484
|
-
Codex and Claude require their advertised capability flags; a missing required
|
|
485
|
-
flag fails instead of silently weakening the policy. A successful built-in call
|
|
486
|
-
must have a successful terminal result and final text. The workflow still owns
|
|
487
|
-
JSON extraction, schema validation, bounded feedback, and retries.
|
|
488
|
-
|
|
489
|
-
After every call, the completion event identifies the module, step, purpose,
|
|
490
|
-
profile, adapter type, and success/failure. It reports a returned session ID and
|
|
491
|
-
session state when available, plus a local transcript path when one was really
|
|
492
|
-
created; unavailable lifecycle data is not invented. OpenCode retains its
|
|
493
|
-
existing export-to-private-storage and cleanup behavior: transcript capture is
|
|
494
|
-
on by default and `delete_session_after_run` is on by default. To inspect an
|
|
495
|
-
exported JSON transcript, use a truthful local view such as:
|
|
496
|
-
|
|
497
|
-
```bash
|
|
498
|
-
python -m json.tool "path/to/exported-transcript.json"
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
An OpenCode session retained by setting `delete_session_after_run = false`
|
|
502
|
-
does not cause ai-push-hooks to print a resume command. In particular, ordinary
|
|
503
|
-
`opencode -s ...` is not a valid way to resume a session created from an
|
|
504
|
-
arbitrary repository/temp-project context; the mock contract test confirms this
|
|
505
|
-
case. Codex is ephemeral and Claude disables session persistence, so neither
|
|
506
|
-
claims a resumable/provider transcript. A command profile has no lifecycle
|
|
507
|
-
inference at all; wrappers own any files or resume behavior they implement.
|
|
508
|
-
|
|
509
|
-
Console status lines have an `[ai-push-hooks]` prefix. LLM lines include
|
|
510
|
-
`module.step` and purpose, and completion lines include profile/type and a
|
|
511
|
-
success or failure label. `NO_COLOR` disables colors; otherwise non-empty
|
|
512
|
-
`FORCE_COLOR` (except `0`) enables them, while `TERM=dumb` disables them when
|
|
513
|
-
not forced. `logging.jsonl = true` writes plain JSONL records to the private
|
|
514
|
-
runtime log; console records remain one line. `print_llm_output` is a sensitive,
|
|
515
|
-
explicit opt-in: it prints normalized final text after redaction, not raw event
|
|
516
|
-
streams or child diagnostics.
|
|
517
|
-
|
|
518
|
-
### Beads maintenance boundary
|
|
519
|
-
|
|
520
|
-
The `beads_alignment` executor is for ordinary native `bd update` and `bd close`
|
|
521
|
-
operations only. It does not run migrations, synchronize embedded Dolt, or
|
|
522
|
-
publish Dolt refs. Hook-launched Beads commands also discard
|
|
523
|
-
`BD_ALLOW_REMOTE_MIGRATE`, `BD_IGNORE_SCHEMA_SKEW`, and `BD_SMART_GATE` from
|
|
524
|
-
their inherited environment so an operator maintenance override cannot leak
|
|
525
|
-
into a push hook.
|
|
526
|
-
|
|
527
|
-
Treat a Beads schema migration as separate operator maintenance. Pin and
|
|
528
|
-
verify the native `bd` version, stop Beads writers and hooks, take a cold full
|
|
529
|
-
backup of `.beads`, and rehearse against a disposable copy before opening the
|
|
530
|
-
live embedded-Dolt store. Verify the schema, semantic issue/dependency data,
|
|
531
|
-
memories, and a clean Dolt working set before and after the live cutover.
|
|
532
|
-
Remote publication, including `bd dolt push`, is a separate explicit action;
|
|
533
|
-
a successful local migration does not authorize it. Do not replace this flow
|
|
534
|
-
with Beads-Rust (`br`).
|
|
535
|
-
|
|
536
|
-
## Full-featured alternative: Lefthook
|
|
537
|
-
|
|
538
|
-
Use Lefthook when the repository needs several hook commands, shared hook
|
|
539
|
-
configuration, or repository-managed installation. Keep one final
|
|
540
|
-
`ai-push-hooks hook` call and forward Git's pre-push input:
|
|
541
|
-
|
|
542
|
-
Create `scripts/hooks/pre-push-runner.sh` in the consuming repository with:
|
|
543
|
-
|
|
544
|
-
```yaml
|
|
545
|
-
pre-push:
|
|
546
|
-
commands:
|
|
547
|
-
repository-pre-push:
|
|
548
|
-
run: bash scripts/hooks/pre-push-runner.sh {1} {2}
|
|
549
|
-
use_stdin: true
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
The runner captures stdin before deterministic checks consume it, then replays
|
|
553
|
-
it to the tool:
|
|
554
|
-
|
|
555
|
-
```bash
|
|
556
|
-
#!/usr/bin/env bash
|
|
557
|
-
set -euo pipefail
|
|
558
|
-
remote_name="${1:-}"
|
|
559
|
-
remote_url="${2:-}"
|
|
560
|
-
push_stdin="$(mktemp)"
|
|
561
|
-
trap 'rm -f "$push_stdin"' EXIT
|
|
562
|
-
cat >"$push_stdin"
|
|
563
|
-
git diff --check
|
|
564
|
-
mise exec -- ai-push-hooks hook "$remote_name" "$remote_url" <"$push_stdin"
|
|
565
|
-
```
|
|
82
|
+
modules = ["rules"]
|
|
566
83
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
chmod +x scripts/hooks/pre-push-runner.sh
|
|
572
|
-
lefthook install
|
|
573
|
-
test -x "$(git rev-parse --git-path hooks/pre-push)" && echo "pre-push hook installed"
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
`use_stdin: true` forwards Git's ref-update stream; `{1}` and `{2}` are the
|
|
577
|
-
remote name and URL. This is the full-featured alternative to the small
|
|
578
|
-
repo-local `install` delegate. Do not install both managers for the same hook
|
|
579
|
-
unless their chaining is deliberate. The repository's deterministic installed
|
|
580
|
-
hook coverage validates the delegate contract; Lefthook remains the tested
|
|
581
|
-
repository-owned integration pattern and should be exercised with the commands
|
|
582
|
-
above in each consuming repository.
|
|
583
|
-
|
|
584
|
-
Configure modules and steps in the [configuration reference](#configuration-reference), then push as usual. If `apply` edits an allowlisted file, the starter assertion blocks that push so you can inspect `git diff`, validate, commit the approved edit, and push again.
|
|
585
|
-
|
|
586
|
-
## Troubleshooting
|
|
587
|
-
|
|
588
|
-
- **`opencode is required but not installed`:** install OpenCode and ensure `opencode` (or `opencode-cli`) is on `PATH` for the Git hook process.
|
|
589
|
-
- **`Runner profile ... does not exist` or a missing-profile config error:** add the exact named profile under `[runners.<name>]`, or change the step/global `runner` to an existing name. Profile names are strict; environment overrides do not create profiles.
|
|
590
|
-
- **Selected runner capability check fails:** install the documented CLI version/flags. Claude's required `--permission-mode`, `--tools`, and `--allowedTools` contract is checked before invocation; it does not silently downgrade. Codex and OpenCode similarly fail when their required adapter contract cannot run.
|
|
591
|
-
- **Provider/model authentication fails:** for OpenCode, run `opencode auth list`, authenticate a built-in provider, and verify the selected profile's model. Built-in auth plugins remain available, while project/global custom-provider configuration is intentionally not loaded. Codex, Claude, and custom commands own their normal login/provider setup; ai-push-hooks never invokes login. See [OpenCode isolation limits](#opencode-isolation-limits).
|
|
592
|
-
- **`no final response`, timeout, signal, or nonzero runner error:** the selected profile, adapter type, and stage are reported with bounded redacted diagnostics. Check that the CLI is usable from the configured cwd and that its final output contract is enabled; do not expect child stderr or raw event streams to be printed.
|
|
593
|
-
- **Invalid JSON after retries:** `json_max_retries` defaults to `2`. The schema retry feedback is bounded by `invalid_json_feedback_max_chars` and, by default, each retry starts a fresh invocation (`json_retry_new_session = true`). If session reuse is requested but unsupported or no session was captured, the run explicitly falls back to a fresh invocation.
|
|
594
|
-
- **The hook does not run:** rerun `lefthook install`, check `git config --get core.hooksPath`, and verify the pre-push path with the command above.
|
|
595
|
-
- **The push is blocked after docs changed:** this is the expected edit-review-commit flow. Review `git diff`, validate and commit the changes, then push again.
|
|
596
|
-
- **Find logs or transcripts:** inspect `.git/ai-push-hooks/logs`, `.git/ai-push-hooks/summaries`, and (when enabled) `.git/ai-push-hooks/transcripts`.
|
|
597
|
-
- **Temporarily skip intentionally:** set `AI_PUSH_HOOKS_SKIP=1` for one invocation. Treat bypasses as an explicit project-policy decision.
|
|
598
|
-
|
|
599
|
-
### Failure, fail-open, and skip semantics
|
|
600
|
-
|
|
601
|
-
- Fail closed is the default: configuration, collection, model, apply, exec,
|
|
602
|
-
and assertion errors return nonzero and block the push. A rejected push does
|
|
603
|
-
not update the remote.
|
|
604
|
-
- Set `[general].allow_push_on_error = true`, or use
|
|
605
|
-
`AI_PUSH_HOOKS_ALLOW_PUSH_ON_ERROR=1`, only as an explicit fail-open policy.
|
|
606
|
-
The error is still logged and a warning is emitted, but the push is allowed.
|
|
607
|
-
- `AI_PUSH_HOOKS_SKIP=1` exits before repository/config access and allows that
|
|
608
|
-
invocation. `general.enabled = false` is a configured disable after config
|
|
609
|
-
loading/log initialization. Neither path validates or runs the workflow.
|
|
610
|
-
- `skip_on_sync_branch = true` can skip configured sync-branch context, and
|
|
611
|
-
step/module conditions can report a normal skip. A skip is not a successful
|
|
612
|
-
model or apply result.
|
|
613
|
-
|
|
614
|
-
Git supplies hook stdin as one line per ref update:
|
|
615
|
-
`<local-ref> <local-object-id> <remote-ref> <remote-object-id>`. New remote
|
|
616
|
-
refs use a zero object ID (40 zeroes for the tested SHA-1 repositories); deletion
|
|
617
|
-
uses `(delete)` and a zero local ID. The hook rejects malformed input, unknown
|
|
618
|
-
objects, and multiple non-deleted branch updates rather than silently choosing
|
|
619
|
-
a branch.
|
|
620
|
-
|
|
621
|
-
### Safe removal of the generated hook
|
|
622
|
-
|
|
623
|
-
`install` has no uninstall command and never edits Git configuration. To remove
|
|
624
|
-
only its delegate, first inspect the effective target and content:
|
|
625
|
-
|
|
626
|
-
```bash
|
|
627
|
-
hook="$(git rev-parse --git-path hooks)/pre-push"
|
|
628
|
-
if [ -f "$hook" ]; then sed -n '1,20p' "$hook"; fi
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
For an exact check before removal, use the installed Python package rather than
|
|
632
|
-
matching only a filename:
|
|
633
|
-
|
|
634
|
-
```bash
|
|
635
|
-
python - "$hook" <<'PY'
|
|
636
|
-
import pathlib
|
|
637
|
-
import sys
|
|
638
|
-
|
|
639
|
-
from ai_push_hooks.install import pre_push_hook_script
|
|
640
|
-
|
|
641
|
-
path = pathlib.Path(sys.argv[1])
|
|
642
|
-
if not path.is_file() or path.is_symlink():
|
|
643
|
-
raise SystemExit("not a regular hook; nothing removed")
|
|
644
|
-
if path.read_text(encoding="utf-8") != pre_push_hook_script():
|
|
645
|
-
raise SystemExit("hook is not the ai-push-hooks delegate; nothing removed")
|
|
646
|
-
path.unlink()
|
|
647
|
-
print(f"removed {path}")
|
|
648
|
-
PY
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
Remove it only after confirming it is the generated `ai-push-hooks` delegate,
|
|
652
|
-
not a Lefthook or shared hook. If it contains other commands, preserve it and
|
|
653
|
-
remove only the documented ai-push-hooks entry instead. Do not run `rm` on an
|
|
654
|
-
uninspected `pre-push` path.
|
|
655
|
-
|
|
656
|
-
## Security and privacy
|
|
657
|
-
|
|
658
|
-
Repository diffs, selected context, artifacts, and prompts may be sent by the
|
|
659
|
-
selected runner to its configured model provider. `project_access = "project"`
|
|
660
|
-
can expose substantially more repository content than the artifact-only default;
|
|
661
|
-
review provider data-handling, retention, and billing terms before using a
|
|
662
|
-
sensitive repository. Authentication belongs to the user and the selected CLI:
|
|
663
|
-
OpenCode retains its existing auth state and recognized provider environment
|
|
664
|
-
variables, while Codex, Claude, and custom commands inherit the normal user
|
|
665
|
-
environment/home they require. ai-push-hooks does not log environment values or
|
|
666
|
-
broker credentials.
|
|
667
|
-
|
|
668
|
-
OpenCode transcripts are exported to private local storage by default under
|
|
669
|
-
`.git/ai-push-hooks/transcripts`, and its sessions are deleted after each run by
|
|
670
|
-
default. Export is best effort: if it fails, the run warns and does not claim a
|
|
671
|
-
transcript exists. Codex and Claude use ephemeral/no-persistence modes and
|
|
672
|
-
command profiles have no inferred transcript. A local transcript never proves
|
|
673
|
-
provider-side deletion. See [SECURITY.md](SECURITY.md) for reporting, the threat
|
|
674
|
-
model, data handling, and sandbox limitations.
|
|
675
|
-
|
|
676
|
-
### BR-06 provider evidence (limited preview)
|
|
677
|
-
|
|
678
|
-
At the time of the recorded synthetic provider run, OpenCode **1.18.29** listed
|
|
679
|
-
`opencode/muse-spark-1.3-contributor-free` as free. Synthetic `query` and
|
|
680
|
-
`analyze` steps passed at **zero reported cost**. This is evidence for that
|
|
681
|
-
specific OpenCode/model path and synthetic inputs only; it does not establish
|
|
682
|
-
that every model, provider, authentication mode, or live `apply` operation is
|
|
683
|
-
compatible. Discover the current catalog with `opencode models opencode` and
|
|
684
|
-
verify pricing before each run; free models can be renamed, replaced, or
|
|
685
|
-
removed. The tested identifier is historical evidence, not a new default or a
|
|
686
|
-
promise of future availability. Review provider billing, retention, and
|
|
687
|
-
transmission terms before using repository content.
|
|
688
|
-
|
|
689
|
-
## Tested matrix and beta boundary
|
|
690
|
-
|
|
691
|
-
The recorded environment details are intentionally not used as compatibility
|
|
692
|
-
proof for every platform. Python 3.10–3.13 and Node 18+ remain declared
|
|
693
|
-
compatibility ranges, and the generated hook requires a POSIX shell.
|
|
694
|
-
|
|
695
|
-
The current direct smoke command uses a loopback mock provider. Do not read this
|
|
696
|
-
as live-provider evidence or as proof of network or operating-system isolation.
|
|
697
|
-
|
|
698
|
-
Final pinned-Lefthook verification passed with no skips:
|
|
699
|
-
|
|
700
|
-
```bash
|
|
701
|
-
mise exec lefthook@2.1.9 -- python -m pytest tests -q
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
```text
|
|
705
|
-
407 passed, 0 skipped
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
Ruff 0.13.3 also passed after the three direct-script `E402` fixes:
|
|
709
|
-
|
|
710
|
-
```bash
|
|
711
|
-
uv tool run --from ruff==0.13.3 ruff check --isolated .
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
Wheel, sdist, Twine 6.1.0, and npm packaging checks passed; the npm 10.9.2
|
|
715
|
-
offline smoke used zero LLM calls. The real OpenCode 1.18.29 smoke test passed
|
|
716
|
-
with an in-process loopback mock provider and no external model call; its read
|
|
717
|
-
probe also checks that no Git-visible project files were mutated. It is
|
|
718
|
-
permission/workspace evidence, not live-provider or operating-system-sandbox
|
|
719
|
-
evidence. Installed no-model conformance passed for OpenCode 1.18.29, Codex
|
|
720
|
-
0.148.0, and Claude 2.1.220 using only version/help commands. Python 3.10.18
|
|
721
|
-
and Python 3.12 each passed 407 tests with 0 skips; the Python 3.10.18 run used
|
|
722
|
-
pytest 8.3.5, build 1.2.2.post1, tomli 2.4.1, and Lefthook 2.1.9. Default tests
|
|
723
|
-
make no authenticated or billable model calls. The live probe rejects a
|
|
724
|
-
present `AI_PUSH_HOOKS_MODEL` before any setup or child call; unset it so the
|
|
725
|
-
explicit `--model "provider/model-id"` argument remains deliberate. See the
|
|
726
|
-
[verification report](docs/reports/runner-verification.md) for the full gated
|
|
727
|
-
Codex/Pi read and apply commands.
|
|
728
|
-
|
|
729
|
-
### Runner references
|
|
730
|
-
|
|
731
|
-
The invocation contracts were checked against the installed CLIs and their
|
|
732
|
-
authoritative documentation: [Codex non-interactive mode](https://developers.openai.com/codex/noninteractive),
|
|
733
|
-
[Codex CLI reference](https://developers.openai.com/codex/cli/reference),
|
|
734
|
-
[Codex authentication](https://developers.openai.com/codex/auth),
|
|
735
|
-
[Codex approvals and security](https://developers.openai.com/codex/agent-approvals-security),
|
|
736
|
-
[Claude CLI reference](https://code.claude.com/docs/en/cli-reference),
|
|
737
|
-
[Claude headless mode](https://code.claude.com/docs/en/headless),
|
|
738
|
-
[Claude permissions](https://code.claude.com/docs/en/permissions),
|
|
739
|
-
[Claude sessions](https://code.claude.com/docs/en/sessions),
|
|
740
|
-
[Claude authentication](https://code.claude.com/docs/en/authentication), and
|
|
741
|
-
[Pi usage/security/providers](https://pi.dev/docs/latest/usage),
|
|
742
|
-
[Pi JSON mode](https://pi.dev/docs/latest/json). CLI flags and model catalogs
|
|
743
|
-
can change; capability checks and additive parsers are intentional.
|
|
744
|
-
|
|
745
|
-
Python 3.10–3.13 and Node 18+ remain the declared compatibility ranges, not a
|
|
746
|
-
claim that every patch/platform combination has passed. The generated hook
|
|
747
|
-
requires a POSIX shell, and Windows has no native beta evidence. On POSIX,
|
|
748
|
-
timeout cleanup signals a private process group on a best-effort basis; on
|
|
749
|
-
Windows, timeout cleanup can terminate only the direct child. Neither behavior
|
|
750
|
-
is a sandbox.
|
|
751
|
-
|
|
752
|
-
## Synthetic demo and evidence
|
|
753
|
-
|
|
754
|
-
For a no-secrets, no-external-model-call wiring/permission demo, run:
|
|
755
|
-
|
|
756
|
-
```bash
|
|
757
|
-
bash scripts/opencode-contract-smoke.sh
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
It builds a disposable image, starts only an in-process loopback mock provider,
|
|
761
|
-
and drives real OpenCode 1.18.29 through a synthetic repository. The Docker
|
|
762
|
-
runtime uses `--network none`; the image build/setup may use network access to
|
|
763
|
-
fetch its pinned inputs. It shows the allowlisted `README.md` edit, denied
|
|
764
|
-
outside/protected edits, and unchanged protected Git metadata. This is
|
|
765
|
-
**wiring and permission evidence only**, not a live-provider demo or OS-sandbox
|
|
766
|
-
claim; it exits 2 when Docker is unavailable.
|
|
84
|
+
[[modules.rules.steps]]
|
|
85
|
+
id = "collect"
|
|
86
|
+
type = "collect"
|
|
87
|
+
python = "checks/hooks.py:collect_rules"
|
|
767
88
|
|
|
768
|
-
|
|
89
|
+
[[modules.rules.steps]]
|
|
90
|
+
id = "review"
|
|
91
|
+
type = "ask"
|
|
92
|
+
prompt = "Check the outgoing diff against rules.txt. Inspect repository context as needed. Report only concrete violations introduced by these changes, with file and description fields. Return a JSON array, or [] if none. Do not edit files."
|
|
93
|
+
inputs = ["collect/push.diff", "collect/rules.txt"]
|
|
94
|
+
output = "issues.json"
|
|
95
|
+
schema = "docs_issue_array"
|
|
769
96
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
97
|
+
[[modules.rules.steps]]
|
|
98
|
+
id = "gate"
|
|
99
|
+
type = "assert"
|
|
100
|
+
python = "checks/hooks.py:assert_rules"
|
|
101
|
+
inputs = ["review/issues.json"]
|
|
773
102
|
```
|
|
774
103
|
|
|
775
|
-
|
|
776
|
-
fail-closed rejection. The actual BR-06 provider preview is documented above;
|
|
777
|
-
it used synthetic query/analyze inputs and must not be presented as fabricated
|
|
778
|
-
provider output or as evidence for live `apply`.
|
|
779
|
-
|
|
780
|
-
## Portfolio case study: bounded documentation maintenance
|
|
781
|
-
|
|
782
|
-
**Problem.** A pushed code change can make repository documentation stale
|
|
783
|
-
before review notices it. The hook inspects the outgoing ref range rather than
|
|
784
|
-
the checked-out branch alone.
|
|
785
|
-
|
|
786
|
-
**Architecture and tradeoffs.** Deterministic `collect` steps establish diff,
|
|
787
|
-
changed-file, and repository context before `ask` query/analyze steps. An
|
|
788
|
-
`apply` step receives a private workspace and a narrow docs allowlist; the
|
|
789
|
-
assertion then blocks the push for human review and commit. `exec` and `assert`
|
|
790
|
-
remain available for deterministic repository actions. This ordering limits
|
|
791
|
-
model scope without pretending to provide an OS sandbox.
|
|
792
|
-
|
|
793
|
-
**Evidence.** The real OpenCode 1.18.29 loopback mock-provider contract found a
|
|
794
|
-
version-specific permission mapping (`write` requests `edit`) and covers
|
|
795
|
-
allowlisted propagation plus protected Git metadata without an external model
|
|
796
|
-
call. The final pinned-Lefthook Python suite recorded 407 passed with no skips.
|
|
797
|
-
Installed runner conformance is version/help-only; live Codex and Pi probes were
|
|
798
|
-
intentionally not run, and Claude live verification is pending.
|
|
104
|
+
`docs_issue_array` is the existing schema name for `{file, description}` findings; it works for code rules too. A missing rules file or a failed check blocks the push by default. Findings live in the run artifacts under `.git/ai-push-hooks/`.
|
|
799
105
|
|
|
800
|
-
**
|
|
801
|
-
ignored trees, shared metadata, and independent filesystem races remain outside
|
|
802
|
-
the product guarantee. Review, validate, and commit any proposed edit; this is
|
|
803
|
-
not unattended autonomous maintenance.
|
|
106
|
+
**Want fixes too?** Add an `apply` step with the findings, your rules, and an explicit `allow_paths` list, then recheck and run tests. Applied edits are not auto-committed: review the diff, commit approved changes, and retry the push.
|
|
804
107
|
|
|
805
|
-
##
|
|
108
|
+
## Build Your Workflow
|
|
806
109
|
|
|
807
|
-
|
|
110
|
+
Each module combines the steps it needs:
|
|
808
111
|
|
|
809
|
-
|
|
|
112
|
+
| Step | Purpose |
|
|
810
113
|
| --- | --- |
|
|
811
|
-
| `
|
|
812
|
-
| `
|
|
813
|
-
| `
|
|
814
|
-
| `
|
|
114
|
+
| `collect` | Gather the outgoing diff and relevant context. |
|
|
115
|
+
| `ask` | Ask AI for findings or another response. |
|
|
116
|
+
| `apply` | Edit a temporary workspace; propagate only allowlisted changes. |
|
|
117
|
+
| `exec` | Run scripts, tests, or other actions. |
|
|
118
|
+
| `assert` | Evaluate a verdict and block the push if it fails. |
|
|
815
119
|
|
|
816
|
-
|
|
120
|
+
`ask` alone does not block on findings; add an `assert` gate. Combine AI review with your existing linters and tests, not instead of them. Independent analysis can run concurrently; built-in `exec`, `assert`, and `apply` steps are serialized. Trusted custom command runners, including project-access `ask` commands, are not enforced read-only.
|
|
817
121
|
|
|
818
|
-
|
|
819
|
-
- Prompt resolution precedence for `ask` and `apply` steps:
|
|
820
|
-
1. `prompt`
|
|
821
|
-
2. `prompt_file`
|
|
822
|
-
3. `fallback_prompt_id`
|
|
122
|
+
## Choose Your AI
|
|
823
123
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
### Top-level keys
|
|
827
|
-
|
|
828
|
-
| Key | Type | Required | Default |
|
|
829
|
-
| --- | --- | --- | --- |
|
|
830
|
-
| `general` | table | no | see section defaults |
|
|
831
|
-
| `llm` | table | no | see section defaults |
|
|
832
|
-
| `logging` | table | no | see section defaults |
|
|
833
|
-
| `workflow` | table | yes | n/a |
|
|
834
|
-
| `modules` | table | yes | n/a |
|
|
835
|
-
| `runners` | table | no | Named runner profiles; omitted for the implicit OpenCode compatibility default. |
|
|
836
|
-
|
|
837
|
-
### `[general]`
|
|
838
|
-
|
|
839
|
-
| Key | Type | Default | Description |
|
|
840
|
-
| --- | --- | --- | --- |
|
|
841
|
-
| `enabled` | bool | `true` | Enables or disables the hook globally. |
|
|
842
|
-
| `allow_push_on_error` | bool | `false` | If `true`, push continues even when workflow fails. |
|
|
843
|
-
| `require_clean_worktree` | bool | `false` | If `true`, aborts when local changes exist. |
|
|
844
|
-
| `skip_on_sync_branch` | bool | `true` | If `true`, skips on sync branch/worktree context. |
|
|
845
|
-
| `base_branch` | string | `"main"` | Base branch used for new-branch range fallback and default PR base/context. |
|
|
846
|
-
|
|
847
|
-
### `[llm]`
|
|
848
|
-
|
|
849
|
-
| Key | Type | Default | Description |
|
|
850
|
-
| --- | --- | --- | --- |
|
|
851
|
-
| `runner` | string | `"opencode"` | Global named runner profile for `ask`/`apply`; the implicit OpenCode compatibility profile is the default. |
|
|
852
|
-
| `model` | string | `"openai/gpt-5.6-terra"` | Compatibility model for implicit OpenCode; explicit profiles use their own model unless overridden by `AI_PUSH_HOOKS_MODEL`. |
|
|
853
|
-
| `variant` | string | `""` | Optional OpenCode compatibility variant. |
|
|
854
|
-
| `timeout_seconds` | int | `800` | Timeout per selected runner invocation and related lifecycle calls. |
|
|
855
|
-
| `max_parallel` | int | `2` | Max concurrent read-only steps (`collect`, `ask`). |
|
|
856
|
-
| `json_max_retries` | int | `2` | Retry count for invalid JSON responses. |
|
|
857
|
-
| `invalid_json_feedback_max_chars` | int | `6000` | Max invalid output included in retry feedback. |
|
|
858
|
-
| `json_retry_new_session` | bool | `true` | Requests a fresh invocation on JSON retry; unsupported/sessionless runners always fall back fresh. |
|
|
859
|
-
| `delete_session_after_run` | bool | `true` | Deletes OpenCode sessions after completion. |
|
|
860
|
-
| `max_diff_bytes` | int | `180000` | Max bytes of git diff sent into workflow artifacts. |
|
|
861
|
-
| `session_title_prefix` | string | `"ai-push-hooks"` | Prefix for OpenCode session titles. |
|
|
862
|
-
|
|
863
|
-
### `[runners.<name>]`
|
|
864
|
-
|
|
865
|
-
| Key | Type | Required/default | Description |
|
|
866
|
-
| --- | --- | --- | --- |
|
|
867
|
-
| `type` | string | required | One of `opencode`, `codex`, `claude`, or `command`. |
|
|
868
|
-
| `model` | string | optional | Opaque runner-specific model identifier; no catalog validation is performed. |
|
|
869
|
-
| `project_access` | string | OpenCode: `artifacts`; others: `project` | `artifacts` or `project`; see [project visibility](#project-visibility-and-apply-boundary). |
|
|
870
|
-
| `variant` | string | optional, OpenCode only | OpenCode variant. |
|
|
871
|
-
| `command` | string array | required for `command` | Direct argv vector; no shell parsing. |
|
|
872
|
-
| `prompt_transport` | `stdin`/`argv` | `stdin` for `command` | `argv` requires one whole-argument `{prompt}` placeholder. |
|
|
873
|
-
|
|
874
|
-
### `[logging]`
|
|
875
|
-
|
|
876
|
-
| Key | Type | Default | Description |
|
|
877
|
-
| --- | --- | --- | --- |
|
|
878
|
-
| `level` | string | `"status"` | Console verbosity (`status`, `info`, `debug`). |
|
|
879
|
-
| `jsonl` | bool | `true` | Enables JSONL event logging. |
|
|
880
|
-
| `dir` | string | `".git/ai-push-hooks/logs"` | Directory for `hook.jsonl`. |
|
|
881
|
-
| `capture_llm_transcript` | bool | `true` | Exports OpenCode session transcripts. |
|
|
882
|
-
| `transcript_dir` | string | `".git/ai-push-hooks/transcripts"` | Transcript export directory. |
|
|
883
|
-
| `summary_dir` | string | `".git/ai-push-hooks/summaries"` | Per-run summary JSON directory. |
|
|
884
|
-
| `print_llm_output` | bool | `false` | Sensitive opt-in; prints normalized, redacted final text, not raw runner events. |
|
|
885
|
-
|
|
886
|
-
### `[workflow]`
|
|
887
|
-
|
|
888
|
-
| Key | Type | Required | Description |
|
|
889
|
-
| --- | --- | --- | --- |
|
|
890
|
-
| `modules` | array of strings | yes | Ordered module IDs to run. Must contain at least one module and each ID must exist under `[modules]`. |
|
|
891
|
-
|
|
892
|
-
### `[modules.<module_id>]`
|
|
893
|
-
|
|
894
|
-
| Key | Type | Required | Description |
|
|
895
|
-
| --- | --- | --- | --- |
|
|
896
|
-
| `enabled` | bool | no | Enables or disables that module. Default `true`. |
|
|
897
|
-
| `steps` | array of step tables | yes | Ordered workflow steps for the module. Must be non-empty. |
|
|
898
|
-
|
|
899
|
-
### `[[modules.<module_id>.steps]]`
|
|
900
|
-
|
|
901
|
-
| Key | Type | Required | Applies to | Description |
|
|
902
|
-
| --- | --- | --- | --- | --- |
|
|
903
|
-
| `id` | string | yes | all step types | Unique step identifier inside the module. |
|
|
904
|
-
| `type` | string | yes | all step types | One of: `collect`, `ask`, `apply`, `exec`, `assert`. Legacy `llm` and `agent` values are rejected; use `ask`. |
|
|
905
|
-
| `inputs` | array of strings | no | non-`collect` steps | Artifact references from earlier steps. |
|
|
906
|
-
| `output` | string | yes | `ask` | Output artifact filename (often `.json`). |
|
|
907
|
-
| `schema` | string | no | `ask` | Validates parsed model output shape. |
|
|
908
|
-
| `prompt` | string | conditional | `ask`, `apply` | Highest-priority prompt source. |
|
|
909
|
-
| `prompt_file` | string | conditional | `ask`, `apply` | Repo-relative prompt file path; absolute, traversing, and symlinked paths are rejected. |
|
|
910
|
-
| `fallback_prompt_id` | string | conditional | `ask`, `apply` | Built-in prompt ID used when no higher source resolves. |
|
|
911
|
-
| `collector` | string | yes | `collect` | Collector handler ID. |
|
|
912
|
-
| `allow_paths` | array of strings | yes | `apply` | File glob allowlist for edits. |
|
|
913
|
-
| `runner` | string | no | `ask`, `apply` | Per-step named profile override; invalid on other step types. |
|
|
914
|
-
| `executor` | string | yes | `exec` | Exec handler ID. |
|
|
915
|
-
| `assertion` | string | yes | `assert` | Assertion handler ID. |
|
|
916
|
-
| `when_env` | string | no | any step | Runs step only when env var parses as true. |
|
|
917
|
-
|
|
918
|
-
`ask` and `apply` are promptable step types: at least one of `prompt`, `prompt_file`, or `fallback_prompt_id` must be set.
|
|
919
|
-
|
|
920
|
-
Artifact references in `inputs` are module-local. Use `<step>/<artifact>` to reference an artifact produced by an earlier step in the same module (for example, `collect/push.diff` or `analyze/issues.json`). Cross-module references such as `docs:collect/push.diff` are not currently supported.
|
|
921
|
-
|
|
922
|
-
### Supported handler and schema values
|
|
923
|
-
|
|
924
|
-
#### Collectors
|
|
925
|
-
|
|
926
|
-
| Value | Purpose |
|
|
927
|
-
| --- | --- |
|
|
928
|
-
| `docs_context` | Collects docs-related context and diff artifacts. |
|
|
929
|
-
| `beads_status_context` | Collects branch/beads alignment context. |
|
|
930
|
-
| `pr_context` | Collects PR composition context. |
|
|
931
|
-
|
|
932
|
-
#### Ask schemas
|
|
933
|
-
|
|
934
|
-
| Value | Expected payload |
|
|
935
|
-
| --- | --- |
|
|
936
|
-
| `string_array` | JSON array of strings. |
|
|
937
|
-
| `docs_issue_array` | JSON array of issue objects with at least `file` and `description`. |
|
|
938
|
-
| `beads_alignment_result` | JSON object, optionally with `commands` string array. |
|
|
939
|
-
| `pr_create_payload` | JSON object for PR creation fields. |
|
|
940
|
-
|
|
941
|
-
#### Exec handlers
|
|
942
|
-
|
|
943
|
-
| Value | Purpose |
|
|
944
|
-
| --- | --- |
|
|
945
|
-
| `beads_alignment` | Runs non-interactive Beads commands and writes action report when needed. |
|
|
946
|
-
| `gh_pr_create` | Creates (or reuses) a GitHub PR via `gh`. |
|
|
947
|
-
|
|
948
|
-
#### Assertion handlers
|
|
949
|
-
|
|
950
|
-
| Value | Purpose |
|
|
951
|
-
| --- | --- |
|
|
952
|
-
| `docs_apply_requires_manual_commit` | Fails when docs were auto-edited and still need user review/commit. |
|
|
953
|
-
| `beads_alignment_clean` | Fails when Beads alignment reports unresolved work. |
|
|
954
|
-
|
|
955
|
-
#### Built-in fallback prompt IDs
|
|
956
|
-
|
|
957
|
-
| Value | Purpose |
|
|
958
|
-
| --- | --- |
|
|
959
|
-
| `docs-query-basic` | Generate doc search queries from diff. |
|
|
960
|
-
| `docs-analysis-basic` | Identify factual documentation drift. |
|
|
961
|
-
| `docs-apply-basic` | Apply minimal doc fixes within allowlist. |
|
|
962
|
-
| `beads-plan-basic` | Build Beads alignment command/report payload. |
|
|
963
|
-
| `pr-compose-basic` | Draft PR title/body/base/head payload. |
|
|
964
|
-
|
|
965
|
-
## Environment variable overrides
|
|
966
|
-
|
|
967
|
-
Boolean env parsing accepts: `1`, `true`, `yes`, `y`, `on` and `0`, `false`, `no`, `n`, `off`.
|
|
968
|
-
|
|
969
|
-
Runner selection is resolved in this order: the step's `runner`, then
|
|
970
|
-
`[llm].runner`, then the named profile (or implicit `opencode` compatibility
|
|
971
|
-
profile). For the selected profile, `AI_PUSH_HOOKS_MODEL` is the final model
|
|
972
|
-
override. Without it, an explicit profile model wins; only the implicit
|
|
973
|
-
OpenCode profile inherits flat `[llm].model`. `AI_PUSH_HOOKS_VARIANT` applies
|
|
974
|
-
only to OpenCode and is the final override for its variant. These environment
|
|
975
|
-
overrides do not turn a missing profile into a valid one or change
|
|
976
|
-
`project_access`.
|
|
977
|
-
|
|
978
|
-
| Env var | Effect |
|
|
979
|
-
| --- | --- |
|
|
980
|
-
| `AI_PUSH_HOOKS_SKIP` | If true, sets `general.enabled = false`. |
|
|
981
|
-
| `AI_PUSH_HOOKS_ALLOW_PUSH_ON_ERROR` | Overrides `general.allow_push_on_error`. |
|
|
982
|
-
| `AI_PUSH_HOOKS_REQUIRE_CLEAN` | Overrides `general.require_clean_worktree`. |
|
|
983
|
-
| `AI_PUSH_HOOKS_ALLOW_DIRTY` | If true, forces `general.require_clean_worktree = false`. |
|
|
984
|
-
| `AI_PUSH_HOOKS_BASE_BRANCH` | Overrides `general.base_branch`. |
|
|
985
|
-
| `AI_PUSH_HOOKS_LOG_LEVEL` | Overrides `logging.level`. |
|
|
986
|
-
| `AI_PUSH_HOOKS_PRINT_LLM_OUTPUT` | Overrides `logging.print_llm_output`; normalized final text only, redacted, and sensitive. |
|
|
987
|
-
| `AI_PUSH_HOOKS_MODEL` | Final model override for the selected profile; values remain opaque identifiers. |
|
|
988
|
-
| `AI_PUSH_HOOKS_VARIANT` | Final variant override for OpenCode only. |
|
|
989
|
-
| `AI_PUSH_HOOKS_TIMEOUT_SECONDS` | Overrides `llm.timeout_seconds` (integer). |
|
|
990
|
-
|
|
991
|
-
`when_env` is step-level and can point to any env var. A common example is `AI_PUSH_HOOKS_CREATE_PR` to gate PR creation steps.
|
|
992
|
-
|
|
993
|
-
## Example: docs + PR with opt-in creation
|
|
124
|
+
OpenCode defaults to **`openai/gpt-5.6-luna`**. Explicit runner profiles use their own model settings. To use Codex or Claude Code, add its profile and change `[llm].runner`:
|
|
994
125
|
|
|
995
126
|
```toml
|
|
996
|
-
[
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
[modules.docs]
|
|
1000
|
-
enabled = true
|
|
1001
|
-
|
|
1002
|
-
[[modules.docs.steps]]
|
|
1003
|
-
id = "collect"
|
|
1004
|
-
type = "collect"
|
|
1005
|
-
collector = "docs_context"
|
|
1006
|
-
|
|
1007
|
-
[[modules.docs.steps]]
|
|
1008
|
-
id = "query"
|
|
1009
|
-
type = "ask"
|
|
1010
|
-
fallback_prompt_id = "docs-query-basic"
|
|
1011
|
-
inputs = ["collect/push.diff", "collect/changed-files.txt"]
|
|
1012
|
-
output = "queries.json"
|
|
1013
|
-
schema = "string_array"
|
|
1014
|
-
|
|
1015
|
-
[[modules.docs.steps]]
|
|
1016
|
-
id = "analyze"
|
|
1017
|
-
type = "ask"
|
|
1018
|
-
fallback_prompt_id = "docs-analysis-basic"
|
|
1019
|
-
inputs = ["collect/push.diff", "collect/docs-context.txt", "query/queries.json", "collect/recent-commits.txt"]
|
|
1020
|
-
output = "issues.json"
|
|
1021
|
-
schema = "docs_issue_array"
|
|
127
|
+
[runners.codex]
|
|
128
|
+
type = "codex"
|
|
1022
129
|
|
|
1023
|
-
[
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
fallback_prompt_id = "docs-apply-basic"
|
|
1027
|
-
inputs = ["collect/push.diff", "collect/docs-context.txt", "analyze/issues.json"]
|
|
1028
|
-
allow_paths = ["README.md", "docs/**/*.md"]
|
|
130
|
+
[runners.claude]
|
|
131
|
+
type = "claude"
|
|
132
|
+
```
|
|
1029
133
|
|
|
1030
|
-
[
|
|
1031
|
-
id = "assert"
|
|
1032
|
-
type = "assert"
|
|
1033
|
-
assertion = "docs_apply_requires_manual_commit"
|
|
1034
|
-
inputs = ["apply/result.json"]
|
|
134
|
+
Each `ask` or `apply` step can override the runner, so one tool can review and another can fix. Use model identifiers available to your provider. Other tools, including Pi, can use a [custom command runner](docs/configuration.md#custom-runners).
|
|
1035
135
|
|
|
1036
|
-
|
|
1037
|
-
enabled = true
|
|
136
|
+
## Control And Safety
|
|
1038
137
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
138
|
+
- Errors block pushes by default. AI judgments can still miss violations or report false positives.
|
|
139
|
+
- `apply` validates file and Git state and limits propagated edits to `allow_paths`. It does not auto-commit.
|
|
140
|
+
- Runners, scripts, and callbacks are trusted local programs, not an OS sandbox.
|
|
141
|
+
- Repository content may be sent to your model provider. Review its privacy and billing terms.
|
|
1043
142
|
|
|
1044
|
-
|
|
1045
|
-
id = "compose"
|
|
1046
|
-
type = "ask"
|
|
1047
|
-
fallback_prompt_id = "pr-compose-basic"
|
|
1048
|
-
inputs = ["collect/pr-context.txt", "collect/changed-files.txt", "collect/push.diff", "collect/commits.txt"]
|
|
1049
|
-
output = "pr-draft.json"
|
|
1050
|
-
schema = "pr_create_payload"
|
|
143
|
+
Logs and run summaries live under `.git/ai-push-hooks/`. OpenCode transcripts are captured there by default. To intentionally skip one push: `AI_PUSH_HOOKS_SKIP=1 git push`.
|
|
1051
144
|
|
|
1052
|
-
[[
|
|
1053
|
-
id = "create"
|
|
1054
|
-
type = "exec"
|
|
1055
|
-
executor = "gh_pr_create"
|
|
1056
|
-
when_env = "AI_PUSH_HOOKS_CREATE_PR"
|
|
1057
|
-
inputs = ["compose/pr-draft.json"]
|
|
1058
|
-
```
|
|
145
|
+
[Configuration](docs/configuration.md) | [Security](SECURITY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) | [MIT License](LICENSE)
|