ai-push-hooks 0.1.10 → 0.1.12

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.
@@ -6,7 +6,7 @@ skip_on_sync_branch = true
6
6
 
7
7
  [llm]
8
8
  runner = "opencode"
9
- model = "openai/gpt-5.3-codex-spark"
9
+ model = "openai/gpt-5.3-codex"
10
10
  variant = ""
11
11
  timeout_seconds = 800
12
12
  max_parallel = 2
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # ai-push-hooks
2
2
 
3
- AI-assisted pre-push workflow runner for modular repo checks, docs sync, Beads alignment, and PR creation.
3
+ `ai-push-hooks` is a configurable pre-push workflow runner for local Git repositories.
4
+
5
+ It runs module-based steps (collect, LLM, apply, exec, assert) before push so you can automate checks and optional repo maintenance tasks.
4
6
 
5
7
  ## Install
6
8
 
7
- ### Python / uv
9
+ ### Python
8
10
 
9
11
  ```bash
10
12
  uv tool install ai-push-hooks
@@ -12,7 +14,7 @@ uv tool install ai-push-hooks
12
14
  pipx install ai-push-hooks
13
15
  ```
14
16
 
15
- ### npm
17
+ ### Node (wrapper around Python package)
16
18
 
17
19
  ```bash
18
20
  npm install --save-dev ai-push-hooks
@@ -20,65 +22,198 @@ npm install --save-dev ai-push-hooks
20
22
  pnpm add -D ai-push-hooks
21
23
  ```
22
24
 
23
- The npm binary wraps the bundled Python module, so `python3` (or `python`) must be available.
25
+ Requirements:
24
26
 
25
- ## Maintainer Release
27
+ - Python 3.10+ (`python3` or `python`) is required, including npm installs.
28
+ - `opencode-cli` (or `opencode`) is required for `llm` and `apply` steps.
29
+ - `gh` is required only if you use PR creation via `gh_pr_create`.
26
30
 
27
- This repo supports automated dual publishing to PyPI and npm from a git tag.
31
+ ## Quick start
28
32
 
29
- 1. Bump `version` in `pyproject.toml` and `package.json` to the same value.
30
- 2. Commit and tag: `git tag vX.Y.Z`.
31
- 3. Push commit + tag: `git push && git push --tags`.
33
+ 1. Install the CLI.
34
+ 2. Generate a starter config:
32
35
 
33
- The GitHub Actions release workflow then:
36
+ ```bash
37
+ ai-push-hooks init --template minimal-docs
38
+ ```
34
39
 
35
- - verifies the tag matches both package versions
36
- - runs tests
37
- - builds and validates Python distributions
38
- - smoke-tests the installed Python CLI
39
- - publishes to PyPI (Trusted Publishing)
40
- - publishes to npm (`NPM_TOKEN` secret)
40
+ 3. Wire it into Lefthook:
41
41
 
42
- Required one-time setup:
42
+ ```yaml
43
+ pre-push:
44
+ commands:
45
+ ai-push-hooks:
46
+ run: ai-push-hooks hook {1} {2}
47
+ ```
43
48
 
44
- - Configure PyPI Trusted Publisher for this repository.
45
- - Add repository secret `NPM_TOKEN` with publish access to `ai-push-hooks`.
49
+ 4. Push as usual. The workflow runs automatically before push completes.
46
50
 
47
51
  ## Commands
48
52
 
49
- ```bash
50
- ai-push-hooks hook <remote-name> <remote-url>
51
- ai-push-hooks init --template minimal-docs
52
- ```
53
-
54
- `init` supports exactly one template: `minimal-docs`. Use `--force` to overwrite an existing config.
55
-
56
- ## Lefthook Usage
57
-
58
- ```yaml
59
- pre-push:
60
- commands:
61
- ai-push-hooks:
62
- run: ai-push-hooks hook {1} {2}
63
- ```
64
-
65
- For local source checkout usage, `./run.sh` works as a wrapper entrypoint.
66
-
67
- ## Configuration
68
-
69
- Put `.ai-push-hooks.toml` in the target repo root. If no file is present, built-in modular defaults are used.
70
-
71
- Prompt resolution precedence is:
72
-
73
- 1. inline `prompt`
74
- 2. `prompt_file`
75
- 3. built-in `fallback_prompt_id`
76
-
77
- Minimal docs example:
53
+ | Command | What it does |
54
+ | --- | --- |
55
+ | `ai-push-hooks hook <remote-name> <remote-url>` | Runs the configured pre-push workflow. |
56
+ | `ai-push-hooks init --template minimal-docs` | Writes `.ai-push-hooks.toml` starter config. |
57
+ | `ai-push-hooks init --template minimal-docs --force` | Overwrites an existing config file. |
58
+
59
+ ## Configuration overview
60
+
61
+ - Config file lookup order: `.ai-push-hooks.toml`, then `ai-push-hooks.toml`.
62
+ - If no config file is present, built-in defaults are used.
63
+ - File values are deep-merged over defaults.
64
+ - Prompt resolution precedence for `llm` and `apply` steps:
65
+ 1. `prompt`
66
+ 2. `prompt_file`
67
+ 3. `fallback_prompt_id`
68
+
69
+ ## Configuration reference
70
+
71
+ ### Top-level keys
72
+
73
+ | Key | Type | Required | Default |
74
+ | --- | --- | --- | --- |
75
+ | `general` | table | no | built-in values |
76
+ | `llm` | table | no | built-in values |
77
+ | `logging` | table | no | built-in values |
78
+ | `workflow` | table | yes | `{ modules = ["docs"] }` |
79
+ | `modules` | table | yes | `{ docs = ... }` |
80
+
81
+ ### `[general]`
82
+
83
+ | Key | Type | Default | Description |
84
+ | --- | --- | --- | --- |
85
+ | `enabled` | bool | `true` | Enables or disables the hook globally. |
86
+ | `allow_push_on_error` | bool | `false` | If `true`, push continues even when workflow fails. |
87
+ | `require_clean_worktree` | bool | `false` | If `true`, aborts when local changes exist. |
88
+ | `skip_on_sync_branch` | bool | `true` | If `true`, skips on sync branch/worktree context. |
89
+
90
+ ### `[llm]`
91
+
92
+ | Key | Type | Default | Description |
93
+ | --- | --- | --- | --- |
94
+ | `runner` | string | `"opencode"` | LLM runner label (currently OpenCode flow). |
95
+ | `model` | string | `"openai/gpt-5.3-codex"` | Model passed to OpenCode. |
96
+ | `variant` | string | `""` | Optional OpenCode variant. |
97
+ | `timeout_seconds` | int | `800` | Timeout per LLM invocation and related OpenCode calls. |
98
+ | `max_parallel` | int | `2` | Max concurrent read-only steps (`collect`, `llm`). |
99
+ | `json_max_retries` | int | `2` | Retry count for invalid JSON responses. |
100
+ | `invalid_json_feedback_max_chars` | int | `6000` | Max invalid output included in retry feedback. |
101
+ | `json_retry_new_session` | bool | `true` | Starts a new OpenCode session on JSON retry. |
102
+ | `delete_session_after_run` | bool | `true` | Deletes OpenCode sessions after completion. |
103
+ | `max_diff_bytes` | int | `180000` | Max bytes of git diff sent into workflow artifacts. |
104
+ | `session_title_prefix` | string | `"ai-push-hooks"` | Prefix for OpenCode session titles. |
105
+
106
+ ### `[logging]`
107
+
108
+ | Key | Type | Default | Description |
109
+ | --- | --- | --- | --- |
110
+ | `level` | string | `"status"` | Console verbosity (`status`, `info`, `debug`). |
111
+ | `jsonl` | bool | `true` | Enables JSONL event logging. |
112
+ | `dir` | string | `".git/ai-push-hooks/logs"` | Directory for `hook.jsonl`. |
113
+ | `capture_llm_transcript` | bool | `true` | Exports OpenCode session transcripts. |
114
+ | `transcript_dir` | string | `".git/ai-push-hooks/transcripts"` | Transcript export directory. |
115
+ | `summary_dir` | string | `".git/ai-push-hooks/summaries"` | Per-run summary JSON directory. |
116
+ | `print_llm_output` | bool | `false` | Mirrors raw OpenCode JSON stream to stdout. |
117
+
118
+ ### `[workflow]`
119
+
120
+ | Key | Type | Required | Description |
121
+ | --- | --- | --- | --- |
122
+ | `modules` | array of strings | yes | Ordered module IDs to run. Must contain at least one module and each ID must exist under `[modules]`. |
123
+
124
+ ### `[modules.<module_id>]`
125
+
126
+ | Key | Type | Required | Description |
127
+ | --- | --- | --- | --- |
128
+ | `enabled` | bool | no | Enables or disables that module. Default `true`. |
129
+ | `steps` | array of step tables | yes | Ordered workflow steps for the module. Must be non-empty. |
130
+
131
+ ### `[[modules.<module_id>.steps]]`
132
+
133
+ | Key | Type | Required | Applies to | Description |
134
+ | --- | --- | --- | --- | --- |
135
+ | `id` | string | yes | all step types | Unique step identifier inside the module. |
136
+ | `type` | string | yes | all step types | One of: `collect`, `llm`, `apply`, `exec`, `assert`. |
137
+ | `inputs` | array of strings | no | non-`collect` steps | Artifact references from earlier steps. |
138
+ | `output` | string | yes | `llm` | Output artifact filename (often `.json`). |
139
+ | `schema` | string | no | `llm` | Validates parsed model output shape. |
140
+ | `prompt` | string | conditional | `llm`, `apply` | Highest-priority prompt source. |
141
+ | `prompt_file` | string | conditional | `llm`, `apply` | Repo-relative or absolute prompt file path. |
142
+ | `fallback_prompt_id` | string | conditional | `llm`, `apply` | Built-in prompt ID used when no higher source resolves. |
143
+ | `collector` | string | yes | `collect` | Collector handler ID. |
144
+ | `allow_paths` | array of strings | yes | `apply` | File glob allowlist for edits. |
145
+ | `executor` | string | yes | `exec` | Exec handler ID. |
146
+ | `assertion` | string | yes | `assert` | Assertion handler ID. |
147
+ | `when_env` | string | no | any step | Runs step only when env var parses as true. |
148
+
149
+ `llm` and `apply` are promptable step types: at least one of `prompt`, `prompt_file`, or `fallback_prompt_id` must be set.
150
+
151
+ ### Supported handler and schema values
152
+
153
+ #### Collectors
154
+
155
+ | Value | Purpose |
156
+ | --- | --- |
157
+ | `docs_context` | Collects docs-related context and diff artifacts. |
158
+ | `beads_status_context` | Collects branch/beads alignment context. |
159
+ | `pr_context` | Collects PR composition context. |
160
+
161
+ #### LLM schemas
162
+
163
+ | Value | Expected payload |
164
+ | --- | --- |
165
+ | `string_array` | JSON array of strings. |
166
+ | `docs_issue_array` | JSON array of issue objects with at least `file` and `description`. |
167
+ | `beads_alignment_result` | JSON object, optionally with `commands` string array. |
168
+ | `pr_create_payload` | JSON object for PR creation fields. |
169
+
170
+ #### Exec handlers
171
+
172
+ | Value | Purpose |
173
+ | --- | --- |
174
+ | `beads_alignment` | Runs non-interactive Beads commands and writes action report when needed. |
175
+ | `gh_pr_create` | Creates (or reuses) a GitHub PR via `gh`. |
176
+
177
+ #### Assertion handlers
178
+
179
+ | Value | Purpose |
180
+ | --- | --- |
181
+ | `docs_apply_requires_manual_commit` | Fails when docs were auto-edited and still need user review/commit. |
182
+ | `beads_alignment_clean` | Fails when Beads alignment reports unresolved work. |
183
+
184
+ #### Built-in fallback prompt IDs
185
+
186
+ | Value | Purpose |
187
+ | --- | --- |
188
+ | `docs-query-basic` | Generate doc search queries from diff. |
189
+ | `docs-analysis-basic` | Identify factual documentation drift. |
190
+ | `docs-apply-basic` | Apply minimal doc fixes within allowlist. |
191
+ | `beads-plan-basic` | Build Beads alignment command/report payload. |
192
+ | `pr-compose-basic` | Draft PR title/body/base/head payload. |
193
+
194
+ ## Environment variable overrides
195
+
196
+ Boolean env parsing accepts: `1`, `true`, `yes`, `y`, `on` and `0`, `false`, `no`, `n`, `off`.
197
+
198
+ | Env var | Effect |
199
+ | --- | --- |
200
+ | `AI_PUSH_HOOKS_SKIP` | If true, sets `general.enabled = false`. |
201
+ | `AI_PUSH_HOOKS_ALLOW_PUSH_ON_ERROR` | Overrides `general.allow_push_on_error`. |
202
+ | `AI_PUSH_HOOKS_REQUIRE_CLEAN` | Overrides `general.require_clean_worktree`. |
203
+ | `AI_PUSH_HOOKS_ALLOW_DIRTY` | If true, forces `general.require_clean_worktree = false`. |
204
+ | `AI_PUSH_HOOKS_LOG_LEVEL` | Overrides `logging.level`. |
205
+ | `AI_PUSH_HOOKS_PRINT_LLM_OUTPUT` | Overrides `logging.print_llm_output`. |
206
+ | `AI_PUSH_HOOKS_MODEL` | Overrides `llm.model`. |
207
+ | `AI_PUSH_HOOKS_VARIANT` | Overrides `llm.variant`. |
208
+ | `AI_PUSH_HOOKS_TIMEOUT_SECONDS` | Overrides `llm.timeout_seconds` (integer). |
209
+
210
+ `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.
211
+
212
+ ## Example: docs + PR with opt-in creation
78
213
 
79
214
  ```toml
80
215
  [workflow]
81
- modules = ["docs"]
216
+ modules = ["docs", "pr"]
82
217
 
83
218
  [modules.docs]
84
219
  enabled = true
@@ -91,82 +226,6 @@ collector = "docs_context"
91
226
  [[modules.docs.steps]]
92
227
  id = "query"
93
228
  type = "llm"
94
- prompt = "Return a JSON array of documentation search queries. JSON only."
95
- inputs = ["collect/push.diff", "collect/changed-files.txt"]
96
- output = "queries.json"
97
- schema = "string_array"
98
-
99
- [[modules.docs.steps]]
100
- id = "analyze"
101
- type = "llm"
102
- prompt = "Return JSON issues only for factual documentation drift."
103
- inputs = ["collect/push.diff", "collect/docs-context.txt", "query/queries.json", "collect/recent-commits.txt"]
104
- output = "issues.json"
105
- schema = "docs_issue_array"
106
-
107
- [[modules.docs.steps]]
108
- id = "apply"
109
- type = "apply"
110
- prompt = "Apply the minimum Markdown fixes required."
111
- inputs = ["collect/push.diff", "collect/docs-context.txt", "analyze/issues.json"]
112
- allow_paths = ["README.md", "docs/**/*.md"]
113
-
114
- [[modules.docs.steps]]
115
- id = "assert"
116
- type = "assert"
117
- assertion = "docs_apply_requires_manual_commit"
118
- inputs = ["apply/result.json"]
119
- ```
120
-
121
- Example config that recreates the current docs + beads + PR behavior through configuration only:
122
-
123
- The sample below is runnable as-is because each `prompt_file` step also declares a built-in `fallback_prompt_id`. If you add local prompt files, they override the built-ins.
124
-
125
- ```toml
126
- [workflow]
127
- modules = ["beads", "docs", "pr"]
128
-
129
- [modules.beads]
130
- enabled = true
131
-
132
- [[modules.beads.steps]]
133
- id = "collect"
134
- type = "collect"
135
- collector = "beads_status_context"
136
-
137
- [[modules.beads.steps]]
138
- id = "plan"
139
- type = "llm"
140
- prompt_file = ".ai-push-hooks.prompts/beads-status.txt"
141
- fallback_prompt_id = "beads-plan-basic"
142
- inputs = ["collect/branch-context.txt", "collect/changed-files.txt", "collect/push.diff", "collect/commits.txt"]
143
- output = "beads-plan.json"
144
- schema = "beads_alignment_result"
145
-
146
- [[modules.beads.steps]]
147
- id = "apply"
148
- type = "exec"
149
- executor = "beads_alignment"
150
- inputs = ["plan/beads-plan.json"]
151
-
152
- [[modules.beads.steps]]
153
- id = "assert"
154
- type = "assert"
155
- assertion = "beads_alignment_clean"
156
- inputs = ["plan/beads-plan.json"]
157
-
158
- [modules.docs]
159
- enabled = true
160
-
161
- [[modules.docs.steps]]
162
- id = "collect"
163
- type = "collect"
164
- collector = "docs_context"
165
-
166
- [[modules.docs.steps]]
167
- id = "query"
168
- type = "llm"
169
- prompt_file = ".ai-push-hooks.prompts/query.txt"
170
229
  fallback_prompt_id = "docs-query-basic"
171
230
  inputs = ["collect/push.diff", "collect/changed-files.txt"]
172
231
  output = "queries.json"
@@ -175,7 +234,6 @@ schema = "string_array"
175
234
  [[modules.docs.steps]]
176
235
  id = "analyze"
177
236
  type = "llm"
178
- prompt_file = ".ai-push-hooks.prompts/analysis.txt"
179
237
  fallback_prompt_id = "docs-analysis-basic"
180
238
  inputs = ["collect/push.diff", "collect/docs-context.txt", "query/queries.json", "collect/recent-commits.txt"]
181
239
  output = "issues.json"
@@ -184,7 +242,6 @@ schema = "docs_issue_array"
184
242
  [[modules.docs.steps]]
185
243
  id = "apply"
186
244
  type = "apply"
187
- prompt_file = ".ai-push-hooks.prompts/apply.txt"
188
245
  fallback_prompt_id = "docs-apply-basic"
189
246
  inputs = ["collect/push.diff", "collect/docs-context.txt", "analyze/issues.json"]
190
247
  allow_paths = ["README.md", "docs/**/*.md"]
@@ -206,7 +263,6 @@ collector = "pr_context"
206
263
  [[modules.pr.steps]]
207
264
  id = "compose"
208
265
  type = "llm"
209
- prompt_file = ".ai-push-hooks.prompts/create-pr.txt"
210
266
  fallback_prompt_id = "pr-compose-basic"
211
267
  inputs = ["collect/pr-context.txt", "collect/changed-files.txt", "collect/push.diff", "collect/commits.txt"]
212
268
  output = "pr-draft.json"
@@ -219,16 +275,3 @@ executor = "gh_pr_create"
219
275
  when_env = "AI_PUSH_HOOKS_CREATE_PR"
220
276
  inputs = ["compose/pr-draft.json"]
221
277
  ```
222
-
223
- ## Layout
224
-
225
- - `src/ai_push_hooks/cli.py` - CLI entrypoint
226
- - `src/ai_push_hooks/config.py` - config loading and validation
227
- - `src/ai_push_hooks/engine.py` - scheduler and workflow runtime
228
- - `src/ai_push_hooks/artifacts.py` - run-directory artifact store
229
- - `src/ai_push_hooks/prompts_builtin.py` - built-in fallback prompts
230
- - `src/ai_push_hooks/modules/` - docs, beads, and PR collectors
231
- - `src/ai_push_hooks/executors/` - LLM, apply, exec, and assertion handlers
232
- - `run.sh` - source checkout wrapper
233
- - `bin/ai-push-hooks.js` - npm bin wrapper
234
- - `.ai-push-hooks.toml` - sample config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-push-hooks",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Modular AI push-hook workflow runner",
5
5
  "license": "MIT",
6
6
  "repository": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-push-hooks"
7
- version = "0.1.10"
7
+ version = "0.1.12"
8
8
  description = "Modular AI push-hook workflow runner"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -25,7 +25,7 @@ DEFAULT_CONFIG_RAW: dict[str, Any] = {
25
25
  },
26
26
  "llm": {
27
27
  "runner": "opencode",
28
- "model": "openai/gpt-5.3-codex-spark",
28
+ "model": "openai/gpt-5.3-codex",
29
29
  "variant": "",
30
30
  "timeout_seconds": 800,
31
31
  "max_parallel": 2,
@@ -67,7 +67,7 @@ skip_on_sync_branch = true
67
67
 
68
68
  [llm]
69
69
  runner = "opencode"
70
- model = "openai/gpt-5.3-codex-spark"
70
+ model = "openai/gpt-5.3-codex"
71
71
  variant = ""
72
72
  timeout_seconds = 800
73
73
  max_parallel = 2
@@ -28,7 +28,7 @@ class GeneralConfig:
28
28
  @dataclass(frozen=True)
29
29
  class LlmConfig:
30
30
  runner: str = "opencode"
31
- model: str = "openai/gpt-5.3-codex-spark"
31
+ model: str = "openai/gpt-5.3-codex"
32
32
  variant: str = ""
33
33
  timeout_seconds: int = 800
34
34
  max_parallel: int = 2