docs(governance): Q-124 — one door, domain split, and failures that teach

Jeroen's shape for the tooling CLI: move the Python into a package with a
proper domain split, one door that answers everything with help, and errors
that hand back instructions rather than a status.

The domain split turns out to be discoverable rather than invented. tooling/ is
85 top-level entries — 37 loose .py, ~36 extensionless executables, 11 dirs of
which only 6 hold anything — across four coexisting naming conventions. But the
domains are already encoded as filename prefixes: blender x14, atlas x8,
generate x7, check x7, then visual/validate/test x3 and
godot/garment/pql/install x2. Those prefixes are the subcommand groups, which
is what makes the consolidation mechanical enough to be safe.

Two constraints recorded against "a new prompt not an error code", because
taken literally each would break something:

- Exit codes stay. Four of these run in the pre-push hook, which fails a push
  ONLY by non-zero exit; a tool that explains itself and exits 0 silently
  disables its own gate. That exact failure was observed in clide today, where
  unsupported-format, no-such-file and unknown-subsystem all returned 0.
  So: code AND message, never either/or.
- It must not become literally interactive. Agents and git hooks have no TTY,
  and the tea scar is already written down — its prompts "crash in Claude Code
  (no TTY)", which is why every tea call passes all flags explicitly. Any
  prompt must be TTY-gated and suppressible.

pql was cited as the precedent and measured rather than assumed. The principle
holds there for unknown subcommands (full usage dump) and not for invalid
values: `ticket status <id> nonsense` says invalid without naming the six legal
values it knows, `ticket new` says "accepts 2 arg(s)" without naming which two.
The gap is the closed sets, and it is the more common failure. Logged upstream
as pql T-112 rather than worked around here — the bar for our CLI is the
stronger one: whenever the accepted set is known, print it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 02:06:25 +02:00
co-authored by Claude Opus 5
parent 3a640f91f7
commit 284ce847c3
+18
View File
@@ -514,6 +514,24 @@ Technical foundation questions: engine, protocols, data structures, performance,
- **But the PATH problem is the actual requirement, and it is separate from the framework.** A `[project.scripts]` entrypoint lands in `.venv/bin/`, which is on PATH only when the venv is activated — and non-interactive shells (agents, git hooks) never activate it. That is the *same* split `VENV_PY` already papers over in the Makefile (`test -x .venv/bin/python || python3`). It is also exactly the failure recorded for `tea`: an absolute path breaks the `Bash(tea *)` rule and prompts every time, and the fix was a bare name on PATH. **So the deliverable is "one bare command reliably on PATH", not "a CLI framework"** — via `uv tool install` / `pipx` into `~/.local/bin`, or a symlink. Choose the framework second; a Typer app behind an absolute venv path solves nothing. - **But the PATH problem is the actual requirement, and it is separate from the framework.** A `[project.scripts]` entrypoint lands in `.venv/bin/`, which is on PATH only when the venv is activated — and non-interactive shells (agents, git hooks) never activate it. That is the *same* split `VENV_PY` already papers over in the Makefile (`test -x .venv/bin/python || python3`). It is also exactly the failure recorded for `tea`: an absolute path breaks the `Bash(tea *)` rule and prompts every time, and the fix was a bare name on PATH. **So the deliverable is "one bare command reliably on PATH", not "a CLI framework"** — via `uv tool install` / `pipx` into `~/.local/bin`, or a symlink. Choose the framework second; a Typer app behind an absolute venv path solves nothing.
- **It does not help friction (2), and may worsen (3).** Typer + Click are two more venv dependencies. And a single entrypoint that imports every subcommand eagerly pays all 123 modules' import cost on every invocation — including four times per push. Lazy subcommand registration is then mandatory, not an optimisation; measure it before and after. - **It does not help friction (2), and may worsen (3).** Typer + Click are two more venv dependencies. And a single entrypoint that imports every subcommand eagerly pays all 123 modules' import cost on every invocation — including four times per push. Lazy subcommand registration is then mandatory, not an optimisation; measure it before and after.
- **Net:** this looks like the answer for the check/gate family and the day-to-day scripts. It leaves the numpy/scipy/PIL planet-gen path untouched, which is the part a Rust port would have had to prove numerical equivalence for. Price this first. - **Net:** this looks like the answer for the check/gate family and the day-to-day scripts. It leaves the numpy/scipy/PIL planet-gen path untouched, which is the part a Rust port would have had to prove numerical equivalence for. Price this first.
- **Amendment 2026-08-20 (b) — the shape Jeroen wants: one door, domain-split behind it, and failures that teach.** *"it would involve moving all python into a separate dir with proper domain split so one door answers all options we have with help and instructions/help when there is an error: a new prompt not an error code."* Three requirements, and the third is the demanding one.
1. **A real package with a domain split.** `tooling/` is currently **85 top-level entries** — 37 loose `.py`, ~36 extensionless executables, and 11 directories of which only 6 hold anything (`planet-gen` 30, `garment-fit` 23, `economy-db` 17, `db` 9, `pql-migrate` 5, `wiki` 1). Four naming conventions coexist: `assign-astro-ids.py` (hyphen + extension), `canvas_sources.py` (underscore), `check-canvas-version` (no extension), `blender_*.py` (prefix-as-namespace).
**The domains are already there, encoded as filename prefixes**`blender` ×14, `atlas` ×8, `generate` ×7, `check` ×7, `visual` ×3, `validate` ×3, `test` ×3, then `godot`/`garment`/`pql`/`install` ×2. So the split is **discoverable, not invented**: those prefixes are the subcommand groups (`sr atlas verify`, `sr check canvas-version`, `sr blender process-bodies`). This is the strongest argument that the consolidation is mechanical enough to be safe.
2. **One door, with help at every level.** A single entrypoint whose `--help` enumerates the domains, and each domain's `--help` enumerates its verbs. Today there is no way to ask "what tooling exists" except `ls`, which is exactly how the `/d2-diagram` skill was mis-reported as missing on 2026-08-20 — a listing that only a directory read can answer is a listing that goes stale in someone's head.
3. **A failure returns the next command, not a status.** This is the requirement with real teeth. Jeroen: *"I have this in pql as well: errors become instructions."* The repo already does it in places worth copying — `check-canvas-version` fails with *"Run `make regen-db`"*, and `check-dataflow-graph` names the path that stopped resolving and says whether to fix the diagram or the path. The rule generalises: **every non-zero exit prints the command that would fix it.**
**Measured against pql (2026-08-20), the principle is established there but applied unevenly**, which sets a more precise bar than "be like pql":
| case | pql output | instruction? |
|---|---|---|
| `pql ticket frobnicate` | full usage + command list | **yes** |
| `pql ticket status T-1246 nonsense` | `invalid status "nonsense"` | no — and the valid set is 6 closed values the program knows |
| `pql ticket new` (no args) | `accepts 2 arg(s), received 0` | no — does not name which 2 |
| `pql ticket show T-99999` | `ticket T-99999 not found` | no — no "try `pql ticket list`" |
So **unknown-subcommand is solved and invalid-value is not** — and invalid-value is the more common failure, precisely because the valid set is closed and enumerable. The bar for this CLI: **whenever the accepted set is known, print it.** Logged upstream to `pql/feature-request.md` rather than worked around here.
- **Constraint the above must not break — exit codes stay.** *"a new prompt not an error code"* cannot mean dropping the code. Four of these run in the pre-push hook, which fails the push **only** by non-zero exit; a tool that explains itself and exits 0 silently disables its own gate. That failure mode was observed first-hand on 2026-08-20 in clide, where `unsupported image format`, `no such file`, and unknown-subsystem all returned exit 0, making every error indistinguishable from success to anything checking `$?`. **So: exit code AND actionable message, never either/or.**
- **And it must not become literally interactive.** If "a new prompt" means asking the user a question, that must be TTY-gated and suppressible (`--no-input`). Agents and git hooks have no TTY, and this repo already has that scar written down: `tea`'s interactive prompts *"crash in Claude Code (no TTY)"*, which is why every `tea` call is required to pass all flags explicitly. A helpful prompt that hangs a hook is worse than a terse exit code.
- **Raised by:** Jeroen, 2026-08-20: *"those python files are a pain. they throw permission prompts all over the place and the venv dependencies are annoying... We should ticket a Q record to retool that to rust probably. maybe make it into an actual cli of the quality level of pql. one can dream"* - **Raised by:** Jeroen, 2026-08-20: *"those python files are a pain. they throw permission prompts all over the place and the venv dependencies are annoying... We should ticket a Q record to retool that to rust probably. maybe make it into an actual cli of the quality level of pql. one can dream"*
- **Cross-reference:** [D-223](../decisions/architecture.md#d-223) and `.claude/rules/asset-pipeline.md` (the stamp contract a rewrite must preserve), [D-262](../decisions/architecture.md#d-262) (`check-dataflow-graph`, the most recent addition to the per-push Python set), `.claude/rules/ticket-cli.md` (`pql` as the quality bar being referenced). - **Cross-reference:** [D-223](../decisions/architecture.md#d-223) and `.claude/rules/asset-pipeline.md` (the stamp contract a rewrite must preserve), [D-262](../decisions/architecture.md#d-262) (`check-dataflow-graph`, the most recent addition to the per-push Python set), `.claude/rules/ticket-cli.md` (`pql` as the quality bar being referenced).