ai-push-hooks 0.1.19 → 0.2.0

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 ADDED
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and releases use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased]
6
+
7
+ No changes yet.
8
+
9
+ ## [0.2.0] - 2026-09-06
10
+
11
+ This is a beta release. Python and npm both use `0.2.0`, the canonical Git tag
12
+ is `v0.2.0`, npm uses the `beta` dist-tag, and GitHub marks the release as a
13
+ prerelease. PyPI does not provide package channels, so Python users must select
14
+ the exact `0.2.0` version rather than a PyPI beta channel.
15
+
16
+ ### Added
17
+
18
+ - Added a safe repo-local `ai-push-hooks install [--force]` delegate that
19
+ preserves hook arguments, stdin, and exit status, refuses unsafe or shared
20
+ hook targets, and never changes Git configuration.
21
+
22
+ ### Changed
23
+
24
+ - Hardened OpenCode execution with isolated configuration, denied tools for read-only work, and allowlisted temporary workspaces for apply steps.
25
+ - Added fail-closed path, symlink, file-mode, artifact, Git metadata, and concurrent-change validation around model-assisted edits.
26
+ - Restricted prompt files and runtime storage to validated repository-owned locations and private filesystem permissions.
27
+ - Updated the default OpenCode model and release workflow dependencies.
28
+ - Added CI/package validation, community health files, public package metadata, and post-publication GitHub Releases.
29
+ - Documented the shortest installed wheel/npm onboarding path, the full-featured
30
+ Lefthook alternative, safe removal, failure/fail-open/skip semantics, and the
31
+ Python requirement for npm installs.
32
+ - Recorded a limited synthetic provider preview: OpenCode 1.18.29 with
33
+ `opencode/muse-spark-1.3-contributor-free`, which was listed as free at test
34
+ time; synthetic query/analyze passed at zero reported cost. Free-model
35
+ availability can change. This does not claim all providers/models or live
36
+ apply.
37
+
38
+ ### Compatibility and limitations
39
+
40
+ - The candidate evidence includes macOS Darwin 24.6.0 arm64 with Python
41
+ 3.12.13, Node 24.19.0, npm 10.9.2, Git 2.55.0, Ruff 0.13.3, OpenCode
42
+ 1.18.29, `gh` 2.93.0, and `bd` 1.2.2. Python 3.10–3.13 and Node 18+ remain
43
+ declared ranges; Windows has no native beta evidence and is untested/not
44
+ supported for this beta.
45
+ - The published `0.1.19` registry artifacts retain their historical provenance;
46
+ this release does not republish, move, or overwrite that version.
47
+
48
+ ## [0.1.19] - 2026-07-13
49
+
50
+ ### Added
51
+
52
+ - Added configurable base-branch support for pull-request workflows.
53
+ - Added packed npm-package smoke coverage.
54
+
55
+ ### Fixed
56
+
57
+ - Added Python 3.10 TOML compatibility and fixed npm-local hook command resolution.
58
+ - Preserved Git porcelain paths and protected pre-existing dirty allowlisted files during apply steps.
59
+ - Clarified module-local artifact references and standardized the repository hook integration.
60
+
61
+ [Unreleased]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.2.0...HEAD
62
+ [0.2.0]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.1.19...v0.2.0
63
+ [0.1.19]: https://github.com/shanebishop1/ai-push-hooks/compare/v0.1.18...v0.1.19
package/README.md CHANGED
@@ -1,108 +1,342 @@
1
1
  # ai-push-hooks
2
2
 
3
- `ai-push-hooks` is a configurable pre-push workflow runner for local Git repositories.
3
+ `ai-push-hooks` catches repository drift before it reaches a remote. It turns `git push` into a configurable workflow that can inspect the exact outgoing diff, ask an LLM for structured findings, apply narrowly allowlisted documentation fixes, run deterministic actions, and block the push until changes are reviewed and committed.
4
4
 
5
- It runs module-based steps (collect, LLM, apply, exec, assert) before push so you can automate checks and optional repo maintenance tasks.
5
+ Use it to keep docs aligned with code, check branch/task consistency, or prepare pull requests without replacing your project's ordinary lint, test, and build checks. Workflows are assembled from `collect`, `llm`, `apply`, `exec`, and `assert` steps and default to failing closed.
6
6
 
7
- ## Install
7
+ ## Quick start: repo-local hook
8
8
 
9
- ### Mise (recommended for repository hooks)
9
+ ### Prerequisites
10
10
 
11
- Pin the currently published release in the consuming repository:
11
+ - [Git](https://git-scm.com/downloads) and a POSIX shell for the generated hook.
12
+ - [Python 3.10–3.13](https://www.python.org/downloads/). Python is required even when installing the npm wrapper. The wrapper probes Python 3.14, 3.13, 3.12, 3.11, 3.10, then `python`; the 3.14 probe is not a beta support claim. Python 3.10 additionally needs the `tomli` package available to that interpreter.
13
+ - [OpenCode](https://opencode.ai/docs/#install) is optional for workflows that use only deterministic steps, but the `minimal-docs` starter uses `llm` and `apply`. Those steps also need a provider/model and authentication; check with `opencode auth list`.
14
+ - [GitHub CLI (`gh`)](https://cli.github.com/manual/installation) is optional and needed only for `gh_pr_create`.
15
+ - [Beads (`bd`)](https://github.com/steveyegge/beads) is optional and needed only for Beads alignment steps.
16
+ - [Lefthook](https://lefthook.dev/installation/) and [Mise](https://mise.jdx.dev/getting-started.html) are optional hook-manager/tool-version alternatives described below.
17
+
18
+ ### Install ai-push-hooks
19
+
20
+ This is the shortest path. It installs the package, writes the exact starter
21
+ configuration filename, and installs a repository-local `pre-push` delegate:
22
+
23
+ ```bash
24
+ python -m pip install ai-push-hooks==0.2.0
25
+ ai-push-hooks init --template minimal-docs
26
+ ai-push-hooks install
27
+ ```
28
+
29
+ The installed-artifact tests exercise the equivalent install and hook sequence
30
+ with wheel and npm artifacts in disposable repositories, including a real
31
+ local push that succeeds and a second push that is rejected without changing
32
+ the bare remote. The first command above can instead be `uv tool install
33
+ ai-push-hooks` or `pipx install ai-push-hooks` when using an isolated
34
+ application environment.
35
+
36
+ These instructions describe the `0.2.0` beta release. npm exposes it through
37
+ the `beta` dist-tag; PyPI has no separate beta channel, so Python installation
38
+ must select the exact `0.2.0` version. Published `0.1.19` artifacts retain
39
+ historical provenance and must not be assumed to contain this release's
40
+ `install` command.
41
+
42
+ The starter config currently writes `openai/gpt-5.6-terra` as its model value;
43
+ availability and authentication are provider-dependent. To use a free OpenCode
44
+ Zen model, run `opencode models opencode`, choose a model currently marked
45
+ free, and set `[llm].model` to that full identifier. Free-model availability
46
+ changes over time, so do not treat the model used in the recorded preview below
47
+ as a permanent recommendation or default.
48
+
49
+ For npm or pnpm, install and invoke the wrapper locally:
50
+
51
+ ```bash
52
+ npm install --save-dev ai-push-hooks@beta
53
+ npx --no-install ai-push-hooks init --template minimal-docs
54
+ npx --no-install ai-push-hooks install
55
+ # or: pnpm add -D ai-push-hooks && pnpm exec ai-push-hooks install
56
+ ```
57
+
58
+ Use `ai-push-hooks@0.2.0` instead of `@beta` when an exact npm version pin is
59
+ required.
60
+
61
+ The npm package does not contain a Python runtime. Ensure the Python
62
+ requirement above is on `PATH` for the hook process; on Python 3.10 install
63
+ `tomli` in that same environment. `npx --no-install` avoids an accidental
64
+ registry lookup or global-package fallback.
65
+
66
+ `install` resolves Git's effective hook path without changing Git
67
+ configuration. It accepts `install [--force]`, creates missing repository-local
68
+ hook directories, writes atomically, and makes the delegate executable. An
69
+ existing regular hook is refused unless `--force` is explicit; symlinks,
70
+ reparse points, FIFOs, directories, external/shared `core.hooksPath` values,
71
+ and linked-worktree shared hooks are refused even with `--force`. `--force`
72
+ replaces a regular existing hook; it does not merge or back it up. Inspect a
73
+ hook before using it and keep a copy if it contains work you need.
74
+
75
+ The generated delegate preserves Git's hook arguments, standard input, and
76
+ exit status. When installed through the local npm wrapper, it records the
77
+ absolute Node and package-script paths, so Git does not need
78
+ `node_modules/.bin` on `PATH`; keep that local package installation in place.
79
+ Python console installs similarly record their executable when it can be
80
+ resolved. The fallback delegate fails clearly with status 127 if
81
+ `ai-push-hooks` is not on the hook process's `PATH`.
82
+
83
+ ### Mise (pinned tool option)
84
+
85
+ Pin an approved published release in the consuming repository:
12
86
 
13
87
  ```bash
14
- mise use npm:ai-push-hooks@0.1.19
88
+ mise use npm:ai-push-hooks@0.2.0
15
89
  ```
16
90
 
17
91
  This adds the following project-level tool entry to `mise.toml` and installs it:
18
92
 
19
93
  ```toml
20
94
  [tools]
21
- "npm:ai-push-hooks" = "0.1.19"
95
+ "npm:ai-push-hooks" = "0.2.0"
22
96
  ```
23
97
 
24
98
  After checking in `mise.toml`, other contributors can install the pinned tool with `mise install`.
25
99
 
26
- ### Python
100
+ ### OpenCode isolation limits
27
101
 
28
- ```bash
29
- uv tool install ai-push-hooks
30
- # or
31
- pipx install ai-push-hooks
32
- ```
102
+ OpenCode runs in `--pure` mode with project configuration disabled, isolated home/config/cache/state directories, sharing disabled, and an ai-push-hooks-owned custom agent configuration. Read-only steps run in an empty scratch directory, receive only hook-owned artifacts through `--file`, and have every tool denied. Apply steps run against a private temporary workspace containing only unignored regular files matching `allow_paths`; their agent permits only reads and allowlisted edits in that workspace. Casefolded, Unicode-normalized `.git` and `AGENTS.md` paths are always protected.
33
103
 
34
- ### Node (wrapper around Python package)
104
+ Global and repository OpenCode instructions, custom agents, MCP servers, formatters, LSP configuration, sharing, and plugins are not inherited. The existing XDG data directory is retained for OpenCode authentication/session state, and recognized provider environment variables are forwarded. Custom providers defined only in global OpenCode configuration are therefore unsupported; use a built-in provider with OpenCode auth state or environment credentials.
35
105
 
36
- ```bash
37
- npm install --save-dev ai-push-hooks
38
- # or
39
- pnpm add -D ai-push-hooks
40
- ```
106
+ After OpenCode session finalization, apply verifies that the Git-visible checkout, index, current-worktree control state, and critical shared `HEAD`/config/packed-refs/refs/hooks state still match their baselines. Pre-existing symlinks in monitored Git metadata fail closed before OpenCode runs, and symlinks introduced during execution fail before propagation. Apply then preflights every destination against its exact baseline type, content digest, and mode before propagating anything, performs atomic file replacement, and verifies the resulting checkout and protected Git state again. Safe existing ordinary `rwx` modes are preserved, existing special bits are stripped, new or group/world-writable modes become owner-only, and staged files carrying setuid/setgid/sticky bits are rejected before any propagation. Hook-owned runtime files default to `0600` and runtime directories to `0700`.
41
107
 
42
- Requirements:
108
+ These controls are OpenCode permission and workspace isolation, not an operating-system sandbox. Compare-and-swap preflight minimizes lost updates but cannot make the interval between preflight and filesystem replacement atomic against an independent local process. Ignored worktree trees, Git object/LFS stores, shared reflogs, and metadata belonging only to other linked worktrees are intentionally excluded from bounded snapshots; direct changes there may not be detected. Critical shared refs/config/hooks remain monitored. Automatic rollback is avoided so pre-existing user changes are not overwritten.
43
109
 
44
- - [Python 3.10+](https://www.python.org/downloads/) (`python3` or `python`) is required. The npm package is a wrapper around the Python CLI, so Python is still required when installed through npm/pnpm.
45
- - [OpenCode](https://github.com/anomalyco/opencode) is required for `llm` and `apply` steps.
46
- - [GitHub CLI (`gh`)](https://cli.github.com/manual/installation) is required only if you use PR creation via `gh_pr_create`.
110
+ ## Full-featured alternative: Lefthook
47
111
 
48
- ## Quick start
112
+ Use Lefthook when the repository needs several hook commands, shared hook
113
+ configuration, or repository-managed installation. Keep one final
114
+ `ai-push-hooks hook` call and forward Git's pre-push input:
49
115
 
50
- 1. Add the pinned Mise tool by following [Mise installation](#mise-recommended-for-repository-hooks) above.
51
- 2. Generate a starter config:
116
+ Create `scripts/hooks/pre-push-runner.sh` in the consuming repository with:
52
117
 
53
- ```bash
54
- mise exec -- ai-push-hooks init --template minimal-docs
55
- ```
118
+ ```yaml
119
+ pre-push:
120
+ commands:
121
+ repository-pre-push:
122
+ run: bash scripts/hooks/pre-push-runner.sh {1} {2}
123
+ use_stdin: true
124
+ ```
56
125
 
57
- 3. Add the single repository-owned runner at `scripts/hooks/pre-push-runner.sh`:
126
+ The runner captures stdin before deterministic checks consume it, then replays
127
+ it to the tool:
58
128
 
59
- ```bash
60
- #!/usr/bin/env bash
61
- set -euo pipefail
129
+ ```bash
130
+ #!/usr/bin/env bash
131
+ set -euo pipefail
132
+ remote_name="${1:-}"
133
+ remote_url="${2:-}"
134
+ push_stdin="$(mktemp)"
135
+ trap 'rm -f "$push_stdin"' EXIT
136
+ cat >"$push_stdin"
137
+ git diff --check
138
+ mise exec -- ai-push-hooks hook "$remote_name" "$remote_url" <"$push_stdin"
139
+ ```
62
140
 
63
- remote_name="${1:-}"
64
- remote_url="${2:-}"
65
- push_stdin="$(mktemp)"
66
- trap 'rm -f "$push_stdin"' EXIT
67
- cat >"$push_stdin"
141
+ Install and verify Lefthook in the consuming repository:
68
142
 
69
- # Run deterministic quality checks first. Replace these with the repository's checks.
70
- npm run lint
71
- npm test
143
+ ```bash
144
+ lefthook version
145
+ chmod +x scripts/hooks/pre-push-runner.sh
146
+ lefthook install
147
+ test -x "$(git rev-parse --git-path hooks/pre-push)" && echo "pre-push hook installed"
148
+ ```
72
149
 
73
- # Keep ai-push-hooks as the single final phase and replay Git's pre-push input.
74
- mise exec -- ai-push-hooks hook "$remote_name" "$remote_url" <"$push_stdin"
75
- ```
150
+ `use_stdin: true` forwards Git's ref-update stream; `{1}` and `{2}` are the
151
+ remote name and URL. This is the full-featured alternative to the small
152
+ repo-local `install` delegate. Do not install both managers for the same hook
153
+ unless their chaining is deliberate. The repository's deterministic installed
154
+ hook coverage validates the delegate contract; Lefthook remains the tested
155
+ repository-owned integration pattern and should be exercised with the commands
156
+ above in each consuming repository.
157
+
158
+ Configure modules and steps in the [configuration reference](#configuration-reference), then push as usual. If `apply` edits an allowlisted file, the starter assertion blocks that push so you can inspect `git diff`, validate, commit the approved edit, and push again.
159
+
160
+ ## Troubleshooting
161
+
162
+ - **`opencode is required but not installed`:** install OpenCode and ensure `opencode` (or `opencode-cli`) is on `PATH` for the Git hook process.
163
+ - **Provider/model authentication fails:** run `opencode auth list`, authenticate a built-in provider, and verify `[llm].model`. Project/global custom-provider configuration is intentionally not loaded; see [OpenCode isolation limits](#opencode-isolation-limits).
164
+ - **The hook does not run:** rerun `lefthook install`, check `git config --get core.hooksPath`, and verify the pre-push path with the command above.
165
+ - **The push is blocked after docs changed:** this is the expected edit-review-commit flow. Review `git diff`, validate and commit the changes, then push again.
166
+ - **Find logs or transcripts:** inspect `.git/ai-push-hooks/logs`, `.git/ai-push-hooks/summaries`, and (when enabled) `.git/ai-push-hooks/transcripts`.
167
+ - **Temporarily skip intentionally:** set `AI_PUSH_HOOKS_SKIP=1` for one invocation. Treat bypasses as an explicit project-policy decision.
168
+
169
+ ### Failure, fail-open, and skip semantics
170
+
171
+ - Fail closed is the default: configuration, collection, model, apply, exec,
172
+ and assertion errors return nonzero and block the push. A rejected push does
173
+ not update the remote.
174
+ - Set `[general].allow_push_on_error = true`, or use
175
+ `AI_PUSH_HOOKS_ALLOW_PUSH_ON_ERROR=1`, only as an explicit fail-open policy.
176
+ The error is still logged and a warning is emitted, but the push is allowed.
177
+ - `AI_PUSH_HOOKS_SKIP=1` exits before repository/config access and allows that
178
+ invocation. `general.enabled = false` is a configured disable after config
179
+ loading/log initialization. Neither path validates or runs the workflow.
180
+ - `skip_on_sync_branch = true` can skip configured sync-branch context, and
181
+ step/module conditions can report a normal skip. A skip is not a successful
182
+ model or apply result.
183
+
184
+ Git supplies hook stdin as one line per ref update:
185
+ `<local-ref> <local-object-id> <remote-ref> <remote-object-id>`. New remote
186
+ refs use a zero object ID (40 zeroes for the tested SHA-1 repositories); deletion
187
+ uses `(delete)` and a zero local ID. The hook rejects malformed input, unknown
188
+ objects, and multiple non-deleted branch updates rather than silently choosing
189
+ a branch.
190
+
191
+ ### Safe removal of the generated hook
192
+
193
+ `install` has no uninstall command and never edits Git configuration. To remove
194
+ only its delegate, first inspect the effective target and content:
76
195
 
77
- Make the runner executable:
196
+ ```bash
197
+ hook="$(git rev-parse --git-path hooks)/pre-push"
198
+ if [ -f "$hook" ]; then sed -n '1,20p' "$hook"; fi
199
+ ```
78
200
 
79
- ```bash
80
- chmod +x scripts/hooks/pre-push-runner.sh
81
- ```
201
+ For an exact check before removal, use the installed Python package rather than
202
+ matching only a filename:
82
203
 
83
- 4. Configure Lefthook to invoke only that runner in `lefthook.yml`:
204
+ ```bash
205
+ python - "$hook" <<'PY'
206
+ import pathlib
207
+ import sys
208
+
209
+ from ai_push_hooks.install import pre_push_hook_script
210
+
211
+ path = pathlib.Path(sys.argv[1])
212
+ if not path.is_file() or path.is_symlink():
213
+ raise SystemExit("not a regular hook; nothing removed")
214
+ if path.read_text(encoding="utf-8") != pre_push_hook_script():
215
+ raise SystemExit("hook is not the ai-push-hooks delegate; nothing removed")
216
+ path.unlink()
217
+ print(f"removed {path}")
218
+ PY
219
+ ```
220
+
221
+ Remove it only after confirming it is the generated `ai-push-hooks` delegate,
222
+ not a Lefthook or shared hook. If it contains other commands, preserve it and
223
+ remove only the documented ai-push-hooks entry instead. Do not run `rm` on an
224
+ uninspected `pre-push` path.
225
+
226
+ ## Security and privacy
227
+
228
+ Repository diffs, selected context, and prompts may be sent by OpenCode to the configured model provider. Review that provider's data-handling terms and do not include secrets in commits or prompts. Transcripts are stored locally by default under `.git/ai-push-hooks/transcripts`; sharing is disabled and OpenCode sessions are deleted after each run by default. If transcript export fails, the run warns and still follows the configured deletion policy; do not assume an export exists. See [SECURITY.md](SECURITY.md) for reporting, the threat model, data handling, and sandbox limitations.
229
+
230
+ ### BR-06 provider evidence (limited preview)
231
+
232
+ At the time of the recorded synthetic provider run, OpenCode **1.18.29** listed
233
+ `opencode/muse-spark-1.3-contributor-free` as free. Synthetic `query` and
234
+ `analyze` steps passed at **zero reported cost**. This is evidence for that
235
+ specific OpenCode/model path and synthetic inputs only; it does not establish
236
+ that every model, provider, authentication mode, or live `apply` operation is
237
+ compatible. Discover the current catalog with `opencode models opencode` and
238
+ verify pricing before each run; free models can be renamed, replaced, or
239
+ removed. The tested identifier is historical evidence, not a new default or a
240
+ promise of future availability. Review provider billing, retention, and
241
+ transmission terms before using repository content.
242
+
243
+ ## Tested matrix and beta boundary
244
+
245
+ The current candidate was exercised on **macOS Darwin 24.6.0 arm64** with
246
+ Python **3.12.13**, Node **24.19.0**, npm **10.9.2**, Git **2.55.0**, Ruff
247
+ **0.13.3**, OpenCode **1.18.29**, `gh` **2.93.0**, and `bd` **1.2.2**. The
248
+ wheel and packed npm installed-hook tests use disposable repositories, full
249
+ 40-character Git object IDs, a local bare remote, and a minimal PATH. The
250
+ Lefthook **2.1.9** was also run in a disposable repository to install a
251
+ pre-push hook and verify argument/stdin forwarding. The real OpenCode contract
252
+ also passed in a Linux arm64 Docker container launched from this macOS host.
253
+ Python 3.10, 3.11, and 3.13 and Node 18 were not available in this validation
254
+ environment and are not claimed as locally run; their jobs remain part of the
255
+ GitHub Actions matrix.
256
+
257
+ Prior recorded BR evidence also covers the real OpenCode 1.18.29 CLI with a
258
+ loopback mock provider inside a Linux Docker runtime with networking disabled.
259
+ That is mock-provider permission/workspace evidence, not live-provider or
260
+ operating-system-sandbox evidence.
261
+
262
+ Validation results for this snapshot are **273 passed, 1 skipped** for the full
263
+ Python suite, **8 passed** for install-unit coverage, **2 passed** for installed
264
+ wheel and npm hook coverage against the exact release artifacts, a passing
265
+ `npm run test:npm-pack`, and a passing Lefthook 2.1.9 disposable
266
+ argument/stdin-forwarding check. The Docker contract passed against the real
267
+ OpenCode 1.18.29 CLI with runtime networking disabled and a loopback mock
268
+ provider.
269
+
270
+ Python 3.10–3.13 and Node 18+ remain the declared compatibility ranges, not a
271
+ claim that every patch/platform combination has passed. Windows has no native
272
+ beta evidence and is explicitly untested/not supported for this beta. The
273
+ generated hook and documented runner require a POSIX shell; defensive path
274
+ handling is not Windows validation.
275
+
276
+ ## Synthetic demo and evidence
277
+
278
+ For a no-secrets, no-external-model-call wiring/permission demo, run:
279
+
280
+ ```bash
281
+ bash scripts/opencode-contract-smoke.sh
282
+ ```
84
283
 
85
- ```yaml
86
- pre-push:
87
- commands:
88
- repository-pre-push:
89
- run: bash scripts/hooks/pre-push-runner.sh {1} {2}
90
- use_stdin: true
91
- ```
284
+ It builds a disposable image, starts only an in-process loopback mock provider,
285
+ and drives real OpenCode 1.18.29 through a synthetic repository. Runtime
286
+ networking is disabled, so no external model call is possible; the initial
287
+ Docker build/setup may need network access to fetch its pinned inputs. It shows
288
+ the allowlisted `README.md` edit, denied outside/protected edits, and unchanged
289
+ protected Git metadata. This is **wiring and permission evidence only**, not a
290
+ live-provider demo or OS-sandbox claim; it exits 2 when Docker is unavailable.
291
+
292
+ For installed hook wiring without any model/provider call:
293
+
294
+ ```bash
295
+ python -m pytest -q tests/test_installed_hook_e2e.py
296
+ npm run test:npm-pack
297
+ ```
92
298
 
93
- `use_stdin: true` forwards Git's ref-update stream to the runner. The runner captures it before quality checks consume or close standard input, then replays it to `ai-push-hooks hook`. Lefthook's `{1}` and `{2}` are the remote name and remote URL. Keep all repository checks in this runner and keep the one `ai-push-hooks hook` call last so failures propagate and block the push.
94
- 5. Configure modules and steps in [Configuration reference](#configuration-reference).
95
- 6. Push as usual. The workflow runs automatically before push completes.
299
+ These disposable fixtures show a successful local push followed by a
300
+ fail-closed rejection. The actual BR-06 provider preview is documented above;
301
+ it used synthetic query/analyze inputs and must not be presented as fabricated
302
+ provider output or as evidence for live `apply`.
303
+
304
+ ## Portfolio case study: bounded documentation maintenance
305
+
306
+ **Problem.** A pushed code change can make repository documentation stale
307
+ before review notices it. The hook inspects the outgoing ref range rather than
308
+ the checked-out branch alone.
309
+
310
+ **Architecture and tradeoffs.** Deterministic `collect` steps establish diff,
311
+ changed-file, and repository context before `llm` query/analyze steps. An
312
+ `apply` step receives a private workspace and a narrow docs allowlist; the
313
+ assertion then blocks the push for human review and commit. `exec` and `assert`
314
+ remain available for deterministic repository actions. This ordering limits
315
+ model scope without pretending to provide an OS sandbox.
316
+
317
+ **Evidence.** The real OpenCode 1.18.29 mock-provider contract found a
318
+ version-specific permission mapping (`write` requests `edit`) and covers
319
+ allowlisted propagation plus protected Git metadata. The BR-06 live synthetic
320
+ preview used `opencode/muse-spark-1.3-contributor-free` for query/analyze at
321
+ zero reported cost; it is not evidence for all providers/models or live apply.
322
+ Installed wheel/npm tests cover hook wiring, a successful local push, and a
323
+ fail-closed rejection. The Docker contract was not rerun in this environment.
324
+
325
+ **Limitations.** Provider availability, billing, retention, OS-level access,
326
+ ignored trees, shared metadata, and independent filesystem races remain outside
327
+ the product guarantee. Review, validate, and commit any proposed edit; this is
328
+ not unattended autonomous maintenance.
96
329
 
97
330
  ## Commands
98
331
 
99
- If installed as a local npm/pnpm dependency, run commands with `npx` or `pnpm exec`.
332
+ If installed as a local npm/pnpm dependency, run commands with `npx --no-install` or `pnpm exec`.
100
333
 
101
334
  | Command | What it does |
102
335
  | --- | --- |
103
336
  | `ai-push-hooks hook <remote-name> <remote-url>` | Runs the configured pre-push workflow. |
104
337
  | `ai-push-hooks init --template minimal-docs` | Writes `ai-push-hooks.toml` starter config. |
105
338
  | `ai-push-hooks init --template minimal-docs --force` | Overwrites an existing config file. |
339
+ | `ai-push-hooks install [--force]` | Installs a repo-local executable pre-push delegate; `--force` replaces a regular existing hook. |
106
340
 
107
341
  ## Configuration overview
108
342
 
@@ -139,7 +373,7 @@ If installed as a local npm/pnpm dependency, run commands with `npx` or `pnpm ex
139
373
  | Key | Type | Default | Description |
140
374
  | --- | --- | --- | --- |
141
375
  | `runner` | string | `"opencode"` | LLM runner label (currently OpenCode flow). |
142
- | `model` | string | `"openai/gpt-5.5"` | Model passed to OpenCode. |
376
+ | `model` | string | `"openai/gpt-5.6-terra"` | Model passed to OpenCode. |
143
377
  | `variant` | string | `""` | Optional OpenCode variant. |
144
378
  | `timeout_seconds` | int | `800` | Timeout per LLM invocation and related OpenCode calls. |
145
379
  | `max_parallel` | int | `2` | Max concurrent read-only steps (`collect`, `llm`). |
@@ -185,7 +419,7 @@ If installed as a local npm/pnpm dependency, run commands with `npx` or `pnpm ex
185
419
  | `output` | string | yes | `llm` | Output artifact filename (often `.json`). |
186
420
  | `schema` | string | no | `llm` | Validates parsed model output shape. |
187
421
  | `prompt` | string | conditional | `llm`, `apply` | Highest-priority prompt source. |
188
- | `prompt_file` | string | conditional | `llm`, `apply` | Repo-relative or absolute prompt file path. |
422
+ | `prompt_file` | string | conditional | `llm`, `apply` | Repo-relative prompt file path; absolute, traversing, and symlinked paths are rejected. |
189
423
  | `fallback_prompt_id` | string | conditional | `llm`, `apply` | Built-in prompt ID used when no higher source resolves. |
190
424
  | `collector` | string | yes | `collect` | Collector handler ID. |
191
425
  | `allow_paths` | array of strings | yes | `apply` | File glob allowlist for edits. |
package/SECURITY.md ADDED
@@ -0,0 +1,35 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please do not open a public issue for a suspected vulnerability. Report it privately with a [GitHub Security Advisory](https://github.com/shanebishop1/ai-push-hooks/security/advisories/new), including affected versions, impact, reproduction details, and any suggested mitigation. Remove credentials, proprietary source, and unneeded transcript content. Maintainers will acknowledge the report, investigate it, and coordinate disclosure and a fix through the advisory. Only the latest release is actively supported with security fixes.
6
+
7
+ ## Threat model and data handling
8
+
9
+ ai-push-hooks treats repository content, Git paths and metadata, configuration, model output, and concurrent local filesystem changes as potentially unsafe. Its controls constrain model-visible inputs and apply destinations, protect Git metadata and instruction files, validate filesystem state before propagation, and fail closed by default. They are designed to prevent accidental or model-directed changes outside configured boundaries, not to protect against a malicious user or process with the same operating-system permissions.
10
+
11
+ OpenCode is a separate local process and communicates with the model provider selected in `[llm].model`. Diffs, changed-file context, prompts, and step artifacts can therefore leave the machine under that provider's terms. ai-push-hooks forwards recognized provider credential environment variables and retains OpenCode's existing authentication data directory, but disables sharing and does not inherit project/global plugins, MCP servers, instructions, or custom-provider configuration. Do not commit secrets, and review provider retention and privacy policies before use on sensitive repositories.
12
+
13
+ Hook logs, summaries, run artifacts, and transcripts are stored locally under `.git/ai-push-hooks/` with private runtime permissions. Transcript capture defaults to **on** at `.git/ai-push-hooks/transcripts`; set `logging.capture_llm_transcript = false` to disable it. OpenCode session deletion defaults to on, but provider-side retention is controlled by the provider.
14
+
15
+ Transcript export is best effort. If export fails or produces no usable output,
16
+ the run emits a warning and still applies the configured session-deletion
17
+ policy; it does not claim that a transcript was captured. A provider may have
18
+ already received the request even when local export fails. Do not use local
19
+ transcript files as proof that provider-side data was deleted.
20
+
21
+ ## Sandbox limitation
22
+
23
+ OpenCode permissions and temporary-workspace isolation are **not an operating-system sandbox**. The process retains the invoking user's OS-level access, and bounded snapshots cannot observe every ignored path, Git object/LFS store, shared reflog, other linked-worktree metadata, or race with an independent local process. Use an OS sandbox, container, VM, or dedicated low-privilege account when stronger isolation is required. See the README's [OpenCode isolation limits](README.md#opencode-isolation-limits) for the detailed guarantees and exclusions.
24
+
25
+ ## Tested security boundary
26
+
27
+ The beta evidence covers the real OpenCode **1.18.29** permission contract in a
28
+ no-network Docker fixture and a limited synthetic provider run using a model
29
+ that was listed as free at test time,
30
+ `opencode/muse-spark-1.3-contributor-free`. Free-model catalogs and pricing can
31
+ change; verify the current catalog before use. This evidence does not cover
32
+ every provider, model, authentication mode, or live `apply` path. Windows has
33
+ no native beta evidence. Treat the generated hook's repository-local path
34
+ checks and the Lefthook runner as integration safeguards, not isolation
35
+ boundaries.
@@ -7,7 +7,7 @@ base_branch = "main"
7
7
 
8
8
  [llm]
9
9
  runner = "opencode"
10
- model = "openai/gpt-5.5"
10
+ model = "openai/gpt-5.6-terra"
11
11
  variant = ""
12
12
  timeout_seconds = 800
13
13
  max_parallel = 2
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawnSync } = require('node:child_process');
4
+ const fs = require('node:fs');
4
5
  const path = require('node:path');
5
6
 
6
7
  const packageRoot = path.resolve(__dirname, '..');
7
8
  const srcDir = path.join(packageRoot, 'src');
8
9
  const args = ['-m', 'ai_push_hooks', ...process.argv.slice(2)];
10
+ const pythonCommands = ['python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3', 'python'];
9
11
 
10
12
  function buildEnv() {
11
13
  const env = { ...process.env };
14
+ env.AI_PUSH_HOOKS_NODE_EXECUTABLE = process.execPath;
15
+ env.AI_PUSH_HOOKS_NODE_SCRIPT = fs.realpathSync(__filename);
12
16
  env.PYTHONPATH = env.PYTHONPATH
13
17
  ? `${srcDir}${path.delimiter}${env.PYTHONPATH}`
14
18
  : srcDir;
@@ -22,14 +26,25 @@ function run(command) {
22
26
  });
23
27
  }
24
28
 
25
- let result = run('python3');
26
- if (result.error && result.error.code === 'ENOENT') {
27
- result = run('python');
29
+ function canRunPackage(command) {
30
+ const check = spawnSync(
31
+ command,
32
+ [
33
+ '-c',
34
+ 'import sys; assert sys.version_info >= (3, 10); __import__("tomllib" if sys.version_info >= (3, 11) else "tomli")',
35
+ ],
36
+ { stdio: 'ignore' },
37
+ );
38
+ return check.status === 0;
28
39
  }
29
40
 
30
- if (result.error && result.error.code === 'ENOENT') {
31
- console.error('[ai-push-hooks] python3/python is required but not installed.');
41
+ const pythonCommand = pythonCommands.find(canRunPackage);
42
+ if (!pythonCommand) {
43
+ console.error(
44
+ '[ai-push-hooks] Python 3.11+ is required for npm installs. Python 3.10 can be used if tomli is installed.',
45
+ );
32
46
  process.exit(1);
33
47
  }
34
48
 
49
+ const result = run(pythonCommand);
35
50
  process.exit(typeof result.status === 'number' ? result.status : 1);
package/package.json CHANGED
@@ -1,23 +1,44 @@
1
1
  {
2
2
  "name": "ai-push-hooks",
3
- "version": "0.1.19",
4
- "description": "Modular AI push-hook workflow runner",
3
+ "version": "0.2.0",
4
+ "description": "Run structured AI-assisted checks and allowlisted maintenance before git push",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "Shane Bishop"
8
+ },
9
+ "homepage": "https://github.com/shanebishop1/ai-push-hooks#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/shanebishop1/ai-push-hooks/issues"
12
+ },
13
+ "keywords": [
14
+ "git",
15
+ "git-hooks",
16
+ "pre-push",
17
+ "ai",
18
+ "llm",
19
+ "opencode",
20
+ "lefthook",
21
+ "documentation",
22
+ "developer-tools",
23
+ "automation"
24
+ ],
6
25
  "repository": {
7
26
  "type": "git",
8
- "url": "https://github.com/shanebishop1/ai-push-hooks"
27
+ "url": "git+https://github.com/shanebishop1/ai-push-hooks.git"
9
28
  },
10
29
  "bin": {
11
30
  "ai-push-hooks": "bin/ai-push-hooks.js"
12
31
  },
13
32
  "scripts": {
14
- "test": "uv run --with pytest pytest tests -q",
33
+ "test": "uv run --no-project --with pytest pytest tests -q",
15
34
  "test:npm-pack": "node tests/npm-pack-smoke.mjs"
16
35
  },
17
36
  "files": [
18
37
  "bin",
19
38
  "src/**/*.py",
20
39
  "README.md",
40
+ "CHANGELOG.md",
41
+ "SECURITY.md",
21
42
  "run.sh",
22
43
  "pyproject.toml",
23
44
  "LICENSE",