ai-push-hooks 0.3.1 → 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 +49 -1
- package/README.md +91 -80
- package/ai-push-hooks.toml +1 -1
- package/docs/configuration.md +263 -0
- package/package.json +3 -3
- package/pyproject.toml +12 -3
- package/src/ai_push_hooks/artifacts.py +19 -6
- package/src/ai_push_hooks/cli.py +24 -8
- package/src/ai_push_hooks/config.py +220 -49
- package/src/ai_push_hooks/engine.py +93 -43
- package/src/ai_push_hooks/executors/apply.py +121 -43
- package/src/ai_push_hooks/executors/ask.py +30 -13
- package/src/ai_push_hooks/executors/exec.py +61 -22
- package/src/ai_push_hooks/executors/runner_workflow.py +38 -16
- 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 +74 -21
- package/src/ai_push_hooks/executors/runners/opencode_support.py +15 -5
- package/src/ai_push_hooks/executors/runners/process.py +26 -7
- package/src/ai_push_hooks/executors/runners/registry.py +14 -5
- package/src/ai_push_hooks/executors/step_commands.py +58 -18
- package/src/ai_push_hooks/git_utils.py +92 -27
- package/src/ai_push_hooks/hook.py +48 -12
- package/src/ai_push_hooks/install.py +40 -18
- package/src/ai_push_hooks/modules/beads.py +18 -7
- package/src/ai_push_hooks/modules/docs.py +17 -7
- package/src/ai_push_hooks/modules/pr.py +18 -7
- package/src/ai_push_hooks/paths.py +6 -2
- package/src/ai_push_hooks/plugin_loader.py +79 -26
- 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 +47 -27
- package/run.sh +0 -29
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.2] - 2026-09-12
|
|
8
|
+
|
|
9
|
+
This is a beta patch release. Python and npm both use `0.3.2`, the canonical
|
|
10
|
+
Git tag is `v0.3.2`, npm uses the `beta` dist-tag, and GitHub marks the release
|
|
11
|
+
as a prerelease. PyPI does not provide a separate beta channel, so Python users
|
|
12
|
+
must select the exact `0.3.2` version.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Added a pinned Ruff 0.13.3 formatting policy for Python sources, tests, and scripts.
|
|
17
|
+
- Updated the default OpenCode model and README positioning to
|
|
18
|
+
`openai/gpt-5.6-luna`.
|
|
19
|
+
- Clarified that built-in mutating steps are serialized, while trusted custom
|
|
20
|
+
command runners, including project-access `ask` commands, are not enforced
|
|
21
|
+
read-only.
|
|
22
|
+
- Removed the unused secondary shell launcher in favor of the canonical Python
|
|
23
|
+
console script and npm wrapper.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Review initial publication of the configured base branch against the empty
|
|
28
|
+
tree, while retaining configured-base comparison for new feature branches.
|
|
29
|
+
- Persist bounded diffs as valid UTF-8, truncating at character boundaries and
|
|
30
|
+
representing malformed bytes with replacement characters within the budget.
|
|
31
|
+
- Hardened pull-request creation by validating generated payload types,
|
|
32
|
+
reconciling failed `gh` calls against GitHub, and keeping remote credentials
|
|
33
|
+
out of diagnostics.
|
|
34
|
+
- Bounded config, prompt, and callback source reads and rejected unsafe config
|
|
35
|
+
file types.
|
|
36
|
+
- Rejected duplicate workflow identifiers and simplified workflow step result
|
|
37
|
+
handling.
|
|
38
|
+
- Made source distributions complete and added validation of their unpacked
|
|
39
|
+
test suite and wheel builds with the minimum supported setuptools 77 backend.
|
|
40
|
+
- Standardized development and release validation on the isolated project
|
|
41
|
+
`.[dev]` extra instead of repeating tool lists.
|
|
42
|
+
- Restored the PyPI release job's checkout permission and pinned the OpenCode
|
|
43
|
+
contract test's base container image.
|
|
44
|
+
|
|
45
|
+
### Documentation
|
|
46
|
+
|
|
47
|
+
- Added an agent setup skill, CI/npm beta badges, and a reproducible,
|
|
48
|
+
deterministic review/fail/fix/commit/pass demo, explicitly not live AI footage.
|
|
49
|
+
- Explained the manual-commit assertion and why applied edits are not part of
|
|
50
|
+
the commit already being pushed.
|
|
51
|
+
- Included the configuration guide in the npm package and removed its link to
|
|
52
|
+
the local-only runner verification report.
|
|
53
|
+
|
|
7
54
|
## [0.3.1] - 2026-09-10
|
|
8
55
|
|
|
9
56
|
### Changed
|
|
@@ -149,7 +196,8 @@ the exact `0.2.0` version rather than a PyPI beta channel.
|
|
|
149
196
|
- Preserved Git porcelain paths and protected pre-existing dirty allowlisted files during apply steps.
|
|
150
197
|
- Clarified module-local artifact references and standardized the repository hook integration.
|
|
151
198
|
|
|
152
|
-
[Unreleased]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.3.
|
|
199
|
+
[Unreleased]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.3.2...HEAD
|
|
200
|
+
[0.3.2]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.3.1...v0.3.2
|
|
153
201
|
[0.3.1]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.3.0...v0.3.1
|
|
154
202
|
[0.3.0]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.2.1...v0.3.0
|
|
155
203
|
[0.2.1]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.2.0...v0.2.1
|
package/README.md
CHANGED
|
@@ -1,134 +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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
|
|
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
|
+
|
|
11
|
+
## Rules Worth Checking
|
|
12
|
+
|
|
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. |
|
|
19
|
+
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Why Now?
|
|
23
|
+
|
|
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.
|
|
11
25
|
|
|
12
26
|
## Quick Start
|
|
13
27
|
|
|
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.
|
|
29
|
+
|
|
14
30
|
```bash
|
|
15
31
|
npm install --save-dev ai-push-hooks@beta
|
|
16
|
-
npx --no-install ai-push-hooks init
|
|
32
|
+
npx --no-install ai-push-hooks init
|
|
17
33
|
npx --no-install ai-push-hooks install
|
|
18
34
|
```
|
|
19
35
|
|
|
20
|
-
|
|
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.
|
|
21
37
|
|
|
22
38
|
[Other installation options](docs/configuration.md#installation) | [Existing hook managers](docs/configuration.md#hook-managers)
|
|
23
39
|
|
|
24
|
-
##
|
|
40
|
+
## Deterministic Demo
|
|
25
41
|
|
|
26
|
-
|
|
42
|
+

|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
| --- | --- |
|
|
30
|
-
| `collect` | Gather the outgoing diff and relevant context. |
|
|
31
|
-
| `ask` | Ask AI for plain text or schema-validated JSON. |
|
|
32
|
-
| `apply` | Let AI edit a temporary workspace; copy back only allowlisted changes. |
|
|
33
|
-
| `exec` | Run a command, Python callback, or built-in action. |
|
|
34
|
-
| `assert` | Enforce a rule and block the push if it fails. |
|
|
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`.
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
## Example: Check Your Rules
|
|
37
47
|
|
|
38
|
-
|
|
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.
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
In `checks/hooks.py`:
|
|
41
51
|
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
runner = "opencode"
|
|
52
|
+
```python
|
|
53
|
+
import json
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
type = "opencode"
|
|
48
|
-
model = "provider/model-id" # Replace with an available OpenCode model.
|
|
49
|
-
project_access = "artifacts"
|
|
55
|
+
from ai_push_hooks.plugins import CollectorResult, PluginContext
|
|
50
56
|
|
|
51
|
-
[runners.codex]
|
|
52
|
-
type = "codex"
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
})
|
|
57
63
|
|
|
58
|
-
Each `ask` or `apply` step can override the default with, for example, `runner = "claude"`. You can review with one tool and apply with another.
|
|
59
64
|
|
|
60
|
-
|
|
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."}
|
|
68
|
+
```
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
In `ai-push-hooks.toml`:
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
```toml
|
|
73
|
+
[llm]
|
|
74
|
+
runner = "opencode"
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
[runners.opencode]
|
|
77
|
+
type = "opencode"
|
|
78
|
+
model = "openai/gpt-5.6-luna"
|
|
79
|
+
project_access = "project"
|
|
67
80
|
|
|
68
|
-
```toml
|
|
69
81
|
[workflow]
|
|
70
|
-
modules = ["
|
|
82
|
+
modules = ["rules"]
|
|
71
83
|
|
|
72
|
-
[[modules.
|
|
84
|
+
[[modules.rules.steps]]
|
|
73
85
|
id = "collect"
|
|
74
86
|
type = "collect"
|
|
75
|
-
|
|
87
|
+
python = "checks/hooks.py:collect_rules"
|
|
76
88
|
|
|
77
|
-
[[modules.
|
|
89
|
+
[[modules.rules.steps]]
|
|
78
90
|
id = "review"
|
|
79
91
|
type = "ask"
|
|
80
|
-
prompt = "Check
|
|
81
|
-
inputs = ["collect/push.diff", "collect/
|
|
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"]
|
|
82
94
|
output = "issues.json"
|
|
83
95
|
schema = "docs_issue_array"
|
|
84
96
|
|
|
85
|
-
[[modules.
|
|
86
|
-
id = "
|
|
87
|
-
type = "apply"
|
|
88
|
-
prompt = "Fix only the reported documentation drift. Keep edits minimal."
|
|
89
|
-
inputs = ["collect/push.diff", "review/issues.json"]
|
|
90
|
-
allow_paths = ["README.md", "docs/**/*.md"]
|
|
91
|
-
|
|
92
|
-
[[modules.docs.steps]]
|
|
93
|
-
id = "review-required"
|
|
97
|
+
[[modules.rules.steps]]
|
|
98
|
+
id = "gate"
|
|
94
99
|
type = "assert"
|
|
95
|
-
|
|
96
|
-
inputs = ["
|
|
100
|
+
python = "checks/hooks.py:assert_rules"
|
|
101
|
+
inputs = ["review/issues.json"]
|
|
97
102
|
```
|
|
98
103
|
|
|
99
|
-
|
|
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/`.
|
|
100
105
|
|
|
101
|
-
**Want
|
|
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.
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
[[modules.verify.steps]]
|
|
105
|
-
id = "tests"
|
|
106
|
-
type = "exec"
|
|
107
|
-
command = ["npm", "test"]
|
|
108
|
-
timeout_seconds = 300
|
|
109
|
-
```
|
|
108
|
+
## Build Your Workflow
|
|
110
109
|
|
|
111
|
-
|
|
110
|
+
Each module combines the steps it needs:
|
|
112
111
|
|
|
113
|
-
|
|
112
|
+
| Step | Purpose |
|
|
113
|
+
| --- | --- |
|
|
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. |
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
- **Custom checks:** use argv commands or repo-local Python callbacks.
|
|
117
|
-
- **Task alignment:** use the Beads collector and actions with the optional `bd` CLI.
|
|
118
|
-
- **Pull requests:** compose a PR with AI and create it with the optional `gh` CLI.
|
|
119
|
-
- **Opt-in steps:** gate a step with `when_env`.
|
|
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.
|
|
120
121
|
|
|
121
|
-
|
|
122
|
+
## Choose Your AI
|
|
122
123
|
|
|
123
|
-
|
|
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`:
|
|
125
|
+
|
|
126
|
+
```toml
|
|
127
|
+
[runners.codex]
|
|
128
|
+
type = "codex"
|
|
124
129
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
[runners.claude]
|
|
131
|
+
type = "claude"
|
|
132
|
+
```
|
|
133
|
+
|
|
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).
|
|
135
|
+
|
|
136
|
+
## Control And Safety
|
|
129
137
|
|
|
130
|
-
|
|
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.
|
|
131
142
|
|
|
132
|
-
To intentionally skip one push: `AI_PUSH_HOOKS_SKIP=1 git push`.
|
|
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`.
|
|
133
144
|
|
|
134
|
-
[Configuration](docs/configuration.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) | [MIT License](LICENSE)
|
|
145
|
+
[Configuration](docs/configuration.md) | [Security](SECURITY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) | [MIT License](LICENSE)
|
package/ai-push-hooks.toml
CHANGED
|
@@ -12,7 +12,7 @@ base_branch = "main"
|
|
|
12
12
|
# Repository callbacks use python = "path/to/file.py:callable" and command
|
|
13
13
|
# steps use a direct argv array; both are trusted local code.
|
|
14
14
|
runner = "opencode"
|
|
15
|
-
model = "openai/gpt-5.6-
|
|
15
|
+
model = "openai/gpt-5.6-luna"
|
|
16
16
|
variant = ""
|
|
17
17
|
timeout_seconds = 800
|
|
18
18
|
max_parallel = 2
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Define workflows in `ai-push-hooks.toml` at the repository root. Start with the [README example](../README.md#example-check-your-rules) or the [starter configuration](../ai-push-hooks.toml).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The [npm quick start](../README.md#quick-start) installs a repository-local wrapper. It needs Node 18+ and Python 3.10-3.13 on the hook's `PATH`; the npm package does not bundle Python.
|
|
8
|
+
|
|
9
|
+
For pnpm:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add -D ai-push-hooks@beta
|
|
13
|
+
pnpm exec ai-push-hooks init --template minimal-docs
|
|
14
|
+
pnpm exec ai-push-hooks install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For a Python-only installation:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install ai-push-hooks
|
|
21
|
+
ai-push-hooks init --template minimal-docs
|
|
22
|
+
ai-push-hooks install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`uv tool install ai-push-hooks` or `pipx install ai-push-hooks` can replace the pip command. npm uses the `beta` tag while the package is in beta.
|
|
26
|
+
|
|
27
|
+
AI steps need the selected CLI installed and authenticated with an available model. Deterministic-only workflows do not need one. `init` and `install` refuse to overwrite existing files; `--force` explicitly replaces them.
|
|
28
|
+
|
|
29
|
+
## Modules And Steps
|
|
30
|
+
|
|
31
|
+
`[workflow].modules` selects the modules to run. Define each module under `modules.<name>` with a non-empty list of steps. Modules are enabled by default; set `enabled = false` to disable one.
|
|
32
|
+
|
|
33
|
+
Step inputs refer to earlier artifacts in the same module: `collect/push.diff`, for example. Cross-module inputs are not supported. Independent `collect` and `ask` work may 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.
|
|
34
|
+
|
|
35
|
+
| Field | Used by | Meaning |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `id` | All | Unique step name within the module. |
|
|
38
|
+
| `type` | All | `collect`, `ask`, `apply`, `exec`, or `assert`. |
|
|
39
|
+
| `inputs` | Non-collector steps | Earlier module-local artifacts. |
|
|
40
|
+
| `when_env` | All | Run only when the named environment variable is true. |
|
|
41
|
+
| `collector` | `collect` | Built-in collector name. |
|
|
42
|
+
| `prompt` | `ask`, `apply` | Inline instructions. |
|
|
43
|
+
| `prompt_file` | `ask`, `apply` | Repository-relative prompt file. |
|
|
44
|
+
| `fallback_prompt_id` | `ask`, `apply` | Built-in prompt name. |
|
|
45
|
+
| `runner` | `ask`, `apply` | Override the default runner profile. |
|
|
46
|
+
| `output` | `ask` | Required response artifact filename. |
|
|
47
|
+
| `schema` | `ask` | Optional JSON schema name. Omit for plain text. |
|
|
48
|
+
| `allow_paths` | `apply` | Required list of permitted edit globs. |
|
|
49
|
+
| `executor` | `exec` | Built-in action name. |
|
|
50
|
+
| `assertion` | `assert` | Built-in assertion name. |
|
|
51
|
+
| `python` | `collect`, `exec`, `assert` | Repository-local callback reference. |
|
|
52
|
+
| `options` | Python steps | JSON-compatible callback options. |
|
|
53
|
+
| `command` | `exec`, `assert` | Direct argv command. |
|
|
54
|
+
| `stdin` | Command steps | One declared input to send on stdin. |
|
|
55
|
+
| `timeout_seconds` | Command steps | Positive timeout; default `60`. |
|
|
56
|
+
|
|
57
|
+
Choose one handler per deterministic step: a built-in, a callback, or a command. Prompt resolution uses `prompt`, then `prompt_file`, then `fallback_prompt_id`.
|
|
58
|
+
|
|
59
|
+
`ask` produces a response, not an automatic policy verdict. Use `assert` to evaluate findings when they should block. `apply` can run without a preceding `ask`; an input ending in `issues.json` containing `[]` skips the apply step.
|
|
60
|
+
|
|
61
|
+
Upgrading from 0.2.1? Rename `type = "llm"` steps to `type = "ask"`.
|
|
62
|
+
|
|
63
|
+
## Runner Profiles
|
|
64
|
+
|
|
65
|
+
`[llm].runner` selects the default; a step's `runner` overrides it. Referenced profiles must exist under `[runners.<name>]`, except for the implicit OpenCode default.
|
|
66
|
+
|
|
67
|
+
| Profile field | Values / behavior |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `type` | Required: `opencode`, `codex`, `claude`, or `command`. |
|
|
70
|
+
| `model` | Runner-specific model identifier. |
|
|
71
|
+
| `project_access` | `artifacts` or `project`; OpenCode defaults to `artifacts`, others to `project`. |
|
|
72
|
+
| `variant` | Optional OpenCode variant. |
|
|
73
|
+
| `command` | Required argv array for command runners. |
|
|
74
|
+
| `prompt_transport` | Command runners: `stdin` (default) or `argv`. |
|
|
75
|
+
|
|
76
|
+
Without an explicit OpenCode profile, `[llm].model` and `[llm].variant` configure it. Explicit profiles use their own model settings. `AI_PUSH_HOOKS_MODEL` overrides the selected profile's model.
|
|
77
|
+
|
|
78
|
+
Artifact mode supplies collected inputs in a scratch directory. Project mode lets analysis inspect the checkout. Apply always uses a temporary staging copy, with propagation limited by `allow_paths`.
|
|
79
|
+
|
|
80
|
+
OpenCode runs with isolated configuration and permissions; project/global configuration, external plugins, and MCP servers are not loaded. Codex uses read-only analysis and workspace-write apply modes. Claude Code uses separate analysis and editing permissions. Authenticate the CLI before using it in a hook.
|
|
81
|
+
|
|
82
|
+
Apply requires a single pushed branch whose local commit is the checked-out `HEAD`. Staging excludes Git metadata, `AGENTS.md`, ignored files, symlinks, and special files. These controls are not an OS sandbox or an automatic rollback system. See [Security](../SECURITY.md).
|
|
83
|
+
|
|
84
|
+
### Apply and manual commits
|
|
85
|
+
|
|
86
|
+
`apply` is generic: it can edit any eligible checkout file matching `allow_paths`; it is not limited to Markdown. The runner edits a temporary staging copy, and only validated changes propagate back to the checkout. Those edits do not enter the commit already being pushed, and `apply` never creates a Git commit.
|
|
87
|
+
|
|
88
|
+
The existing `docs_apply_requires_manual_commit` assertion is a workflow gate, not human-review enforcement. It prevents the original push from passing after `apply` changes files; it cannot prove that anyone reviewed the edits or that they conform to policy. Add it after the `apply` step when that gate is desired:
|
|
89
|
+
|
|
90
|
+
```toml
|
|
91
|
+
[[modules.docs.steps]]
|
|
92
|
+
id = "manual-commit"
|
|
93
|
+
type = "assert"
|
|
94
|
+
assertion = "docs_apply_requires_manual_commit"
|
|
95
|
+
inputs = ["apply/result.json"]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The assertion checks `apply/result.json`'s `changed_files` and intentionally blocks when edits were propagated. Review `git diff`, run the relevant checks, commit the approved changes, and retry the push. On the retry, the assertion passes when the apply step reports no changes.
|
|
99
|
+
|
|
100
|
+
## Custom Runners
|
|
101
|
+
|
|
102
|
+
Use a command profile for another CLI, including Pi, or your own wrapper. This example expects a local `scripts/review-agent` program that reads the prompt from stdin and writes its final response to stdout:
|
|
103
|
+
|
|
104
|
+
```toml
|
|
105
|
+
[runners.custom]
|
|
106
|
+
type = "command"
|
|
107
|
+
command = ["/absolute/path/to/scripts/review-agent"]
|
|
108
|
+
prompt_transport = "stdin"
|
|
109
|
+
project_access = "project"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Select it with `runner = "custom"` on an `ask` or `apply` step. The runner receives the full instruction and artifact packet. For apply, its working directory is the staging copy.
|
|
113
|
+
|
|
114
|
+
Commands are argv arrays, not shell strings. Whole-argument placeholders are `{model}`, `{cwd}`, `{stage}`, and `{prompt}`. The `argv` transport requires exactly one `{prompt}` argument; stdin avoids exposing prompts in process listings. Custom programs inherit the user environment and own their permissions and session lifecycle.
|
|
115
|
+
|
|
116
|
+
## Commands And Callbacks
|
|
117
|
+
|
|
118
|
+
Command steps run in the real repository, with no implicit shell. Exit zero means success; a nonzero exit blocks the workflow. stdout, stderr, and a `result.json` report are saved as step artifacts.
|
|
119
|
+
|
|
120
|
+
```toml
|
|
121
|
+
[[modules.verify.steps]]
|
|
122
|
+
id = "tests"
|
|
123
|
+
type = "exec"
|
|
124
|
+
command = ["{python}", "-m", "pytest", "-q"]
|
|
125
|
+
timeout_seconds = 300
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Available whole-argument placeholders are `{repo}`, `{python}`, and `{input:<step/artifact>}`. Input placeholders must also appear in `inputs`. Stdin is closed unless `stdin` names a declared input. Command output is bounded to 16 MiB per stream.
|
|
129
|
+
|
|
130
|
+
For custom context or policy, reference a top-level synchronous Python function:
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
[[modules.policy.steps]]
|
|
134
|
+
id = "change-size"
|
|
135
|
+
type = "assert"
|
|
136
|
+
python = "checks/hooks.py:check_size"
|
|
137
|
+
options = { max_files = 25 }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
In `checks/hooks.py`:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from ai_push_hooks.plugins import PluginContext
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def check_size(context: PluginContext) -> dict:
|
|
147
|
+
ok = len(context.push.changed_files) <= context.options["max_files"]
|
|
148
|
+
return {"ok": ok, "message": "Change exceeds the configured file limit." if not ok else ""}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Add `policy` to `[workflow].modules` to enable it. Callbacks receive repository and step identifiers, push facts, input paths, options, prior module metadata, and a logger.
|
|
152
|
+
|
|
153
|
+
| Callback type | Return value |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `collect` | `CollectorResult` from `ai_push_hooks.plugins`, with artifacts and metadata. |
|
|
156
|
+
| `exec` | JSON-serializable dictionary, saved as `result.json`. |
|
|
157
|
+
| `assert` | Dictionary with boolean `ok` and optional string `message`; false blocks. |
|
|
158
|
+
|
|
159
|
+
Callbacks run as trusted in-process code with already-installed dependencies. There is no callback timeout or automatic dependency installation. Collectors may run concurrently, so custom collectors must be concurrency-safe.
|
|
160
|
+
|
|
161
|
+
## Built-In Handlers
|
|
162
|
+
|
|
163
|
+
| Kind | Name | Purpose |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| Collector | `docs_context` | Push diff, changed files, docs excerpts, recent commits. |
|
|
166
|
+
| Collector | `beads_status_context` | Branch and Beads task context. |
|
|
167
|
+
| Collector | `pr_context` | Context for a pull request. |
|
|
168
|
+
| Action | `beads_alignment` | Apply supported `bd update` / `bd close` operations. |
|
|
169
|
+
| Action | `gh_pr_create` | Create or reuse a PR through `gh`. |
|
|
170
|
+
| Assertion | `docs_apply_requires_manual_commit` | Block after applied docs changes. |
|
|
171
|
+
| Assertion | `beads_alignment_clean` | Block on unresolved task alignment. |
|
|
172
|
+
|
|
173
|
+
Beads steps need the native `bd` CLI; PR creation needs `gh`. Gate optional actions with `when_env = "AI_PUSH_HOOKS_CREATE_PR"`, for example. Beads schema and database maintenance remain separate from hook execution.
|
|
174
|
+
|
|
175
|
+
| JSON schema | Payload |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `string_array` | Array of strings. |
|
|
178
|
+
| `docs_issue_array` | Array of objects with `file` and `description`. |
|
|
179
|
+
| `beads_alignment_result` | Object with optional `commands` string array. |
|
|
180
|
+
| `pr_create_payload` | Object containing PR creation fields. |
|
|
181
|
+
|
|
182
|
+
Built-in prompts: `docs-query-basic`, `docs-analysis-basic`, `docs-apply-basic`, `beads-plan-basic`, and `pr-compose-basic`.
|
|
183
|
+
|
|
184
|
+
## Settings
|
|
185
|
+
|
|
186
|
+
These optional settings supplement the workflow and runner definitions:
|
|
187
|
+
|
|
188
|
+
| Section | Key | Default |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `general` | `enabled` | `true` |
|
|
191
|
+
| `general` | `allow_push_on_error` | `false` |
|
|
192
|
+
| `general` | `require_clean_worktree` | `false` |
|
|
193
|
+
| `general` | `skip_on_sync_branch` | `true` |
|
|
194
|
+
| `general` | `base_branch` | `"main"` |
|
|
195
|
+
| `llm` | `runner` | `"opencode"` |
|
|
196
|
+
| `llm` | `model` | `"openai/gpt-5.6-luna"` (implicit OpenCode profile) |
|
|
197
|
+
| `llm` | `variant` | `""` |
|
|
198
|
+
| `llm` | `timeout_seconds` | `800` |
|
|
199
|
+
| `llm` | `max_parallel` | `2` |
|
|
200
|
+
| `llm` | `json_max_retries` | `2` |
|
|
201
|
+
| `llm` | `invalid_json_feedback_max_chars` | `6000` |
|
|
202
|
+
| `llm` | `json_retry_new_session` | `true` |
|
|
203
|
+
| `llm` | `delete_session_after_run` | `true` |
|
|
204
|
+
| `llm` | `max_diff_bytes` | `180000` |
|
|
205
|
+
| `llm` | `session_title_prefix` | `"ai-push-hooks"` |
|
|
206
|
+
| `logging` | `level` | `"status"` (`info` and `debug` also available) |
|
|
207
|
+
| `logging` | `jsonl` | `true` |
|
|
208
|
+
| `logging` | `capture_llm_transcript` | `true` (OpenCode only) |
|
|
209
|
+
| `logging` | `print_llm_output` | `false` |
|
|
210
|
+
| `logging` | `dir` | `".git/ai-push-hooks/logs"` |
|
|
211
|
+
| `logging` | `transcript_dir` | `".git/ai-push-hooks/transcripts"` |
|
|
212
|
+
| `logging` | `summary_dir` | `".git/ai-push-hooks/summaries"` |
|
|
213
|
+
|
|
214
|
+
Model availability depends on the provider. Select one available to your account rather than assuming the starter's model is accessible.
|
|
215
|
+
|
|
216
|
+
## Environment Overrides
|
|
217
|
+
|
|
218
|
+
| Variable | Effect |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `AI_PUSH_HOOKS_SKIP=1` | Skip the hook before loading configuration. |
|
|
221
|
+
| `AI_PUSH_HOOKS_ALLOW_PUSH_ON_ERROR=1` | Log errors but allow the push. |
|
|
222
|
+
| `AI_PUSH_HOOKS_REQUIRE_CLEAN=1` | Require a clean worktree. |
|
|
223
|
+
| `AI_PUSH_HOOKS_ALLOW_DIRTY=1` | Allow a dirty worktree. |
|
|
224
|
+
| `AI_PUSH_HOOKS_BASE_BRANCH` | Override the base branch. |
|
|
225
|
+
| `AI_PUSH_HOOKS_MODEL` | Override the selected runner's model. |
|
|
226
|
+
| `AI_PUSH_HOOKS_VARIANT` | Override the OpenCode variant. |
|
|
227
|
+
| `AI_PUSH_HOOKS_TIMEOUT_SECONDS` | Override the runner timeout. |
|
|
228
|
+
| `AI_PUSH_HOOKS_LOG_LEVEL` | Set console verbosity. |
|
|
229
|
+
| `AI_PUSH_HOOKS_PRINT_LLM_OUTPUT=1` | Print normalized, redacted final responses; sensitive opt-in. |
|
|
230
|
+
|
|
231
|
+
Boolean values accept `1/0`, `true/false`, `yes/no`, `y/n`, and `on/off`. Skips and fail-open settings are explicit policy choices, not successful checks.
|
|
232
|
+
|
|
233
|
+
## Hook Managers
|
|
234
|
+
|
|
235
|
+
The built-in `install` command creates a repository-local pre-push hook without changing Git configuration. Existing hooks require explicit `--force` replacement; shared/external paths and symlinks are refused. Keep the installed Python executable or local npm package available at its installed location.
|
|
236
|
+
|
|
237
|
+
For Lefthook, use this in `lefthook.yml` instead of the built-in installer:
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
pre-push:
|
|
241
|
+
commands:
|
|
242
|
+
ai-checks:
|
|
243
|
+
run: npx --no-install ai-push-hooks hook {1} {2}
|
|
244
|
+
use_stdin: true
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Run `lefthook install`. For a Python installation, omit the `npx --no-install` prefix. Other hook managers must likewise forward Git's remote arguments, ref-update stdin, and exit status. If earlier commands consume stdin, capture it first and replay it to ai-push-hooks.
|
|
248
|
+
|
|
249
|
+
To remove the generated hook, inspect `git rev-parse --git-path hooks/pre-push` and remove only the ai-push-hooks delegate or its entry in your hook manager. Preserve unrelated hook commands.
|
|
250
|
+
|
|
251
|
+
## Troubleshooting
|
|
252
|
+
|
|
253
|
+
| Symptom | Check |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| Hook does not run | Reinstall with your chosen hook manager and inspect `git config --get core.hooksPath`. |
|
|
256
|
+
| Missing runner or authentication failure | Ensure the CLI is on the hook's `PATH`, authenticated, and configured with an available model. |
|
|
257
|
+
| Unknown profile | Match `runner` to an existing `[runners.<name>]`. |
|
|
258
|
+
| Runner capability error | Update the CLI to a version with the required adapter flags. |
|
|
259
|
+
| Invalid JSON | Check the prompt and schema; invalid JSON is retried twice by default. |
|
|
260
|
+
| Push blocked after edits | Review `git diff`, run checks, commit approved changes, and retry the push. |
|
|
261
|
+
| Need diagnostics | Inspect `.git/ai-push-hooks/logs` and `.git/ai-push-hooks/summaries`. |
|
|
262
|
+
|
|
263
|
+
For validation commands, see [Contributing](../CONTRIBUTING.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-push-hooks",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Run structured AI-assisted checks and allowlisted maintenance before git push",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"ai-push-hooks": "bin/ai-push-hooks.js"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"test": "uv run --no-project --with
|
|
33
|
+
"test": "uv run --no-project --with \".[dev]\" python -m pytest tests -q",
|
|
34
34
|
"test:npm-pack": "node tests/npm-pack-smoke.mjs"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"src/**/*.py",
|
|
39
39
|
"vendor",
|
|
40
40
|
"README.md",
|
|
41
|
+
"docs/configuration.md",
|
|
41
42
|
"CHANGELOG.md",
|
|
42
43
|
"SECURITY.md",
|
|
43
|
-
"run.sh",
|
|
44
44
|
"pyproject.toml",
|
|
45
45
|
"LICENSE",
|
|
46
46
|
"ai-push-hooks.toml"
|
package/pyproject.toml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ai-push-hooks"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Run structured AI-assisted checks and allowlisted maintenance before git push"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -34,7 +34,7 @@ Changelog = "https://github.com/shanebishop1/ai-push-hooks/blob/main/CHANGELOG.m
|
|
|
34
34
|
ai-push-hooks = "ai_push_hooks.cli:main"
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
|
-
dev = ["pytest>=8.0"]
|
|
37
|
+
dev = ["pytest>=8.0", "build", "pip", "ruff", "twine"]
|
|
38
38
|
|
|
39
39
|
[tool.setuptools]
|
|
40
40
|
include-package-data = true
|
|
@@ -44,3 +44,12 @@ where = ["src"]
|
|
|
44
44
|
|
|
45
45
|
[tool.pytest.ini_options]
|
|
46
46
|
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
target-version = "py310"
|
|
50
|
+
line-length = 88
|
|
51
|
+
|
|
52
|
+
[tool.ruff.format]
|
|
53
|
+
quote-style = "double"
|
|
54
|
+
indent-style = "space"
|
|
55
|
+
line-ending = "lf"
|