Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

Notable changes to Agent Code Guard are recorded here.

## Unreleased

### Added

- Deterministic `code-guard --version` reporting from installed distribution
metadata, with human and JSON output modes.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ Deliberate full audit:
code-guard .
```

Confirm the installed distribution identity without running analysis:

```text
$ code-guard --version
agent-code-guard <version>
```

Use `code-guard --version --json` for the exact JSON shape
`{"distribution": "agent-code-guard", "version": "<version>"}`. The version
comes from installed `agent-code-guard` distribution metadata. Version mode may
be combined only with `--json`. Both successful version forms exit `0`;
incompatible arguments or unavailable metadata are tool errors that exit `3`
through the normal human or JSON error channel.

Changed work is not a full audit. Use Git selection during normal development;
do not repeatedly scan unrelated repository history after every edit.

Expand Down
28 changes: 28 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,34 @@ repository and skill compatibility; it is not the primary installed command.
The public command is `code-guard`. With no paths, it defaults to `.`, but
normal development should select current work explicitly.

### Installed version

Report the installed distribution identity without configuration, scope, Git,
provider, skill, or guard work:

```text
$ code-guard --version
agent-code-guard <version>
```

For machine-readable output:

```text
$ code-guard --version --json
{
"distribution": "agent-code-guard",
"version": "<version>"
}
```

The value is read from installed metadata for the canonical
`agent-code-guard` distribution. `--version` may be combined only with
`--json`. Both successful version forms exit `0`; incompatible arguments or
unavailable metadata exit `3`. Human errors use standard error and JSON errors
use an `error` object on standard output. If distribution metadata is
unavailable, the error is
`installed distribution metadata is unavailable for agent-code-guard`.

### Git changed-only

```bash
Expand Down
82 changes: 76 additions & 6 deletions src/agent_code_guard/code_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import argparse
from importlib import import_module
from importlib.metadata import PackageNotFoundError, version as distribution_version
import json
import sys
from pathlib import Path
Expand All @@ -15,9 +16,34 @@
from .result_model import GuardResult, aggregate_state, required_policies
from .skill_distribution import export_skill, skill_path as installed_skill_path

DISTRIBUTION_NAME = "agent-code-guard"
METADATA_UNAVAILABLE = f"installed distribution metadata is unavailable for {DISTRIBUTION_NAME}"


def _installed_distribution_version() -> str:
try:
installed_version = distribution_version(DISTRIBUTION_NAME)
except (PackageNotFoundError, OSError, UnicodeError) as exc:
raise ValueError(METADATA_UNAVAILABLE) from exc
if not isinstance(installed_version, str):
raise ValueError(METADATA_UNAVAILABLE)
return installed_version


def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser(description="Run deterministic Code Guard checks.")
value = argparse.ArgumentParser(
prog="code-guard",
description="Run deterministic Code Guard checks.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Version reporting:
code-guard --version
Output: agent-code-guard <version>
code-guard --version --json
Output: {"distribution": "agent-code-guard", "version": "<version>"}

Successful version reporting exits 0. Incompatible arguments or unavailable
metadata exit 3. --version may be combined only with --json.""",
)
value.add_argument(
"paths", nargs="*", default=[],
help="Files or directories to inspect; bounds files selected by a Git selection mode.",
Expand All @@ -26,6 +52,10 @@ def parser() -> argparse.ArgumentParser:
value.add_argument("--warn", type=int, help="Override the global LOC warning threshold.")
value.add_argument("--fail", type=int, help="Override the global LOC failure threshold.")
value.add_argument("--json", action="store_true", help="Emit normalized JSON.")
value.add_argument(
"--version", action="store_true",
help="Print the installed agent-code-guard distribution version; may be combined only with --json.",
)
value.add_argument("--ci", action="store_true", help="Do not fail solely on REVIEW.")
value.add_argument("--changed-only", action="store_true", help="Inspect staged, unstaged, and untracked files.")
value.add_argument("--staged", action="store_true", help="Inspect index-only changes.")
Expand All @@ -49,6 +79,36 @@ def parser() -> argparse.ArgumentParser:
return value


def _version_mode(args: argparse.Namespace) -> int | None:
if not args.version:
return None
incompatible = (
bool(args.paths)
or args.config is not None
or args.warn is not None
or args.fail is not None
or args.ci
or args.changed_only
or args.staged
or args.base_ref is not None
or bool(args.include)
or bool(args.exclude)
or bool(args.scope_exclude)
or args.count_blank_lines
or args.ignore_comment_lines
or args.skill_path
or args.export_skill is not None
)
if incompatible:
raise ValueError("--version may be combined only with --json")
installed_version = _installed_distribution_version()
if args.json:
print(json.dumps({"distribution": DISTRIBUTION_NAME, "version": installed_version}, indent=2))
else:
print(f"{DISTRIBUTION_NAME} {installed_version}")
return 0


def _management_mode(args: argparse.Namespace) -> int | None:
if not args.skill_path and args.export_skill is None:
args.paths = args.paths or ["."]
Expand Down Expand Up @@ -210,9 +270,23 @@ def exit_code(overall: str, ci: bool) -> int:
return 0


def _print_tool_error(message: str, json_mode: bool) -> int:
if json_mode:
print(json.dumps({"error": message}, indent=2))
else:
print(f"Code Guard error: {message}", file=sys.stderr)
return 3


def main() -> int:
raw_arguments = sys.argv[1:]
if "--version" in raw_arguments and any(value in raw_arguments for value in ("-h", "--help")):
return _print_tool_error("--version may be combined only with --json", "--json" in raw_arguments)
args = parser().parse_args()
try:
version_result = _version_mode(args)
if version_result is not None:
return version_result
management_result = _management_mode(args)
if management_result is not None:
return management_result
Expand All @@ -225,11 +299,7 @@ def main() -> int:
print_text(data)
return exit_code(data["overall"], args.ci)
except Exception as exc:
if args.json:
print(json.dumps({"error": str(exc)}, indent=2))
else:
print(f"Code Guard error: {exc}", file=sys.stderr)
return 3
return _print_tool_error(str(exc), args.json)


if __name__ == "__main__":
Expand Down
Loading
Loading