qodo-cli 0.4.0__tar.gz → 0.5.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.
- qodo_cli-0.5.0/.pr_agent.toml +26 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/CHANGELOG.md +27 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/PKG-INFO +1 -1
- qodo_cli-0.5.0/best_practices.md +59 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/pyproject.toml +1 -1
- qodo_cli-0.5.0/qodo/cli/_commands/doctor.py +262 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/learn.py +2 -2
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/overview.py +1 -1
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/explain/catalog.py +15 -3
- qodo_cli-0.5.0/tests/test_cli_introspection.py +197 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/uv.lock +1 -1
- qodo_cli-0.4.0/qodo/cli/_commands/doctor.py +0 -124
- qodo_cli-0.4.0/tests/test_cli_introspection.py +0 -106
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/agent-config/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/prompts/review.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/prompts/write.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/think/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/think/scripts/think.sh +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills.local.yaml.example +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.devague/current +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.devague/frames/qodo-cli-now-does-qodo-s-two-core-jobs-natively-fr.json +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.flake8 +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.github/workflows/publish.yml +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.github/workflows/tests.yml +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.gitignore +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.markdownlint-cli2.yaml +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/AGENTS.colleague.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/CLAUDE.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/LICENSE +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/README.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/culture.yaml +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/docs/qodo-skills-sources.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/docs/skill-sources.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/docs/specs/2026-06-16-qodo-cli-now-does-qodo-s-two-core-jobs-natively-fr.md +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/__init__.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/__main__.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/__init__.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/__init__.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/cli.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/explain.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/review.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/rules.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/whoami.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_errors.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_output.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_providers.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_qodo_api.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/explain/__init__.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/sonar-project.properties +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/tests/__init__.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/tests/test_cli.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/tests/test_review.py +0 -0
- {qodo_cli-0.4.0 → qodo_cli-0.5.0}/tests/test_rules.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Qodo Merge / PR-Agent configuration for qodo-cli.
|
|
2
|
+
# Docs: https://docs.qodo.ai/qodo-documentation/qodo-merge/configuration/configuration-file
|
|
3
|
+
#
|
|
4
|
+
# Kept minimal on purpose (per Qodo's guidance: only override what you need).
|
|
5
|
+
# The repo's coding conventions live in best_practices.md at the root, which the
|
|
6
|
+
# reviewer auto-references; this file just reinforces a few intentional patterns
|
|
7
|
+
# that an earlier review flagged as false positives.
|
|
8
|
+
|
|
9
|
+
[pr_reviewer]
|
|
10
|
+
extra_instructions = """
|
|
11
|
+
qodo-cli is an unofficial, community CLI to manage Qodo, built as a
|
|
12
|
+
zero-runtime-dependency, stdlib-only Python package. Intentional patterns (do
|
|
13
|
+
not flag these as issues):
|
|
14
|
+
- Command handlers return a bare 0/1/None for the exit code. The EXIT_* constants
|
|
15
|
+
in qodo/cli/_errors.py are for CliError codes (the failure path), not for
|
|
16
|
+
handler return values. `return 0` in a handler is intentional and consistent
|
|
17
|
+
across every command.
|
|
18
|
+
- Nested argparse subparsers inherit the structured-error parser class via
|
|
19
|
+
`add_subparsers(parser_class=type(p))` (see qodo/cli/_commands/cli.py). Passing
|
|
20
|
+
`type(p)` is the established idiom and is equivalent to naming
|
|
21
|
+
`_CliArgumentParser` directly — it is not a missing parser_class.
|
|
22
|
+
- `qodo review resolve --reply` drives the user's own `gh` with their own auth;
|
|
23
|
+
the reply text belongs to the caller, so the tool must not auto-append an agent
|
|
24
|
+
signature.
|
|
25
|
+
See best_practices.md at the repo root for the full conventions.
|
|
26
|
+
"""
|
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-06-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `.pr_agent.toml` + `best_practices.md` at the repo root — the Qodo Merge
|
|
13
|
+
reviewer config (cite, don't fork). They codify this repo's intentional
|
|
14
|
+
patterns (bare `return 0` handlers, `parser_class=type(p)` subparser nesting,
|
|
15
|
+
the mechanics-only `review resolve --reply`) so Qodo reviews accurately and
|
|
16
|
+
stops raising them as violations.
|
|
17
|
+
- `qodo doctor` now also checks **Qodo setup**, against the current git repo:
|
|
18
|
+
`.pr_agent.toml` present, `best_practices.md` present, and whether a usable
|
|
19
|
+
Qodo API key is resolvable for `qodo rules` — `QODO_API_KEY`, else a non-empty
|
|
20
|
+
`API_KEY` in `~/.qodo/config.json` (a present-but-keyless or malformed file
|
|
21
|
+
fails the check with guidance, and never throws). These are advisory and each
|
|
22
|
+
carries a `remediation` that guides an agent through setup. Runs in any repo
|
|
23
|
+
(not just a source checkout). (Addresses #7.)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `qodo doctor` `healthy` now depends on **error**-severity checks only;
|
|
28
|
+
`warning`/`info` checks surface guidance without flipping `healthy` or the
|
|
29
|
+
exit code. Fixes the `claude → CLAUDE.md` drift in the `doctor` explain entry
|
|
30
|
+
(this repo's backend is `colleague` → `AGENTS.colleague.md`).
|
|
31
|
+
- `learn` now describes `doctor` as "agent-identity invariants + Qodo setup" in
|
|
32
|
+
both its text and JSON payload, matching `overview` and the explain catalog
|
|
33
|
+
(self-description stays consistent across the introspection surfaces).
|
|
34
|
+
|
|
8
35
|
## [0.4.0] - 2026-06-17
|
|
9
36
|
|
|
10
37
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qodo-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Community CLI and agent to manage Qodo — the AI code reviewer and Qodo's other agents (requires a Qodo subscription). Unofficial: not affiliated with, authorized, or endorsed by Qodo; the Qodo name and trademark belong to Qodo Ltd.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentculture/qodo-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/agentculture/qodo-cli/issues
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Best practices for qodo-cli
|
|
2
|
+
|
|
3
|
+
Repository-specific coding standards for the Qodo reviewer — and for any agent
|
|
4
|
+
working in this repo. `qodo-cli` is an unofficial, community CLI to manage Qodo,
|
|
5
|
+
built as a **zero-runtime-dependency, stdlib-only** Python package.
|
|
6
|
+
|
|
7
|
+
These conventions are pinned by the test suite and the agent-first rubric
|
|
8
|
+
(`teken cli doctor . --strict`); please review against them rather than against
|
|
9
|
+
generic defaults.
|
|
10
|
+
|
|
11
|
+
## Dependencies
|
|
12
|
+
|
|
13
|
+
- Runtime dependencies must stay empty (`dependencies = []` in `pyproject.toml`).
|
|
14
|
+
Use only the Python standard library at runtime, and flag any new third-party
|
|
15
|
+
runtime import. `teken` and the lint/test tools are dev-only.
|
|
16
|
+
|
|
17
|
+
## Exit codes
|
|
18
|
+
|
|
19
|
+
- Command handlers return a bare `0` / `1` / `None` for their exit code (`0` is
|
|
20
|
+
success). The `EXIT_*` constants in `qodo/cli/_errors.py` are for `CliError`
|
|
21
|
+
codes on the failure path, **not** for handler return values. A bare
|
|
22
|
+
`return 0` in a handler is intentional and consistent across every command —
|
|
23
|
+
do not flag it as a magic number.
|
|
24
|
+
|
|
25
|
+
## Errors and output
|
|
26
|
+
|
|
27
|
+
- Every failure raises `CliError(code, message, remediation)`; no Python
|
|
28
|
+
traceback may leak to stderr. Text-mode errors render `error: <msg>` then
|
|
29
|
+
`hint: <remediation>` (the `hint:` prefix is required).
|
|
30
|
+
- Results go to stdout; errors and diagnostics go to stderr. Never mix the two,
|
|
31
|
+
in text or `--json` mode. Every command supports `--json`.
|
|
32
|
+
|
|
33
|
+
## argparse
|
|
34
|
+
|
|
35
|
+
- Build subparsers with the structured-error parser class. Nested subparsers
|
|
36
|
+
inherit it via `add_subparsers(parser_class=type(p))` (see
|
|
37
|
+
`qodo/cli/_commands/cli.py`); passing `type(p)` is the established idiom and is
|
|
38
|
+
equivalent to naming `_CliArgumentParser` directly — it is not a missing
|
|
39
|
+
`parser_class`.
|
|
40
|
+
- Add the standard `--json` flag with `add_json_flag()` from `qodo.cli._output`
|
|
41
|
+
rather than re-declaring the literal.
|
|
42
|
+
|
|
43
|
+
## The CLI is mechanics-only
|
|
44
|
+
|
|
45
|
+
- `qodo review resolve --reply` drives the user's own `gh` with their own auth;
|
|
46
|
+
the reply text belongs to the caller. The tool must not auto-append an agent
|
|
47
|
+
signature — that would mis-attribute human-authored replies. Signing is the
|
|
48
|
+
caller's responsibility (an opt-in flag may be added later).
|
|
49
|
+
|
|
50
|
+
## Cite, don't import
|
|
51
|
+
|
|
52
|
+
- Behavior derived from `qodo-ai/qodo-skills` and the vendored `.claude/skills/`
|
|
53
|
+
kit is cited as the source of truth, not forked or vendored into runtime code.
|
|
54
|
+
Keep `docs/qodo-skills-sources.md` in sync when the upstream contract changes.
|
|
55
|
+
|
|
56
|
+
## Keep the self-description in sync
|
|
57
|
+
|
|
58
|
+
- When adding a command, update `learn`, `overview`, and the explain catalog so
|
|
59
|
+
the rubric and the self-describing text stay consistent.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qodo-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Community CLI and agent to manage Qodo — the AI code reviewer and Qodo's other agents (requires a Qodo subscription). Unofficial: not affiliated with, authorized, or endorsed by Qodo; the Qodo name and trademark belong to Qodo Ltd."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""``qodo-cli doctor`` — check agent-identity invariants and Qodo setup.
|
|
2
|
+
|
|
3
|
+
Two groups of checks, reported in one rubric-shaped contract
|
|
4
|
+
``{healthy, checks: [{id, passed, severity, message, remediation}]}``:
|
|
5
|
+
|
|
6
|
+
* **agent identity** (only in a source checkout with ``culture.yaml``):
|
|
7
|
+
prompt-file-present + backend-consistency (mirrors ``steward doctor``), plus a
|
|
8
|
+
``.claude/skills/`` present check.
|
|
9
|
+
* **Qodo setup** (always, against the current repo): whether the repo carries a
|
|
10
|
+
Qodo reviewer config (``.pr_agent.toml`` / ``best_practices.md``) and whether
|
|
11
|
+
client API credentials (``~/.qodo/config.json`` / ``QODO_API_KEY``) are
|
|
12
|
+
available for ``qodo rules``. These are advisory — their ``remediation`` guides
|
|
13
|
+
an agent through setting them up.
|
|
14
|
+
|
|
15
|
+
``healthy`` is true when every **error**-severity check passes; ``warning`` /
|
|
16
|
+
``info`` checks surface guidance without failing the command (so ``doctor`` is
|
|
17
|
+
useful in any repo without hard-failing when optional configs are absent).
|
|
18
|
+
Read-only.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from qodo.cli._commands.whoami import find_culture_yaml, read_agent_fields
|
|
29
|
+
from qodo.cli._output import add_json_flag, emit_result
|
|
30
|
+
|
|
31
|
+
# backend → required prompt file (the backend-consistency mapping).
|
|
32
|
+
_PROMPT_FILE = {
|
|
33
|
+
"claude": "CLAUDE.md",
|
|
34
|
+
"colleague": "AGENTS.colleague.md",
|
|
35
|
+
"acp": "AGENTS.md",
|
|
36
|
+
"gemini": "GEMINI.md",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_CONFIG_DOCS = "https://docs.qodo.ai/qodo-documentation/qodo-merge/configuration/configuration-file"
|
|
40
|
+
_BEST_PRACTICES_DOCS = "https://qodo-merge-docs.qodo.ai/core-abilities/auto_best_practices/"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _identity_checks(root: Path, backend: str) -> list[dict[str, object]]:
|
|
44
|
+
"""Agent-identity invariants (prompt-file-present, backend-consistency, skills)."""
|
|
45
|
+
checks: list[dict[str, object]] = []
|
|
46
|
+
|
|
47
|
+
expected = _PROMPT_FILE.get(backend)
|
|
48
|
+
if expected is None:
|
|
49
|
+
checks.append(
|
|
50
|
+
{
|
|
51
|
+
"id": "backend_consistency",
|
|
52
|
+
"passed": False,
|
|
53
|
+
"severity": "error",
|
|
54
|
+
"message": f"unknown backend '{backend}' in culture.yaml",
|
|
55
|
+
"remediation": f"set backend to one of: {', '.join(sorted(_PROMPT_FILE))}",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
present = (root / expected).is_file()
|
|
60
|
+
checks.append(
|
|
61
|
+
{
|
|
62
|
+
"id": "prompt_file_present",
|
|
63
|
+
"passed": present,
|
|
64
|
+
"severity": "error",
|
|
65
|
+
"message": (
|
|
66
|
+
f"backend '{backend}' requires {expected} — "
|
|
67
|
+
+ ("present" if present else "missing")
|
|
68
|
+
),
|
|
69
|
+
"remediation": "" if present else f"create {expected} at the repo root",
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
skills_dir = root / ".claude" / "skills"
|
|
74
|
+
has_skills = skills_dir.is_dir() and any(skills_dir.iterdir())
|
|
75
|
+
checks.append(
|
|
76
|
+
{
|
|
77
|
+
"id": "skills_present",
|
|
78
|
+
"passed": has_skills,
|
|
79
|
+
"severity": "warning",
|
|
80
|
+
"message": (
|
|
81
|
+
".claude/skills/ vendored" if has_skills else ".claude/skills/ missing or empty"
|
|
82
|
+
),
|
|
83
|
+
"remediation": (
|
|
84
|
+
"" if has_skills else "vendor the skill kit (see docs/skill-sources.md)"
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
return checks
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _qodo_setup_checks(repo_root: Path, home: Path) -> list[dict[str, object]]:
|
|
92
|
+
"""Detect the Qodo configs a repo using qodo-cli should carry.
|
|
93
|
+
|
|
94
|
+
Advisory (``warning`` / ``info``): the remediation text guides an agent
|
|
95
|
+
through creating each one. None of these flip ``healthy``.
|
|
96
|
+
"""
|
|
97
|
+
checks: list[dict[str, object]] = []
|
|
98
|
+
|
|
99
|
+
pr_agent = (repo_root / ".pr_agent.toml").is_file()
|
|
100
|
+
checks.append(
|
|
101
|
+
{
|
|
102
|
+
"id": "pr_agent_config_present",
|
|
103
|
+
"passed": pr_agent,
|
|
104
|
+
"severity": "warning",
|
|
105
|
+
"message": (
|
|
106
|
+
".pr_agent.toml present (Qodo reviewer config)"
|
|
107
|
+
if pr_agent
|
|
108
|
+
else "no .pr_agent.toml — Qodo reviews this repo using only inferred conventions"
|
|
109
|
+
),
|
|
110
|
+
"remediation": (
|
|
111
|
+
""
|
|
112
|
+
if pr_agent
|
|
113
|
+
else (
|
|
114
|
+
"add a minimal .pr_agent.toml with a [pr_reviewer] section "
|
|
115
|
+
"(extra_instructions) describing this repo's intentional patterns. "
|
|
116
|
+
f"Docs: {_CONFIG_DOCS}"
|
|
117
|
+
)
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
best_practices = (repo_root / "best_practices.md").is_file()
|
|
123
|
+
checks.append(
|
|
124
|
+
{
|
|
125
|
+
"id": "best_practices_present",
|
|
126
|
+
"passed": best_practices,
|
|
127
|
+
"severity": "warning",
|
|
128
|
+
"message": (
|
|
129
|
+
"best_practices.md present"
|
|
130
|
+
if best_practices
|
|
131
|
+
else "no best_practices.md — Qodo won't flag this repo's best-practice violations"
|
|
132
|
+
),
|
|
133
|
+
"remediation": (
|
|
134
|
+
""
|
|
135
|
+
if best_practices
|
|
136
|
+
else (
|
|
137
|
+
"add best_practices.md at the repo root listing this repo's coding "
|
|
138
|
+
f"standards (auto-referenced by the reviewer). Docs: {_BEST_PRACTICES_DOCS}"
|
|
139
|
+
)
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
ok, message, remediation = _client_credential_status(home)
|
|
145
|
+
checks.append(
|
|
146
|
+
{
|
|
147
|
+
"id": "qodo_client_config_present",
|
|
148
|
+
"passed": ok,
|
|
149
|
+
"severity": "info",
|
|
150
|
+
"message": message,
|
|
151
|
+
"remediation": remediation,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
return checks
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _client_credential_status(home: Path) -> tuple[bool, str, str]:
|
|
158
|
+
"""Whether 'qodo rules' would find a usable API key — never raises.
|
|
159
|
+
|
|
160
|
+
Mirrors what the rules client actually needs (``qodo/cli/_qodo_api.py``):
|
|
161
|
+
a resolvable ``API_KEY`` from ``QODO_API_KEY`` or a readable, well-formed
|
|
162
|
+
``~/.qodo/config.json``. Mere file existence is not enough. Returns
|
|
163
|
+
``(passed, message, remediation)``.
|
|
164
|
+
"""
|
|
165
|
+
if os.environ.get("QODO_API_KEY"):
|
|
166
|
+
return True, "Qodo API key available via QODO_API_KEY (for 'qodo rules')", ""
|
|
167
|
+
|
|
168
|
+
cfg_path = home / ".qodo" / "config.json"
|
|
169
|
+
if not cfg_path.is_file():
|
|
170
|
+
return (
|
|
171
|
+
False,
|
|
172
|
+
"no ~/.qodo/config.json and QODO_API_KEY unset (needed for 'qodo rules')",
|
|
173
|
+
'create ~/.qodo/config.json with an "API_KEY" (or export QODO_API_KEY). '
|
|
174
|
+
"Only 'qodo rules' needs it; 'qodo review' uses your provider-CLI auth.",
|
|
175
|
+
)
|
|
176
|
+
try:
|
|
177
|
+
data = json.loads(cfg_path.read_text(encoding="utf-8"))
|
|
178
|
+
except OSError:
|
|
179
|
+
return (
|
|
180
|
+
False,
|
|
181
|
+
"~/.qodo/config.json is present but unreadable",
|
|
182
|
+
"check the permissions on ~/.qodo/config.json",
|
|
183
|
+
)
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
return (
|
|
186
|
+
False,
|
|
187
|
+
"~/.qodo/config.json is not valid JSON",
|
|
188
|
+
'repair ~/.qodo/config.json (it must be a JSON object with an "API_KEY")',
|
|
189
|
+
)
|
|
190
|
+
if not isinstance(data, dict) or not str(data.get("API_KEY") or "").strip():
|
|
191
|
+
return (
|
|
192
|
+
False,
|
|
193
|
+
"~/.qodo/config.json has no API_KEY (needed for 'qodo rules')",
|
|
194
|
+
'add a non-empty "API_KEY" to ~/.qodo/config.json (or export QODO_API_KEY)',
|
|
195
|
+
)
|
|
196
|
+
return True, "Qodo API key present in ~/.qodo/config.json (for 'qodo rules')", ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _repo_root(start: Path) -> Path:
|
|
200
|
+
"""The git repo root at/above ``start`` (where Qodo reads its config), or ``start``."""
|
|
201
|
+
for candidate in [start, *start.parents]:
|
|
202
|
+
if (candidate / ".git").exists():
|
|
203
|
+
return candidate
|
|
204
|
+
return start
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _is_healthy(checks: list[dict[str, object]]) -> bool:
|
|
208
|
+
"""Healthy iff every error-severity check passed (advisory checks never fail)."""
|
|
209
|
+
return all(c["passed"] for c in checks if c["severity"] == "error")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _diagnose() -> dict[str, object]:
|
|
213
|
+
checks: list[dict[str, object]] = []
|
|
214
|
+
|
|
215
|
+
cfg = find_culture_yaml()
|
|
216
|
+
if cfg is not None:
|
|
217
|
+
checks.extend(_identity_checks(cfg.parent, str(read_agent_fields()["backend"])))
|
|
218
|
+
else:
|
|
219
|
+
checks.append(
|
|
220
|
+
{
|
|
221
|
+
"id": "source_checkout",
|
|
222
|
+
"passed": True,
|
|
223
|
+
"severity": "info",
|
|
224
|
+
"message": "no culture.yaml alongside the package; agent-identity checks skipped",
|
|
225
|
+
"remediation": "",
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
checks.extend(_qodo_setup_checks(_repo_root(Path.cwd()), Path.home()))
|
|
230
|
+
|
|
231
|
+
return {"healthy": _is_healthy(checks), "checks": checks}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _mark(check: dict[str, object]) -> str:
|
|
235
|
+
if check["passed"]:
|
|
236
|
+
return "ok"
|
|
237
|
+
return {"error": "FAIL", "warning": "WARN"}.get(str(check["severity"]), "note")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
241
|
+
report = _diagnose()
|
|
242
|
+
json_mode = bool(getattr(args, "json", False))
|
|
243
|
+
if json_mode:
|
|
244
|
+
emit_result(report, json_mode=True)
|
|
245
|
+
else:
|
|
246
|
+
status = "healthy" if report["healthy"] else "unhealthy"
|
|
247
|
+
lines = [f"qodo-cli doctor: {status}", ""]
|
|
248
|
+
for check in report["checks"]: # type: ignore[attr-defined]
|
|
249
|
+
lines.append(f"[{_mark(check)}] {check['id']}: {check['message']}")
|
|
250
|
+
if not check["passed"] and check["remediation"]:
|
|
251
|
+
lines.append(f" hint: {check['remediation']}")
|
|
252
|
+
emit_result("\n".join(lines), json_mode=False)
|
|
253
|
+
return 0 if report["healthy"] else 1
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
257
|
+
p = sub.add_parser(
|
|
258
|
+
"doctor",
|
|
259
|
+
help="Check agent-identity invariants and Qodo setup (configs); guides any fixes.",
|
|
260
|
+
)
|
|
261
|
+
add_json_flag(p)
|
|
262
|
+
p.set_defaults(func=cmd_doctor)
|
|
@@ -34,7 +34,7 @@ Commands
|
|
|
34
34
|
qodo-cli learn This self-teaching prompt.
|
|
35
35
|
qodo-cli explain <path>... Markdown docs for any noun/verb path.
|
|
36
36
|
qodo-cli overview Descriptive snapshot of the agent.
|
|
37
|
-
qodo-cli doctor Check
|
|
37
|
+
qodo-cli doctor Check agent-identity invariants + Qodo setup.
|
|
38
38
|
qodo-cli cli overview Describe the CLI surface itself.
|
|
39
39
|
|
|
40
40
|
Machine-readable output
|
|
@@ -69,7 +69,7 @@ def _as_json_payload() -> dict[str, object]:
|
|
|
69
69
|
{"path": ["learn"], "summary": "Self-teaching prompt."},
|
|
70
70
|
{"path": ["explain"], "summary": "Markdown docs by path."},
|
|
71
71
|
{"path": ["overview"], "summary": "Descriptive snapshot of the agent."},
|
|
72
|
-
{"path": ["doctor"], "summary": "Check
|
|
72
|
+
{"path": ["doctor"], "summary": "Check agent-identity invariants + Qodo setup."},
|
|
73
73
|
{"path": ["cli", "overview"], "summary": "Describe the CLI surface."},
|
|
74
74
|
],
|
|
75
75
|
"exit_codes": {
|
|
@@ -32,7 +32,7 @@ _VERBS = [
|
|
|
32
32
|
"learn — structured self-teaching prompt",
|
|
33
33
|
"explain <path> — markdown docs for a topic",
|
|
34
34
|
"overview — this descriptive snapshot",
|
|
35
|
-
"doctor — check
|
|
35
|
+
"doctor — check agent-identity invariants + Qodo setup (configs), with fix guidance",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
|
|
@@ -156,9 +156,21 @@ ignored `target` so a stray path never hard-fails.
|
|
|
156
156
|
_DOCTOR = """\
|
|
157
157
|
# qodo-cli doctor
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
Two groups of checks, emitted as
|
|
160
|
+
`{healthy, checks: [{id, passed, severity, message, remediation}]}`:
|
|
161
|
+
|
|
162
|
+
- **agent identity** (in a source checkout) — prompt-file-present and
|
|
163
|
+
backend-consistency, mirroring `steward doctor` (this repo's backend is
|
|
164
|
+
`colleague` → `AGENTS.colleague.md`), plus a `.claude/skills/` check.
|
|
165
|
+
- **Qodo setup** (any repo, against the current git root) — whether
|
|
166
|
+
`.pr_agent.toml` and `best_practices.md` are present (tune Qodo's PR reviews)
|
|
167
|
+
and whether `~/.qodo/config.json` / `QODO_API_KEY` is available (for
|
|
168
|
+
`qodo rules`). These are advisory; each carries a `remediation` that guides
|
|
169
|
+
setup.
|
|
170
|
+
|
|
171
|
+
`healthy` is true when every **error**-severity check passes; advisory
|
|
172
|
+
(`warning`/`info`) checks surface guidance without flipping it. Exits 1 only on
|
|
173
|
+
an error-severity failure.
|
|
162
174
|
|
|
163
175
|
## Usage
|
|
164
176
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Tests for the introspection verbs: overview, cli overview, doctor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from qodo.cli import main
|
|
10
|
+
from qodo.cli._commands import doctor as _doctor
|
|
11
|
+
|
|
12
|
+
# --- overview -------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_overview_text(capsys: pytest.CaptureFixture[str]) -> None:
|
|
16
|
+
rc = main(["overview"])
|
|
17
|
+
assert rc == 0
|
|
18
|
+
out = capsys.readouterr().out
|
|
19
|
+
assert "# qodo-cli" in out
|
|
20
|
+
assert "Identity" in out
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_overview_json_shape(capsys: pytest.CaptureFixture[str]) -> None:
|
|
24
|
+
rc = main(["overview", "--json"])
|
|
25
|
+
assert rc == 0
|
|
26
|
+
payload = json.loads(capsys.readouterr().out)
|
|
27
|
+
assert payload["subject"] == "qodo-cli"
|
|
28
|
+
assert isinstance(payload["sections"], list)
|
|
29
|
+
assert payload["sections"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_overview_graceful_on_bad_path(capsys: pytest.CaptureFixture[str]) -> None:
|
|
33
|
+
# Rubric contract: descriptive verbs never hard-fail on a missing target.
|
|
34
|
+
rc = main(["overview", "/no/such/path/here"])
|
|
35
|
+
assert rc == 0
|
|
36
|
+
assert capsys.readouterr().out.strip()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- cli overview ---------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_cli_overview_text(capsys: pytest.CaptureFixture[str]) -> None:
|
|
43
|
+
rc = main(["cli", "overview"])
|
|
44
|
+
assert rc == 0
|
|
45
|
+
assert "# qodo-cli cli" in capsys.readouterr().out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_cli_overview_json_shape(capsys: pytest.CaptureFixture[str]) -> None:
|
|
49
|
+
rc = main(["cli", "overview", "--json"])
|
|
50
|
+
assert rc == 0
|
|
51
|
+
payload = json.loads(capsys.readouterr().out)
|
|
52
|
+
assert payload["subject"] == "qodo-cli cli"
|
|
53
|
+
assert isinstance(payload["sections"], list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_cli_noun_bare_is_non_empty(capsys: pytest.CaptureFixture[str]) -> None:
|
|
57
|
+
rc = main(["cli"])
|
|
58
|
+
assert rc == 0
|
|
59
|
+
assert capsys.readouterr().out.strip()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_cli_overview_unknown_flag_structured_error(
|
|
63
|
+
capsys: pytest.CaptureFixture[str],
|
|
64
|
+
) -> None:
|
|
65
|
+
# `cli overview` parse errors must route through the structured error
|
|
66
|
+
# contract (error:/hint: + exit 1), not argparse's default stderr/exit 2.
|
|
67
|
+
with pytest.raises(SystemExit) as exc:
|
|
68
|
+
main(["cli", "overview", "--bogus"])
|
|
69
|
+
assert exc.value.code == 1
|
|
70
|
+
err = capsys.readouterr().err
|
|
71
|
+
assert err.startswith("error:")
|
|
72
|
+
assert "hint:" in err
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- doctor ---------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_doctor_text(capsys: pytest.CaptureFixture[str]) -> None:
|
|
79
|
+
rc = main(["doctor"])
|
|
80
|
+
assert rc in (0, 1)
|
|
81
|
+
assert "qodo-cli doctor" in capsys.readouterr().out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_doctor_json_shape(capsys: pytest.CaptureFixture[str]) -> None:
|
|
85
|
+
rc = main(["doctor", "--json"])
|
|
86
|
+
assert rc in (0, 1)
|
|
87
|
+
payload = json.loads(capsys.readouterr().out)
|
|
88
|
+
assert isinstance(payload["healthy"], bool)
|
|
89
|
+
assert isinstance(payload["checks"], list)
|
|
90
|
+
assert payload["checks"]
|
|
91
|
+
for check in payload["checks"]:
|
|
92
|
+
assert {"id", "passed", "severity", "message", "remediation"} <= set(check)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_doctor_recognizes_declared_backend(capsys: pytest.CaptureFixture[str]) -> None:
|
|
96
|
+
"""The repo's own declared backend must be a known one — doctor stays healthy.
|
|
97
|
+
|
|
98
|
+
Guards the backend-consistency invariant: a promotion that changes
|
|
99
|
+
``culture.yaml``'s backend without teaching ``doctor`` the matching prompt
|
|
100
|
+
file would otherwise slip through (the shape tests above tolerate rc==1).
|
|
101
|
+
"""
|
|
102
|
+
rc = main(["doctor", "--json"])
|
|
103
|
+
payload = json.loads(capsys.readouterr().out)
|
|
104
|
+
messages = " ".join(str(c["message"]) for c in payload["checks"])
|
|
105
|
+
assert "unknown backend" not in messages
|
|
106
|
+
assert rc == 0
|
|
107
|
+
assert payload["healthy"] is True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --- doctor: Qodo-setup detection -----------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_doctor_reports_qodo_setup_checks(capsys: pytest.CaptureFixture[str]) -> None:
|
|
114
|
+
main(["doctor", "--json"])
|
|
115
|
+
payload = json.loads(capsys.readouterr().out)
|
|
116
|
+
ids = {c["id"] for c in payload["checks"]}
|
|
117
|
+
assert {
|
|
118
|
+
"pr_agent_config_present",
|
|
119
|
+
"best_practices_present",
|
|
120
|
+
"qodo_client_config_present",
|
|
121
|
+
} <= ids
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_qodo_setup_checks_flag_missing(tmp_path) -> None:
|
|
125
|
+
# Empty repo root + home with no ~/.qodo/config.json and no QODO_API_KEY.
|
|
126
|
+
home = tmp_path / "home"
|
|
127
|
+
home.mkdir()
|
|
128
|
+
import os
|
|
129
|
+
from unittest import mock
|
|
130
|
+
|
|
131
|
+
with mock.patch.dict(os.environ, {}, clear=False):
|
|
132
|
+
os.environ.pop("QODO_API_KEY", None)
|
|
133
|
+
checks = {c["id"]: c for c in _doctor._qodo_setup_checks(tmp_path, home)}
|
|
134
|
+
assert checks["pr_agent_config_present"]["passed"] is False
|
|
135
|
+
assert checks["best_practices_present"]["passed"] is False
|
|
136
|
+
assert checks["qodo_client_config_present"]["passed"] is False
|
|
137
|
+
# Advisory only — none is error-severity, and each guides setup.
|
|
138
|
+
for c in checks.values():
|
|
139
|
+
assert c["severity"] in ("warning", "info")
|
|
140
|
+
assert c["remediation"] # non-empty guidance
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_qodo_setup_checks_pass_when_present(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
144
|
+
(tmp_path / ".pr_agent.toml").write_text("[pr_reviewer]\n", encoding="utf-8")
|
|
145
|
+
(tmp_path / "best_practices.md").write_text("# bp\n", encoding="utf-8")
|
|
146
|
+
home = tmp_path / "home"
|
|
147
|
+
(home / ".qodo").mkdir(parents=True)
|
|
148
|
+
# A real API_KEY — not just an existing file (that was the bug Qodo caught).
|
|
149
|
+
(home / ".qodo" / "config.json").write_text('{"API_KEY": "k"}', encoding="utf-8")
|
|
150
|
+
monkeypatch.delenv("QODO_API_KEY", raising=False)
|
|
151
|
+
checks = {c["id"]: c for c in _doctor._qodo_setup_checks(tmp_path, home)}
|
|
152
|
+
assert all(checks[k]["passed"] for k in checks)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_qodo_client_config_fails_without_api_key(
|
|
156
|
+
tmp_path, monkeypatch: pytest.MonkeyPatch
|
|
157
|
+
) -> None:
|
|
158
|
+
# An existing config.json with no API_KEY must NOT pass — `qodo rules` needs the key.
|
|
159
|
+
home = tmp_path / "home"
|
|
160
|
+
(home / ".qodo").mkdir(parents=True)
|
|
161
|
+
(home / ".qodo" / "config.json").write_text("{}", encoding="utf-8")
|
|
162
|
+
monkeypatch.delenv("QODO_API_KEY", raising=False)
|
|
163
|
+
checks = {c["id"]: c for c in _doctor._qodo_setup_checks(tmp_path, home)}
|
|
164
|
+
cc = checks["qodo_client_config_present"]
|
|
165
|
+
assert cc["passed"] is False
|
|
166
|
+
assert cc["remediation"]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_qodo_client_config_malformed_does_not_throw(
|
|
170
|
+
tmp_path, monkeypatch: pytest.MonkeyPatch
|
|
171
|
+
) -> None:
|
|
172
|
+
home = tmp_path / "home"
|
|
173
|
+
(home / ".qodo").mkdir(parents=True)
|
|
174
|
+
(home / ".qodo" / "config.json").write_text("{not json", encoding="utf-8")
|
|
175
|
+
monkeypatch.delenv("QODO_API_KEY", raising=False)
|
|
176
|
+
# Must not raise; reports a failed advisory check with guidance.
|
|
177
|
+
checks = {c["id"]: c for c in _doctor._qodo_setup_checks(tmp_path, home)}
|
|
178
|
+
cc = checks["qodo_client_config_present"]
|
|
179
|
+
assert cc["passed"] is False
|
|
180
|
+
assert "JSON" in cc["message"] or "valid" in cc["message"]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_qodo_client_config_satisfied_by_env(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
184
|
+
monkeypatch.setenv("QODO_API_KEY", "k")
|
|
185
|
+
checks = {c["id"]: c for c in _doctor._qodo_setup_checks(tmp_path, tmp_path)}
|
|
186
|
+
assert checks["qodo_client_config_present"]["passed"] is True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_is_healthy_ignores_advisory_failures() -> None:
|
|
190
|
+
checks = [
|
|
191
|
+
{"id": "a", "passed": True, "severity": "error"},
|
|
192
|
+
{"id": "b", "passed": False, "severity": "warning"},
|
|
193
|
+
{"id": "c", "passed": False, "severity": "info"},
|
|
194
|
+
]
|
|
195
|
+
assert _doctor._is_healthy(checks) is True
|
|
196
|
+
checks.append({"id": "d", "passed": False, "severity": "error"})
|
|
197
|
+
assert _doctor._is_healthy(checks) is False
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"""``qodo-cli doctor`` — check the agent-identity invariants.
|
|
2
|
-
|
|
3
|
-
Mirrors the two invariants ``steward doctor`` verifies for a mesh agent:
|
|
4
|
-
|
|
5
|
-
* **prompt-file-present** — the repo declares an agent in ``culture.yaml`` and
|
|
6
|
-
has the matching prompt file on disk;
|
|
7
|
-
* **backend-consistency** — the declared ``backend`` matches the prompt file
|
|
8
|
-
(``claude`` → ``CLAUDE.md``, ``colleague`` → ``AGENTS.colleague.md``,
|
|
9
|
-
``acp`` → ``AGENTS.md``, ``gemini`` → ``GEMINI.md``).
|
|
10
|
-
|
|
11
|
-
Plus a **skills-present** check (the vendored ``.claude/skills/`` kit). Read-only.
|
|
12
|
-
|
|
13
|
-
Reports the rubric-shaped contract
|
|
14
|
-
``{healthy, checks: [{id, passed, severity, message, remediation}]}`` so the
|
|
15
|
-
agent-first rubric's bundle 7 passes. When run from a wheel install (no
|
|
16
|
-
``culture.yaml`` alongside the package), it reports a single info check and
|
|
17
|
-
exits 0 — there is nothing to diagnose.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import argparse
|
|
23
|
-
|
|
24
|
-
from qodo.cli._commands.whoami import find_culture_yaml, read_agent_fields
|
|
25
|
-
from qodo.cli._output import add_json_flag, emit_result
|
|
26
|
-
|
|
27
|
-
# backend → required prompt file (the backend-consistency mapping).
|
|
28
|
-
_PROMPT_FILE = {
|
|
29
|
-
"claude": "CLAUDE.md",
|
|
30
|
-
"colleague": "AGENTS.colleague.md",
|
|
31
|
-
"acp": "AGENTS.md",
|
|
32
|
-
"gemini": "GEMINI.md",
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _diagnose() -> dict[str, object]:
|
|
37
|
-
cfg = find_culture_yaml()
|
|
38
|
-
if cfg is None:
|
|
39
|
-
check = {
|
|
40
|
-
"id": "source_checkout",
|
|
41
|
-
"passed": True,
|
|
42
|
-
"severity": "info",
|
|
43
|
-
"message": "no culture.yaml found alongside the package; identity checks skipped",
|
|
44
|
-
"remediation": "",
|
|
45
|
-
}
|
|
46
|
-
return {"healthy": True, "checks": [check]}
|
|
47
|
-
|
|
48
|
-
root = cfg.parent
|
|
49
|
-
fields = read_agent_fields()
|
|
50
|
-
backend = fields["backend"]
|
|
51
|
-
checks: list[dict[str, object]] = []
|
|
52
|
-
|
|
53
|
-
# 1. backend-consistency: the prompt file for the declared backend exists.
|
|
54
|
-
expected = _PROMPT_FILE.get(backend)
|
|
55
|
-
if expected is None:
|
|
56
|
-
checks.append(
|
|
57
|
-
{
|
|
58
|
-
"id": "backend_consistency",
|
|
59
|
-
"passed": False,
|
|
60
|
-
"severity": "error",
|
|
61
|
-
"message": f"unknown backend '{backend}' in culture.yaml",
|
|
62
|
-
"remediation": f"set backend to one of: {', '.join(sorted(_PROMPT_FILE))}",
|
|
63
|
-
}
|
|
64
|
-
)
|
|
65
|
-
else:
|
|
66
|
-
present = (root / expected).is_file()
|
|
67
|
-
checks.append(
|
|
68
|
-
{
|
|
69
|
-
"id": "prompt_file_present",
|
|
70
|
-
"passed": present,
|
|
71
|
-
"severity": "error",
|
|
72
|
-
"message": (
|
|
73
|
-
f"backend '{backend}' requires {expected} — "
|
|
74
|
-
+ ("present" if present else "missing")
|
|
75
|
-
),
|
|
76
|
-
"remediation": "" if present else f"create {expected} at the repo root",
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
# 2. skills-present: the vendored skill kit is on disk.
|
|
81
|
-
skills_dir = root / ".claude" / "skills"
|
|
82
|
-
has_skills = skills_dir.is_dir() and any(skills_dir.iterdir())
|
|
83
|
-
checks.append(
|
|
84
|
-
{
|
|
85
|
-
"id": "skills_present",
|
|
86
|
-
"passed": has_skills,
|
|
87
|
-
"severity": "warning",
|
|
88
|
-
"message": (
|
|
89
|
-
".claude/skills/ vendored" if has_skills else ".claude/skills/ missing or empty"
|
|
90
|
-
),
|
|
91
|
-
"remediation": (
|
|
92
|
-
"" if has_skills else "vendor the skill kit (see docs/skill-sources.md)"
|
|
93
|
-
),
|
|
94
|
-
}
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
healthy = all(c["passed"] for c in checks)
|
|
98
|
-
return {"healthy": healthy, "checks": checks}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
102
|
-
report = _diagnose()
|
|
103
|
-
json_mode = bool(getattr(args, "json", False))
|
|
104
|
-
if json_mode:
|
|
105
|
-
emit_result(report, json_mode=True)
|
|
106
|
-
else:
|
|
107
|
-
status = "healthy" if report["healthy"] else "unhealthy"
|
|
108
|
-
lines = [f"qodo-cli doctor: {status}", ""]
|
|
109
|
-
for check in report["checks"]:
|
|
110
|
-
mark = "ok" if check["passed"] else "FAIL"
|
|
111
|
-
lines.append(f"[{mark}] {check['id']}: {check['message']}")
|
|
112
|
-
if not check["passed"] and check["remediation"]:
|
|
113
|
-
lines.append(f" hint: {check['remediation']}")
|
|
114
|
-
emit_result("\n".join(lines), json_mode=False)
|
|
115
|
-
return 0 if report["healthy"] else 1
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def register(sub: argparse._SubParsersAction) -> None:
|
|
119
|
-
p = sub.add_parser(
|
|
120
|
-
"doctor",
|
|
121
|
-
help="Check the agent-identity invariants (prompt-file-present, backend-consistency).",
|
|
122
|
-
)
|
|
123
|
-
add_json_flag(p)
|
|
124
|
-
p.set_defaults(func=cmd_doctor)
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"""Tests for the introspection verbs: overview, cli overview, doctor."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from qodo.cli import main
|
|
10
|
-
|
|
11
|
-
# --- overview -------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_overview_text(capsys: pytest.CaptureFixture[str]) -> None:
|
|
15
|
-
rc = main(["overview"])
|
|
16
|
-
assert rc == 0
|
|
17
|
-
out = capsys.readouterr().out
|
|
18
|
-
assert "# qodo-cli" in out
|
|
19
|
-
assert "Identity" in out
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_overview_json_shape(capsys: pytest.CaptureFixture[str]) -> None:
|
|
23
|
-
rc = main(["overview", "--json"])
|
|
24
|
-
assert rc == 0
|
|
25
|
-
payload = json.loads(capsys.readouterr().out)
|
|
26
|
-
assert payload["subject"] == "qodo-cli"
|
|
27
|
-
assert isinstance(payload["sections"], list)
|
|
28
|
-
assert payload["sections"]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_overview_graceful_on_bad_path(capsys: pytest.CaptureFixture[str]) -> None:
|
|
32
|
-
# Rubric contract: descriptive verbs never hard-fail on a missing target.
|
|
33
|
-
rc = main(["overview", "/no/such/path/here"])
|
|
34
|
-
assert rc == 0
|
|
35
|
-
assert capsys.readouterr().out.strip()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# --- cli overview ---------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_cli_overview_text(capsys: pytest.CaptureFixture[str]) -> None:
|
|
42
|
-
rc = main(["cli", "overview"])
|
|
43
|
-
assert rc == 0
|
|
44
|
-
assert "# qodo-cli cli" in capsys.readouterr().out
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_cli_overview_json_shape(capsys: pytest.CaptureFixture[str]) -> None:
|
|
48
|
-
rc = main(["cli", "overview", "--json"])
|
|
49
|
-
assert rc == 0
|
|
50
|
-
payload = json.loads(capsys.readouterr().out)
|
|
51
|
-
assert payload["subject"] == "qodo-cli cli"
|
|
52
|
-
assert isinstance(payload["sections"], list)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_cli_noun_bare_is_non_empty(capsys: pytest.CaptureFixture[str]) -> None:
|
|
56
|
-
rc = main(["cli"])
|
|
57
|
-
assert rc == 0
|
|
58
|
-
assert capsys.readouterr().out.strip()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_cli_overview_unknown_flag_structured_error(
|
|
62
|
-
capsys: pytest.CaptureFixture[str],
|
|
63
|
-
) -> None:
|
|
64
|
-
# `cli overview` parse errors must route through the structured error
|
|
65
|
-
# contract (error:/hint: + exit 1), not argparse's default stderr/exit 2.
|
|
66
|
-
with pytest.raises(SystemExit) as exc:
|
|
67
|
-
main(["cli", "overview", "--bogus"])
|
|
68
|
-
assert exc.value.code == 1
|
|
69
|
-
err = capsys.readouterr().err
|
|
70
|
-
assert err.startswith("error:")
|
|
71
|
-
assert "hint:" in err
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# --- doctor ---------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def test_doctor_text(capsys: pytest.CaptureFixture[str]) -> None:
|
|
78
|
-
rc = main(["doctor"])
|
|
79
|
-
assert rc in (0, 1)
|
|
80
|
-
assert "qodo-cli doctor" in capsys.readouterr().out
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_doctor_json_shape(capsys: pytest.CaptureFixture[str]) -> None:
|
|
84
|
-
rc = main(["doctor", "--json"])
|
|
85
|
-
assert rc in (0, 1)
|
|
86
|
-
payload = json.loads(capsys.readouterr().out)
|
|
87
|
-
assert isinstance(payload["healthy"], bool)
|
|
88
|
-
assert isinstance(payload["checks"], list)
|
|
89
|
-
assert payload["checks"]
|
|
90
|
-
for check in payload["checks"]:
|
|
91
|
-
assert {"id", "passed", "severity", "message", "remediation"} <= set(check)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_doctor_recognizes_declared_backend(capsys: pytest.CaptureFixture[str]) -> None:
|
|
95
|
-
"""The repo's own declared backend must be a known one — doctor stays healthy.
|
|
96
|
-
|
|
97
|
-
Guards the backend-consistency invariant: a promotion that changes
|
|
98
|
-
``culture.yaml``'s backend without teaching ``doctor`` the matching prompt
|
|
99
|
-
file would otherwise slip through (the shape tests above tolerate rc==1).
|
|
100
|
-
"""
|
|
101
|
-
rc = main(["doctor", "--json"])
|
|
102
|
-
payload = json.loads(capsys.readouterr().out)
|
|
103
|
-
messages = " ".join(str(c["message"]) for c in payload["checks"])
|
|
104
|
-
assert "unknown backend" not in messages
|
|
105
|
-
assert rc == 0
|
|
106
|
-
assert payload["healthy"] is True
|
|
File without changes
|
{qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md
RENAMED
|
File without changes
|
{qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|