agent-first-data 0.20.0__tar.gz → 0.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/PKG-INFO +3 -13
  2. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/README.md +2 -12
  3. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data/cli.py +98 -26
  4. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data.egg-info/PKG-INFO +3 -13
  5. agent_first_data-0.22.0/agent_first_data.egg-info/SOURCES.txt +14 -0
  6. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/pyproject.toml +1 -4
  7. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/tests/test_cli.py +177 -13
  8. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/tests/test_no_stderr_policy.py +0 -2
  9. agent_first_data-0.20.0/agent_first_data/assets/skills/agent-first-data/SKILL.md +0 -280
  10. agent_first_data-0.20.0/agent_first_data/assets/skills/agent-first-data/references/protocol-v1.schema.json +0 -66
  11. agent_first_data-0.20.0/agent_first_data/assets/skills/agent-first-data/references/registry.json +0 -251
  12. agent_first_data-0.20.0/agent_first_data/assets/skills/agent-first-data/references/rules.md +0 -358
  13. agent_first_data-0.20.0/agent_first_data/skill.py +0 -663
  14. agent_first_data-0.20.0/agent_first_data/stream_redirect.py +0 -176
  15. agent_first_data-0.20.0/agent_first_data.egg-info/SOURCES.txt +0 -22
  16. agent_first_data-0.20.0/tests/test_skill.py +0 -267
  17. agent_first_data-0.20.0/tests/test_stream_redirect.py +0 -64
  18. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data/__init__.py +0 -0
  19. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data/assets/protocol-v1.schema.json +0 -0
  20. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data/assets/registry.json +0 -0
  21. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data/format.py +0 -0
  22. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
  23. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/agent_first_data.egg-info/top_level.txt +0 -0
  24. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/setup.cfg +0 -0
  25. {agent_first_data-0.20.0 → agent_first_data-0.22.0}/tests/test_format.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.20.0
3
+ Version: 0.22.0
4
4
  Summary: A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
@@ -28,15 +28,7 @@ print(render(value, OutputFormat.JSON))
28
28
  print(render(value, OutputFormat.PLAIN))
29
29
  ```
30
30
 
31
- Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
32
-
33
- Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
34
-
35
- ```python
36
- from agent_first_data import init_logging_json
37
-
38
- init_logging_json("INFO", secret_names=("authorization",))
39
- ```
31
+ Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`.
40
32
 
41
33
  ## Behavior Notes
42
34
 
@@ -45,12 +37,10 @@ init_logging_json("INFO", secret_names=("authorization",))
45
37
  - YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
46
38
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
47
39
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
48
- - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
49
- - Use `agent_first_data.stream_redirect.install_from_raw_args()` before version/help handling if a CLI exposes `--stdout-file` or `--stderr-file`; stderr is redirected as native diagnostics, not JSON.
40
+ - Use `cli_handle_version_or_continue()` before argument parsing so `--version`/`-V` always answers with a structured protocol-v1 `kind:"result"` version event — JSON by default, or `--output yaml|plain`/`--json` for another format; there is no conventional bare-text form. Pass your own value-taking global flag names so their value is not mistaken for the subcommand boundary.
50
41
 
51
42
  ## Reference
52
43
 
53
- - Full convention and API groups: [docs/overview.md](https://github.com/agentfirstkit/agent-first-data/blob/main/docs/overview.md)
54
44
  - Formal cross-language contract: [spec/agent-first-data.md](https://github.com/agentfirstkit/agent-first-data/blob/main/spec/agent-first-data.md)
55
45
  - Conformance fixtures: [spec/fixtures](https://github.com/agentfirstkit/agent-first-data/tree/main/spec/fixtures)
56
46
  - Agent skill: [skills/agent-first-data/SKILL.md](https://github.com/agentfirstkit/agent-first-data/blob/main/skills/agent-first-data/SKILL.md)
@@ -19,15 +19,7 @@ print(render(value, OutputFormat.JSON))
19
19
  print(render(value, OutputFormat.PLAIN))
20
20
  ```
21
21
 
22
- Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
23
-
24
- Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
25
-
26
- ```python
27
- from agent_first_data import init_logging_json
28
-
29
- init_logging_json("INFO", secret_names=("authorization",))
30
- ```
22
+ Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`.
31
23
 
32
24
  ## Behavior Notes
33
25
 
@@ -36,12 +28,10 @@ init_logging_json("INFO", secret_names=("authorization",))
36
28
  - YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
37
29
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
38
30
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
39
- - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
40
- - Use `agent_first_data.stream_redirect.install_from_raw_args()` before version/help handling if a CLI exposes `--stdout-file` or `--stderr-file`; stderr is redirected as native diagnostics, not JSON.
31
+ - Use `cli_handle_version_or_continue()` before argument parsing so `--version`/`-V` always answers with a structured protocol-v1 `kind:"result"` version event — JSON by default, or `--output yaml|plain`/`--json` for another format; there is no conventional bare-text form. Pass your own value-taking global flag names so their value is not mistaken for the subcommand boundary.
41
32
 
42
33
  ## Reference
43
34
 
44
- - Full convention and API groups: [docs/overview.md](https://github.com/agentfirstkit/agent-first-data/blob/main/docs/overview.md)
45
35
  - Formal cross-language contract: [spec/agent-first-data.md](https://github.com/agentfirstkit/agent-first-data/blob/main/spec/agent-first-data.md)
46
36
  - Conformance fixtures: [spec/fixtures](https://github.com/agentfirstkit/agent-first-data/tree/main/spec/fixtures)
47
37
  - Agent skill: [skills/agent-first-data/SKILL.md](https://github.com/agentfirstkit/agent-first-data/blob/main/skills/agent-first-data/SKILL.md)
@@ -385,39 +385,84 @@ class CliEmitter:
385
385
  return self.finish(json_result(payload).build(), 0)
386
386
 
387
387
 
388
- def build_cli_version(version: str) -> dict:
388
+ def _split_flag(arg: str) -> tuple[str | None, str | None]:
389
+ """Split a flag token into its long name and optional inline ``=value``.
390
+
391
+ Mirrors the Rust pre-parser: leading dashes are stripped from the name and a
392
+ bare ``-`` (or an empty name) yields ``(None, None)``. ``--flag`` →
393
+ ``("flag", None)``; ``--flag=x`` → ``("flag", "x")`` (``x`` may be ``""``).
394
+ """
395
+ if not arg.startswith("-") or arg == "-":
396
+ return (None, None)
397
+ flag, sep, value = arg.partition("=")
398
+ name = flag.lstrip("-")
399
+ if not name:
400
+ return (None, None)
401
+ if sep:
402
+ return (name, value)
403
+ return (name, None)
404
+
405
+
406
+ def build_cli_version(
407
+ name: str,
408
+ display_name: str | None,
409
+ version: str,
410
+ build: str | None,
411
+ ) -> dict:
389
412
  """Build a standard CLI version result event.
390
413
 
391
- The result payload always carries ``code: "version"`` alongside
392
- ``version``, so structured consumers can dispatch on ``code`` the same
393
- way they would for any other result shape.
414
+ The result payload always carries ``code: "version"`` alongside ``name`` and
415
+ ``version``, so structured consumers can dispatch on ``code`` the same way
416
+ they would for any other result shape. ``name`` is the short/bin identity
417
+ (e.g. ``"afdata"``); ``display_name`` is an optional human-facing product
418
+ name (e.g. ``"Agent-First Data"``); ``build`` is an opaque caller-supplied
419
+ identifier (a git commit SHA, for example) — its meaning is entirely up to
420
+ the caller. Both ``display_name`` and ``build`` are ``None`` when
421
+ unavailable, and simply absent from the payload.
394
422
  """
395
- return json_result({"code": "version", "version": version}).build().to_dict()
423
+ payload: dict[str, Any] = {"code": "version", "name": name, "version": version}
424
+ if display_name is not None:
425
+ payload["display_name"] = display_name
426
+ if build is not None:
427
+ payload["build"] = build
428
+ return json_result(payload).build().to_dict()
396
429
 
397
430
 
398
431
  def cli_render_version(
399
432
  name: str,
433
+ display_name: str | None,
400
434
  version: str,
401
- format: OutputFormat | None = None,
435
+ build: str | None,
436
+ format: OutputFormat,
402
437
  ) -> str:
403
- """Render CLI version output.
438
+ """Render a CLI version response as a protocol-v1 event in ``format``.
404
439
 
405
- Pass an OutputFormat for AFDATA JSON/YAML/plain. Pass None to preserve
406
- conventional "<name> <version>" text.
440
+ There is no conventional ``"<name> <version>"`` path: ``--version`` always
441
+ answers with the structured event (see :func:`build_cli_version`).
407
442
  """
408
- rendered = (
409
- f"{name} {version}" if format is None else render(build_cli_version(version), format)
410
- )
443
+ rendered = render(build_cli_version(name, display_name, version, build), format)
411
444
  return rendered.rstrip("\n") + "\n"
412
445
 
413
446
 
414
- def cli_handle_version_or_continue(raw_args: list[str], name: str, version: str) -> str | None:
447
+ def cli_handle_version_or_continue(
448
+ raw_args: list[str],
449
+ value_flags: list[str],
450
+ name: str,
451
+ display_name: str | None,
452
+ version: str,
453
+ build: str | None,
454
+ ) -> str | None:
415
455
  """Render version output if --version/-V is present; otherwise return None.
416
456
 
417
- One blessed behavior, no configurable knobs: a bare version request
418
- (``--version``/``-V`` with no output flag) always renders conventional
419
- ``"<name> <version>\\n"`` text; adding ``--json`` or ``--output
420
- <json|yaml|plain>`` selects the structured AFDATA rendering instead.
457
+ ``raw_args`` is ``sys.argv[1:]`` (no program name); scanning starts at index
458
+ 0. ``value_flags`` names the caller's own value-taking global flags (with or
459
+ without leading dashes) so their value is never mistaken for the subcommand
460
+ boundary — the Python stand-in for Rust's ``&clap::Command`` lookup.
461
+
462
+ The one blessed behavior: ``--version``/``-V`` always answers with a
463
+ protocol-v1 ``kind:"result"`` version event (see :func:`build_cli_version`) —
464
+ JSON by default, or ``--json`` / ``--output <json|yaml|plain>`` to select
465
+ another format.
421
466
 
422
467
  Only a top-level version request is recognized: scanning stops at the first
423
468
  positional argument (the subcommand), so ``tool sub --version <value>``
@@ -428,6 +473,7 @@ def cli_handle_version_or_continue(raw_args: list[str], name: str, version: str)
428
473
  ``--version --output xml``. The caller should convert that to a CLI error
429
474
  with ``build_cli_error``.
430
475
  """
476
+ value_flag_names = {vf.lstrip("-") for vf in value_flags}
431
477
  version_requested = False
432
478
  output_format: OutputFormat | None = None
433
479
  output_error: ValueError | None = None
@@ -442,6 +488,8 @@ def cli_handle_version_or_continue(raw_args: list[str], name: str, version: str)
442
488
  # git/cargo/clap: this pre-parser only owns a top-level version request.
443
489
  if not arg.startswith("-"):
444
490
  break
491
+
492
+ flag_name, inline_value = _split_flag(arg)
445
493
  if arg in ("--version", "-V"):
446
494
  version_requested = True
447
495
  i += 1
@@ -455,17 +503,25 @@ def cli_handle_version_or_continue(raw_args: list[str], name: str, version: str)
455
503
  output_format = OutputFormat.JSON
456
504
  i += 1
457
505
  continue
458
- if arg == "--output" or arg.startswith("--output="):
506
+ # `--output-to` takes a value but does not affect version output. Consume
507
+ # its space-separated value so it is not mistaken for the subcommand
508
+ # boundary (which would hide a later `--version`/`--output`).
509
+ if flag_name == "output-to":
510
+ has_space_value = (
511
+ inline_value is None
512
+ and i + 1 < len(raw_args)
513
+ and not raw_args[i + 1].startswith("-")
514
+ )
515
+ i += 2 if has_space_value else 1
516
+ continue
517
+ if flag_name == "output":
459
518
  value: str | None
460
- if arg.startswith("--output="):
461
- value = arg.split("=", 1)[1]
462
- step = 1
519
+ if inline_value is not None:
520
+ value = inline_value
463
521
  elif i + 1 < len(raw_args) and not raw_args[i + 1].startswith("-"):
464
522
  value = raw_args[i + 1]
465
- step = 2
466
523
  else:
467
524
  value = None
468
- step = 1
469
525
  if value is None:
470
526
  output_error = ValueError(
471
527
  "missing value for --output: expected json, yaml, or plain"
@@ -481,15 +537,31 @@ def cli_handle_version_or_continue(raw_args: list[str], name: str, version: str)
481
537
  output_format = parsed_output
482
538
  except ValueError as e:
483
539
  output_error = e
484
- i += step
540
+ i += 1 if (inline_value is not None or value is None) else 2
485
541
  continue
486
- i += 1
542
+
543
+ # Any other flag: consume a space-separated value only if the caller
544
+ # listed this flag as value-taking, there is no inline `=value`, and the
545
+ # next arg exists and doesn't start with `-`. Otherwise it is a boolean
546
+ # flag and consumes only itself.
547
+ has_space_value = (
548
+ inline_value is None
549
+ and i + 1 < len(raw_args)
550
+ and not raw_args[i + 1].startswith("-")
551
+ )
552
+ i += 2 if (has_space_value and flag_name in value_flag_names) else 1
487
553
 
488
554
  if not version_requested:
489
555
  return None
490
556
  if output_error is not None:
491
557
  raise output_error
492
- return cli_render_version(name, version, output_format)
558
+ return cli_render_version(
559
+ name,
560
+ display_name,
561
+ version,
562
+ build,
563
+ output_format if output_format is not None else OutputFormat.JSON,
564
+ )
493
565
 
494
566
 
495
567
  def build_cli_error(message: str, hint: str | None = None) -> dict | Event:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.20.0
3
+ Version: 0.22.0
4
4
  Summary: A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
@@ -28,15 +28,7 @@ print(render(value, OutputFormat.JSON))
28
28
  print(render(value, OutputFormat.PLAIN))
29
29
  ```
30
30
 
31
- Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
32
-
33
- Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
34
-
35
- ```python
36
- from agent_first_data import init_logging_json
37
-
38
- init_logging_json("INFO", secret_names=("authorization",))
39
- ```
31
+ Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`.
40
32
 
41
33
  ## Behavior Notes
42
34
 
@@ -45,12 +37,10 @@ init_logging_json("INFO", secret_names=("authorization",))
45
37
  - YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
46
38
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
47
39
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
48
- - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
49
- - Use `agent_first_data.stream_redirect.install_from_raw_args()` before version/help handling if a CLI exposes `--stdout-file` or `--stderr-file`; stderr is redirected as native diagnostics, not JSON.
40
+ - Use `cli_handle_version_or_continue()` before argument parsing so `--version`/`-V` always answers with a structured protocol-v1 `kind:"result"` version event — JSON by default, or `--output yaml|plain`/`--json` for another format; there is no conventional bare-text form. Pass your own value-taking global flag names so their value is not mistaken for the subcommand boundary.
50
41
 
51
42
  ## Reference
52
43
 
53
- - Full convention and API groups: [docs/overview.md](https://github.com/agentfirstkit/agent-first-data/blob/main/docs/overview.md)
54
44
  - Formal cross-language contract: [spec/agent-first-data.md](https://github.com/agentfirstkit/agent-first-data/blob/main/spec/agent-first-data.md)
55
45
  - Conformance fixtures: [spec/fixtures](https://github.com/agentfirstkit/agent-first-data/tree/main/spec/fixtures)
56
46
  - Agent skill: [skills/agent-first-data/SKILL.md](https://github.com/agentfirstkit/agent-first-data/blob/main/skills/agent-first-data/SKILL.md)
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ agent_first_data/__init__.py
4
+ agent_first_data/cli.py
5
+ agent_first_data/format.py
6
+ agent_first_data.egg-info/PKG-INFO
7
+ agent_first_data.egg-info/SOURCES.txt
8
+ agent_first_data.egg-info/dependency_links.txt
9
+ agent_first_data.egg-info/top_level.txt
10
+ agent_first_data/assets/protocol-v1.schema.json
11
+ agent_first_data/assets/registry.json
12
+ tests/test_cli.py
13
+ tests/test_format.py
14
+ tests/test_no_stderr_policy.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.20.0"
3
+ version = "0.22.0"
4
4
  description = "A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents."
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -17,7 +17,4 @@ build-backend = "setuptools.build_meta"
17
17
  [tool.setuptools.package-data]
18
18
  agent_first_data = [
19
19
  "assets/*.json",
20
- "assets/skills/agent-first-data/*.md",
21
- "assets/skills/agent-first-data/references/*.md",
22
- "assets/skills/agent-first-data/references/*.json",
23
20
  ]
@@ -484,36 +484,72 @@ def test_finish_returns_4_on_lifecycle_violation():
484
484
 
485
485
  # ── version helpers ───────────────────────────────────────────────────────────
486
486
 
487
+ # The value-taking global flags a caller passes through so their space-separated
488
+ # value is not mistaken for the subcommand boundary (the Python stand-in for the
489
+ # Rust tests' `version_test_command()` with its `--stdout-file`/`--stderr-file`).
490
+ VERSION_VALUE_FLAGS = ["--stdout-file", "--stderr-file"]
491
+
492
+
487
493
  def test_build_cli_version_standard_shape():
488
- v = build_cli_version("1.2.3")
494
+ v = build_cli_version("agent-cli", "Agent CLI Example", "1.2.3", "abc1234")
489
495
  assert v["kind"] == "result"
490
496
  assert v["result"]["code"] == "version"
497
+ assert v["result"]["name"] == "agent-cli"
498
+ assert v["result"]["display_name"] == "Agent CLI Example"
491
499
  assert v["result"]["version"] == "1.2.3"
500
+ assert v["result"]["build"] == "abc1234"
492
501
  # 0.16 spec: all events have trace by default
493
502
  assert v["trace"] == {}
494
503
 
495
504
 
496
- def test_cli_render_version_is_conventional_by_default():
497
- assert cli_render_version("agent-cli", "1.2.3") == "agent-cli 1.2.3\n"
505
+ def test_build_cli_version_omits_absent_display_name_and_build():
506
+ v = build_cli_version("agent-cli", None, "1.2.3", None)
507
+ result = v["result"]
508
+ assert result["name"] == "agent-cli"
509
+ assert result["version"] == "1.2.3"
510
+ assert "display_name" not in result
511
+ assert "build" not in result
498
512
 
499
513
 
500
- def test_cli_render_version_can_render_json():
501
- out = cli_render_version("agent-cli", "1.2.3", OutputFormat.JSON)
514
+ def test_cli_render_version_renders_json():
515
+ out = cli_render_version("agent-cli", None, "1.2.3", None, OutputFormat.JSON)
502
516
  assert out.endswith("\n")
503
517
  assert '"kind":"result"' in out
504
518
  assert '"code":"version"' in out
519
+ assert '"name":"agent-cli"' in out
505
520
  assert '"version":"1.2.3"' in out
506
521
 
507
522
 
508
- def test_cli_handle_version_is_conventional_by_default():
509
- assert cli_handle_version_or_continue(["--version"], "agent-cli", "1.2.3") == "agent-cli 1.2.3\n"
523
+ def test_cli_handle_version_bare_defaults_to_json():
524
+ # The one blessed behavior: `--version` always answers with a protocol-v1
525
+ # event, JSON by default — no more conventional bare-text special case.
526
+ out = cli_handle_version_or_continue(
527
+ ["--version"],
528
+ VERSION_VALUE_FLAGS,
529
+ "agent-cli",
530
+ "Agent CLI Example",
531
+ "1.2.3",
532
+ None,
533
+ )
534
+ assert out is not None
535
+ parsed = json.loads(out.strip())
536
+ assert parsed["kind"] == "result"
537
+ assert parsed["result"]["code"] == "version"
538
+ assert parsed["result"]["name"] == "agent-cli"
539
+ assert parsed["result"]["display_name"] == "Agent CLI Example"
540
+ assert parsed["result"]["version"] == "1.2.3"
541
+ assert "build" not in parsed["result"]
542
+ assert parsed["trace"] == {}
510
543
 
511
544
 
512
- def test_cli_handle_version_honors_output_flag():
545
+ def test_cli_handle_version_honors_explicit_plain_output():
513
546
  out = cli_handle_version_or_continue(
514
547
  ["--version", "--output", "plain"],
548
+ VERSION_VALUE_FLAGS,
515
549
  "agent-cli",
550
+ None,
516
551
  "1.2.3",
552
+ None,
517
553
  )
518
554
  assert out is not None
519
555
  assert "kind=result" in out
@@ -521,37 +557,155 @@ def test_cli_handle_version_honors_output_flag():
521
557
  assert "result.version=1.2.3" in out
522
558
 
523
559
 
560
+ def test_cli_handle_version_skips_output_to_space_value():
561
+ # A preceding `--output-to <value>` (space form) must not be mistaken for
562
+ # the subcommand boundary; the later `--version --output json` must still be
563
+ # detected.
564
+ out = cli_handle_version_or_continue(
565
+ ["--output-to", "stdout", "--version", "--output", "json"],
566
+ VERSION_VALUE_FLAGS,
567
+ "agent-cli",
568
+ None,
569
+ "1.2.3",
570
+ None,
571
+ )
572
+ assert out is not None
573
+ parsed = json.loads(out.strip())
574
+ assert parsed["kind"] == "result"
575
+ assert parsed["result"]["version"] == "1.2.3"
576
+
577
+
578
+ def test_cli_handle_version_skips_caller_value_flag_space_value():
579
+ # A caller's own value-taking global flag (here `--stdout-file`/
580
+ # `--stderr-file`): the path value must not be mistaken for the subcommand
581
+ # boundary either.
582
+ out = cli_handle_version_or_continue(
583
+ ["--stdout-file", "/tmp/out.log", "--stderr-file", "/tmp/err.log", "--version"],
584
+ VERSION_VALUE_FLAGS,
585
+ "agent-cli",
586
+ None,
587
+ "1.2.3",
588
+ None,
589
+ )
590
+ assert out is not None
591
+ parsed = json.loads(out.strip())
592
+ assert parsed["result"]["name"] == "agent-cli"
593
+ assert parsed["result"]["version"] == "1.2.3"
594
+
595
+
596
+ def test_cli_handle_version_skips_caller_value_flag_inline_value():
597
+ out = cli_handle_version_or_continue(
598
+ ["--stdout-file=/tmp/out.log", "--version"],
599
+ VERSION_VALUE_FLAGS,
600
+ "agent-cli",
601
+ None,
602
+ "1.2.3",
603
+ None,
604
+ )
605
+ assert out is not None
606
+
607
+
608
+ def test_cli_handle_version_skips_caller_defined_value_flag():
609
+ # A consumer's *own* value-taking global flag the pre-parser has no special
610
+ # knowledge of — here a hypha-style comma-list `--log` — must have its
611
+ # space-separated value recognized through `value_flags`, not a hardcoded
612
+ # allowlist. Otherwise `request,startup` would be read as the subcommand
613
+ # boundary and `--version` dropped.
614
+ out = cli_handle_version_or_continue(
615
+ ["--log", "request,startup", "--version"],
616
+ ["--log"],
617
+ "hypha",
618
+ None,
619
+ "1.2.3",
620
+ None,
621
+ )
622
+ assert out is not None
623
+ parsed = json.loads(out.strip())
624
+ assert parsed["result"]["name"] == "hypha"
625
+ assert parsed["result"]["version"] == "1.2.3"
626
+
627
+
628
+ def test_cli_handle_version_boolean_global_flag_does_not_over_consume():
629
+ # The mirror of the case above: a caller's boolean global flag (absent from
630
+ # value_flags) takes no value, so the following positional is the subcommand
631
+ # boundary and a `--version` after it belongs to the subcommand.
632
+ assert (
633
+ cli_handle_version_or_continue(
634
+ ["--verbose", "sense", "--version"],
635
+ ["--log"],
636
+ "hypha",
637
+ None,
638
+ "1.2.3",
639
+ None,
640
+ )
641
+ is None
642
+ )
643
+
644
+
645
+ def test_cli_handle_version_supports_inline_output_format():
646
+ out = cli_handle_version_or_continue(
647
+ ["--output=yaml", "--version"],
648
+ VERSION_VALUE_FLAGS,
649
+ "agent-cli",
650
+ None,
651
+ "1.2.3",
652
+ None,
653
+ )
654
+ assert out is not None
655
+ assert out.startswith("---\n")
656
+ assert 'version: "1.2.3"' in out
657
+
658
+
524
659
  def test_cli_handle_version_json_alias():
525
660
  out = cli_handle_version_or_continue(
526
661
  ["--version", "--json"],
662
+ VERSION_VALUE_FLAGS,
527
663
  "agent-cli",
664
+ None,
528
665
  "1.2.3",
666
+ None,
529
667
  )
530
668
  assert out is not None
531
- assert '"kind":"result"' in out
532
- assert '"code":"version"' in out
533
- assert '"version":"1.2.3"' in out
669
+ parsed = json.loads(out.strip())
670
+ assert parsed["kind"] == "result"
671
+ assert parsed["result"]["version"] == "1.2.3"
534
672
 
535
673
 
536
674
  def test_cli_handle_version_json_alias_conflict():
537
675
  with pytest.raises(ValueError, match="conflicting output formats"):
538
676
  cli_handle_version_or_continue(
539
677
  ["--version", "--json", "--output", "yaml"],
678
+ VERSION_VALUE_FLAGS,
540
679
  "agent-cli",
680
+ None,
541
681
  "1.2.3",
682
+ None,
542
683
  )
543
684
 
544
685
 
545
686
  def test_cli_handle_version_returns_none_without_version():
546
- assert cli_handle_version_or_continue(["ping"], "agent-cli", "1.2.3") is None
687
+ assert (
688
+ cli_handle_version_or_continue(
689
+ ["ping"],
690
+ VERSION_VALUE_FLAGS,
691
+ "agent-cli",
692
+ None,
693
+ "1.2.3",
694
+ None,
695
+ )
696
+ is None
697
+ )
547
698
 
548
699
 
549
700
  def test_cli_handle_version_rejects_invalid_output():
550
701
  with pytest.raises(ValueError, match="xml"):
551
702
  cli_handle_version_or_continue(
552
703
  ["--version", "--output", "xml"],
704
+ VERSION_VALUE_FLAGS,
553
705
  "agent-cli",
706
+ None,
554
707
  "1.2.3",
708
+ None,
555
709
  )
556
710
 
557
711
 
@@ -561,16 +715,22 @@ def test_cli_handle_version_ignores_version_flag_after_subcommand():
561
715
  assert (
562
716
  cli_handle_version_or_continue(
563
717
  ["hatch", "--version", "1.3.0"],
718
+ VERSION_VALUE_FLAGS,
564
719
  "agent-cli",
720
+ None,
565
721
  "1.2.3",
722
+ None,
566
723
  )
567
724
  is None
568
725
  )
569
726
  assert (
570
727
  cli_handle_version_or_continue(
571
728
  ["hatch", "-V", "1.3.0"],
729
+ VERSION_VALUE_FLAGS,
572
730
  "agent-cli",
731
+ None,
573
732
  "1.2.3",
733
+ None,
574
734
  )
575
735
  is None
576
736
  )
@@ -581,8 +741,12 @@ def test_cli_handle_version_honors_output_flag_before_top_level_version():
581
741
  # --version is still recognized.
582
742
  out = cli_handle_version_or_continue(
583
743
  ["--output", "json", "--version"],
744
+ VERSION_VALUE_FLAGS,
584
745
  "agent-cli",
746
+ None,
585
747
  "1.2.3",
748
+ None,
586
749
  )
587
750
  assert out is not None
588
- assert '"version":"1.2.3"' in out
751
+ parsed = json.loads(out.strip())
752
+ assert parsed["result"]["version"] == "1.2.3"
@@ -28,8 +28,6 @@ def test_no_ad_hoc_stderr_writes_in_runtime_sources() -> None:
28
28
 
29
29
  violations: list[str] = []
30
30
  for path in files:
31
- if path.name == "stream_redirect.py":
32
- continue
33
31
  for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
34
32
  if DISALLOWED.search(line):
35
33
  violations.append(f"{path.name}:{lineno}: {line.strip()}")