wade-cli 0.29.6__tar.gz → 0.30.2__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 (274) hide show
  1. {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/auto-version.yml +24 -2
  2. {wade_cli-0.29.6 → wade_cli-0.30.2}/CHANGELOG.md +36 -0
  3. {wade_cli-0.29.6 → wade_cli-0.30.2}/KNOWLEDGE.md +16 -0
  4. {wade_cli-0.29.6 → wade_cli-0.30.2}/KNOWLEDGE.ratings.yml +6 -3
  5. {wade_cli-0.29.6 → wade_cli-0.30.2}/PKG-INFO +1 -1
  6. {wade_cli-0.29.6 → wade_cli-0.30.2}/pyproject.toml +1 -1
  7. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/__init__.py +1 -1
  8. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/knowledge.py +9 -6
  9. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/review_pr_comments_session.py +6 -0
  10. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/stash.py +0 -36
  11. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/markdown.py +6 -4
  12. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/core.py +20 -7
  13. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/init_service.py +23 -1
  14. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/knowledge_service.py +15 -7
  15. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/install.py +5 -0
  16. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/update_check.py +48 -21
  17. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/knowledge/SKILL.md +9 -6
  18. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_knowledge_cli.py +72 -0
  19. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_markdown.py +12 -0
  20. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_init.py +107 -0
  21. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_service.py +96 -1
  22. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_update_check.py +117 -0
  23. {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/ci.yml +0 -0
  24. {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/pr-title-lint.yml +0 -0
  25. {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/publish.yml +0 -0
  26. {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/release.yml +0 -0
  27. {wade_cli-0.29.6 → wade_cli-0.30.2}/.gitignore +0 -0
  28. {wade_cli-0.29.6 → wade_cli-0.30.2}/.pre-commit-config.yaml +0 -0
  29. {wade_cli-0.29.6 → wade_cli-0.30.2}/.wade.yml +0 -0
  30. {wade_cli-0.29.6 → wade_cli-0.30.2}/AGENTS.md +0 -0
  31. {wade_cli-0.29.6 → wade_cli-0.30.2}/CONTRIBUTING.md +0 -0
  32. {wade_cli-0.29.6 → wade_cli-0.30.2}/README.md +0 -0
  33. {wade_cli-0.29.6 → wade_cli-0.30.2}/assets/wade.png +0 -0
  34. {wade_cli-0.29.6 → wade_cli-0.30.2}/docker-compose.e2e.yml +0 -0
  35. {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/architecture.md +0 -0
  36. {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/documentation-policies.md +0 -0
  37. {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/extending.md +0 -0
  38. {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/skills-system.md +0 -0
  39. {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/testing.md +0 -0
  40. {wade_cli-0.29.6 → wade_cli-0.30.2}/install.sh +0 -0
  41. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/auto_version.py +0 -0
  42. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/changelog.py +0 -0
  43. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/check-all.sh +0 -0
  44. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/check.sh +0 -0
  45. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/fmt.sh +0 -0
  46. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/hooks/pre-push +0 -0
  47. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/install-hooks.sh +0 -0
  48. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/release.sh +0 -0
  49. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/setup-worktree.sh +0 -0
  50. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-e2e-docker.sh +0 -0
  51. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-e2e.sh +0 -0
  52. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-live-ai-taskr.sh +0 -0
  53. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-live-ai.sh +0 -0
  54. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-live-gh.sh +0 -0
  55. {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test.sh +0 -0
  56. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/__main__.py +0 -0
  57. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/__init__.py +0 -0
  58. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/admin.py +0 -0
  59. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/autocomplete.py +0 -0
  60. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/implementation_session.py +0 -0
  61. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/main.py +0 -0
  62. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/plan_session.py +0 -0
  63. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/review.py +0 -0
  64. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/session_shared.py +0 -0
  65. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/task.py +0 -0
  66. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/worktree.py +0 -0
  67. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/__init__.py +0 -0
  68. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/loader.py +0 -0
  69. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/migrations.py +0 -0
  70. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/schema.py +0 -0
  71. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/__init__.py +0 -0
  72. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/engine.py +0 -0
  73. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/repositories.py +0 -0
  74. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/tables.py +0 -0
  75. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/__init__.py +0 -0
  76. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/branch.py +0 -0
  77. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/pr.py +0 -0
  78. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/repo.py +0 -0
  79. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/sync.py +0 -0
  80. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/worktree.py +0 -0
  81. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/hooks/__init__.py +0 -0
  82. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/hooks/plan_write_guard.py +0 -0
  83. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/hooks/worktree_guard.py +0 -0
  84. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/logging/__init__.py +0 -0
  85. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/logging/context.py +0 -0
  86. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/logging/setup.py +0 -0
  87. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/__init__.py +0 -0
  88. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/batch.py +0 -0
  89. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/config.py +0 -0
  90. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/delegation.py +0 -0
  91. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/deps.py +0 -0
  92. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/events.py +0 -0
  93. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/review.py +0 -0
  94. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/session.py +0 -0
  95. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/task.py +0 -0
  96. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/__init__.py +0 -0
  97. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/_pr_delegate.py +0 -0
  98. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/base.py +0 -0
  99. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/clickup.py +0 -0
  100. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/github.py +0 -0
  101. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/registry.py +0 -0
  102. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/__init__.py +0 -0
  103. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/ai_resolution.py +0 -0
  104. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/batch_review_service.py +0 -0
  105. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/check_service.py +0 -0
  106. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/delegation_service.py +0 -0
  107. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/deps_service.py +0 -0
  108. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/__init__.py +0 -0
  109. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/batch.py +0 -0
  110. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/bootstrap.py +0 -0
  111. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/usage_tracking.py +0 -0
  112. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/knowledge_search.py +0 -0
  113. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/plan_service.py +0 -0
  114. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/prompt_delivery.py +0 -0
  115. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/review_delegation_service.py +0 -0
  116. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/review_service.py +0 -0
  117. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/review_settle.py +0 -0
  118. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/smart_start.py +0 -0
  119. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/task_service.py +0 -0
  120. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/skills/__init__.py +0 -0
  121. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/skills/installer.py +0 -0
  122. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/skills/pointer.py +0 -0
  123. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/ui/__init__.py +0 -0
  124. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/ui/console.py +0 -0
  125. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/ui/prompts.py +0 -0
  126. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/__init__.py +0 -0
  127. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/clipboard.py +0 -0
  128. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/http.py +0 -0
  129. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/markdown.py +0 -0
  130. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/process.py +0 -0
  131. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/slug.py +0 -0
  132. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/terminal.py +0 -0
  133. {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/token_usage_markdown.py +0 -0
  134. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/agents-pointer.md +0 -0
  135. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/deps-analysis.md +0 -0
  136. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/implement-context.md +0 -0
  137. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/plan-session.md +0 -0
  138. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-batch.md +0 -0
  139. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-code.md +0 -0
  140. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-plan.md +0 -0
  141. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-pr-comments.md +0 -0
  142. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/setup-worktree.sh.example +0 -0
  143. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/review-enforcement-rule.md +0 -0
  144. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/review-implementation-closing-step.md +0 -0
  145. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/review-plan-step.md +0 -0
  146. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/user-interaction.md +0 -0
  147. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/deps/SKILL.md +0 -0
  148. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/implementation-session/SKILL.md +0 -0
  149. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/plan-session/SKILL.md +0 -0
  150. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/review-pr-comments-session/SKILL.md +0 -0
  151. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/task/SKILL.md +0 -0
  152. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/task/examples.md +0 -0
  153. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/task/plan-format.md +0 -0
  154. {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/statusline-command.sh +0 -0
  155. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/__init__.py +0 -0
  156. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/conftest.py +0 -0
  157. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/__init__.py +0 -0
  158. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/_support.py +0 -0
  159. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/conftest.py +0 -0
  160. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/mock_gh_script.py +0 -0
  161. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_admin_contract.py +0 -0
  162. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_check_contract.py +0 -0
  163. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_implement_work_done_contract.py +0 -0
  164. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_knowledge_contract.py +0 -0
  165. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_mock_gh_contract.py +0 -0
  166. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_plan_contract.py +0 -0
  167. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_review_contract.py +0 -0
  168. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_smart_start_contract.py +0 -0
  169. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_task_contract.py +0 -0
  170. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_work_sync_list_contract.py +0 -0
  171. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_hi_body.md +0 -0
  172. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_howdy_body.md +0 -0
  173. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/claude_session.txt +0 -0
  174. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/codex_session.txt +0 -0
  175. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/copilot_session.txt +0 -0
  176. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/gemini_session.txt +0 -0
  177. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/generic_session.txt +0 -0
  178. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/__init__.py +0 -0
  179. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_autostash.py +0 -0
  180. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_check.py +0 -0
  181. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_git.py +0 -0
  182. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_implementation_lifecycle.py +0 -0
  183. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_init.py +0 -0
  184. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_knowledge.py +0 -0
  185. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_markdown_provider.py +0 -0
  186. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_skill_install.py +0 -0
  187. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/__init__.py +0 -0
  188. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/test_wade_live_ai.py +0 -0
  189. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/test_wade_live_ai_taskr.py +0 -0
  190. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/test_wade_live_gh.py +0 -0
  191. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/test_cli_basics.py +0 -0
  192. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/__init__.py +0 -0
  193. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/conftest.py +0 -0
  194. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/__init__.py +0 -0
  195. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_done_review_hint.py +0 -0
  196. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_review_cli.py +0 -0
  197. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_session_subapps.py +0 -0
  198. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_shell_init.py +0 -0
  199. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_task_cli.py +0 -0
  200. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_ai_flags.py +0 -0
  201. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_cli.py +0 -0
  202. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/__init__.py +0 -0
  203. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/test_loader.py +0 -0
  204. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations.py +0 -0
  205. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations_atomic.py +0 -0
  206. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_console.py +0 -0
  207. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_db/__init__.py +0 -0
  208. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_db/test_repository_transactions.py +0 -0
  209. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_effort.py +0 -0
  210. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/__init__.py +0 -0
  211. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_branch_slugify.py +0 -0
  212. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_detached_worktree.py +0 -0
  213. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_pr_base.py +0 -0
  214. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_repo.py +0 -0
  215. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_scaffold_commit.py +0 -0
  216. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_sync_conflicted_files.py +0 -0
  217. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_worktree_remove.py +0 -0
  218. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_hooks/__init__.py +0 -0
  219. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_hooks/test_plan_write_guard.py +0 -0
  220. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_hooks/test_worktree_guard.py +0 -0
  221. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/__init__.py +0 -0
  222. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_batch.py +0 -0
  223. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_config_models.py +0 -0
  224. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_db.py +0 -0
  225. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_delegation.py +0 -0
  226. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_models.py +0 -0
  227. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_partition.py +0 -0
  228. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_review.py +0 -0
  229. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_task_models.py +0 -0
  230. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/__init__.py +0 -0
  231. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_clickup.py +0 -0
  232. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_github.py +0 -0
  233. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_github_pr.py +0 -0
  234. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/__init__.py +0 -0
  235. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_ai_resolution.py +0 -0
  236. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_batch_review_service.py +0 -0
  237. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_allowlist.py +0 -0
  238. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_hook_timeout.py +0 -0
  239. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_catchup.py +0 -0
  240. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_check.py +0 -0
  241. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_delegation_service.py +0 -0
  242. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_deps_service.py +0 -0
  243. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_done_plan_flag.py +0 -0
  244. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_done_via_direct_cleanup.py +0 -0
  245. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_done_sync.py +0 -0
  246. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service.py +0 -0
  247. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service_resume.py +0 -0
  248. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_init_shell_integration.py +0 -0
  249. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_search.py +0 -0
  250. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_plan_service.py +0 -0
  251. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_poll_for_reviews.py +0 -0
  252. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_post_implementation_lifecycle.py +0 -0
  253. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_pr_summary_path.py +0 -0
  254. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_preserve_session_data.py +0 -0
  255. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_prompt_delivery.py +0 -0
  256. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_review_delegation_service.py +0 -0
  257. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_review_service.py +0 -0
  258. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start.py +0 -0
  259. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start_resume.py +0 -0
  260. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_sync_base_branch.py +0 -0
  261. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_task_service.py +0 -0
  262. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_tracked_managed_files.py +0 -0
  263. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_worktree_gitignore.py +0 -0
  264. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_skills/test_managed_gitignore.py +0 -0
  265. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/__init__.py +0 -0
  266. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_http.py +0 -0
  267. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_install.py +0 -0
  268. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_markdown.py +0 -0
  269. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_process.py +0 -0
  270. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_prompts.py +0 -0
  271. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_slug.py +0 -0
  272. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal.py +0 -0
  273. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal_launch.py +0 -0
  274. {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_yolo.py +0 -0
@@ -8,6 +8,10 @@ on:
8
8
  permissions:
9
9
  contents: write
10
10
 
11
+ concurrency:
12
+ group: auto-version-bump
13
+ cancel-in-progress: false
14
+
11
15
  jobs:
12
16
  version-bump:
13
17
  # Only run when the PR was actually merged
@@ -38,9 +42,18 @@ jobs:
38
42
  if: steps.detect.outputs.bump == ''
39
43
  run: echo "No conventional commit prefix detected — skipping version bump."
40
44
 
45
+ - name: Generate app token
46
+ if: steps.detect.outputs.bump != ''
47
+ id: app-token
48
+ uses: actions/create-github-app-token@v1
49
+ with:
50
+ app-id: ${{ secrets.RELEASE_APP_ID }}
51
+ private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
52
+
41
53
  - uses: actions/checkout@v4
42
54
  if: steps.detect.outputs.bump != ''
43
55
  with:
56
+ token: ${{ steps.app-token.outputs.token }}
44
57
  fetch-depth: 0
45
58
 
46
59
  - uses: astral-sh/setup-uv@v3
@@ -69,5 +82,14 @@ jobs:
69
82
  - name: Push commit and tag
70
83
  if: steps.detect.outputs.bump != ''
71
84
  run: |
72
- git push
73
- git push --tags
85
+ # Pull any commits that landed since checkout (e.g. from a
86
+ # concurrent version-bump that finished first), then retry push.
87
+ for attempt in 1 2 3; do
88
+ git pull --rebase origin main && git push && git push --tags && exit 0
89
+ if [ "$attempt" -lt 3 ]; then
90
+ echo "Push attempt $attempt failed, retrying in $((attempt * 2))s..."
91
+ sleep $((attempt * 2))
92
+ fi
93
+ done
94
+ echo "All push attempts failed"
95
+ exit 1
@@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Conventional Commits](https://conventionalcommits.org/).
6
6
 
7
+ ## [v0.30.2] — 2026-07-16
8
+
9
+ ### Bug Fixes
10
+
11
+ - prevent infinite self-upgrade re-exec loop in `wade update` (#322) (1e7afc7)
12
+
13
+ ## [v0.30.1] — 2026-07-16
14
+
15
+ ### CI/CD
16
+
17
+ - use GitHub App token for auto-version push (#319) (7045ef8)
18
+
19
+ ## [v0.30.0] — 2026-07-16
20
+
21
+ ### Features
22
+
23
+ - add `stale` rating to knowledge entries (56f6f44)
24
+
25
+ ## [v0.29.9] — 2026-07-16
26
+
27
+ ### Bug Fixes
28
+
29
+ - prevent auto-version workflow race condition on concurrent PR merges (2657e8c)
30
+
31
+ ## [v0.29.8] — 2026-07-16
32
+
33
+ ### Bug Fixes
34
+
35
+ - accept 'in-progress' as a synonym for 'in_progress' in Markdown task state parsing (6d03639)
36
+
37
+ ## [v0.29.7] — 2026-07-16
38
+
39
+ ### Refactoring
40
+
41
+ - refine auto-stash sync — skip redundant fetch, extend --no-stash to review session, drop dead stash helpers (45b5f49)
42
+
7
43
  ## [v0.29.6] — 2026-07-16
8
44
 
9
45
  ### Build
@@ -113,3 +113,19 @@ and keep `bot_status` as a secondary input only.
113
113
  When a service does 'from datetime import UTC, datetime' and calls datetime.now(UTC), patch the module-level class reference (e.g. wade.services.review_service.datetime). Set mock_datetime.now.return_value = fixed_now. Avoid naming local variables 'now' as datetime when the same function later uses 'now = time.time()' (float) — mypy strict mode flags the type mismatch even across mutually-exclusive code paths (use a distinct name like 'settle_now').
114
114
 
115
115
  ---
116
+
117
+ ## 4863036c | 2026-07-16 | plan | tags: refactoring, testing, mocking, architecture
118
+
119
+ When splitting a wade service module into submodules, moving a function to a new module breaks tests that patch its dependencies: the suite patches fully-qualified paths heavily (e.g. mock.patch("wade.services.implementation_service.core.git_sync"), ~200+ such sites), and mock.patch binds where a name is looked up. Repoint every moved function's patch targets to its new submodule. Also note: the implementation_service package re-exports via "from .sub import *", which does NOT re-export underscore-prefixed helpers — re-export those explicitly in __init__.py.
120
+
121
+ ---
122
+
123
+ ## a24d65b0 | 2026-07-16 | plan | tags: update, self-upgrade, gotcha
124
+
125
+ `wade update` self-upgrade can infinitely re-exec. Three defects combine:
126
+ (1) `utils/install._run_upgrade` treats package-manager exit code 0 as a successful upgrade — but `uv tool upgrade wade-cli` exits 0 for no-ops too (already up to date, or the target version is yanked/unavailable), so a no-op is reported as success;
127
+ (2) `services/init_service._maybe_self_upgrade` calls `re_exec()` with no loop guard — after re-exec it re-runs the same check with no memory it just tried and no check that the version actually advanced;
128
+ (3) `utils/update_check.check_for_update` trusts the 2-hour nag cache at `~/.local/share/wade/update-check.json`. A stale/yanked `latest_version` (observed: cache said 0.30.1 while 0.29.6 was installed and was repo HEAD) makes every re-exec take the identical upgrade path → infinite "wade upgraded — restarting..." loop for the whole cache TTL.
129
+ Fix: add a re-exec breadcrumb env var (WADE_SELF_UPGRADE_FROM=<old version>) so a post-re-exec process detects the version did NOT advance and stops with a warning; force a cache-bypassing fresh PyPI check for the explicit self-upgrade path; and skip yanked releases when selecting the latest version.
130
+
131
+ ---
@@ -4,15 +4,18 @@
4
4
  1c18f1d4:
5
5
  down: 0
6
6
  up: 1
7
+ 2b4d5319:
8
+ down: 0
9
+ up: 2
7
10
  3463fbd4:
8
11
  down: 0
9
- up: 1
12
+ up: 2
10
13
  54326c98:
11
14
  down: 1
12
15
  up: 0
13
16
  b61e247e:
14
17
  down: 0
15
- up: 2
18
+ up: 3
16
19
  b6a3a637:
17
20
  down: 0
18
21
  up: 1
@@ -24,4 +27,4 @@ cedeaad0:
24
27
  up: 1
25
28
  e2a49ac1:
26
29
  down: 0
27
- up: 2
30
+ up: 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wade-cli
3
- Version: 0.29.6
3
+ Version: 0.30.2
4
4
  Summary: WADE — Workflow for AI-Driven Engineering
5
5
  Project-URL: Homepage, https://github.com/ivanviragine/wade
6
6
  Project-URL: Repository, https://github.com/ivanviragine/wade
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wade-cli"
7
- version = "0.29.6"
7
+ version = "0.30.2"
8
8
  description = "WADE — Workflow for AI-Driven Engineering"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """WADE (Workflow for AI-Driven Engineering) — AI-agent-driven git workflow management CLI."""
2
2
 
3
- __version__ = "0.29.6"
3
+ __version__ = "0.30.2"
@@ -155,9 +155,9 @@ def get(
155
155
  @knowledge_app.command()
156
156
  def rate(
157
157
  entry_id: str = typer.Argument(help="Entry ID to rate."),
158
- direction: str = typer.Argument(help="Rating direction: up or down."),
158
+ direction: str = typer.Argument(help="Rating direction: up, down, or stale."),
159
159
  ) -> None:
160
- """Rate a knowledge entry (thumbs up or down)."""
160
+ """Rate a knowledge entry (up, down, or stale)."""
161
161
  from pathlib import Path
162
162
 
163
163
  from wade.config.loader import load_config
@@ -169,8 +169,8 @@ def rate(
169
169
  )
170
170
  from wade.ui.console import console
171
171
 
172
- if direction not in ("up", "down"):
173
- console.error(f"Invalid direction '{direction}'. Must be 'up' or 'down'.")
172
+ if direction not in ("up", "down", "stale"):
173
+ console.error(f"Invalid direction '{direction}'. Must be 'up', 'down', or 'stale'.")
174
174
  raise typer.Exit(1)
175
175
 
176
176
  config = load_config()
@@ -198,8 +198,11 @@ def rate(
198
198
  except OSError as exc:
199
199
  console.error(str(exc))
200
200
  raise typer.Exit(1) from exc
201
- symbol = "+" if direction == "up" else "-"
202
- console.success(f"Recorded {symbol}1 for entry {entry_id}")
201
+ if direction == "stale":
202
+ console.success(f"Recorded stale vote for entry {entry_id}")
203
+ else:
204
+ symbol = "+" if direction == "up" else "-"
205
+ console.success(f"Recorded {symbol}1 for entry {entry_id}")
203
206
 
204
207
 
205
208
  @knowledge_app.command()
@@ -32,6 +32,11 @@ def sync(
32
32
  main_branch: str | None = typer.Option(
33
33
  None, "--main-branch", help="Override main branch name."
34
34
  ),
35
+ no_stash: bool = typer.Option(
36
+ False,
37
+ "--no-stash",
38
+ help="Disable auto-stash: fail immediately on any uncommitted changes.",
39
+ ),
35
40
  ) -> None:
36
41
  """Sync current branch with main."""
37
42
  from wade.cli.session_shared import handle_sync_result
@@ -42,6 +47,7 @@ def sync(
42
47
  main_branch=main_branch,
43
48
  json_output=json_output,
44
49
  session_type="review-pr-comments",
50
+ no_stash=no_stash,
45
51
  )
46
52
  handle_sync_result(
47
53
  result,
@@ -7,7 +7,6 @@ from datetime import UTC, datetime
7
7
  from pathlib import Path
8
8
 
9
9
  import structlog
10
- from pydantic import BaseModel
11
10
 
12
11
  from wade.git.repo import GitError, _run_git
13
12
 
@@ -16,13 +15,6 @@ log = structlog.get_logger(__name__)
16
15
  AUTOSTASH_PREFIX = "wade-autostash"
17
16
 
18
17
 
19
- class AutoStashEntry(BaseModel):
20
- """A single wade autostash entry from the git stash list."""
21
-
22
- ref: str
23
- message: str
24
-
25
-
26
18
  def _stash_message(session_type: str, branch: str) -> str:
27
19
  ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
28
20
  safe_branch = branch.replace("/", "_")
@@ -82,34 +74,6 @@ def pop_stash(stash_ref: str, cwd: Path) -> subprocess.CompletedProcess[str]:
82
74
  return _run_git("stash", "pop", stash_ref, cwd=cwd, check=False)
83
75
 
84
76
 
85
- def apply_stash(stash_ref: str, cwd: Path) -> subprocess.CompletedProcess[str]:
86
- """Apply *stash_ref* without removing it. Returns CompletedProcess."""
87
- return _run_git("stash", "apply", stash_ref, cwd=cwd, check=False)
88
-
89
-
90
- def drop_stash(stash_ref: str, cwd: Path) -> subprocess.CompletedProcess[str]:
91
- """Remove *stash_ref* from the stash list. Returns CompletedProcess."""
92
- return _run_git("stash", "drop", stash_ref, cwd=cwd, check=False)
93
-
94
-
95
- def list_wade_autostashes(cwd: Path) -> list[AutoStashEntry]:
96
- """Return all wade autostash entries."""
97
- result = _run_git("stash", "list", "--format=%gd %gs", cwd=cwd, check=False)
98
- if result.returncode != 0:
99
- return []
100
- entries: list[AutoStashEntry] = []
101
- for line in result.stdout.splitlines():
102
- if not line.strip():
103
- continue
104
- parts = line.split(" ", 1)
105
- if len(parts) < 2:
106
- continue
107
- ref, subject = parts[0], parts[1]
108
- if AUTOSTASH_PREFIX in subject:
109
- entries.append(AutoStashEntry(ref=ref, message=subject))
110
- return entries
111
-
112
-
113
77
  def detect_untracked_collisions(cwd: Path, merge_ref: str) -> list[str]:
114
78
  """Return untracked file paths that would be overwritten by merging *merge_ref*.
115
79
 
@@ -230,7 +230,9 @@ def _format_section(section: _Section) -> str:
230
230
 
231
231
  def _section_to_task(section: _Section) -> Task:
232
232
  """Convert a parsed _Section to a Task model."""
233
- state_str = (section.meta.get("state") or "open").lower()
233
+ # Accept ``in-progress`` (dash) as a synonym for ``in_progress`` so a
234
+ # hand-edited file doesn't silently downgrade to OPEN.
235
+ state_str = (section.meta.get("state") or "open").lower().replace("-", "_")
234
236
  state = TaskState(state_str) if state_str in _VALID_STATES else TaskState.OPEN
235
237
 
236
238
  label_names = _split_labels(section.meta.get("labels", ""))
@@ -580,11 +582,11 @@ class MarkdownIssueProvider(GitHubPRDelegateMixin, AbstractTaskProvider):
580
582
  rejection, signing failure) is logged at WARNING and swallowed —
581
583
  the close itself already succeeded on disk.
582
584
  """
583
- repo_root = self._path.parent
585
+ cwd = self._path.parent
584
586
  try:
585
587
  add = run(
586
588
  ["git", "add", str(self._path)],
587
- cwd=repo_root,
589
+ cwd=cwd,
588
590
  check=False,
589
591
  )
590
592
  if add.returncode != 0:
@@ -596,7 +598,7 @@ class MarkdownIssueProvider(GitHubPRDelegateMixin, AbstractTaskProvider):
596
598
  return
597
599
  commit = run(
598
600
  ["git", "commit", "-m", f"chore: close #{task_id}"],
599
- cwd=repo_root,
601
+ cwd=cwd,
600
602
  check=False,
601
603
  )
602
604
  if commit.returncode != 0:
@@ -1726,6 +1726,7 @@ def _merge_base(
1726
1726
  json_output: bool = False,
1727
1727
  abort_on_conflict: bool = False,
1728
1728
  session_type: str = "implementation",
1729
+ already_fetched: bool = False,
1729
1730
  ) -> SyncResult:
1730
1731
  """Fetch, count commits behind, and merge base branch into current branch.
1731
1732
 
@@ -1744,6 +1745,9 @@ def _merge_base(
1744
1745
  path) so the worktree stays clean. When False (sync path), leave
1745
1746
  the merge in progress for the AI to resolve manually.
1746
1747
  session_type: Used in the conflict hint message.
1748
+ already_fetched: Skip the fetch step — the caller already fetched
1749
+ origin (e.g. for the autostash collision probe) and the local
1750
+ ``origin/<main>`` ref is fresh enough for the merge.
1747
1751
 
1748
1752
  Returns:
1749
1753
  SyncResult with success/conflicts/commits_merged (events=[]).
@@ -1756,14 +1760,17 @@ def _merge_base(
1756
1760
  has_remote = False
1757
1761
 
1758
1762
  if has_remote:
1759
- try:
1760
- git_sync.fetch_origin(repo_root)
1763
+ if already_fetched:
1761
1764
  merge_ref = f"origin/{resolved_main}"
1762
- if not json_output:
1763
- console.detail(f"Fetched latest from origin/{resolved_main}")
1764
- except GitError:
1765
- if not json_output:
1766
- console.warn("Fetch failed; using local main")
1765
+ else:
1766
+ try:
1767
+ git_sync.fetch_origin(repo_root)
1768
+ merge_ref = f"origin/{resolved_main}"
1769
+ if not json_output:
1770
+ console.detail(f"Fetched latest from origin/{resolved_main}")
1771
+ except GitError:
1772
+ if not json_output:
1773
+ console.warn("Fetch failed; using local main")
1767
1774
 
1768
1775
  # Count commits behind
1769
1776
  try:
@@ -1884,6 +1891,7 @@ def sync(
1884
1891
  resolved_main = preflight.resolved_main
1885
1892
 
1886
1893
  stash_ref: str | None = None
1894
+ already_fetched = False
1887
1895
 
1888
1896
  if preflight.dirty_category == _DirtyCategory.USER_DIRTY:
1889
1897
  if no_stash:
@@ -1900,6 +1908,7 @@ def sync(
1900
1908
  if has_remote:
1901
1909
  git_sync.fetch_origin(repo_root)
1902
1910
  merge_ref_for_probe = f"origin/{resolved_main}"
1911
+ already_fetched = True
1903
1912
  except GitError:
1904
1913
  pass
1905
1914
 
@@ -1966,6 +1975,7 @@ def sync(
1966
1975
  json_output=json_output,
1967
1976
  abort_on_conflict=stash_ref is not None,
1968
1977
  session_type=session_type,
1978
+ already_fetched=already_fetched,
1969
1979
  )
1970
1980
 
1971
1981
  if stash_ref is not None:
@@ -2033,6 +2043,7 @@ def catchup(
2033
2043
  resolved_main = preflight.resolved_main
2034
2044
 
2035
2045
  stash_ref: str | None = None
2046
+ already_fetched = False
2036
2047
 
2037
2048
  if preflight.dirty_category == _DirtyCategory.USER_DIRTY:
2038
2049
  if no_stash:
@@ -2047,6 +2058,7 @@ def catchup(
2047
2058
  if git_repo.has_remote(repo_root):
2048
2059
  git_sync.fetch_origin(repo_root)
2049
2060
  merge_ref_for_probe = f"origin/{resolved_main}"
2061
+ already_fetched = True
2050
2062
  except GitError:
2051
2063
  pass
2052
2064
 
@@ -2093,6 +2105,7 @@ def catchup(
2093
2105
  dry_run=dry_run,
2094
2106
  json_output=json_output,
2095
2107
  abort_on_conflict=True,
2108
+ already_fetched=already_fetched,
2096
2109
  )
2097
2110
 
2098
2111
  stash_restored = True
@@ -795,11 +795,32 @@ def _maybe_self_upgrade() -> bool:
795
795
  Checks PyPI first — only upgrades if a newer version is actually available.
796
796
  If upgrade is applied, calls re_exec() which replaces this process.
797
797
  Returns True if an upgrade was triggered, False if skipped or up to date.
798
+
799
+ Guards against an infinite re-exec loop: immediately before re-exec'ing,
800
+ the pre-upgrade version is recorded in WADE_SELF_UPGRADE_FROM, which
801
+ os.execv inherits into the replacement process. On entry, if that
802
+ breadcrumb is present this call is a post-re-exec retry, so it is always
803
+ cleared and the function returns False without attempting another
804
+ upgrade — regardless of whether the version actually advanced.
798
805
  """
799
806
  from wade import __version__
800
807
  from wade.utils.install import InstallMethod, detect_install_method, re_exec, self_upgrade
801
808
  from wade.utils.update_check import check_for_update
802
809
 
810
+ breadcrumb = os.environ.pop("WADE_SELF_UPGRADE_FROM", None)
811
+ if breadcrumb is not None:
812
+ if breadcrumb == __version__:
813
+ console.warn(
814
+ f"self-upgrade did not change the installed version ({__version__}); "
815
+ "the reported latest may be unavailable — continuing with "
816
+ f"{__version__}. Use --skip-self-upgrade to skip."
817
+ )
818
+ else:
819
+ logger.info(
820
+ "_maybe_self_upgrade.upgraded", from_version=breadcrumb, to_version=__version__
821
+ )
822
+ return False
823
+
803
824
  method = detect_install_method()
804
825
 
805
826
  if method in (InstallMethod.EDITABLE, InstallMethod.UNKNOWN):
@@ -808,12 +829,13 @@ def _maybe_self_upgrade() -> bool:
808
829
 
809
830
  console.step("Checking for WADE updates...")
810
831
 
811
- latest = check_for_update(__version__)
832
+ latest = check_for_update(__version__, force=True)
812
833
  if not latest:
813
834
  return False # Already at the latest version
814
835
 
815
836
  if self_upgrade():
816
837
  console.success("wade upgraded — restarting...")
838
+ os.environ["WADE_SELF_UPGRADE_FROM"] = __version__
817
839
  re_exec() # Does not return
818
840
  return True # pragma: no cover
819
841
 
@@ -72,6 +72,7 @@ class EntryRating(BaseModel):
72
72
 
73
73
  up: int = 0
74
74
  down: int = 0
75
+ stale: int = 0
75
76
  superseded_by: str | None = None
76
77
 
77
78
 
@@ -317,19 +318,21 @@ def record_rating(
317
318
  entry_id: str,
318
319
  direction: str,
319
320
  ) -> None:
320
- """Increment the up or down counter for an entry in the sidecar file.
321
+ """Increment the up, down, or stale counter for an entry in the sidecar file.
321
322
 
322
- ``direction`` must be ``"up"`` or ``"down"``.
323
+ ``direction`` must be ``"up"``, ``"down"``, or ``"stale"``.
323
324
  """
324
- if direction not in ("up", "down"):
325
- raise ValueError(f"Invalid direction {direction!r}: must be 'up' or 'down'")
325
+ if direction not in ("up", "down", "stale"):
326
+ raise ValueError(f"Invalid direction {direction!r}: must be 'up', 'down', or 'stale'")
326
327
 
327
328
  def _modify(data: dict[str, EntryRating]) -> None:
328
329
  entry = data.setdefault(entry_id, EntryRating())
329
330
  if direction == "up":
330
331
  entry.up += 1
331
- else:
332
+ elif direction == "down":
332
333
  entry.down += 1
334
+ else:
335
+ entry.stale += 1
333
336
 
334
337
  _read_modify_write_ratings(ratings_path, _modify)
335
338
 
@@ -454,11 +457,12 @@ def get_annotated_knowledge(
454
457
  entry_rating = ratings.get(entry.entry_id) if entry.entry_id else None
455
458
  up = entry_rating.up if entry_rating else 0
456
459
  down = entry_rating.down if entry_rating else 0
460
+ stale = entry_rating.stale if entry_rating else 0
457
461
  net_score = up - down
458
462
  total_votes = up + down
459
463
  should_annotate = entry.entry_id is not None
460
464
 
461
- # Score filtering
465
+ # Score and stale filtering
462
466
  if not no_filter:
463
467
  if min_score is not None:
464
468
  # Hard cutoff mode
@@ -471,6 +475,9 @@ def get_annotated_knowledge(
471
475
  and net_score < auto_threshold
472
476
  ):
473
477
  continue
478
+ # Stale threshold filter (independent of score filter)
479
+ if entry.entry_id is not None and stale >= 2:
480
+ continue
474
481
 
475
482
  # Search/tag filtering (OR semantics)
476
483
  if parsed_query is not None or filter_tags:
@@ -489,7 +496,8 @@ def get_annotated_knowledge(
489
496
  if heading_match and should_annotate:
490
497
  id_part = f"{entry.entry_id} | " if entry.entry_id else ""
491
498
  assert entry.date is not None # entry_id is always accompanied by a date
492
- heading = f"## {id_part}{entry.date} | {entry.heading_rest} [+{up}/-{down}]"
499
+ stale_part = f"/stale:{stale}" if stale > 0 else ""
500
+ heading = f"## {id_part}{entry.date} | {entry.heading_rest} [+{up}/-{down}{stale_part}]"
493
501
  raw_lines = entry.raw.split("\n")
494
502
  raw_lines[0] = heading
495
503
  result_parts.append("\n".join(raw_lines))
@@ -106,6 +106,11 @@ def re_exec() -> None:
106
106
 
107
107
  Replaces the current process image so the newly installed code is loaded.
108
108
  Does not return.
109
+
110
+ Uses os.execv (not execve), so the replacement process inherits the
111
+ calling process's environment unchanged — callers rely on this to pass
112
+ state across the re-exec (e.g. init_service._maybe_self_upgrade sets
113
+ WADE_SELF_UPGRADE_FROM beforehand to guard against re-exec loops).
109
114
  """
110
115
  executable = sys.argv[0]
111
116
 
@@ -26,20 +26,22 @@ PYPI_URL = "https://pypi.org/pypi/wade-cli/json"
26
26
  CACHE_FILE = Path.home() / ".local" / "share" / "wade" / "update-check.json"
27
27
 
28
28
 
29
- def _is_newer(candidate: str, current: str) -> bool:
30
- """Return True if candidate version is strictly newer than current.
29
+ def _version_tuple(v: str) -> tuple[int, ...]:
30
+ """Parse a dotted version string into a comparable integer tuple.
31
31
 
32
- Uses simple numeric tuple comparison — sufficient for standard semver.
33
- Falls back to False on any parse error.
32
+ Uses simple numeric tuple parsing — sufficient for standard semver.
33
+ Non-numeric segments are dropped; falls back to an empty tuple on any
34
+ parse error.
34
35
  """
35
36
  try:
37
+ return tuple(int(x) for x in v.split(".")[:3] if x.isdigit())
38
+ except (ValueError, TypeError):
39
+ return ()
36
40
 
37
- def to_tuple(v: str) -> tuple[int, ...]:
38
- return tuple(int(x) for x in v.split(".")[:3] if x.isdigit())
39
41
 
40
- return to_tuple(candidate) > to_tuple(current)
41
- except (ValueError, TypeError):
42
- return False
42
+ def _is_newer(candidate: str, current: str) -> bool:
43
+ """Return True if candidate version is strictly newer than current."""
44
+ return _version_tuple(candidate) > _version_tuple(current)
43
45
 
44
46
 
45
47
  def _load_cache() -> dict[str, object] | None:
@@ -72,38 +74,63 @@ def _save_cache(latest_version: str, timestamp: float) -> None:
72
74
  pass
73
75
 
74
76
 
77
+ def _select_latest_non_yanked(releases: dict[str, object]) -> str | None:
78
+ """Return the highest version among releases with at least one non-yanked file.
79
+
80
+ A release with no files, or where every file is yanked, is skipped —
81
+ PyPI's `info.version` does not account for yanking, so it can point at a
82
+ release that is no longer installable.
83
+ """
84
+ candidates = [
85
+ version
86
+ for version, files in releases.items()
87
+ if isinstance(files, list)
88
+ and files
89
+ and not all(isinstance(f, dict) and f.get("yanked") for f in files)
90
+ ]
91
+ if not candidates:
92
+ return None
93
+ return max(candidates, key=_version_tuple)
94
+
95
+
75
96
  def _fetch_latest_version() -> str | None:
76
- """Fetch the latest wade-cli version from PyPI. Returns None on any error."""
97
+ """Fetch the latest non-yanked wade-cli version from PyPI. Returns None on any error."""
77
98
  try:
78
99
  with urlopen(PYPI_URL, timeout=5) as resp:
79
100
  data = json.loads(resp.read())
80
- version = data["info"]["version"]
81
- if isinstance(version, str):
82
- return version
101
+ releases = data["releases"]
102
+ if not isinstance(releases, dict):
103
+ return None
104
+ return _select_latest_non_yanked(releases)
83
105
  except (URLError, OSError, json.JSONDecodeError, KeyError, TypeError):
84
106
  pass
85
107
  return None
86
108
 
87
109
 
88
- def check_for_update(current_version: str) -> str | None:
110
+ def check_for_update(current_version: str, force: bool = False) -> str | None:
89
111
  """Return latest PyPI version if newer than current, else None.
90
112
 
91
- Uses a 2-hour file cache to avoid hammering PyPI.
113
+ Uses a 2-hour file cache to avoid hammering PyPI, unless force=True.
92
114
 
93
115
  Args:
94
116
  current_version: The currently installed version string.
117
+ force: When True, bypass the cache and fetch live from PyPI (the
118
+ fresh result still refreshes the cache). Used by the explicit
119
+ self-upgrade path so a stale or yanked cached "latest" cannot
120
+ drive the upgrade decision.
95
121
 
96
122
  Returns:
97
123
  The latest version string if an update is available, otherwise None.
98
124
  """
99
125
  now = datetime.now(UTC).timestamp()
100
126
 
101
- cached = _load_cache()
102
- if cached is not None:
103
- age = now - float(str(cached["timestamp"]))
104
- if age < CHECK_INTERVAL_SECS:
105
- latest = str(cached["latest_version"])
106
- return latest if _is_newer(latest, current_version) else None
127
+ if not force:
128
+ cached = _load_cache()
129
+ if cached is not None:
130
+ age = now - float(str(cached["timestamp"]))
131
+ if age < CHECK_INTERVAL_SECS:
132
+ latest = str(cached["latest_version"])
133
+ return latest if _is_newer(latest, current_version) else None
107
134
 
108
135
  fetched = _fetch_latest_version()
109
136
  if fetched: