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.
Files changed (219) hide show
  1. {agex_cli-0.18.0 → agex_cli-0.20.0}/CHANGELOG.md +38 -0
  2. {agex_cli-0.18.0 → agex_cli-0.20.0}/PKG-INFO +1 -2
  3. {agex_cli-0.18.0 → agex_cli-0.20.0}/pyproject.toml +1 -2
  4. agex_cli-0.20.0/src/agent_experience/cli.py +443 -0
  5. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/pr_briefing.md.j2 +16 -0
  6. agex_cli-0.20.0/src/agent_experience/commands/pr/scripts/_qodo.py +147 -0
  7. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/await_.py +4 -1
  8. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/read.py +5 -1
  9. agex_cli-0.20.0/src/agent_experience/core/backend.py +80 -0
  10. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/github.py +1 -0
  11. agex_cli-0.20.0/tests/commands/pr/fixtures/gh/qodo_summary_comment.html +66 -0
  12. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_await.py +38 -51
  13. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_delta.py +20 -20
  14. agex_cli-0.20.0/tests/commands/pr/test_lint.py +44 -0
  15. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_open.py +36 -37
  16. agex_cli-0.20.0/tests/commands/pr/test_qodo.py +82 -0
  17. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_read.py +90 -34
  18. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_reply.py +42 -27
  19. agex_cli-0.20.0/tests/commands/test_doctor.py +181 -0
  20. agex_cli-0.20.0/tests/commands/test_explain.py +31 -0
  21. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/test_gamify.py +45 -45
  22. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/test_hook.py +33 -33
  23. agex_cli-0.20.0/tests/commands/test_learn.py +55 -0
  24. agex_cli-0.20.0/tests/commands/test_overview.py +56 -0
  25. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_backend.py +5 -1
  26. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_resolve_backend.py +24 -0
  27. agex_cli-0.20.0/tests/test_cli_dispatch.py +73 -0
  28. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/test_cli_errors.py +47 -41
  29. agex_cli-0.20.0/tests/test_cli_smoke.py +12 -0
  30. {agex_cli-0.18.0 → agex_cli-0.20.0}/uv.lock +1 -36
  31. agex_cli-0.18.0/src/agent_experience/cli.py +0 -361
  32. agex_cli-0.18.0/src/agent_experience/core/backend.py +0 -43
  33. agex_cli-0.18.0/tests/commands/pr/test_lint.py +0 -44
  34. agex_cli-0.18.0/tests/commands/test_doctor.py +0 -183
  35. agex_cli-0.18.0/tests/commands/test_explain.py +0 -33
  36. agex_cli-0.18.0/tests/commands/test_learn.py +0 -57
  37. agex_cli-0.18.0/tests/commands/test_overview.py +0 -55
  38. agex_cli-0.18.0/tests/test_cli_smoke.py +0 -11
  39. {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/SKILL.md +0 -0
  40. {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  41. {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  42. {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  43. {agex_cli-0.18.0 → agex_cli-0.20.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  44. {agex_cli-0.18.0 → agex_cli-0.20.0}/.flake8 +0 -0
  45. {agex_cli-0.18.0 → agex_cli-0.20.0}/.github/workflows/docs.yml +0 -0
  46. {agex_cli-0.18.0 → agex_cli-0.20.0}/.github/workflows/publish.yml +0 -0
  47. {agex_cli-0.18.0 → agex_cli-0.20.0}/.github/workflows/test.yml +0 -0
  48. {agex_cli-0.18.0 → agex_cli-0.20.0}/.gitignore +0 -0
  49. {agex_cli-0.18.0 → agex_cli-0.20.0}/.python-version +0 -0
  50. {agex_cli-0.18.0 → agex_cli-0.20.0}/CLAUDE.md +0 -0
  51. {agex_cli-0.18.0 → agex_cli-0.20.0}/LICENSE +0 -0
  52. {agex_cli-0.18.0 → agex_cli-0.20.0}/README.md +0 -0
  53. {agex_cli-0.18.0 → agex_cli-0.20.0}/culture.yaml +0 -0
  54. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/.gitignore +0 -0
  55. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/404.md +0 -0
  56. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/Gemfile +0 -0
  57. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_config.yml +0 -0
  58. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_includes/head_custom.html +0 -0
  59. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_sass/color_schemes/anthropic.scss +0 -0
  60. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_sass/color_schemes/dark-terminal.scss +0 -0
  61. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/_sass/custom/custom.scss +0 -0
  62. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/apple-touch-icon.png +0 -0
  63. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/favicon-16x16.png +0 -0
  64. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/favicon-32x32.png +0 -0
  65. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/favicon.ico +0 -0
  66. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/og-agex.png +0 -0
  67. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/assets/images/og-culture.png +0 -0
  68. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/explain.md +0 -0
  69. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/gamify.md +0 -0
  70. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/hook.md +0 -0
  71. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/index.md +0 -0
  72. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/learn.md +0 -0
  73. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/commands/overview.md +0 -0
  74. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/getting-started.md +0 -0
  75. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/index.md +0 -0
  76. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/skill-sources.md +0 -0
  77. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/plans/2026-04-18-agex-v0.1.md +0 -0
  78. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/plans/2026-05-10-agex-pr.md +0 -0
  79. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/specs/2026-04-18-agex-design.md +0 -0
  80. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/specs/2026-04-26-agex-doctor.md +0 -0
  81. {agex_cli-0.18.0 → agex_cli-0.20.0}/docs/superpowers/specs/2026-05-10-agex-pr-design.md +0 -0
  82. {agex_cli-0.18.0 → agex_cli-0.20.0}/scripts/sync_skill_md.py +0 -0
  83. {agex_cli-0.18.0 → agex_cli-0.20.0}/sonar-project.properties +0 -0
  84. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/__init__.py +0 -0
  85. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/__main__.py +0 -0
  86. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/__init__.py +0 -0
  87. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/acp/__init__.py +0 -0
  88. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/acp/probe.py +0 -0
  89. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/acp.yaml +0 -0
  90. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/claude-code.yaml +0 -0
  91. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/codex.yaml +0 -0
  92. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/capabilities/copilot.yaml +0 -0
  93. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/claude_code/__init__.py +0 -0
  94. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/claude_code/probe.py +0 -0
  95. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/codex/__init__.py +0 -0
  96. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/codex/probe.py +0 -0
  97. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/copilot/__init__.py +0 -0
  98. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/backends/copilot/probe.py +0 -0
  99. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/__init__.py +0 -0
  100. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/SKILL.md +0 -0
  101. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/__init__.py +0 -0
  102. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/assets/report.md.j2 +0 -0
  103. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/references/design.md +0 -0
  104. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/scripts/__init__.py +0 -0
  105. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/doctor/scripts/doctor.py +0 -0
  106. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/SKILL.md +0 -0
  107. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/__init__.py +0 -0
  108. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/assets/topics/agex.md +0 -0
  109. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/references/.gitkeep +0 -0
  110. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/scripts/__init__.py +0 -0
  111. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/explain/scripts/explain.py +0 -0
  112. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/SKILL.md +0 -0
  113. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/__init__.py +0 -0
  114. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/assets/hooks/claude-code.json +0 -0
  115. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/references/.gitkeep +0 -0
  116. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/scripts/__init__.py +0 -0
  117. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/gamify/scripts/install.py +0 -0
  118. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/SKILL.md +0 -0
  119. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/__init__.py +0 -0
  120. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/assets/table.md.j2 +0 -0
  121. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/references/.gitkeep +0 -0
  122. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/scripts/__init__.py +0 -0
  123. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/scripts/read.py +0 -0
  124. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/hook/scripts/write.py +0 -0
  125. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/SKILL.md +0 -0
  126. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/__init__.py +0 -0
  127. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/menu.md.j2 +0 -0
  128. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/cicd/SKILL.md +0 -0
  129. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/gamify/SKILL.md +0 -0
  130. {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
  131. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/introspect/SKILL.md +0 -0
  132. {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
  133. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/levelup/SKILL.md +0 -0
  134. {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
  135. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/assets/topics/visualize/SKILL.md +0 -0
  136. {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
  137. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/references/.gitkeep +0 -0
  138. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/scripts/__init__.py +0 -0
  139. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/learn/scripts/learn.py +0 -0
  140. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/SKILL.md +0 -0
  141. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/__init__.py +0 -0
  142. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/acp.yaml +0 -0
  143. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/claude-code.yaml +0 -0
  144. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/codex.yaml +0 -0
  145. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/backends/copilot.yaml +0 -0
  146. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/assets/sections.md.j2 +0 -0
  147. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/references/.gitkeep +0 -0
  148. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/scripts/__init__.py +0 -0
  149. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/overview/scripts/overview.py +0 -0
  150. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/SKILL.md +0 -0
  151. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/__init__.py +0 -0
  152. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/__init__.py +0 -0
  153. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/__init__.py +0 -0
  154. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/acp.yaml +0 -0
  155. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/claude-code.yaml +0 -0
  156. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/codex.yaml +0 -0
  157. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/backends/copilot.yaml +0 -0
  158. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/rules/__init__.py +0 -0
  159. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/rules/lint_rules.py +0 -0
  160. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/rules/next_step_rules.py +0 -0
  161. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/__init__.py +0 -0
  162. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/delta.md.j2 +0 -0
  163. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/footer.md.j2 +0 -0
  164. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/lint_result.md.j2 +0 -0
  165. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/pr_open_result.md.j2 +0 -0
  166. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/assets/templates/pr_reply_result.md.j2 +0 -0
  167. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/__init__.py +0 -0
  168. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_footer.py +0 -0
  169. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_journal.py +0 -0
  170. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_readiness.py +0 -0
  171. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/_sonar.py +0 -0
  172. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/delta.py +0 -0
  173. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/lint.py +0 -0
  174. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/open_.py +0 -0
  175. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/commands/pr/scripts/reply.py +0 -0
  176. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/__init__.py +0 -0
  177. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/capabilities.py +0 -0
  178. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/config.py +0 -0
  179. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/hook_io.py +0 -0
  180. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/journal.py +0 -0
  181. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/paths.py +0 -0
  182. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/render.py +0 -0
  183. {agex_cli-0.18.0 → agex_cli-0.20.0}/src/agent_experience/core/skill_loader.py +0 -0
  184. {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/.claude/settings.json +0 -0
  185. {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/CLAUDE.md +0 -0
  186. {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/README.md +0 -0
  187. {agex_cli-0.18.0 → agex_cli-0.20.0}/tester-agents/claude/culture.yaml +0 -0
  188. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/__init__.py +0 -0
  189. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/backends/__init__.py +0 -0
  190. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/backends/test_claude_code_probe.py +0 -0
  191. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/backends/test_stub_probes.py +0 -0
  192. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/__init__.py +0 -0
  193. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/__init__.py +0 -0
  194. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/gh/.gitkeep +0 -0
  195. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/gh/pr_checks_42.json +0 -0
  196. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/gh/pr_comments_42.json +0 -0
  197. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/fixtures/journals/dogfood_40.jsonl +0 -0
  198. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_footer.py +0 -0
  199. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/commands/pr/test_lint_rules.py +0 -0
  200. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/__init__.py +0 -0
  201. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_capabilities.py +0 -0
  202. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_config.py +0 -0
  203. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_github.py +0 -0
  204. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_hook_io.py +0 -0
  205. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_journal.py +0 -0
  206. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_paths.py +0 -0
  207. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_render.py +0 -0
  208. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_skill_loader.py +0 -0
  209. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/core/test_version_lookup.py +0 -0
  210. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/empty/.gitkeep +0 -0
  211. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/hooks.json +0 -0
  212. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/settings.json +0 -0
  213. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/skills/bad/SKILL.md +0 -0
  214. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/malformed/.claude/skills/broken-yaml/SKILL.md +0 -0
  215. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/.claude/hooks.json +0 -0
  216. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/.claude/settings.json +0 -0
  217. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/.claude/skills/example/SKILL.md +0 -0
  218. {agex_cli-0.18.0 → agex_cli-0.20.0}/tests/fixtures/claude-code/typical/CLAUDE.md +0 -0
  219. {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.18.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.18.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}