boost-skill-cli 1.0.81__tar.gz → 1.0.83__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 (233) hide show
  1. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/PKG-INFO +1 -1
  2. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/_version.py +2 -2
  3. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/pkg.py +6 -6
  4. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/quality.py +15 -13
  5. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/taps.py +15 -10
  6. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/catalog.py +46 -44
  7. boost_skill_cli-1.0.83/boost_cli/core/staleness.py +78 -0
  8. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  9. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_skill_cli.egg-info/SOURCES.txt +2 -0
  10. boost_skill_cli-1.0.83/docs/roadmap/items/consolidate-skill-staleness-drift-logic-into-cor.md +16 -0
  11. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/prune-ignored-dirs-during-scan-dir-walk.md +3 -3
  12. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap.html +14 -13
  13. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_catalog.py +21 -0
  14. boost_skill_cli-1.0.83/tests/unit/test_staleness.py +102 -0
  15. boost_skill_cli-1.0.81/docs/roadmap/items/consolidate-skill-staleness-drift-logic-into-cor.md +0 -16
  16. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/.gitattributes +0 -0
  17. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/.gitignore +0 -0
  18. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/CLAUDE.md +0 -0
  19. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/CONTRIBUTING.md +0 -0
  20. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/LICENSE +0 -0
  21. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/MANIFEST.in +0 -0
  22. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/Makefile +0 -0
  23. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/README.md +0 -0
  24. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/SECURITY.md +0 -0
  25. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost +0 -0
  26. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/__init__.py +0 -0
  27. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/__main__.py +0 -0
  28. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/cli.py +0 -0
  29. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/cliparse.py +0 -0
  30. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/__init__.py +0 -0
  31. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/bmad.py +0 -0
  32. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/configuration.py +0 -0
  33. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/discovery.py +0 -0
  34. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/hooks.py +0 -0
  35. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/info.py +0 -0
  36. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/intelligence.py +0 -0
  37. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/commands/team.py +0 -0
  38. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/__init__.py +0 -0
  39. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/agents.py +0 -0
  40. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/ai.py +0 -0
  41. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/claude_settings.py +0 -0
  42. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/config.py +0 -0
  43. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/dense.py +0 -0
  44. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/embed.py +0 -0
  45. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/frontmatter.py +0 -0
  46. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/gitutil.py +0 -0
  47. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/journal.py +0 -0
  48. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/lockfile.py +0 -0
  49. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/logs.py +0 -0
  50. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/mcp.py +0 -0
  51. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/output.py +0 -0
  52. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/paths.py +0 -0
  53. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/policy.py +0 -0
  54. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/rag.py +0 -0
  55. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/registry.py +0 -0
  56. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/store.py +0 -0
  57. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/core/util.py +0 -0
  58. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/data/registries.json +0 -0
  59. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/errors.py +0 -0
  60. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_cli/spin.py +0 -0
  61. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  62. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  63. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_skill_cli.egg-info/requires.txt +0 -0
  64. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/boost_skill_cli.egg-info/top_level.txt +0 -0
  65. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/.claude/settings.local.json +0 -0
  66. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/DEBUGGING.md +0 -0
  67. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/demo.gif +0 -0
  68. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/demo.tape +0 -0
  69. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/design-roadmap.html +0 -0
  70. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/index.html +0 -0
  71. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/mcp-hub.html +0 -0
  72. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/rag-architecture.md +0 -0
  73. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D01.md +0 -0
  74. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D02.md +0 -0
  75. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D03.md +0 -0
  76. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D04.md +0 -0
  77. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D05.md +0 -0
  78. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D06.md +0 -0
  79. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D07.md +0 -0
  80. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D08.md +0 -0
  81. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D09.md +0 -0
  82. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D10.md +0 -0
  83. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D11.md +0 -0
  84. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D12.md +0 -0
  85. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D13.md +0 -0
  86. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D14.md +0 -0
  87. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D15.md +0 -0
  88. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D16.md +0 -0
  89. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D17.md +0 -0
  90. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D18.md +0 -0
  91. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D19.md +0 -0
  92. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D20.md +0 -0
  93. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D21.md +0 -0
  94. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D22.md +0 -0
  95. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D23.md +0 -0
  96. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D24.md +0 -0
  97. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D25.md +0 -0
  98. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/BOOST-D26.md +0 -0
  99. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/accessibility-audit-pa11y-ci-axe-core.md +0 -0
  100. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/atomic-corruption-safe-lock-file-writes.md +0 -0
  101. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/atomic-skill-install-temp-dir-swap.md +0 -0
  102. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/autonomous-ship-workflow-and-isolated-worktree.md +0 -0
  103. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/bring-commands-under-mutation-testing.md +0 -0
  104. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/broken-link-and-anchor-checking-lychee.md +0 -0
  105. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/build-provenance-slsa-attestations.md +0 -0
  106. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/cache-the-catalog-entry-set-across-rag-queries.md +0 -0
  107. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/capability-manifest-and-least-privilege-policy.md +0 -0
  108. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/clean-env-install-smoke-pip-and-pipx.md +0 -0
  109. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/command-reference-docs-site.md +0 -0
  110. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/complexity-and-dead-code-radar-xenon-vulture.md +0 -0
  111. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/coverage-dashboard-codecov-free-for-oss.md +0 -0
  112. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/coverage-guided-fuzzing-atheris-oss-fuzz.md +0 -0
  113. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/crash-recorder-error-paths-core-logs-py.md +0 -0
  114. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/dependency-cve-gate-pip-audit.md +0 -0
  115. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/docstring-coverage-interrogate.md +0 -0
  116. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/end-of-options-guard-on-git-commands.md +0 -0
  117. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/extension-free-tests-core-dense-py.md +0 -0
  118. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +0 -0
  119. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/finish-mutation-hardening-across-core.md +0 -0
  120. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/fix-self-update-version-detection-dead-branch.md +0 -0
  121. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/fork-safe-network-proxy-handler.md +0 -0
  122. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/frontmatter-scalar-over-coercion.md +0 -0
  123. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/github-pages-deploy-is-broken-every-push.md +0 -0
  124. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/hash-pinned-reproducible-toolchain-uv-lock.md +0 -0
  125. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/html-validation-html-validate.md +0 -0
  126. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/integrity-verification-boost-verify.md +0 -0
  127. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/invocation-pid-logging-for-crash-correlation.md +0 -0
  128. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/layering-guard-import-linter.md +0 -0
  129. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/lighthouse-ci-on-the-pages-site.md +0 -0
  130. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/localize-the-stored-bm25-snippet.md +0 -0
  131. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/lockfile-enforcement-and-commit-pinning.md +0 -0
  132. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/lowest-version-resolution-uv-resolution-lowest-d.md +0 -0
  133. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/make-the-roadmaps-discoverable.md +0 -0
  134. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/markdown-consistency-markdownlint-cli2.md +0 -0
  135. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/memoize-config-load-in-process.md +0 -0
  136. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/modernization-smells-refurb-pyupgrade.md +0 -0
  137. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/mutation-hardening-core-frontmatter-py.md +0 -0
  138. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/mutation-hardening-core-gitutil-py.md +0 -0
  139. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/mutation-hardening-core-store-py.md +0 -0
  140. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/one-command-every-env-nox.md +0 -0
  141. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/one-shared-atomic-write-helper.md +0 -0
  142. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/package-metadata-validation-twine-check-friends.md +0 -0
  143. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/patch-coverage-gate-diff-cover.md +0 -0
  144. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/performance-regression-gate-pytest-benchmark.md +0 -0
  145. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/post-deploy-smoke-headless-load-check.md +0 -0
  146. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/pre-release-python-canary-3-14t-free-threaded.md +0 -0
  147. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/promote-nav-footer-into-the-shared-style-system.md +0 -0
  148. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/prompt-injection-scanning-of-skill-markdown.md +0 -0
  149. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/property-based-tests-hypothesis-on-the-parsers.md +0 -0
  150. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/prose-and-terminology-linting-vale.md +0 -0
  151. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/quality-dashboard-sonarcloud-free-for-oss.md +0 -0
  152. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/reconcile-the-theme-drift.md +0 -0
  153. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/refresh-the-marketing-surface.md +0 -0
  154. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/reuse-helpers-kill-minor-dead-work.md +0 -0
  155. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/robust-tag-argument-parsing.md +0 -0
  156. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/runner-egress-monitoring-stepsecurity-harden-run.md +0 -0
  157. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/sbom-aware-scanning-osv-scanner.md +0 -0
  158. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/sbom-on-every-release-cyclonedx-syft.md +0 -0
  159. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/second-type-checker-pyright.md +0 -0
  160. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/secret-and-pii-scanning-of-installed-skills.md +0 -0
  161. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/secret-scanning-gitleaks-push-protection.md +0 -0
  162. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/security-linting-bandit-via-ruff-s-rules.md +0 -0
  163. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/shift-left-gate-pre-commit-pre-commit-ci.md +0 -0
  164. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/single-imperative-rule-extractor.md +0 -0
  165. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/single-tech-stack-prober.md +0 -0
  166. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/split-oversized-command-modules.md +0 -0
  167. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/startup-and-import-time-budget-x-importtime.md +0 -0
  168. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/stop-re-serializing-entry-meta-on-every-search.md +0 -0
  169. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/supply-chain-posture-openssf-scorecard.md +0 -0
  170. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/tap-signing-and-provenance-sigstore-minisign.md +0 -0
  171. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/theme-asset-linting-stylelint-eslint.md +0 -0
  172. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/typo-detection-codespell.md +0 -0
  173. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/typosquat-and-name-confusion-detection.md +0 -0
  174. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/unify-tilde-two-copies-have-a-boundary-bug.md +0 -0
  175. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/untrack-generated-build-noise.md +0 -0
  176. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/update-diff-before-apply.md +0 -0
  177. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/visual-regression-pass-on-the-guide.md +0 -0
  178. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/widen-the-ruff-rule-surface-b-sim-c4-perf-ruf.md +0 -0
  179. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/windows-in-the-ci-matrix.md +0 -0
  180. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/workflow-linting-actionlint.md +0 -0
  181. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/docs/roadmap/items/workflow-sast-zizmor.md +0 -0
  182. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/index.html +0 -0
  183. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/pyproject.toml +0 -0
  184. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/scripts/build_registries.py +0 -0
  185. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/scripts/build_roadmap.py +0 -0
  186. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/scripts/mutation_gate.py +0 -0
  187. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/setup.cfg +0 -0
  188. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/style/README.md +0 -0
  189. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/style/boost.css +0 -0
  190. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/style/boost.js +0 -0
  191. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/style/demo.html +0 -0
  192. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/conftest.py +0 -0
  193. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_bmad.py +0 -0
  194. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_configuration.py +0 -0
  195. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_discovery.py +0 -0
  196. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_hooks.py +0 -0
  197. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_info.py +0 -0
  198. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_intelligence.py +0 -0
  199. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_pkg.py +0 -0
  200. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_quality.py +0 -0
  201. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_taps.py +0 -0
  202. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_cli_team.py +0 -0
  203. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/functional/test_everyday_loop.py +0 -0
  204. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/make_fixture.py +0 -0
  205. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/smoke.sh +0 -0
  206. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_agents.py +0 -0
  207. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_ai.py +0 -0
  208. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_claude_settings.py +0 -0
  209. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_cliparse.py +0 -0
  210. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_config.py +0 -0
  211. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_dense.py +0 -0
  212. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_dense_fallback.py +0 -0
  213. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_embed.py +0 -0
  214. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_errors_and_cli_table.py +0 -0
  215. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_frontmatter.py +0 -0
  216. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_gitutil.py +0 -0
  217. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_journal.py +0 -0
  218. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_lockfile.py +0 -0
  219. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_logs.py +0 -0
  220. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_mcp.py +0 -0
  221. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_mutation_hardening.py +0 -0
  222. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_output.py +0 -0
  223. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_paths.py +0 -0
  224. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_policy.py +0 -0
  225. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_rag.py +0 -0
  226. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_registries_fresh.py +0 -0
  227. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_registry.py +0 -0
  228. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_roadmap_fresh.py +0 -0
  229. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_spin.py +0 -0
  230. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_store.py +0 -0
  231. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_token_parity.py +0 -0
  232. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_util.py +0 -0
  233. {boost_skill_cli-1.0.81 → boost_skill_cli-1.0.83}/tests/unit/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.81
3
+ Version: 1.0.83
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.81'
22
- __version_tuple__ = version_tuple = (1, 0, 81)
21
+ __version__ = version = '1.0.83'
22
+ __version_tuple__ = version_tuple = (1, 0, 83)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -16,7 +16,7 @@ from typing import Dict, List, Optional
16
16
 
17
17
  from .. import cliparse
18
18
  from ..core import (agents, catalog, gitutil, journal, lockfile, paths,
19
- registry, store, util)
19
+ registry, staleness, store, util)
20
20
  from ..core import output as out
21
21
  from ..errors import BoostError
22
22
 
@@ -287,16 +287,16 @@ def cmd_update(argv: List[str]) -> int:
287
287
  entry = matches[0]
288
288
  old_v = str(lk.get("version", "0.0.0"))
289
289
  new_v = str(entry.get("version", "0.0.0"))
290
- reason = "version" if util.semver_gt(new_v, old_v) else None
291
- if reason is None:
290
+ head, src_sha = "", None
291
+ if not util.semver_gt(new_v, old_v):
292
292
  head = gitutil.head_commit(registry.get(tapname).path)
293
293
  if head and head != lk.get("commit"):
294
294
  try:
295
- src = store.source_dir_for(entry)
295
+ src_sha = util.sha256_dir(store.source_dir_for(entry))
296
296
  except BoostError:
297
297
  continue
298
- if util.sha256_dir(src) != lk.get("sha256"):
299
- reason = "content"
298
+ reason = staleness.upstream_reason(old_v, new_v, lk.get("commit", ""),
299
+ head, lk.get("sha256", ""), src_sha)
300
300
  if reason is None:
301
301
  continue
302
302
  try:
@@ -18,7 +18,7 @@ from typing import List, Optional, Tuple
18
18
  from .. import cliparse
19
19
  from ..core import (agents, ai, catalog, frontmatter, gitutil, journal,
20
20
  lockfile, logs, output as out, paths, policy, registry,
21
- store, util)
21
+ staleness, store, util)
22
22
  from ..errors import BoostError
23
23
 
24
24
  # --- audit: dangerous-content patterns ------------------------------------
@@ -119,18 +119,20 @@ def _drift_status(name: str, entry: dict) -> str:
119
119
  """'in-sync' | 'local-edits' | 'upstream-moved' | 'source-missing'
120
120
  | 'store-missing' | 'n/a' (local imports with no tap source)."""
121
121
  sdir = store.skill_store_dir(name)
122
- if not sdir.is_dir():
123
- return "store-missing"
124
- if util.sha256_dir(sdir) != entry.get("sha256"):
125
- return "local-edits"
126
- if entry.get("tap") == "local":
127
- return "n/a"
128
- try:
129
- src = store.source_dir_for({"name": name, "tap": entry.get("tap", ""),
130
- "rel_dir": entry.get("source_dir", ".")})
131
- except BoostError:
132
- return "source-missing"
133
- return "in-sync" if util.sha256_dir(src) == entry.get("sha256") else "upstream-moved"
122
+ store_sha = util.sha256_dir(sdir) if sdir.is_dir() else None
123
+ is_local = entry.get("tap") == "local"
124
+ lock_sha = entry.get("sha256", "")
125
+ source_sha = None
126
+ if store_sha is not None and store_sha == lock_sha and not is_local:
127
+ try:
128
+ src = store.source_dir_for(
129
+ {"name": name, "tap": entry.get("tap", ""),
130
+ "rel_dir": entry.get("source_dir", ".")})
131
+ except BoostError:
132
+ source_sha = None
133
+ else:
134
+ source_sha = util.sha256_dir(src)
135
+ return staleness.drift_state(store_sha, lock_sha, is_local, source_sha)
134
136
 
135
137
 
136
138
  _DRIFT_STYLE = {"in-sync": out.GREEN, "local-edits": out.YELLOW,
@@ -5,7 +5,7 @@ import argparse
5
5
  import json
6
6
 
7
7
  from .. import cliparse, spin
8
- from ..core import catalog, config, gitutil, journal, lockfile, paths, registry, store, util
8
+ from ..core import catalog, config, gitutil, journal, lockfile, paths, registry, staleness, store, util
9
9
  from ..core import output as out
10
10
  from ..errors import BoostError
11
11
 
@@ -218,9 +218,8 @@ def cmd_outdated(argv) -> int:
218
218
  latest = str(entry.get("version") or "0.0.0")
219
219
  installed_v = str(lk.get("version") or "0.0.0")
220
220
  stale, latest_disp = False, latest
221
- if util.semver_gt(latest, installed_v):
222
- stale = True
223
- else:
221
+ head, src_sha, src_missing = "", None, False
222
+ if not util.semver_gt(latest, installed_v):
224
223
  if tap_name not in heads:
225
224
  try:
226
225
  tap = registry.get(tap_name)
@@ -231,13 +230,19 @@ def cmd_outdated(argv) -> int:
231
230
  head = heads[tap_name]
232
231
  if head and head != lk.get("commit"):
233
232
  try:
234
- src = store.source_dir_for(entry)
233
+ src_sha = util.sha256_dir(store.source_dir_for(entry))
235
234
  except BoostError:
236
- stale, latest_disp = True, "source missing"
237
- else:
238
- if util.sha256_dir(src) != lk.get("sha256"):
239
- stale = True
240
- latest_disp = "%s (%s)" % (latest, head[:7])
235
+ src_missing = True
236
+ if src_missing:
237
+ stale, latest_disp = True, "source missing"
238
+ else:
239
+ reason = staleness.upstream_reason(
240
+ installed_v, latest, lk.get("commit", ""), head,
241
+ lk.get("sha256", ""), src_sha)
242
+ if reason == staleness.VERSION:
243
+ stale = True
244
+ elif reason == staleness.CONTENT:
245
+ stale, latest_disp = True, "%s (%s)" % (latest, head[:7])
241
246
  if stale:
242
247
  results.append({"name": name, "installed": installed_v,
243
248
  "latest": latest_disp, "tap": tap_name,
@@ -18,9 +18,10 @@ that ship in the same GitHub registries:
18
18
  from __future__ import annotations
19
19
 
20
20
  import json
21
+ import os
21
22
  import re
22
23
  from pathlib import Path
23
- from typing import List, Optional, Tuple
24
+ from typing import List, Optional, Set, Tuple
24
25
 
25
26
  from ..errors import BoostError
26
27
  from . import frontmatter, gitutil, paths, registry, util
@@ -42,10 +43,6 @@ DOC_STEMS = {"readme", "contributing", "license", "changelog", "security",
42
43
  "code_of_conduct", "index", "_index", "authors", "notice"}
43
44
 
44
45
 
45
- def _ignored(path: Path) -> bool:
46
- return any(part in util.IGNORED for part in path.parts)
47
-
48
-
49
46
  def _read(path: Path) -> Optional[Tuple[dict, str]]:
50
47
  try:
51
48
  return frontmatter.parse(path.read_text(encoding="utf-8", errors="replace"))
@@ -107,46 +104,51 @@ def _classify_workflow(path: Path, meta: dict) -> bool:
107
104
  def scan_dir(root: Path, tap_name: str = "local", curated: bool = False) -> List[dict]:
108
105
  root = Path(root)
109
106
  entries: List[dict] = []
110
- skill_dirs: List[Path] = []
111
- for skill_md in sorted(root.rglob("SKILL.md")):
112
- if _ignored(skill_md):
113
- continue
114
- parsed = _read(skill_md)
115
- if parsed is None:
116
- continue
117
- meta, body = parsed
118
- skill_dir = skill_md.parent
119
- default = skill_dir.name if skill_dir != root else root.name
120
- entries.append(_make_entry(root, skill_md, KIND_SKILL, default,
121
- tap_name, curated, meta, body))
122
- skill_dirs.append(skill_dir)
123
-
124
- for path in sorted(root.rglob("*")):
125
- if not path.is_file() or _ignored(path):
126
- continue
127
- # never re-index a file that belongs to a scanned skill directory
128
- if any(path == d or d in path.parents for d in skill_dirs):
129
- continue
130
- is_rule = _classify_rule(path)
131
- is_md = path.suffix.lower() == ".md" and path.name != "SKILL.md"
132
- if not is_rule and not is_md:
133
- continue
134
- parsed = _read(path)
135
- if parsed is None:
136
- continue
137
- meta, body = parsed
138
- if is_rule:
139
- kind = KIND_RULE
140
- elif _classify_workflow(path, meta):
141
- kind = KIND_WORKFLOW
142
- else:
107
+ skill_dirs: Set[Path] = set()
108
+ # One top-down walk instead of two rglob passes: prune ignored dirs in place
109
+ # so we never descend into .git/__pycache__, and test skill-dir membership
110
+ # against a set via each dir's own ancestor chain rather than O(files × dirs).
111
+ # Top-down order guarantees a file's governing SKILL.md dir is seen first, and
112
+ # the final entries.sort() normalizes output order regardless of walk order.
113
+ for dirpath, dirnames, filenames in os.walk(root):
114
+ dirnames[:] = [d for d in dirnames if d not in util.IGNORED]
115
+ here = Path(dirpath)
116
+ # A directory holding a SKILL.md is a skill: index it and remember the
117
+ # dir so its other files are not re-indexed as loose rules/workflows.
118
+ if "SKILL.md" in filenames:
119
+ parsed = _read(here / "SKILL.md")
120
+ if parsed is not None:
121
+ meta, body = parsed
122
+ default = here.name if here != root else root.name
123
+ entries.append(_make_entry(root, here / "SKILL.md", KIND_SKILL,
124
+ default, tap_name, curated,
125
+ meta, body))
126
+ skill_dirs.add(here)
127
+ # Files inside any skill directory belong to that skill, never re-indexed.
128
+ if here in skill_dirs or any(a in skill_dirs for a in here.parents):
143
129
  continue
144
- if path.name in RULE_FILENAMES:
145
- default = path.parent.name if path.parent != root else root.name
146
- else:
147
- default = path.stem
148
- entries.append(_make_entry(root, path, kind, default,
149
- tap_name, curated, meta, body))
130
+ for name in filenames:
131
+ path = here / name
132
+ is_rule = _classify_rule(path)
133
+ is_md = path.suffix.lower() == ".md" and path.name != "SKILL.md"
134
+ if not is_rule and not is_md:
135
+ continue
136
+ parsed = _read(path)
137
+ if parsed is None:
138
+ continue
139
+ meta, body = parsed
140
+ if is_rule:
141
+ kind = KIND_RULE
142
+ elif _classify_workflow(path, meta):
143
+ kind = KIND_WORKFLOW
144
+ else:
145
+ continue
146
+ if path.name in RULE_FILENAMES:
147
+ default = path.parent.name if path.parent != root else root.name
148
+ else:
149
+ default = path.stem
150
+ entries.append(_make_entry(root, path, kind, default,
151
+ tap_name, curated, meta, body))
150
152
 
151
153
  entries.sort(key=lambda e: (e["skill_md"], e["name"]))
152
154
  return entries
@@ -0,0 +1,78 @@
1
+ """Single source of truth for "is an installed skill stale vs its tap?".
2
+
3
+ ``cmd_update`` (pkg.py), ``cmd_outdated`` (taps.py) and ``_drift_status``
4
+ (quality.py) each re-derived this from the same three signals — the catalog
5
+ version, the tap's HEAD commit and the source tree's ``sha256`` — so the logic
6
+ lived in three places that would inevitably drift apart. These two pure
7
+ (I/O-free) functions are now the one decision the commands render.
8
+
9
+ Keeping them free of any filesystem/subprocess access is deliberate: the
10
+ commands gather the raw signals (``head_commit``, ``source_dir_for``,
11
+ ``sha256_dir``) and pass them in, so the *decision* stays trivially testable.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from typing import Optional
16
+
17
+ from . import util
18
+
19
+ # ── upstream_reason: why a tap's copy is newer than the installed one ──────
20
+ VERSION = "version" # the catalog advertises a higher semver
21
+ CONTENT = "content" # the tap HEAD moved AND the source tree's bytes changed
22
+
23
+
24
+ def upstream_reason(installed_version: str, latest_version: str,
25
+ installed_commit: str, tap_head: str,
26
+ installed_sha: str, source_sha: Optional[str]
27
+ ) -> Optional[str]:
28
+ """Why the tap's copy is newer than the installed one, else ``None``.
29
+
30
+ Mirrors the historical order exactly: a higher advertised semver wins
31
+ immediately; otherwise a *moved* tap HEAD **plus** a changed source tree
32
+ counts as content drift. ``source_sha`` is ``None`` when the source tree
33
+ could not be hashed (missing checkout) — treated as "no content signal",
34
+ i.e. not stale on that axis.
35
+ """
36
+ if util.semver_gt(latest_version, installed_version):
37
+ return VERSION
38
+ if tap_head and tap_head != installed_commit:
39
+ if source_sha is not None and source_sha != installed_sha:
40
+ return CONTENT
41
+ return None
42
+
43
+
44
+ # ── drift_state: how an installed skill's store copy relates to lock+tap ───
45
+ IN_SYNC = "in-sync"
46
+ LOCAL_EDITS = "local-edits"
47
+ UPSTREAM_MOVED = "upstream-moved"
48
+ SOURCE_MISSING = "source-missing"
49
+ STORE_MISSING = "store-missing"
50
+ NA = "n/a"
51
+
52
+
53
+ def drift_state(store_sha: Optional[str], lock_sha: str,
54
+ is_local: bool, source_sha: Optional[str]) -> str:
55
+ """Drift of an installed skill's store copy vs its lock entry and tap.
56
+
57
+ Signals (all pre-computed by the caller):
58
+
59
+ * ``store_sha`` — ``sha256`` of the on-disk store dir, or ``None`` if the
60
+ store dir is missing.
61
+ * ``lock_sha`` — the ``sha256`` recorded in the lock entry.
62
+ * ``is_local`` — ``True`` for local imports (no tap source to compare).
63
+ * ``source_sha`` — ``sha256`` of the tap source dir, or ``None`` if it is
64
+ unavailable (only consulted once the store copy matches the lock and the
65
+ skill has a tap).
66
+
67
+ The ordering is significant and matches the historical ``_drift_status``:
68
+ a missing store beats local edits, which beat the local/upstream split.
69
+ """
70
+ if store_sha is None:
71
+ return STORE_MISSING
72
+ if store_sha != lock_sha:
73
+ return LOCAL_EDITS
74
+ if is_local:
75
+ return NA
76
+ if source_sha is None:
77
+ return SOURCE_MISSING
78
+ return IN_SYNC if source_sha == lock_sha else UPSTREAM_MOVED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.81
3
+ Version: 1.0.83
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -48,6 +48,7 @@ boost_cli/core/paths.py
48
48
  boost_cli/core/policy.py
49
49
  boost_cli/core/rag.py
50
50
  boost_cli/core/registry.py
51
+ boost_cli/core/staleness.py
51
52
  boost_cli/core/store.py
52
53
  boost_cli/core/util.py
53
54
  boost_cli/data/registries.json
@@ -223,6 +224,7 @@ tests/unit/test_registries_fresh.py
223
224
  tests/unit/test_registry.py
224
225
  tests/unit/test_roadmap_fresh.py
225
226
  tests/unit/test_spin.py
227
+ tests/unit/test_staleness.py
226
228
  tests/unit/test_store.py
227
229
  tests/unit/test_token_parity.py
228
230
  tests/unit/test_util.py
@@ -0,0 +1,16 @@
1
+ ---
2
+ id: consolidate-skill-staleness-drift-logic-into-cor
3
+ board: code
4
+ section: shipped
5
+ status: shipped
6
+ category: Correctness
7
+ complexity: M
8
+ impact: High
9
+ wow: 3
10
+ note: three copies → one core module
11
+ order: 3
12
+ owner: loop/consolidate-staleness
13
+ pr: 91
14
+ title: Consolidate skill-staleness / drift logic into core
15
+ ---
16
+ The "is this skill outdated" decision — <code>semver_gt</code> → commit compare → <code>sha256_dir</code> vs lock — was reimplemented nearly verbatim in <code>cmd_update</code>, <code>cmd_outdated</code> and <code>_drift_status</code> — three copies of core business logic that would drift apart. It now lives in one place: <code>core/staleness.py</code> exposes two <b>pure</b> (I/O-free) decisions — <code>upstream_reason()</code> (the version→commit→content ladder rendered by <code>cmd_update</code>/<code>cmd_outdated</code>) and <code>drift_state()</code> (the store/local-edits/upstream ladder rendered by <code>_drift_status</code>). The commands became thin renderers; behavior is byte-for-byte preserved and the new module is fully unit-tested (every branch pinned, all mutants killed).
@@ -2,15 +2,15 @@
2
2
  id: prune-ignored-dirs-during-scan-dir-walk
3
3
  board: code
4
4
  section: internals
5
- status: planned
5
+ status: shipped
6
6
  category: Performance
7
7
  complexity: M
8
8
  impact: Low-Med
9
9
  wow: 2
10
10
  note:
11
11
  order: 12
12
- owner:
13
- pr:
12
+ owner: loop/prune-scan-dir
13
+ pr: 92
14
14
  title: Prune ignored dirs during <code>scan_dir</code> walk
15
15
  ---
16
16
  Two full <code>rglob</code> passes descend <b>into</b> <code>.git</code>/<code>node_modules</code> and only filter afterward, and the skill-dir membership test is O(files × skill_dirs) (<code>catalog.py:106,119–123</code>). Switch to an <code>os.walk</code> that prunes ignored dirs in place; index skill dirs in a set.
@@ -115,7 +115,7 @@
115
115
  <section class="block reveal">
116
116
  <div class="stats">
117
117
  <!-- ROADMAP:stats -->
118
- <div class="stat"><b>9</b><span>Shipped</span></div>
118
+ <div class="stat"><b>10</b><span>Shipped</span></div>
119
119
  <div class="stat"><b>3</b><span>Next up</span></div>
120
120
  <div class="stat"><b>4</b><span>Planned</span></div>
121
121
  <div class="stat"><b>85</b><span>Loop finds</span></div>
@@ -162,6 +162,18 @@
162
162
  </div>
163
163
  </article>
164
164
 
165
+ <article class="cap rcard">
166
+ <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Correctness</span></div>
167
+ <h3>Consolidate skill-staleness / drift logic into core</h3>
168
+ <p>The "is this skill outdated" decision — <code>semver_gt</code> → commit compare → <code>sha256_dir</code> vs lock — was reimplemented nearly verbatim in <code>cmd_update</code>, <code>cmd_outdated</code> and <code>_drift_status</code> — three copies of core business logic that would drift apart. It now lives in one place: <code>core/staleness.py</code> exposes two <b>pure</b> (I/O-free) decisions — <code>upstream_reason()</code> (the version→commit→content ladder rendered by <code>cmd_update</code>/<code>cmd_outdated</code>) and <code>drift_state()</code> (the store/local-edits/upstream ladder rendered by <code>_drift_status</code>). The commands became thin renderers; behavior is byte-for-byte preserved and the new module is fully unit-tested (every branch pinned, all mutants killed).</p>
169
+ <div class="meta">
170
+ <span class="m">Complexity <b>M</b></span>
171
+ <span class="m hi">Impact <b>High</b></span>
172
+ <span class="m">Wow <b class="wow">★★★</b></span>
173
+ <span class="m">three copies → one core module</span>
174
+ </div>
175
+ </article>
176
+
165
177
  <article class="cap rcard">
166
178
  <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Testing</span></div>
167
179
  <h3>Mutation hardening — <code>core/frontmatter.py</code></h3>
@@ -439,17 +451,6 @@
439
451
  </div>
440
452
  </article>
441
453
 
442
- <article class="cap rcard">
443
- <div class="head"><span class="pill next">Next</span><span class="cat">Correctness</span></div>
444
- <h3>Consolidate skill-staleness / drift logic into core</h3>
445
- <p>The "is this skill outdated" decision — <code>semver_gt</code> → commit compare → <code>sha256_dir</code> vs lock — is reimplemented nearly verbatim in <code>cmd_update</code>, <code>cmd_outdated</code> and <code>_drift_status</code> (<code>pkg.py:277 · taps.py:225 · quality.py:120</code>). Three copies of core business logic that <b>will drift apart</b>. Make it one <code>core</code> function the commands render.</p>
446
- <div class="meta">
447
- <span class="m">Complexity <b>M</b></span>
448
- <span class="m hi">Impact <b>High</b></span>
449
- <span class="m">Wow <b class="wow">★★★</b></span>
450
- </div>
451
- </article>
452
-
453
454
  <article class="cap rcard">
454
455
  <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Performance</span></div>
455
456
  <h3>Memoize <code>config.load()</code> in-process</h3>
@@ -528,7 +529,7 @@
528
529
  </article>
529
530
 
530
531
  <article class="cap rcard">
531
- <div class="head"><span class="pill planned">Planned</span><span class="cat">Performance</span></div>
532
+ <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Performance</span></div>
532
533
  <h3>Prune ignored dirs during <code>scan_dir</code> walk</h3>
533
534
  <p>Two full <code>rglob</code> passes descend <b>into</b> <code>.git</code>/<code>node_modules</code> and only filter afterward, and the skill-dir membership test is O(files × skill_dirs) (<code>catalog.py:106,119–123</code>). Switch to an <code>os.walk</code> that prunes ignored dirs in place; index skill dirs in a set.</p>
534
535
  <div class="meta">
@@ -239,6 +239,27 @@ class TestScanRulesAndWorkflows:
239
239
  assert [e["name"] for e in entries] == ["s"]
240
240
  assert entries[0]["kind"] == "skill"
241
241
 
242
+ def test_rule_entry_carries_tap_and_curated(self, tmp_path):
243
+ root = tmp_path / "tap"
244
+ (root / "nextjs").mkdir(parents=True)
245
+ (root / "nextjs" / ".cursorrules").write_text("Next.js expert.\n")
246
+ (e,) = catalog.scan_dir(root, "mytap", curated=True)
247
+ assert e["kind"] == "rule"
248
+ assert e["tap"] == "mytap"
249
+ assert e["curated"] is True
250
+
251
+ def test_ignored_dirs_pruned_for_loose_rules_and_workflows(self, tmp_path):
252
+ root = tmp_path / "tap"
253
+ (root / ".git").mkdir(parents=True)
254
+ (root / "sub" / "__pycache__").mkdir(parents=True)
255
+ (root / ".git" / "hook.mdc").write_text("---\ndescription: x\n---\n")
256
+ (root / "sub" / "__pycache__" / "commands").mkdir()
257
+ (root / "sub" / "__pycache__" / "commands" / "c.md").write_text(
258
+ "---\ndescription: y\n---\nx\n")
259
+ (root / "real").mkdir()
260
+ (root / "real" / ".cursorrules").write_text("real rule\n")
261
+ assert [e["name"] for e in catalog.scan_dir(root)] == ["real"]
262
+
242
263
  def test_unreadable_rule_file_skipped(self, tmp_path):
243
264
  root = tmp_path / "tap"
244
265
  (root / "a").mkdir(parents=True)
@@ -0,0 +1,102 @@
1
+ """Unit tests: boost_cli/core/staleness.py — the single-source staleness/drift
2
+ decision that cmd_update, cmd_outdated and _drift_status now render.
3
+
4
+ Both functions are pure, so every branch (and the ordering between branches) is
5
+ pinned here with assertions specific enough to kill mutants.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from boost_cli.core import staleness
10
+
11
+ SHA_A = "a" * 64
12
+ SHA_B = "b" * 64
13
+
14
+
15
+ class TestUpstreamReason:
16
+ def test_higher_semver_is_version(self):
17
+ assert staleness.upstream_reason(
18
+ "1.0.0", "1.1.0", "c1", "c2", SHA_A, SHA_B) == staleness.VERSION
19
+
20
+ def test_version_constant_value(self):
21
+ # pins the literal so a mutated constant is caught
22
+ assert staleness.VERSION == "version"
23
+
24
+ def test_content_constant_value(self):
25
+ assert staleness.CONTENT == "content"
26
+
27
+ def test_version_wins_even_when_commit_and_content_also_differ(self):
28
+ # semver must be checked FIRST — a higher version reports "version"
29
+ # regardless of the (also-changed) commit/content signals.
30
+ assert staleness.upstream_reason(
31
+ "1.0.0", "2.0.0", "old", "new", SHA_A, SHA_B) == staleness.VERSION
32
+
33
+ def test_same_version_no_commit_move_is_none(self):
34
+ assert staleness.upstream_reason(
35
+ "1.0.0", "1.0.0", "c1", "c1", SHA_A, SHA_B) is None
36
+
37
+ def test_empty_tap_head_is_none(self):
38
+ # no HEAD signal (e.g. tap not cloned) → not stale on the content axis
39
+ assert staleness.upstream_reason(
40
+ "1.0.0", "1.0.0", "c1", "", SHA_A, SHA_B) is None
41
+
42
+ def test_moved_commit_but_source_unhashable_is_none(self):
43
+ # source_sha None means "no content signal" — must NOT report content
44
+ assert staleness.upstream_reason(
45
+ "1.0.0", "1.0.0", "old", "new", SHA_A, None) is None
46
+
47
+ def test_moved_commit_same_content_is_none(self):
48
+ assert staleness.upstream_reason(
49
+ "1.0.0", "1.0.0", "old", "new", SHA_A, SHA_A) is None
50
+
51
+ def test_moved_commit_changed_content_is_content(self):
52
+ assert staleness.upstream_reason(
53
+ "1.0.0", "1.0.0", "old", "new", SHA_A, SHA_B) == staleness.CONTENT
54
+
55
+ def test_lower_advertised_version_not_stale_by_version(self):
56
+ # a downgrade in the catalog is not an upgrade — no commit move → None
57
+ assert staleness.upstream_reason(
58
+ "2.0.0", "1.0.0", "c1", "c1", SHA_A, SHA_B) is None
59
+
60
+
61
+ class TestDriftState:
62
+ def test_store_missing(self):
63
+ assert staleness.drift_state(
64
+ None, SHA_A, False, SHA_A) == staleness.STORE_MISSING
65
+
66
+ def test_store_missing_takes_precedence_over_local(self):
67
+ # a missing store is reported even for a local import
68
+ assert staleness.drift_state(
69
+ None, SHA_A, True, None) == staleness.STORE_MISSING
70
+
71
+ def test_local_edits(self):
72
+ assert staleness.drift_state(
73
+ SHA_B, SHA_A, False, SHA_A) == staleness.LOCAL_EDITS
74
+
75
+ def test_local_edits_take_precedence_over_na(self):
76
+ # edited store beats the local/upstream split even for local imports
77
+ assert staleness.drift_state(
78
+ SHA_B, SHA_A, True, None) == staleness.LOCAL_EDITS
79
+
80
+ def test_local_import_in_sync_is_na(self):
81
+ assert staleness.drift_state(
82
+ SHA_A, SHA_A, True, None) == staleness.NA
83
+
84
+ def test_source_missing(self):
85
+ assert staleness.drift_state(
86
+ SHA_A, SHA_A, False, None) == staleness.SOURCE_MISSING
87
+
88
+ def test_in_sync(self):
89
+ assert staleness.drift_state(
90
+ SHA_A, SHA_A, False, SHA_A) == staleness.IN_SYNC
91
+
92
+ def test_upstream_moved(self):
93
+ assert staleness.drift_state(
94
+ SHA_A, SHA_A, False, SHA_B) == staleness.UPSTREAM_MOVED
95
+
96
+ def test_status_constant_values(self):
97
+ assert staleness.IN_SYNC == "in-sync"
98
+ assert staleness.LOCAL_EDITS == "local-edits"
99
+ assert staleness.UPSTREAM_MOVED == "upstream-moved"
100
+ assert staleness.SOURCE_MISSING == "source-missing"
101
+ assert staleness.STORE_MISSING == "store-missing"
102
+ assert staleness.NA == "n/a"
@@ -1,16 +0,0 @@
1
- ---
2
- id: consolidate-skill-staleness-drift-logic-into-cor
3
- board: code
4
- section: internals
5
- status: next
6
- category: Correctness
7
- complexity: M
8
- impact: High
9
- wow: 3
10
- note:
11
- order: 3
12
- owner:
13
- pr:
14
- title: Consolidate skill-staleness / drift logic into core
15
- ---
16
- The "is this skill outdated" decision — <code>semver_gt</code> → commit compare → <code>sha256_dir</code> vs lock — is reimplemented nearly verbatim in <code>cmd_update</code>, <code>cmd_outdated</code> and <code>_drift_status</code> (<code>pkg.py:277 · taps.py:225 · quality.py:120</code>). Three copies of core business logic that <b>will drift apart</b>. Make it one <code>core</code> function the commands render.
File without changes