devstuff 1.17.0__tar.gz → 1.18.0__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 (108) hide show
  1. {devstuff-1.17.0 → devstuff-1.18.0}/.github/workflows/test-installs.yml +1 -0
  2. {devstuff-1.17.0 → devstuff-1.18.0}/CHANGELOG.md +6 -0
  3. {devstuff-1.17.0 → devstuff-1.18.0}/CLAUDE.md +43 -4
  4. {devstuff-1.17.0 → devstuff-1.18.0}/PKG-INFO +98 -1
  5. {devstuff-1.17.0 → devstuff-1.18.0}/README.md +97 -0
  6. {devstuff-1.17.0 → devstuff-1.18.0}/docs/specs/README.md +1 -0
  7. devstuff-1.18.0/docs/specs/commitizen-config/development-plan.md +80 -0
  8. devstuff-1.18.0/docs/specs/commitizen-config/specifications.md +119 -0
  9. devstuff-1.18.0/docs/specs/commitizen-config/stack-decisions.md +183 -0
  10. devstuff-1.18.0/pyproject.toml +174 -0
  11. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/configure/__init__.py +6 -0
  12. devstuff-1.18.0/src/dev_setup/configure/commitizen/__init__.py +1 -0
  13. devstuff-1.18.0/src/dev_setup/configure/commitizen/detect.py +213 -0
  14. devstuff-1.18.0/src/dev_setup/configure/commitizen/model.py +510 -0
  15. devstuff-1.18.0/src/dev_setup/configure/commitizen/render.py +552 -0
  16. devstuff-1.18.0/src/dev_setup/configure/commitizen/validate.py +321 -0
  17. devstuff-1.18.0/src/dev_setup/configure/commitizen/wizard.py +613 -0
  18. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/tools.yaml +12 -0
  19. devstuff-1.18.0/tests/integration/test_commitizen_config.py +135 -0
  20. devstuff-1.18.0/tests/test_configure_commitizen.py +759 -0
  21. {devstuff-1.17.0 → devstuff-1.18.0}/uv.lock +1 -1
  22. devstuff-1.17.0/pyproject.toml +0 -75
  23. {devstuff-1.17.0 → devstuff-1.18.0}/.claude/skills/run-stuff/SKILL.md +0 -0
  24. {devstuff-1.17.0 → devstuff-1.18.0}/.claude/skills/run-stuff/smoke.sh +0 -0
  25. {devstuff-1.17.0 → devstuff-1.18.0}/.github/workflows/bump.yml +0 -0
  26. {devstuff-1.17.0 → devstuff-1.18.0}/.github/workflows/ci.yml +0 -0
  27. {devstuff-1.17.0 → devstuff-1.18.0}/.github/workflows/publish.yml +0 -0
  28. {devstuff-1.17.0 → devstuff-1.18.0}/.gitignore +0 -0
  29. {devstuff-1.17.0 → devstuff-1.18.0}/dev/Dockerfile +0 -0
  30. {devstuff-1.17.0 → devstuff-1.18.0}/dev/Dockerfile.ci +0 -0
  31. {devstuff-1.17.0 → devstuff-1.18.0}/dev/Makefile +0 -0
  32. {devstuff-1.17.0 → devstuff-1.18.0}/dev/docker-compose.yml +0 -0
  33. {devstuff-1.17.0 → devstuff-1.18.0}/dev-setup +0 -0
  34. {devstuff-1.17.0 → devstuff-1.18.0}/docs/specs/agent/development-plan.md +0 -0
  35. {devstuff-1.17.0 → devstuff-1.18.0}/docs/specs/agent/specifications.md +0 -0
  36. {devstuff-1.17.0 → devstuff-1.18.0}/docs/specs/agent/stack-decisions.md +0 -0
  37. {devstuff-1.17.0 → devstuff-1.18.0}/docs/specs/starship-config/development-plan.md +0 -0
  38. {devstuff-1.17.0 → devstuff-1.18.0}/docs/specs/starship-config/specifications.md +0 -0
  39. {devstuff-1.17.0 → devstuff-1.18.0}/docs/specs/starship-config/stack-decisions.md +0 -0
  40. {devstuff-1.17.0 → devstuff-1.18.0}/install.sh +0 -0
  41. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/__init__.py +0 -0
  42. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/__main__.py +0 -0
  43. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/__init__.py +0 -0
  44. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/approval.py +0 -0
  45. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/bridges.py +0 -0
  46. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/catalog.py +0 -0
  47. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/completion.py +0 -0
  48. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/config.py +0 -0
  49. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/loop.py +0 -0
  50. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/ollama.py +0 -0
  51. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/preflight.py +0 -0
  52. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/primitives.py +0 -0
  53. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/registry.py +0 -0
  54. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/sandbox.py +0 -0
  55. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/session.py +0 -0
  56. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/transcript.py +0 -0
  57. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent/wizard.py +0 -0
  58. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent_tools.schema.json +0 -0
  59. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/agent_tools.yaml +0 -0
  60. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/base.py +0 -0
  61. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/catalog.py +0 -0
  62. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/cli.py +0 -0
  63. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/__init__.py +0 -0
  64. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/add_cmd.py +0 -0
  65. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/agent_cmd.py +0 -0
  66. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/catalog_cmd.py +0 -0
  67. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/configure_cmd.py +0 -0
  68. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/delete_cmd.py +0 -0
  69. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/docs_cmd.py +0 -0
  70. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/functions_cmd.py +0 -0
  71. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/help_cmd.py +0 -0
  72. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/install_cmd.py +0 -0
  73. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/list_cmd.py +0 -0
  74. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/remove_cmd.py +0 -0
  75. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/run_cmd.py +0 -0
  76. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/skills_cmd.py +0 -0
  77. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/commands/update_cmd.py +0 -0
  78. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/configure/starship/__init__.py +0 -0
  79. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/configure/starship/fonts.py +0 -0
  80. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/configure/starship/model.py +0 -0
  81. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/configure/starship/preview.py +0 -0
  82. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/configure/starship/render.py +0 -0
  83. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/configure/starship/wizard.py +0 -0
  84. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/function_runner.py +0 -0
  85. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/functions.schema.json +0 -0
  86. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/functions.yaml +0 -0
  87. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/functions_catalog.py +0 -0
  88. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/functions_registry.py +0 -0
  89. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/generic.py +0 -0
  90. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/registry.py +0 -0
  91. {devstuff-1.17.0 → devstuff-1.18.0}/src/dev_setup/ui.py +0 -0
  92. {devstuff-1.17.0 → devstuff-1.18.0}/tests/__init__.py +0 -0
  93. {devstuff-1.17.0 → devstuff-1.18.0}/tests/integration/__init__.py +0 -0
  94. {devstuff-1.17.0 → devstuff-1.18.0}/tests/integration/conftest.py +0 -0
  95. {devstuff-1.17.0 → devstuff-1.18.0}/tests/integration/test_agent_smoke.py +0 -0
  96. {devstuff-1.17.0 → devstuff-1.18.0}/tests/integration/test_agent_tui.py +0 -0
  97. {devstuff-1.17.0 → devstuff-1.18.0}/tests/integration/test_tools.py +0 -0
  98. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_agent.py +0 -0
  99. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_agent_completion.py +0 -0
  100. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_agent_loop.py +0 -0
  101. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_agent_sandbox.py +0 -0
  102. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_agent_tools.py +0 -0
  103. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_agent_wizard.py +0 -0
  104. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_catalog.py +0 -0
  105. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_configure_starship.py +0 -0
  106. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_functions.py +0 -0
  107. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_generic.py +0 -0
  108. {devstuff-1.17.0 → devstuff-1.18.0}/tests/test_registry.py +0 -0
@@ -52,6 +52,7 @@ jobs:
52
52
  matrix:
53
53
  tool:
54
54
  - aws
55
+ - commitizen # uvx tool, requires uv
55
56
  - eza
56
57
  - gh
57
58
  - git-lfs
@@ -1,3 +1,9 @@
1
+ ## v1.18.0 (2026-08-01)
2
+
3
+ ### Features
4
+
5
+ - **configure**: add commitizen tool and configuration wizard
6
+
1
7
  ## v1.17.0 (2026-07-31)
2
8
 
3
9
  ### Feat
@@ -173,8 +173,9 @@ ones tools already have.
173
173
  ## Configurators (`configure/`) — tool-specific wizards, deliberately *not* catalog-driven
174
174
 
175
175
  `src/dev_setup/configure/` holds per-tool setup wizards (`devstuff configure <tool>`), registered
176
- in a `CONFIGURATORS` dict keyed by catalog tool key. `starship` is the first one:
177
- `configure/starship/{model,render,preview,wizard}.py`.
176
+ in a `CONFIGURATORS` dict keyed by catalog tool key. There are two:
177
+ `configure/starship/{model,render,preview,wizard}.py` and
178
+ `configure/commitizen/{model,render,detect,validate,wizard}.py`.
178
179
 
179
180
  **Why this one breaks the YAML-catalog rule.** Installation generalises into ~7 mechanisms, which
180
181
  is what makes `GenericTool` possible. Configuration does not: starship's config is a TOML file of
@@ -244,8 +245,46 @@ or palette is one entry; no other file changes.
244
245
  the ordinary catalog path (`nerd-font` in `tools.yaml` → `install_cmd.install_by_key`), never a
245
246
  private download (SD-10).
246
247
 
247
- Not yet built: configurators for anything other than starship, and round-tripping an existing
248
- hand-edited config back into wizard state (the timestamped backup is the safety net instead).
248
+ **Within the commitizen configurator, the object being configured is a list of commit types, not
249
+ a settings sheet.** `TYPES` in `model.py` is ordered (declaration order *is* prompt order, changelog
250
+ order and regex-alternation order), and `render.py` derives all nine `cz_customize` settings from
251
+ it — so adding a type is one `ChangeType` record and it reaches `bump_pattern`, `bump_map`,
252
+ `schema_pattern`, `change_type_map`, `change_type_order`, `commit_parser` and `questions` with no
253
+ other edit. Full reasoning in `docs/specs/commitizen-config/`.
254
+
255
+ **Things learned from the real binary — don't "simplify" these away:**
256
+ - `bump_map` is an **ordered** map and commitizen `break`s at the first key that `re.match`es, so
257
+ the two breaking-change rules (`^.+!$`, `^BREAKING[\-\ ]CHANGE`) must be emitted first. Reorder
258
+ them and `feat(api)!:` silently ships as a MINOR.
259
+ - What `bump_map`'s keys are matched against is **group 1 of `bump_pattern`** (`feat(api)!`), not
260
+ the commit message. That is why `^.+!$` works at all, and why every selected type belongs in the
261
+ pattern even when it has no map entry.
262
+ - `schema_pattern` must always accept the `bump:` prefix (`ALWAYS_ACCEPTED` in `model.py`):
263
+ `cz bump` writes its own commit with that prefix, and `cz check --rev-range` over a release
264
+ otherwise rejects commitizen's own commit. `cz_conventional_commits` accepts `bump` for exactly
265
+ this reason without ever offering it in the picker.
266
+ - `commit_parser`'s trailing `|\w+!` alternative is load-bearing: it is what keeps `docs!: …` (a
267
+ breaking change on a type with no changelog section) in the release notes at all. Verified — it
268
+ lands in an unlabelled group rather than vanishing.
269
+ - The `BREAKING CHANGE` changelog heading only collects commits with a **footer**. A `feat!:` still
270
+ bumps the major but is written up under Features, because that is the type it declared.
271
+ - Regexes are emitted as TOML **literal** strings (`'…'`) so no backslash needs doubling;
272
+ `message_template` and `schema` are the exceptions (they carry real newlines, so they are basic
273
+ strings). A user-supplied value containing a quote falls back to a basic string automatically.
274
+ - `config_path()` mirrors `commitizen.config.read_cfg`: search order *and* the rule that a file
275
+ without a `commitizen` section doesn't count — otherwise every Python project on disk looks
276
+ already-configured because it has a `pyproject.toml`.
277
+ - The `pyproject.toml` splice is line-based, so it verifies itself by parsing the result back and
278
+ comparing the settings; a mismatch returns `None` and the caller writes `.cz.toml` instead. Don't
279
+ replace that check with reasoning about which files it can handle.
280
+ - `validate.py` is the "measured, not assumed" half: it replays commits through the real
281
+ `cz bump --dry-run` in a throwaway repo. It runs on an explicit menu action plus once at save
282
+ time — not on every redraw (~3s, unlike starship's millisecond preview) — and a disagreement
283
+ *warns*, it never vetoes a save. Keep that distinction in the comments.
284
+
285
+ Not yet built: configurators for anything other than starship and commitizen, and round-tripping an
286
+ existing hand-edited config back into wizard state (the timestamped backup is the safety net
287
+ instead).
249
288
 
250
289
  ## Specs (`docs/specs/`)
251
290
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devstuff
3
- Version: 1.17.0
3
+ Version: 1.18.0
4
4
  Summary: Development environment setup CLI for Linux
5
5
  Project-URL: Repository, https://github.com/thesawdawg/dev-setup-py
6
6
  Author-email: Sawyer <sawyerksu@gmail.com>
@@ -203,8 +203,104 @@ devstuff configure starship --output /tmp/try.toml # write elsewhere, leave th
203
203
 
204
204
  | Tool | What it configures |
205
205
  |------|-------------------|
206
+ | `commitizen` | Commit types, what each one bumps, git tags, and changelog sections |
206
207
  | `starship` | Prompt style, colour palette, which sections appear, and layout |
207
208
 
209
+ #### Commitizen wizard
210
+
211
+ Commitizen needs a config file before it does anything, and everything past its built-in
212
+ Conventional Commits rules means `cz_customize` — nine coupled regexes where `bump_map`'s keys
213
+ are matched against **group 1** of `bump_pattern`, the two breaking-change rules have to come
214
+ *first* because commitizen stops at the first key that matches, and `schema_pattern` has to
215
+ accept whatever `message_template` produces. Get the order wrong and `feat(api)!:` quietly ships
216
+ as a minor release.
217
+
218
+ The wizard asks about commit types and version rules, and derives all nine settings from the
219
+ answers:
220
+
221
+ | Step | Options |
222
+ |------|---------|
223
+ | **Convention** | `cz_conventional_commits` (commitizen's fixed rules — nothing to configure) · `cz_customize` (your types, your bump rules) |
224
+ | **Commit types** | Checkbox over `feat`, `fix`, `refactor`, `perf`, `docs`, `style`, `test`, `build`, `ci` (all on, matching Conventional Commits) plus `chore`, `revert`, `deps`, `security` — and you can add your own |
225
+ | **Per type** | What it bumps (`MAJOR` / `MINOR` / `PATCH` / no release), whether it appears in the changelog, under which heading, and its hotkey in `cz commit` |
226
+ | **Versioning** | Where the version lives (`commitizen`, `scm`, `pep621`, `uv`, `poetry`, `cargo`, `npm`, `composer`) · scheme (`semver`, `semver2`, `pep440`) · tag format · other files carrying the version |
227
+ | **Release** | Changelog on bump · `0.x` mode · annotated tags · GPG signing · incremental changelog · prerelease merging · `allow_abort` · hotkeys · changelog file · bump commit message |
228
+ | **Prompt** | Which questions `cz commit` asks — scope, body, footer |
229
+ | **Destination** | `pyproject.toml` (spliced in place) or a standalone `.cz.toml` |
230
+
231
+ The custom branch **starts as an exact copy of Conventional Commits** — the built-in type table
232
+ reproduces `commitizen.defaults.BUMP_MAP` and its picker, read out of the installed package and
233
+ pinned by a test. Switching to custom to add one type does not cost you the convention.
234
+
235
+ Before it asks anything, the wizard reads the project: git root, any existing commitizen config
236
+ (in commitizen's own search order, ignoring files without a `commitizen` section), the version
237
+ provider and current version from `pyproject.toml`/`package.json`/`Cargo.toml`/`composer.json`,
238
+ the latest git tag, and an existing changelog.
239
+
240
+ The review screen shows a sample `cz commit` message, the bump table, and the changelog sections:
241
+
242
+ ```
243
+ what each type does to 1.4.2
244
+
245
+ commit increment new version
246
+ feat: … MINOR 1.5.0
247
+ fix: … PATCH 1.4.3
248
+ docs: … — 1.4.2
249
+ feat!: … MAJOR 2.0.0
250
+ BREAKING CHANGE: … (footer) MAJOR 2.0.0
251
+ ```
252
+
253
+ **That table is checked against the real thing.** "Check these rules against the real cz" builds
254
+ a throwaway git repo tagged at 1.4.2, replays one commit per bump level plus both breaking forms
255
+ through `cz bump --dry-run`, and reports whether commitizen agrees rule by rule. The same check
256
+ runs automatically before saving — a disagreement is reported and asks for confirmation, but
257
+ never blocks the save. "Preview a generated changelog" runs the real `cz changelog --dry-run`
258
+ over one sample commit per type. Without `cz` installed the wizard still works; the checks say so
259
+ rather than failing.
260
+
261
+ On save:
262
+
263
+ - Into `pyproject.toml`, every `[tool.commitizen…]` table is replaced and **the rest of the file
264
+ is left byte for byte**. The result is parsed back and compared to what was meant; if it does
265
+ not match, the write falls back to a standalone `.cz.toml` and the original is untouched.
266
+ - Any existing file is copied to `<name>.bak.<timestamp>` first, and a config that wasn't written
267
+ by this wizard asks before being replaced.
268
+ - If more than one commitizen config now exists, you're told which one commitizen will actually
269
+ read — `.cz.toml` beats `pyproject.toml`, so a new dotfile can silently shadow settings you
270
+ forgot about.
271
+ - In a git repo with no `commit-msg` hook, you're offered one running `cz check`. An existing
272
+ hook is never replaced.
273
+
274
+ The output is meant to be hand-edited afterwards: commented, ordered, and with regexes written as
275
+ TOML literal strings so `^((BREAKING[\-\ ]CHANGE|feat)(\(.+\))?!?):` reads as itself instead of
276
+ with every backslash doubled.
277
+
278
+ ```toml
279
+ # Commitizen configuration
280
+ # Generated by `devstuff configure commitizen` — edit freely, or re-run the wizard.
281
+ # Convention: Custom types and bump rules · Types: feat, fix, docs, deps
282
+ # Option reference: https://commitizen-tools.github.io/commitizen/config/
283
+ [tool.commitizen]
284
+ name = 'cz_customize'
285
+ version_provider = 'pep621'
286
+ version_scheme = 'pep440'
287
+ tag_format = 'v$version'
288
+ update_changelog_on_bump = true
289
+ # …
290
+
291
+ # Prefix -> version increment. Order matters: commitizen stops at the first
292
+ # key that matches, so the two breaking-change rules have to lead.
293
+ [tool.commitizen.customize.bump_map]
294
+ '^.+!$' = 'MAJOR'
295
+ '^BREAKING[\-\ ]CHANGE' = 'MAJOR'
296
+ '^feat' = 'MINOR'
297
+ '^fix' = 'PATCH'
298
+ '^deps' = 'PATCH'
299
+ ```
300
+
301
+ Design notes, including why this one isn't catalog-driven and why the checks warn rather than
302
+ veto: [`docs/specs/commitizen-config/`](docs/specs/commitizen-config/).
303
+
208
304
  #### Starship prompt wizard
209
305
 
210
306
  Four questions, with the prompt re-rendered after each one:
@@ -685,6 +781,7 @@ Optional utilities you may want on some machines.
685
781
  | Key | Name | Description | Help |
686
782
  |-----|------|-------------|------|
687
783
  | `aws` | AWS CLI | Amazon Web Services CLI v2 | `aws help` |
784
+ | `commitizen` | Commitizen | Conventional-commit prompt, semantic version bumping, and changelog generation (`devstuff configure commitizen`) | `cz --help` |
688
785
  | `eza` | eza | Modern ls replacement with git status, icons, and tree view | `eza --help` |
689
786
  | `gh` | GitHub CLI | GitHub's official CLI | `gh --help` |
690
787
  | `htop` | htop | Interactive process and resource monitor | `man htop` |
@@ -176,8 +176,104 @@ devstuff configure starship --output /tmp/try.toml # write elsewhere, leave th
176
176
 
177
177
  | Tool | What it configures |
178
178
  |------|-------------------|
179
+ | `commitizen` | Commit types, what each one bumps, git tags, and changelog sections |
179
180
  | `starship` | Prompt style, colour palette, which sections appear, and layout |
180
181
 
182
+ #### Commitizen wizard
183
+
184
+ Commitizen needs a config file before it does anything, and everything past its built-in
185
+ Conventional Commits rules means `cz_customize` — nine coupled regexes where `bump_map`'s keys
186
+ are matched against **group 1** of `bump_pattern`, the two breaking-change rules have to come
187
+ *first* because commitizen stops at the first key that matches, and `schema_pattern` has to
188
+ accept whatever `message_template` produces. Get the order wrong and `feat(api)!:` quietly ships
189
+ as a minor release.
190
+
191
+ The wizard asks about commit types and version rules, and derives all nine settings from the
192
+ answers:
193
+
194
+ | Step | Options |
195
+ |------|---------|
196
+ | **Convention** | `cz_conventional_commits` (commitizen's fixed rules — nothing to configure) · `cz_customize` (your types, your bump rules) |
197
+ | **Commit types** | Checkbox over `feat`, `fix`, `refactor`, `perf`, `docs`, `style`, `test`, `build`, `ci` (all on, matching Conventional Commits) plus `chore`, `revert`, `deps`, `security` — and you can add your own |
198
+ | **Per type** | What it bumps (`MAJOR` / `MINOR` / `PATCH` / no release), whether it appears in the changelog, under which heading, and its hotkey in `cz commit` |
199
+ | **Versioning** | Where the version lives (`commitizen`, `scm`, `pep621`, `uv`, `poetry`, `cargo`, `npm`, `composer`) · scheme (`semver`, `semver2`, `pep440`) · tag format · other files carrying the version |
200
+ | **Release** | Changelog on bump · `0.x` mode · annotated tags · GPG signing · incremental changelog · prerelease merging · `allow_abort` · hotkeys · changelog file · bump commit message |
201
+ | **Prompt** | Which questions `cz commit` asks — scope, body, footer |
202
+ | **Destination** | `pyproject.toml` (spliced in place) or a standalone `.cz.toml` |
203
+
204
+ The custom branch **starts as an exact copy of Conventional Commits** — the built-in type table
205
+ reproduces `commitizen.defaults.BUMP_MAP` and its picker, read out of the installed package and
206
+ pinned by a test. Switching to custom to add one type does not cost you the convention.
207
+
208
+ Before it asks anything, the wizard reads the project: git root, any existing commitizen config
209
+ (in commitizen's own search order, ignoring files without a `commitizen` section), the version
210
+ provider and current version from `pyproject.toml`/`package.json`/`Cargo.toml`/`composer.json`,
211
+ the latest git tag, and an existing changelog.
212
+
213
+ The review screen shows a sample `cz commit` message, the bump table, and the changelog sections:
214
+
215
+ ```
216
+ what each type does to 1.4.2
217
+
218
+ commit increment new version
219
+ feat: … MINOR 1.5.0
220
+ fix: … PATCH 1.4.3
221
+ docs: … — 1.4.2
222
+ feat!: … MAJOR 2.0.0
223
+ BREAKING CHANGE: … (footer) MAJOR 2.0.0
224
+ ```
225
+
226
+ **That table is checked against the real thing.** "Check these rules against the real cz" builds
227
+ a throwaway git repo tagged at 1.4.2, replays one commit per bump level plus both breaking forms
228
+ through `cz bump --dry-run`, and reports whether commitizen agrees rule by rule. The same check
229
+ runs automatically before saving — a disagreement is reported and asks for confirmation, but
230
+ never blocks the save. "Preview a generated changelog" runs the real `cz changelog --dry-run`
231
+ over one sample commit per type. Without `cz` installed the wizard still works; the checks say so
232
+ rather than failing.
233
+
234
+ On save:
235
+
236
+ - Into `pyproject.toml`, every `[tool.commitizen…]` table is replaced and **the rest of the file
237
+ is left byte for byte**. The result is parsed back and compared to what was meant; if it does
238
+ not match, the write falls back to a standalone `.cz.toml` and the original is untouched.
239
+ - Any existing file is copied to `<name>.bak.<timestamp>` first, and a config that wasn't written
240
+ by this wizard asks before being replaced.
241
+ - If more than one commitizen config now exists, you're told which one commitizen will actually
242
+ read — `.cz.toml` beats `pyproject.toml`, so a new dotfile can silently shadow settings you
243
+ forgot about.
244
+ - In a git repo with no `commit-msg` hook, you're offered one running `cz check`. An existing
245
+ hook is never replaced.
246
+
247
+ The output is meant to be hand-edited afterwards: commented, ordered, and with regexes written as
248
+ TOML literal strings so `^((BREAKING[\-\ ]CHANGE|feat)(\(.+\))?!?):` reads as itself instead of
249
+ with every backslash doubled.
250
+
251
+ ```toml
252
+ # Commitizen configuration
253
+ # Generated by `devstuff configure commitizen` — edit freely, or re-run the wizard.
254
+ # Convention: Custom types and bump rules · Types: feat, fix, docs, deps
255
+ # Option reference: https://commitizen-tools.github.io/commitizen/config/
256
+ [tool.commitizen]
257
+ name = 'cz_customize'
258
+ version_provider = 'pep621'
259
+ version_scheme = 'pep440'
260
+ tag_format = 'v$version'
261
+ update_changelog_on_bump = true
262
+ # …
263
+
264
+ # Prefix -> version increment. Order matters: commitizen stops at the first
265
+ # key that matches, so the two breaking-change rules have to lead.
266
+ [tool.commitizen.customize.bump_map]
267
+ '^.+!$' = 'MAJOR'
268
+ '^BREAKING[\-\ ]CHANGE' = 'MAJOR'
269
+ '^feat' = 'MINOR'
270
+ '^fix' = 'PATCH'
271
+ '^deps' = 'PATCH'
272
+ ```
273
+
274
+ Design notes, including why this one isn't catalog-driven and why the checks warn rather than
275
+ veto: [`docs/specs/commitizen-config/`](docs/specs/commitizen-config/).
276
+
181
277
  #### Starship prompt wizard
182
278
 
183
279
  Four questions, with the prompt re-rendered after each one:
@@ -658,6 +754,7 @@ Optional utilities you may want on some machines.
658
754
  | Key | Name | Description | Help |
659
755
  |-----|------|-------------|------|
660
756
  | `aws` | AWS CLI | Amazon Web Services CLI v2 | `aws help` |
757
+ | `commitizen` | Commitizen | Conventional-commit prompt, semantic version bumping, and changelog generation (`devstuff configure commitizen`) | `cz --help` |
661
758
  | `eza` | eza | Modern ls replacement with git status, icons, and tree view | `eza --help` |
662
759
  | `gh` | GitHub CLI | GitHub's official CLI | `gh --help` |
663
760
  | `htop` | htop | Interactive process and resource monitor | `man htop` |
@@ -40,3 +40,4 @@ They are most useful when they capture:
40
40
  |---------|--------|
41
41
  | [`agent/`](agent/) — `devstuff agent`, the local-model agent | Complete (v1) |
42
42
  | [`starship-config/`](starship-config/) — `devstuff configure starship`, the prompt wizard | Complete (v1) |
43
+ | [`commitizen-config/`](commitizen-config/) — `devstuff configure commitizen`, the commit-convention and bump-rule wizard | Complete (v1) |
@@ -0,0 +1,80 @@
1
+ # Development Plan: `devstuff configure commitizen`
2
+
3
+ **Date:** 2026-07-31
4
+ **Status:** Milestones 1–5 complete
5
+
6
+ ---
7
+
8
+ ## Milestones
9
+
10
+ | # | Milestone | Deliverable | Done when |
11
+ |---|-----------|-------------|-----------|
12
+ | 1 | Catalog entry | `commitizen` in `src/dev_setup/tools.yaml` (`type: uvx`), CI matrix row, README table | `devstuff install commitizen` puts `cz` on the PATH; `devstuff list` shows its version |
13
+ | 2 | Data model | `configure/commitizen/model.py` — `ChangeType`, the ordered `TYPES` table, providers/schemes/tag formats, `CommitizenConfig` | Defaults reproduce `commitizen.defaults.BUMP_MAP` and the conventional picker, with no I/O |
14
+ | 3 | Emitter | `configure/commitizen/render.py` — the nine derived rules, `to_toml()`, `splice_pyproject()` | Output parses under `tomllib` for every convention × provider; splicing this repo's own `pyproject.toml` leaves every other table byte for byte |
15
+ | 4 | Live check | `configure/commitizen/validate.py` — sandbox repo, `cz bump --dry-run` replay, `cz check`, `cz changelog --dry-run` | Seven configs replay through the real `cz` with every rule agreeing |
16
+ | 5 | Wizard + wiring | `configure/commitizen/{detect,wizard}.py`, registry entry, README, CLAUDE.md, this spec | `devstuff configure commitizen` walks the steps, previews, checks, and saves with a backup |
17
+
18
+ ## Testing Strategy
19
+
20
+ **`tests/test_configure_commitizen.py` — unit only, no `cz` required (NFR-5).** 114 tests:
21
+
22
+ - **Model invariants:** every type key is regex-safe (it is spliced raw into four patterns);
23
+ shortcuts unique; bump levels known; `SAMPLE_BUMPS` is a real semver walk.
24
+ - **Agreement with commitizen's shipped defaults:** the selected set, bump map and changelog set
25
+ are compared against constants transcribed from the installed package, and `CONFIG_FILES` is
26
+ compared against `commitizen.defaults.CONFIG_FILES` directly — the precedence warning is only
27
+ correct if that list is.
28
+ - **TOML validity:** every convention × provider parses; regexes come back out of the parser
29
+ byte-identical (the literal-string guarantee); a quote in a user-supplied section name does not
30
+ produce an unparseable file.
31
+ - **The generated rules, executed in Python:** `bump_pattern` group 1 is fed through `bump_map`
32
+ the way commitizen does it, and the result is asserted for `feat`, `feat(api)`, `docs`,
33
+ `feat(api)!`, `docs!`, and both spellings of the breaking footer. This is the offline half of
34
+ the FR-0 guarantee; the sandbox is the other half.
35
+ - **Cross-consistency:** the bump table shown on screen is re-derived from the emitted map and
36
+ compared; `schema_pattern` is matched against messages produced by `message_template`; turning
37
+ off a question removes both the prompt and its template slot.
38
+ - **Splicing:** append, replace, idempotency, other tables preserved, a similarly named table
39
+ (`[tool.commitizen_helper]`) not mistaken for ours, and the fail-closed case — a table header
40
+ inside a multi-line string returns `None` and the original file is untouched.
41
+ - **Detection:** provider and version read off six project shapes; a `pyproject.toml` with no
42
+ commitizen section is not a config; a malformed file is not a config; JSON and YAML configs
43
+ recognised.
44
+ - **Interactive steps:** the prompt helpers are driven through a scripted `ui` double, covering
45
+ the add-a-type validation paths, bump-level overrides, the empty-selection guard, the release
46
+ toggles, the commit hook (written executable; an existing one never touched) and the shadowing
47
+ warning.
48
+
49
+ **`tests/integration/test_commitizen_config.py` — marked `integration`, needs `cz` and `git` but
50
+ *not* sudo or the network** (unlike the rest of that directory — noted in its docstring). 12
51
+ tests, ~24s. Seven configs (conventional, custom defaults, `major_version_zero`, pep440 with bare
52
+ tags, no scope/body/footer, a two-type minimum, renamed sections with `docs` promoted to PATCH)
53
+ are each replayed through `cz bump --dry-run`; plus a user-added type, the changelog headings,
54
+ the exclusion of non-changelog types, a negative `cz check`, and `cz info` round-tripping the
55
+ file through commitizen's own parser.
56
+
57
+ **Manual verification.** The full wizard was driven end to end through a forked pty (the
58
+ technique CLAUDE.md describes for the starship wizard) in a scratch git repo tagged `v0.3.0`:
59
+ version and tag format were picked up from the tag, the review screen rendered, the automatic
60
+ pre-save check reported "8 rules agree", and the written file was accepted by `cz --config …
61
+ example`.
62
+
63
+ ## Risks
64
+
65
+ | Risk | Mitigation |
66
+ |------|-----------|
67
+ | commitizen changes `find_increment`'s first-match-wins semantics, or how `bump_map` keys are matched | The integration suite replays real commits; a change shows up as a failed check rather than as wrong version numbers in the field. The wizard's pre-save check gives the same signal to users. |
68
+ | commitizen changes its shipped defaults | `test_defaults_reproduce_commitizens_own_rules` and `test_config_file_order_matches_commitizens` compare against the installed package. |
69
+ | The pyproject splice damages a user's file | The result is parsed back and compared to the intended settings; a mismatch aborts to `.cz.toml` and leaves the original untouched (FR-14). Every write is backed up first. |
70
+ | `cz_customize` is renamed or removed upstream (commitizen's own docs warn of this — issue #1385) | The convention name lives in one place, `CONVENTIONS` in `model.py`. A rename is a one-line change plus a spec note. |
71
+ | A user's own `commit-msg` hook is clobbered | The offer is only made when no hook exists (FR-18). |
72
+ | The live check makes the wizard feel slow | It is an explicit action plus one automatic run at save (NFR-2), never on redraw. |
73
+
74
+ ## Follow-ups (not built)
75
+
76
+ - Reading an existing config back into wizard state (SD-6 — deliberately out).
77
+ - `pre_bump_hooks` / `post_bump_hooks` prompts (see the open questions in `specifications.md`).
78
+ - Changelog template (`template`/`extras`) configuration.
79
+ - A `.pre-commit-config.yaml` entry alongside the git hook (SD-9).
80
+ - Configurators for anything other than starship and commitizen.
@@ -0,0 +1,119 @@
1
+ # Specification: `devstuff configure commitizen`
2
+
3
+ **Date:** 2026-07-31
4
+ **Status:** Implemented (v1)
5
+ **Authors:** Sawyer + Claude
6
+
7
+ ---
8
+
9
+ ## 1. Problem Statement & Goals
10
+
11
+ `devstuff install commitizen` puts `cz` on the PATH. That is where it stops. Commitizen does
12
+ nothing at all until a config file exists, and its `cz init` writes a five-key stub for
13
+ `cz_conventional_commits` — which is fine right up to the moment somebody wants a type
14
+ commitizen does not ship, or wants `docs:` to stop triggering releases, or wants their
15
+ changelog headings to say something other than "Feat".
16
+
17
+ Everything past that stub means `cz_customize`, and `cz_customize` is not a setting — it is
18
+ nine coupled regexes and an ordered map. The user has to write a `bump_pattern` whose **group
19
+ 1** is what `bump_map`'s keys get `re.match`ed against, keep a `schema_pattern` in step with a
20
+ Jinja2 `message_template` they also wrote, and put the two breaking-change rules at the *top*
21
+ of `bump_map`, because commitizen stops at the first key that matches. Get the order wrong and
22
+ `feat(api)!:` quietly ships as a minor release. Nothing tells you; the version is just wrong.
23
+
24
+ `devstuff configure commitizen` replaces that with a wizard: choose the commit types, say what
25
+ each one does to the version and where it lands in the changelog, and see the resulting bump
26
+ table and changelog sections before anything is written — with the rules replayed through the
27
+ real `cz` to confirm commitizen agrees.
28
+
29
+ **Success criteria**
30
+ - A user who has never read commitizen's config reference gets a working `cz_customize` setup,
31
+ with their own types and bump rules, in under two minutes.
32
+ - The bump table the wizard shows is the bump commitizen actually performs — checked by
33
+ replaying commits through `cz bump --dry-run`, not asserted from intent.
34
+ - Nothing is written until the user confirms; an existing config is never lost, and a
35
+ `pyproject.toml` is edited in place rather than rewritten.
36
+ - The generated file is one a human can then hand-edit: commented, ordered, and using
37
+ commitizen's own key names rather than a devstuff-specific encoding.
38
+ - Zero new runtime dependencies.
39
+
40
+ **Non-goals**
41
+ - A general-purpose config-wizard DSL declared in `tools.yaml` (SD-1, inherited from starship).
42
+ - Round-tripping a hand-written config back into wizard state (SD-6).
43
+ - `cz_jira` and third-party commitizen plugins (SD-8).
44
+ - Writing JSON or YAML configs. commitizen reads `.cz.json`/`.cz.yaml`, but TOML is the form
45
+ every commitizen example is written in and the only one `pyproject.toml` can hold (SD-7).
46
+ - Changelog *templates* (commitizen's `template`/`extras` Jinja2 hooks). Section names and
47
+ ordering are configurable here; the markdown around them is not.
48
+ - Running `cz bump` for the user. The wizard configures releases; it does not cut one.
49
+
50
+ ## 2. Users & Personas
51
+
52
+ | Persona | Description | Primary needs |
53
+ |---------|-------------|---------------|
54
+ | New commitizen user | Just ran `devstuff install commitizen` | A conventional-commits setup that works, without reading the manual |
55
+ | Team lead | Wants house rules — a `deps:` type, `docs:` never releasing | Custom types and per-type bump levels, enforced on commit |
56
+ | Polyglot maintainer | Rust/Node/PHP repo, not Python | A standalone config, the right version provider, semver not pep440 |
57
+ | Existing user | Has a hand-written `[tool.commitizen]` | To not lose the rest of `pyproject.toml`, and to be told what is being replaced |
58
+
59
+ ## 3. Functional Requirements
60
+
61
+ | ID | Requirement | Priority |
62
+ |----|-------------|----------|
63
+ | FR-0 | Every generated config is loaded by the real `cz` without error, and produces exactly the increments the wizard's bump table claims, for every convention × provider × scheme × tag-format combination under test. | Must |
64
+ | FR-1 | `commitizen` is a built-in catalog tool (`type: uvx`, `pip_name: commitizen`, `check_cmd: cz`, requires `uv`), so `devstuff install commitizen` works and `devstuff configure` offers the wizard afterwards. | Must |
65
+ | FR-2 | The wizard covers: commit convention, which commit types exist, what each bumps, changelog membership and heading per type, version provider/scheme/tag format/version files, release toggles, the questions `cz commit` asks, and the destination file. | Must |
66
+ | FR-3 | Two conventions — `cz_conventional_commits` (commitizen's own fixed rules, no `customize` block emitted) and `cz_customize` (everything configurable). | Must |
67
+ | FR-4 | The built-in type table reproduces `commitizen.defaults.BUMP_MAP` and `ConventionalCommitsCz`'s picker exactly: the nine conventional types selected, `feat`→MINOR, `fix`/`refactor`/`perf`→PATCH, everything else no-release, and `feat`/`fix`/`refactor`/`perf` in the changelog. | Must |
68
+ | FR-5 | Additional types (`chore`, `revert`, `deps`, `security`) are offered unselected; the user can also add their own with a key, description, bump level, changelog heading and hotkey. | Must |
69
+ | FR-6 | The generated `bump_map` lists the two breaking-change rules (`^.+!$`, `^BREAKING[\-\ ]CHANGE`) first, since commitizen stops at the first matching key. | Must |
70
+ | FR-7 | `bump_map_major_version_zero` is always emitted, derived from `bump_map` by demoting MAJOR to MINOR. | Must |
71
+ | FR-8 | `schema_pattern` accepts every message `message_template` can produce, including the breaking form, and always accepts the `bump:` prefix commitizen's own release commit uses. | Must |
72
+ | FR-9 | The review screen shows: a summary, a sample `cz commit` message, a bump table (`type → increment → resulting version` against 1.4.2), and the changelog sections with the types filed under each. | Must |
73
+ | FR-10 | "Check these rules against the real cz" builds a throwaway git repo tagged at 1.4.2, replays one commit per distinct bump level plus both breaking forms through `cz bump --dry-run`, and reports agreement or disagreement per rule. | Must |
74
+ | FR-11 | The same check runs automatically before saving. A disagreement is reported and requires confirmation, but never blocks the save — the user's config is the user's. | Must |
75
+ | FR-12 | "Preview a generated changelog" runs the real `cz changelog --dry-run` over one sample commit per selected type. | Should |
76
+ | FR-13 | The wizard detects, and defaults from: git root, existing commitizen configs (in commitizen's own search order, skipping files without a commitizen section), version provider and current version from project files, latest git tag, and an existing changelog file. | Must |
77
+ | FR-14 | Writing to `pyproject.toml` replaces every `[tool.commitizen…]` table and leaves the rest of the file byte for byte. The result is verified by parsing it back; if it does not match, the write falls back to a standalone `.cz.toml` and the original file is untouched. | Must |
78
+ | FR-15 | Any file about to be overwritten is copied to `<name>.bak.<timestamp>` first. A config not written by this wizard prompts before being replaced. | Must |
79
+ | FR-16 | Nothing is written until the user picks "Save"; "Cancel" writes nothing. | Must |
80
+ | FR-17 | After saving, if more than one commitizen config now exists in the project, the wizard names them and says which one commitizen will actually read. | Must |
81
+ | FR-18 | After saving into a git repo with no `commit-msg` hook, the wizard offers to install one running `cz check --allow-abort --commit-msg-file "$1"`. An existing hook is never replaced. | Should |
82
+ | FR-19 | `devstuff configure commitizen --path` prints the config file commitizen would read here, or where the wizard would create one. | Must |
83
+ | FR-20 | `devstuff configure commitizen --output PATH` writes a standalone config to `PATH` and touches nothing in the project — no destination question, no shadowing warning, no git hook. | Must |
84
+ | FR-21 | Without `cz` installed, the wizard still runs end to end; the live checks say so rather than failing. | Must |
85
+ | FR-22 | Two selected types sharing a hotkey is reported — commitizen does not validate it, and the second type simply becomes unreachable. | Should |
86
+ | FR-23 | Deselecting every type keeps the previous selection: an empty alternation would match everything. | Must |
87
+
88
+ ## 4. Non-Functional Requirements
89
+
90
+ | ID | Requirement |
91
+ |----|-------------|
92
+ | NFR-1 | No new runtime dependencies. TOML is emitted as text and read back with stdlib `tomllib`. |
93
+ | NFR-2 | A live check completes in a few seconds (~8 `cz` invocations); it is an explicit menu action plus one automatic run at save time, never on every redraw. |
94
+ | NFR-3 | Every live-check failure path degrades to the offline table. A check must never be able to end the wizard. |
95
+ | NFR-4 | The generated file is stable: the same answers produce the same bytes, and re-splicing an already-spliced `pyproject.toml` is a no-op. |
96
+ | NFR-5 | Unit tests run without `cz` installed. The commitizen-backed tests are marked `integration`. |
97
+
98
+ ## 5. Verification
99
+
100
+ | Requirement | How it is verified |
101
+ |-------------|--------------------|
102
+ | FR-0, FR-10 | `tests/integration/test_commitizen_config.py` — seven configs replayed through the real `cz` |
103
+ | FR-4 | `test_defaults_reproduce_commitizens_own_rules`, against constants read out of the installed package |
104
+ | FR-6, FR-7 | `test_bump_map_leads_with_the_breaking_rules`, `test_major_version_zero_demotes_only_the_breaking_rules` |
105
+ | FR-8 | `test_schema_pattern_accepts_the_wizards_own_messages`, `test_schema_pattern_always_accepts_commitizens_own_bump_commit` |
106
+ | FR-13 | `test_the_version_provider_is_read_off_the_project`, `test_existing_configs_follow_commitizens_search_order` |
107
+ | FR-14 | `test_splice_*` — including the fail-closed case and idempotency |
108
+ | FR-15, FR-18 | `test_save_backs_up_what_was_there`, `test_an_existing_commit_hook_is_never_touched` |
109
+ | FR-17 | `test_shadowing_another_config_is_called_out` |
110
+ | FR-22, FR-23 | `test_duplicate_shortcuts_are_reported`, `test_deselecting_everything_keeps_the_previous_types` |
111
+
112
+ ## 6. Open Questions
113
+
114
+ | Question | Status |
115
+ |----------|--------|
116
+ | Should the wizard read an existing config back into its state? | **Resolved 2026-07-31 — no.** See SD-6: partial reconstruction is worse than none, and the timestamped backup covers the real need. |
117
+ | Should a disagreement from the live check block saving? | **Resolved 2026-07-31 — no.** FR-11: it warns and asks. A check that can veto a save turns a helpful cross-check into an obstacle the moment commitizen changes. |
118
+ | Should `pre_bump_hooks`/`post_bump_hooks` be configurable? | **Deferred.** They are shell commands with eight environment variables; a text prompt for them would be a worse editor than `$EDITOR`. Hand-editable in the generated file. |
119
+ | Should the wizard offer a `.pre-commit-config.yaml` entry as well as a git hook? | **Deferred.** It means owning a second file's merge semantics; the git hook covers the same need with no new file format. |