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.
Files changed (85) hide show
  1. qodo_cli-0.5.0/.pr_agent.toml +26 -0
  2. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/CHANGELOG.md +27 -0
  3. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/PKG-INFO +1 -1
  4. qodo_cli-0.5.0/best_practices.md +59 -0
  5. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/pyproject.toml +1 -1
  6. qodo_cli-0.5.0/qodo/cli/_commands/doctor.py +262 -0
  7. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/learn.py +2 -2
  8. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/overview.py +1 -1
  9. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/explain/catalog.py +15 -3
  10. qodo_cli-0.5.0/tests/test_cli_introspection.py +197 -0
  11. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/uv.lock +1 -1
  12. qodo_cli-0.4.0/qodo/cli/_commands/doctor.py +0 -124
  13. qodo_cli-0.4.0/tests/test_cli_introspection.py +0 -106
  14. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/agent-config/SKILL.md +0 -0
  15. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  16. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
  17. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/SKILL.md +0 -0
  18. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  19. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  20. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  21. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
  22. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  23. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  24. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/SKILL.md +0 -0
  25. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  26. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  27. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  28. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  29. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  30. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/SKILL.md +0 -0
  31. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  32. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  33. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  34. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  35. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  36. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  37. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  38. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  39. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  40. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  41. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/run-tests/SKILL.md +0 -0
  42. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  43. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
  44. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  45. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  46. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  47. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/think/SKILL.md +0 -0
  48. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/think/scripts/think.sh +0 -0
  49. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/version-bump/SKILL.md +0 -0
  50. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
  51. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.claude/skills.local.yaml.example +0 -0
  52. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.devague/current +0 -0
  53. {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
  54. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.flake8 +0 -0
  55. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.github/workflows/publish.yml +0 -0
  56. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.github/workflows/tests.yml +0 -0
  57. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.gitignore +0 -0
  58. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/.markdownlint-cli2.yaml +0 -0
  59. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/AGENTS.colleague.md +0 -0
  60. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/CLAUDE.md +0 -0
  61. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/LICENSE +0 -0
  62. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/README.md +0 -0
  63. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/culture.yaml +0 -0
  64. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/docs/qodo-skills-sources.md +0 -0
  65. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/docs/skill-sources.md +0 -0
  66. {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
  67. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/__init__.py +0 -0
  68. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/__main__.py +0 -0
  69. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/__init__.py +0 -0
  70. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/__init__.py +0 -0
  71. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/cli.py +0 -0
  72. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/explain.py +0 -0
  73. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/review.py +0 -0
  74. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/rules.py +0 -0
  75. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_commands/whoami.py +0 -0
  76. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_errors.py +0 -0
  77. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_output.py +0 -0
  78. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_providers.py +0 -0
  79. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/cli/_qodo_api.py +0 -0
  80. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/qodo/explain/__init__.py +0 -0
  81. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/sonar-project.properties +0 -0
  82. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/tests/__init__.py +0 -0
  83. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/tests/test_cli.py +0 -0
  84. {qodo_cli-0.4.0 → qodo_cli-0.5.0}/tests/test_review.py +0 -0
  85. {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.4.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.4.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 the agent-identity invariants.
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 the agent-identity invariants."},
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 the agent-identity invariants",
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
- Checks the agent-identity invariants `steward doctor` verifies:
160
- prompt-file-present and backend-consistency (`claude` `CLAUDE.md`), plus a
161
- skills-present check. Exits 1 when unhealthy.
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
@@ -417,7 +417,7 @@ wheels = [
417
417
 
418
418
  [[package]]
419
419
  name = "qodo-cli"
420
- version = "0.4.0"
420
+ version = "0.5.0"
421
421
  source = { editable = "." }
422
422
 
423
423
  [package.dev-dependencies]
@@ -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
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