ai-push-hooks 0.1.17 → 0.1.18
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/README.md +26 -13
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/ai_push_hooks/cli.py +2 -4
- package/src/ai_push_hooks/config.py +12 -98
- package/src/ai_push_hooks/executors/llm.py +1 -1
package/README.md
CHANGED
|
@@ -24,20 +24,31 @@ pnpm add -D ai-push-hooks
|
|
|
24
24
|
|
|
25
25
|
Requirements:
|
|
26
26
|
|
|
27
|
-
- [Python 3.10+](https://www.python.org/downloads/) (`python3` or `python`) is required,
|
|
28
|
-
- [OpenCode
|
|
27
|
+
- [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.
|
|
28
|
+
- [OpenCode](https://github.com/anomalyco/opencode) is required for `llm` and `apply` steps.
|
|
29
29
|
- [GitHub CLI (`gh`)](https://cli.github.com/manual/installation) is required only if you use PR creation via `gh_pr_create`.
|
|
30
30
|
|
|
31
31
|
## Quick start
|
|
32
32
|
|
|
33
|
-
1. Install the
|
|
33
|
+
1. Install by following the steps above.
|
|
34
34
|
2. Generate a starter config:
|
|
35
35
|
|
|
36
|
+
Python tool install (`uv tool` / `pipx`):
|
|
37
|
+
|
|
36
38
|
```bash
|
|
37
39
|
ai-push-hooks init --template minimal-docs
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
npm/pnpm local install:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx ai-push-hooks init --template minimal-docs
|
|
46
|
+
# or
|
|
47
|
+
pnpm exec ai-push-hooks init --template minimal-docs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
3. Configure modules and steps in [Configuration reference](#configuration-reference).
|
|
51
|
+
4. Wire it into your pre-push hook manager. Lefthook example:
|
|
41
52
|
|
|
42
53
|
```yaml
|
|
43
54
|
pre-push:
|
|
@@ -46,10 +57,14 @@ Requirements:
|
|
|
46
57
|
run: ai-push-hooks hook {1} {2}
|
|
47
58
|
```
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
In Lefthook, `{1}` is the remote name and `{2}` is the remote URL from Git's `pre-push` hook args.
|
|
61
|
+
|
|
62
|
+
5. Push as usual. The workflow runs automatically before push completes.
|
|
50
63
|
|
|
51
64
|
## Commands
|
|
52
65
|
|
|
66
|
+
If installed as a local npm/pnpm dependency, run commands with `npx` or `pnpm exec`.
|
|
67
|
+
|
|
53
68
|
| Command | What it does |
|
|
54
69
|
| --- | --- |
|
|
55
70
|
| `ai-push-hooks hook <remote-name> <remote-url>` | Runs the configured pre-push workflow. |
|
|
@@ -58,9 +73,7 @@ Requirements:
|
|
|
58
73
|
|
|
59
74
|
## Configuration overview
|
|
60
75
|
|
|
61
|
-
- Config file
|
|
62
|
-
- If no config file is present, built-in defaults are used.
|
|
63
|
-
- File values are deep-merged over defaults.
|
|
76
|
+
- Config file: `ai-push-hooks.toml` in repo root (required).
|
|
64
77
|
- Prompt resolution precedence for `llm` and `apply` steps:
|
|
65
78
|
1. `prompt`
|
|
66
79
|
2. `prompt_file`
|
|
@@ -72,11 +85,11 @@ Requirements:
|
|
|
72
85
|
|
|
73
86
|
| Key | Type | Required | Default |
|
|
74
87
|
| --- | --- | --- | --- |
|
|
75
|
-
| `general` | table | no |
|
|
76
|
-
| `llm` | table | no |
|
|
77
|
-
| `logging` | table | no |
|
|
78
|
-
| `workflow` | table | yes |
|
|
79
|
-
| `modules` | table | yes |
|
|
88
|
+
| `general` | table | no | see section defaults |
|
|
89
|
+
| `llm` | table | no | see section defaults |
|
|
90
|
+
| `logging` | table | no | see section defaults |
|
|
91
|
+
| `workflow` | table | yes | n/a |
|
|
92
|
+
| `modules` | table | yes | n/a |
|
|
80
93
|
|
|
81
94
|
### `[general]`
|
|
82
95
|
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/ai_push_hooks/cli.py
CHANGED
|
@@ -28,10 +28,8 @@ def init_config(template: str, force: bool, cwd: pathlib.Path | None = None) ->
|
|
|
28
28
|
raise HookError("Only `minimal-docs` is supported")
|
|
29
29
|
target_dir = cwd or pathlib.Path.cwd()
|
|
30
30
|
config_path = target_dir / "ai-push-hooks.toml"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if existing_path is not None and not force:
|
|
34
|
-
raise HookError(f"Refusing to overwrite existing config without --force: {existing_path}")
|
|
31
|
+
if config_path.exists() and not force:
|
|
32
|
+
raise HookError(f"Refusing to overwrite existing config without --force: {config_path}")
|
|
35
33
|
config_path.write_text(MINIMAL_DOCS_TEMPLATE, encoding="utf-8")
|
|
36
34
|
sys.stdout.write(str(config_path) + "\n")
|
|
37
35
|
return 0
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import copy
|
|
4
3
|
import json
|
|
5
4
|
import os
|
|
6
5
|
import pathlib
|
|
@@ -16,80 +15,6 @@ try:
|
|
|
16
15
|
except ModuleNotFoundError: # pragma: no cover
|
|
17
16
|
tomllib = None # type: ignore[assignment]
|
|
18
17
|
|
|
19
|
-
DEFAULT_CONFIG_RAW: dict[str, Any] = {
|
|
20
|
-
"general": {
|
|
21
|
-
"enabled": True,
|
|
22
|
-
"allow_push_on_error": False,
|
|
23
|
-
"require_clean_worktree": False,
|
|
24
|
-
"skip_on_sync_branch": True,
|
|
25
|
-
},
|
|
26
|
-
"llm": {
|
|
27
|
-
"runner": "opencode",
|
|
28
|
-
"model": "openai/gpt-5.3-codex",
|
|
29
|
-
"variant": "",
|
|
30
|
-
"timeout_seconds": 800,
|
|
31
|
-
"max_parallel": 2,
|
|
32
|
-
"json_max_retries": 2,
|
|
33
|
-
"invalid_json_feedback_max_chars": 6000,
|
|
34
|
-
"json_retry_new_session": True,
|
|
35
|
-
"delete_session_after_run": True,
|
|
36
|
-
"max_diff_bytes": 180000,
|
|
37
|
-
"session_title_prefix": "ai-push-hooks",
|
|
38
|
-
},
|
|
39
|
-
"logging": {
|
|
40
|
-
"level": "status",
|
|
41
|
-
"jsonl": True,
|
|
42
|
-
"dir": ".git/ai-push-hooks/logs",
|
|
43
|
-
"capture_llm_transcript": True,
|
|
44
|
-
"transcript_dir": ".git/ai-push-hooks/transcripts",
|
|
45
|
-
"summary_dir": ".git/ai-push-hooks/summaries",
|
|
46
|
-
"print_llm_output": False,
|
|
47
|
-
},
|
|
48
|
-
"workflow": {"modules": ["docs"]},
|
|
49
|
-
"modules": {
|
|
50
|
-
"docs": {
|
|
51
|
-
"enabled": True,
|
|
52
|
-
"steps": [
|
|
53
|
-
{"id": "collect", "type": "collect", "collector": "docs_context"},
|
|
54
|
-
{
|
|
55
|
-
"id": "query",
|
|
56
|
-
"type": "llm",
|
|
57
|
-
"inputs": ["collect/push.diff", "collect/changed-files.txt"],
|
|
58
|
-
"output": "queries.json",
|
|
59
|
-
"schema": "string_array",
|
|
60
|
-
"fallback_prompt_id": "docs-query-basic",
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"id": "analyze",
|
|
64
|
-
"type": "llm",
|
|
65
|
-
"inputs": [
|
|
66
|
-
"collect/push.diff",
|
|
67
|
-
"collect/docs-context.txt",
|
|
68
|
-
"query/queries.json",
|
|
69
|
-
"collect/recent-commits.txt",
|
|
70
|
-
],
|
|
71
|
-
"output": "issues.json",
|
|
72
|
-
"schema": "docs_issue_array",
|
|
73
|
-
"fallback_prompt_id": "docs-analysis-basic",
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"id": "apply",
|
|
77
|
-
"type": "apply",
|
|
78
|
-
"inputs": ["collect/push.diff", "collect/docs-context.txt", "analyze/issues.json"],
|
|
79
|
-
"allow_paths": ["README.md", "docs/**/*.md"],
|
|
80
|
-
"fallback_prompt_id": "docs-apply-basic",
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"id": "assert",
|
|
84
|
-
"type": "assert",
|
|
85
|
-
"inputs": ["apply/result.json"],
|
|
86
|
-
"assertion": "docs_apply_requires_manual_commit",
|
|
87
|
-
},
|
|
88
|
-
]
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
|
|
93
18
|
ALLOWED_TOP_LEVEL_KEYS = {"general", "llm", "logging", "workflow", "modules"}
|
|
94
19
|
|
|
95
20
|
|
|
@@ -174,16 +99,6 @@ def parse_toml_fallback(raw: str) -> dict[str, Any]:
|
|
|
174
99
|
return parsed
|
|
175
100
|
|
|
176
101
|
|
|
177
|
-
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
178
|
-
merged = copy.deepcopy(base)
|
|
179
|
-
for key, value in override.items():
|
|
180
|
-
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
|
181
|
-
merged[key] = deep_merge(merged[key], value)
|
|
182
|
-
else:
|
|
183
|
-
merged[key] = copy.deepcopy(value)
|
|
184
|
-
return merged
|
|
185
|
-
|
|
186
|
-
|
|
187
102
|
def _normalize_step(raw: dict[str, Any]) -> StepConfig:
|
|
188
103
|
step_type = str(raw.get("type", "")).strip()
|
|
189
104
|
if step_type not in SUPPORTED_STEP_TYPES:
|
|
@@ -315,19 +230,18 @@ def _apply_env_overrides(config: HookConfig) -> HookConfig:
|
|
|
315
230
|
return _build_config(raw)
|
|
316
231
|
|
|
317
232
|
|
|
318
|
-
def load_config(repo_root: pathlib.Path) -> tuple[HookConfig, pathlib.Path
|
|
319
|
-
config_path
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return _apply_env_overrides(_build_config(raw)), config_path
|
|
233
|
+
def load_config(repo_root: pathlib.Path) -> tuple[HookConfig, pathlib.Path]:
|
|
234
|
+
config_path = repo_root / "ai-push-hooks.toml"
|
|
235
|
+
if not config_path.exists():
|
|
236
|
+
raise HookError(
|
|
237
|
+
"Missing required config file `ai-push-hooks.toml` in repo root. "
|
|
238
|
+
"Run `ai-push-hooks init --template minimal-docs` first"
|
|
239
|
+
)
|
|
240
|
+
text = config_path.read_text(encoding="utf-8")
|
|
241
|
+
loaded = tomllib.loads(text) if tomllib is not None else parse_toml_fallback(text)
|
|
242
|
+
if not isinstance(loaded, dict):
|
|
243
|
+
raise HookError(f"Invalid config format in {config_path}")
|
|
244
|
+
return _apply_env_overrides(_build_config(loaded)), config_path
|
|
331
245
|
|
|
332
246
|
|
|
333
247
|
def resolve_prompt_text(repo_root: pathlib.Path, step: StepConfig) -> str:
|
|
@@ -32,7 +32,7 @@ def resolve_opencode_executable() -> str:
|
|
|
32
32
|
cli_path = shutil.which("opencode-cli")
|
|
33
33
|
if cli_path:
|
|
34
34
|
return cli_path
|
|
35
|
-
raise HookError("opencode
|
|
35
|
+
raise HookError("opencode is required but not installed")
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def parse_opencode_json_run_output(raw: str) -> tuple[str | None, str]:
|