grip-hook 0.2.1__tar.gz → 0.2.2__tar.gz

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 (58) hide show
  1. {grip_hook-0.2.1 → grip_hook-0.2.2}/CHANGELOG.md +41 -1
  2. grip_hook-0.2.2/PKG-INFO +149 -0
  3. grip_hook-0.2.2/README.md +100 -0
  4. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/__init__.py +1 -1
  5. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/cli.py +132 -14
  6. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/config.py +39 -4
  7. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/git.py +65 -2
  8. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/providers/__init__.py +2 -2
  9. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/providers/anthropic.py +12 -0
  10. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/providers/openai_compat.py +1 -1
  11. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_config.py +16 -1
  12. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_git.py +37 -0
  13. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_init.py +38 -0
  14. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_providers.py +10 -0
  15. grip_hook-0.2.2/tests/test_release.py +88 -0
  16. grip_hook-0.2.1/PKG-INFO +0 -381
  17. grip_hook-0.2.1/README.md +0 -332
  18. {grip_hook-0.2.1 → grip_hook-0.2.2}/.gitignore +0 -0
  19. {grip_hook-0.2.1 → grip_hook-0.2.2}/LICENSE +0 -0
  20. {grip_hook-0.2.1 → grip_hook-0.2.2}/branding/README.md +0 -0
  21. {grip_hook-0.2.1 → grip_hook-0.2.2}/evals/README.md +0 -0
  22. {grip_hook-0.2.1 → grip_hook-0.2.2}/evals/cases/clamp-add/after/tests/test_ops.py +0 -0
  23. {grip_hook-0.2.1 → grip_hook-0.2.2}/pyproject.toml +0 -0
  24. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/__main__.py +0 -0
  25. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/agent.py +0 -0
  26. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/errors.py +0 -0
  27. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/hooks.py +0 -0
  28. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/integrations.py +0 -0
  29. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/memory.py +0 -0
  30. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/models.py +0 -0
  31. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/platform.py +0 -0
  32. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/prompts.py +0 -0
  33. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/providers/agents.py +0 -0
  34. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/providers/base.py +0 -0
  35. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/providers/fake.py +0 -0
  36. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/py.typed +0 -0
  37. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/quiz.py +0 -0
  38. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/study.py +0 -0
  39. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/terminal.py +0 -0
  40. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/whiteboard.html +0 -0
  41. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/whiteboard.py +0 -0
  42. {grip_hook-0.2.1 → grip_hook-0.2.2}/src/grip_hook/whiteboard_server.py +0 -0
  43. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/__init__.py +0 -0
  44. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/conftest.py +0 -0
  45. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_agent.py +0 -0
  46. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_agents.py +0 -0
  47. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_branding.py +0 -0
  48. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_cli.py +0 -0
  49. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_evals.py +0 -0
  50. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_hooks.py +0 -0
  51. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_integrations.py +0 -0
  52. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_memory.py +0 -0
  53. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_platform.py +0 -0
  54. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_quiz.py +0 -0
  55. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_skip.py +0 -0
  56. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_study.py +0 -0
  57. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_terminal.py +0 -0
  58. {grip_hook-0.2.1 → grip_hook-0.2.2}/tests/test_whiteboard.py +0 -0
@@ -6,6 +6,45 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.2] - 2026-10-19
10
+
11
+ ### Changed
12
+
13
+ - Releases are automatic. Each pull request carries one label, `release:patch`,
14
+ `release:minor`, `release:major` or `release:none`; merging a labelled one bumps the
15
+ version, dates the changelog, tags `main` and publishes to PyPI and GitHub. A check
16
+ makes sure every pull request has exactly one.
17
+ - `grip init` asks, in a terminal, which model to use, the pass mark and whether to quiz
18
+ before pushes, commits or both, with defaults detected on the machine, then says what
19
+ the chosen provider still needs. `--yes` takes every default; scripts are not asked.
20
+ - `grip install` says which model will write the questions when none is configured.
21
+
22
+ ### Fixed
23
+
24
+ - Pushing a new branch said "nothing to quiz" when its commits were also on a branch that
25
+ had been deleted upstream (a merged pull request) but not pruned locally: the stale
26
+ `origin/...` ref made them look pushed. grip now asks the remote with `git ls-remote`
27
+ whenever the local refs say a new branch has nothing to send.
28
+ - With no provider configured, grip used the Anthropic API even when Claude Code, Codex or
29
+ Gemini CLI was installed, and without an API key it crashed with a traceback. The
30
+ default provider is now `auto`: the first installed coding agent, else Anthropic. A
31
+ missing Anthropic key is now a clear error that says what to set.
32
+ - `grip quiz --range main..feat/x` cut branch names to 12 characters in its header
33
+ (`main..feat/kidnap-`); only full hashes are shortened now.
34
+
35
+ ### Documentation
36
+
37
+ - A shorter README in the usual open-source shape: what grip is, install, quick start,
38
+ configuration, links. The long-form material moved to the docs site.
39
+ - New [walkthrough](https://guilyx.github.io/grip/walkthrough/): one real change, from
40
+ `git commit` to an accepted push, with the real questions, grades and screenshots.
41
+ - New [inside grip](https://guilyx.github.io/grip/internals/) deep dive: the checks before
42
+ a quiz, diff collection, prompts, structured output per provider, rubric grading, pass
43
+ memory, the agent flow, the whiteboard's validation, layout and server, and what is
44
+ stored or sent.
45
+ - The docs navigation is regrouped into Get started, Guides, Deep dive, Reference and
46
+ Project.
47
+
9
48
  ## [0.2.1] - 2026-10-19
10
49
 
11
50
  grip is on PyPI.
@@ -167,7 +206,8 @@ The first public release.
167
206
  - The score table uses rounded borders, matching the panels.
168
207
  - The "blocked" message prints the report path relative to the working directory.
169
208
 
170
- [Unreleased]: https://github.com/guilyx/grip/compare/v0.2.1...HEAD
209
+ [Unreleased]: https://github.com/guilyx/grip/compare/v0.2.2...HEAD
210
+ [0.2.2]: https://github.com/guilyx/grip/compare/v0.2.1...v0.2.2
171
211
  [0.2.1]: https://github.com/guilyx/grip/compare/v0.2.0...v0.2.1
172
212
  [0.2.0]: https://github.com/guilyx/grip/compare/v0.1.0...v0.2.0
173
213
  [0.1.0]: https://github.com/guilyx/grip/releases/tag/v0.1.0
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.5
2
+ Name: grip-hook
3
+ Version: 0.2.2
4
+ Summary: Pass the whiteboard defense before you push: a git hook that quizzes you on your own diff.
5
+ Project-URL: Homepage, https://github.com/guilyx/grip
6
+ Project-URL: Documentation, https://guilyx.github.io/grip/
7
+ Project-URL: Repository, https://github.com/guilyx/grip
8
+ Project-URL: Issues, https://github.com/guilyx/grip/issues
9
+ Project-URL: Changelog, https://github.com/guilyx/grip/blob/main/CHANGELOG.md
10
+ Project-URL: Releases, https://github.com/guilyx/grip/releases
11
+ Author-email: "Erwin L." <erwin.lejeune15@gmail.com>
12
+ License-Expression: BSD-3-Clause
13
+ License-File: LICENSE
14
+ Keywords: code-review,git,hook,llm,pre-commit,pre-push,quiz,understanding
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Environment :: Console
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: BSD License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Classifier: Topic :: Software Development :: Version Control :: Git
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: anthropic<2,>=1.0
30
+ Requires-Dist: click>=8.1
31
+ Requires-Dist: pydantic>=2.5
32
+ Requires-Dist: rich>=13.7
33
+ Provides-Extra: demo
34
+ Requires-Dist: fonttools>=4.40; extra == 'demo'
35
+ Requires-Dist: imageio-ffmpeg>=0.5; extra == 'demo'
36
+ Requires-Dist: pillow>=10.0; extra == 'demo'
37
+ Requires-Dist: pyte>=0.8; extra == 'demo'
38
+ Provides-Extra: dev
39
+ Requires-Dist: mypy>=1.10; extra == 'dev'
40
+ Requires-Dist: pre-commit>=3.7; extra == 'dev'
41
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
42
+ Requires-Dist: pytest>=8.0; extra == 'dev'
43
+ Requires-Dist: ruff>=0.5; extra == 'dev'
44
+ Provides-Extra: docs
45
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
46
+ Requires-Dist: mkdocs<2,>=1.6; extra == 'docs'
47
+ Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # grip
51
+
52
+ **Pass the whiteboard defense before you push.**
53
+
54
+ [![CI](https://github.com/guilyx/grip/actions/workflows/ci.yml/badge.svg)](https://github.com/guilyx/grip/actions/workflows/ci.yml)
55
+ [![PyPI](https://img.shields.io/pypi/v/grip-hook)](https://pypi.org/project/grip-hook/)
56
+ [![Python](https://img.shields.io/pypi/pyversions/grip-hook)](https://pypi.org/project/grip-hook/)
57
+ [![Docs](https://img.shields.io/badge/docs-guilyx.github.io%2Fgrip-ff8644)](https://guilyx.github.io/grip/)
58
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/guilyx/grip/blob/main/LICENSE)
59
+
60
+ grip is a git hook that asks you five questions about your own diff before it leaves your
61
+ machine: what it does, why it is built this way, where it fails, what could go wrong, how
62
+ you know it works. A model writes the questions and a hidden rubric from the diff, then
63
+ grades your answers. Score below your team's pass mark and the push is blocked.
64
+
65
+ It checks the developer, not the code: you can ship code a model wrote, as long as you can
66
+ explain and defend it.
67
+
68
+ ![grip blocking a commit with lazy answers, then letting it through after real ones](https://raw.githubusercontent.com/guilyx/grip/main/docs/assets/demo.gif)
69
+
70
+ ## Features
71
+
72
+ - **Five questions from your diff**, graded against a hidden rubric into a Grip Score out
73
+ of 100. Never line numbers or trivia.
74
+ - **`grip whiteboard`**: your change drawn as parts and arrows in the browser, with the
75
+ questions pinned to it, including what a malicious actor could do.
76
+ - **Works with what you have**: reuse Claude Code, Codex or Gemini CLI as the model (no
77
+ API key), or use Anthropic, any OpenAI-compatible API, or Ollama locally.
78
+ - **Inside coding agents**: `/grip` in Claude Code, Codex, Gemini CLI, Cursor and others,
79
+ and a gate that keeps the agent from pushing until you pass.
80
+ - **Out of the way when it should be**: a passed diff is remembered, `grip skip` lets one
81
+ push through, CI and non-interactive runs are skipped.
82
+
83
+ ## Installation
84
+
85
+ ```bash
86
+ pipx install grip-hook # or: uv tool install grip-hook
87
+ ```
88
+
89
+ Python 3.11 or newer, on Linux, macOS or Windows. This installs the `grip` command and a
90
+ `git grip` alias.
91
+
92
+ ## Quick start
93
+
94
+ ```bash
95
+ cd your-repo
96
+ grip init # writes .grip.toml, installs the pre-push hook
97
+ git push # grip asks five questions first
98
+ ```
99
+
100
+ `grip init` picks Claude Code, Codex or Gemini CLI as the model when one is installed.
101
+ Otherwise set `ANTHROPIC_API_KEY`, or see [Providers](https://guilyx.github.io/grip/providers/).
102
+
103
+ Other commands you will use:
104
+
105
+ ```bash
106
+ grip quiz # quiz your staged changes now, no hook needed
107
+ grip whiteboard # defend the change in the browser
108
+ grip quiz --provider fake # try it offline with a scripted grader
109
+ grip skip # let the next push through
110
+ grip status # hooks, last score, configuration
111
+ ```
112
+
113
+ The [walkthrough](https://guilyx.github.io/grip/walkthrough/) follows one real change from
114
+ `git commit` to an accepted push.
115
+
116
+ ## Configuration
117
+
118
+ `.grip.toml` at the root of the repository, committed so the team shares one bar:
119
+
120
+ ```toml
121
+ passing_score = 70 # 0-100
122
+ difficulty = "normal" # easy | normal | hard
123
+ provider = "claude-code" # auto | anthropic | claude-code | codex | gemini | openai | ollama
124
+ ```
125
+
126
+ Every option, environment variables and the pre-commit framework hook are in the
127
+ [configuration reference](https://guilyx.github.io/grip/configuration/).
128
+
129
+ ## Documentation
130
+
131
+ **[guilyx.github.io/grip](https://guilyx.github.io/grip/)**: getting started, a
132
+ walkthrough, coding agents, the whiteboard, how grip works inside, and the honest numbers
133
+ on what the score does and does not measure.
134
+
135
+ ## Contributing
136
+
137
+ Issues and pull requests are welcome. See [CONTRIBUTING.md](https://github.com/guilyx/grip/blob/main/CONTRIBUTING.md)
138
+ for the development setup and [SECURITY.md](https://github.com/guilyx/grip/blob/main/SECURITY.md)
139
+ for reporting vulnerabilities and what data grip sends. This project follows a
140
+ [code of conduct](https://github.com/guilyx/grip/blob/main/CODE_OF_CONDUCT.md).
141
+
142
+ ## Citation
143
+
144
+ If grip is part of a study or a course, please cite it; GitHub's "Cite this repository"
145
+ button reads [CITATION.cff](https://github.com/guilyx/grip/blob/main/CITATION.cff).
146
+
147
+ ## License
148
+
149
+ [BSD-3-Clause](https://github.com/guilyx/grip/blob/main/LICENSE). Copyright (c) 2026, Erwin L.
@@ -0,0 +1,100 @@
1
+ # grip
2
+
3
+ **Pass the whiteboard defense before you push.**
4
+
5
+ [![CI](https://github.com/guilyx/grip/actions/workflows/ci.yml/badge.svg)](https://github.com/guilyx/grip/actions/workflows/ci.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/grip-hook)](https://pypi.org/project/grip-hook/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/grip-hook)](https://pypi.org/project/grip-hook/)
8
+ [![Docs](https://img.shields.io/badge/docs-guilyx.github.io%2Fgrip-ff8644)](https://guilyx.github.io/grip/)
9
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/guilyx/grip/blob/main/LICENSE)
10
+
11
+ grip is a git hook that asks you five questions about your own diff before it leaves your
12
+ machine: what it does, why it is built this way, where it fails, what could go wrong, how
13
+ you know it works. A model writes the questions and a hidden rubric from the diff, then
14
+ grades your answers. Score below your team's pass mark and the push is blocked.
15
+
16
+ It checks the developer, not the code: you can ship code a model wrote, as long as you can
17
+ explain and defend it.
18
+
19
+ ![grip blocking a commit with lazy answers, then letting it through after real ones](https://raw.githubusercontent.com/guilyx/grip/main/docs/assets/demo.gif)
20
+
21
+ ## Features
22
+
23
+ - **Five questions from your diff**, graded against a hidden rubric into a Grip Score out
24
+ of 100. Never line numbers or trivia.
25
+ - **`grip whiteboard`**: your change drawn as parts and arrows in the browser, with the
26
+ questions pinned to it, including what a malicious actor could do.
27
+ - **Works with what you have**: reuse Claude Code, Codex or Gemini CLI as the model (no
28
+ API key), or use Anthropic, any OpenAI-compatible API, or Ollama locally.
29
+ - **Inside coding agents**: `/grip` in Claude Code, Codex, Gemini CLI, Cursor and others,
30
+ and a gate that keeps the agent from pushing until you pass.
31
+ - **Out of the way when it should be**: a passed diff is remembered, `grip skip` lets one
32
+ push through, CI and non-interactive runs are skipped.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pipx install grip-hook # or: uv tool install grip-hook
38
+ ```
39
+
40
+ Python 3.11 or newer, on Linux, macOS or Windows. This installs the `grip` command and a
41
+ `git grip` alias.
42
+
43
+ ## Quick start
44
+
45
+ ```bash
46
+ cd your-repo
47
+ grip init # writes .grip.toml, installs the pre-push hook
48
+ git push # grip asks five questions first
49
+ ```
50
+
51
+ `grip init` picks Claude Code, Codex or Gemini CLI as the model when one is installed.
52
+ Otherwise set `ANTHROPIC_API_KEY`, or see [Providers](https://guilyx.github.io/grip/providers/).
53
+
54
+ Other commands you will use:
55
+
56
+ ```bash
57
+ grip quiz # quiz your staged changes now, no hook needed
58
+ grip whiteboard # defend the change in the browser
59
+ grip quiz --provider fake # try it offline with a scripted grader
60
+ grip skip # let the next push through
61
+ grip status # hooks, last score, configuration
62
+ ```
63
+
64
+ The [walkthrough](https://guilyx.github.io/grip/walkthrough/) follows one real change from
65
+ `git commit` to an accepted push.
66
+
67
+ ## Configuration
68
+
69
+ `.grip.toml` at the root of the repository, committed so the team shares one bar:
70
+
71
+ ```toml
72
+ passing_score = 70 # 0-100
73
+ difficulty = "normal" # easy | normal | hard
74
+ provider = "claude-code" # auto | anthropic | claude-code | codex | gemini | openai | ollama
75
+ ```
76
+
77
+ Every option, environment variables and the pre-commit framework hook are in the
78
+ [configuration reference](https://guilyx.github.io/grip/configuration/).
79
+
80
+ ## Documentation
81
+
82
+ **[guilyx.github.io/grip](https://guilyx.github.io/grip/)**: getting started, a
83
+ walkthrough, coding agents, the whiteboard, how grip works inside, and the honest numbers
84
+ on what the score does and does not measure.
85
+
86
+ ## Contributing
87
+
88
+ Issues and pull requests are welcome. See [CONTRIBUTING.md](https://github.com/guilyx/grip/blob/main/CONTRIBUTING.md)
89
+ for the development setup and [SECURITY.md](https://github.com/guilyx/grip/blob/main/SECURITY.md)
90
+ for reporting vulnerabilities and what data grip sends. This project follows a
91
+ [code of conduct](https://github.com/guilyx/grip/blob/main/CODE_OF_CONDUCT.md).
92
+
93
+ ## Citation
94
+
95
+ If grip is part of a study or a course, please cite it; GitHub's "Cite this repository"
96
+ button reads [CITATION.cff](https://github.com/guilyx/grip/blob/main/CITATION.cff).
97
+
98
+ ## License
99
+
100
+ [BSD-3-Clause](https://github.com/guilyx/grip/blob/main/LICENSE). Copyright (c) 2026, Erwin L.
@@ -3,7 +3,7 @@
3
3
  A git hook that quizzes you about your own diff before you commit or push it.
4
4
  """
5
5
 
6
- __version__ = "0.2.1"
6
+ __version__ = "0.2.2"
7
7
 
8
8
  QUESTION_COUNT = 5
9
9
  """Number of questions asked in every quiz. Fixed by design."""
@@ -6,6 +6,7 @@ import contextlib
6
6
  import dataclasses
7
7
  import json
8
8
  import os
9
+ import shutil
9
10
  import subprocess
10
11
  import sys
11
12
  import webbrowser
@@ -32,8 +33,11 @@ from grip_hook.agent import (
32
33
  report_payload,
33
34
  )
34
35
  from grip_hook.config import (
36
+ AGENT_PROVIDERS,
37
+ AUTO_PROVIDER,
35
38
  DEFAULT_PASSING_SCORE,
36
39
  Config,
40
+ auto_provider,
37
41
  describe,
38
42
  load_config,
39
43
  render_template,
@@ -41,7 +45,7 @@ from grip_hook.config import (
41
45
  from grip_hook.errors import GripError, NoTerminalError, ProviderError, QuizFailed
42
46
  from grip_hook.git import Diff, Git, PushedRef
43
47
  from grip_hook.hooks import INSTALLABLE_STAGES, install, status, uninstall
44
- from grip_hook.integrations import AGENTS, Action, agent, apply, detect, is_installed
48
+ from grip_hook.integrations import AGENTS, Action, agent, apply, detect
45
49
  from grip_hook.memory import PassMemory, SkipState
46
50
  from grip_hook.models import Difficulty, QuestionSet, Report, Stage
47
51
  from grip_hook.platform import (
@@ -634,10 +638,92 @@ def _push_diff(git: Git, cfg: Config, hook_args: tuple[str, ...]) -> Diff:
634
638
 
635
639
  def _default_provider() -> str:
636
640
  """The provider ``grip init`` picks: a signed-in coding agent when there is one."""
637
- for key in ("claude-code", "codex", "gemini"):
638
- if is_installed(agent(key)):
639
- return key
640
- return "anthropic"
641
+ return auto_provider()
642
+
643
+
644
+ _PROVIDER_HINTS = {
645
+ "claude-code": "Claude Code, already signed in: no key, no extra bill",
646
+ "codex": "Codex CLI, already signed in: no key, no extra bill",
647
+ "gemini": "Gemini CLI, already signed in: no key, no extra bill",
648
+ "auto": "each teammate's installed coding agent, else anthropic",
649
+ "anthropic": "Anthropic API: needs ANTHROPIC_API_KEY or `ant auth login`",
650
+ "ollama": "a local model through Ollama: free, set `model` in .grip.toml",
651
+ "openai": "any OpenAI-compatible API: set `base_url` and OPENAI_API_KEY",
652
+ }
653
+
654
+
655
+ @dataclasses.dataclass(frozen=True, slots=True)
656
+ class _InitChoices:
657
+ provider: str
658
+ passing_score: int
659
+ stages: tuple[str, ...]
660
+ agents: bool
661
+
662
+
663
+ def _init_wizard(
664
+ defaults: _InitChoices,
665
+ *,
666
+ ask_provider: bool,
667
+ ask_score: bool,
668
+ ask_stages: bool,
669
+ ask_agents: bool,
670
+ ) -> _InitChoices:
671
+ """Ask, in the terminal, for what the flags did not say. Enter keeps each default."""
672
+ _out.print("[bold]grip init[/bold] [dim]· Enter keeps the default in brackets[/dim]\n")
673
+ provider = defaults.provider
674
+ if ask_provider:
675
+ installed = [name for name, command in AGENT_PROVIDERS if shutil.which(command)]
676
+ options = [*installed, "auto", "anthropic", "ollama", "openai"]
677
+ _out.print("[bold]Which model asks and grades the questions?[/bold]")
678
+ for name in options:
679
+ _out.print(f" [cyan]{name:<12}[/cyan] {_PROVIDER_HINTS[name]}")
680
+ provider = click.prompt(
681
+ "Provider", type=click.Choice(options), default=defaults.provider, show_choices=False
682
+ )
683
+ score = defaults.passing_score
684
+ if ask_score:
685
+ score = click.prompt(
686
+ f"\nPass mark, out of {MAX_SCORE}", type=click.IntRange(0, MAX_SCORE), default=score
687
+ )
688
+ stages = defaults.stages
689
+ if ask_stages:
690
+ when = click.prompt(
691
+ "\nQuiz before every push, every commit, or both?",
692
+ type=click.Choice(["push", "commit", "both"]),
693
+ default="push",
694
+ )
695
+ stages = {"push": ("pre-push",), "commit": ("pre-commit",)}.get(
696
+ when, ("pre-commit", "pre-push")
697
+ )
698
+ agents = defaults.agents
699
+ if ask_agents and (found := detect()):
700
+ names = ", ".join(a.name for a in found)
701
+ agents = click.confirm(
702
+ f"\nTell the coding agents here ({names}) to run the quiz before they push?",
703
+ default=True,
704
+ )
705
+ _out.print()
706
+ return _InitChoices(provider, score, stages, agents)
707
+
708
+
709
+ def _interactive() -> bool:
710
+ """Whether someone is at the keyboard to answer ``grip init``'s questions."""
711
+ return sys.stdin.isatty() and sys.stdout.isatty()
712
+
713
+
714
+ def _provider_tip(provider: str) -> str | None:
715
+ """What still has to happen for ``provider`` to work, if anything."""
716
+ resolved = auto_provider() if provider == "auto" else provider
717
+ command = dict(AGENT_PROVIDERS).get(resolved)
718
+ if command and not shutil.which(command):
719
+ return f"`{command}` is not on PATH yet; install it and sign in before the next push."
720
+ if resolved == "anthropic" and not os.environ.get("ANTHROPIC_API_KEY"):
721
+ return "set ANTHROPIC_API_KEY (or run `ant auth login`) before the next push."
722
+ if resolved == "ollama":
723
+ return "set `model` in .grip.toml to a model you have pulled, e.g. qwen3:14b."
724
+ if resolved == "openai":
725
+ return "set `base_url` in .grip.toml and export OPENAI_API_KEY."
726
+ return None
641
727
 
642
728
 
643
729
  @cli.command(name="init")
@@ -669,6 +755,7 @@ def _default_provider() -> str:
669
755
  @click.option("--no-hook", is_flag=True, help="Do not install a git hook.")
670
756
  @click.option("--no-agents", is_flag=True, help="Do not write coding-agent rule files.")
671
757
  @click.option("--force", is_flag=True, help="Overwrite an existing .grip.toml.")
758
+ @click.option("--yes", "-y", is_flag=True, help="Take every default instead of asking.")
672
759
  def init_cmd(
673
760
  *,
674
761
  provider: str | None,
@@ -678,34 +765,56 @@ def init_cmd(
678
765
  no_hook: bool,
679
766
  no_agents: bool,
680
767
  force: bool,
768
+ yes: bool,
681
769
  ) -> None:
682
770
  """Set a repository up in one go: .grip.toml, the git hook and the agent rule files.
683
771
 
684
- Picks a provider you will not need a key for when Claude Code, Codex or Gemini CLI is
685
- installed. Everything it writes is meant to be committed so the whole team shares it.
772
+ In a terminal it asks which model to use, the pass mark and when to quiz, with
773
+ defaults picked for this machine; flags answer a question up front and --yes takes
774
+ every default. It picks a provider you will not need a key for when Claude Code, Codex
775
+ or Gemini CLI is installed. Everything it writes is meant to be committed so the whole
776
+ team shares it.
686
777
  """
687
778
  git = Git()
688
779
  root = git.root()
689
780
  toml_path = root / ".grip.toml"
690
- if toml_path.exists() and not force:
781
+ writing = force or not toml_path.exists()
782
+ choices = _InitChoices(
783
+ provider=(provider or _default_provider()).lower(),
784
+ passing_score=DEFAULT_PASSING_SCORE if passing_score is None else passing_score,
785
+ stages=stages or (Stage.PRE_PUSH.value,),
786
+ agents=not no_agents,
787
+ )
788
+ if not yes and _interactive():
789
+ choices = _init_wizard(
790
+ choices,
791
+ ask_provider=writing and provider is None,
792
+ ask_score=writing and passing_score is None,
793
+ ask_stages=not no_hook and not stages,
794
+ ask_agents=not no_agents,
795
+ )
796
+ if not writing:
691
797
  _out.print(
692
798
  f"[dim]grip: {_pretty_path(toml_path)} exists, keeping it (--force overwrites)[/dim]"
693
799
  )
694
800
  else:
695
- chosen = (provider or _default_provider()).lower()
696
801
  text = render_template(
697
- provider=chosen,
698
- passing_score=DEFAULT_PASSING_SCORE if passing_score is None else passing_score,
802
+ provider=choices.provider,
803
+ passing_score=choices.passing_score,
699
804
  difficulty=difficulty or Difficulty.NORMAL.value,
700
805
  )
701
806
  toml_path.write_text(text, "utf-8")
702
- _out.print(f"[green]grip:[/green] wrote {_pretty_path(toml_path)} (provider {chosen})")
807
+ _out.print(
808
+ f"[green]grip:[/green] wrote {_pretty_path(toml_path)} (provider {choices.provider})"
809
+ )
810
+ if tip := _provider_tip(choices.provider):
811
+ _out.print(f"[yellow]grip:[/yellow] {tip}")
703
812
  if not no_hook:
704
- for stage in stages or (Stage.PRE_PUSH.value,):
813
+ for stage in choices.stages:
705
814
  result = install(git, Stage(stage), append=True)
706
815
  how = "appended to" if result.appended else "installed at"
707
816
  _out.print(f"[green]grip:[/green] {stage} hook {how} {_pretty_path(result.path)}")
708
- if not no_agents:
817
+ if choices.agents:
709
818
  changes = apply(root, detect())
710
819
  past = {Action.WRITTEN: "wrote", Action.UPDATED: "updated"}
711
820
  for change in changes:
@@ -808,6 +917,15 @@ def install_cmd(stages: tuple[str, ...], force: bool, append: bool) -> None:
808
917
  result = install(git, Stage(stage), force=force, append=append)
809
918
  how = "appended to" if result.appended else "installed at"
810
919
  _out.print(f"[green]grip:[/green] {stage} hook {how} {result.path}")
920
+ cfg = _config(git)
921
+ if cfg.provider == AUTO_PROVIDER:
922
+ _out.print(
923
+ f"[dim]grip: no provider configured, so the questions come from "
924
+ f"{cfg.provider_name} (provider auto). `grip init` sets the model, the pass mark "
925
+ "and the team's defaults.[/dim]"
926
+ )
927
+ if tip := _provider_tip(cfg.provider):
928
+ _out.print(f"[yellow]grip:[/yellow] {tip}")
811
929
  _out.print("[dim]Run `grip status` to check, `grip uninstall` to remove.[/dim]")
812
930
 
813
931
 
@@ -12,6 +12,7 @@ Precedence, highest first:
12
12
  from __future__ import annotations
13
13
 
14
14
  import os
15
+ import shutil
15
16
  import tomllib
16
17
  from dataclasses import dataclass, fields, replace
17
18
  from pathlib import Path
@@ -47,6 +48,27 @@ DEFAULT_EXCLUDES: tuple[str, ...] = (
47
48
 
48
49
  ENV_PREFIX = "GRIP_"
49
50
 
51
+ AUTO_PROVIDER = "auto"
52
+ AGENT_PROVIDERS: tuple[tuple[str, str], ...] = (
53
+ ("claude-code", "claude"),
54
+ ("codex", "codex"),
55
+ ("gemini", "gemini"),
56
+ )
57
+ """Coding-agent providers ``auto`` tries, in order, with the command each one needs."""
58
+
59
+
60
+ def auto_provider() -> str:
61
+ """What ``provider = "auto"`` means on this machine.
62
+
63
+ A coding agent that is installed (and so, usually, signed in) needs no API key and adds
64
+ no bill, so the first of Claude Code, Codex and Gemini CLI on ``PATH`` wins; without
65
+ one, the Anthropic API.
66
+ """
67
+ for name, command in AGENT_PROVIDERS:
68
+ if shutil.which(command):
69
+ return name
70
+ return "anthropic"
71
+
50
72
 
51
73
  @dataclass(frozen=True, slots=True)
52
74
  class Config:
@@ -55,9 +77,10 @@ class Config:
55
77
  passing_score: int = DEFAULT_PASSING_SCORE
56
78
  """Minimum Grip Score (0-100) required to let the commit or push through."""
57
79
 
58
- provider: str = "anthropic"
59
- """Provider name: ``anthropic``, ``openai``, ``ollama``, ``claude-code``, ``codex``,
60
- ``gemini`` or ``fake``."""
80
+ provider: str = AUTO_PROVIDER
81
+ """Provider name: ``auto``, ``anthropic``, ``openai``, ``ollama``, ``claude-code``,
82
+ ``codex``, ``gemini`` or ``fake``. ``auto`` picks an installed coding agent, else
83
+ ``anthropic``; see :func:`auto_provider`."""
61
84
 
62
85
  model: str = ""
63
86
  """Model identifier passed to the provider. Empty means the provider's default."""
@@ -106,6 +129,12 @@ class Config:
106
129
  raise ConfigError("provider must not be empty")
107
130
  return self
108
131
 
132
+ @property
133
+ def provider_name(self) -> str:
134
+ """The provider that will actually run: ``auto`` resolved on this machine."""
135
+ name = self.provider.lower()
136
+ return auto_provider() if name == AUTO_PROVIDER else name
137
+
109
138
 
110
139
  _FIELD_NAMES = {f.name for f in fields(Config)}
111
140
 
@@ -205,7 +234,8 @@ passing_score = {passing_score}
205
234
  # easy | normal | hard
206
235
  difficulty = "{difficulty}"
207
236
 
208
- # anthropic | claude-code | codex | gemini | openai | ollama | fake
237
+ # auto | anthropic | claude-code | codex | gemini | openai | ollama | fake
238
+ # (auto: an installed coding agent, else anthropic)
209
239
  provider = "{provider}"
210
240
 
211
241
  # model = "" # empty: the provider's default
@@ -239,16 +269,21 @@ def describe(cfg: Config) -> list[tuple[str, str]]:
239
269
  text = value.value
240
270
  else:
241
271
  text = str(value) if value != "" else "(provider default)"
272
+ if f.name == "provider" and value == AUTO_PROVIDER:
273
+ text = f"{AUTO_PROVIDER} ({cfg.provider_name})"
242
274
  rows.append((f.name, text))
243
275
  return rows
244
276
 
245
277
 
246
278
  __all__ = [
279
+ "AGENT_PROVIDERS",
280
+ "AUTO_PROVIDER",
247
281
  "DEFAULT_EXCLUDES",
248
282
  "DEFAULT_MODEL",
249
283
  "DEFAULT_PASSING_SCORE",
250
284
  "TEMPLATE",
251
285
  "Config",
286
+ "auto_provider",
252
287
  "describe",
253
288
  "load_config",
254
289
  "render_template",