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.
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/auto-version.yml +24 -2
- {wade_cli-0.29.6 → wade_cli-0.30.2}/CHANGELOG.md +36 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/KNOWLEDGE.md +16 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/KNOWLEDGE.ratings.yml +6 -3
- {wade_cli-0.29.6 → wade_cli-0.30.2}/PKG-INFO +1 -1
- {wade_cli-0.29.6 → wade_cli-0.30.2}/pyproject.toml +1 -1
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/__init__.py +1 -1
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/knowledge.py +9 -6
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/review_pr_comments_session.py +6 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/stash.py +0 -36
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/markdown.py +6 -4
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/core.py +20 -7
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/init_service.py +23 -1
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/knowledge_service.py +15 -7
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/install.py +5 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/update_check.py +48 -21
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/knowledge/SKILL.md +9 -6
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_knowledge_cli.py +72 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_markdown.py +12 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_init.py +107 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_service.py +96 -1
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_update_check.py +117 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/ci.yml +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/pr-title-lint.yml +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/publish.yml +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.github/workflows/release.yml +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.gitignore +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.pre-commit-config.yaml +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/.wade.yml +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/AGENTS.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/CONTRIBUTING.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/README.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/assets/wade.png +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/docker-compose.e2e.yml +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/architecture.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/documentation-policies.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/extending.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/skills-system.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/docs/dev/testing.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/install.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/auto_version.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/changelog.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/check-all.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/check.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/fmt.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/hooks/pre-push +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/install-hooks.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/release.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/setup-worktree.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-e2e-docker.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-e2e.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-live-ai-taskr.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-live-ai.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test-live-gh.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/scripts/test.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/__main__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/admin.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/autocomplete.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/implementation_session.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/main.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/plan_session.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/review.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/session_shared.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/task.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/cli/worktree.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/loader.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/migrations.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/config/schema.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/engine.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/repositories.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/db/tables.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/branch.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/pr.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/repo.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/sync.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/git/worktree.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/hooks/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/hooks/plan_write_guard.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/hooks/worktree_guard.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/logging/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/logging/context.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/logging/setup.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/batch.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/config.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/delegation.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/deps.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/events.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/review.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/session.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/models/task.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/_pr_delegate.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/base.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/clickup.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/github.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/providers/registry.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/ai_resolution.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/batch_review_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/check_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/delegation_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/deps_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/batch.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/bootstrap.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/implementation_service/usage_tracking.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/knowledge_search.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/plan_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/prompt_delivery.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/review_delegation_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/review_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/review_settle.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/smart_start.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/services/task_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/skills/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/skills/installer.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/skills/pointer.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/ui/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/ui/console.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/ui/prompts.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/clipboard.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/http.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/markdown.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/process.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/slug.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/terminal.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/src/wade/utils/token_usage_markdown.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/agents-pointer.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/deps-analysis.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/implement-context.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/plan-session.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-batch.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-code.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-plan.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/prompts/review-pr-comments.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/setup-worktree.sh.example +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/review-enforcement-rule.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/review-implementation-closing-step.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/review-plan-step.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/_partials/user-interaction.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/deps/SKILL.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/implementation-session/SKILL.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/plan-session/SKILL.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/review-pr-comments-session/SKILL.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/task/SKILL.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/task/examples.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/skills/task/plan-format.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/templates/statusline-command.sh +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/conftest.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/_support.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/conftest.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/mock_gh_script.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_admin_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_check_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_implement_work_done_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_knowledge_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_mock_gh_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_plan_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_review_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_smart_start_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_task_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/e2e/test_work_sync_list_contract.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_hi_body.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_howdy_body.md +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/claude_session.txt +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/codex_session.txt +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/copilot_session.txt +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/gemini_session.txt +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/fixtures/transcripts/generic_session.txt +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_autostash.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_check.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_git.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_implementation_lifecycle.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_init.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_knowledge.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_markdown_provider.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/integration/test_skill_install.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/test_wade_live_ai.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/test_wade_live_ai_taskr.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/live/test_wade_live_gh.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/test_cli_basics.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/conftest.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_done_review_hint.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_review_cli.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_session_subapps.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_shell_init.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_task_cli.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_ai_flags.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_cli.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/test_loader.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations_atomic.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_console.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_db/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_db/test_repository_transactions.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_effort.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_branch_slugify.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_detached_worktree.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_pr_base.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_repo.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_scaffold_commit.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_sync_conflicted_files.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_git/test_worktree_remove.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_hooks/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_hooks/test_plan_write_guard.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_hooks/test_worktree_guard.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_batch.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_config_models.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_db.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_delegation.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_models.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_partition.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_review.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_models/test_task_models.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_clickup.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_github.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_providers/test_github_pr.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_ai_resolution.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_batch_review_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_allowlist.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_hook_timeout.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_catchup.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_check.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_delegation_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_deps_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_done_plan_flag.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_done_via_direct_cleanup.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_done_sync.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service_resume.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_init_shell_integration.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_search.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_plan_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_poll_for_reviews.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_post_implementation_lifecycle.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_pr_summary_path.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_preserve_session_data.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_prompt_delivery.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_review_delegation_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_review_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start_resume.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_sync_base_branch.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_task_service.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_tracked_managed_files.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_services/test_worktree_gitignore.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_skills/test_managed_gitignore.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/__init__.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_http.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_install.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_markdown.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_process.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_prompts.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_slug.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal.py +0 -0
- {wade_cli-0.29.6 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal_launch.py +0 -0
- {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
|
-
|
|
73
|
-
|
|
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:
|
|
12
|
+
up: 2
|
|
10
13
|
54326c98:
|
|
11
14
|
down: 1
|
|
12
15
|
up: 0
|
|
13
16
|
b61e247e:
|
|
14
17
|
down: 0
|
|
15
|
-
up:
|
|
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:
|
|
30
|
+
up: 3
|
|
@@ -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
|
|
158
|
+
direction: str = typer.Argument(help="Rating direction: up, down, or stale."),
|
|
159
159
|
) -> None:
|
|
160
|
-
"""Rate a knowledge entry (
|
|
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 '
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
585
|
+
cwd = self._path.parent
|
|
584
586
|
try:
|
|
585
587
|
add = run(
|
|
586
588
|
["git", "add", str(self._path)],
|
|
587
|
-
cwd=
|
|
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=
|
|
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
|
-
|
|
1760
|
-
git_sync.fetch_origin(repo_root)
|
|
1763
|
+
if already_fetched:
|
|
1761
1764
|
merge_ref = f"origin/{resolved_main}"
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
|
321
|
+
"""Increment the up, down, or stale counter for an entry in the sidecar file.
|
|
321
322
|
|
|
322
|
-
``direction`` must be ``"up"`` or ``"
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
"""
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
81
|
-
if isinstance(
|
|
82
|
-
return
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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:
|