diff --git a/.github/vstack.json b/.github/vstack.json index 33f5ce7..947c705 100644 --- a/.github/vstack.json +++ b/.github/vstack.json @@ -1,139 +1,139 @@ { - "vstack_version": "0.1.0", - "installed_at": "2026-04-16T22:29:03.096690+00:00", + "vstack_version": "0.0.0", + "installed_at": "2026-04-16T23:34:10.546914+00:00", "artifacts": { "skills": [ { "name": "adr", "file": "skills/adr/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "analyse", "file": "skills/analyse/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "architecture", "file": "skills/architecture/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "cicd", "file": "skills/cicd/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "code-review", "file": "skills/code-review/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "consult", "file": "skills/consult/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "container", "file": "skills/container/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "debug", "file": "skills/debug/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "design", "file": "skills/design/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "docs", "file": "skills/docs/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "explore", "file": "skills/explore/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "guardrails", "file": "skills/guardrails/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "inspect", "file": "skills/inspect/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "performance", "file": "skills/performance/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "pr", "file": "skills/pr/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "release-notes", "file": "skills/release-notes/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "requirements", "file": "skills/requirements/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "security", "file": "skills/security/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "verify", "file": "skills/verify/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "vision", "file": "skills/vision/SKILL.md", - "version": "1.0.0" + "version": "1.0.1" } ], "agents": [ { "name": "architect", "file": "agents/architect.agent.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "designer", "file": "agents/designer.agent.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "engineer", "file": "agents/engineer.agent.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "product", "file": "agents/product.agent.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "release", "file": "agents/release.agent.md", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "tester", "file": "agents/tester.agent.md", - "version": "1.0.0" + "version": "1.0.1" } ], "instructions": [ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index abb2a79..6776d4b 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -1,5 +1,6 @@ # Quality gate for branch pushes (excluding main). -# Purpose: keep fast feedback for formatting, linting, and type checking. +# Purpose: keep fast feedback for formatting/lint/type checks and run tests across +# supported Python versions. name: QA on: @@ -8,30 +9,29 @@ on: branches-ignore: [main] env: - # Single Python version used across all QA checks. PYTHON_VERSION: "3.11" + POETRY_VIRTUALENVS_IN_PROJECT: "true" jobs: - qa: - # One consolidated job for style and static-analysis checks. + quality: + # Fast quality checks run once on the baseline Python version. name: Format Lint Typecheck runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 + + - name: Install Poetry + run: pipx install poetry - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - # Cache Poetry downloads based on lockfile changes. python-version: ${{ env.PYTHON_VERSION }} cache: poetry cache-dependency-path: poetry.lock - - name: Install Poetry - run: pipx install poetry - - name: Install dependencies run: poetry install --no-interaction --no-ansi @@ -43,3 +43,31 @@ jobs: - name: Typecheck run: make typecheck + + test-matrix: + name: Tests (py${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Poetry + run: pipx install poetry + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + cache: poetry + cache-dependency-path: poetry.lock + + - name: Install dependencies + run: poetry install --no-interaction --no-ansi + + - name: Test + run: make test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b4c244..6d49b83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: # Full history is required by semantic-version to inspect commit history. fetch-depth: 0 @@ -96,12 +96,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.merge_commit_sha }} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: poetry diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index bb24e7a..072349b 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -19,18 +19,18 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 + + - name: Install Poetry + run: pipx install poetry - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: poetry cache-dependency-path: poetry.lock - - name: Install Poetry - run: pipx install poetry - - name: Install pip-audit run: pipx install pip-audit @@ -47,4 +47,4 @@ jobs: with: path: ./ base: ${{ github.event.pull_request.base.sha }} - head: ${{ github.event.pull_request.head.sha }} \ No newline at end of file + head: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 2e299d3..f9a0594 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -10,6 +10,7 @@ name: Verify env: # Shared interpreter version for reproducible CI behavior. PYTHON_VERSION: "3.11" + POETRY_VIRTUALENVS_IN_PROJECT: "true" jobs: unit-tests: @@ -19,18 +20,18 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 + + - name: Install Poetry + run: pipx install poetry - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: poetry cache-dependency-path: poetry.lock - - name: Install Poetry - run: pipx install poetry - - name: Install dependencies run: poetry install --no-interaction --no-ansi @@ -44,18 +45,18 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 + + - name: Install Poetry + run: pipx install poetry - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: poetry cache-dependency-path: poetry.lock - - name: Install Poetry - run: pipx install poetry - - name: Install dependencies run: poetry install --no-interaction --no-ansi @@ -63,8 +64,11 @@ jobs: # Avoid cross-run collisions on shared/self-hosted runners. run: echo "TARGET_DIR=${RUNNER_TEMP}/vstack-pr-target-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_ENV" + - name: Create temp target directory + run: mkdir -p "${TARGET_DIR}" + - name: Install artifacts into temp target run: poetry run vstack install --target "${TARGET_DIR}" - name: Verify source and installed output - run: poetry run vstack verify --target "${TARGET_DIR}" \ No newline at end of file + run: poetry run vstack verify --target "${TARGET_DIR}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83dd15a..a13c091 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,19 @@ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.4 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + - repo: https://github.com/executablebooks/mdformat rev: 0.7.22 hooks: diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..ef3e413 --- /dev/null +++ b/.python-version @@ -0,0 +1,4 @@ +3.14.3 +3.13.12 +3.12.12 +3.11.14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fd39c..a902763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ - No unreleased changes yet. +## 1.0.1 — 2026-04-17 + +Tooling and release hygiene update focused on making local and CI verification match. + +### Added in 1.0.1 + +- Repo-local `.python-version` for consistent pyenv interpreter resolution. +- Tox-based multi-version test execution across Python 3.11, 3.12, 3.13, and 3.14. +- Pre-commit hooks expanded: `trailing-whitespace`, `end-of-file-fixer`, `check-toml`, `check-yaml`, `ruff`, `ruff-format`. + +### Changed in 1.0.1 + +- `pyproject.toml` migrated from `[tool.poetry]` to PEP 621 `[project]` form; `version = "0.0.0"` is a build-time placeholder overwritten by `poetry-dynamic-versioning`. +- Python support metadata is now explicitly bounded to 3.11–3.14. +- QA workflow now runs a Python test matrix across all supported runtimes. +- Local developer workflow now documents pyenv as the standard multi-version setup. + +### Fixed in 1.0.1 + +- CI workflows: `pipx install poetry` now runs before `actions/setup-python` so the `cache: poetry` lookup always succeeds. +- CI workflows: bumped `actions/checkout@v4` → `v5` and `actions/setup-python@v5` → `v6` to resolve Node.js 20 deprecation warnings. + +### Quality in 1.0.1 + +- Coverage gate raised back to 100%. +- `make test` now exercises every supported Python version when interpreters are installed. + ## 1.0.0 — 2026-04-01 Initial public baseline for the VS Code-native vstack system. diff --git a/LICENSE b/LICENSE index 6c80fdb..012479c 100644 --- a/LICENSE +++ b/LICENSE @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/Makefile b/Makefile index 307bfe4..4fd50a2 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,14 @@ VENV ?= .venv VENV_PYTHON ?= $(VENV)/bin/python TARGET ?= . PIP ?= $(VENV_PYTHON) -m pip +TOX_PYTHON ?= $(if $(wildcard $(VENV_PYTHON)),$(VENV_PYTHON),$(if $(shell command -v $(POETRY) 2>/dev/null),$(POETRY) run python,$(PYTHON))) PYTHON_CHECK_PATHS ?= src tests -DEV_PACKAGES := pip setuptools wheel pytest pytest-cov ruff mypy pre-commit +DEV_PACKAGES := pip setuptools wheel pytest pytest-cov ruff mypy pre-commit tox .PHONY: help help-all preflight bootstrap venv install build clean clean-deep format format-check lint lint-fix \ deps pre-commit-install setup all markdown-format markdown-format-check markdown-lint \ - typecheck test check ci vstack-validate vstack-install vstack-install-global vstack-verify + typecheck test test-local check ci tox tox-all vstack-validate vstack-install vstack-install-global vstack-verify preflight: @command -v $(POETRY) >/dev/null 2>&1 || { echo "Poetry not found. Install from https://python-poetry.org/docs/"; exit 1; } @@ -29,9 +30,11 @@ help: @echo " make setup - create .venv, install dev tools, install pre-commit hook" @echo " make format - format Python and Markdown" @echo " make lint - lint Python" - @echo " make test - run pytest" + @echo " make test - run tests on Python 3.11-3.14 via tox" + @echo " make test-local - run pytest on current interpreter only" @echo " make check - full local quality gate" @echo " make ci - check + vstack template validation" + @echo " make tox - run pytest across Python 3.11-3.14 (if interpreters are available)" @echo " make clean - remove build/cache artifacts" @echo " make vstack-install - install generated artifacts into target" @echo "" @@ -56,9 +59,12 @@ help-all: @echo " make markdown-format-check - run markdown formatting check hook (may rewrite files)" @echo " make markdown-lint - lint Markdown via pre-commit markdownlint hook" @echo " make typecheck - run mypy on src/ and tests/" - @echo " make test - run pytest" + @echo " make test - run pytest matrix via tox (py311, py312, py313, py314)" + @echo " make test-local - run pytest on current interpreter only" @echo " make check - run format-check + markdown checks + lint + typecheck + test" @echo " make ci - run check + vstack-validate" + @echo " make tox - run tox pytest matrix (py311, py312, py313, py314)" + @echo " make tox-all - run all tox envs (tests + lint + type)" @echo " make vstack-validate - run vstack validate" @echo " make vstack-install - run vstack install --target (default .)" @echo " make vstack-install-global - run vstack install --global" @@ -116,13 +122,22 @@ markdown-lint: typecheck: $(VENV_PYTHON) -m mypy $(PYTHON_CHECK_PATHS) -test: +test-local: $(VENV_PYTHON) -m pytest -q +test: + $(TOX_PYTHON) -m tox -e py311,py312,py313,py314 + check: format-check markdown-lint lint typecheck test ci: check vstack-validate +tox: + $(TOX_PYTHON) -m tox -e py311,py312,py313,py314 + +tox-all: + $(TOX_PYTHON) -m tox + vstack-validate: $(VENV_PYTHON) -m vstack validate diff --git a/README.md b/README.md index 8c42c59..bb66dd5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ______________________________________________________________________ ## install -Requires **Python 3.11+**. +Requires **Python 3.11-3.14**. ```bash pip install vstack @@ -128,7 +128,7 @@ ______________________________________________________________________ ## development -Requires **Poetry** and **Python 3.11+**. +Requires **Poetry** and **Python 3.11-3.14**. ```bash git clone https://github.com/your-org/vstack @@ -153,7 +153,33 @@ make ci poetry run vstack validate # validate templates without writing files poetry run vstack install # install all artifacts -> .github/ poetry run vstack verify # full validation suite -poetry run pytest # run test suite +make test-local # run pytest on current interpreter +make test # run tests on py311-py314 via tox +make tox # run tests on py311-py314 (when installed) +``` + +### local multi-python testing (pyenv + tox) + +This repository keeps its local interpreter order in `.python-version` so `tox` +can resolve the supported runtimes consistently. + +Install and activate the supported Python runtimes with `pyenv`: + +```bash +pyenv install 3.11.14 +pyenv install 3.12.12 +pyenv install 3.13.12 +pyenv install 3.14.3 +pyenv local 3.14.3 3.13.12 3.12.12 3.11.14 +``` + +That command writes the repo-local `.python-version` file. + +Run cross-version tests locally: + +```bash +make tox # pytest on py311, py312, py313, py314 +make tox-all # all tox envs: tests + lint + type ``` **Editing skills:** source of truth is `src/vstack/_templates/skills//{config.yaml,template.md}`. Never edit generated files. diff --git a/docs/design/agents.md b/docs/design/agents.md index 7da0288..a9be8f5 100644 --- a/docs/design/agents.md +++ b/docs/design/agents.md @@ -217,7 +217,7 @@ ______________________________________________________________________ ```yaml name: architect -version: 1.0.0 +version: 1.0.1 description: "Senior software architect. Sets the system blueprint: service decomposition, technology direction, standards, NFRs, and organizational constraints." argument-hint: "[design architecture | write ADR | review architecture | check implementation alignment]" tools: diff --git a/docs/design/design.md b/docs/design/design.md index 0598e9b..d72fd2e 100644 --- a/docs/design/design.md +++ b/docs/design/design.md @@ -64,7 +64,7 @@ Each skill is a directory containing two files: ```yaml name: architecture -version: 1.0.0 +version: 1.0.1 description: | Engineering-lead plan review. Lock in the execution plan — service boundaries, data models, API contracts, error handling, test strategy, ... diff --git a/docs/design/skills.md b/docs/design/skills.md index faf5e10..9015891 100644 --- a/docs/design/skills.md +++ b/docs/design/skills.md @@ -97,7 +97,7 @@ The generator reads `config.yaml` and emits recognised fields as frontmatter in ```yaml name: architecture -version: 1.0.0 +version: 1.0.1 description: | Engineering-lead plan review. Lock in the execution plan — service boundaries, data models, API contracts, error handling, test strategy, ... diff --git a/poetry.lock b/poetry.lock index ff8c0df..214bd8c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,29 @@ # This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +[[package]] +name = "cachetools" +version = "7.0.5" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114"}, + {file = "cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990"}, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, + {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -7,7 +31,6 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] -markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -132,6 +155,45 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "filelock" +version = "3.28.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db"}, + {file = "filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6"}, +] + +[[package]] +name = "identify" +version = "2.6.18" +description = "File identification library for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737"}, + {file = "identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "iniconfig" version = "2.3.0" @@ -318,6 +380,18 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, +] + [[package]] name = "packaging" version = "26.0" @@ -348,6 +422,18 @@ optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] +[[package]] +name = "platformdirs" +version = "4.9.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, + {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -364,6 +450,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "4.5.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77"}, + {file = "pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pygments" version = "2.19.2" @@ -379,6 +484,25 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyproject-api" +version = "1.10.0" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}, + {file = "pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}, +] + +[package.dependencies] +packaging = ">=25" + +[package.extras] +docs = ["furo (>=2025.9.25)", "sphinx-autodoc-typehints (>=3.5.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)", "setuptools (>=80.9)"] + [[package]] name = "pytest" version = "9.0.2" @@ -421,6 +545,109 @@ pytest = ">=7" [package.extras] testing = ["process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "python-discovery" +version = "1.2.2" +description = "Python interpreter discovery" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a"}, + {file = "python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + [[package]] name = "ruff" version = "0.15.8" @@ -449,6 +676,45 @@ files = [ {file = "ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e"}, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +description = "A lil' TOML writer" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] + +[[package]] +name = "tox" +version = "4.53.0" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "tox-4.53.0-py3-none-any.whl", hash = "sha256:cc4e716d18c4889aa179d785175c438fa60c35deef20ce689ec288d8fb656096"}, + {file = "tox-4.53.0.tar.gz", hash = "sha256:62c780e42f87d34ee60f2ea20342156253794fdcbd6885fd797d98ee05009f22"}, +] + +[package.dependencies] +cachetools = ">=7.0.3" +colorama = ">=0.4.6" +filelock = ">=3.25" +packaging = ">=26" +platformdirs = ">=4.9.4" +pluggy = ">=1.6" +pyproject-api = ">=1.10" +python-discovery = ">=1.2.2" +tomli-w = ">=1.2" +virtualenv = ">=21.1" + +[package.extras] +completion = ["argcomplete (>=3.6.3)"] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -461,7 +727,25 @@ files = [ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +[[package]] +name = "virtualenv" +version = "21.2.4" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac"}, + {file = "virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} +platformdirs = ">=3.9.1,<5" +python-discovery = ">=1.2.2" + [metadata] lock-version = "2.1" -python-versions = ">=3.11" -content-hash = "efe273ac3069ba9febe8e00cac9a4b486e1e37e33ff714eaf281b37a69b5121b" +python-versions = ">=3.11,<3.15" +content-hash = "2d7100af3a60d80e213f99733bf17ad81af3081850b6e9e143f1218ea9e2b474" diff --git a/pyproject.toml b/pyproject.toml index 248e852..fab6c67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,33 @@ -[tool.poetry] +[project] name = "vstack" -dynamic = ["version"] +version = "0.0.0" description = "VS Code-native AI engineering workflow system for microservices, libraries, and backend systems." -authors = [] license = "MIT" readme = "README.md" +requires-python = ">=3.11,<3.15" +dependencies = [] + +[project.scripts] +vstack = "vstack:main" + +# --------------------------------------------------------------------------- +# Poetry-specific packaging config (package layout, file inclusion, dev deps). +# [project] above covers PEP 621 metadata; Poetry reads both. +# --------------------------------------------------------------------------- + +[tool.poetry] packages = [{include = "vstack", from = "src"}] include = [ { path = "src/vstack/_templates", format = ["sdist", "wheel"] }, ] -[tool.poetry.scripts] -vstack = "vstack:main" - -[tool.poetry.dependencies] -python = ">=3.11" - [tool.poetry.group.dev.dependencies] pytest = ">=9.0" pytest-cov = ">=6.0" ruff = ">=0.9" mypy = ">=1.0" pre-commit = ">=3.8" +tox = ">=4.15" # --------------------------------------------------------------------------- @@ -38,7 +44,7 @@ addopts = [ "--import-mode=importlib", "--cov=src/vstack", "--cov-report=term-missing", - "--cov-fail-under=95", + "--cov-fail-under=100", ] # --------------------------------------------------------------------------- @@ -92,9 +98,44 @@ fallback-version = "0.0.0" # --------------------------------------------------------------------------- # pyright is used for type checking in VS Code, by Pylance. # The "extraPaths" setting is used to find the "scripts" directory, -# which contains the main code for vstack, but is not a project dependency. +# which contains the main code for vstack, but is not a project dependency. # --------------------------------------------------------------------------- [tool.pyright] pythonVersion = "3.11" extraPaths = ["scripts"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +min_version = 4.15 +env_list = py311, py312, py313, py314, lint, type +skip_missing_interpreters = true + +[testenv] +description = Run pytest for {env_name} +package = skip +deps = + pytest>=9.0 + pytest-cov>=6.0 +setenv = + PYTHONPATH = {tox_root}/src +commands = + python -m pytest -q + +[testenv:lint] +description = Run ruff lint checks +package = skip +deps = + ruff>=0.9 +commands = + ruff check src tests + +[testenv:type] +description = Run mypy type checks +package = skip +deps = + mypy>=1.0 +commands = + mypy src tests +""" diff --git a/src/vstack/_templates/agents/architect/config.yaml b/src/vstack/_templates/agents/architect/config.yaml index 7e22fc7..7e76404 100644 --- a/src/vstack/_templates/agents/architect/config.yaml +++ b/src/vstack/_templates/agents/architect/config.yaml @@ -1,5 +1,5 @@ name: architect -version: 1.0.0 +version: 1.0.1 description: > Senior software architect. Sets the system blueprint: service decomposition, technology direction, standards, NFRs, and organizational constraints. diff --git a/src/vstack/_templates/agents/designer/config.yaml b/src/vstack/_templates/agents/designer/config.yaml index 1310972..d2417e3 100644 --- a/src/vstack/_templates/agents/designer/config.yaml +++ b/src/vstack/_templates/agents/designer/config.yaml @@ -1,5 +1,5 @@ name: designer -version: 1.0.0 +version: 1.0.1 description: > Senior interaction designer. Translates architecture blueprint into developer-ready specifications: API contracts, event schemas, data flows, diff --git a/src/vstack/_templates/agents/engineer/config.yaml b/src/vstack/_templates/agents/engineer/config.yaml index 1db8535..c546092 100644 --- a/src/vstack/_templates/agents/engineer/config.yaml +++ b/src/vstack/_templates/agents/engineer/config.yaml @@ -1,5 +1,5 @@ name: engineer -version: 1.0.0 +version: 1.0.1 description: > Senior software engineer. Implements features, bug fixes, and unit tests based on docs/design/design.md, docs/architecture/architecture.md, and ADRs. diff --git a/src/vstack/_templates/agents/product/config.yaml b/src/vstack/_templates/agents/product/config.yaml index 1939515..60fe874 100644 --- a/src/vstack/_templates/agents/product/config.yaml +++ b/src/vstack/_templates/agents/product/config.yaml @@ -1,5 +1,5 @@ name: product -version: 1.0.0 +version: 1.0.1 description: > Senior product manager. Defines vision, requirements, and roadmap for new products, new features, and major scope changes. Baseline-first on branch: diff --git a/src/vstack/_templates/agents/release/config.yaml b/src/vstack/_templates/agents/release/config.yaml index b42f925..149b826 100644 --- a/src/vstack/_templates/agents/release/config.yaml +++ b/src/vstack/_templates/agents/release/config.yaml @@ -1,5 +1,5 @@ name: release -version: 1.0.0 +version: 1.0.1 description: > Senior platform and release engineer. Acts as release gatekeeper: verifies baseline artifacts are complete (docs/product, docs/architecture, diff --git a/src/vstack/_templates/agents/tester/config.yaml b/src/vstack/_templates/agents/tester/config.yaml index d0c943c..a614758 100644 --- a/src/vstack/_templates/agents/tester/config.yaml +++ b/src/vstack/_templates/agents/tester/config.yaml @@ -1,5 +1,5 @@ name: tester -version: 1.0.0 +version: 1.0.1 description: > Senior QA, security, and reliability engineer. Runs functional, security, and performance tests. Produces docs/test-report.md, docs/security-report.md, diff --git a/src/vstack/_templates/skills/adr/config.yaml b/src/vstack/_templates/skills/adr/config.yaml index 6c93a73..54644dd 100644 --- a/src/vstack/_templates/skills/adr/config.yaml +++ b/src/vstack/_templates/skills/adr/config.yaml @@ -1,5 +1,5 @@ name: adr -version: 1.0.0 +version: 1.0.1 description: | Architecture Decision Record writing. Documents a significant architectural decision with context, alternatives considered, rationale, and impact. diff --git a/src/vstack/_templates/skills/analyse/config.yaml b/src/vstack/_templates/skills/analyse/config.yaml index 1c41f42..b1bedca 100644 --- a/src/vstack/_templates/skills/analyse/config.yaml +++ b/src/vstack/_templates/skills/analyse/config.yaml @@ -1,5 +1,5 @@ name: analyse -version: 1.0.0 +version: 1.0.1 description: | Cross-cutting technical analysis. Investigates impact, tradeoffs, root causes, or feasibility without implementing changes. Use when asked to "analyse this", diff --git a/src/vstack/_templates/skills/architecture/config.yaml b/src/vstack/_templates/skills/architecture/config.yaml index caa21a5..233d61b 100644 --- a/src/vstack/_templates/skills/architecture/config.yaml +++ b/src/vstack/_templates/skills/architecture/config.yaml @@ -1,5 +1,5 @@ name: architecture -version: 1.0.0 +version: 1.0.1 description: | Engineering-lead plan review. Lock in the execution plan — service boundaries, data models, API contracts, error handling, test strategy, diff --git a/src/vstack/_templates/skills/cicd/config.yaml b/src/vstack/_templates/skills/cicd/config.yaml index 9a55e93..bbab73d 100644 --- a/src/vstack/_templates/skills/cicd/config.yaml +++ b/src/vstack/_templates/skills/cicd/config.yaml @@ -1,5 +1,5 @@ name: cicd -version: 1.0.0 +version: 1.0.1 description: | Write GitHub Actions CI/CD workflow configuration. Covers build, test, lint, security scan, container publish, and deployment trigger workflows. diff --git a/src/vstack/_templates/skills/code-review/config.yaml b/src/vstack/_templates/skills/code-review/config.yaml index 7456933..a8f2c8c 100644 --- a/src/vstack/_templates/skills/code-review/config.yaml +++ b/src/vstack/_templates/skills/code-review/config.yaml @@ -1,5 +1,5 @@ name: code-review -version: 1.0.0 +version: 1.0.1 description: | Pre-landing code review. Finds bugs that pass CI but break in production — race conditions, missing error handling, API contract violations, observability diff --git a/src/vstack/_templates/skills/consult/config.yaml b/src/vstack/_templates/skills/consult/config.yaml index f23e563..5b78fe8 100644 --- a/src/vstack/_templates/skills/consult/config.yaml +++ b/src/vstack/_templates/skills/consult/config.yaml @@ -1,5 +1,5 @@ name: consult -version: 1.0.0 +version: 1.0.1 description: | DX triage and focused review. First classifies whether the request is API DX, CLI/tool DX, or developer workflow DX, then runs exactly one review path with diff --git a/src/vstack/_templates/skills/container/config.yaml b/src/vstack/_templates/skills/container/config.yaml index fb1d7c0..2def729 100644 --- a/src/vstack/_templates/skills/container/config.yaml +++ b/src/vstack/_templates/skills/container/config.yaml @@ -1,5 +1,5 @@ name: container -version: 1.0.0 +version: 1.0.1 description: | Write and review Dockerfile, docker-compose, and container configuration. Covers multi-stage builds, image hardening, non-root users, minimal base images, diff --git a/src/vstack/_templates/skills/debug/config.yaml b/src/vstack/_templates/skills/debug/config.yaml index 4d115eb..d95b682 100644 --- a/src/vstack/_templates/skills/debug/config.yaml +++ b/src/vstack/_templates/skills/debug/config.yaml @@ -1,5 +1,5 @@ name: debug -version: 1.0.0 +version: 1.0.1 description: | Systematic root-cause debugging for backend services, APIs, and libraries. No fixes without investigation. Follows the scientific method: observe → diff --git a/src/vstack/_templates/skills/design/config.yaml b/src/vstack/_templates/skills/design/config.yaml index aa03b33..562d863 100644 --- a/src/vstack/_templates/skills/design/config.yaml +++ b/src/vstack/_templates/skills/design/config.yaml @@ -1,5 +1,5 @@ name: design -version: 1.0.0 +version: 1.0.1 description: | Build a complete API design or service design from scratch. Produces OpenAPI specs, error conventions, naming standards, pagination patterns, and versioning diff --git a/src/vstack/_templates/skills/docs/config.yaml b/src/vstack/_templates/skills/docs/config.yaml index 7782e0f..1ec26d9 100644 --- a/src/vstack/_templates/skills/docs/config.yaml +++ b/src/vstack/_templates/skills/docs/config.yaml @@ -1,5 +1,5 @@ name: docs -version: 1.0.0 +version: 1.0.1 description: | Post-release documentation alignment. Updates README, API docs, migration guides, and related docs to match shipped behavior. Does not own release-note diff --git a/src/vstack/_templates/skills/explore/config.yaml b/src/vstack/_templates/skills/explore/config.yaml index 2f1eec2..09abef1 100644 --- a/src/vstack/_templates/skills/explore/config.yaml +++ b/src/vstack/_templates/skills/explore/config.yaml @@ -1,5 +1,5 @@ name: explore -version: 1.0.0 +version: 1.0.1 description: | Repository and system discovery. Maps the architecture, understands the codebase, identifies technical debt, and produces a structured onboarding diff --git a/src/vstack/_templates/skills/guardrails/config.yaml b/src/vstack/_templates/skills/guardrails/config.yaml index 93486b6..1734bc7 100644 --- a/src/vstack/_templates/skills/guardrails/config.yaml +++ b/src/vstack/_templates/skills/guardrails/config.yaml @@ -1,5 +1,5 @@ name: guardrails -version: 1.0.0 +version: 1.0.1 description: | Activate safety guardrails for the current session. Before any destructive command (rm -rf, DROP TABLE, git push --force, git reset --hard, kubectl delete, diff --git a/src/vstack/_templates/skills/inspect/config.yaml b/src/vstack/_templates/skills/inspect/config.yaml index 1a9f3fb..f0ed115 100644 --- a/src/vstack/_templates/skills/inspect/config.yaml +++ b/src/vstack/_templates/skills/inspect/config.yaml @@ -1,5 +1,5 @@ name: inspect -version: 1.0.0 +version: 1.0.1 description: | Read-only verification audit. Runs baseline plus optional extended checks, produces severity-ranked findings, and makes no code or commit changes. diff --git a/src/vstack/_templates/skills/performance/config.yaml b/src/vstack/_templates/skills/performance/config.yaml index fad8afb..62453c2 100644 --- a/src/vstack/_templates/skills/performance/config.yaml +++ b/src/vstack/_templates/skills/performance/config.yaml @@ -1,5 +1,5 @@ name: performance -version: 1.0.0 +version: 1.0.1 description: | Performance profiling and regression detection. Establishes baselines, detects regressions, profiles bottlenecks, and recommends optimizations. Use when asked diff --git a/src/vstack/_templates/skills/pr/config.yaml b/src/vstack/_templates/skills/pr/config.yaml index b6f06f5..9a43d1e 100644 --- a/src/vstack/_templates/skills/pr/config.yaml +++ b/src/vstack/_templates/skills/pr/config.yaml @@ -1,5 +1,5 @@ name: pr -version: 1.0.0 +version: 1.0.1 description: | Commit, push, and open a pull request from the current branch to main. Uses the release notes from docs/releases/{date}.md as the PR body. diff --git a/src/vstack/_templates/skills/release-notes/config.yaml b/src/vstack/_templates/skills/release-notes/config.yaml index 702af79..92d3aa5 100644 --- a/src/vstack/_templates/skills/release-notes/config.yaml +++ b/src/vstack/_templates/skills/release-notes/config.yaml @@ -1,5 +1,5 @@ name: release-notes -version: 1.0.0 +version: 1.0.1 description: | Prepare release artifacts: verify all docs are present, write release notes, own CHANGELOG.md updates, and produce docs/releases/{date}.md. diff --git a/src/vstack/_templates/skills/requirements/config.yaml b/src/vstack/_templates/skills/requirements/config.yaml index 259a79f..7b461bf 100644 --- a/src/vstack/_templates/skills/requirements/config.yaml +++ b/src/vstack/_templates/skills/requirements/config.yaml @@ -1,5 +1,5 @@ name: requirements -version: 1.0.0 +version: 1.0.1 description: | Collaborative requirements gathering and documentation. Clarifies what must be built, defines success criteria, constraints, and non-functional requirements. diff --git a/src/vstack/_templates/skills/security/config.yaml b/src/vstack/_templates/skills/security/config.yaml index 75a0e69..6c5efe4 100644 --- a/src/vstack/_templates/skills/security/config.yaml +++ b/src/vstack/_templates/skills/security/config.yaml @@ -1,5 +1,5 @@ name: security -version: 1.0.0 +version: 1.0.1 description: | OWASP Top 10 + STRIDE security audit for APIs, services, and libraries. Finds authentication bypasses, injection vulnerabilities, insecure configurations, diff --git a/src/vstack/_templates/skills/verify/config.yaml b/src/vstack/_templates/skills/verify/config.yaml index a31b510..adf6bb9 100644 --- a/src/vstack/_templates/skills/verify/config.yaml +++ b/src/vstack/_templates/skills/verify/config.yaml @@ -1,5 +1,5 @@ name: verify -version: 1.0.0 +version: 1.0.1 description: | Verification fix-loop skill. Routes by mode (quick/standard/exhaustive), runs targeted checks, fixes findings by severity, and re-verifies impacted paths. diff --git a/src/vstack/_templates/skills/vision/config.yaml b/src/vstack/_templates/skills/vision/config.yaml index af218f5..c58ccca 100644 --- a/src/vstack/_templates/skills/vision/config.yaml +++ b/src/vstack/_templates/skills/vision/config.yaml @@ -1,5 +1,5 @@ name: vision -version: 1.0.0 +version: 1.0.1 description: | CEO/founder-mode plan review. Rethink the problem from first principles, validate ambition and scope, challenge premises, find the 10x solution. Four modes: diff --git a/src/vstack/cli/manifest.py b/src/vstack/cli/manifest.py index 33d2572..88e6039 100644 --- a/src/vstack/cli/manifest.py +++ b/src/vstack/cli/manifest.py @@ -10,7 +10,7 @@ "vstack_version": "…", "installed_at": "…", "artifacts": { - "skills": [{"name": "vision", "version": "1.0.0", "file": "skills/vision/SKILL.md"}], + "skills": [{"name": "vision", "version": "1.0.1", "file": "skills/vision/SKILL.md"}], "agents": [{"name": "engineer", "file": "agents/engineer.agent.md"}] } } diff --git a/src/vstack/constants.py b/src/vstack/constants.py index 0e9c379..6cf96bd 100644 --- a/src/vstack/constants.py +++ b/src/vstack/constants.py @@ -2,6 +2,8 @@ from __future__ import annotations +import re +import subprocess from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _pkg_version from importlib.resources import files @@ -12,12 +14,71 @@ TEMPLATES_ROOT = Path(str(_PACKAGE_ROOT / "_templates")) -try: - # Reads from installed package metadata, populated by poetry-dynamic-versioning - # at build time from the nearest reachable git tag (e.g. "1.2.3"). - VERSION = _pkg_version("vstack") -except PackageNotFoundError: - # Fallback: uninstalled source tree or shallow clone without any git tag. - VERSION = "0.0.0" +_SEMVER_TAG_RE = re.compile(r"^\d+\.\d+\.\d+$") + + +def _version_tuple(tag: str) -> tuple[int, int, int]: + major, minor, patch = tag.split(".") + return int(major), int(minor), int(patch) + + +def _vstack_repo_root() -> Path | None: + """Return the root of the vstack source checkout, or None if not in one. + + Walks up from the package directory looking for a .git directory. + Returns None when invoked outside the vstack source tree, preventing + accidental version reads from the user's working directory. + """ + candidate = Path(str(_PACKAGE_ROOT)).resolve() + for parent in [candidate, *candidate.parents]: + if (parent / ".git").exists(): + # Confirm this is the vstack repo, not an arbitrary repo that + # happens to contain the installed package somewhere inside it. + if (parent / "src" / "vstack").exists(): + return parent + break + return None + + +def _head_semver_tag() -> str | None: + """Return the highest plain semver tag pointing at HEAD, if available. + + Only runs git inside the vstack source checkout to prevent picking up + tags from whatever repository the user is working in. + """ + repo_root = _vstack_repo_root() + if repo_root is None: + return None + try: + out = subprocess.check_output( + ["git", "-C", str(repo_root), "tag", "--points-at", "HEAD"], + stderr=subprocess.DEVNULL, + text=True, + ) + except (FileNotFoundError, subprocess.CalledProcessError): + return None + + tags = [line.strip() for line in out.splitlines() if _SEMVER_TAG_RE.fullmatch(line.strip())] + if not tags: + return None + return max(tags, key=_version_tuple) + + +def _resolve_version() -> str: + """Resolve the package version from git tags, package metadata, or fallback.""" + version = _head_semver_tag() or "" + if version: + return version + + try: + # Reads from installed package metadata, populated by build-time versioning. + return _pkg_version("vstack") + except PackageNotFoundError: + # Fallback: uninstalled source tree or shallow clone without any git tag. + return "0.0.0" + + +# Prefer an exact semver tag on HEAD for source checkouts. +VERSION = _resolve_version() MANIFEST_FILENAME = "vstack.json" diff --git a/tests/vstack/test_constants.py b/tests/vstack/test_constants.py index 348378f..856c814 100644 --- a/tests/vstack/test_constants.py +++ b/tests/vstack/test_constants.py @@ -2,8 +2,8 @@ from __future__ import annotations -import importlib import importlib.metadata as importlib_metadata +import subprocess from pathlib import Path import vstack.constants as constants_module @@ -26,18 +26,84 @@ def test_manifest_filename(self) -> None: """Test that manifest filename.""" assert MANIFEST_FILENAME == "vstack.json" + def test_head_semver_tag_returns_none_when_head_has_no_semver_tags(self, monkeypatch) -> None: + """Test that non-semver tags on HEAD are ignored.""" + monkeypatch.setattr(constants_module, "_vstack_repo_root", lambda: Path("/fake/root")) + monkeypatch.setattr(subprocess, "check_output", lambda *_args, **_kwargs: "main\nv1.0.0\n") + + assert constants_module._head_semver_tag() is None + + def test_head_semver_tag_returns_none_when_git_lookup_fails(self, monkeypatch) -> None: + """Test that git lookup failures fall back cleanly.""" + monkeypatch.setattr(constants_module, "_vstack_repo_root", lambda: Path("/fake/root")) + + def _raise_called_process_error(*_args, **_kwargs) -> str: + """Internal helper to simulate git lookup failure.""" + raise subprocess.CalledProcessError(returncode=1, cmd=["git", "tag"]) + + monkeypatch.setattr(subprocess, "check_output", _raise_called_process_error) + + assert constants_module._head_semver_tag() is None + + def test_head_semver_tag_returns_none_outside_vstack_repo(self, monkeypatch) -> None: + """Test that git is not consulted when not inside the vstack source tree.""" + monkeypatch.setattr(constants_module, "_vstack_repo_root", lambda: None) + called = [] + + def _record_call(*_args, **_kwargs) -> str: + called.append(1) + return "" + + monkeypatch.setattr(subprocess, "check_output", _record_call) + + assert constants_module._head_semver_tag() is None + assert not called, "git must not be invoked outside the vstack repo" + + def test_vstack_repo_root_returns_none_for_unrelated_git_repo( + self, monkeypatch, tmp_path + ) -> None: + """Test that a .git dir without src/vstack does not match as vstack repo.""" + fake_git = tmp_path / ".git" + fake_git.mkdir() + monkeypatch.setattr(constants_module, "_PACKAGE_ROOT", tmp_path) + + assert constants_module._vstack_repo_root() is None + + def test_head_semver_tag_returns_highest_plain_semver(self, monkeypatch) -> None: + """Test that the highest semver tag on HEAD is selected.""" + monkeypatch.setattr(constants_module, "_vstack_repo_root", lambda: Path("/fake/root")) + monkeypatch.setattr( + subprocess, + "check_output", + lambda *_args, **_kwargs: "1.0.0\n1.0.2\n1.0.1\nrelease-candidate\n", + ) + + assert constants_module._head_semver_tag() == "1.0.2" + + def test_version_uses_installed_package_metadata_when_git_tag_missing( + self, monkeypatch + ) -> None: + """Test that installed metadata is used when no semver tag points at HEAD.""" + monkeypatch.setattr(constants_module, "_head_semver_tag", lambda: None) + monkeypatch.setattr(constants_module, "_pkg_version", lambda _name: "1.0.1") + + assert constants_module._resolve_version() == "1.0.1" + + def test_version_prefers_head_semver_tag(self, monkeypatch) -> None: + """Test that a semver tag on HEAD wins over package metadata.""" + monkeypatch.setattr(constants_module, "_head_semver_tag", lambda: "1.0.1") + monkeypatch.setattr(constants_module, "_pkg_version", lambda _name: "9.9.9") + + assert constants_module._resolve_version() == "1.0.1" + def test_version_fallback_when_package_metadata_missing(self, monkeypatch) -> None: - """Test that version fallback when package metadata missing.""" - original_version = importlib_metadata.version + """Test that version fallback when git and package metadata are unavailable.""" def _raise_not_found(_: str) -> str: """Internal helper to raise not found.""" raise importlib_metadata.PackageNotFoundError - monkeypatch.setattr(importlib_metadata, "version", _raise_not_found) - fallback_loaded = importlib.reload(constants_module) - assert fallback_loaded.VERSION == "0.0.0" + monkeypatch.setattr(constants_module, "_head_semver_tag", lambda: None) + monkeypatch.setattr(constants_module, "_pkg_version", _raise_not_found) - monkeypatch.setattr(importlib_metadata, "version", original_version) - restored = importlib.reload(constants_module) - assert restored.VERSION + assert constants_module._resolve_version() == "0.0.0" diff --git a/tests/vstack/test_main.py b/tests/vstack/test_main.py index a30d49b..ae7a3fa 100644 --- a/tests/vstack/test_main.py +++ b/tests/vstack/test_main.py @@ -65,6 +65,12 @@ def uninstall(self, *args, **kwargs): class TestMain: """Test cases for Main.""" + def test_resolve_only_for_scope_returns_requested_only_for_non_global(self) -> None: + """Test that non-global commands keep the explicit type filter.""" + args = _Args("install", only=["skill"], use_global=False) + + assert main_module._resolve_only_for_scope(args) == ["skill"] + def test_main_dispatch_validate(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test that main dispatch validate.""" cli = _CLI() @@ -148,6 +154,12 @@ def test_main_global_install_defaults_to_supported_types( }, ) + def test_resolve_only_for_scope_returns_requested_only_for_global_allowed_types(self) -> None: + """Test that allowed global type filters are passed through unchanged.""" + args = _Args("install", only=["agent", "prompt"], use_global=True) + + assert main_module._resolve_only_for_scope(args) == ["agent", "prompt"] + def test_main_global_install_rejects_unknown_type( self, monkeypatch: pytest.MonkeyPatch, tmp_path ) -> None: @@ -198,3 +210,22 @@ def test_main_dispatch_verify_includes_only_filter( "only": ["agent", "instruction", "prompt", "skill"], }, ) + + def test_main_dispatch_uninstall(self, monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + """Test that uninstall resolves the target directory and dispatches correctly.""" + cli = _CLI() + args = _Args("uninstall") + parser = type("P", (), {"parse_args": lambda self: args})() + + monkeypatch.setattr(main_module, "build_parser", lambda: parser) + monkeypatch.setattr(main_module, "resolve_targets", lambda _args: tmp_path) + monkeypatch.setattr(main_module, "CommandLineInterface", lambda templates_root: cli) + monkeypatch.setattr( + main_module.sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code)) + ) + + with pytest.raises(SystemExit) as exc: + main_module.main() + + assert exc.value.code == 10 + assert cli.called == ("uninstall", (tmp_path,), {})