feat(config): T-1275 — bare reach is discovery, so it exits 0

Bare `reach` and bare `reach <domain>` printed help and exited 2, Click's
usage-error convention. Running reach with no arguments is the DISCOVERY
action — it is how the tool gets learned from nothing — and a caller that
branches on exit status would read its own onboarding as a failure. Now they
exit 0.

D-263's exit-code contract is untouched: it governs failures, and printing a
command list is not one. Verified across the whole matrix, because this change
flirts with the exit-0 trap that record opens with — bare 0, bare domain 0,
--help 0, unknown domain 2, unknown verb 2, real failure 1. All five are now
pinned as a sixth conformance invariant, since an exit code regresses silently
and nothing else would notice. Proven to fail by putting the 2 back.

The implementation also collapses a duplicated class. core/cli.py holds
ReachGroup with both shared behaviours — no-args-prints-help-and-exits-0, and
unknown-name-enumerates — and LazyDomainGroup now extends it instead of
subclassing TyperGroup directly, keeping only the laziness and the
domain-specific wording. The enumeration logic previously existed twice in
slightly different forms, which is how the root and the domains would have
drifted into disagreeing about their own conventions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 15:49:08 +02:00
co-authored by Claude Opus 5
parent 91e25a3e7a
commit b5beda0df7
6 changed files with 105 additions and 9 deletions
+17
View File
@@ -1869,3 +1869,20 @@ FINDING — env-prefixed calls will still prompt, and that is accepted.
Permission rules prefix-match the WHOLE command string (root CLAUDE.md: `PATH=/opt/bin make build` does not match `Bash(make *)`). So `SR_REPO_ROOT=... reach ...` and `SR_OUTPUT_FORMAT=text reach ...` do not match Bash(reach *) and will prompt. Deliberately not worked around: an environment override IS a genuine departure from normal invocation, and it is the ordinary form that needs to be frictionless. Recorded in docs/DEVOPS.md with the mitigation for tests pass overrides through the subprocess environment rather than the command string, as tooling/test_check_parity.py does.
REVISITED 2026-08-31: the env-prefix limitation this ticket recorded as ''accepted'' deserves a second look, because the primary user of reach is an AGENT (D-263, stated 2026-08-31), not a human at a terminal. Permission rules prefix-match the whole command string, so SR_REPO_ROOT=... reach ... and SR_OUTPUT_FORMAT=text reach ... do not match Bash(reach *) and will prompt. I called that acceptable on the grounds that an env override is a departure from normal invocation which is true for a human and less true for me, since the override forms are exactly what a test harness and a debugging session reach for. NOT changing it here: the ordinary form is frictionless, the override forms are rare in normal use, and a broad Bash(env *) grant would be a much larger permission surface for a small convenience. Recorded so the tradeoff is visible if it starts costing prompts in practice if it does, the fix is a narrow rule per variable, not a blanket one.', NULL, '2026-08-31 13:41:02', '2026-08-31 13:41:02.169', '2026-08-31 13:41:02.169', NULL, '00216fed794c544c5b7e8264f83e835f', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'description', 'Bare and bare currently print help and exit 2 — Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0. Reason, and it follows from the primary-user statement in D-263: running with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened — that contract governs FAILURES, and printing the domain list is not one. Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours — no-args-prints-help-and-exits-0, and unknown-command-enumerates — and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.', 'Bare reach and bare ''reach <domain>'' currently print help and exit 2 — Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0.
Reason, and it follows from the primary-user statement in D-263: running reach with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened that contract governs FAILURES, and printing the domain list is not one.
Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours no-args-prints-help-and-exits-0, and unknown-command-enumerates and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.', NULL, '2026-08-31 13:44:35', '2026-08-31 13:44:35.093', '2026-08-31 13:44:35.093', NULL, 'edeb0265c65a890c323be51c862c09a4', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'status', 'backlog', 'in_progress', NULL, '2026-08-31 13:44:35', '2026-08-31 13:44:35.236', '2026-08-31 13:44:35.236', NULL, '40e04d525d6616bf146d81c00ef82dd0', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'description', 'Bare reach and bare ''reach <domain>'' currently print help and exit 2 — Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0.
Reason, and it follows from the primary-user statement in D-263: running reach with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened that contract governs FAILURES, and printing the domain list is not one.
Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours no-args-prints-help-and-exits-0, and unknown-command-enumerates and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.', 'Bare reach and bare ''reach <domain>'' currently print help and exit 2 Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0.
Reason, and it follows from the primary-user statement in D-263: running reach with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened that contract governs FAILURES, and printing the domain list is not one.
Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours no-args-prints-help-and-exits-0, and unknown-command-enumerates and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.
DONE 2026-08-31. Bare reach and bare ''reach <domain>'' print help and exit 0. Verified across the full matrix, because this change flirts with exactly the trap D-263 opens with making failures pass: bare reach 0, bare domain 0, --help 0, unknown domain 2, unknown verb 2, real failure (bad SR_REPO_ROOT) 1. All five are now PINNED in tooling/test_conformance.py as a sixth invariant, since an exit code is the kind of thing that regresses silently and no other assertion would notice. Proven to fail: setting the exit back to 2 made the test report both discovery cases by name. IMPLEMENTATION also removed a duplicated class, which is the part worth keeping. core/cli.py now holds ReachGroup carrying both shared behaviours — no-args-prints-help-and-exits-0, and unknown-name-enumerates — and main.py''s LazyDomainGroup EXTENDS it rather than subclassing TyperGroup directly, so it keeps only the lazy loading and the domain-specific wording. Before this the enumeration logic existed twice in slightly different forms, which is the drift that would have left the root and the domains disagreeing about their own conventions. ReachDomainGroup survives as an alias because domain routers read better with the name, but there is no separate behaviour: a domain group IS a reach group.', NULL, '2026-08-31 13:48:56', '2026-08-31 13:48:56.725', '2026-08-31 13:48:56.725', NULL, '0672c5ef76cf2432b82e0423a7c79a79', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -34,3 +34,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FZCVZXH6M00C57EXMZD0SG', 'T-1272', '2026-08-31 13:23:53.471', '2026-08-31 13:23:53.471', NULL, 'f193e26d0e4314a88014a2dc23a6e335', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FZDN9YBP0MZ021N3TJCFXM', 'T-1273', '2026-08-31 13:23:59.951', '2026-08-31 13:23:59.951', NULL, 'b9643fcc4583630d76552c35dc0ed48d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5FZEM6JT5KKVE1RZ68DENSC', 'T-1274', '2026-08-31 13:24:07.865', '2026-08-31 13:24:07.865', NULL, 'dfd2948f52c1342b42f88097c0c43274', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'T-1275', '2026-08-31 13:44:24.225', '2026-08-31 13:44:24.225', NULL, '7870375af5df61aea8e9de0a34c98796', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
+18
View File
@@ -2138,3 +2138,21 @@ FINDING — env-prefixed calls will still prompt, and that is accepted.
Permission rules prefix-match the WHOLE command string (root CLAUDE.md: `PATH=/opt/bin make build` does not match `Bash(make *)`). So `SR_REPO_ROOT=... reach ...` and `SR_OUTPUT_FORMAT=text reach ...` do not match Bash(reach *) and will prompt. Deliberately not worked around: an environment override IS a genuine departure from normal invocation, and it is the ordinary form that needs to be frictionless. Recorded in docs/DEVOPS.md with the mitigation for tests pass overrides through the subprocess environment rather than the command string, as tooling/test_check_parity.py does.
REVISITED 2026-08-31: the env-prefix limitation this ticket recorded as ''accepted'' deserves a second look, because the primary user of reach is an AGENT (D-263, stated 2026-08-31), not a human at a terminal. Permission rules prefix-match the whole command string, so SR_REPO_ROOT=... reach ... and SR_OUTPUT_FORMAT=text reach ... do not match Bash(reach *) and will prompt. I called that acceptable on the grounds that an env override is a departure from normal invocation which is true for a human and less true for me, since the override forms are exactly what a test harness and a debugging session reach for. NOT changing it here: the ordinary form is frictionless, the override forms are rare in normal use, and a broad Bash(env *) grant would be a much larger permission surface for a small convenience. Recorded so the tradeoff is visible if it starts costing prompts in practice if it does, the fix is a narrow rule per variable, not a blanket one.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:41:43.428', '2026-08-31 13:41:02.168', NULL, '0d424100c4529a4ad0ac8ca2111edfbd', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'task', '06G1S37Y1ARMV68RJT802Z3VPG', 'Bare reach is an alias for --help, and exits 0', 'Bare and bare currently print help and exit 2 — Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0. Reason, and it follows from the primary-user statement in D-263: running with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened — that contract governs FAILURES, and printing the domain list is not one. Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours — no-args-prints-help-and-exits-0, and unknown-command-enumerates — and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-31 13:44:24.225', '2026-08-31 13:44:24.225', NULL, '9d858d6e3cae73056736dc0ccd6f3997', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'task', '06G1S37Y1ARMV68RJT802Z3VPG', 'Bare reach is an alias for --help, and exits 0', 'Bare reach and bare ''reach <domain>'' currently print help and exit 2 — Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0.
Reason, and it follows from the primary-user statement in D-263: running reach with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened that contract governs FAILURES, and printing the domain list is not one.
Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours no-args-prints-help-and-exits-0, and unknown-command-enumerates and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-31 13:44:24.225', '2026-08-31 13:44:35.092', NULL, '1f18d7bb6ffe7f17a7cc833c72f56962', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'task', '06G1S37Y1ARMV68RJT802Z3VPG', 'Bare reach is an alias for --help, and exits 0', 'Bare reach and bare ''reach <domain>'' currently print help and exit 2 — Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0.
Reason, and it follows from the primary-user statement in D-263: running reach with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened that contract governs FAILURES, and printing the domain list is not one.
Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours no-args-prints-help-and-exits-0, and unknown-command-enumerates and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 13:44:24.225', '2026-08-31 13:44:35.236', NULL, 'c3a6bef2d40b5a37e992a5944920e6bb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5G433M6K7YSX3ZQFAMW95GC', 'task', '06G1S37Y1ARMV68RJT802Z3VPG', 'Bare reach is an alias for --help, and exits 0', 'Bare reach and bare ''reach <domain>'' currently print help and exit 2 — Click''s no_args_is_help raises a usage error. That was flagged as an open question on T-1260 and is now settled: they exit 0.
Reason, and it follows from the primary-user statement in D-263: running reach with no arguments is the DISCOVERY action, not a mistake. An agent learning what exists should not receive a failure code for succeeding, and one that branches on exit status would treat its own onboarding as an error. Nothing about D-263''s exit-code contract is weakened that contract governs FAILURES, and printing the domain list is not one.
Implementation note: this also removes a duplicated group class. core/cli.py gains ReachGroup as the shared base carrying both behaviours no-args-prints-help-and-exits-0, and unknown-command-enumerates and main.py''s LazyDomainGroup extends it rather than reimplementing the enumeration, keeping only the lazy loading and the domain-specific wording.
DONE 2026-08-31. Bare reach and bare ''reach <domain>'' print help and exit 0. Verified across the full matrix, because this change flirts with exactly the trap D-263 opens with making failures pass: bare reach 0, bare domain 0, --help 0, unknown domain 2, unknown verb 2, real failure (bad SR_REPO_ROOT) 1. All five are now PINNED in tooling/test_conformance.py as a sixth invariant, since an exit code is the kind of thing that regresses silently and no other assertion would notice. Proven to fail: setting the exit back to 2 made the test report both discovery cases by name. IMPLEMENTATION also removed a duplicated class, which is the part worth keeping. core/cli.py now holds ReachGroup carrying both shared behaviours — no-args-prints-help-and-exits-0, and unknown-name-enumerates — and main.py''s LazyDomainGroup EXTENDS it rather than subclassing TyperGroup directly, so it keeps only the lazy loading and the domain-specific wording. Before this the enumeration logic existed twice in slightly different forms, which is the drift that would have left the root and the domains disagreeing about their own conventions. ReachDomainGroup survives as an alias because domain routers read better with the name, but there is no separate behaviour: a domain group IS a reach group.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 13:44:24.225', '2026-08-31 13:48:56.725', NULL, 'befbb50fcdf4b6b7e53e298311d13539', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
+30 -7
View File
@@ -19,17 +19,35 @@ from __future__ import annotations
import typer
from typer.core import TyperGroup
from tooling.core import console
class ReachDomainGroup(TyperGroup):
"""A domain group whose unknown-verb error names the verbs that exist.
Click's default is `No such command 'x'` — which tells you that you are
wrong without telling you what would be right. That is the closed-set gap
D-263 measured in pql (an invalid status rejected without naming the six
valid ones), and the fix is nearly free: the verb list is already registered
on the group, so enumerating it costs a sort.
class ReachGroup(TyperGroup):
"""Shared behaviour for every group in `reach` — the root and each domain.
Two things, both of which exist because the primary user is an agent
(D-263) rather than a person at a terminal.
**No arguments means "what is here?", not "you got it wrong".** Click's
`no_args_is_help` prints help and exits **2**, a usage error. But running
`reach` or `reach <domain>` bare is the DISCOVERY action — it is how the
tool gets learned from nothing — and a caller that branches on exit status
would read its own onboarding as a failure. So help is printed and the exit
is **0**. This does not weaken D-263's exit-code contract, which governs
*failures*; printing a command list is not one.
**An unknown name enumerates what exists.** Click's default is
`No such command 'x'`, which says you are wrong without saying what would
be right — the closed-set gap D-263 measured in pql. The list is already
registered on the group, so naming it costs a sort.
"""
def parse_args(self, ctx: typer.Context, args: list[str]) -> list[str]:
if not args and self.no_args_is_help and not ctx.resilient_parsing:
console.out(ctx.get_help())
ctx.exit(0)
return super().parse_args(ctx, args)
def resolve_command(self, ctx: typer.Context, args: list[str]):
if args and self.get_command(ctx, args[0]) is None:
listed = ", ".join(sorted(self.list_commands(ctx)))
@@ -37,6 +55,11 @@ class ReachDomainGroup(TyperGroup):
return super().resolve_command(ctx, args)
# Kept as a name because domain routers read better with it, but there is no
# separate behaviour: a domain group IS a reach group.
ReachDomainGroup = ReachGroup
def domain(name: str, help: str) -> typer.Typer:
"""Build a domain's Typer app with the house settings applied.
+7 -2
View File
@@ -35,7 +35,8 @@ from __future__ import annotations
import importlib
import typer
from typer.core import TyperGroup
from tooling.core.cli import ReachGroup
# The domain registry: name -> (import target, one-line help).
#
@@ -53,9 +54,13 @@ DOMAINS: dict[str, tuple[str, str]] = {
}
class LazyDomainGroup(TyperGroup):
class LazyDomainGroup(ReachGroup):
"""Lists domains without importing them; imports exactly the one invoked.
Extends `ReachGroup`, so bare `reach` prints the domain list and exits 0
like every domain group does. Only the laziness and the domain-specific
wording live here.
Three overrides, and the third is the one that matters. `TyperGroup`'s own
`format_commands` loops over `list_commands` calling `get_command` on each,
just to read a short help string off the loaded command — which, with lazy
+32
View File
@@ -31,6 +31,7 @@ Run: python3 tooling/test_conformance.py
"""
import ast
import shutil
import subprocess
import sys
from pathlib import Path
@@ -195,12 +196,43 @@ def check_errors_name_a_remedy(failures: list[str]) -> None:
)
def check_exit_codes(failures: list[str]) -> None:
"""(6) Discovery succeeds; being wrong fails.
The distinction is easy to break in either direction, and both directions
are bad in ways nothing else would catch. Make discovery a usage error and
an agent reads its own onboarding as a failure. Make a wrong name succeed
and a typo in a hook passes silently — the exit-0 trap D-263 opens with.
"""
cases = [
([], 0, "bare `reach` is discovery, not a usage error"),
(["check"], 0, "bare `reach <domain>` is discovery, not a usage error"),
(["--help"], 0, "--help succeeds"),
(["definitely-not-a-domain"], 2, "an unknown domain is a usage error"),
(["check", "definitely-not-a-verb"], 2, "an unknown verb is a usage error"),
]
for args, expected, why in cases:
result = subprocess.run(
["reach", *args], capture_output=True, text=True, cwd=REPO_ROOT
)
if result.returncode != expected:
failures.append(
f"[exit] `reach {' '.join(args)}` exited {result.returncode}, "
f"expected {expected}{why}"
)
def main() -> int:
if shutil.which("reach") is None:
print("test_conformance: `reach` is not on PATH.\n Fix: make install-reach", file=sys.stderr)
return 1
failures: list[str] = []
check_transport_isolation(failures)
check_single_output_path(failures)
check_commands_decorated(failures)
check_errors_name_a_remedy(failures)
check_exit_codes(failures)
if failures:
print("test_conformance: FAIL", file=sys.stderr)