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 @@ -12,6 +12,7 @@ This project has a published GitHub Release line, but no stable support or API g
- Added initial output and exit-code contract documentation for existing `check` and `init` behavior and planned v0.3 `doctor`, `budget`, and `explain` commands.
- Added golden output foundation tests for current `check` and `init` console, JSON, Markdown, stdout, stderr, and exit-code behavior.
- 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.

## [0.2.3] - 2026-06-18

Expand Down
29 changes: 17 additions & 12 deletions docs/EXIT-CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ Notes:
- `check` returns `1` only for the supported no-instruction-files state.
- `check` returns `2` for repository validation errors raised by discovery and for argparse usage errors.

### `doctor`

| Condition | Exit code | Stdout | Stderr |
| --- | ---: | --- | --- |
| Supported instruction files found | `0` | Console diagnosis summary | Empty unless lower-level runtime fails unexpectedly |
| No supported instruction files found | `1` | Console no-result diagnosis summary | Empty unless lower-level runtime fails unexpectedly |
| Invalid repository input or command-line usage error | `2` | Empty | Error message or argparse-dependent |

Notes:

- `doctor` is read-only.
- `doctor` reuses the existing discovery and governance diagnostics.
- `doctor` findings do not currently make the command fail.
- `doctor` does not audit GitHub branch protection, CI, dependencies, or security certification.

### `init --dry-run`

| Condition | Exit code | Stdout | Stderr |
Expand Down Expand Up @@ -68,18 +83,6 @@ Notes:

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

### `doctor`

Planned direction:

| Condition | Exit code |
| --- | ---: |
| Repository diagnosis completed with supported instruction files | `0` |
| No supported instruction files found, if aligned with `check` | `1` |
| Invalid repository input or command-line usage error | `2` |

The implementation phase must decide and test whether no supported instruction files should mirror `check` with `1`.

### `budget`

Planned direction:
Expand Down Expand Up @@ -114,6 +117,8 @@ The contract regression matrix currently checks:
- `check` exits `0` when supported instruction files are found;
- `check` exits `1` when no supported instruction files are found;
- `check --format json` and `check --format markdown` preserve the same success and no-result exit-code behavior;
- `doctor` exits `0` when supported instruction files are found;
- `doctor` exits `1` when no supported instruction files are found;
- `init --dry-run` exits `0`;
- `init` without `--dry-run` or `--write` exits `2` and writes the supported error to stderr.

Expand Down
39 changes: 19 additions & 20 deletions docs/OUTPUTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ Implemented command surface:
- `agent-rules-kit --version`;
- `agent-rules-kit check`;
- `agent-rules-kit init --dry-run`;
- `agent-rules-kit init --write`.
- `agent-rules-kit init --write`;
- `agent-rules-kit doctor`.

Planned v0.3 command surface:

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

The planned commands are not implemented yet. Their output contracts are design targets for future phases and must not be documented as available behavior until their implementation phases are merged.
`doctor` is implemented as the first v0.3 command baseline. The remaining planned commands are not implemented yet. Their output contracts are design targets for future phases and must not be documented as available behavior until their implementation phases are merged.

## Contract status

Expand Down Expand Up @@ -72,7 +72,7 @@ Future behavior should preserve that distinction unless a dedicated phase change
| `check --format markdown` | Markdown | yes | Human-readable Markdown report. |
| `init --dry-run` | console | yes | Read-only plan; no files modified. |
| `init --write` | console | yes | Explicit write mode with backup behavior for existing root `AGENTS.md`. |
| `doctor` | to be defined | no | Planned v0.3 read-only repository summary. |
| `doctor` | console | yes | Read-only repository-level diagnosis summary. |
| `budget` | to be defined | no | Planned v0.3 read-only local size/context-pressure approximation. |
| `explain` | to be defined | no | Planned v0.3 local rule explanation command. |

Expand Down Expand Up @@ -200,29 +200,28 @@ Current `init` does not support JSON or Markdown output.

`init --dry-run` is read-only. `init --write` is explicit write mode and must remain separate from read-only diagnosis commands.

## Planned v0.3 command contracts

These commands are design targets. They are not available until their dedicated implementation phases are merged.
## Doctor output contract

### `doctor`
Current `doctor` console output includes:

Planned purpose:
- command header;
- status line;
- supported instruction file count;
- finding count;
- finding counts by severity and rule when findings exist;
- short next-step guidance.

- read-only repository-level diagnosis summary;
- reuse discovery and governance findings;
- summarize supported instruction files, finding counts, and high-level review status.
Current `doctor` exit-code behavior:

Planned output direction:
- `0`: diagnosis completed and supported instruction files were found;
- `1`: no supported instruction files were found;
- `2`: invalid repository input or command-line usage error.

- console summary first;
- JSON only if the implementation phase explicitly defines and tests it;
- no branch protection, CI, dependency, or security certification audit in v0.3.
`doctor` is read-only. It does not audit GitHub branch protection, CI, dependencies, or security certification.

Planned exit-code direction:
## Planned v0.3 command contracts

- `0`: diagnosis completed and supported instruction files were found;
- `1`: no supported instruction files were found, if this mirrors `check`;
- `2`: invalid input or command-line usage error.
The remaining commands are design targets. They are not available until their dedicated implementation phases are merged.

### `budget`

Expand Down
90 changes: 90 additions & 0 deletions src/agent_rules_kit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ def build_parser() -> argparse.ArgumentParser:
help="Write baseline files, backing up existing files first.",
)

doctor_parser = subparsers.add_parser(
"doctor",
help="Summarize repository-level instruction health.",
)
doctor_parser.add_argument(
"repository",
nargs="?",
default=".",
help="Repository root to inspect. Defaults to the current directory.",
)

return parser


Expand All @@ -93,10 +104,89 @@ def main(argv: Sequence[str] | None = None) -> int:
write=args.write,
)

if args.command == "doctor":
return _run_doctor(Path(args.repository))

parser.print_help()
return 0


def _run_doctor(repository_root: Path) -> int:
try:
instruction_files = discover_instruction_files(repository_root)
except ValueError as error:
print(f"ERROR: {redact_secret_like_values(str(error))}", file=sys.stderr)
return 2

findings = find_governance_findings(repository_root, instruction_files)
return _print_console_doctor(repository_root, instruction_files, findings)


def _print_console_doctor(
repository_root: Path,
instruction_files: tuple[InstructionFile, ...],
findings: tuple[Finding, ...],
) -> int:
print(f"agent-rules-kit doctor: {redact_secret_like_values(str(repository_root))}")

if not instruction_files:
print("Status: no_instruction_files")
print("Supported instruction files: 0")
print("Findings: 0")
print(
"Next step: add a supported agent instruction file before reviewing "
"governance findings."
)
return 1

status = "review" if findings else "ok"
print(f"Status: {status}")
print(f"Supported instruction files: {len(instruction_files)}")
print(f"Findings: {len(findings)}")

if findings:
print("Findings by severity:")
for severity, count in _count_findings_by_severity(findings):
print(f"- {severity}: {count}")

print("Findings by rule:")
for rule_id, count in _count_findings_by_rule(findings):
print(f"- {rule_id}: {count}")

print("Next step: review the listed governance findings with `agent-rules-kit check`.")
else:
print("Next step: no governance findings were detected by implemented checks.")

return 0


def _count_findings_by_severity(
findings: tuple[Finding, ...],
) -> tuple[tuple[str, int], ...]:
counts: dict[str, int] = {}

for finding in findings:
severity = finding.severity.value
counts[severity] = counts.get(severity, 0) + 1

return tuple(
(severity, counts[severity])
for severity in ("info", "warning", "error")
if severity in counts
)


def _count_findings_by_rule(
findings: tuple[Finding, ...],
) -> tuple[tuple[str, int], ...]:
counts: dict[str, int] = {}

for finding in findings:
counts[finding.rule_id] = counts.get(finding.rule_id, 0) + 1

return tuple(counts.items())


def _run_check(repository_root: Path, *, output_format: str = "console") -> int:
try:
instruction_files = discover_instruction_files(repository_root)
Expand Down
58 changes: 58 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,64 @@ def test_check_returns_one_when_no_instruction_files_are_found(self) -> None:
self.assertEqual(exit_code, 1)
self.assertIn("No supported agent instruction files found.", output.getvalue())

def test_doctor_reports_clean_fixture_summary(self) -> None:
output = io.StringIO()

with redirect_stdout(output):
exit_code = main(["doctor", str(FIXTURE_ROOT / "single-agent")])

text = output.getvalue()

self.assertEqual(exit_code, 0)
self.assertIn("agent-rules-kit doctor:", text)
self.assertIn("Status: ok", text)
self.assertIn("Supported instruction files: 1", text)
self.assertIn("Findings: 0", text)
self.assertIn(
"Next step: no governance findings were detected by implemented checks.",
text,
)

def test_doctor_reports_governance_summary(self) -> None:
output = io.StringIO()

with redirect_stdout(output):
exit_code = main(["doctor", str(FIXTURE_ROOT / "risky-instructions")])

text = output.getvalue()

self.assertEqual(exit_code, 0)
self.assertIn("Status: review", text)
self.assertIn("Supported instruction files: 1", text)
self.assertIn("Findings: 3", text)
self.assertIn("Findings by severity:", text)
self.assertIn("- warning: 3", text)
self.assertIn("Findings by rule:", text)
self.assertIn("- AIRK-GOV003: 3", text)
self.assertIn("agent-rules-kit check", text)

def test_doctor_returns_one_when_no_instruction_files_are_found(self) -> None:
output = io.StringIO()

with redirect_stdout(output):
exit_code = main(["doctor", str(FIXTURE_ROOT / "empty-repo")])

text = output.getvalue()

self.assertEqual(exit_code, 1)
self.assertIn("Status: no_instruction_files", text)
self.assertIn("Supported instruction files: 0", text)
self.assertIn("Findings: 0", text)

def test_doctor_returns_two_for_invalid_repository_root(self) -> None:
output = io.StringIO()

with redirect_stderr(output):
exit_code = main(["doctor", str(FIXTURE_ROOT / "missing-repo")])

self.assertEqual(exit_code, 2)
self.assertIn("ERROR: repository root does not exist:", output.getvalue())

def test_check_returns_two_for_invalid_repository_root(self) -> None:
output = io.StringIO()

Expand Down
30 changes: 30 additions & 0 deletions tests/test_golden_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,22 @@ def test_init_dry_run_existing_agents_file_matches_golden_output(self) -> None:
"existing file would be backed up before replacement\n",
)

def test_doctor_clean_fixture_matches_golden_output(self) -> None:
repository = FIXTURE_ROOT / "single-agent"

result = run_cli(["doctor", str(repository)])

self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stderr, "")
self.assertEqual(
result.stdout,
f"agent-rules-kit doctor: {repository}\n"
"Status: ok\n"
"Supported instruction files: 1\n"
"Findings: 0\n"
"Next step: no governance findings were detected by implemented checks.\n",
)

def test_init_without_mode_matches_golden_error_output(self) -> None:
repository = FIXTURE_ROOT / "single-agent"

Expand Down Expand Up @@ -234,6 +250,20 @@ def test_current_cli_contract_matrix_matches_expected_channels_and_exit_codes(se
"stdout_contains": ["# agent-rules-kit check", "- Status: no_instruction_files"],
"stderr": "",
},
{
"name": "doctor-clean",
"args": ["doctor", str(FIXTURE_ROOT / "single-agent")],
"exit_code": 0,
"stdout_contains": ["Status: ok", "Supported instruction files: 1"],
"stderr": "",
},
{
"name": "doctor-empty",
"args": ["doctor", str(FIXTURE_ROOT / "empty-repo")],
"exit_code": 1,
"stdout_contains": ["Status: no_instruction_files", "Findings: 0"],
"stderr": "",
},
{
"name": "init-dry-run",
"args": ["init", str(FIXTURE_ROOT / "single-agent"), "--dry-run"],
Expand Down