boost-skill-cli 1.0.239__tar.gz → 1.0.241__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.
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/PKG-INFO +1 -1
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/_version.py +2 -2
- boost_skill_cli-1.0.241/boost_cli/core/journal.py +114 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/util.py +54 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_skill_cli.egg-info/PKG-INFO +1 -1
- boost_skill_cli-1.0.241/docs/roadmap/items/journal-rotation-race-and-handle-leak.md +37 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap.html +17 -2
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_journal.py +112 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_util.py +67 -0
- boost_skill_cli-1.0.239/boost_cli/core/journal.py +0 -69
- boost_skill_cli-1.0.239/docs/roadmap/items/journal-rotation-race-and-handle-leak.md +0 -22
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.gitattributes +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.gitignore +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.gitleaks.toml +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.htmlvalidate.json +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.lighthouserc.json +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.lycheeignore +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.markdownlint-cli2.jsonc +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.pre-commit-config.yaml +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.stylelintrc.json +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.vale/styles/boost/Terminology.yml +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.vale/styles/config/vocabularies/boost/accept.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/.vale.ini +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/CLAUDE.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/CONTRIBUTING.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/LICENSE +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/MANIFEST.in +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/Makefile +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/README.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/SECURITY.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/__init__.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/__main__.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/cli.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/cliparse.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/__init__.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/_common.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/bmad.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/configuration.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/discovery.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/hooks.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/info.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/intelligence.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/pkg.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/quality.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/run.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/safety.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/taps.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/commands/team.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/__init__.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/adapters.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/agents.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/ai.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/capabilities.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/catalog.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/claude_settings.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/config.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/dense.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/ed25519.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/embed.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/faithfulness.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/frontmatter.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/gitutil.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/imperative.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/injectscan.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/integrity.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/lockfile.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/logs.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/mcp.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/mcpdecl.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/minisign.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/nethttp.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/output.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/paths.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/policy.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/projectlock.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/provenance.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/rag.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/registry.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/resolve.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/rules.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/scopes.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/secretscan.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/selfupdate.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/serve.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/stackprobe.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/staleness.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/store.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/trustaudit.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/typosquat.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/updatediff.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/core/workflows.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/data/registries.json +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/errors.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_cli/spin.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_skill_cli.egg-info/SOURCES.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_skill_cli.egg-info/entry_points.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_skill_cli.egg-info/requires.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/boost_skill_cli.egg-info/top_level.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/codecov.yml +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/.claude/settings.local.json +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/DEBUGGING.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/adapters.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/distill.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/doctor.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/explain.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/hooks.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/install.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/pulse.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/search.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/gifs/tap.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/distill.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/doctor.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/explain.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/hooks.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/install.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/pulse.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/search.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel/tapes/tap.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/carousel.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/commands.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/demo.gif +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/demo.tape +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/design-roadmap.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/eval.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/index.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/mcp-hub.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/rag-architecture.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D01.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D02.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D03.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D04.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D05.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D06.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D07.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D08.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D09.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D10.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D11.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D12.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D13.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D14.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D15.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D16.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D17.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D18.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D19.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D20.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D21.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D22.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D23.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D24.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D25.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/BOOST-D26.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/accessibility-audit-pa11y-ci-axe-core.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/adapter-conformance-langgraph-leg-never-ran.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/ai-bridge-silent-failure-logging.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/ambiguous-tap-short-name-resolution.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/atomic-corruption-safe-lock-file-writes.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/atomic-skill-install-temp-dir-swap.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/autonomous-ship-workflow-and-isolated-worktree.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/boost-run-live-agents.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/bring-commands-under-mutation-testing.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/broken-link-and-anchor-checking-lychee.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/browse-crashes-selecting-a-rule-or-workflow.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/build-provenance-slsa-attestations.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/cache-the-catalog-entry-set-across-rag-queries.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/capability-manifest-and-least-privilege-policy.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/ci-job-timeout-minutes.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/clean-env-install-smoke-pip-and-pipx.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/command-carousel-page.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/command-reference-docs-site.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/complexity-and-dead-code-radar-xenon-vulture.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/consolidate-skill-staleness-drift-logic-into-cor.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/coverage-dashboard-codecov-free-for-oss.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/coverage-guided-fuzzing-atheris-oss-fuzz.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/crash-listing-branch-untested.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/crash-recorder-error-paths-core-logs-py.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/dense-search-fallback-and-stale-tap-pruning.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/dependabot-missing-pip-ecosystem.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/dependency-cve-gate-pip-audit.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/docsite-chrome-and-content-audit.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/docstring-coverage-interrogate.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/doctor-checks-rules-and-workflows.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/end-of-options-guard-on-git-commands.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/extension-free-tests-core-dense-py.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/finish-mutation-hardening-across-core.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/fix-self-update-version-detection-dead-branch.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/fork-safe-network-proxy-handler.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/framework-adapter-langgraph.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/framework-adapter-multi-agent.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/framework-adapters-boost-adapt.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/frontmatter-scalar-over-coercion.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/github-community-health-files.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/github-pages-deploy-is-broken-every-push.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/golden-set-statistical-power.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/hash-pinned-reproducible-toolchain-uv-lock.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/html-validation-html-validate.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/install-from-path-bypasses-policy-and-pin-checks.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/install-resolves-skill-dependencies.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/install-scope-user-or-project.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/installed-skill-trust-audit.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/integrity-verification-boost-verify.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/invocation-pid-logging-for-crash-correlation.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/layering-guard-import-linter.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/license-compliance-scanning.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/lighthouse-ci-on-the-pages-site.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/lint-tap-misscopes-rule-workflow-entries.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/list-rules-and-workflows.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/localize-the-stored-bm25-snippet.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/lock-invariant-cannot-parse-extras-pins.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/lockfile-enforcement-and-commit-pinning.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/log-timestamps-mislabeled-utc.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/lowest-version-resolution-uv-resolution-lowest-d.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/main-has-no-branch-protection.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/make-the-roadmaps-discoverable.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/markdown-consistency-markdownlint-cli2.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mcp-aware-skills.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mcp-hub-diagram-node-text-overflow.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mcp-launch-objc-fork-safety.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mcp-register-names-server-before-env.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mcp-search-before-reinventing.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mcp-startup-self-harden-fork-safety.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/memoize-config-load-in-process.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/modernization-smells-refurb-pyupgrade.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mutation-hardening-core-frontmatter-py.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mutation-hardening-core-gitutil-py.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mutation-hardening-core-store-py.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/mypy-strict-mode-for-commands.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/negative-limit-inverts-log-pulse-output.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/onboard-overwrites-generated-files-without-confirm.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/one-command-every-env-nox.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/one-shared-atomic-write-helper.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/package-metadata-validation-twine-check-friends.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/patch-coverage-gate-diff-cover.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/path-traversal-unsanitized-rule-workflow-name.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/performance-regression-gate-pytest-benchmark.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/pin-the-lint-toolchain.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/post-deploy-smoke-headless-load-check.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/pre-release-python-canary-3-14t-free-threaded.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/project-scope-across-every-command.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/project-scope-symlink-escape-on-write.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/promote-nav-footer-into-the-shared-style-system.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/prompt-injection-scanning-of-skill-markdown.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/property-based-tests-hypothesis-on-the-parsers.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/prose-and-terminology-linting-vale.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/prune-ignored-dirs-during-scan-dir-walk.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/publish-gate-ignores-pip-audit-and-metadata.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/pytest-tmpdir-cve-blocked-by-py39-floor.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/quality-dashboard-sonarcloud-free-for-oss.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/ranx-significance-monitor.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/reconcile-the-theme-drift.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/refresh-the-marketing-surface.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/rel-time-tests-race-the-wall-clock.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/required-checks-can-declare-a-check-that-deadlocks-prs.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/required-checks-config-drift.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/retrieval-quality-eval-harness.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/reuse-helpers-kill-minor-dead-work.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/robust-tag-argument-parsing.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/ruff-016-widens-the-default-rule-set.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/rule-install-native-materialization.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/runner-egress-monitoring-stepsecurity-harden-run.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/runtime-explain-faithfulness-guardrail.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/sbom-aware-scanning-osv-scanner.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/sbom-on-every-release-cyclonedx-syft.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/scan-and-sync-rules-and-workflows.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/scorecard-findings-triage.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/second-type-checker-pyright.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/secret-and-pii-scanning-of-installed-skills.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/secret-scanning-gitleaks-push-protection.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/security-linting-bandit-via-ruff-s-rules.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/self-update-broken-for-pip-pipx-installs.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/serve-py-traversal-guards-untested.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/shift-left-gate-pre-commit-pre-commit-ci.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/single-imperative-rule-extractor.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/single-tech-stack-prober.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/split-oversized-command-modules.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/startup-and-import-time-budget-x-importtime.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/stop-re-serializing-entry-meta-on-every-search.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/structured-json-log-output-mode.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/supply-chain-posture-openssf-scorecard.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/surface-every-docs-page-from-the-guide.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/sync-deletes-unowned-broken-symlinks.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/tap-signing-and-provenance-sigstore-minisign.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/theme-asset-linting-stylelint-eslint.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/typo-detection-codespell.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/typosquat-and-name-confusion-detection.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/unify-tilde-two-copies-have-a-boundary-bug.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/uninstall-has-no-confirmation-prompt.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/untrack-generated-build-noise.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/update-diff-before-apply.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/update-refreshes-rules-and-workflows.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/update-reinstall-widens-agent-scope.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/visual-regression-pass-on-the-guide.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/widen-the-ruff-rule-surface-b-sim-c4-perf-ruf.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/windows-in-the-ci-matrix.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/wire-bdd-suite-into-ci.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/workflow-install-native-materialization.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/workflow-linting-actionlint.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/workflow-sast-zizmor.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/docs/roadmap/items/workspace-scope-install.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/eslint.config.mjs +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/examples/README.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/examples/adapt-demo.sh +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/examples/boost-run-prototype.sh +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/index.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/noxfile.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/pyproject.toml +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/coverage-tools.in +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/coverage-tools.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/lint-tools.in +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/lint-tools.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/mutation-tools.in +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/mutation-tools.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/release-tools.in +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/release-tools.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/test-tools.in +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/requirements/test-tools.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/a11y_check.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/bench_cli.sh +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/build_command_reference.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/build_registries.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/build_roadmap.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/check_anchors.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/check_required_checks.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/ensure_eval_corpus.sh +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/eval_explain.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/eval_recommend.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/eval_retrieval.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/eval_stats_summary.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/import_budget.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/lock_toolchain.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/mutation_gate.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/perf_gate.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/post_deploy_smoke.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/scripts/release_preflight.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/setup.cfg +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/sonar-project.properties +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/style/README.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/style/boost.css +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/style/boost.js +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/style/demo.html +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/adapt.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/browse.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/count.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/discover.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/doctor.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/environment.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/explain.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/index.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/install.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/list.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/mcp.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/search.feature +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/steps/cli_steps.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/steps/discovery_steps.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/steps/mcp_steps.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/bdd/features/steps/quality_steps.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/conftest.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/eval/baseline.json +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/eval/explain.jsonl +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/eval/golden.jsonl +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/eval/recommend.jsonl +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/eval/taps.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_capabilities_policy.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_adapt.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_bmad.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_configuration.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_discovery.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_hooks.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_info.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_intelligence.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_pkg.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_quality.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_taps.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_cli_team.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_everyday_loop.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_integrity_enforce.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_property_parsers.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_run.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_tap_signing.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/functional/test_workspace_scope.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/README.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/01-typical.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/02-lossy-version.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/03-lossy-numbers.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/04-lists.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/05-nested.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/06-empty-block.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/07-unterminated.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/08-none.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/09-unbalanced-quote.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/10-keywords.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/11-crlf.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/frontmatter/12-bom.md +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/01-shorthand.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/02-https.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/03-ssh.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/04-traversal.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/05-empty.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/06-space.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/07-slash.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/corpus/registry/08-bare-host.txt +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/fuzz_frontmatter.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/fuzz/fuzz_registry.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/make_fixture.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/perf/test_benchmarks.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/smoke.sh +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_a11y_check.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_adapters.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_adapters_multi.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_adapters_runner.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_agents.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_ai.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_capabilities.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_catalog.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_check_anchors.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_claude_settings.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_cliparse.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_command_reference_fresh.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_config.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_dense.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_dense_fallback.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_docs_pages_linked.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_docsite_chrome.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_ed25519.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_embed.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_errors_and_cli_table.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_eval_stats_summary.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_faithfulness.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_frontmatter.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_fuzz_targets.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_gitleaks_config.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_gitutil.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_imperative.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_import_budget.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_injectscan.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_integrity.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_lockfile.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_logs.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_marketing_counts.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_mcp.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_mcpdecl.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_minisign.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_mutation_hardening.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_nethttp.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_output.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_paths.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_perf_gate.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_pkg_confusions.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_pkg_injection.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_pkg_secrets.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_policy.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_post_deploy_smoke.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_projectlock.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_provenance.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_rag.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_registries_fresh.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_registry.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_release_preflight.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_required_checks.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_resolve.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_roadmap_fresh.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_rules.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_scopes.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_secretscan.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_selfupdate.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_serve.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_spin.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_stackprobe.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_staleness.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_store.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_token_parity.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_toolchain_lock.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_trustaudit.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_typosquat.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_updatediff.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_version.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/unit/test_workflows.py +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/visual/a11y_check.mjs +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/visual/console_check.mjs +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/visual/package.json +0 -0
- {boost_skill_cli-1.0.239 → boost_skill_cli-1.0.241}/tests/visual/visual_check.mjs +0 -0
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.241'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 241)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""The pulse journal: append-only feed of skill-management events.
|
|
2
|
+
|
|
3
|
+
Powers `boost pulse`, `boost trending`, `boost stats`, and `boost who`.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from . import paths, util
|
|
11
|
+
|
|
12
|
+
ROTATE_AT = 5000
|
|
13
|
+
ROTATE_KEEP = 2500
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def log(action: str, subject: str = "", **fields) -> None:
|
|
17
|
+
"""Append one event line to the pulse feed, rotating when oversized.
|
|
18
|
+
|
|
19
|
+
None-valued keyword fields are dropped from the record.
|
|
20
|
+
"""
|
|
21
|
+
paths.ensure_dirs()
|
|
22
|
+
event = {"ts": util.now_iso(), "user": util.user(), "action": action,
|
|
23
|
+
"subject": subject}
|
|
24
|
+
event.update({k: v for k, v in fields.items() if v is not None})
|
|
25
|
+
p = paths.pulse_path()
|
|
26
|
+
with p.open("a") as f:
|
|
27
|
+
f.write(json.dumps(event) + "\n")
|
|
28
|
+
_maybe_rotate()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def events(n: Optional[int] = None, action: Optional[str] = None,
|
|
32
|
+
subject: Optional[str] = None) -> List[dict]:
|
|
33
|
+
"""Most-recent-first list of journal events."""
|
|
34
|
+
p = paths.pulse_path()
|
|
35
|
+
if not p.exists():
|
|
36
|
+
return []
|
|
37
|
+
out = []
|
|
38
|
+
# errors="replace": one torn append must not make the whole feed
|
|
39
|
+
# unreadable — the corrupt line simply fails to parse as JSON below.
|
|
40
|
+
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
41
|
+
try:
|
|
42
|
+
e = json.loads(line)
|
|
43
|
+
except json.JSONDecodeError:
|
|
44
|
+
continue
|
|
45
|
+
if action and e.get("action") != action:
|
|
46
|
+
continue
|
|
47
|
+
if subject and e.get("subject") != subject:
|
|
48
|
+
continue
|
|
49
|
+
out.append(e)
|
|
50
|
+
out.reverse()
|
|
51
|
+
return out[:n] if n else out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _line_count(p) -> int:
|
|
55
|
+
"""Lines in `p`, closing the handle rather than leaving it to the GC.
|
|
56
|
+
|
|
57
|
+
Decodes leniently: the feed carries user-supplied text and a torn append
|
|
58
|
+
can leave a partial multi-byte sequence behind. A strict decode raises
|
|
59
|
+
UnicodeDecodeError, which is a ValueError — it would sail past the OSError
|
|
60
|
+
guard below and take down every command that logs, forever, over one bad
|
|
61
|
+
byte in an advisory feed.
|
|
62
|
+
"""
|
|
63
|
+
with p.open(encoding="utf-8", errors="replace") as f:
|
|
64
|
+
return sum(1 for _ in f)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def rotation_healthy() -> bool:
|
|
68
|
+
"""True when the pulse feed is absent or at most ROTATE_AT lines long."""
|
|
69
|
+
p = paths.pulse_path()
|
|
70
|
+
if not p.exists():
|
|
71
|
+
return True
|
|
72
|
+
return _line_count(p) <= ROTATE_AT
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _maybe_rotate() -> None:
|
|
76
|
+
"""Trim the feed to ROTATE_KEEP lines once it passes ROTATE_AT.
|
|
77
|
+
|
|
78
|
+
Rotation is a read-modify-write of the whole file, and this repo's own
|
|
79
|
+
working model expects concurrent boost processes. Unserialized, two of
|
|
80
|
+
them could each read the feed, each build a snapshot, and the second
|
|
81
|
+
write would silently drop whatever the first had appended in between.
|
|
82
|
+
|
|
83
|
+
So rotation runs under a lock, and a process that cannot take it simply
|
|
84
|
+
returns: another one is already trimming, and doing it twice adds nothing.
|
|
85
|
+
Inside the lock the file is re-read — the count that got us here was taken
|
|
86
|
+
outside it and may already be stale — and anything appended between that
|
|
87
|
+
read and the swap is carried into the new file rather than dropped.
|
|
88
|
+
`log()` itself stays lock-free: O_APPEND writes of a single short record
|
|
89
|
+
do not tear, and making every command contend on a lock to protect an
|
|
90
|
+
advisory feed would be a bad trade.
|
|
91
|
+
"""
|
|
92
|
+
p = paths.pulse_path()
|
|
93
|
+
try:
|
|
94
|
+
if _line_count(p) <= ROTATE_AT:
|
|
95
|
+
return
|
|
96
|
+
except OSError:
|
|
97
|
+
return
|
|
98
|
+
with util.try_lock(p.with_name(p.name + ".rotate.lock")) as locked:
|
|
99
|
+
if not locked:
|
|
100
|
+
return
|
|
101
|
+
try:
|
|
102
|
+
raw = p.read_bytes()
|
|
103
|
+
lines = raw.decode("utf-8", "replace").splitlines()
|
|
104
|
+
if len(lines) <= ROTATE_AT:
|
|
105
|
+
return # someone else already rotated
|
|
106
|
+
keep = lines[-ROTATE_KEEP:]
|
|
107
|
+
# Re-read past the point we stopped, so an append that landed
|
|
108
|
+
# while we were building the snapshot survives it.
|
|
109
|
+
with p.open("rb") as f:
|
|
110
|
+
f.seek(len(raw))
|
|
111
|
+
keep += f.read().decode("utf-8", "replace").splitlines()
|
|
112
|
+
except OSError:
|
|
113
|
+
return
|
|
114
|
+
util.atomic_write_text(p, "\n".join(keep) + "\n")
|
|
@@ -10,6 +10,7 @@ import shutil
|
|
|
10
10
|
import stat
|
|
11
11
|
import sys
|
|
12
12
|
import tempfile
|
|
13
|
+
import time
|
|
13
14
|
from datetime import datetime, timezone
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import List, Tuple
|
|
@@ -37,6 +38,59 @@ def rmtree(path) -> None:
|
|
|
37
38
|
shutil.rmtree(str(path), onerror=_clear_readonly_and_retry)
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def _lock_is_stale(path: Path, stale_after: float) -> bool:
|
|
42
|
+
"""True when a lock file is old enough to have been abandoned."""
|
|
43
|
+
try:
|
|
44
|
+
return (time.time() - path.stat().st_mtime) > stale_after
|
|
45
|
+
except OSError:
|
|
46
|
+
return True # it vanished under us — treat it as free
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@contextlib.contextmanager
|
|
50
|
+
def try_lock(path, stale_after: float = 300.0):
|
|
51
|
+
"""Best-effort exclusive lock, yielding whether it was actually taken.
|
|
52
|
+
|
|
53
|
+
An `O_CREAT | O_EXCL` create is atomic on every filesystem boost targets,
|
|
54
|
+
and needs neither `fcntl` (missing on Windows) nor `msvcrt` (missing
|
|
55
|
+
everywhere else). Yields True to the holder and **False** to everyone
|
|
56
|
+
else — this does not wait, because its callers are jobs where "another
|
|
57
|
+
process is already doing it" is a perfectly good outcome, and blocking
|
|
58
|
+
would trade a rare lost update for a common stall.
|
|
59
|
+
|
|
60
|
+
A lock file older than `stale_after` is stolen: a process killed
|
|
61
|
+
mid-operation must not wedge the operation forever. Any error acquiring
|
|
62
|
+
it yields False rather than raising — failing to take a lock must never
|
|
63
|
+
be worse than the problem it guards.
|
|
64
|
+
"""
|
|
65
|
+
path = Path(path)
|
|
66
|
+
fd = None
|
|
67
|
+
try:
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
try:
|
|
70
|
+
fd = os.open(str(path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
71
|
+
except FileExistsError:
|
|
72
|
+
if not _lock_is_stale(path, stale_after):
|
|
73
|
+
yield False
|
|
74
|
+
return
|
|
75
|
+
with contextlib.suppress(OSError):
|
|
76
|
+
path.unlink()
|
|
77
|
+
fd = os.open(str(path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
78
|
+
except OSError:
|
|
79
|
+
yield False
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
with contextlib.suppress(OSError), os.fdopen(fd, "w") as f:
|
|
83
|
+
fd = None # fdopen owns it now
|
|
84
|
+
f.write(str(os.getpid()))
|
|
85
|
+
yield True
|
|
86
|
+
finally:
|
|
87
|
+
if fd is not None:
|
|
88
|
+
with contextlib.suppress(OSError):
|
|
89
|
+
os.close(fd)
|
|
90
|
+
with contextlib.suppress(OSError):
|
|
91
|
+
path.unlink()
|
|
92
|
+
|
|
93
|
+
|
|
40
94
|
def atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> None:
|
|
41
95
|
"""Write ``text`` to ``path`` so a reader never sees a partial file.
|
|
42
96
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: journal-rotation-race-and-handle-leak
|
|
3
|
+
board: code
|
|
4
|
+
section: internals
|
|
5
|
+
status: shipped
|
|
6
|
+
category: Concurrency · Bug
|
|
7
|
+
complexity: M
|
|
8
|
+
impact: Med
|
|
9
|
+
wow: 2
|
|
10
|
+
note:
|
|
11
|
+
order: 31
|
|
12
|
+
owner: loop/journal-rotation
|
|
13
|
+
pr: 275
|
|
14
|
+
title: Journal rotation has a lost-update race between concurrent processes
|
|
15
|
+
---
|
|
16
|
+
<code>_maybe_rotate</code> reads the whole pulse file and atomically replaces it with a truncated
|
|
17
|
+
snapshot — but two concurrent boost processes (explicitly expected per this repo's parallel-loop
|
|
18
|
+
model) can both read, then both write their own snapshot, and whichever writes last silently
|
|
19
|
+
discards any event the other appended in between. <code>rotation_healthy()</code> also opens the
|
|
20
|
+
file with a bare <code>p.open()</code> and never closes it, relying on GC instead of a
|
|
21
|
+
<code>with</code> block like every other open in the module. Rotate under a lock (or an
|
|
22
|
+
append-only rename scheme), and close the handle explicitly.
|
|
23
|
+
Shipped with <code>util.try_lock()</code>, a portable <code>O_CREAT | O_EXCL</code> advisory
|
|
24
|
+
lock that needs neither <code>fcntl</code> (absent on Windows) nor <code>msvcrt</code> (absent
|
|
25
|
+
everywhere else). It <em>yields False rather than waiting</em>: a process that cannot take it
|
|
26
|
+
just returns, because another one is already trimming and blocking would trade a rare lost
|
|
27
|
+
update for a common stall. A lock older than five minutes is stolen, so a process killed
|
|
28
|
+
mid-rotation cannot wedge the feed forever. Inside the lock the file is re-read (the count
|
|
29
|
+
that got us there was taken outside it) and anything appended since is carried into the new
|
|
30
|
+
file instead of dropped. <code>log()</code> stays lock-free — <code>O_APPEND</code> writes of
|
|
31
|
+
one short record do not tear, and making every command contend on a lock to protect an
|
|
32
|
+
advisory feed would be the wrong trade.
|
|
33
|
+
Writing the test for a torn append turned up a third defect neither the card nor the code
|
|
34
|
+
had noticed: a single invalid byte in the feed raised <code>UnicodeDecodeError</code>, which
|
|
35
|
+
is a <code>ValueError</code> and sails straight past the <code>except OSError</code> guard —
|
|
36
|
+
so one bad byte crashed <em>every command that logs</em>, permanently. Both reads decode with
|
|
37
|
+
<code>errors="replace"</code> now.
|
|
@@ -1014,7 +1014,7 @@ live hits," and prune removed taps on every build, not just changed ones.</p>
|
|
|
1014
1014
|
</article>
|
|
1015
1015
|
|
|
1016
1016
|
<article class="cap rcard" id="journal-rotation-race-and-handle-leak">
|
|
1017
|
-
<div class="head"><span class="pill
|
|
1017
|
+
<div class="head"><span class="pill shipped">Shipped</span><span class="cat">Concurrency · Bug</span></div>
|
|
1018
1018
|
<h3>Journal rotation has a lost-update race between concurrent processes</h3>
|
|
1019
1019
|
<p><code>_maybe_rotate</code> reads the whole pulse file and atomically replaces it with a truncated
|
|
1020
1020
|
snapshot — but two concurrent boost processes (explicitly expected per this repo's parallel-loop
|
|
@@ -1022,7 +1022,22 @@ model) can both read, then both write their own snapshot, and whichever writes l
|
|
|
1022
1022
|
discards any event the other appended in between. <code>rotation_healthy()</code> also opens the
|
|
1023
1023
|
file with a bare <code>p.open()</code> and never closes it, relying on GC instead of a
|
|
1024
1024
|
<code>with</code> block like every other open in the module. Rotate under a lock (or an
|
|
1025
|
-
append-only rename scheme), and close the handle explicitly
|
|
1025
|
+
append-only rename scheme), and close the handle explicitly.
|
|
1026
|
+
Shipped with <code>util.try_lock()</code>, a portable <code>O_CREAT | O_EXCL</code> advisory
|
|
1027
|
+
lock that needs neither <code>fcntl</code> (absent on Windows) nor <code>msvcrt</code> (absent
|
|
1028
|
+
everywhere else). It <em>yields False rather than waiting</em>: a process that cannot take it
|
|
1029
|
+
just returns, because another one is already trimming and blocking would trade a rare lost
|
|
1030
|
+
update for a common stall. A lock older than five minutes is stolen, so a process killed
|
|
1031
|
+
mid-rotation cannot wedge the feed forever. Inside the lock the file is re-read (the count
|
|
1032
|
+
that got us there was taken outside it) and anything appended since is carried into the new
|
|
1033
|
+
file instead of dropped. <code>log()</code> stays lock-free — <code>O_APPEND</code> writes of
|
|
1034
|
+
one short record do not tear, and making every command contend on a lock to protect an
|
|
1035
|
+
advisory feed would be the wrong trade.
|
|
1036
|
+
Writing the test for a torn append turned up a third defect neither the card nor the code
|
|
1037
|
+
had noticed: a single invalid byte in the feed raised <code>UnicodeDecodeError</code>, which
|
|
1038
|
+
is a <code>ValueError</code> and sails straight past the <code>except OSError</code> guard —
|
|
1039
|
+
so one bad byte crashed <em>every command that logs</em>, permanently. Both reads decode with
|
|
1040
|
+
<code>errors="replace"</code> now.</p>
|
|
1026
1041
|
<div class="meta">
|
|
1027
1042
|
<span class="m">Complexity <b>M</b></span>
|
|
1028
1043
|
<span class="m">Impact <b>Med</b></span>
|
|
@@ -117,6 +117,118 @@ class TestRotation:
|
|
|
117
117
|
assert first["subject"] == "s2502"
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
class TestRotationConcurrency:
|
|
121
|
+
"""Rotation is a read-modify-write, and this repo expects concurrent
|
|
122
|
+
boost processes. Every assertion here is about not losing an event."""
|
|
123
|
+
|
|
124
|
+
def _lock_path(self):
|
|
125
|
+
p = paths.pulse_path()
|
|
126
|
+
return p.with_name(p.name + ".rotate.lock")
|
|
127
|
+
|
|
128
|
+
def test_a_held_lock_skips_rotation_entirely(self, sandbox):
|
|
129
|
+
# Another process is already trimming. Doing it a second time from a
|
|
130
|
+
# stale read is exactly how an event goes missing.
|
|
131
|
+
write_lines(5001)
|
|
132
|
+
paths.ensure_dirs()
|
|
133
|
+
self._lock_path().write_text("999999", encoding="utf-8")
|
|
134
|
+
journal._maybe_rotate()
|
|
135
|
+
assert len(paths.pulse_path().read_text(
|
|
136
|
+
encoding="utf-8").splitlines()) == 5001
|
|
137
|
+
|
|
138
|
+
def test_the_lock_is_released_after_rotating(self, sandbox):
|
|
139
|
+
write_lines(5001)
|
|
140
|
+
journal._maybe_rotate()
|
|
141
|
+
assert not self._lock_path().exists()
|
|
142
|
+
assert len(paths.pulse_path().read_text(
|
|
143
|
+
encoding="utf-8").splitlines()) == journal.ROTATE_KEEP
|
|
144
|
+
|
|
145
|
+
def test_an_append_during_the_snapshot_survives_it(self, sandbox,
|
|
146
|
+
monkeypatch):
|
|
147
|
+
# The lost update: a concurrent log() lands after rotation has read
|
|
148
|
+
# the feed but before it swaps the trimmed file into place. The old
|
|
149
|
+
# code wrote its stale snapshot over the top and the event vanished.
|
|
150
|
+
write_lines(5001)
|
|
151
|
+
p = paths.pulse_path()
|
|
152
|
+
real_read_bytes = type(p).read_bytes
|
|
153
|
+
|
|
154
|
+
def read_then_someone_appends(self, *a, **kw):
|
|
155
|
+
# Scoped to the pulse file: this patches a method every Path in
|
|
156
|
+
# the process shares, so an unscoped version hands some unrelated
|
|
157
|
+
# read the feed's bytes and appends to the feed behind its back.
|
|
158
|
+
data = real_read_bytes(self, *a, **kw)
|
|
159
|
+
if self == p:
|
|
160
|
+
with p.open("a", encoding="utf-8") as f:
|
|
161
|
+
f.write(json.dumps({"ts": "2026-01-01T00:00:00Z",
|
|
162
|
+
"user": "u", "action": "install",
|
|
163
|
+
"subject": "raced-in"}) + "\n")
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
monkeypatch.setattr(type(p), "read_bytes", read_then_someone_appends)
|
|
167
|
+
journal._maybe_rotate()
|
|
168
|
+
subjects = [json.loads(line)["subject"] for line
|
|
169
|
+
in p.read_text(encoding="utf-8").splitlines()]
|
|
170
|
+
assert "raced-in" in subjects
|
|
171
|
+
assert subjects[-1] == "raced-in" # and in the right order
|
|
172
|
+
|
|
173
|
+
def test_a_second_rotation_inside_the_lock_is_a_no_op(self, sandbox,
|
|
174
|
+
monkeypatch):
|
|
175
|
+
# The count that gets us into the lock is taken outside it. If another
|
|
176
|
+
# process rotated in between, the re-read must notice and leave the
|
|
177
|
+
# already-trimmed file alone rather than trim it again.
|
|
178
|
+
write_lines(5001)
|
|
179
|
+
p = paths.pulse_path()
|
|
180
|
+
real_read_bytes = type(p).read_bytes
|
|
181
|
+
|
|
182
|
+
def rotated_by_someone_else(self, *a, **kw):
|
|
183
|
+
if self == p:
|
|
184
|
+
# Exactly at the threshold, the boundary that decides it:
|
|
185
|
+
# ROTATE_AT lines is healthy, ROTATE_AT + 1 is not.
|
|
186
|
+
write_lines(journal.ROTATE_AT, action="fresh")
|
|
187
|
+
return real_read_bytes(self, *a, **kw)
|
|
188
|
+
|
|
189
|
+
monkeypatch.setattr(journal, "_line_count", lambda _p: 5001)
|
|
190
|
+
monkeypatch.setattr(type(p), "read_bytes", rotated_by_someone_else)
|
|
191
|
+
journal._maybe_rotate()
|
|
192
|
+
assert len(p.read_text(encoding="utf-8").splitlines()) == journal.ROTATE_AT
|
|
193
|
+
|
|
194
|
+
def test_undecodable_bytes_do_not_abort_rotation(self, sandbox):
|
|
195
|
+
# The feed carries user-supplied text (skill names, tap names), and a
|
|
196
|
+
# torn append can leave a partial multi-byte sequence behind. Rotation
|
|
197
|
+
# must trim the file anyway rather than raise and let it grow forever.
|
|
198
|
+
write_lines(5001)
|
|
199
|
+
p = paths.pulse_path()
|
|
200
|
+
with p.open("ab") as f:
|
|
201
|
+
f.write(b'{"ts": "2026-01-01T00:00:00Z", "user": "u", '
|
|
202
|
+
b'"action": "install", "subject": "caf\xe9"}\n')
|
|
203
|
+
journal._maybe_rotate()
|
|
204
|
+
lines = p.read_text(encoding="utf-8").splitlines()
|
|
205
|
+
assert len(lines) == journal.ROTATE_KEEP
|
|
206
|
+
assert "caf" in lines[-1] # kept, with the bad byte replaced
|
|
207
|
+
|
|
208
|
+
def test_rotation_healthy_closes_its_handle(self, sandbox, monkeypatch):
|
|
209
|
+
# It used to be a bare `p.open()` left to the garbage collector — the
|
|
210
|
+
# one open in the module without a `with`. Asserted by holding a
|
|
211
|
+
# reference to every handle it opens rather than by watching for a
|
|
212
|
+
# ResourceWarning: that warning is raised during finalization, where
|
|
213
|
+
# pytest reports it as unraisable instead of failing the test, so it
|
|
214
|
+
# would not have caught this.
|
|
215
|
+
write_lines(3)
|
|
216
|
+
opened = []
|
|
217
|
+
path_type = type(paths.pulse_path())
|
|
218
|
+
real_open = path_type.open
|
|
219
|
+
|
|
220
|
+
def tracking_open(self, *a, **kw):
|
|
221
|
+
handle = real_open(self, *a, **kw)
|
|
222
|
+
if self == paths.pulse_path(): # only the feed's handles are ours
|
|
223
|
+
opened.append(handle)
|
|
224
|
+
return handle
|
|
225
|
+
|
|
226
|
+
monkeypatch.setattr(path_type, "open", tracking_open)
|
|
227
|
+
assert journal.rotation_healthy() is True
|
|
228
|
+
assert opened, "expected rotation_healthy to read the feed"
|
|
229
|
+
assert all(handle.closed for handle in opened)
|
|
230
|
+
|
|
231
|
+
|
|
120
232
|
class TestFallbacks:
|
|
121
233
|
def test_user_falls_back_to_unknown(self, sandbox, monkeypatch):
|
|
122
234
|
def no_user():
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import re
|
|
6
|
+
import time
|
|
6
7
|
from datetime import datetime, timedelta, timezone
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
@@ -533,6 +534,72 @@ class TestAtomicWriteText:
|
|
|
533
534
|
assert leftovers == []
|
|
534
535
|
|
|
535
536
|
|
|
537
|
+
class TestTryLock:
|
|
538
|
+
"""A lock that silently fails open is worse than no lock at all, so both
|
|
539
|
+
answers are pinned: who gets True, who gets False, and what is left behind."""
|
|
540
|
+
|
|
541
|
+
def test_taken_lock_yields_true_and_cleans_up(self, tmp_path):
|
|
542
|
+
lock = tmp_path / "x.lock"
|
|
543
|
+
with util.try_lock(lock) as got:
|
|
544
|
+
assert got is True
|
|
545
|
+
assert lock.is_file()
|
|
546
|
+
assert not lock.exists()
|
|
547
|
+
|
|
548
|
+
def test_second_holder_is_refused_rather_than_queued(self, tmp_path):
|
|
549
|
+
lock = tmp_path / "x.lock"
|
|
550
|
+
with util.try_lock(lock) as first, util.try_lock(lock) as second:
|
|
551
|
+
assert first is True
|
|
552
|
+
assert second is False
|
|
553
|
+
|
|
554
|
+
def test_the_loser_does_not_delete_the_winners_lock(self, tmp_path):
|
|
555
|
+
# The nastiest failure mode: a refused caller cleaning up on its way
|
|
556
|
+
# out would hand the lock to whoever asked next, while the holder is
|
|
557
|
+
# still working.
|
|
558
|
+
lock = tmp_path / "x.lock"
|
|
559
|
+
with util.try_lock(lock):
|
|
560
|
+
with util.try_lock(lock) as second:
|
|
561
|
+
assert second is False
|
|
562
|
+
assert lock.is_file()
|
|
563
|
+
|
|
564
|
+
def test_lock_is_released_when_the_body_raises(self, tmp_path):
|
|
565
|
+
lock = tmp_path / "x.lock"
|
|
566
|
+
with pytest.raises(ValueError), util.try_lock(lock):
|
|
567
|
+
raise ValueError("boom")
|
|
568
|
+
assert not lock.exists()
|
|
569
|
+
|
|
570
|
+
def test_stale_lock_is_stolen(self, tmp_path):
|
|
571
|
+
import os as _os
|
|
572
|
+
lock = tmp_path / "x.lock"
|
|
573
|
+
lock.write_text("999999", encoding="utf-8")
|
|
574
|
+
old = time.time() - 600
|
|
575
|
+
_os.utime(lock, (old, old))
|
|
576
|
+
with util.try_lock(lock, stale_after=300.0) as got:
|
|
577
|
+
assert got is True # the holder died; don't wedge forever
|
|
578
|
+
|
|
579
|
+
def test_fresh_lock_is_not_stolen(self, tmp_path):
|
|
580
|
+
lock = tmp_path / "x.lock"
|
|
581
|
+
lock.write_text("999999", encoding="utf-8")
|
|
582
|
+
with util.try_lock(lock, stale_after=300.0) as got:
|
|
583
|
+
assert got is False
|
|
584
|
+
assert lock.is_file() # and it is left for its owner
|
|
585
|
+
|
|
586
|
+
def test_a_vanished_lock_reads_as_free(self, tmp_path):
|
|
587
|
+
assert util._lock_is_stale(tmp_path / "never-existed", 300.0) is True
|
|
588
|
+
|
|
589
|
+
def test_unusable_path_refuses_instead_of_raising(self, tmp_path):
|
|
590
|
+
# Failing to take a lock must never be worse than the race it guards.
|
|
591
|
+
blocker = tmp_path / "not-a-dir"
|
|
592
|
+
blocker.write_text("x", encoding="utf-8")
|
|
593
|
+
with util.try_lock(blocker / "sub" / "x.lock") as got:
|
|
594
|
+
assert got is False
|
|
595
|
+
|
|
596
|
+
def test_the_holder_records_its_pid(self, tmp_path):
|
|
597
|
+
import os as _os
|
|
598
|
+
lock = tmp_path / "x.lock"
|
|
599
|
+
with util.try_lock(lock):
|
|
600
|
+
assert lock.read_text(encoding="utf-8") == str(_os.getpid())
|
|
601
|
+
|
|
602
|
+
|
|
536
603
|
class TestSafeComponent:
|
|
537
604
|
"""A catalog name becomes a path component, so it is attacker-controlled
|
|
538
605
|
input on the way to an install path (a tap writes its own frontmatter)."""
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""The pulse journal: append-only feed of skill-management events.
|
|
2
|
-
|
|
3
|
-
Powers `boost pulse`, `boost trending`, `boost stats`, and `boost who`.
|
|
4
|
-
"""
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
from typing import List, Optional
|
|
9
|
-
|
|
10
|
-
from . import paths, util
|
|
11
|
-
|
|
12
|
-
ROTATE_AT = 5000
|
|
13
|
-
ROTATE_KEEP = 2500
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def log(action: str, subject: str = "", **fields) -> None:
|
|
17
|
-
"""Append one event line to the pulse feed, rotating when oversized.
|
|
18
|
-
|
|
19
|
-
None-valued keyword fields are dropped from the record.
|
|
20
|
-
"""
|
|
21
|
-
paths.ensure_dirs()
|
|
22
|
-
event = {"ts": util.now_iso(), "user": util.user(), "action": action,
|
|
23
|
-
"subject": subject}
|
|
24
|
-
event.update({k: v for k, v in fields.items() if v is not None})
|
|
25
|
-
p = paths.pulse_path()
|
|
26
|
-
with p.open("a") as f:
|
|
27
|
-
f.write(json.dumps(event) + "\n")
|
|
28
|
-
_maybe_rotate()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def events(n: Optional[int] = None, action: Optional[str] = None,
|
|
32
|
-
subject: Optional[str] = None) -> List[dict]:
|
|
33
|
-
"""Most-recent-first list of journal events."""
|
|
34
|
-
p = paths.pulse_path()
|
|
35
|
-
if not p.exists():
|
|
36
|
-
return []
|
|
37
|
-
out = []
|
|
38
|
-
for line in p.read_text(encoding="utf-8").splitlines():
|
|
39
|
-
try:
|
|
40
|
-
e = json.loads(line)
|
|
41
|
-
except json.JSONDecodeError:
|
|
42
|
-
continue
|
|
43
|
-
if action and e.get("action") != action:
|
|
44
|
-
continue
|
|
45
|
-
if subject and e.get("subject") != subject:
|
|
46
|
-
continue
|
|
47
|
-
out.append(e)
|
|
48
|
-
out.reverse()
|
|
49
|
-
return out[:n] if n else out
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def rotation_healthy() -> bool:
|
|
53
|
-
"""True when the pulse feed is absent or at most ROTATE_AT lines long."""
|
|
54
|
-
p = paths.pulse_path()
|
|
55
|
-
if not p.exists():
|
|
56
|
-
return True
|
|
57
|
-
return sum(1 for _ in p.open()) <= ROTATE_AT
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _maybe_rotate() -> None:
|
|
61
|
-
p = paths.pulse_path()
|
|
62
|
-
try:
|
|
63
|
-
lines = p.read_text(encoding="utf-8").splitlines()
|
|
64
|
-
except OSError:
|
|
65
|
-
return
|
|
66
|
-
if len(lines) > ROTATE_AT:
|
|
67
|
-
# Atomic swap: a bare write_text here races concurrent log() appends and
|
|
68
|
-
# can truncate the feed mid-line if interrupted.
|
|
69
|
-
util.atomic_write_text(p, "\n".join(lines[-ROTATE_KEEP:]) + "\n")
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: journal-rotation-race-and-handle-leak
|
|
3
|
-
board: code
|
|
4
|
-
section: internals
|
|
5
|
-
status: planned
|
|
6
|
-
category: Concurrency · Bug
|
|
7
|
-
complexity: M
|
|
8
|
-
impact: Med
|
|
9
|
-
wow: 2
|
|
10
|
-
note:
|
|
11
|
-
order: 31
|
|
12
|
-
owner:
|
|
13
|
-
pr:
|
|
14
|
-
title: Journal rotation has a lost-update race between concurrent processes
|
|
15
|
-
---
|
|
16
|
-
<code>_maybe_rotate</code> reads the whole pulse file and atomically replaces it with a truncated
|
|
17
|
-
snapshot — but two concurrent boost processes (explicitly expected per this repo's parallel-loop
|
|
18
|
-
model) can both read, then both write their own snapshot, and whichever writes last silently
|
|
19
|
-
discards any event the other appended in between. <code>rotation_healthy()</code> also opens the
|
|
20
|
-
file with a bare <code>p.open()</code> and never closes it, relying on GC instead of a
|
|
21
|
-
<code>with</code> block like every other open in the module. Rotate under a lock (or an
|
|
22
|
-
append-only rename scheme), and close the handle explicitly.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|