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.
Files changed (35) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +91 -80
  3. package/ai-push-hooks.toml +1 -1
  4. package/docs/configuration.md +263 -0
  5. package/package.json +3 -3
  6. package/pyproject.toml +12 -3
  7. package/src/ai_push_hooks/artifacts.py +19 -6
  8. package/src/ai_push_hooks/cli.py +24 -8
  9. package/src/ai_push_hooks/config.py +220 -49
  10. package/src/ai_push_hooks/engine.py +93 -43
  11. package/src/ai_push_hooks/executors/apply.py +121 -43
  12. package/src/ai_push_hooks/executors/ask.py +30 -13
  13. package/src/ai_push_hooks/executors/exec.py +61 -22
  14. package/src/ai_push_hooks/executors/runner_workflow.py +38 -16
  15. package/src/ai_push_hooks/executors/runners/claude.py +18 -6
  16. package/src/ai_push_hooks/executors/runners/codex.py +9 -3
  17. package/src/ai_push_hooks/executors/runners/command.py +25 -9
  18. package/src/ai_push_hooks/executors/runners/contracts.py +52 -25
  19. package/src/ai_push_hooks/executors/runners/opencode.py +74 -21
  20. package/src/ai_push_hooks/executors/runners/opencode_support.py +15 -5
  21. package/src/ai_push_hooks/executors/runners/process.py +26 -7
  22. package/src/ai_push_hooks/executors/runners/registry.py +14 -5
  23. package/src/ai_push_hooks/executors/step_commands.py +58 -18
  24. package/src/ai_push_hooks/git_utils.py +92 -27
  25. package/src/ai_push_hooks/hook.py +48 -12
  26. package/src/ai_push_hooks/install.py +40 -18
  27. package/src/ai_push_hooks/modules/beads.py +18 -7
  28. package/src/ai_push_hooks/modules/docs.py +17 -7
  29. package/src/ai_push_hooks/modules/pr.py +18 -7
  30. package/src/ai_push_hooks/paths.py +6 -2
  31. package/src/ai_push_hooks/plugin_loader.py +79 -26
  32. package/src/ai_push_hooks/plugins.py +3 -1
  33. package/src/ai_push_hooks/prompts_builtin.py +1 -1
  34. package/src/ai_push_hooks/types.py +47 -27
  35. 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.1...HEAD
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
- **Modular AI checks before `git push`.**
3
+ [![CI](https://github.com/shanebishop1/ai-push-hooks/actions/workflows/ci.yml/badge.svg)](https://github.com/shanebishop1/ai-push-hooks/actions/workflows/ci.yml) [![npm beta](https://img.shields.io/npm/v/ai-push-hooks/beta?label=npm%20beta)](https://www.npmjs.com/package/ai-push-hooks/v/beta)
4
4
 
5
- Use **OpenCode, Codex, or Claude Code** to review outgoing changes, check that code and docs agree, and apply scoped fixes. Combine AI reasoning with your own scripts, tests, and rules in a repo-local Git hook.
5
+ **Agentic linting for the rules your coding agent forgot.**
6
6
 
7
- - **Check alignment:** compare changes with documentation, requirements, or task state.
8
- - **Ask or apply:** get findings without edits, or let AI update explicitly allowed files.
9
- - **Verify before pushing:** run deterministic checks and block when your rules fail.
10
- - **Build your workflow:** choose modules, prompts, and runners per step.
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 --template minimal-docs
32
+ npx --no-install ai-push-hooks init
17
33
  npx --no-install ai-push-hooks install
18
34
  ```
19
35
 
20
- Choose your [runner and model](#choose-your-ai) in `ai-push-hooks.toml`, then push normally. The starter checks docs, applies fixes, and stops for review if anything changed.
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
- ## Modular By Design
40
+ ## Deterministic Demo
25
41
 
26
- A workflow is a list of modules. Each module combines the steps it needs:
42
+ ![Review, fail, fix, commit, pass: deterministic demo](https://raw.githubusercontent.com/shanebishop1/ai-push-hooks/main/docs/demo/review-fix.gif)
27
43
 
28
- | Step | Purpose |
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
- Use a review-only module, an apply-only module, or a full collect/ask/apply/verify flow. Add your existing lint and test commands alongside AI checks. No AI runner is needed for deterministic-only workflows.
46
+ ## Example: Check Your Rules
37
47
 
38
- ## Choose Your AI
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
- Set the default runner in `ai-push-hooks.toml`. Change it to `codex` or `claude` to switch tools:
50
+ In `checks/hooks.py`:
41
51
 
42
- ```toml
43
- [llm]
44
- runner = "opencode"
52
+ ```python
53
+ import json
45
54
 
46
- [runners.opencode]
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
- [runners.claude]
55
- type = "claude"
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
- OpenCode defaults to collected artifacts only. Set `project_access = "project"` for repository reads; Codex and Claude default to project access. Each runner uses its own authentication and model identifiers.
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
- Other tools, including Pi, can use a [custom command runner](docs/configuration.md#custom-runners).
70
+ In `ai-push-hooks.toml`:
63
71
 
64
- ## Example: Docs Alignment
72
+ ```toml
73
+ [llm]
74
+ runner = "opencode"
65
75
 
66
- This workflow asks AI to find documentation drift, applies narrow fixes, and requires human review when files change. Use it with the runner settings above:
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 = ["docs"]
82
+ modules = ["rules"]
71
83
 
72
- [[modules.docs.steps]]
84
+ [[modules.rules.steps]]
73
85
  id = "collect"
74
86
  type = "collect"
75
- collector = "docs_context"
87
+ python = "checks/hooks.py:collect_rules"
76
88
 
77
- [[modules.docs.steps]]
89
+ [[modules.rules.steps]]
78
90
  id = "review"
79
91
  type = "ask"
80
- prompt = "Check that the docs match the outgoing code changes. Return a JSON array of factual issues with file and description fields, or [] if aligned."
81
- inputs = ["collect/push.diff", "collect/docs-context.txt"]
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.docs.steps]]
86
- id = "fix"
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
- assertion = "docs_apply_requires_manual_commit"
96
- inputs = ["fix/result.json"]
100
+ python = "checks/hooks.py:assert_rules"
101
+ inputs = ["review/issues.json"]
97
102
  ```
98
103
 
99
- **Want findings only?** Remove the `fix` and `review-required` steps. `ask` saves a response; findings do not block a push unless you add an assertion to evaluate them.
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 test verification too?** Change the workflow list to `modules = ["docs", "verify"]` and append a module using your project's test command:
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
- ```toml
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
- A nonzero test exit blocks the push. For requirements or plan alignment, use project access and a prompt such as: "Compare the outgoing diff with docs/requirements.md. Identify unmet acceptance criteria and missing tests." AI review complements verification; it does not replace running the tests.
110
+ Each module combines the steps it needs:
112
111
 
113
- ## Extend It
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
- - **Prompts:** write them inline, load a `prompt_file`, or use a built-in prompt.
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
- See the [configuration guide](docs/configuration.md) for settings, callbacks, and built-in handlers.
122
+ ## Choose Your AI
122
123
 
123
- ## Control And Safety
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
- - Errors block pushes by default. Review and commit any applied edits before retrying.
126
- - `apply` checks file and Git state before and after copying allowlisted changes back. It does not auto-commit.
127
- - Runners, scripts, and callbacks are local programs, not an OS sandbox. Use only trusted configuration.
128
- - Repository content may be sent to your runner's model provider. Review its privacy and billing terms.
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
- Logs and run summaries live under `.git/ai-push-hooks/`. OpenCode transcripts are captured there by default. See [Security](SECURITY.md) for access and data-handling details.
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)
@@ -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-terra"
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.1",
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 pytest --with build --with pip --with \"tomli; python_version < '3.11'\" pytest tests -q",
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>=69", "wheel"]
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.1"
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"