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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This project has a published GitHub Release line, but no stable support or API g
- Added a CLI contract regression matrix for current version, help, `check`, and `init` output channels and exit codes.
- Added the read-only `doctor` baseline command for repository-level instruction diagnosis summaries.
- Added the read-only `budget` baseline command for deterministic local instruction-file size metrics.
- Added the read-only `explain` baseline command for local governance rule explanations.

## [0.2.3] - 2026-06-18

Expand Down
30 changes: 18 additions & 12 deletions docs/EXIT-CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ Notes:
- `budget` does not perform tokenizer-specific counting, remote tokenization, LLM calls, pricing estimates, or optimization claims.
- `Approximate words` is not a model token count.

### `explain`

| Condition | Exit code | Stdout | Stderr |
| --- | ---: | --- | --- |
| Known rules listed or known rule explained | `0` | Console rule explanation output | Empty unless lower-level runtime fails unexpectedly |
| Unknown rule ID, conflicting input, missing input, or command-line usage error | `2` | Empty or argparse-dependent | Error message or argparse-dependent |

Notes:

- `explain` is read-only.
- `explain` uses local rule metadata only.
- `explain` does not fetch external documentation, call an LLM, infer new rules, or generate free-form policy advice.
- Unknown rule IDs fail predictably.

### `init --dry-run`

| Condition | Exit code | Stdout | Stderr |
Expand Down Expand Up @@ -96,18 +110,7 @@ Notes:

## Planned v0.3 exit-code direction

The following commands are not implemented yet. Their exit-code contracts are design targets for future implementation phases.

### `explain`

Planned direction:

| Condition | Exit code |
| --- | ---: |
| Known rule explained or known rules listed | `0` |
| Unknown rule ID, invalid input, or command-line usage error | `2` |

Unknown rule IDs should fail predictably. They should not silently produce generic guidance.
No remaining v0.3 command exit-code target is documented here after the `explain` baseline. Future release or documentation phases must update this file only from verified behavior.

## Test evidence

Expand All @@ -124,6 +127,9 @@ The contract regression matrix currently checks:
- `doctor` exits `1` when no supported instruction files are found;
- `budget` exits `0` when supported instruction files are found;
- `budget` exits `1` when no supported instruction files are found;
- `explain --list` exits `0`;
- `explain RULE_ID` exits `0` for known rule IDs;
- `explain RULE_ID` exits `2` for unknown rule IDs;
- `init --dry-run` exits `0`;
- `init` without `--dry-run` or `--write` exits `2` and writes the supported error to stderr.

Expand Down
38 changes: 15 additions & 23 deletions docs/OUTPUTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@ Implemented command surface:
- `agent-rules-kit init --dry-run`;
- `agent-rules-kit init --write`;
- `agent-rules-kit doctor`;
- `agent-rules-kit budget`.

Planned v0.3 command surface:

- `agent-rules-kit budget`;
- `agent-rules-kit explain`.

`doctor` and `budget` are implemented as v0.3 command baselines. The remaining planned command is not implemented yet. Its output contract is a design target for a future phase and must not be documented as available behavior until its implementation phase is merged.
`doctor`, `budget`, and `explain` are implemented as v0.3 command baselines. Release preparation remains a separate phase and must not imply tag, release, or PyPI publication until those phases are completed.

## Contract status

Expand Down Expand Up @@ -47,7 +44,7 @@ That test module currently pins representative exact output for:
- `init --dry-run` console output for an existing root `AGENTS.md`;
- `init` missing-mode stderr behavior.

It also includes a contract regression matrix for current version, no-command help, `check` console/JSON/Markdown success and no-result behavior, `init --dry-run`, and missing-mode `init` behavior.
It also includes a contract regression matrix for current version, no-command help, `check` console/JSON/Markdown success and no-result behavior, `doctor`, `budget`, `explain`, `init --dry-run`, and missing-mode `init` behavior.

This is regression evidence for implemented behavior on current `main`. It is not a stable public API guarantee before v1.0.

Expand All @@ -74,7 +71,7 @@ Future behavior should preserve that distinction unless a dedicated phase change
| `init --write` | console | yes | Explicit write mode with backup behavior for existing root `AGENTS.md`. |
| `doctor` | console | yes | Read-only repository-level diagnosis summary. |
| `budget` | console | yes | Read-only local size and context-pressure approximation. |
| `explain` | to be defined | no | Planned v0.3 local rule explanation command. |
| `explain` | console | yes | Read-only local governance rule explanation command. |

## Exit codes

Expand All @@ -95,6 +92,8 @@ Summary for current implemented commands:
| `budget` | `0` | Budget approximation completed for supported instruction files. |
| `budget` | `1` | No supported instruction files were found. |
| `budget` | `2` | Invalid repository input, unsupported instruction-file input, or command-line usage error. |
| `explain` | `0` | Known rule listed or explained. |
| `explain` | `2` | Unknown rule ID, conflicting input, missing input, or command-line usage error. |

## JSON contract for `check`

Expand Down Expand Up @@ -246,28 +245,21 @@ Current `budget` exit-code behavior:

`Approximate words` is a local whitespace-based approximation, not a model token count.

## Planned v0.3 command contracts

The remaining command is a design target. It is not available until its dedicated implementation phase is merged.

### `explain`
## Explain output contract

Planned purpose:
Current `explain` console output includes either:

- explain known rule IDs and their limits from local rule metadata or documentation-backed text;
- optionally list known rules.
- a list of known rule IDs with category and title;
- one known rule explanation with title, category, summary, and limits.

Planned output direction:
Current `explain` exit-code behavior:

- local explanations only;
- no external documentation fetch;
- no LLM-generated explanations;
- unsupported rule IDs must fail predictably.
- `0`: known rules listed or a known rule was explained;
- `2`: unknown rule ID, conflicting input, missing input, or command-line usage error.

Planned exit-code direction:
`explain` is read-only. It uses local rule metadata only. It does not fetch external documentation, call an LLM, infer new rules, or generate free-form policy advice.

- `0`: explanation or rule list completed;
- `2`: unknown rule ID, invalid input, or command-line usage error.
Unsupported rule IDs fail predictably instead of producing generic guidance.

## Redaction expectations

Expand Down
67 changes: 67 additions & 0 deletions src/agent_rules_kit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
from agent_rules_kit import __version__
from agent_rules_kit.budget import BudgetReport, build_budget_report
from agent_rules_kit.discovery import InstructionFile, discover_instruction_files
from agent_rules_kit.explain import (
RuleExplanation,
get_rule_explanation,
list_rule_explanations,
)
from agent_rules_kit.findings import Finding
from agent_rules_kit.governance import find_governance_findings
from agent_rules_kit.init_plan import InitPlan, build_init_plan
Expand Down Expand Up @@ -94,6 +99,22 @@ def build_parser() -> argparse.ArgumentParser:
help="Repository root to inspect. Defaults to the current directory.",
)

explain_parser = subparsers.add_parser(
"explain",
help="Explain known governance rule IDs.",
)
explain_parser.add_argument(
"rule_id",
nargs="?",
help="Known rule ID to explain, such as AIRK-GOV003.",
)
explain_parser.add_argument(
"--list",
action="store_true",
dest="list_rules",
help="List known rule IDs.",
)

return parser


Expand Down Expand Up @@ -122,10 +143,56 @@ def main(argv: Sequence[str] | None = None) -> int:
if args.command == "budget":
return _run_budget(Path(args.repository))

if args.command == "explain":
return _run_explain(args.rule_id, list_rules=args.list_rules)

parser.print_help()
return 0


def _run_explain(rule_id: str | None, *, list_rules: bool) -> int:
if list_rules and rule_id is not None:
print("ERROR: explain accepts either --list or a rule ID.", file=sys.stderr)
return 2

if list_rules:
_print_known_rules()
return 0

if rule_id is None:
print("ERROR: explain requires --list or RULE_ID.", file=sys.stderr)
return 2

explanation = get_rule_explanation(rule_id)
if explanation is None:
print(
f"ERROR: unknown rule ID: {redact_secret_like_values(rule_id)}",
file=sys.stderr,
)
return 2

_print_rule_explanation(explanation)
return 0


def _print_known_rules() -> None:
print("agent-rules-kit explain")
print("Known rules:")
for explanation in list_rule_explanations():
print(
f"- {explanation.rule_id} [{explanation.category}] "
f"{explanation.title}"
)


def _print_rule_explanation(explanation: RuleExplanation) -> None:
print(f"agent-rules-kit explain: {explanation.rule_id}")
print(f"Title: {explanation.title}")
print(f"Category: {explanation.category}")
print(f"Summary: {explanation.summary}")
print(f"Limits: {explanation.limits}")


def _run_budget(repository_root: Path) -> int:
try:
instruction_files = discover_instruction_files(repository_root)
Expand Down
139 changes: 139 additions & 0 deletions src/agent_rules_kit/explain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Local governance rule explanations."""

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class RuleExplanation:
"""Human-readable explanation for one known rule."""

rule_id: str
title: str
category: str
summary: str
limits: str


RULE_EXPLANATIONS: tuple[RuleExplanation, ...] = (
RuleExplanation(
rule_id="AIRK-SYS001",
title="Unreadable instruction file",
category="system",
summary=(
"Flags supported instruction files that cannot be analyzed as UTF-8."
),
limits="Does not print raw undecodable bytes and does not repair encoding.",
),
RuleExplanation(
rule_id="AIRK-SYS002",
title="Symlinked instruction file",
category="system",
summary=(
"Flags supported instruction file paths that are symlinks and are not "
"analyzed."
),
limits="Does not follow symlinked instruction files or wildcard directories.",
),
RuleExplanation(
rule_id="AIRK-GOV006",
title="Unsupported security or maturity claim",
category="governance",
summary=(
"Flags instruction text that appears to claim unsupported safety, "
"security, production readiness, completeness, or maturity."
),
limits=(
"Does not prove a claim false and does not replace human release "
"review."
),
),
RuleExplanation(
rule_id="AIRK-GOV003",
title="Review or CI bypass guidance",
category="governance",
summary=(
"Flags instruction text that appears to encourage bypassing review, "
"CI, PRs, branch protection, or safe integration flow."
),
limits=(
"Does not audit real GitHub settings, CI configuration, or branch "
"protection."
),
),
RuleExplanation(
rule_id="AIRK-GOV004",
title="Unsafe command execution guidance",
category="governance",
summary=(
"Flags instruction text that appears to ask assistants to run "
"destructive, privileged, broad, or externally fetched commands "
"without an explicit confirmation boundary."
),
limits=(
"Is not a full shell safety analyzer and does not evaluate every "
"possible command form."
),
),
RuleExplanation(
rule_id="AIRK-GOV005",
title="Runtime network or LLM dependency guidance",
category="governance",
summary=(
"Flags instruction text that appears to require remote services, LLMs, "
"API tokens, or network behavior that conflicts with local-first "
"boundaries."
),
limits=(
"Does not inspect real runtime behavior or external service "
"configuration."
),
),
RuleExplanation(
rule_id="AIRK-GOV002",
title="Missing secret-handling boundary",
category="governance",
summary=(
"Flags supported instruction files that may lack explicit guidance "
"for handling secrets, tokens, credentials, or sensitive data."
),
limits=(
"Is not complete secret scanning and does not prove that a repository "
"is free of secrets."
),
),
RuleExplanation(
rule_id="AIRK-GOV001",
title="Missing instruction scope or authority",
category="governance",
summary=(
"Flags supported instruction files that may lack clear scope, "
"authority, or precedence boundaries."
),
limits=(
"Does not resolve organizational policy or prove that instructions "
"are complete."
),
),
)

_RULE_INDEX = {explanation.rule_id: explanation for explanation in RULE_EXPLANATIONS}


def list_rule_explanations() -> tuple[RuleExplanation, ...]:
"""Return known rule explanations in stable order."""
return RULE_EXPLANATIONS


def get_rule_explanation(rule_id: str) -> RuleExplanation | None:
"""Return a known rule explanation by ID."""
return _RULE_INDEX.get(rule_id.strip().upper())


__all__ = [
"RULE_EXPLANATIONS",
"RuleExplanation",
"get_rule_explanation",
"list_rule_explanations",
]
Loading