diff --git a/.claude/settings.json b/.claude/settings.json index b39e7103..513546b0 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -21,7 +21,7 @@ "vergil-marketplace": { "source": { "source": "github", - "repo": "vergil-project/vergil-plugin" + "repo": "vergil-project/vergil-claude-plugin" } } }, diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d6573948..0146e945 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -31,6 +31,4 @@ jobs: language: python container-tag: "3.14" registry-publish: true - secrets: - APP_CLIENT_ID: ${{ secrets.APP_CLIENT_ID }} - APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + secrets: inherit diff --git a/.gitignore b/.gitignore index 82c51d4d..89b587b7 100644 --- a/.gitignore +++ b/.gitignore @@ -207,3 +207,6 @@ marimo/_static/ marimo/_lsp/ __marimo__/ .worktrees/ + +# Vergil tooling scratch (PR/session working dir) +.vergil/ diff --git a/CLAUDE.md b/CLAUDE.md index aefca2a2..5b6c5d1b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,19 +36,19 @@ on-ramp. ### Structure ```text -~/dev/github/mq-rest-admin-python/ ← sessions ALWAYS start here +/ ← sessions ALWAYS start here .git/ - CLAUDE.md, src/, tests/, … ← main worktree (usually `develop`) - .worktrees/ ← container for parallel worktrees - issue-454-adopt-worktree-convention/ ← worktree on feature/454-... + CLAUDE.md, … ← main worktree (usually `develop`) + .worktrees/ ← container for parallel worktrees + issue--/ ← worktree on feature/- … ``` ### Rules 1. **Sessions always start at the project root.** - `cd ~/dev/github/mq-rest-admin-python && claude` — never from inside - `.worktrees//`. This keeps the memory-path slug stable and shared. + Never start Claude from inside `.worktrees//`. This keeps the + memory-path slug stable and shared. 2. **Each parallel agent is assigned exactly one worktree.** The session prompt names the worktree (see Agent prompt contract below). - For Read / Edit / Write tools: use the worktree's absolute path. @@ -56,7 +56,7 @@ on-ramp. or use absolute paths. 3. **The main worktree is read-only.** All edits flow through a worktree on a feature branch — the logical endpoint of the standing - "no direct commits to `develop`" policy. + "no direct commits to develop" policy. 4. **One worktree per issue.** Don't stack in-flight issues. When a branch lands, remove the worktree before starting the next. 5. **Naming: `issue--`.** `` is the GitHub issue @@ -70,22 +70,44 @@ placeholders): ```text You are working on issue #: . -Your worktree is: /Users/pmoore/dev/github/mq-rest-admin-python/.worktrees/issue--/ +Your worktree is: /.worktrees/issue--/ Your branch is: feature/- Rules for this session: - Do all git operations from inside your worktree: - cd && git + cd && vrg-git - For Read / Edit / Write tools, use the absolute worktree path. - For Bash commands that touch files, cd into the worktree first or use absolute paths. - Do not edit files at the project root. The main worktree is read-only — all changes flow through your worktree on your feature branch. +- When you need to run validation, run it from inside your worktree + (vrg-container-run mounts the current directory). ``` All fields are required. +## Shell command policy + +Use `vrg-git` instead of `git` for all git operations. Use `vrg-gh` +instead of `gh` for all GitHub CLI operations. These wrappers enforce +subcommand allowlists, flag deny lists, and credential selection. + +Raw `git` and `gh` are denied by the permission model. If a command +is not available through the wrappers, explain the situation to the +human who can run it directly via `! ` in the prompt. + +## Validation + +```bash +vrg-container-run -- vrg-validate +``` + +This is the **only** validation command. Do not run individual linters, +formatters, or other tools outside of `vrg-validate`. If a tool is not +invoked by `vrg-validate`, it is not part of the validation pipeline. + ## Project Overview `pymqrest` is a Python wrapper for the IBM MQ administrative REST API. The project provides a Python mapping layer for MQ REST API attribute translations and command metadata experiments. The current focus is on attribute mapping and metadata modeling. @@ -123,7 +145,7 @@ gates. ### Validation ```bash -vrg-docker-run -- vrg-validate # Full validation (runs in dev container) +vrg-container-run -- vrg-validate # Full validation (runs in dev container) ``` - Lock file verification - Security audit (pip-audit) diff --git a/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py b/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py index 9ff64681..31e3a426 100755 --- a/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py +++ b/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py @@ -248,7 +248,7 @@ def read_group_entries() -> list[GroupEntry]: def fetch_html(href: str) -> str: url = f"{IBM_DOCS_BASE}{href}" - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = Request( # noqa: S310 url, headers={"User-Agent": "pymqrest-pcf-map/1.0", "Accept": "text/html"}, diff --git a/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py b/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py index a2a01630..93878c23 100755 --- a/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py +++ b/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py @@ -602,7 +602,7 @@ def extract_command_name(html: str) -> str | None: def fetch_html(href: str) -> str: url = f"{IBM_DOCS_BASE}{href}" - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = urllib.request.Request( # noqa: S310 url, headers={ diff --git a/docs/archive/extraction/scripts/extract_pcf_command_pages.py b/docs/archive/extraction/scripts/extract_pcf_command_pages.py index 77238f4f..8b283432 100755 --- a/docs/archive/extraction/scripts/extract_pcf_command_pages.py +++ b/docs/archive/extraction/scripts/extract_pcf_command_pages.py @@ -63,7 +63,7 @@ def handle_data(self, data: str) -> None: def fetch_index(url: str) -> str: - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = Request( # noqa: S310 url, headers={"User-Agent": "pymqrest-pcf-index/1.0", "Accept": "text/html"}, diff --git a/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py b/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py index 665ae542..3bdd4c63 100755 --- a/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py +++ b/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py @@ -162,7 +162,7 @@ def fetch_html(href: str) -> str: if cache_path.exists(): return cache_path.read_text(encoding="utf-8", errors="ignore") url = f"{IBM_DOCS_BASE}{href}" - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = urllib.request.Request( # noqa: S310 url, headers={ diff --git a/examples/channel_status.py b/examples/channel_status.py index eaecb19a..0db70994 100644 --- a/examples/channel_status.py +++ b/examples/channel_status.py @@ -11,7 +11,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -120,7 +120,7 @@ def main(session: MQRESTSession) -> list[ChannelInfo]: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/dlq_inspector.py b/examples/dlq_inspector.py index 59e93f16..d3a78631 100644 --- a/examples/dlq_inspector.py +++ b/examples/dlq_inspector.py @@ -12,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTSession from pymqrest.auth import LTPAAuth @@ -147,7 +147,7 @@ def _to_int(value: object) -> int: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/health_check.py b/examples/health_check.py index 1cd8f2f8..8e04c277 100644 --- a/examples/health_check.py +++ b/examples/health_check.py @@ -14,7 +14,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -122,7 +122,7 @@ def main(sessions: list[MQRESTSession]) -> list[QMHealthResult]: MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) ) @@ -133,7 +133,7 @@ def main(sessions: list[MQRESTSession]) -> list[QMHealthResult]: MQRESTSession( rest_base_url=qm2_url, qmgr_name="QM2", - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) ) diff --git a/examples/provision_environment.py b/examples/provision_environment.py index 9ac06f6d..232f1ea5 100644 --- a/examples/provision_environment.py +++ b/examples/provision_environment.py @@ -15,7 +15,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -289,14 +289,14 @@ def _delete( qm1_session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name="QM1", - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) qm2_session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL_QM2", "https://localhost:9444/ibmmq/rest/v2"), qmgr_name="QM2", - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/queue_depth_monitor.py b/examples/queue_depth_monitor.py index ac04cbae..73ac1eff 100644 --- a/examples/queue_depth_monitor.py +++ b/examples/queue_depth_monitor.py @@ -14,7 +14,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTSession from pymqrest.auth import LTPAAuth @@ -126,7 +126,7 @@ def _to_int(value: object) -> int: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/queue_status.py b/examples/queue_status.py index b542fdf9..e68be881 100644 --- a/examples/queue_status.py +++ b/examples/queue_status.py @@ -12,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -139,7 +139,7 @@ def main(session: MQRESTSession) -> None: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/tests/pymqrest/test_auth.py b/tests/pymqrest/test_auth.py index b11c3fe9..305b4250 100644 --- a/tests/pymqrest/test_auth.py +++ b/tests/pymqrest/test_auth.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +from os import getenv from typing import TYPE_CHECKING import pytest @@ -20,7 +21,7 @@ if TYPE_CHECKING: from collections.abc import Mapping -TEST_PASSWORD = "secret" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") STATUS_OK = 200 STATUS_UNAUTHORIZED = 401 diff --git a/tests/pymqrest/test_ensure.py b/tests/pymqrest/test_ensure.py index 0f0a64cb..725d0d35 100644 --- a/tests/pymqrest/test_ensure.py +++ b/tests/pymqrest/test_ensure.py @@ -4,6 +4,7 @@ import json from dataclasses import dataclass +from os import getenv from typing import TYPE_CHECKING import pytest @@ -15,7 +16,7 @@ if TYPE_CHECKING: from collections.abc import Mapping, Sequence -TEST_PASSWORD = "pass" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") EXPECT_ONE_REQUEST = 1 EXPECT_TWO_REQUESTS = 2 diff --git a/tests/pymqrest/test_session.py b/tests/pymqrest/test_session.py index 23cf959b..5bb6321e 100644 --- a/tests/pymqrest/test_session.py +++ b/tests/pymqrest/test_session.py @@ -4,6 +4,7 @@ import json from dataclasses import dataclass +from os import getenv from typing import TYPE_CHECKING import pytest @@ -28,7 +29,7 @@ REQUEST_EXCEPTION_MESSAGE = "boom" STATUS_INTERNAL_SERVER_ERROR = 500 STATUS_CREATED = 201 -TEST_PASSWORD = "pass" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") TEST_DEPTH = 5 diff --git a/tests/pymqrest/test_sync.py b/tests/pymqrest/test_sync.py index bb5c76c8..4bf2acc4 100644 --- a/tests/pymqrest/test_sync.py +++ b/tests/pymqrest/test_sync.py @@ -5,6 +5,7 @@ import json import time from dataclasses import dataclass +from os import getenv from typing import TYPE_CHECKING import pytest @@ -17,7 +18,7 @@ if TYPE_CHECKING: from collections.abc import Mapping, Sequence -TEST_PASSWORD = "pass" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") EXPECT_ONE_POLL = 1 EXPECT_TWO_POLLS = 2 EXPECT_THREE_POLLS = 3