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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user