00Research
Issue: ty5dx · Generated 2026-09-08
01Strategy
The issue frames the change as “when args.action is empty, substitute the
default.” That framing is wrong and would silently fail one of the acceptance criteria.
service and action are both nargs="?" positionals, so
argparse consumes the -- separator and assigns the first column filter to
action: awsquery ec2 -- InstanceId parses
to action='InstanceId' — indistinguishable, post-parse, from
awsquery ec2 InstanceId.
The fix that works is pre-parse argv injection: a pure function scans the raw token list,
finds the first non-flag token (honouring VALUE_FLAGS consuming a following
value), and — if the next token is absent, is --, or starts with
- — splices the configured default action in right after the service.
Everything downstream then sees exactly the argv the user “would have typed”, so
the acceptance criteria become true by construction rather than by a parallel code path.
Post-parse substitution cannot work.
Executing the project’s own parser configuration shows
ec2 -- InstanceId and ec2 InstanceId collapse to the same
parsed state. The distinguishing information exists only in the raw token list, so the
substitution must happen before parse_known_args() at
cli.py:752.
snake_case in the YAML turns the shortcut into a blocking prompt.
security.is_readonly_operation() only PascalCases its
input when the string contains a -. Given describe_instances it
returns False, falls through to prompt_unsafe_operation(), and
blocks on input(). The map must hold kebab-case.
Equivalence was demonstrated, not assumed.
A replay of main()’s reorder pass plus the real
parse_multi_level_filters_for_mode over 11 bare/explicit command pairs
— flags before and after the service, -p with a value,
--region, double separators, trailing -- — produced
identical parse state for every pair.
All 50 curated entries pass four automated gates.
Real botocore operation, zero required members, ReadOnly by the
project’s own prefix rule, canonical kebab spelling — plus every one already
has curated columns in default_filters.yaml. Five of the 55 services are
skipped because they have no parameterless ReadOnly operation at all.
Six entries are editorial judgement calls.
iam, ssm, sagemaker,
glue, backup and athena are mechanically valid but
arguable. iam: list-users is the highest-disagreement pick.
Primary recommendation
Add _inject_default_action(argv) to cli.py, call it on
sys.argv[1:] immediately after argcomplete.autocomplete(...) and
pass the result to parse_known_args(); store kebab-case in
src/awsquery/default_actions.yaml and register it in
pyproject.toml:82; split the cli.py:830 dead-end into a
no-service branch (stdout, exit 0) and a no-default branch (stderr action list, exit 1).
02Flow
The recommended change surface, in the order the research says it should be built — each node corresponds to a section of this document.
src/awsquery/default_actions.yaml (50 kebab-case entries)
and register it in pyproject.toml:82 in the same commit.
load_default_actions() and get_default_action()
in config.py, mirroring load_default_filters() /
get_default_columns().
_inject_default_action(argv) in cli.py, called
on sys.argv[1:] before parse_known_args() at line 752.
cli.py:830; add _print_service_actions()
reusing get_service_operations + get_service_valid_operations.
test_main_function_service_listing.
main() parser
epilog examples.
03Decisions
Every entry the research resolves, with the rejected alternative recorded where there was one. The first three were left to the researcher’s discretion by CONTEXT.md.
Insertion point in main()
Inject the default action into the raw token list before the first
parse_known_args() call, leaving sys.argv unmutated. Downstream
code — _build_filter_argv, the two-pass reorder,
parse_multi_level_filters_for_mode, validate_readonly,
determine_column_filters — needs no change.
Rejected: substituting after parsing on
args.action is None. Proven insufficient — argparse has already eaten
the -- and promoted the first column filter into the action slot.
YAML value casing
Chosen · kebab-case, mandatoryStore ec2: describe-instances, and normalise
defensively with to_kebab_case(normalize_action_name(v)) at the injection
boundary so either spelling in the file is safe.
Rejected: snake_case (the spelling CONTEXT.md’s
example shows). It defeats is_readonly_operation’s prefix match and
makes awsquery ec2 hang on an interactive confirmation prompt.
Fallback action listing
Chosen · verbatim reuseA 10-line _print_service_actions() running the same
four steps as action_completer (cli.py:638-655). All three helpers are already
imported into cli.py — zero new imports.
Rejected: extracting a shared helper. The completer also
mutates _current_completion_context and swallows every exception, so the
shared core would be a three-line comprehension.
Treatment of an unrecognised second positional
Chosen · leave as todayawsquery ec2 prod keeps current behavior. The injector
only fires when the token after the service is absent, is --, or starts with
-.
Rejected: looking the token up in
get_service_operations() and demoting non-operations to value filters. A typo
like describe-instance would then silently run describe-instances
with a spurious filter and print zero rows — and it adds a botocore model load to the
hot path.
Required-parameter check in the validation test
Chosen · botocore LoaderUse botocore.loaders.Loader as shapes.py
does — no region, no credentials. Guard input_shape is None and match
operation names case-insensitively for acronym operations.
Rejected: core.check_parameter_requirements
for the test. It is correct at runtime but builds a live boto3 client, which needs a region
and sometimes credentials.
Test placement
Chosen · extend five existing filesLoader tests to test_config_loading_simple.py; the
whole-map guard to test_default_column_filters.py; injector and equivalence
tests to test_cli_parser.py; flag pass-through to
test_cli_flags.py; the fallback to
tests/integration/test_end_to_end.py.
Rejected: a new
tests/unit/test_default_actions.py. CLAUDE.md forbids duplicate test
files.
Six curated picks that are editorially arguable
Open · needs a maintainer glanceiam: list-users (vs list-roles),
ssm: describe-parameters, sagemaker: list-endpoints,
glue: get-databases, backup: list-backup-vaults,
athena: list-work-groups. All pass the four mechanical gates; the tie-breaker
used was “the service’s root resource”. Each is a one-line change.
An existing integration test goes vacuously green
Risk · must be rewrittentest_end_to_end.py:889 guards its assertions with
if "Available services:" in output. After this change that string is gone, so
the test passes while main() actually attempts a real AWS call under a
partially-mocked session. Repoint it at a service with no default and assert the stderr
listing plus a non-zero exit.
04Detail
The canonical RESEARCH.md above is embedded verbatim as data and rendered as
text — it carries the full seven-question analysis, the complete 50-entry curated map,
the <interfaces> block, the pitfall catalogue and the source ledger.
05Provenance
Generated from the canonical .md. This HTML is never
hand-edited — if the .md changes, regenerate with
/issue:view.