agex-cli 0.18.0__tar.gz → 0.20.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.
- {agex_cli-0.18.0 → agex_cli-0.20.0}/CHANGELOG.md +38 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/PKG-INFO +1 -2
- {agex_cli-0.18.0 → agex_cli-0.20.0}/pyproject.toml +1 -2
- agex_cli-0.20.0/src/agent_experience/cli.py +443 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/pr_briefing.md.j2 +16 -0
- agex_cli-0.20.0/src/agent_experience/commands/pr/scripts/_qodo.py +147 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/await_.py +4 -1
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/read.py +5 -1
- agex_cli-0.20.0/src/agent_experience/core/backend.py +80 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/github.py +1 -0
- agex_cli-0.20.0/tests/commands/pr/fixtures/gh/qodo_summary_comment.html +66 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_await.py +38 -51
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_delta.py +20 -20
- agex_cli-0.20.0/tests/commands/pr/test_lint.py +44 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_open.py +36 -37
- agex_cli-0.20.0/tests/commands/pr/test_qodo.py +82 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_read.py +90 -34
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_reply.py +42 -27
- agex_cli-0.20.0/tests/commands/test_doctor.py +181 -0
- agex_cli-0.20.0/tests/commands/test_explain.py +31 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/test_gamify.py +45 -45
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/test_hook.py +33 -33
- agex_cli-0.20.0/tests/commands/test_learn.py +55 -0
- agex_cli-0.20.0/tests/commands/test_overview.py +56 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_backend.py +5 -1
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_resolve_backend.py +24 -0
- agex_cli-0.20.0/tests/test_cli_dispatch.py +73 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/test_cli_errors.py +47 -41
- agex_cli-0.20.0/tests/test_cli_smoke.py +12 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/uv.lock +1 -36
- agex_cli-0.18.0/src/agent_experience/cli.py +0 -361
- agex_cli-0.18.0/src/agent_experience/core/backend.py +0 -43
- agex_cli-0.18.0/tests/commands/pr/test_lint.py +0 -44
- agex_cli-0.18.0/tests/commands/test_doctor.py +0 -183
- agex_cli-0.18.0/tests/commands/test_explain.py +0 -33
- agex_cli-0.18.0/tests/commands/test_learn.py +0 -57
- agex_cli-0.18.0/tests/commands/test_overview.py +0 -55
- agex_cli-0.18.0/tests/test_cli_smoke.py +0 -11
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.flake8 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.github/workflows/docs.yml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.github/workflows/publish.yml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.github/workflows/test.yml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.gitignore +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/.python-version +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/CLAUDE.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/LICENSE +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/README.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/culture.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/.gitignore +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/404.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/Gemfile +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_config.yml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_includes/head_custom.html +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_sass/color_schemes/anthropic.scss +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_sass/color_schemes/dark-terminal.scss +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_sass/custom/custom.scss +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/apple-touch-icon.png +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/favicon-16x16.png +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/favicon-32x32.png +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/favicon.ico +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/og-agex.png +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/og-culture.png +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/explain.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/gamify.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/hook.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/index.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/learn.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/overview.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/getting-started.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/index.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/skill-sources.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/plans/2026-04-18-agex-v0.1.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/plans/2026-05-10-agex-pr.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/specs/2026-04-18-agex-design.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/specs/2026-04-26-agex-doctor.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/specs/2026-05-10-agex-pr-design.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/scripts/sync_skill_md.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/sonar-project.properties +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/__main__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/acp/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/acp/probe.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/acp.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/claude-code.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/codex.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/copilot.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/claude_code/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/claude_code/probe.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/codex/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/codex/probe.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/copilot/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/copilot/probe.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/assets/report.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/references/design.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/scripts/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/scripts/doctor.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/assets/topics/agex.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/references/.gitkeep +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/scripts/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/scripts/explain.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/assets/hooks/claude-code.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/references/.gitkeep +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/scripts/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/scripts/install.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/assets/table.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/references/.gitkeep +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/scripts/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/scripts/read.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/scripts/write.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/menu.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/cicd/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/gamify/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/introspect/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/levelup/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/visualize/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/references/.gitkeep +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/scripts/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/scripts/learn.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/acp.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/claude-code.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/codex.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/copilot.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/sections.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/references/.gitkeep +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/scripts/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/scripts/overview.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/acp.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/claude-code.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/codex.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/copilot.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/rules/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/rules/lint_rules.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/rules/next_step_rules.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/delta.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/footer.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/lint_result.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/pr_open_result.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/pr_reply_result.md.j2 +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_footer.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_journal.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_readiness.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_sonar.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/delta.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/lint.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/open_.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/reply.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/capabilities.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/config.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/hook_io.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/journal.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/paths.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/render.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/skill_loader.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/.claude/settings.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/CLAUDE.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/README.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/culture.yaml +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/backends/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/backends/test_claude_code_probe.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/backends/test_stub_probes.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/gh/.gitkeep +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/gh/pr_checks_42.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/gh/pr_comments_42.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/journals/dogfood_40.jsonl +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_footer.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_lint_rules.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/__init__.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_capabilities.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_config.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_github.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_hook_io.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_journal.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_paths.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_render.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_skill_loader.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_version_lookup.py +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/empty/.gitkeep +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/hooks.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/settings.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/skills/bad/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/skills/broken-yaml/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/.claude/hooks.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/.claude/settings.json +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/.claude/skills/example/SKILL.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/CLAUDE.md +0 -0
- {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/test_skill_md_consistency.py +0 -0
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.20.0] - 2026-05-23
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Dropped the `typer` runtime dependency.** The CLI is now built on the
|
|
15
|
+
Python standard library's `argparse`, mirroring the sibling Culture repos
|
|
16
|
+
(`steward`, `devague`). This removes `typer`, `rich`, `shellingham`,
|
|
17
|
+
`annotated-doc`, `markdown-it-py`, `mdurl`, and `pygments` from the shipped
|
|
18
|
+
runtime closure (the wheel now depends only on `jinja2`, `pyyaml`,
|
|
19
|
+
`tomlkit`, and `portalocker`), shrinking the dependency-chain attack
|
|
20
|
+
surface. CLI behaviour is unchanged: every command, flag, exit code, and
|
|
21
|
+
stderr message is preserved, including the `agex explain agex`
|
|
22
|
+
unknown-command routing and the bare `--version` output.
|
|
23
|
+
|
|
24
|
+
## [0.19.0] - 2026-05-23
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `agex pr read` (and `pr await`) now surface **Qodo code-review
|
|
29
|
+
findings** that Qodo posts inside collapsed `<details>` blocks of a
|
|
30
|
+
single top-level comment. A new `## Qodo review` section lists the
|
|
31
|
+
headline counts (🐞 Bugs / 📘 Rule violations / 📎 Requirement gaps)
|
|
32
|
+
and each finding's title + `file:line` + link, mirroring how inline
|
|
33
|
+
threads are shown. When counts are non-zero but no per-finding detail
|
|
34
|
+
could be parsed, the briefing flags it (`⚠️ N finding(s) in collapsed
|
|
35
|
+
Qodo review block — expand on GitHub`) so a bug is never silently
|
|
36
|
+
missed. Closes [#47](https://github.com/agentculture/agex-cli/issues/47).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Backend resolution accepts `claude` as an alias of `claude-code`, so
|
|
41
|
+
the AgentCulture-standard `culture.yaml` shape (`backend: claude`)
|
|
42
|
+
works with `agex pr` out of the box. When a `culture.yaml` backend is
|
|
43
|
+
genuinely unknown, the error now names the source, the offending agent
|
|
44
|
+
`suffix`, and the fix (e.g. `culture.yaml agent 'devague' has unknown
|
|
45
|
+
backend 'foo' / hint: expected one of claude (= claude-code), codex,
|
|
46
|
+
copilot, acp`). Closes [#46](https://github.com/agentculture/agex-cli/issues/46).
|
|
47
|
+
|
|
10
48
|
## [0.18.0] - 2026-05-12
|
|
11
49
|
|
|
12
50
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agex-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Agent-operated developer-experience CLI — deterministic per-backend markdown briefings for autonomous agents.
|
|
5
5
|
Project-URL: Homepage, https://culture.dev/agex/
|
|
6
6
|
Project-URL: Repository, https://github.com/agentculture/agex-cli
|
|
@@ -13,7 +13,6 @@ Requires-Dist: jinja2>=3.1
|
|
|
13
13
|
Requires-Dist: portalocker>=2.8
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: tomlkit>=0.12
|
|
16
|
-
Requires-Dist: typer>=0.12
|
|
17
16
|
Provides-Extra: dev
|
|
18
17
|
Requires-Dist: bandit>=1.7; extra == 'dev'
|
|
19
18
|
Requires-Dist: black>=24.0; extra == 'dev'
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agex-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.20.0"
|
|
4
4
|
description = "Agent-operated developer-experience CLI — deterministic per-backend markdown briefings for autonomous agents."
|
|
5
5
|
authors = [{name = "Ori Nachum"}]
|
|
6
6
|
license = {text = "MIT"}
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"typer>=0.12",
|
|
11
10
|
"jinja2>=3.1",
|
|
12
11
|
"pyyaml>=6.0",
|
|
13
12
|
"tomlkit>=0.12",
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""agex CLI — stdlib argparse front end.
|
|
2
|
+
|
|
3
|
+
No third-party CLI framework: this module routes `agex <command> [args]`
|
|
4
|
+
through `argparse` only, mirroring the skeleton used by the sibling Culture
|
|
5
|
+
repos (steward, devague). Business logic stays in `commands/<name>/scripts/`,
|
|
6
|
+
which return ``(stdout, exit_code, stderr)`` tuples; this module just parses
|
|
7
|
+
arguments and echoes those tuples. Adding a backend or command never touches
|
|
8
|
+
the parsing core beyond a `register_*` call.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from agent_experience import __version__
|
|
17
|
+
from agent_experience.commands.doctor.scripts import doctor as doctor_script
|
|
18
|
+
from agent_experience.commands.explain.scripts import explain as explain_script
|
|
19
|
+
from agent_experience.commands.gamify.scripts import install as gamify_script
|
|
20
|
+
from agent_experience.commands.hook.scripts import read as hook_read_script
|
|
21
|
+
from agent_experience.commands.hook.scripts import write as hook_write_script
|
|
22
|
+
from agent_experience.commands.learn.scripts import learn as learn_script
|
|
23
|
+
from agent_experience.commands.overview.scripts import overview as overview_script
|
|
24
|
+
from agent_experience.commands.pr.scripts import await_ as pr_await_script
|
|
25
|
+
from agent_experience.commands.pr.scripts import delta as pr_delta_script
|
|
26
|
+
from agent_experience.commands.pr.scripts import lint as pr_lint_script
|
|
27
|
+
from agent_experience.commands.pr.scripts import open_ as pr_open_script
|
|
28
|
+
from agent_experience.commands.pr.scripts import read as pr_read_script
|
|
29
|
+
from agent_experience.commands.pr.scripts import reply as pr_reply_script
|
|
30
|
+
from agent_experience.core.backend import parse_backend
|
|
31
|
+
|
|
32
|
+
_GH_RERUN_HINT = "agex: rerun once network is reachable (gh failed)"
|
|
33
|
+
_AGENT_HELP = "Backend: claude-code, codex, copilot, or acp."
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _AgexArgumentParser(argparse.ArgumentParser):
|
|
37
|
+
"""ArgumentParser used everywhere via ``parser_class=``.
|
|
38
|
+
|
|
39
|
+
argparse's native ``error()`` already prints usage to stderr and exits
|
|
40
|
+
with code 2 — which matches agex's existing bad-argument behavior — so no
|
|
41
|
+
override is required. The subclass exists only so nested subparsers inherit
|
|
42
|
+
it and to give a single place for any future tweak.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Output helpers — preserve the exact newline behavior of the old typer.echo
|
|
48
|
+
# calls. ``typer.echo(x, nl=False)`` wrote x verbatim; ``typer.echo(x)`` added
|
|
49
|
+
# a newline; ``err=True`` selected stderr.
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _emit(stdout: str, stderr: str, *, stderr_newline: bool = True) -> None:
|
|
54
|
+
"""Write a command's stdout/stderr the way the old CLI did."""
|
|
55
|
+
if stdout:
|
|
56
|
+
sys.stdout.write(stdout)
|
|
57
|
+
if stderr:
|
|
58
|
+
if stderr_newline:
|
|
59
|
+
print(stderr, file=sys.stderr)
|
|
60
|
+
else:
|
|
61
|
+
sys.stderr.write(stderr)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_backend_or_report(agent: Optional[str]):
|
|
65
|
+
"""Parse ``--agent`` into a Backend.
|
|
66
|
+
|
|
67
|
+
Returns ``(backend, None)`` on success, or ``(None, 2)`` after printing the
|
|
68
|
+
canonical ``agex: error: <msg>`` to stderr — letting the caller ``return``
|
|
69
|
+
the exit code without exception gymnastics.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return parse_backend(agent), None
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
print(f"agex: error: {exc}", file=sys.stderr)
|
|
75
|
+
return None, 2
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Top-level command handlers
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cmd_explain(args: argparse.Namespace) -> int:
|
|
84
|
+
stdout, exit_code, stderr = explain_script.run(args.topic)
|
|
85
|
+
_emit(stdout, stderr)
|
|
86
|
+
return exit_code
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _cmd_doctor(args: argparse.Namespace) -> int:
|
|
90
|
+
stdout, exit_code, stderr = doctor_script.run(args.role)
|
|
91
|
+
_emit(stdout, stderr)
|
|
92
|
+
return exit_code
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cmd_learn(args: argparse.Namespace) -> int:
|
|
96
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
97
|
+
if err is not None:
|
|
98
|
+
return err
|
|
99
|
+
if args.topic is None:
|
|
100
|
+
stdout, exit_code, stderr = learn_script.run_menu(backend)
|
|
101
|
+
else:
|
|
102
|
+
stdout, exit_code, stderr = learn_script.run_topic(args.topic, backend)
|
|
103
|
+
_emit(stdout, stderr)
|
|
104
|
+
return exit_code
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _cmd_gamify(args: argparse.Namespace) -> int:
|
|
108
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
109
|
+
if err is not None:
|
|
110
|
+
return err
|
|
111
|
+
if args.uninstall:
|
|
112
|
+
stdout, exit_code, stderr = gamify_script.uninstall(backend)
|
|
113
|
+
else:
|
|
114
|
+
stdout, exit_code, stderr = gamify_script.install(backend)
|
|
115
|
+
_emit(stdout, stderr)
|
|
116
|
+
return exit_code
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _cmd_overview(args: argparse.Namespace) -> int:
|
|
120
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
121
|
+
if err is not None:
|
|
122
|
+
return err
|
|
123
|
+
stdout, exit_code, stderr = overview_script.run(backend)
|
|
124
|
+
_emit(stdout, stderr)
|
|
125
|
+
return exit_code
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# hook subcommands
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cmd_hook_write(args: argparse.Namespace) -> int:
|
|
134
|
+
_, exit_code, stderr = hook_write_script.run(args.event, args.args or [])
|
|
135
|
+
_emit("", stderr)
|
|
136
|
+
return exit_code
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _cmd_hook_read(args: argparse.Namespace) -> int:
|
|
140
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
141
|
+
if err is not None:
|
|
142
|
+
return err
|
|
143
|
+
stdout, exit_code, stderr = hook_read_script.run(backend)
|
|
144
|
+
_emit(stdout, stderr)
|
|
145
|
+
return exit_code
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# pr subcommands
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cmd_pr_lint(args: argparse.Namespace) -> int:
|
|
154
|
+
try:
|
|
155
|
+
stdout, exit_code, stderr = pr_lint_script.run(
|
|
156
|
+
agent=args.agent, project_dir=Path.cwd(), exit_on_violation=args.exit_on_violation
|
|
157
|
+
)
|
|
158
|
+
except ValueError as exc:
|
|
159
|
+
print(f"agex: {exc}", file=sys.stderr)
|
|
160
|
+
return 2
|
|
161
|
+
_emit(stdout, stderr)
|
|
162
|
+
return exit_code
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _cmd_pr_open(args: argparse.Namespace) -> int:
|
|
166
|
+
try:
|
|
167
|
+
stdout, exit_code, stderr = pr_open_script.run(
|
|
168
|
+
agent=args.agent,
|
|
169
|
+
project_dir=Path.cwd(),
|
|
170
|
+
title=args.title,
|
|
171
|
+
body_file=args.body_file,
|
|
172
|
+
draft=args.draft,
|
|
173
|
+
delayed_read=args.delayed_read,
|
|
174
|
+
)
|
|
175
|
+
except ValueError as exc:
|
|
176
|
+
print(f"agex: {exc}", file=sys.stderr)
|
|
177
|
+
return 2
|
|
178
|
+
except RuntimeError as exc:
|
|
179
|
+
print(str(exc), file=sys.stderr)
|
|
180
|
+
print("agex: rerun 'agex pr open ...' once network is reachable", file=sys.stderr)
|
|
181
|
+
return 1
|
|
182
|
+
_emit(stdout, stderr)
|
|
183
|
+
return exit_code
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _cmd_pr_reply(args: argparse.Namespace) -> int:
|
|
187
|
+
try:
|
|
188
|
+
stdout, exit_code, stderr = pr_reply_script.run(
|
|
189
|
+
agent=args.agent, project_dir=Path.cwd(), pr=args.pr
|
|
190
|
+
)
|
|
191
|
+
except ValueError as exc:
|
|
192
|
+
print(f"agex: {exc}", file=sys.stderr)
|
|
193
|
+
return 2
|
|
194
|
+
except RuntimeError as exc:
|
|
195
|
+
print(str(exc), file=sys.stderr)
|
|
196
|
+
print(_GH_RERUN_HINT, file=sys.stderr)
|
|
197
|
+
return 1
|
|
198
|
+
_emit(stdout, stderr, stderr_newline=False)
|
|
199
|
+
return exit_code
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _cmd_pr_read(args: argparse.Namespace) -> int:
|
|
203
|
+
try:
|
|
204
|
+
stdout, exit_code, stderr = pr_read_script.run(
|
|
205
|
+
agent=args.agent, project_dir=Path.cwd(), pr=args.pr, wait=args.wait
|
|
206
|
+
)
|
|
207
|
+
except ValueError as exc:
|
|
208
|
+
print(f"agex: {exc}", file=sys.stderr)
|
|
209
|
+
return 2
|
|
210
|
+
except RuntimeError as exc:
|
|
211
|
+
print(str(exc), file=sys.stderr)
|
|
212
|
+
print(_GH_RERUN_HINT, file=sys.stderr)
|
|
213
|
+
return 1
|
|
214
|
+
_emit(stdout, stderr)
|
|
215
|
+
return exit_code
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _cmd_pr_await(args: argparse.Namespace) -> int:
|
|
219
|
+
try:
|
|
220
|
+
stdout, exit_code, stderr = pr_await_script.run(
|
|
221
|
+
agent=args.agent, project_dir=Path.cwd(), pr=args.pr, max_wait=args.max_wait
|
|
222
|
+
)
|
|
223
|
+
except ValueError as exc:
|
|
224
|
+
print(f"agex: {exc}", file=sys.stderr)
|
|
225
|
+
return 2
|
|
226
|
+
except RuntimeError as exc:
|
|
227
|
+
print(str(exc), file=sys.stderr)
|
|
228
|
+
print(_GH_RERUN_HINT, file=sys.stderr)
|
|
229
|
+
return 1
|
|
230
|
+
_emit(stdout, stderr)
|
|
231
|
+
return exit_code
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _cmd_pr_delta(args: argparse.Namespace) -> int:
|
|
235
|
+
try:
|
|
236
|
+
stdout, exit_code, stderr = pr_delta_script.run(agent=args.agent, project_dir=Path.cwd())
|
|
237
|
+
except ValueError as exc:
|
|
238
|
+
print(f"agex: {exc}", file=sys.stderr)
|
|
239
|
+
return 2
|
|
240
|
+
except RuntimeError as exc:
|
|
241
|
+
print(str(exc), file=sys.stderr)
|
|
242
|
+
print(_GH_RERUN_HINT, file=sys.stderr)
|
|
243
|
+
return 1
|
|
244
|
+
_emit(stdout, stderr, stderr_newline=False)
|
|
245
|
+
return exit_code
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# Parser construction
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _add_agent_option(parser: argparse.ArgumentParser, *, required: bool, help_text: str) -> None:
|
|
254
|
+
parser.add_argument("--agent", required=required, default=None, help=help_text)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _group_help(parser: argparse.ArgumentParser):
|
|
258
|
+
"""Return a handler that prints a group's help to stderr and exits 2.
|
|
259
|
+
|
|
260
|
+
Mirrors Typer's ``no_args_is_help`` for ``agex hook`` / ``agex pr`` invoked
|
|
261
|
+
with no subcommand.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def _handle(_args: argparse.Namespace) -> int:
|
|
265
|
+
parser.print_help(sys.stderr)
|
|
266
|
+
return 2
|
|
267
|
+
|
|
268
|
+
return _handle
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
272
|
+
parser = _AgexArgumentParser(
|
|
273
|
+
prog="agex",
|
|
274
|
+
description="Agent-operated developer-experience CLI.",
|
|
275
|
+
)
|
|
276
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
277
|
+
sub = parser.add_subparsers(dest="command", parser_class=_AgexArgumentParser)
|
|
278
|
+
|
|
279
|
+
# explain
|
|
280
|
+
p_explain = sub.add_parser("explain", help="Describe a command or concept.")
|
|
281
|
+
p_explain.add_argument("topic", help="Topic to explain.")
|
|
282
|
+
p_explain.set_defaults(func=_cmd_explain)
|
|
283
|
+
|
|
284
|
+
# doctor
|
|
285
|
+
p_doctor = sub.add_parser("doctor", help="Diagnose the project's agex setup.")
|
|
286
|
+
p_doctor.add_argument(
|
|
287
|
+
"--role", default=None, help="Render a role-specific check section (e.g., pr-review)."
|
|
288
|
+
)
|
|
289
|
+
p_doctor.set_defaults(func=_cmd_doctor)
|
|
290
|
+
|
|
291
|
+
# learn
|
|
292
|
+
p_learn = sub.add_parser("learn", help="Teach a lesson topic (or show the menu).")
|
|
293
|
+
p_learn.add_argument("topic", nargs="?", default=None, help="Lesson topic (omit for menu).")
|
|
294
|
+
_add_agent_option(p_learn, required=True, help_text=_AGENT_HELP)
|
|
295
|
+
p_learn.set_defaults(func=_cmd_learn)
|
|
296
|
+
|
|
297
|
+
# gamify
|
|
298
|
+
p_gamify = sub.add_parser("gamify", help="Install (or uninstall) gamification.")
|
|
299
|
+
_add_agent_option(p_gamify, required=True, help_text=_AGENT_HELP)
|
|
300
|
+
p_gamify.add_argument("--uninstall", action="store_true", help="Reverse gamify.")
|
|
301
|
+
p_gamify.set_defaults(func=_cmd_gamify)
|
|
302
|
+
|
|
303
|
+
# overview
|
|
304
|
+
p_overview = sub.add_parser("overview", help="Render the per-backend overview briefing.")
|
|
305
|
+
_add_agent_option(p_overview, required=True, help_text=_AGENT_HELP)
|
|
306
|
+
p_overview.set_defaults(func=_cmd_overview)
|
|
307
|
+
|
|
308
|
+
_register_hook(sub)
|
|
309
|
+
_register_pr(sub)
|
|
310
|
+
|
|
311
|
+
return parser
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _register_hook(sub: argparse._SubParsersAction) -> None:
|
|
315
|
+
hook_p = sub.add_parser("hook", help="Write and read agex tracking events.")
|
|
316
|
+
hook_sub = hook_p.add_subparsers(dest="hook_command", parser_class=_AgexArgumentParser)
|
|
317
|
+
|
|
318
|
+
p_write = hook_sub.add_parser("write", help="Append a tracking event.")
|
|
319
|
+
p_write.add_argument("event", help="Event name (e.g., post-tool-use).")
|
|
320
|
+
p_write.add_argument("args", nargs="*", help="Additional key=value pairs.")
|
|
321
|
+
p_write.set_defaults(func=_cmd_hook_write)
|
|
322
|
+
|
|
323
|
+
p_read = hook_sub.add_parser("read", help="Render tracked events for a backend.")
|
|
324
|
+
_add_agent_option(p_read, required=True, help_text=_AGENT_HELP)
|
|
325
|
+
p_read.set_defaults(func=_cmd_hook_read)
|
|
326
|
+
|
|
327
|
+
hook_p.set_defaults(func=_group_help(hook_p))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _register_pr(sub: argparse._SubParsersAction) -> None:
|
|
331
|
+
pr_p = sub.add_parser("pr", help="GitHub PR lifecycle commands.")
|
|
332
|
+
pr_sub = pr_p.add_subparsers(dest="pr_command", parser_class=_AgexArgumentParser)
|
|
333
|
+
|
|
334
|
+
p_lint = pr_sub.add_parser("lint", help="Lint the PR branch state.")
|
|
335
|
+
_add_agent_option(
|
|
336
|
+
p_lint,
|
|
337
|
+
required=False,
|
|
338
|
+
help_text="Backend (claude-code|codex|copilot|acp); falls back to culture.yaml.",
|
|
339
|
+
)
|
|
340
|
+
p_lint.add_argument(
|
|
341
|
+
"--exit-on-violation",
|
|
342
|
+
action="store_true",
|
|
343
|
+
help="Exit 1 when violations are found (CI mode).",
|
|
344
|
+
)
|
|
345
|
+
p_lint.set_defaults(func=_cmd_pr_lint)
|
|
346
|
+
|
|
347
|
+
p_open = pr_sub.add_parser("open", help="Open a PR.")
|
|
348
|
+
p_open.add_argument("--title", required=True)
|
|
349
|
+
p_open.add_argument("--body-file", type=Path, default=None)
|
|
350
|
+
p_open.add_argument("--draft", action="store_true", default=False)
|
|
351
|
+
_add_agent_option(p_open, required=False, help_text=_AGENT_HELP)
|
|
352
|
+
p_open.add_argument(
|
|
353
|
+
"--delayed-read",
|
|
354
|
+
action="store_true",
|
|
355
|
+
default=False,
|
|
356
|
+
help="After create, immediately run `pr read --wait 180`.",
|
|
357
|
+
)
|
|
358
|
+
p_open.set_defaults(func=_cmd_pr_open)
|
|
359
|
+
|
|
360
|
+
p_reply = pr_sub.add_parser("reply", help="Reply to PR review threads.")
|
|
361
|
+
p_reply.add_argument("pr", type=int)
|
|
362
|
+
_add_agent_option(p_reply, required=False, help_text=_AGENT_HELP)
|
|
363
|
+
p_reply.set_defaults(func=_cmd_pr_reply)
|
|
364
|
+
|
|
365
|
+
p_read = pr_sub.add_parser("read", help="Read PR review state.")
|
|
366
|
+
p_read.add_argument("pr", type=int, nargs="?", default=None)
|
|
367
|
+
p_read.add_argument(
|
|
368
|
+
"--wait", type=int, default=None, help="Poll for readiness up to SECS seconds."
|
|
369
|
+
)
|
|
370
|
+
_add_agent_option(p_read, required=False, help_text=_AGENT_HELP)
|
|
371
|
+
p_read.set_defaults(func=_cmd_pr_read)
|
|
372
|
+
|
|
373
|
+
p_await = pr_sub.add_parser(
|
|
374
|
+
"await",
|
|
375
|
+
help="Wake-me-when-triage-able combo verb.",
|
|
376
|
+
description=(
|
|
377
|
+
"Polls readiness, runs CI + Sonar gate, renders briefing. Exits "
|
|
378
|
+
"non-zero on quality-gate ERROR or unresolved review threads."
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
p_await.add_argument("pr", type=int, nargs="?", default=None)
|
|
382
|
+
p_await.add_argument(
|
|
383
|
+
"--max-wait",
|
|
384
|
+
type=int,
|
|
385
|
+
default=1800,
|
|
386
|
+
help="Poll for readiness up to SECS seconds (default 1800).",
|
|
387
|
+
)
|
|
388
|
+
_add_agent_option(p_await, required=False, help_text=_AGENT_HELP)
|
|
389
|
+
p_await.set_defaults(func=_cmd_pr_await)
|
|
390
|
+
|
|
391
|
+
p_delta = pr_sub.add_parser("delta", help="Show the delta since the last PR read.")
|
|
392
|
+
_add_agent_option(p_delta, required=False, help_text=_AGENT_HELP)
|
|
393
|
+
p_delta.set_defaults(func=_cmd_pr_delta)
|
|
394
|
+
|
|
395
|
+
pr_p.set_defaults(func=_group_help(pr_p))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ---------------------------------------------------------------------------
|
|
399
|
+
# Dispatch + entrypoint
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
404
|
+
rc = args.func(args)
|
|
405
|
+
return rc if rc is not None else 0
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
409
|
+
parser = _build_parser()
|
|
410
|
+
args = parser.parse_args(argv)
|
|
411
|
+
if getattr(args, "func", None) is None:
|
|
412
|
+
# No top-level command given — mirror Typer's no_args_is_help (exit 2).
|
|
413
|
+
parser.print_help(sys.stderr)
|
|
414
|
+
return 2
|
|
415
|
+
return _dispatch(args)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# Keep in sync with the sub.add_parser registrations above.
|
|
419
|
+
# If a new top-level command is added, extend this set so _main_entrypoint
|
|
420
|
+
# stops routing it to the unknown-command fallback page.
|
|
421
|
+
_KNOWN_COMMANDS = {"explain", "overview", "learn", "gamify", "hook", "doctor", "pr"}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _main_entrypoint() -> None:
|
|
425
|
+
"""CLI entry point that routes unknown subcommands to ``agex explain agex``.
|
|
426
|
+
|
|
427
|
+
When the first positional argument is not a known command (and is not a
|
|
428
|
+
flag), print the ``agex explain agex`` page to stdout and the canonical
|
|
429
|
+
error message to stderr, then exit with code 2. All other invocations —
|
|
430
|
+
known commands, ``--version``, ``--help``, zero-arg help — fall through to
|
|
431
|
+
the normal ``main()`` dispatch unchanged.
|
|
432
|
+
"""
|
|
433
|
+
argv = sys.argv[1:]
|
|
434
|
+
if argv and not argv[0].startswith("-") and argv[0] not in _KNOWN_COMMANDS:
|
|
435
|
+
print(f"agex: error: unknown command '{argv[0]}'", file=sys.stderr)
|
|
436
|
+
stdout, _, _ = explain_script.run("agex")
|
|
437
|
+
sys.stdout.write(stdout)
|
|
438
|
+
sys.exit(2)
|
|
439
|
+
sys.exit(main())
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
if __name__ == "__main__":
|
|
443
|
+
_main_entrypoint()
|
|
@@ -42,6 +42,22 @@ _No comments yet._
|
|
|
42
42
|
{% endfor %}
|
|
43
43
|
{%- endif %}
|
|
44
44
|
|
|
45
|
+
## Qodo review
|
|
46
|
+
|
|
47
|
+
{% if not qodo -%}
|
|
48
|
+
_No Qodo review found._
|
|
49
|
+
{%- else -%}
|
|
50
|
+
🐞 Bugs: **{{ qodo.counts.bugs }}** · 📘 Rule violations: **{{ qodo.counts.rule_violations }}** · 📎 Requirement gaps: **{{ qodo.counts.requirement_gaps }}**
|
|
51
|
+
{% if qodo.findings -%}
|
|
52
|
+
{% for f in qodo.findings -%}
|
|
53
|
+
- {{ f.title or "(untitled finding)" }}{% if f.path %} — `{{ f.path }}{% if f.line %}:{{ f.line }}{% endif %}`{% endif %}{% if f.link %} ([link]({{ f.link }})){% endif %}
|
|
54
|
+
{% endfor %}
|
|
55
|
+
{%- endif %}
|
|
56
|
+
{% if qodo.total > 0 and not qodo.findings -%}
|
|
57
|
+
> ⚠️ {{ qodo.total }} finding(s) in collapsed Qodo review block — expand on GitHub{% if qodo.url %} {{ qodo.url }}{% endif %}
|
|
58
|
+
{%- endif %}
|
|
59
|
+
{%- endif %}
|
|
60
|
+
|
|
45
61
|
## Readiness
|
|
46
62
|
|
|
47
63
|
{% if waiting_for -%}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Parse Qodo (``qodo-code-review[bot]``) "Code Review by Qodo" comments.
|
|
2
|
+
|
|
3
|
+
Qodo posts its code review as a single top-level issue comment whose body is
|
|
4
|
+
HTML containing collapsed ``<details>`` blocks — one per finding, each itself
|
|
5
|
+
holding nested ``<details>`` for Description / Code / Evidence / Agent prompt.
|
|
6
|
+
``agex pr read`` would otherwise render this comment truncated to 200 chars,
|
|
7
|
+
hiding the findings (and any real bug) entirely.
|
|
8
|
+
|
|
9
|
+
This module extracts the headline counts and, where possible, each finding's
|
|
10
|
+
title + ``file:line`` + permalink so the briefing can surface them the way it
|
|
11
|
+
surfaces inline review threads. It is deliberately defensive: malformed or
|
|
12
|
+
partial HTML yields whatever is parseable and the public ``parse`` never raises.
|
|
13
|
+
|
|
14
|
+
Stdlib only (``re`` + ``html``) — the project ships no HTML-parsing dependency.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import html
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
_QODO_MARKER = "Code Review by Qodo"
|
|
24
|
+
|
|
25
|
+
# Headline counts: <code>🐞 Bugs (3)</code> <code>📘 Rule violations (1)</code>
|
|
26
|
+
# <code>📎 Requirement gaps (0)</code>. Emoji-agnostic; matches label + integer.
|
|
27
|
+
_COUNT_RE = re.compile(
|
|
28
|
+
r"<code>[^<]*?(Bugs|Rule violations|Requirement gaps)\s*\((\d+)\)",
|
|
29
|
+
re.IGNORECASE,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_COUNT_KEYS = {
|
|
33
|
+
"bugs": "bugs",
|
|
34
|
+
"rule violations": "rule_violations",
|
|
35
|
+
"requirement gaps": "requirement_gaps",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Each finding is introduced by a <summary> whose (tag-stripped) text starts
|
|
39
|
+
# with an ordinal, e.g. "1. Orphan honesty rendered ...". Nested Description /
|
|
40
|
+
# Code / Evidence / Agent-prompt summaries do not start with a digit, so they
|
|
41
|
+
# are not mistaken for findings — which sidesteps the nested-<details> problem.
|
|
42
|
+
_SUMMARY_RE = re.compile(r"<summary\b[^>]*>(.*?)</summary>", re.IGNORECASE | re.DOTALL)
|
|
43
|
+
_FINDING_TITLE_RE = re.compile(r"^\d+[.)]\s")
|
|
44
|
+
|
|
45
|
+
# Qodo's "Code" location, a markdown link with a bracketed line range:
|
|
46
|
+
# [devague/render/spec_md.py[R44-49]](https://github.com/.../files#diff-…R44-R49)
|
|
47
|
+
_CODE_LINK_RE = re.compile(
|
|
48
|
+
r"\[([^\[\]\n]+?)\[([RLrl]?\d+(?:-[RLrl]?\d+)?)\]\]\((https?://[^)\s]+)\)"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Fallback: a bare GitHub blob permalink (used in the Evidence section):
|
|
52
|
+
# https://github.com/owner/repo/blob/<sha>/<path>#L42-L55 (trailing / tolerated)
|
|
53
|
+
_PERMALINK_RE = re.compile(
|
|
54
|
+
r"https://github\.com/\S+?/blob/[0-9a-fA-F]+/"
|
|
55
|
+
r"(?P<path>[^#\s\"'<>)\]]+?)/?"
|
|
56
|
+
r"(?:#L(?P<start>\d+)(?:-L(?P<end>\d+))?)?(?=[\s\"'<>)\]]|$)"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
_TAG_RE = re.compile(r"<[^>]+>")
|
|
60
|
+
_WS_RE = re.compile(r"\s+")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _strip_tags(fragment: str) -> str:
|
|
64
|
+
text = _TAG_RE.sub(" ", fragment)
|
|
65
|
+
text = html.unescape(text)
|
|
66
|
+
return _WS_RE.sub(" ", text).strip()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _find_qodo_comment(comments: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
70
|
+
"""Return the "Code Review by Qodo" summary comment, or None."""
|
|
71
|
+
for c in comments:
|
|
72
|
+
if _QODO_MARKER in (c.get("body") or ""):
|
|
73
|
+
return c
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_counts(body: str) -> dict[str, int]:
|
|
78
|
+
counts = {"bugs": 0, "rule_violations": 0, "requirement_gaps": 0}
|
|
79
|
+
for label, n in _COUNT_RE.findall(body):
|
|
80
|
+
key = _COUNT_KEYS.get(label.lower())
|
|
81
|
+
if key:
|
|
82
|
+
counts[key] = int(n)
|
|
83
|
+
return counts
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _extract_location(span: str) -> tuple[str | None, str | None, str | None]:
|
|
87
|
+
"""Return (path, line, link) for the first location in a finding span."""
|
|
88
|
+
m = _CODE_LINK_RE.search(span)
|
|
89
|
+
if m:
|
|
90
|
+
path = m.group(1).strip()
|
|
91
|
+
line = re.sub(r"[RLrl]", "", m.group(2))
|
|
92
|
+
return path, line, m.group(3)
|
|
93
|
+
m = _PERMALINK_RE.search(span)
|
|
94
|
+
if m:
|
|
95
|
+
path = m.group("path").rstrip("/")
|
|
96
|
+
start, end = m.group("start"), m.group("end")
|
|
97
|
+
line = None
|
|
98
|
+
if start:
|
|
99
|
+
line = f"{start}-{end}" if end else start
|
|
100
|
+
return path, line, m.group(0)
|
|
101
|
+
return None, None, None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_findings(body: str) -> list[dict[str, Any]]:
|
|
105
|
+
summaries = list(_SUMMARY_RE.finditer(body))
|
|
106
|
+
title_idxs = [
|
|
107
|
+
i for i, m in enumerate(summaries) if _FINDING_TITLE_RE.match(_strip_tags(m.group(1)))
|
|
108
|
+
]
|
|
109
|
+
findings: list[dict[str, Any]] = []
|
|
110
|
+
for n, idx in enumerate(title_idxs):
|
|
111
|
+
# Title = the summary text up to the first <code> status/type badge.
|
|
112
|
+
head = summaries[idx].group(1).split("<code>")[0]
|
|
113
|
+
title = _strip_tags(head)
|
|
114
|
+
span_start = summaries[idx].end()
|
|
115
|
+
span_end = summaries[title_idxs[n + 1]].start() if n + 1 < len(title_idxs) else len(body)
|
|
116
|
+
path, line, link = _extract_location(body[span_start:span_end])
|
|
117
|
+
if not title and not path:
|
|
118
|
+
continue
|
|
119
|
+
findings.append({"title": title, "path": path, "line": line, "link": link})
|
|
120
|
+
return findings
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def parse(comments: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
124
|
+
"""Return ``{counts, total, findings, url}`` for the Qodo summary comment,
|
|
125
|
+
or ``None`` *only* when no such comment is present.
|
|
126
|
+
|
|
127
|
+
Once a Qodo comment is found this always returns a dict (with safe
|
|
128
|
+
defaults), so a parsing hiccup degrades to "0 findings" / the collapsed
|
|
129
|
+
warning rather than the misleading ``_No Qodo review found._``. ``counts``
|
|
130
|
+
and ``findings`` are parsed independently so a partial failure still
|
|
131
|
+
surfaces whatever was recoverable. Never raises.
|
|
132
|
+
"""
|
|
133
|
+
comment = _find_qodo_comment(comments)
|
|
134
|
+
if comment is None:
|
|
135
|
+
return None
|
|
136
|
+
body = comment.get("body") or ""
|
|
137
|
+
try:
|
|
138
|
+
counts = parse_counts(body)
|
|
139
|
+
except Exception: # pragma: no cover - defensive
|
|
140
|
+
counts = {"bugs": 0, "rule_violations": 0, "requirement_gaps": 0}
|
|
141
|
+
try:
|
|
142
|
+
findings = parse_findings(body)
|
|
143
|
+
except Exception: # pragma: no cover - defensive
|
|
144
|
+
findings = []
|
|
145
|
+
total = counts["bugs"] + counts["rule_violations"] + counts["requirement_gaps"]
|
|
146
|
+
url = comment.get("html_url") or next((f["link"] for f in findings if f.get("link")), None)
|
|
147
|
+
return {"counts": counts, "total": total, "findings": findings, "url": url}
|