mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-27 02:44:56 +08:00
feat: #178 — argparse errors emit JSON envelope when --output-format json requested
Dogfood pinpoint: running 'claw nonexistent-command --output-format json' bypasses
the JSON envelope contract — argparse dumps human-readable usage to stderr with
exit 2, breaking the SCHEMAS.md guarantee that JSON mode returns structured output.
Problem:
$ claw nonexistent --output-format json
usage: main.py [-h] {summary,manifest,...} ...
main.py: error: argument command: invalid choice: 'nonexistent' (choose from ...)
[exit 2 — no envelope, claws must parse argparse usage messages]
Fix:
$ claw nonexistent --output-format json
{
"timestamp": "2026-04-22T11:00:29Z",
"command": "nonexistent-command",
"exit_code": 1,
"output_format": "json",
"schema_version": "1.0",
"error": {
"kind": "parse",
"operation": "argparse",
"target": "nonexistent-command",
"retryable": false,
"message": "invalid command or argument (argparse rejection)",
"hint": "run with no arguments to see available subcommands"
}
}
[exit 1, clean JSON envelope on stdout per SCHEMAS.md]
Changes:
- src/main.py:
- _wants_json_output(argv): pre-scan for --output-format json before parsing
- _emit_parse_error_envelope(argv, message): emit wrapped envelope on stdout
- main(): catch SystemExit from argparse; if JSON requested, emit envelope
instead of letting argparse's help dump go through
- tests/test_parse_error_envelope.py (new, 9 tests):
- TestParseErrorJsonEnvelope (7): unknown command, =syntax, text mode unchanged,
invalid flag, missing command, valid command unaffected, common fields
- TestParseErrorSchemaCompliance (2): error.kind='parse', retryable=false
Contract:
- text mode (default): unchanged — argparse dumps help to stderr, exits 2
- JSON mode: envelope per SCHEMAS.md, error.kind='parse', exit 1
- Parse errors always retryable=false (typo won't self-fix)
- error.kind='parse' already enumerated in SCHEMAS.md (no schema changes)
This closes a real gap: claws invoking unknown commands in JSON mode can now route
via exit code + envelope.kind='parse' instead of scraping argparse output.
Test results: 192 → 201 passing, 3 skipped unchanged, zero regression.
Pinpoint discovered via dogfood at 2026-04-22 19:59 KST (cycle #19).
This commit is contained in:
52
src/main.py
52
src/main.py
@@ -203,9 +203,59 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _emit_parse_error_envelope(argv: list[str], message: str) -> None:
|
||||
"""#178: emit JSON envelope for argparse-level errors when --output-format json is requested.
|
||||
|
||||
Pre-scans argv for --output-format json. If found, prints a parse-error envelope
|
||||
to stdout (per SCHEMAS.md 'error' envelope shape) instead of letting argparse
|
||||
dump help text to stderr. This preserves the JSON contract for claws that can't
|
||||
parse argparse usage messages.
|
||||
"""
|
||||
import json
|
||||
# Extract the attempted command (argv[0] is the first positional)
|
||||
attempted = argv[0] if argv and not argv[0].startswith('-') else '<missing>'
|
||||
envelope = wrap_json_envelope(
|
||||
{
|
||||
'error': {
|
||||
'kind': 'parse',
|
||||
'operation': 'argparse',
|
||||
'target': attempted,
|
||||
'retryable': False,
|
||||
'message': message,
|
||||
'hint': 'run with no arguments to see available subcommands',
|
||||
},
|
||||
},
|
||||
command=attempted,
|
||||
exit_code=1,
|
||||
)
|
||||
print(json.dumps(envelope))
|
||||
|
||||
|
||||
def _wants_json_output(argv: list[str]) -> bool:
|
||||
"""#178: check if argv contains --output-format json anywhere (for parse-error routing)."""
|
||||
for i, arg in enumerate(argv):
|
||||
if arg == '--output-format' and i + 1 < len(argv) and argv[i + 1] == 'json':
|
||||
return True
|
||||
if arg == '--output-format=json':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
import sys
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
# #178: catch argparse errors and emit JSON envelope when --output-format json present
|
||||
try:
|
||||
args = parser.parse_args(argv)
|
||||
except SystemExit as exc:
|
||||
# argparse exits with SystemExit on error (code 2) or --help (code 0)
|
||||
if exc.code != 0 and _wants_json_output(argv):
|
||||
# Reconstruct a generic parse-error message
|
||||
_emit_parse_error_envelope(argv, 'invalid command or argument (argparse rejection)')
|
||||
return 1
|
||||
raise
|
||||
manifest = build_port_manifest()
|
||||
if args.command == 'summary':
|
||||
print(QueryEnginePort(manifest).render_summary())
|
||||
|
||||
Reference in New Issue
Block a user